@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.
Files changed (77) 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/mcp/instructions.ts +14 -1
  73. package/mcp/server.ts +11 -5
  74. package/mcp/tools/image/module.ts +2 -70
  75. package/mcp/tools/shared/media.ts +76 -0
  76. package/mcp/tools/video/module.ts +676 -0
  77. 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, '&amp;')
23
+ .replace(/</g, '&lt;')
24
+ .replace(/>/g, '&gt;')
25
+ .replace(/"/g, '&quot;')
26
+ .replace(/'/g, '&#39;');
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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"}
@@ -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;AAiDzE,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,CAgpGR"}
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"}
@@ -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
- const tmp = cfgPath + '.tmp.' + (0, crypto_1.randomUUID)();
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
- const next = configWriteLock.catch(() => { }).then(() => writeAgentsToConfigImpl(cfgPath, mutate, newId));
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,