@0xmaxma/claude-gateway 1.8.7 → 1.8.8
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/README.md +96 -0
- package/dist/agent/runner.d.ts +47 -1
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +116 -17
- package/dist/agent/runner.js.map +1 -1
- package/dist/api/connectors-router.d.ts +50 -0
- package/dist/api/connectors-router.d.ts.map +1 -0
- package/dist/api/connectors-router.js +650 -0
- package/dist/api/connectors-router.js.map +1 -0
- package/dist/api/gateway-router.d.ts +4 -0
- package/dist/api/gateway-router.d.ts.map +1 -1
- package/dist/api/gateway-router.js +26 -0
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/api/oauth-connectors-router.d.ts +43 -0
- package/dist/api/oauth-connectors-router.d.ts.map +1 -0
- package/dist/api/oauth-connectors-router.js +384 -0
- package/dist/api/oauth-connectors-router.js.map +1 -0
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +101 -13
- package/dist/api/router.js.map +1 -1
- package/dist/apps/agent-manager.d.ts +6 -2
- package/dist/apps/agent-manager.d.ts.map +1 -1
- package/dist/apps/agent-manager.js +10 -18
- package/dist/apps/agent-manager.js.map +1 -1
- package/dist/config/config-write-lock.d.ts +45 -0
- package/dist/config/config-write-lock.d.ts.map +1 -0
- package/dist/config/config-write-lock.js +164 -0
- package/dist/config/config-write-lock.js.map +1 -0
- package/dist/config/watcher.d.ts.map +1 -1
- package/dist/config/watcher.js +29 -0
- package/dist/config/watcher.js.map +1 -1
- package/dist/connectors/custom-connectors-store.d.ts +54 -0
- package/dist/connectors/custom-connectors-store.d.ts.map +1 -0
- package/dist/connectors/custom-connectors-store.js +204 -0
- package/dist/connectors/custom-connectors-store.js.map +1 -0
- package/dist/connectors/custom.d.ts +69 -0
- package/dist/connectors/custom.d.ts.map +1 -0
- package/dist/connectors/custom.js +158 -0
- package/dist/connectors/custom.js.map +1 -0
- package/dist/connectors/mcp-oauth.d.ts +152 -0
- package/dist/connectors/mcp-oauth.d.ts.map +1 -0
- package/dist/connectors/mcp-oauth.js +522 -0
- package/dist/connectors/mcp-oauth.js.map +1 -0
- package/dist/connectors/oauth-refresh-sweep.d.ts +71 -0
- package/dist/connectors/oauth-refresh-sweep.d.ts.map +1 -0
- package/dist/connectors/oauth-refresh-sweep.js +337 -0
- package/dist/connectors/oauth-refresh-sweep.js.map +1 -0
- package/dist/connectors/pending-oauth-store.d.ts +39 -0
- package/dist/connectors/pending-oauth-store.d.ts.map +1 -0
- package/dist/connectors/pending-oauth-store.js +50 -0
- package/dist/connectors/pending-oauth-store.js.map +1 -0
- package/dist/connectors/resolve.d.ts +52 -0
- package/dist/connectors/resolve.d.ts.map +1 -0
- package/dist/connectors/resolve.js +176 -0
- package/dist/connectors/resolve.js.map +1 -0
- package/dist/connectors/token-env.d.ts +93 -0
- package/dist/connectors/token-env.d.ts.map +1 -0
- package/dist/connectors/token-env.js +323 -0
- package/dist/connectors/token-env.js.map +1 -0
- package/dist/connectors/types.d.ts +126 -0
- package/dist/connectors/types.d.ts.map +1 -0
- package/dist/connectors/types.js +13 -0
- package/dist/connectors/types.js.map +1 -0
- package/dist/index.js +32 -1
- package/dist/index.js.map +1 -1
- package/dist/session/process.d.ts +23 -0
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +78 -2
- package/dist/session/process.js.map +1 -1
- package/dist/types.d.ts +46 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createCustomConnectorsStore = createCustomConnectorsStore;
|
|
37
|
+
const fsp = __importStar(require("fs/promises"));
|
|
38
|
+
const config_write_lock_1 = require("../config/config-write-lock");
|
|
39
|
+
/**
|
|
40
|
+
* Re-key a parsed connector map onto a null-prototype object. Every map this module
|
|
41
|
+
* hands out — to a caller of `read()`, to a `mutate` callback — goes through here.
|
|
42
|
+
*
|
|
43
|
+
* Connector ids are slugs derived from a user-supplied label, and `isValidConnectorId`
|
|
44
|
+
* accepts `constructor`, `toString`, `valueOf` — all inherited properties of a plain
|
|
45
|
+
* `JSON.parse` result. Every lookup in the routers is a bare `map[id]`, so an id like
|
|
46
|
+
* that resolved to a Function rather than to nothing: `GET /:id/status` answered
|
|
47
|
+
* `500 Connector has an unreadable configuration`, DELETE answered
|
|
48
|
+
* `500 Cannot read properties of undefined (reading 'map')` and `/connect`
|
|
49
|
+
* `... (reading 'length')` — both leaking an internal error message — where the honest
|
|
50
|
+
* answer to all three is 404. A null-prototype map has nothing to inherit, so `map[id]`
|
|
51
|
+
* is undefined exactly when the connector does not exist.
|
|
52
|
+
*
|
|
53
|
+
* `Object.assign` copies own enumerable keys only, which is also what `JSON.stringify`
|
|
54
|
+
* writes back out, so a map that has been through here round-trips unchanged.
|
|
55
|
+
*/
|
|
56
|
+
function ownKeysOnly(map) {
|
|
57
|
+
return Object.assign(Object.create(null), map);
|
|
58
|
+
}
|
|
59
|
+
function createCustomConnectorsStore(configPath) {
|
|
60
|
+
// Per store instance, not per module: one store exists per gateway process, so this
|
|
61
|
+
// is process-wide in production, while tests that build their own store each start
|
|
62
|
+
// from a clean throttle instead of inheriting a previous test's timestamp.
|
|
63
|
+
const READ_FAILURE_LOG_INTERVAL_MS = 60 * 1000;
|
|
64
|
+
let lastReadFailureLog = 0;
|
|
65
|
+
async function mutate(fn) {
|
|
66
|
+
if (!configPath)
|
|
67
|
+
return; // no persistence target (e.g. tests) — secret store is authoritative
|
|
68
|
+
return (0, config_write_lock_1.withConfigWriteLock)(configPath, async () => {
|
|
69
|
+
const raw = await fsp.readFile(configPath, 'utf-8');
|
|
70
|
+
const config = JSON.parse(raw);
|
|
71
|
+
config.gateway = config.gateway ?? {};
|
|
72
|
+
// The callback both reads (`slugify(label, Object.keys(c))`, /oauth/receive's
|
|
73
|
+
// `c[id]` capture of the entry it replaces) and writes, so it gets the same
|
|
74
|
+
// null-prototype treatment `read()` gives — see ownKeysOnly.
|
|
75
|
+
const connectors = ownKeysOnly(config.gateway.customConnectors);
|
|
76
|
+
config.gateway.customConnectors = connectors;
|
|
77
|
+
fn(connectors);
|
|
78
|
+
await (0, config_write_lock_1.writeConfigAtomic)(configPath, config);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async function read() {
|
|
82
|
+
if (!configPath)
|
|
83
|
+
return ownKeysOnly(undefined);
|
|
84
|
+
try {
|
|
85
|
+
const raw = await fsp.readFile(configPath, 'utf-8');
|
|
86
|
+
const config = JSON.parse(raw);
|
|
87
|
+
return ownKeysOnly(config.gateway?.customConnectors);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
noteReadFailure(err);
|
|
91
|
+
return ownKeysOnly(undefined);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Degrading to {} is right — see readTokenEnv()'s doc for the same argument: this
|
|
96
|
+
* feeds `GET /v1/connectors`, and a throw from an async Express 4 handler reaches
|
|
97
|
+
* index.ts's `unhandledRejection` hook and shuts the gateway down.
|
|
98
|
+
*
|
|
99
|
+
* But degrading SILENTLY is not. An EACCES config.json (one `sudo`, a restored
|
|
100
|
+
* volume) or a hand-edit that left invalid JSON both land here, and the caller
|
|
101
|
+
* cannot tell "no connectors are configured" from "the file that lists them is
|
|
102
|
+
* unreadable": the caller gets an empty list and the refresh sweep concludes
|
|
103
|
+
* there is nothing to refresh, with nothing written anywhere to say why. It
|
|
104
|
+
* also means connectors-router.ts's `500 Connector configuration could not be
|
|
105
|
+
* read` — the documented answer for exactly this — is unreachable, because
|
|
106
|
+
* this function never throws.
|
|
107
|
+
*
|
|
108
|
+
* A missing file is not that: it is the ordinary pre-first-write state, and
|
|
109
|
+
* says the same thing as an empty map.
|
|
110
|
+
*/
|
|
111
|
+
function noteReadFailure(err) {
|
|
112
|
+
if (err.code === 'ENOENT')
|
|
113
|
+
return;
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
// Throttled for readTokenEnv's reason — status pollers hit this every couple
|
|
116
|
+
// of seconds, so one line per failure buries the log it belongs in.
|
|
117
|
+
if (now - lastReadFailureLog < READ_FAILURE_LOG_INTERVAL_MS)
|
|
118
|
+
return;
|
|
119
|
+
lastReadFailureLog = now;
|
|
120
|
+
console.error(`custom-connectors-store: cannot read connectors from ${configPath}` +
|
|
121
|
+
` (${err.code ?? 'invalid JSON'}) — reporting no` +
|
|
122
|
+
` connectors until it is readable: ${err.message}`);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The entry AND every agent's enablement flag for it. Returns whether anything went.
|
|
126
|
+
*
|
|
127
|
+
* Both halves, because per-agent enablement is stored separately from the
|
|
128
|
+
* connector entry itself (AgentConfig.connectors vs gateway.customConnectors),
|
|
129
|
+
* so removing only the entry leaves its enablement flags behind as orphans.
|
|
130
|
+
* They are inert while the id is unused — but ids are slugs derived from
|
|
131
|
+
* labels, so re-adding a connector with the same label revives whatever the
|
|
132
|
+
* old one's flags said. An agent that was explicitly disabled for the deleted
|
|
133
|
+
* connector would then start out disabled for the brand-new one, for no reason
|
|
134
|
+
* its owner can see.
|
|
135
|
+
*
|
|
136
|
+
* One write rather than two, because two would reintroduce the very orphan
|
|
137
|
+
* this exists to prevent: a crash — or the emergency shutdown index.ts runs on
|
|
138
|
+
* an unhandled rejection — landing between them leaves the entry gone and the
|
|
139
|
+
* flags behind, and with the entry gone nothing will ever come back for them.
|
|
140
|
+
*
|
|
141
|
+
* Only for a real delete of the entry. A soft disconnect keeps the entry and so
|
|
142
|
+
* must keep its enablement.
|
|
143
|
+
*/
|
|
144
|
+
function dropConnector(config, connectorId) {
|
|
145
|
+
let changed = false;
|
|
146
|
+
// `hasOwnProperty`, not `in`: `'constructor' in {}` is true, so the `in` form
|
|
147
|
+
// deleted nothing (delete on an inherited property is a no-op), reported
|
|
148
|
+
// `changed`, and rewrote config.json for it — see ownKeysOnly for the same
|
|
149
|
+
// hazard on the read side.
|
|
150
|
+
if (config.gateway?.customConnectors &&
|
|
151
|
+
Object.prototype.hasOwnProperty.call(config.gateway.customConnectors, connectorId)) {
|
|
152
|
+
delete config.gateway.customConnectors[connectorId];
|
|
153
|
+
changed = true;
|
|
154
|
+
}
|
|
155
|
+
for (const agent of config.agents ?? []) {
|
|
156
|
+
if (agent.connectors && Object.prototype.hasOwnProperty.call(agent.connectors, connectorId)) {
|
|
157
|
+
delete agent.connectors[connectorId];
|
|
158
|
+
changed = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return changed;
|
|
162
|
+
}
|
|
163
|
+
async function withEntry(connectorId, fn) {
|
|
164
|
+
if (!configPath) {
|
|
165
|
+
// No persistence target (tests): the same "there is nothing there" `read()`
|
|
166
|
+
// reports, and a `remove()` that has nothing to remove.
|
|
167
|
+
return fn({ entry: undefined, remove: () => { } });
|
|
168
|
+
}
|
|
169
|
+
return (0, config_write_lock_1.withConfigWriteLock)(configPath, async () => {
|
|
170
|
+
let config = null;
|
|
171
|
+
try {
|
|
172
|
+
config = JSON.parse(await fsp.readFile(configPath, 'utf-8'));
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
// Same degrade-and-log as read(): the callers reach this through a route
|
|
176
|
+
// that would already have answered 404 off `read()`'s empty map, so an
|
|
177
|
+
// unreadable config keeps meaning "no such connector" rather than
|
|
178
|
+
// rejecting into index.ts's unhandledRejection hook.
|
|
179
|
+
noteReadFailure(err);
|
|
180
|
+
}
|
|
181
|
+
const connectors = ownKeysOnly(config?.gateway?.customConnectors);
|
|
182
|
+
let removeRequested = false;
|
|
183
|
+
const result = await fn({
|
|
184
|
+
entry: connectors[connectorId],
|
|
185
|
+
remove: () => {
|
|
186
|
+
removeRequested = true;
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
if (!removeRequested)
|
|
190
|
+
return result; // decided to keep it — nothing to write
|
|
191
|
+
if (!config) {
|
|
192
|
+
// Unreachable through the routes (no entry to act on means they 404 first),
|
|
193
|
+
// and a lie if it ever were: answering "removed" for a file we could not
|
|
194
|
+
// read. Surfaces as a 500 instead.
|
|
195
|
+
throw new Error(`Cannot remove connector '${connectorId}': ${configPath} is unreadable`);
|
|
196
|
+
}
|
|
197
|
+
if (dropConnector(config, connectorId))
|
|
198
|
+
await (0, config_write_lock_1.writeConfigAtomic)(configPath, config);
|
|
199
|
+
return result;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return { mutate, read, withEntry };
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=custom-connectors-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom-connectors-store.js","sourceRoot":"","sources":["../../src/connectors/custom-connectors-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom (user-pasted) connector helpers — id generation, placeholder
|
|
3
|
+
* extraction, and secret substitution. These entries are admin-trusted data,
|
|
4
|
+
* not code — see CustomConnectorEntry's doc comment in connectors/types.ts.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* MCP server names the gateway writes into every session's mcp-config.json itself
|
|
8
|
+
* (see session/process.ts's writeMcpConfig). That writer drops any injected entry whose
|
|
9
|
+
* key collides — a correct guard, but a silent one: a connector landing on one of these
|
|
10
|
+
* ids is created, stores its secret, and reports "Connected ✓" while never reaching a
|
|
11
|
+
* session, unfixable except by deleting it. Reserving the names where ids are minted
|
|
12
|
+
* turns that into a different, working id.
|
|
13
|
+
*
|
|
14
|
+
* Exported so process.ts's skip list is this same set, not a second copy of the
|
|
15
|
+
* literals — a name added there but not here silently reopens the hole.
|
|
16
|
+
*/
|
|
17
|
+
export declare const RESERVED_CONNECTOR_IDS: ReadonlySet<string>;
|
|
18
|
+
export declare function isReservedConnectorId(id: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* A slug not already used by a reserved id or an existing custom id. Appends
|
|
21
|
+
* -2, -3, ... on collision (custom ids are user-facing, not secret, so a readable
|
|
22
|
+
* suffix beats a random one).
|
|
23
|
+
*
|
|
24
|
+
* The result is always a valid connector id, length included. Every management route
|
|
25
|
+
* takes the id back from the URL and runs it through `isValidConnectorId`, so an id
|
|
26
|
+
* this function emits but that one rejects produces a connector that resolves into
|
|
27
|
+
* sessions and works — yet whose status, connect, delete and oauth/start routes all
|
|
28
|
+
* answer 400, fixable only by hand-editing config.json. The base is therefore truncated
|
|
29
|
+
* with the collision suffix's width already reserved.
|
|
30
|
+
*
|
|
31
|
+
* Managed ids like 'github'/'gmail' are NOT reserved here — an external control plane
|
|
32
|
+
* pushes those in, so a user-added connector could slug-collide with one. Accepted as a
|
|
33
|
+
* self-recoverable edge case (both paths are admin-trusted) rather than hardcoding
|
|
34
|
+
* another system's ids. RESERVED_CONNECTOR_IDS is reserved because that collision is
|
|
35
|
+
* NOT self-recoverable.
|
|
36
|
+
*/
|
|
37
|
+
export declare function slugify(label: string, existingCustomIds: Iterable<string>): string;
|
|
38
|
+
/** Recursively collect every unique {name} placeholder found in string values. */
|
|
39
|
+
export declare function extractPlaceholders(config: unknown): string[];
|
|
40
|
+
/** Recursively replace every {name} in string values with secrets[name] (or '' if absent). */
|
|
41
|
+
export declare function substitutePlaceholders(config: Record<string, unknown>, secrets: Record<string, string>): Record<string, unknown>;
|
|
42
|
+
/** Namespaced mcp-token.env key so two custom connectors reusing {api_key} don't collide. */
|
|
43
|
+
export declare function customSecretKey(id: string, name: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* Namespaced key for gateway-internal bookkeeping about a connector (the refresh
|
|
46
|
+
* token, its expiry, failure counters — see oauth-refresh-sweep.ts), stored in the
|
|
47
|
+
* same file but under a prefix `customSecretKey()` can never produce.
|
|
48
|
+
*
|
|
49
|
+
* The separate prefix is the whole point: `PLACEHOLDER_RE` accepts a leading
|
|
50
|
+
* underscore, so a pasted `{__refresh_token}` DOES yield `__refresh_token` as a
|
|
51
|
+
* secretName. Sharing one prefix would make `customSecretKey(id, '__refresh_token')`
|
|
52
|
+
* and the sweep's own slot the same string — a pasted config that resolves the
|
|
53
|
+
* gateway's refresh token into an outbound header. `isReservedPlaceholder` reports that
|
|
54
|
+
* at add-time; this split makes it unreachable even if a caller forgets to validate.
|
|
55
|
+
*/
|
|
56
|
+
export declare function internalSecretKey(id: string, name: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Placeholder names the gateway reserves for itself. Rejected when adding or pushing
|
|
59
|
+
* a connector so the admin gets a clear 400 instead of a placeholder that silently
|
|
60
|
+
* resolves to the empty string.
|
|
61
|
+
*/
|
|
62
|
+
export declare function isReservedPlaceholder(name: string): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Upper bound shared by the generator (`slugify`) and the validator below so the two
|
|
65
|
+
* can't drift — see slugify's doc comment for what happens when they do.
|
|
66
|
+
*/
|
|
67
|
+
export declare const MAX_CONNECTOR_ID_LENGTH = 64;
|
|
68
|
+
export declare function isValidConnectorId(id: string): boolean;
|
|
69
|
+
//# sourceMappingURL=custom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom.d.ts","sourceRoot":"","sources":["../../src/connectors/custom.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAqBH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAAoC,CAAC;AAE5F,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,CAalF;AAED,kFAAkF;AAClF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,EAAE,CAa7D;AAED,8FAA8F;AAC9F,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAczB;AAED,6FAA6F;AAC7F,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE3D;AAUD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEtD"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Custom (user-pasted) connector helpers — id generation, placeholder
|
|
4
|
+
* extraction, and secret substitution. These entries are admin-trusted data,
|
|
5
|
+
* not code — see CustomConnectorEntry's doc comment in connectors/types.ts.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.MAX_CONNECTOR_ID_LENGTH = exports.RESERVED_CONNECTOR_IDS = void 0;
|
|
9
|
+
exports.isReservedConnectorId = isReservedConnectorId;
|
|
10
|
+
exports.slugify = slugify;
|
|
11
|
+
exports.extractPlaceholders = extractPlaceholders;
|
|
12
|
+
exports.substitutePlaceholders = substitutePlaceholders;
|
|
13
|
+
exports.customSecretKey = customSecretKey;
|
|
14
|
+
exports.internalSecretKey = internalSecretKey;
|
|
15
|
+
exports.isReservedPlaceholder = isReservedPlaceholder;
|
|
16
|
+
exports.isValidConnectorId = isValidConnectorId;
|
|
17
|
+
const PLACEHOLDER_RE = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
18
|
+
/**
|
|
19
|
+
* Lowercase, dash-separated id from a human label, e.g. "Google Calendar!" → "google-calendar".
|
|
20
|
+
*
|
|
21
|
+
* Truncated to `budget` characters (and re-trimmed, so a cut that lands on a dash
|
|
22
|
+
* doesn't leave a trailing one). See `slugify` for why the cap is not optional.
|
|
23
|
+
*/
|
|
24
|
+
function slugBase(label, budget) {
|
|
25
|
+
const slug = label
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.trim()
|
|
28
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
29
|
+
.replace(/^-+|-+$/g, '')
|
|
30
|
+
.slice(0, budget)
|
|
31
|
+
.replace(/-+$/g, '');
|
|
32
|
+
return slug || 'connector';
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* MCP server names the gateway writes into every session's mcp-config.json itself
|
|
36
|
+
* (see session/process.ts's writeMcpConfig). That writer drops any injected entry whose
|
|
37
|
+
* key collides — a correct guard, but a silent one: a connector landing on one of these
|
|
38
|
+
* ids is created, stores its secret, and reports "Connected ✓" while never reaching a
|
|
39
|
+
* session, unfixable except by deleting it. Reserving the names where ids are minted
|
|
40
|
+
* turns that into a different, working id.
|
|
41
|
+
*
|
|
42
|
+
* Exported so process.ts's skip list is this same set, not a second copy of the
|
|
43
|
+
* literals — a name added there but not here silently reopens the hole.
|
|
44
|
+
*/
|
|
45
|
+
exports.RESERVED_CONNECTOR_IDS = new Set(['gateway', 'telegram']);
|
|
46
|
+
function isReservedConnectorId(id) {
|
|
47
|
+
return exports.RESERVED_CONNECTOR_IDS.has(id);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A slug not already used by a reserved id or an existing custom id. Appends
|
|
51
|
+
* -2, -3, ... on collision (custom ids are user-facing, not secret, so a readable
|
|
52
|
+
* suffix beats a random one).
|
|
53
|
+
*
|
|
54
|
+
* The result is always a valid connector id, length included. Every management route
|
|
55
|
+
* takes the id back from the URL and runs it through `isValidConnectorId`, so an id
|
|
56
|
+
* this function emits but that one rejects produces a connector that resolves into
|
|
57
|
+
* sessions and works — yet whose status, connect, delete and oauth/start routes all
|
|
58
|
+
* answer 400, fixable only by hand-editing config.json. The base is therefore truncated
|
|
59
|
+
* with the collision suffix's width already reserved.
|
|
60
|
+
*
|
|
61
|
+
* Managed ids like 'github'/'gmail' are NOT reserved here — an external control plane
|
|
62
|
+
* pushes those in, so a user-added connector could slug-collide with one. Accepted as a
|
|
63
|
+
* self-recoverable edge case (both paths are admin-trusted) rather than hardcoding
|
|
64
|
+
* another system's ids. RESERVED_CONNECTOR_IDS is reserved because that collision is
|
|
65
|
+
* NOT self-recoverable.
|
|
66
|
+
*/
|
|
67
|
+
function slugify(label, existingCustomIds) {
|
|
68
|
+
const taken = new Set(exports.RESERVED_CONNECTOR_IDS);
|
|
69
|
+
for (const id of existingCustomIds)
|
|
70
|
+
taken.add(id);
|
|
71
|
+
const base = slugBase(label, exports.MAX_CONNECTOR_ID_LENGTH);
|
|
72
|
+
if (!taken.has(base))
|
|
73
|
+
return base;
|
|
74
|
+
for (let n = 2;; n++) {
|
|
75
|
+
const suffix = `-${n}`;
|
|
76
|
+
// Re-slug against a smaller budget rather than trimming `base`, so the cut
|
|
77
|
+
// still lands on a clean boundary as the suffix grows past one digit.
|
|
78
|
+
const candidate = `${slugBase(label, exports.MAX_CONNECTOR_ID_LENGTH - suffix.length)}${suffix}`;
|
|
79
|
+
if (!taken.has(candidate))
|
|
80
|
+
return candidate;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Recursively collect every unique {name} placeholder found in string values. */
|
|
84
|
+
function extractPlaceholders(config) {
|
|
85
|
+
const found = new Set();
|
|
86
|
+
const walk = (value) => {
|
|
87
|
+
if (typeof value === 'string') {
|
|
88
|
+
for (const m of value.matchAll(PLACEHOLDER_RE))
|
|
89
|
+
found.add(m[1]);
|
|
90
|
+
}
|
|
91
|
+
else if (Array.isArray(value)) {
|
|
92
|
+
value.forEach(walk);
|
|
93
|
+
}
|
|
94
|
+
else if (value && typeof value === 'object') {
|
|
95
|
+
Object.values(value).forEach(walk);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
walk(config);
|
|
99
|
+
return [...found];
|
|
100
|
+
}
|
|
101
|
+
/** Recursively replace every {name} in string values with secrets[name] (or '' if absent). */
|
|
102
|
+
function substitutePlaceholders(config, secrets) {
|
|
103
|
+
const walk = (value) => {
|
|
104
|
+
if (typeof value === 'string') {
|
|
105
|
+
return value.replace(PLACEHOLDER_RE, (_match, name) => secrets[name] ?? '');
|
|
106
|
+
}
|
|
107
|
+
if (Array.isArray(value))
|
|
108
|
+
return value.map(walk);
|
|
109
|
+
if (value && typeof value === 'object') {
|
|
110
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, walk(v)]));
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
};
|
|
114
|
+
return walk(config);
|
|
115
|
+
}
|
|
116
|
+
/** Namespaced mcp-token.env key so two custom connectors reusing {api_key} don't collide. */
|
|
117
|
+
function customSecretKey(id, name) {
|
|
118
|
+
return `CUSTOM__${id}__${name}`;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Namespaced key for gateway-internal bookkeeping about a connector (the refresh
|
|
122
|
+
* token, its expiry, failure counters — see oauth-refresh-sweep.ts), stored in the
|
|
123
|
+
* same file but under a prefix `customSecretKey()` can never produce.
|
|
124
|
+
*
|
|
125
|
+
* The separate prefix is the whole point: `PLACEHOLDER_RE` accepts a leading
|
|
126
|
+
* underscore, so a pasted `{__refresh_token}` DOES yield `__refresh_token` as a
|
|
127
|
+
* secretName. Sharing one prefix would make `customSecretKey(id, '__refresh_token')`
|
|
128
|
+
* and the sweep's own slot the same string — a pasted config that resolves the
|
|
129
|
+
* gateway's refresh token into an outbound header. `isReservedPlaceholder` reports that
|
|
130
|
+
* at add-time; this split makes it unreachable even if a caller forgets to validate.
|
|
131
|
+
*/
|
|
132
|
+
function internalSecretKey(id, name) {
|
|
133
|
+
return `CUSTOMINT__${id}__${name}`;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Placeholder names the gateway reserves for itself. Rejected when adding or pushing
|
|
137
|
+
* a connector so the admin gets a clear 400 instead of a placeholder that silently
|
|
138
|
+
* resolves to the empty string.
|
|
139
|
+
*/
|
|
140
|
+
function isReservedPlaceholder(name) {
|
|
141
|
+
return name.startsWith('__');
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Connector ids are config.json object keys and are interpolated into mcp-token.env
|
|
145
|
+
* key names, so they are constrained to the shape `slugify()` produces. Routes taking
|
|
146
|
+
* an `:id` from the URL must check this first — `/oauth/receive` in particular names a
|
|
147
|
+
* connector it is creating, so there is no existing entry to validate against.
|
|
148
|
+
*/
|
|
149
|
+
const CONNECTOR_ID_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
150
|
+
/**
|
|
151
|
+
* Upper bound shared by the generator (`slugify`) and the validator below so the two
|
|
152
|
+
* can't drift — see slugify's doc comment for what happens when they do.
|
|
153
|
+
*/
|
|
154
|
+
exports.MAX_CONNECTOR_ID_LENGTH = 64;
|
|
155
|
+
function isValidConnectorId(id) {
|
|
156
|
+
return id.length <= exports.MAX_CONNECTOR_ID_LENGTH && CONNECTOR_ID_RE.test(id);
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=custom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom.js","sourceRoot":"","sources":["../../src/connectors/custom.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic OAuth 2.1 + PKCE (+ optional Dynamic Client Registration) support for
|
|
3
|
+
* "custom" MCP connectors whose only auth option is a real OAuth sign-in (no
|
|
4
|
+
* static API key) — e.g. Firecrawl's `https://mcp.firecrawl.dev/v2/mcp-oauth`.
|
|
5
|
+
*
|
|
6
|
+
* This module never touches Express or the pending-flow store — pure discovery
|
|
7
|
+
* + token functions, unit-testable with plain HTTP mocks. See
|
|
8
|
+
* `pending-oauth-store.ts` for the in-flight-flow state and
|
|
9
|
+
* `api/oauth-connectors-router.ts` for the HTTP endpoints that glue this
|
|
10
|
+
* together with `connectors/custom.ts`'s existing secret storage.
|
|
11
|
+
*
|
|
12
|
+
* Empirically verified against production Firecrawl (2026-09, this repo's own
|
|
13
|
+
* throwaway PoC — see git history / PR description, not reproduced here):
|
|
14
|
+
* DCR genuinely works with an arbitrary redirect_uri (no pre-registration
|
|
15
|
+
* needed), and the RFC 8707 `resource` parameter is REQUIRED on both the
|
|
16
|
+
* authorize request and the token exchange — omitting it yields a token that
|
|
17
|
+
* exchanges fine but is rejected by the MCP endpoint itself
|
|
18
|
+
* ("OAUTH_CONNECTION_INVALID"). Always pass `resource` through this module's
|
|
19
|
+
* functions; never make it optional.
|
|
20
|
+
*/
|
|
21
|
+
export interface OAuthMetadata {
|
|
22
|
+
/** The MCP server URL this metadata was discovered for — also the RFC 8707 `resource`. */
|
|
23
|
+
resource: string;
|
|
24
|
+
authorizationEndpoint: string;
|
|
25
|
+
tokenEndpoint: string;
|
|
26
|
+
/** Absent when the AS doesn't advertise RFC 7591 Dynamic Client Registration. */
|
|
27
|
+
registrationEndpoint?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Scopes to request for this resource: the protected-resource metadata's list
|
|
30
|
+
* when it publishes one, else the authorization server's. Empty when neither
|
|
31
|
+
* does — see `resolveScope` for what gets sent then.
|
|
32
|
+
*/
|
|
33
|
+
scopesSupported: string[];
|
|
34
|
+
}
|
|
35
|
+
export interface TokenResponse {
|
|
36
|
+
access_token: string;
|
|
37
|
+
token_type: string;
|
|
38
|
+
expires_in?: number;
|
|
39
|
+
refresh_token?: string;
|
|
40
|
+
scope?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A token endpoint that answered, and answered with a refusal.
|
|
44
|
+
*
|
|
45
|
+
* The distinction this class exists to draw is between "the grant is dead" and
|
|
46
|
+
* "we couldn't reach the provider just now". The refresh sweep gives up on a
|
|
47
|
+
* connector after a few consecutive failures and deletes its stored credentials,
|
|
48
|
+
* which is right for the first case and destructive for the second — a fifteen
|
|
49
|
+
* minute provider outage is an ordinary event and must not cost the user their
|
|
50
|
+
* sign-in. Only an explicit OAuth error response can be classified, so only this
|
|
51
|
+
* error type carries `isPermanent`; anything else the sweep sees (a DNS failure,
|
|
52
|
+
* a socket timeout, a 502, a discovery step that returned the wrong shape) is
|
|
53
|
+
* treated as retryable by default.
|
|
54
|
+
*/
|
|
55
|
+
export declare class OAuthTokenError extends Error {
|
|
56
|
+
readonly status: number;
|
|
57
|
+
/** RFC 6749 §5.2 `error` code from the response body, when it had one. */
|
|
58
|
+
readonly errorCode?: string | undefined;
|
|
59
|
+
constructor(message: string, status: number,
|
|
60
|
+
/** RFC 6749 §5.2 `error` code from the response body, when it had one. */
|
|
61
|
+
errorCode?: string | undefined);
|
|
62
|
+
/**
|
|
63
|
+
* True when the authorization server has told us this grant will never work
|
|
64
|
+
* again. Deliberately a small allowlist rather than "any 4xx": a 400 with no
|
|
65
|
+
* recognisable `error` code, or a 429, says more about this request than about
|
|
66
|
+
* the grant, and retrying costs nothing but a backoff.
|
|
67
|
+
*/
|
|
68
|
+
get isPermanent(): boolean;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Discover OAuth metadata for an MCP server by probing it unauthenticated
|
|
72
|
+
* (expects a 401 advertising `resource_metadata`, per RFC 9728), then walking
|
|
73
|
+
* protected-resource metadata → authorization-server metadata (RFC 8414).
|
|
74
|
+
* Throws a descriptive error at whichever step fails — callers surface it
|
|
75
|
+
* to the admin as "can't set up OAuth for this URL", not a generic 500.
|
|
76
|
+
*/
|
|
77
|
+
export declare function discoverOAuthMetadata(mcpUrl: string): Promise<OAuthMetadata>;
|
|
78
|
+
/**
|
|
79
|
+
* `discoverOAuthMetadata` with a process-local cache, for the background refresh
|
|
80
|
+
* sweep.
|
|
81
|
+
*
|
|
82
|
+
* Discovery costs three round trips — an MCP probe that is *expected* to 401, then
|
|
83
|
+
* protected-resource metadata, then authorization-server metadata — to obtain a
|
|
84
|
+
* document that changes about never. Paying that on every refresh is not just waste:
|
|
85
|
+
* each of those three requests is another way for a refresh to fail, and a failed
|
|
86
|
+
* refresh is what eventually makes the sweep give up and delete the connector's
|
|
87
|
+
* credentials. Fewer moving parts on the periodic path means fewer spurious failures.
|
|
88
|
+
*
|
|
89
|
+
* Only the sweep uses this. An admin-initiated `oauth/start` still calls
|
|
90
|
+
* `discoverOAuthMetadata` directly: that is the moment a human is waiting to find out
|
|
91
|
+
* whether this URL works at all, so it should see today's truth, not a cached answer
|
|
92
|
+
* from before they fixed their provider's configuration.
|
|
93
|
+
*/
|
|
94
|
+
export declare function discoverOAuthMetadataCached(mcpUrl: string): Promise<OAuthMetadata>;
|
|
95
|
+
/** Drop every cached metadata document. For tests, and for a forced re-discovery. */
|
|
96
|
+
export declare function clearOAuthMetadataCache(): void;
|
|
97
|
+
/**
|
|
98
|
+
* Register a public (no client_secret) client via RFC 7591 DCR. Throws if the
|
|
99
|
+
* server rejects it — callers fall back to a per-connector static client_id
|
|
100
|
+
* (an env var an admin configured by hand) when this isn't available at all,
|
|
101
|
+
* see `resolveClientId` below.
|
|
102
|
+
*/
|
|
103
|
+
export declare function registerClient(registrationEndpoint: string, redirectUri: string, clientName: string): Promise<string>;
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a client_id for `metadata`: try DCR first, fall back to a static
|
|
106
|
+
* `MCP_OAUTH_CLIENT_ID__<connectorId>` env var (an admin-configured client_id
|
|
107
|
+
* for a provider that advertises no registration_endpoint at all).
|
|
108
|
+
*/
|
|
109
|
+
export declare function resolveClientId(metadata: OAuthMetadata, connectorId: string, redirectUri: string): Promise<string>;
|
|
110
|
+
/**
|
|
111
|
+
* The `scope` to send with an authorization request for `metadata`.
|
|
112
|
+
*
|
|
113
|
+
* An admin-set `MCP_OAUTH_SCOPES__<connectorId>` (space-separated) wins, for the
|
|
114
|
+
* same reason `MCP_OAUTH_CLIENT_ID__<connectorId>` exists above: discovery is the
|
|
115
|
+
* provider's story about itself, and when that story is wrong the sign-in fails at
|
|
116
|
+
* the provider with an `invalid_scope` the operator cannot otherwise do anything
|
|
117
|
+
* about. Some servers publish a scope the client is not entitled to; some grant a
|
|
118
|
+
* refresh_token only when `offline_access` is asked for and never advertise it.
|
|
119
|
+
* Both are one env var away from working, instead of a code change away.
|
|
120
|
+
*
|
|
121
|
+
* `offline_access` is the fallback when nothing is discovered or configured: the
|
|
122
|
+
* refresh sweep needs a refresh_token, and an authorization request carrying no
|
|
123
|
+
* scope at all gets whatever the provider's default happens to be.
|
|
124
|
+
*/
|
|
125
|
+
export declare function resolveScope(metadata: OAuthMetadata, connectorId: string): string;
|
|
126
|
+
export interface PkcePair {
|
|
127
|
+
codeVerifier: string;
|
|
128
|
+
codeChallenge: string;
|
|
129
|
+
}
|
|
130
|
+
export declare function generatePkce(): PkcePair;
|
|
131
|
+
export declare function generateState(): string;
|
|
132
|
+
export declare function buildAuthorizeUrl(opts: {
|
|
133
|
+
metadata: OAuthMetadata;
|
|
134
|
+
clientId: string;
|
|
135
|
+
redirectUri: string;
|
|
136
|
+
scope: string;
|
|
137
|
+
codeChallenge: string;
|
|
138
|
+
state: string;
|
|
139
|
+
}): string;
|
|
140
|
+
export declare function exchangeCode(opts: {
|
|
141
|
+
metadata: OAuthMetadata;
|
|
142
|
+
clientId: string;
|
|
143
|
+
redirectUri: string;
|
|
144
|
+
code: string;
|
|
145
|
+
codeVerifier: string;
|
|
146
|
+
}): Promise<TokenResponse>;
|
|
147
|
+
export declare function refreshAccessToken(opts: {
|
|
148
|
+
metadata: OAuthMetadata;
|
|
149
|
+
clientId: string;
|
|
150
|
+
refreshToken: string;
|
|
151
|
+
}): Promise<TokenResponse>;
|
|
152
|
+
//# sourceMappingURL=mcp-oauth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-oauth.d.ts","sourceRoot":"","sources":["../../src/connectors/mcp-oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAiIH,MAAM,WAAW,aAAa;IAC5B,0FAA0F;IAC1F,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IAGtC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM;gBAH3B,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM;IACvB,0EAA0E;IACjE,SAAS,CAAC,EAAE,MAAM,YAAA;IAM7B;;;;;OAKG;IACH,IAAI,WAAW,IAAI,OAAO,CASzB;CACF;AA6BD;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAyElF;AA6CD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAaxF;AAED,qFAAqF;AACrF,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,oBAAoB,EAAE,MAAM,EAC5B,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAiBjB;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,aAAa,EACvB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,CAYjB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAKjF;AAED,MAAM,WAAW,QAAQ;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB;AAMD,wBAAgB,YAAY,IAAI,QAAQ,CAIvC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,QAAQ,EAAE,aAAa,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAYT;AAsED,wBAAgB,YAAY,CAAC,IAAI,EAAE;IACjC,QAAQ,EAAE,aAAa,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,aAAa,CAAC,CAYzB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,QAAQ,EAAE,aAAa,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,aAAa,CAAC,CAUzB"}
|