@0xmaxma/claude-gateway 1.8.7 → 1.8.9
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/mcp/instructions.ts +14 -1
- package/mcp/server.ts +11 -5
- package/mcp/tools/image/module.ts +2 -70
- package/mcp/tools/shared/media.ts +76 -0
- package/mcp/tools/video/module.ts +676 -0
- package/package.json +1 -1
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createOauthConnectorsRouter = createOauthConnectorsRouter;
|
|
4
|
+
exports.createOauthCallbackRouter = createOauthCallbackRouter;
|
|
5
|
+
const express_1 = require("express");
|
|
6
|
+
const auth_1 = require("./auth");
|
|
7
|
+
const custom_1 = require("../connectors/custom");
|
|
8
|
+
const token_env_1 = require("../connectors/token-env");
|
|
9
|
+
const public_url_1 = require("../config/public-url");
|
|
10
|
+
const mcp_oauth_1 = require("../connectors/mcp-oauth");
|
|
11
|
+
const pending_oauth_store_1 = require("../connectors/pending-oauth-store");
|
|
12
|
+
const oauth_refresh_sweep_1 = require("../connectors/oauth-refresh-sweep");
|
|
13
|
+
// This route is PUBLIC (see module doc comment) and some of what lands in its
|
|
14
|
+
// plain-HTML fallback pages is not ours to trust: `error` is a raw query
|
|
15
|
+
// param off an unauthenticated request, and a token-exchange failure message
|
|
16
|
+
// can embed a token endpoint's own raw JSON response body (see mcp-oauth.ts's
|
|
17
|
+
// tokenRequest()) — reachable via an admin-added custom connector whose
|
|
18
|
+
// token endpoint is attacker-controlled. Escape before ever interpolating
|
|
19
|
+
// into an `<h1>`.
|
|
20
|
+
function escapeHtml(s) {
|
|
21
|
+
return s
|
|
22
|
+
.replace(/&/g, '&')
|
|
23
|
+
.replace(/</g, '<')
|
|
24
|
+
.replace(/>/g, '>')
|
|
25
|
+
.replace(/"/g, '"')
|
|
26
|
+
.replace(/'/g, ''');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* RFC 6749 §4.1.2.1 error codes are short ASCII tokens (`access_denied`,
|
|
30
|
+
* `invalid_scope`, …). `escapeHtml` covers this value on the way into our own
|
|
31
|
+
* fallback page, but `fail()` also forwards it OUT — as `connector_oauth_error` on
|
|
32
|
+
* the configured return URL — into a downstream app whose rendering is not ours to
|
|
33
|
+
* vouch for, and it arrives from the query string of an unauthenticated public
|
|
34
|
+
* route. URL-encoding makes it a well-formed parameter, not a safe one. Anything
|
|
35
|
+
* outside the token shape becomes a generic code before it leaves this process; the
|
|
36
|
+
* provider's own words still reach the operator, on the page and in the log.
|
|
37
|
+
*/
|
|
38
|
+
const OAUTH_ERROR_CODE_RE = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
39
|
+
function safeErrorCode(raw) {
|
|
40
|
+
return OAUTH_ERROR_CODE_RE.test(raw) ? raw : 'provider_error';
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Generic OAuth 2.1 + PKCE flow for connectors whose credential this gateway owns
|
|
44
|
+
* (`credentialOwner: 'gateway'` — see ConnectorCredentialOwner) — e.g. Firecrawl's
|
|
45
|
+
* `https://mcp.firecrawl.dev/v2/mcp-oauth`. This is the gateway-owned
|
|
46
|
+
* counterpart to an external control plane's own OAuth handlers (which produce
|
|
47
|
+
* 'external' entries via /oauth/receive): the whole dance (discovery, DCR,
|
|
48
|
+
* PKCE, token exchange, refresh) happens here, inside the user's own VM —
|
|
49
|
+
* no external service ever sees the resulting token.
|
|
50
|
+
*
|
|
51
|
+
* Two routers, deliberately NOT combined into one:
|
|
52
|
+
* - createOauthConnectorsRouter(): admin-gated, mounted under /api like every
|
|
53
|
+
* other connector-management route (`POST /v1/connectors/custom/:id/oauth/start`).
|
|
54
|
+
* - createOauthCallbackRouter(): PUBLIC, no auth — this is the URL the OAuth
|
|
55
|
+
* provider redirects the end user's own browser to
|
|
56
|
+
* (`GET /oauth/mcp/callback`), which has no API key to present. Its
|
|
57
|
+
* security rests on the single-use, TTL'd, unguessable `state` value
|
|
58
|
+
* (see pending-oauth-store.ts), the same posture cliPairingStore already
|
|
59
|
+
* uses for its own unauthenticated browser-facing routes — plus a re-read of
|
|
60
|
+
* the connector before it commits anything, because `state` proves which
|
|
61
|
+
* browser started the flow and nothing about what happened to the connector
|
|
62
|
+
* while that browser sat on the provider's consent screen.
|
|
63
|
+
*/
|
|
64
|
+
function createOauthConnectorsRouter(apiKeys, gatewayConfig, store, pendingStore = pending_oauth_store_1.pendingOAuthStore) {
|
|
65
|
+
const router = (0, express_1.Router)();
|
|
66
|
+
if (apiKeys?.length)
|
|
67
|
+
router.use((0, auth_1.createApiAuthMiddleware)(apiKeys));
|
|
68
|
+
function requireAdmin(req, res) {
|
|
69
|
+
if (!apiKeys?.length)
|
|
70
|
+
return true;
|
|
71
|
+
if (!(0, auth_1.isAdmin)(req.apiKey)) {
|
|
72
|
+
res.status(403).json({ error: 'Connector management requires an admin API key' });
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
// Same shape check as connectors-router.ts's — see its `router.param('id')` comment.
|
|
78
|
+
// Here the id is also looked up in `store`, so this is defence in depth rather than
|
|
79
|
+
// the only guard, but it keeps the 400-vs-404 distinction honest.
|
|
80
|
+
router.param('id', (req, res, next, id) => {
|
|
81
|
+
if (!(0, custom_1.isValidConnectorId)(id)) {
|
|
82
|
+
res.status(400).json({ error: `Invalid connector id '${id}'` });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
next();
|
|
86
|
+
});
|
|
87
|
+
router.post('/v1/connectors/custom/:id/oauth/start', async (req, res) => {
|
|
88
|
+
if (!requireAdmin(req, res))
|
|
89
|
+
return;
|
|
90
|
+
const id = req.params.id;
|
|
91
|
+
// Read and validate inside a try, for the reason connectors-router.ts's
|
|
92
|
+
// /connect route states at length: `store.read()` does no runtime shape
|
|
93
|
+
// validation — `config` is required by the TypeScript type and by nothing
|
|
94
|
+
// else — so an entry hand-written into config.json without it turns
|
|
95
|
+
// `entry.config.url` into a TypeError, and on Express 4 a rejected async
|
|
96
|
+
// handler escapes to index.ts's `unhandledRejection` hook, which calls
|
|
97
|
+
// emergencyShutdown(). A malformed entry must cost this caller a 500, not
|
|
98
|
+
// every agent on the box. Kept separate from the try below, which reports
|
|
99
|
+
// 502: that one covers the third-party AS, and a broken local entry is not
|
|
100
|
+
// an upstream failure.
|
|
101
|
+
let mcpUrl;
|
|
102
|
+
try {
|
|
103
|
+
const entry = (await store.read())[id];
|
|
104
|
+
if (!entry) {
|
|
105
|
+
res.status(404).json({ error: `Unknown connector '${id}'` });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (entry.credentialOwner !== 'gateway') {
|
|
109
|
+
res.status(400).json({
|
|
110
|
+
error: entry.credentialOwner === 'external'
|
|
111
|
+
? `Connector '${id}' has its credential owned externally — its token is pushed in via POST /v1/connectors/${id}/oauth/receive, not signed in for here`
|
|
112
|
+
: `Connector '${id}' was not added with oauth: true`,
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const url = entry.config?.url;
|
|
117
|
+
if (typeof url !== 'string') {
|
|
118
|
+
res.status(400).json({ error: `Connector '${id}'.config.url is missing` });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
mcpUrl = url;
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
res.status(500).json({ error: err.message });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const publicUrl = (0, public_url_1.resolveGatewayPublicUrl)(gatewayConfig?.gateway?.publicUrl);
|
|
128
|
+
if (!publicUrl) {
|
|
129
|
+
res.status(500).json({
|
|
130
|
+
error: 'This gateway has no valid gateway.publicUrl configured — OAuth sign-in needs a reachable HTTPS callback URL.',
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const redirectUri = `${publicUrl}/oauth/mcp/callback`;
|
|
135
|
+
try {
|
|
136
|
+
const metadata = await (0, mcp_oauth_1.discoverOAuthMetadata)(mcpUrl);
|
|
137
|
+
// Reuse a client this connector already registered via DCR, rather
|
|
138
|
+
// than minting (and orphaning, at the provider) a brand-new one on
|
|
139
|
+
// every Connect click — including retries after an abandoned attempt.
|
|
140
|
+
// Only valid while the redirect_uri it was registered with still
|
|
141
|
+
// matches; a changed gateway.publicUrl invalidates the cache.
|
|
142
|
+
//
|
|
143
|
+
// Falls back to the live client_id so a connector that registered before
|
|
144
|
+
// the DCR cache had a key of its own keeps reusing it, instead of orphaning
|
|
145
|
+
// one more registration at the provider on the first Connect after upgrade.
|
|
146
|
+
// Both reads only — see dcrClientIdSecretKey for why start never WRITES the
|
|
147
|
+
// key the sweep refreshes with.
|
|
148
|
+
const cachedClientId = (0, token_env_1.getSecret)((0, oauth_refresh_sweep_1.dcrClientIdSecretKey)(id)) ?? (0, token_env_1.getSecret)((0, oauth_refresh_sweep_1.clientIdSecretKey)(id));
|
|
149
|
+
const cachedRedirectUri = (0, token_env_1.getSecret)((0, oauth_refresh_sweep_1.clientRedirectUriSecretKey)(id));
|
|
150
|
+
let clientId;
|
|
151
|
+
if (cachedClientId && cachedRedirectUri === redirectUri) {
|
|
152
|
+
clientId = cachedClientId;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
clientId = await (0, mcp_oauth_1.resolveClientId)(metadata, id, redirectUri);
|
|
156
|
+
(0, token_env_1.setSecret)((0, oauth_refresh_sweep_1.dcrClientIdSecretKey)(id), clientId);
|
|
157
|
+
(0, token_env_1.setSecret)((0, oauth_refresh_sweep_1.clientRedirectUriSecretKey)(id), redirectUri);
|
|
158
|
+
}
|
|
159
|
+
const { codeVerifier, codeChallenge } = (0, mcp_oauth_1.generatePkce)();
|
|
160
|
+
const state = pendingStore.create({ connectorId: id, metadata, clientId, redirectUri, codeVerifier });
|
|
161
|
+
const scope = (0, mcp_oauth_1.resolveScope)(metadata, id);
|
|
162
|
+
const authorizeUrl = (0, mcp_oauth_1.buildAuthorizeUrl)({
|
|
163
|
+
metadata,
|
|
164
|
+
clientId,
|
|
165
|
+
redirectUri,
|
|
166
|
+
scope,
|
|
167
|
+
codeChallenge,
|
|
168
|
+
state,
|
|
169
|
+
});
|
|
170
|
+
res.json({ authorizeUrl });
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
res.status(502).json({ error: err.message });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
return router;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Both paths, for the same reason share-router.ts registers
|
|
180
|
+
* `['/shared/:token', '/gateway/shared/:token']`.
|
|
181
|
+
*
|
|
182
|
+
* `resolveGatewayPublicUrl` REQUIRES the configured publicUrl to end in `/gateway`,
|
|
183
|
+
* so the redirect_uri handed to the provider is always
|
|
184
|
+
* `<origin>/gateway/oauth/mcp/callback`. Behind Traefik that prefix is stripped
|
|
185
|
+
* before the request reaches Express and the bare path matches. In the direct-path
|
|
186
|
+
* deployment the same function explicitly permits (`http://localhost` / `127.*` /
|
|
187
|
+
* `*.internal` / `*.local` + `/gateway`) nothing strips it — there is no prefix
|
|
188
|
+
* middleware anywhere in this app — so the provider redirected the user's own
|
|
189
|
+
* browser to a path with no route. The result was a bare 404 and a pending flow
|
|
190
|
+
* left to expire silently: the sign-in fails, and the one page the user sees says
|
|
191
|
+
* nothing about why.
|
|
192
|
+
*/
|
|
193
|
+
const CALLBACK_PATHS = ['/oauth/mcp/callback', '/gateway/oauth/mcp/callback'];
|
|
194
|
+
/** See this file's module doc comment — mounted directly on the Express app,
|
|
195
|
+
* NOT under any auth middleware, at `/oauth/mcp/callback`.
|
|
196
|
+
*
|
|
197
|
+
* `agents` (all live AgentRunners) lets a completed sign-in restart the sessions
|
|
198
|
+
* already using the connector, exactly as POST /v1/connectors/:id/oauth/receive
|
|
199
|
+
* does for a pushed token. Without it the user finishes the OAuth dance, sees the
|
|
200
|
+
* connector go green, and then finds the agent they are talking to still has no
|
|
201
|
+
* such tool — its MCP subprocess was spawned with no token and cannot be
|
|
202
|
+
* hot-patched (see session/process.ts's writeMcpConfig). */
|
|
203
|
+
function createOauthCallbackRouter(store, pendingStore = pending_oauth_store_1.pendingOAuthStore, returnUrl, agents) {
|
|
204
|
+
const router = (0, express_1.Router)();
|
|
205
|
+
// This gateway is a generic, product-agnostic fork — it has no business
|
|
206
|
+
// hardcoding a downstream product's own domain (e.g. app.example.com).
|
|
207
|
+
// Whoever deploys it can opt into an auto-redirect by setting
|
|
208
|
+
// gateway.oauthReturnUrl in config.json; absent that, the plain "close this
|
|
209
|
+
// tab" message below is the safe default. Validated once, at router
|
|
210
|
+
// construction — a malformed value degrades to "not configured" rather
|
|
211
|
+
// than injecting a broken redirect into every future callback response.
|
|
212
|
+
let validReturnUrl;
|
|
213
|
+
if (returnUrl) {
|
|
214
|
+
try {
|
|
215
|
+
const parsed = new URL(returnUrl);
|
|
216
|
+
// Scheme-gated, not merely parseable. `new URL()` accepts every scheme
|
|
217
|
+
// there is — `javascript:`, `data:`, `file:`, `intent:` — and this value's
|
|
218
|
+
// whole purpose is to become the `Location` of a 302 sent to the end
|
|
219
|
+
// user's own browser, on a route that is PUBLIC and reachable by anyone
|
|
220
|
+
// who can hit the gateway. A `javascript:` return URL is a stored XSS
|
|
221
|
+
// primitive aimed at every user who ever finishes (or abandons) a sign-in,
|
|
222
|
+
// and `file:` points the browser at the operator's own disk. Nothing an
|
|
223
|
+
// OAuth flow needs to return to is anything but http(s), so requiring it
|
|
224
|
+
// costs no real deployment anything.
|
|
225
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
226
|
+
console.error(`oauth-connectors-router: gateway.oauthReturnUrl "${returnUrl}" is not an http(s) URL — ignoring it`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
validReturnUrl = parsed.toString();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
console.error(`oauth-connectors-router: gateway.oauthReturnUrl "${returnUrl}" is not a valid URL — ignoring it`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// No interstitial "Connected!" page + timed meta-refresh here on purpose —
|
|
237
|
+
// that just makes the user wait and watch a flash of gateway-branded HTML
|
|
238
|
+
// before landing back in the app. A real HTTP redirect goes straight to
|
|
239
|
+
// validReturnUrl with nothing to look at in between — on EVERY terminal
|
|
240
|
+
// outcome, not just success (a denied/expired/failed sign-in used to leave
|
|
241
|
+
// the user stranded on a bare, unbranded gateway page with no way back).
|
|
242
|
+
// The plain HTML pages below are only for the unconfigured (self-hosted,
|
|
243
|
+
// no oauthReturnUrl) case, where there's nowhere else to send the browser.
|
|
244
|
+
const CLOSE_TAB_PAGE = '<h1>Connected — you can close this tab.</h1>';
|
|
245
|
+
/** Terminal-failure response: redirect back with the reason as a query
|
|
246
|
+
* param when the app knows where "back" is, else render it in place. */
|
|
247
|
+
function fail(res, status, message, errorCode) {
|
|
248
|
+
if (validReturnUrl) {
|
|
249
|
+
const url = new URL(validReturnUrl);
|
|
250
|
+
url.searchParams.set('connector_oauth_error', errorCode);
|
|
251
|
+
res.redirect(302, url.toString());
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
res.status(status).send(`<h1>${escapeHtml(message)}</h1>`);
|
|
255
|
+
}
|
|
256
|
+
/** Is this id still a connector whose credential THIS gateway owns? */
|
|
257
|
+
async function isGatewayOwned(connectorId) {
|
|
258
|
+
return (await store.read())[connectorId]?.credentialOwner === 'gateway';
|
|
259
|
+
}
|
|
260
|
+
/** Terminal response for a connector that went away (or changed owner) mid-flow. */
|
|
261
|
+
function failGone(res) {
|
|
262
|
+
fail(res, 409, 'This connector was removed or handed to another owner while you were signing in — nothing was stored. Add it again and reconnect.', 'connector_gone');
|
|
263
|
+
}
|
|
264
|
+
router.get(CALLBACK_PATHS, async (req, res) => {
|
|
265
|
+
const state = typeof req.query.state === 'string' ? req.query.state : '';
|
|
266
|
+
const code = typeof req.query.code === 'string' ? req.query.code : '';
|
|
267
|
+
const providerError = typeof req.query.error === 'string' ? req.query.error : '';
|
|
268
|
+
const flow = state ? pendingStore.consume(state) : null;
|
|
269
|
+
if (!flow) {
|
|
270
|
+
fail(res, 400, 'This sign-in link expired or was already used. Go back and click Connect again.', 'expired_link');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (providerError) {
|
|
274
|
+
// The message keeps the provider's own words (escaped on the way into our
|
|
275
|
+
// HTML) — on a self-hosted install that page is the only diagnostic the admin
|
|
276
|
+
// gets. Only the code, which leaves for someone else's app, is clamped.
|
|
277
|
+
fail(res, 400, `Sign-in failed: ${providerError}`, safeErrorCode(providerError));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (!code) {
|
|
281
|
+
fail(res, 400, 'Sign-in failed: no authorization code returned.', 'missing_code');
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
// The flow was authorised against the connector as it stood when
|
|
286
|
+
// /oauth/start ran, and `state` only proves this browser is the one that
|
|
287
|
+
// started it. It says nothing about the connector still existing, or the
|
|
288
|
+
// gateway still being the party that owns its credential — and the window
|
|
289
|
+
// is a full FLOW_TTL_MS (see pending-oauth-store.ts) of the user sitting
|
|
290
|
+
// on the provider's consent screen. Two things can land inside it:
|
|
291
|
+
//
|
|
292
|
+
// - DELETE /v1/connectors/:id, which clears the credentials precisely so
|
|
293
|
+
// a live refresh_token cannot resurrect a connector the admin just
|
|
294
|
+
// disconnected. Writing this token set would do exactly that.
|
|
295
|
+
// - POST /v1/connectors/:id/oauth/receive, which can hand the id over to
|
|
296
|
+
// 'external'. The sweep skips a non-'gateway' entry and DELETE only
|
|
297
|
+
// clears internal keys for a 'gateway' one, so the CUSTOMINT__* keys
|
|
298
|
+
// written here would be orphans nothing ever collects.
|
|
299
|
+
//
|
|
300
|
+
// Checked once here — before spending a code on a token nothing will
|
|
301
|
+
// keep — and again after the exchange, which is the check that actually
|
|
302
|
+
// closes the race. `store.read()` degrades to {} rather than throwing
|
|
303
|
+
// (see custom-connectors-store.ts), so an unreadable config fails closed.
|
|
304
|
+
if (!(await isGatewayOwned(flow.connectorId))) {
|
|
305
|
+
failGone(res);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const token = await (0, mcp_oauth_1.exchangeCode)({
|
|
309
|
+
metadata: flow.metadata,
|
|
310
|
+
clientId: flow.clientId,
|
|
311
|
+
redirectUri: flow.redirectUri,
|
|
312
|
+
code,
|
|
313
|
+
codeVerifier: flow.codeVerifier,
|
|
314
|
+
});
|
|
315
|
+
// Authoritative re-check: either of the two actions above could have landed
|
|
316
|
+
// during the exchange round trip, which is the race the pre-check cannot
|
|
317
|
+
// see. Discarding the token here loses nothing the caller can miss — it was
|
|
318
|
+
// never stored, and reconnecting mints a fresh one.
|
|
319
|
+
if (!(await isGatewayOwned(flow.connectorId))) {
|
|
320
|
+
failGone(res);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// One rewrite for the whole result — a crash between separate writes
|
|
324
|
+
// could leave an access_token filed with no expiry, which the refresh
|
|
325
|
+
// sweep then reads as "due now" on every tick.
|
|
326
|
+
(0, token_env_1.updateSecrets)({
|
|
327
|
+
[(0, custom_1.customSecretKey)(flow.connectorId, 'access_token')]: token.access_token,
|
|
328
|
+
[(0, oauth_refresh_sweep_1.clientIdSecretKey)(flow.connectorId)]: flow.clientId,
|
|
329
|
+
...(token.refresh_token
|
|
330
|
+
? { [(0, oauth_refresh_sweep_1.refreshTokenSecretKey)(flow.connectorId)]: token.refresh_token }
|
|
331
|
+
: {}),
|
|
332
|
+
[(0, oauth_refresh_sweep_1.expiresAtSecretKey)(flow.connectorId)]: String(Date.now() + (token.expires_in ?? 3600) * 1000),
|
|
333
|
+
// Bumped so the background refresh sweep (oauth-refresh-sweep.ts) can
|
|
334
|
+
// tell, after its own two-network-round-trip refresh, whether a fresher
|
|
335
|
+
// token landed here in the meantime — and if so, discard its own
|
|
336
|
+
// now-stale result instead of clobbering this one.
|
|
337
|
+
[(0, oauth_refresh_sweep_1.tokenGenerationSecretKey)(flow.connectorId)]: String(Date.now()),
|
|
338
|
+
},
|
|
339
|
+
// A completed sign-in is as much a fresh start as the sweep's own
|
|
340
|
+
// successful refresh, and has to clear the same bookkeeping. Leaving it
|
|
341
|
+
// behind meant a connector the user had just reconnected was still
|
|
342
|
+
// serving a backoff of up to six hours — so its brand-new one-hour token
|
|
343
|
+
// expired unrefreshed — while status reported the old failure streak
|
|
344
|
+
// against it. Worst of the three: a permanent count already sitting at 2
|
|
345
|
+
// turned the very next refusal into credential deletion, one tick after
|
|
346
|
+
// a successful sign-in.
|
|
347
|
+
[
|
|
348
|
+
(0, oauth_refresh_sweep_1.refreshFailCountSecretKey)(flow.connectorId),
|
|
349
|
+
(0, oauth_refresh_sweep_1.refreshTransientCountSecretKey)(flow.connectorId),
|
|
350
|
+
(0, oauth_refresh_sweep_1.refreshBackoffUntilSecretKey)(flow.connectorId),
|
|
351
|
+
// `client_id` above is written unconditionally; `refresh_token` only
|
|
352
|
+
// when this response carried one. An AS is entitled to omit it (RFC
|
|
353
|
+
// 6749 §6, and it happens routinely whenever the granted scopes don't
|
|
354
|
+
// include `offline_access`), and without this removal the old one
|
|
355
|
+
// survived — leaving refresh_token R1, issued to client C1, sitting
|
|
356
|
+
// beside the new client_id C2. The sweep then POSTs that mismatched
|
|
357
|
+
// pair, the AS answers `invalid_client`, which is classified permanent,
|
|
358
|
+
// and three ticks later it deletes the sign-in that just succeeded.
|
|
359
|
+
// The two values are one credential: they are replaced together or not
|
|
360
|
+
// at all.
|
|
361
|
+
...(token.refresh_token ? [] : [(0, oauth_refresh_sweep_1.refreshTokenSecretKey)(flow.connectorId)]),
|
|
362
|
+
]);
|
|
363
|
+
// The connector only just became resolvable; a session spawned before
|
|
364
|
+
// this holds an MCP config without it. Failure to restart must not turn a
|
|
365
|
+
// successful sign-in into an error page — the token IS stored — so this
|
|
366
|
+
// is logged, not surfaced.
|
|
367
|
+
if (agents) {
|
|
368
|
+
await Promise.all([...agents.values()].map((runner) => runner.restartSessionsUsingConnector(flow.connectorId).catch((e) => {
|
|
369
|
+
console.error(`oauth-connectors-router: restart for connector=${flow.connectorId} failed: ${e.message}`);
|
|
370
|
+
})));
|
|
371
|
+
}
|
|
372
|
+
if (validReturnUrl) {
|
|
373
|
+
res.redirect(302, validReturnUrl);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
res.send(CLOSE_TAB_PAGE);
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
fail(res, 502, `Sign-in failed: ${err.message}`, 'exchange_failed');
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
return router;
|
|
383
|
+
}
|
|
384
|
+
//# sourceMappingURL=oauth-connectors-router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-connectors-router.js","sourceRoot":"","sources":["../../src/api/oauth-connectors-router.ts"],"names":[],"mappings":"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"}
|
package/dist/api/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/api/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAW,QAAQ,EAAE,MAAM,SAAS,CAAC;AAOpD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,EAAe,WAAW,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/api/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAW,QAAQ,EAAE,MAAM,SAAS,CAAC;AAOpD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,EAAe,WAAW,EAAE,MAAM,UAAU,CAAC;AAmDzE,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,MAAM,CAExD;AAMD,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,MAAM,CAEtD;AAuED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CA4CjD;AAoID,wBAAgB,eAAe,CAC7B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,EACtC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,EACtC,OAAO,EAAE,MAAM,EAAE,EACjB,UAAU,CAAC,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,WAAW,EAAE,GACrB,MAAM,CAsuGR"}
|
package/dist/api/router.js
CHANGED
|
@@ -46,7 +46,9 @@ const path = __importStar(require("path"));
|
|
|
46
46
|
const child_process_1 = require("child_process");
|
|
47
47
|
const runner_1 = require("../agent/runner");
|
|
48
48
|
const turn_stream_1 = require("../agent/turn-stream");
|
|
49
|
+
const custom_1 = require("../connectors/custom");
|
|
49
50
|
const agent_env_1 = require("../config/agent-env");
|
|
51
|
+
const config_write_lock_1 = require("../config/config-write-lock");
|
|
50
52
|
const auth_1 = require("./auth");
|
|
51
53
|
const media_store_1 = require("../history/media-store");
|
|
52
54
|
const db_1 = require("../history/db");
|
|
@@ -373,8 +375,6 @@ function apiKeyId(key) {
|
|
|
373
375
|
function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models) {
|
|
374
376
|
const router = (0, express_1.Router)();
|
|
375
377
|
const auth = (0, auth_1.createApiAuthMiddleware)(apiKeys);
|
|
376
|
-
// Closure-scoped lock: serialises concurrent writes to config.json within this router instance.
|
|
377
|
-
let configWriteLock = Promise.resolve();
|
|
378
378
|
async function writeAgentsToConfigImpl(cfgPath, mutate, newId) {
|
|
379
379
|
const raw = await fsp.readFile(cfgPath, 'utf-8');
|
|
380
380
|
const config = JSON.parse(raw);
|
|
@@ -384,18 +384,14 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
384
384
|
throw Object.assign(new Error(`Agent '${newId}' already exists in config`), { code: 'DUPLICATE' });
|
|
385
385
|
}
|
|
386
386
|
mutate(config.agents);
|
|
387
|
-
|
|
388
|
-
// mode: 0o600 — config.json carries the admin API key and every agent's
|
|
389
|
-
// channel bot tokens; rename() onto cfgPath carries this file's mode
|
|
390
|
-
// with it, so an unmoded tmp file silently downgrades an existing 0600
|
|
391
|
-
// config to 0644 on its very first edit (issue #460).
|
|
392
|
-
await fsp.writeFile(tmp, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
393
|
-
await fsp.rename(tmp, cfgPath);
|
|
387
|
+
await (0, config_write_lock_1.writeConfigAtomic)(cfgPath, config);
|
|
394
388
|
}
|
|
389
|
+
// Process-wide, keyed by config path — NOT a lock private to this router. The
|
|
390
|
+
// connectors store and the app-agent manager rewrite the same file, and a lock
|
|
391
|
+
// scoped to this closure would serialise agent writes against each other while
|
|
392
|
+
// still letting one of those clobber them. See config/config-write-lock.ts.
|
|
395
393
|
function writeAgentsToConfig(cfgPath, mutate, newId) {
|
|
396
|
-
|
|
397
|
-
configWriteLock = next.catch(() => { });
|
|
398
|
-
return next;
|
|
394
|
+
return (0, config_write_lock_1.withConfigWriteLock)(cfgPath, () => writeAgentsToConfigImpl(cfgPath, mutate, newId));
|
|
399
395
|
}
|
|
400
396
|
/**
|
|
401
397
|
* GET /api/v1/commands
|
|
@@ -719,6 +715,7 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
719
715
|
description: cfg.description,
|
|
720
716
|
model: cfg.claude?.model ?? null,
|
|
721
717
|
allow_tools: cfg.allow_tools ?? false,
|
|
718
|
+
connectors: cfg.connectors ?? {},
|
|
722
719
|
avatarUrl: cfg.avatar ? `/api/v1/agents/${id}/avatar` : null,
|
|
723
720
|
telegram_connected: !!cfg.telegram?.botToken,
|
|
724
721
|
discord_connected: !!cfg.discord?.botToken,
|
|
@@ -1452,7 +1449,7 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
1452
1449
|
return;
|
|
1453
1450
|
}
|
|
1454
1451
|
const body = req.body;
|
|
1455
|
-
const { name, description, model, allow_tools, telegram_bot_token, discord_bot_token, line_channel_access_token, line_channel_secret, line_dm_policy, line_dm_allowlist, line_group_policy, line_group_allowlist, line_require_mention, line_pairing, slack_bot_token, slack_signing_secret, slack_dm_policy, slack_dm_allowlist, slack_group_policy, slack_group_allowlist, slack_require_mention, slack_pairing } = body;
|
|
1452
|
+
const { name, description, model, allow_tools, telegram_bot_token, discord_bot_token, line_channel_access_token, line_channel_secret, line_dm_policy, line_dm_allowlist, line_group_policy, line_group_allowlist, line_require_mention, line_pairing, slack_bot_token, slack_signing_secret, slack_dm_policy, slack_dm_allowlist, slack_group_policy, slack_group_allowlist, slack_require_mention, slack_pairing, connectors } = body;
|
|
1456
1453
|
if (name !== undefined && name !== null && typeof name !== 'string') {
|
|
1457
1454
|
res.status(400).json({ error: 'name must be a string or null' });
|
|
1458
1455
|
return;
|
|
@@ -1604,6 +1601,51 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
1604
1601
|
const slackAccessTouched = slack_dm_policy !== undefined || slack_dm_allowlist !== undefined ||
|
|
1605
1602
|
slack_group_policy !== undefined || slack_group_allowlist !== undefined || slack_require_mention !== undefined ||
|
|
1606
1603
|
slack_pairing !== undefined;
|
|
1604
|
+
// connectors: a partial map of { [connectorId]: { enabled: boolean } } to merge.
|
|
1605
|
+
const connectorPatch = {};
|
|
1606
|
+
if (connectors !== undefined) {
|
|
1607
|
+
// Admin, unlike the rest of this route. Every connector route is admin-only
|
|
1608
|
+
// (see connectors-router.ts's requireAdmin), and enabling one here reaches the
|
|
1609
|
+
// same credential those routes guard: a connector's secret is resolved into
|
|
1610
|
+
// the agent's mcp-config at spawn, so flipping this flag is what actually
|
|
1611
|
+
// hands an agent the token an admin connected. Under the multi-owner posture
|
|
1612
|
+
// README documents — `gateway.connectorsDefaultEnabled: false`, connectors off
|
|
1613
|
+
// unless named — a `write` key scoped to nothing but its own agent could
|
|
1614
|
+
// otherwise grant that agent any connector on the box. It would not even need
|
|
1615
|
+
// to enumerate them first: ids are label slugs (`github`, `notion`) and the
|
|
1616
|
+
// check below deliberately does not require the id to exist.
|
|
1617
|
+
//
|
|
1618
|
+
// Deliberately field-level rather than route-level: the rest of PATCH is a
|
|
1619
|
+
// `write`-key operation on the caller's own agent (API.md's table) and must
|
|
1620
|
+
// stay that way. Only the field that crosses into another owner's credential
|
|
1621
|
+
// is raised.
|
|
1622
|
+
if (!(0, auth_1.isAdmin)(req.apiKey)) {
|
|
1623
|
+
res.status(403).json({ error: 'Admin permission required to change connectors' });
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
if (typeof connectors !== 'object' || connectors === null || Array.isArray(connectors)) {
|
|
1627
|
+
res.status(400).json({ error: 'connectors must be an object' });
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
for (const [id, val] of Object.entries(connectors)) {
|
|
1631
|
+
// Shape only, not existence: enablement is stored per agent and read back
|
|
1632
|
+
// by id, so an id that can't be a connector id is an entry nothing will
|
|
1633
|
+
// ever match — it just accumulates in config.json. Existence is
|
|
1634
|
+
// deliberately NOT required; enablement defaults to opt-out, so
|
|
1635
|
+
// pre-setting `{enabled: false}` for a connector nobody has added yet is a
|
|
1636
|
+
// legitimate way to keep it off an agent from the moment it appears.
|
|
1637
|
+
if (!(0, custom_1.isValidConnectorId)(id)) {
|
|
1638
|
+
res.status(400).json({ error: `Invalid connector id '${id}'` });
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
const enabled = val?.enabled;
|
|
1642
|
+
if (typeof enabled !== 'boolean') {
|
|
1643
|
+
res.status(400).json({ error: `connectors.${id}.enabled must be a boolean` });
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
connectorPatch[id] = { enabled };
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1607
1649
|
try {
|
|
1608
1650
|
await writeAgentsToConfig(configPath, (agents) => {
|
|
1609
1651
|
const agent = agents.find((a) => a.id === agentId);
|
|
@@ -1753,6 +1795,10 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
1753
1795
|
}
|
|
1754
1796
|
}
|
|
1755
1797
|
}
|
|
1798
|
+
if (connectors !== undefined) {
|
|
1799
|
+
const existing = agent.connectors ?? {};
|
|
1800
|
+
agent.connectors = { ...existing, ...connectorPatch };
|
|
1801
|
+
}
|
|
1756
1802
|
});
|
|
1757
1803
|
}
|
|
1758
1804
|
catch (err) {
|
|
@@ -1945,6 +1991,47 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
1945
1991
|
(0, pending_senders_1.clearPendingSender)('slack', agentId, id);
|
|
1946
1992
|
}
|
|
1947
1993
|
}
|
|
1994
|
+
if (connectors !== undefined) {
|
|
1995
|
+
const before = cfg.connectors ?? {};
|
|
1996
|
+
const merged = { ...before, ...connectorPatch };
|
|
1997
|
+
// Only respawn when the merged map actually differs. An external panel
|
|
1998
|
+
// that echoes the whole agent form back on every save sends `connectors`
|
|
1999
|
+
// on edits that have nothing to do with connectors, and restarting every
|
|
2000
|
+
// live session for a no-op patch is a visible interruption to whoever is
|
|
2001
|
+
// talking to that agent. The entries are `{enabled: boolean}`, so
|
|
2002
|
+
// comparing that one field per key is the whole comparison.
|
|
2003
|
+
const ids = new Set([...Object.keys(before), ...Object.keys(merged)]);
|
|
2004
|
+
const changed = [...ids].some((id) => before[id]?.enabled !== merged[id]?.enabled);
|
|
2005
|
+
cfg.connectors = merged;
|
|
2006
|
+
// Enablement is read at spawn — respawn live sessions so they pick it up.
|
|
2007
|
+
// The runner's view of the map is updated either way; only the teardown is
|
|
2008
|
+
// conditional, so a no-op patch costs nothing but still leaves the runner
|
|
2009
|
+
// holding the same map that was just written to disk.
|
|
2010
|
+
const runner = agentRunners.get(agentId);
|
|
2011
|
+
if (runner) {
|
|
2012
|
+
runner.updateAgentConfig(cfg);
|
|
2013
|
+
// Same options as AgentRunner.restartSessionsUsingConnector — a connector
|
|
2014
|
+
// enablement change is exactly the same kind of change, so it must not
|
|
2015
|
+
// SIGKILL an idle channel session either. Bare restartOrDefer() defaults
|
|
2016
|
+
// deferIdle to false, which stops idle sessions immediately.
|
|
2017
|
+
//
|
|
2018
|
+
// Caught, because this call sits AFTER the handler's try/catch closes and
|
|
2019
|
+
// restartOrDefer awaits proc.stop() unguarded: on Express 4 a rejection
|
|
2020
|
+
// here escapes the handler entirely and lands in index.ts's
|
|
2021
|
+
// `unhandledRejection` hook, which calls emergencyShutdown() — every agent
|
|
2022
|
+
// on the box killed because one PATCH toggled a connector. The config is
|
|
2023
|
+
// already written and the runner's map already updated at this point, so a
|
|
2024
|
+
// failed teardown costs the caller nothing but a session that respawns on
|
|
2025
|
+
// its own next natural restart. Every sibling call site guards this the
|
|
2026
|
+
// same way (oauth-refresh-sweep.ts's .catch, connectors-router.ts's
|
|
2027
|
+
// restartSessionsUsing).
|
|
2028
|
+
if (changed) {
|
|
2029
|
+
await runner.restartOrDefer({ skipBusy: false, deferIdle: true }).catch((err) => {
|
|
2030
|
+
console.error(`router: connector-enablement restart for agent=${agentId} failed: ${err.message}`);
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
1948
2035
|
res.json({
|
|
1949
2036
|
agent: {
|
|
1950
2037
|
id: agentId,
|
|
@@ -1952,6 +2039,7 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
|
|
|
1952
2039
|
description: cfg.description,
|
|
1953
2040
|
model: cfg.claude?.model,
|
|
1954
2041
|
allow_tools: cfg.allow_tools ?? false,
|
|
2042
|
+
connectors: cfg.connectors ?? {},
|
|
1955
2043
|
telegram_connected: !!cfg.telegram?.botToken,
|
|
1956
2044
|
discord_connected: !!cfg.discord?.botToken,
|
|
1957
2045
|
telegram_token_preview: cfg.telegram?.botToken ? maskToken(cfg.telegram.botToken) : null,
|