@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.
Files changed (72) hide show
  1. package/README.md +96 -0
  2. package/dist/agent/runner.d.ts +47 -1
  3. package/dist/agent/runner.d.ts.map +1 -1
  4. package/dist/agent/runner.js +116 -17
  5. package/dist/agent/runner.js.map +1 -1
  6. package/dist/api/connectors-router.d.ts +50 -0
  7. package/dist/api/connectors-router.d.ts.map +1 -0
  8. package/dist/api/connectors-router.js +650 -0
  9. package/dist/api/connectors-router.js.map +1 -0
  10. package/dist/api/gateway-router.d.ts +4 -0
  11. package/dist/api/gateway-router.d.ts.map +1 -1
  12. package/dist/api/gateway-router.js +26 -0
  13. package/dist/api/gateway-router.js.map +1 -1
  14. package/dist/api/oauth-connectors-router.d.ts +43 -0
  15. package/dist/api/oauth-connectors-router.d.ts.map +1 -0
  16. package/dist/api/oauth-connectors-router.js +384 -0
  17. package/dist/api/oauth-connectors-router.js.map +1 -0
  18. package/dist/api/router.d.ts.map +1 -1
  19. package/dist/api/router.js +101 -13
  20. package/dist/api/router.js.map +1 -1
  21. package/dist/apps/agent-manager.d.ts +6 -2
  22. package/dist/apps/agent-manager.d.ts.map +1 -1
  23. package/dist/apps/agent-manager.js +10 -18
  24. package/dist/apps/agent-manager.js.map +1 -1
  25. package/dist/config/config-write-lock.d.ts +45 -0
  26. package/dist/config/config-write-lock.d.ts.map +1 -0
  27. package/dist/config/config-write-lock.js +164 -0
  28. package/dist/config/config-write-lock.js.map +1 -0
  29. package/dist/config/watcher.d.ts.map +1 -1
  30. package/dist/config/watcher.js +29 -0
  31. package/dist/config/watcher.js.map +1 -1
  32. package/dist/connectors/custom-connectors-store.d.ts +54 -0
  33. package/dist/connectors/custom-connectors-store.d.ts.map +1 -0
  34. package/dist/connectors/custom-connectors-store.js +204 -0
  35. package/dist/connectors/custom-connectors-store.js.map +1 -0
  36. package/dist/connectors/custom.d.ts +69 -0
  37. package/dist/connectors/custom.d.ts.map +1 -0
  38. package/dist/connectors/custom.js +158 -0
  39. package/dist/connectors/custom.js.map +1 -0
  40. package/dist/connectors/mcp-oauth.d.ts +152 -0
  41. package/dist/connectors/mcp-oauth.d.ts.map +1 -0
  42. package/dist/connectors/mcp-oauth.js +522 -0
  43. package/dist/connectors/mcp-oauth.js.map +1 -0
  44. package/dist/connectors/oauth-refresh-sweep.d.ts +71 -0
  45. package/dist/connectors/oauth-refresh-sweep.d.ts.map +1 -0
  46. package/dist/connectors/oauth-refresh-sweep.js +337 -0
  47. package/dist/connectors/oauth-refresh-sweep.js.map +1 -0
  48. package/dist/connectors/pending-oauth-store.d.ts +39 -0
  49. package/dist/connectors/pending-oauth-store.d.ts.map +1 -0
  50. package/dist/connectors/pending-oauth-store.js +50 -0
  51. package/dist/connectors/pending-oauth-store.js.map +1 -0
  52. package/dist/connectors/resolve.d.ts +52 -0
  53. package/dist/connectors/resolve.d.ts.map +1 -0
  54. package/dist/connectors/resolve.js +176 -0
  55. package/dist/connectors/resolve.js.map +1 -0
  56. package/dist/connectors/token-env.d.ts +93 -0
  57. package/dist/connectors/token-env.d.ts.map +1 -0
  58. package/dist/connectors/token-env.js +323 -0
  59. package/dist/connectors/token-env.js.map +1 -0
  60. package/dist/connectors/types.d.ts +126 -0
  61. package/dist/connectors/types.d.ts.map +1 -0
  62. package/dist/connectors/types.js +13 -0
  63. package/dist/connectors/types.js.map +1 -0
  64. package/dist/index.js +32 -1
  65. package/dist/index.js.map +1 -1
  66. package/dist/session/process.d.ts +23 -0
  67. package/dist/session/process.d.ts.map +1 -1
  68. package/dist/session/process.js +78 -2
  69. package/dist/session/process.js.map +1 -1
  70. package/dist/types.d.ts +46 -1
  71. package/dist/types.d.ts.map +1 -1
  72. 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"}