slack-max-api-mcp 1.0.11 → 1.0.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-max-api-mcp",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "Operations-first Slack MCP server (stdio) for Codex and Claude Code",
5
5
  "main": "src/slack-mcp-server.js",
6
6
  "bin": {
@@ -71,6 +71,25 @@ const GATEWAY_COMPAT_HOST = "43.202.54.65.sslip.io";
71
71
  const GATEWAY_URL_ENV = process.env.SLACK_GATEWAY_URL || "";
72
72
  const GATEWAY_API_KEY = process.env.SLACK_GATEWAY_API_KEY || "";
73
73
  const GATEWAY_PROFILE = process.env.SLACK_GATEWAY_PROFILE || "";
74
+ const GATEWAY_PUBLIC_BASE_URL =
75
+ process.env.SLACK_GATEWAY_PUBLIC_BASE_URL || ONBOARD_PUBLIC_BASE_URL;
76
+ const GATEWAY_ALLOW_PUBLIC = parseBooleanEnv(process.env.SLACK_GATEWAY_ALLOW_PUBLIC, false);
77
+ const GATEWAY_SHARED_SECRET =
78
+ process.env.SLACK_GATEWAY_SHARED_SECRET || process.env.SLACK_GATEWAY_API_KEY || "";
79
+ const GATEWAY_CLIENT_API_KEY =
80
+ process.env.SLACK_GATEWAY_CLIENT_API_KEY || GATEWAY_API_KEY || GATEWAY_SHARED_SECRET;
81
+ const GATEWAY_PUBLIC_ONBOARD_ENABLED = parseBooleanEnv(
82
+ process.env.SLACK_GATEWAY_PUBLIC_ONBOARD,
83
+ true
84
+ );
85
+ const GATEWAY_PUBLIC_ONBOARD_EXPOSE_API_KEY = parseBooleanEnv(
86
+ process.env.SLACK_GATEWAY_PUBLIC_ONBOARD_EXPOSE_API_KEY,
87
+ false
88
+ );
89
+ const GATEWAY_PUBLIC_ONBOARD_API_KEY = process.env.SLACK_GATEWAY_PUBLIC_ONBOARD_API_KEY || "";
90
+ const GATEWAY_PUBLIC_ONBOARD_PROFILE_PREFIX =
91
+ process.env.SLACK_GATEWAY_PUBLIC_ONBOARD_PROFILE_PREFIX || "auto";
92
+ const GATEWAY_STATE_TTL_MS = Number(process.env.SLACK_GATEWAY_STATE_TTL_MS || 15 * 60 * 1000);
74
93
  const INSECURE_TLS = parseBooleanEnv(process.env.SLACK_INSECURE_TLS, false);
75
94
  const GATEWAY_SKIP_TLS_VERIFY = parseBooleanEnv(
76
95
  process.env.SLACK_GATEWAY_SKIP_TLS_VERIFY,
@@ -1702,6 +1721,132 @@ function formatClientConfigSummary(config) {
1702
1721
  ].join("\n");
1703
1722
  }
1704
1723
 
1724
+ function buildGatewayRedirectUri(publicBaseUrl, callbackPath) {
1725
+ const url = new URL(callbackPath, `${String(publicBaseUrl || "").replace(/\/+$/, "")}/`);
1726
+ return url.toString();
1727
+ }
1728
+
1729
+ function parseScopesFromQuery(searchParams, key, fallback) {
1730
+ const value = searchParams.get(key);
1731
+ return parseScopeList(value || fallback);
1732
+ }
1733
+
1734
+ function buildOauthStartUrlFromPayload(gatewayBaseUrl, payload) {
1735
+ const params = new URLSearchParams();
1736
+ if (payload.profile) params.set("profile", payload.profile);
1737
+ if (payload.team) params.set("team", payload.team);
1738
+ if (payload.scope) params.set("scope", payload.scope);
1739
+ if (payload.user_scope) params.set("user_scope", payload.user_scope);
1740
+ return `${String(gatewayBaseUrl || "").replace(/\/+$/, "")}/oauth/start${
1741
+ params.toString() ? `?${params.toString()}` : ""
1742
+ }`;
1743
+ }
1744
+
1745
+ function buildPublicOnboardPayload(gatewayBaseUrl, params = {}) {
1746
+ const profile =
1747
+ String(params.profile || "").trim() ||
1748
+ createAutoOnboardProfileName(GATEWAY_PUBLIC_ONBOARD_PROFILE_PREFIX);
1749
+ const team = String(params.team || process.env.SLACK_OAUTH_TEAM_ID || "").trim();
1750
+ const scope = parseScopeList(params.scope || DEFAULT_OAUTH_BOT_SCOPES).join(",");
1751
+ const userScope = parseScopeList(params.user_scope || DEFAULT_OAUTH_USER_SCOPES).join(",");
1752
+ const payload = {
1753
+ gateway_url: String(gatewayBaseUrl || "").replace(/\/+$/, ""),
1754
+ gateway_api_key: "",
1755
+ profile,
1756
+ team,
1757
+ scope,
1758
+ user_scope: userScope,
1759
+ };
1760
+ if (GATEWAY_ALLOW_PUBLIC) {
1761
+ payload.gateway_api_key = "";
1762
+ } else if (GATEWAY_PUBLIC_ONBOARD_API_KEY) {
1763
+ payload.gateway_api_key = GATEWAY_PUBLIC_ONBOARD_API_KEY;
1764
+ } else if (GATEWAY_PUBLIC_ONBOARD_EXPOSE_API_KEY) {
1765
+ payload.gateway_api_key = GATEWAY_CLIENT_API_KEY || "";
1766
+ }
1767
+ return {
1768
+ ok: true,
1769
+ mode: "public_onboard",
1770
+ gateway_url: payload.gateway_url,
1771
+ gateway_api_key: payload.gateway_api_key,
1772
+ profile: payload.profile,
1773
+ oauth_start_url: buildOauthStartUrlFromPayload(gatewayBaseUrl, payload),
1774
+ requires_gateway_api_key: !GATEWAY_ALLOW_PUBLIC,
1775
+ };
1776
+ }
1777
+
1778
+ async function readRequestText(req, maxBytes = 1024 * 1024) {
1779
+ return new Promise((resolve, reject) => {
1780
+ const chunks = [];
1781
+ let total = 0;
1782
+ req.on("data", (chunk) => {
1783
+ total += chunk.length;
1784
+ if (total > maxBytes) {
1785
+ reject(new Error("Request body too large."));
1786
+ req.destroy();
1787
+ return;
1788
+ }
1789
+ chunks.push(chunk);
1790
+ });
1791
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
1792
+ req.on("error", (error) => reject(error));
1793
+ });
1794
+ }
1795
+
1796
+ async function readRequestJson(req, maxBytes) {
1797
+ const text = await readRequestText(req, maxBytes);
1798
+ if (!text.trim()) return {};
1799
+ try {
1800
+ return JSON.parse(text);
1801
+ } catch {
1802
+ throw new Error("Invalid JSON body.");
1803
+ }
1804
+ }
1805
+
1806
+ function getRequestApiKey(req) {
1807
+ const authHeader = req.headers.authorization || "";
1808
+ if (authHeader.toLowerCase().startsWith("bearer ")) {
1809
+ return authHeader.slice(7).trim();
1810
+ }
1811
+ const xApiKey = req.headers["x-api-key"];
1812
+ return typeof xApiKey === "string" ? xApiKey.trim() : "";
1813
+ }
1814
+
1815
+ function isGatewayAuthorized(req) {
1816
+ if (GATEWAY_ALLOW_PUBLIC) return true;
1817
+ const allowedKeys = [GATEWAY_SHARED_SECRET, GATEWAY_CLIENT_API_KEY].filter(Boolean);
1818
+ if (allowedKeys.length === 0) return false;
1819
+ const provided = getRequestApiKey(req);
1820
+ return Boolean(provided && allowedKeys.includes(provided));
1821
+ }
1822
+
1823
+ function requireGatewayClientCredentials() {
1824
+ const clientId = process.env.SLACK_CLIENT_ID || "";
1825
+ const clientSecret = process.env.SLACK_CLIENT_SECRET || "";
1826
+ if (!clientId || !clientSecret) {
1827
+ throw new Error("Gateway OAuth requires SLACK_CLIENT_ID and SLACK_CLIENT_SECRET on the gateway server.");
1828
+ }
1829
+ return { clientId, clientSecret };
1830
+ }
1831
+
1832
+ function profileSummariesFromStore(store) {
1833
+ const summaries = [];
1834
+ for (const [key, profile] of Object.entries(store.profiles || {})) {
1835
+ summaries.push({
1836
+ key,
1837
+ profile_name: profile.profile_name || "",
1838
+ team_id: profile.team_id || "",
1839
+ team_name: profile.team_name || "",
1840
+ authed_user_id: profile.authed_user_id || "",
1841
+ has_bot_token: Boolean(profile.bot_token),
1842
+ has_user_token: Boolean(profile.user_token),
1843
+ updated_at: profile.updated_at || null,
1844
+ is_default: store.default_profile === key,
1845
+ });
1846
+ }
1847
+ return summaries;
1848
+ }
1849
+
1705
1850
  async function runOauthLogin(args) {
1706
1851
  const { options } = parseCliArgs(args);
1707
1852
  const clientId = options["client-id"] || process.env.SLACK_CLIENT_ID;
@@ -1923,6 +2068,9 @@ function printOnboardServerHelp() {
1923
2068
  " SLACK_ONBOARD_SERVER_HOST, SLACK_ONBOARD_SERVER_PORT",
1924
2069
  " SLACK_ONBOARD_PUBLIC_BASE_URL, SLACK_ONBOARD_SERVER_CALLBACK_PATH",
1925
2070
  " SLACK_ONBOARD_CLAIM_TTL_MS",
2071
+ " SLACK_GATEWAY_PUBLIC_BASE_URL",
2072
+ " SLACK_GATEWAY_SHARED_SECRET / SLACK_GATEWAY_CLIENT_API_KEY",
2073
+ " SLACK_GATEWAY_ALLOW_PUBLIC, SLACK_GATEWAY_PUBLIC_ONBOARD",
1926
2074
  ];
1927
2075
  console.log(lines.join("\n"));
1928
2076
  }
@@ -2060,21 +2208,20 @@ async function runOnboardClient(args) {
2060
2208
 
2061
2209
  async function runOnboardServerStart(args) {
2062
2210
  const { options } = parseCliArgs(args);
2063
- const clientId = options["client-id"] || process.env.SLACK_CLIENT_ID;
2064
- const clientSecret = options["client-secret"] || process.env.SLACK_CLIENT_SECRET;
2065
- if (!clientId || !clientSecret) {
2066
- throw new Error("Missing SLACK_CLIENT_ID or SLACK_CLIENT_SECRET on onboarding server.");
2067
- }
2211
+ const { clientId, clientSecret } = requireGatewayClientCredentials();
2068
2212
 
2069
2213
  const host = String(options.host || ONBOARD_SERVER_HOST);
2070
2214
  const port = Number(options.port || ONBOARD_SERVER_PORT);
2071
2215
  const callbackPath = String(options["callback-path"] || ONBOARD_CALLBACK_PATH);
2072
- const publicBaseUrl = String(options["public-base-url"] || ONBOARD_PUBLIC_BASE_URL).replace(/\/+$/, "");
2216
+ const publicBaseUrl = String(
2217
+ options["public-base-url"] || GATEWAY_PUBLIC_BASE_URL || ONBOARD_PUBLIC_BASE_URL
2218
+ ).replace(/\/+$/, "");
2073
2219
  const claimTtlMs = Math.max(60_000, Number(options["claim-ttl-ms"] || ONBOARD_CLAIM_TTL_MS));
2074
2220
  const redirectUri = new URL(callbackPath, `${publicBaseUrl}/`).toString();
2075
2221
 
2076
2222
  const claimSessions = new Map();
2077
2223
  const stateToClaim = new Map();
2224
+ const pendingStates = new Map();
2078
2225
 
2079
2226
  const server = http.createServer(async (req, res) => {
2080
2227
  try {
@@ -2082,21 +2229,34 @@ async function runOnboardServerStart(args) {
2082
2229
  const method = req.method || "GET";
2083
2230
  const requestUrl = new URL(req.url || "/", `http://${host}:${port}`);
2084
2231
 
2085
- if (method === "GET" && requestUrl.pathname === "/health") {
2086
- sendJson(res, 200, {
2087
- ok: true,
2088
- service: SERVER_NAME,
2089
- mode: "onboard_server",
2090
- public_base_url: publicBaseUrl,
2091
- callback_path: callbackPath,
2092
- });
2093
- return;
2094
- }
2232
+ if (method === "GET" && requestUrl.pathname === "/health") {
2233
+ sendJson(res, 200, {
2234
+ ok: true,
2235
+ service: SERVER_NAME,
2236
+ mode: "gateway",
2237
+ public_base_url: publicBaseUrl,
2238
+ callback_path: callbackPath,
2239
+ token_store_path: TOKEN_STORE_PATH,
2240
+ client_config_path: CLIENT_CONFIG_PATH,
2241
+ });
2242
+ return;
2243
+ }
2244
+
2245
+ if (method === "GET" && requestUrl.pathname === "/onboard/bootstrap") {
2246
+ if (GATEWAY_PUBLIC_ONBOARD_ENABLED) {
2247
+ const payload = buildPublicOnboardPayload(publicBaseUrl, {
2248
+ profile: requestUrl.searchParams.get("profile") || "",
2249
+ team: requestUrl.searchParams.get("team") || "",
2250
+ scope: requestUrl.searchParams.get("scope") || "",
2251
+ user_scope: requestUrl.searchParams.get("user_scope") || "",
2252
+ });
2253
+ sendJson(res, 200, payload);
2254
+ return;
2255
+ }
2095
2256
 
2096
- if (method === "GET" && requestUrl.pathname === "/onboard/bootstrap") {
2097
- const profile = String(requestUrl.searchParams.get("profile") || "").trim();
2098
- const team = String(requestUrl.searchParams.get("team") || "").trim();
2099
- const botScopes = parseScopeList(requestUrl.searchParams.get("scope") || DEFAULT_OAUTH_BOT_SCOPES);
2257
+ const profile = String(requestUrl.searchParams.get("profile") || "").trim();
2258
+ const team = String(requestUrl.searchParams.get("team") || "").trim();
2259
+ const botScopes = parseScopeList(requestUrl.searchParams.get("scope") || DEFAULT_OAUTH_BOT_SCOPES);
2100
2260
  const userScopes = parseScopeList(
2101
2261
  requestUrl.searchParams.get("user_scope") || DEFAULT_OAUTH_USER_SCOPES
2102
2262
  );
@@ -2157,42 +2317,106 @@ async function runOnboardServerStart(args) {
2157
2317
  res.writeHead(302, { Location: authorizeUrl });
2158
2318
  res.end();
2159
2319
  return;
2160
- }
2320
+ }
2321
+
2322
+ if (method === "GET" && requestUrl.pathname === "/oauth/start") {
2323
+ const profileName = requestUrl.searchParams.get("profile") || "";
2324
+ const teamId = requestUrl.searchParams.get("team") || process.env.SLACK_OAUTH_TEAM_ID || "";
2325
+ const botScopes = parseScopesFromQuery(
2326
+ requestUrl.searchParams,
2327
+ "scope",
2328
+ DEFAULT_OAUTH_BOT_SCOPES
2329
+ );
2330
+ const userScopes = parseScopesFromQuery(
2331
+ requestUrl.searchParams,
2332
+ "user_scope",
2333
+ DEFAULT_OAUTH_USER_SCOPES
2334
+ );
2335
+
2336
+ if (botScopes.length === 0 && userScopes.length === 0) {
2337
+ sendJson(res, 400, { ok: false, error: "missing_scope" });
2338
+ return;
2339
+ }
2161
2340
 
2162
- if (method === "GET" && requestUrl.pathname === callbackPath) {
2163
- const receivedError = requestUrl.searchParams.get("error");
2164
- if (receivedError) {
2165
- sendText(res, 400, `Slack OAuth failed: ${receivedError}`);
2341
+ const state = crypto.randomBytes(24).toString("hex");
2342
+ pendingStates.set(state, {
2343
+ created_at: Date.now(),
2344
+ profile_name: profileName,
2345
+ team_id: teamId,
2346
+ bot_scopes: botScopes,
2347
+ user_scopes: userScopes,
2348
+ });
2349
+
2350
+ const authorizeUrl = buildOauthAuthorizeUrl({
2351
+ clientId,
2352
+ state,
2353
+ redirectUri,
2354
+ botScopes,
2355
+ userScopes,
2356
+ teamId,
2357
+ });
2358
+
2359
+ res.writeHead(302, { Location: authorizeUrl });
2360
+ res.end();
2166
2361
  return;
2167
2362
  }
2168
2363
 
2364
+ if (method === "GET" && requestUrl.pathname === callbackPath) {
2365
+ const receivedError = requestUrl.searchParams.get("error");
2366
+ if (receivedError) {
2367
+ sendText(res, 400, `Slack OAuth failed: ${receivedError}`);
2368
+ return;
2369
+ }
2370
+
2169
2371
  const state = requestUrl.searchParams.get("state");
2170
2372
  const code = requestUrl.searchParams.get("code");
2171
- if (!state || !code) {
2172
- sendText(res, 400, "Missing state/code.");
2173
- return;
2174
- }
2373
+ if (!state || !code) {
2374
+ sendText(res, 400, "Missing state/code.");
2375
+ return;
2376
+ }
2175
2377
 
2176
- const claimToken = stateToClaim.get(state);
2177
- if (!claimToken) {
2178
- sendText(res, 400, "Invalid OAuth state.");
2179
- return;
2180
- }
2378
+ const claimToken = stateToClaim.get(state);
2379
+ if (claimToken) {
2380
+ const session = claimSessions.get(claimToken);
2381
+ if (!session || isClaimSessionExpired(session)) {
2382
+ sendText(res, 400, "Expired onboarding claim.");
2383
+ return;
2384
+ }
2385
+
2386
+ const oauthResponse = await exchangeOauthCode({ clientId, clientSecret, code, redirectUri });
2387
+ session.oauth_response = oauthResponse;
2388
+ stateToClaim.delete(state);
2389
+ sendText(res, 200, "Slack OAuth completed. Return to your CLI and wait for onboarding to finish.");
2390
+ return;
2391
+ }
2181
2392
 
2182
- const session = claimSessions.get(claimToken);
2183
- if (!session || isClaimSessionExpired(session)) {
2184
- sendText(res, 400, "Expired onboarding claim.");
2393
+ const pending = pendingStates.get(state);
2394
+ pendingStates.delete(state);
2395
+ if (!pending) {
2396
+ sendText(res, 400, "Invalid or expired OAuth state.");
2397
+ return;
2398
+ }
2399
+ if (Date.now() - pending.created_at > GATEWAY_STATE_TTL_MS) {
2400
+ sendText(res, 400, "Expired OAuth state.");
2401
+ return;
2402
+ }
2403
+
2404
+ const oauthResponse = await exchangeOauthCode({ clientId, clientSecret, code, redirectUri });
2405
+ const { key, profile } = upsertOauthProfile(oauthResponse, pending.profile_name);
2406
+ sendText(
2407
+ res,
2408
+ 200,
2409
+ [
2410
+ "Slack OAuth authorization completed.",
2411
+ `Saved profile: ${profile.profile_name || key}`,
2412
+ `Profile key: ${key}`,
2413
+ "You can close this tab.",
2414
+ ].join("\n")
2415
+ );
2185
2416
  return;
2186
2417
  }
2187
2418
 
2188
- const oauthResponse = await exchangeOauthCode({ clientId, clientSecret, code, redirectUri });
2189
- session.oauth_response = oauthResponse;
2190
- stateToClaim.delete(state);
2191
- sendText(res, 200, "Slack OAuth completed. Return to your CLI and wait for onboarding to finish.");
2192
- return;
2193
- }
2194
-
2195
- if (method === "GET" && requestUrl.pathname === "/onboard/claim") {
2419
+ if (method === "GET" && requestUrl.pathname === "/onboard/claim") {
2196
2420
  const claimToken = String(requestUrl.searchParams.get("claim") || "");
2197
2421
  const session = claimSessions.get(claimToken);
2198
2422
  if (!session || isClaimSessionExpired(session)) {
@@ -2211,26 +2435,114 @@ async function runOnboardServerStart(args) {
2211
2435
  profile: session.profile || "",
2212
2436
  oauth_response: session.oauth_response,
2213
2437
  });
2214
- claimSessions.delete(claimToken);
2215
- return;
2216
- }
2438
+ claimSessions.delete(claimToken);
2439
+ return;
2440
+ }
2217
2441
 
2218
- sendJson(res, 404, { ok: false, error: "not_found" });
2219
- } catch (error) {
2220
- sendJson(res, 500, {
2221
- ok: false,
2222
- error: error instanceof Error ? error.message : String(error),
2223
- });
2224
- }
2225
- });
2442
+ if (method === "GET" && requestUrl.pathname === "/oauth/link") {
2443
+ const params = new URLSearchParams();
2444
+ const profile = requestUrl.searchParams.get("profile") || "";
2445
+ const team = requestUrl.searchParams.get("team") || "";
2446
+ const scope = requestUrl.searchParams.get("scope") || "";
2447
+ const userScope = requestUrl.searchParams.get("user_scope") || "";
2448
+ if (profile) params.set("profile", profile);
2449
+ if (team) params.set("team", team);
2450
+ if (scope) params.set("scope", scope);
2451
+ if (userScope) params.set("user_scope", userScope);
2452
+ sendJson(res, 200, {
2453
+ ok: true,
2454
+ url: `${publicBaseUrl}/oauth/start${params.toString() ? `?${params.toString()}` : ""}`,
2455
+ });
2456
+ return;
2457
+ }
2458
+
2459
+ if (method === "GET" && requestUrl.pathname === "/profiles") {
2460
+ if (!isGatewayAuthorized(req)) {
2461
+ sendJson(res, 401, { ok: false, error: "unauthorized" });
2462
+ return;
2463
+ }
2464
+ const tokenStore = loadTokenStore();
2465
+ sendJson(res, 200, {
2466
+ ok: true,
2467
+ default_profile: tokenStore.default_profile,
2468
+ profiles: profileSummariesFromStore(tokenStore),
2469
+ });
2470
+ return;
2471
+ }
2472
+
2473
+ if (method === "POST" && requestUrl.pathname === "/api/slack/call") {
2474
+ if (!isGatewayAuthorized(req)) {
2475
+ sendJson(res, 401, { ok: false, error: "unauthorized" });
2476
+ return;
2477
+ }
2478
+
2479
+ const payload = await readRequestJson(req, 1024 * 1024);
2480
+ const methodName = payload.method;
2481
+ if (!methodName || typeof methodName !== "string") {
2482
+ sendJson(res, 400, { ok: false, error: "missing_method" });
2483
+ return;
2484
+ }
2485
+
2486
+ const candidates = getSlackTokenCandidates(payload.token_override, {
2487
+ profileSelector:
2488
+ payload.profile_selector || process.env.SLACK_PROFILE || GATEWAY_PROFILE || undefined,
2489
+ preferredTokenType:
2490
+ payload.preferred_token_type || process.env.SLACK_DEFAULT_TOKEN_TYPE || undefined,
2491
+ });
2492
+ if (candidates.length === 0) {
2493
+ sendJson(res, 400, { ok: false, error: "missing_token" });
2494
+ return;
2495
+ }
2496
+
2497
+ const data = await callSlackApiWithCandidates(methodName, payload.params || {}, candidates);
2498
+ sendJson(res, 200, { ok: true, data });
2499
+ return;
2500
+ }
2501
+
2502
+ if (method === "POST" && requestUrl.pathname === "/api/slack/http") {
2503
+ if (!isGatewayAuthorized(req)) {
2504
+ sendJson(res, 401, { ok: false, error: "unauthorized" });
2505
+ return;
2506
+ }
2507
+
2508
+ const payload = await readRequestJson(req, 1024 * 1024);
2509
+ if (!payload.url || typeof payload.url !== "string") {
2510
+ sendJson(res, 400, { ok: false, error: "missing_url" });
2511
+ return;
2512
+ }
2513
+
2514
+ const data = await proxySlackHttpRequest(payload);
2515
+ sendJson(res, 200, { ok: true, data });
2516
+ return;
2517
+ }
2518
+
2519
+ sendJson(res, 404, { ok: false, error: "not_found" });
2520
+ } catch (error) {
2521
+ sendJson(res, 500, {
2522
+ ok: false,
2523
+ error: error instanceof Error ? error.message : String(error),
2524
+ slack_error: error?.slack_error || null,
2525
+ needed: error?.needed || null,
2526
+ provided: error?.provided || null,
2527
+ token_source: error?.token_source || null,
2528
+ });
2529
+ } finally {
2530
+ for (const [state, value] of pendingStates.entries()) {
2531
+ if (Date.now() - value.created_at > GATEWAY_STATE_TTL_MS) {
2532
+ pendingStates.delete(state);
2533
+ }
2534
+ }
2535
+ }
2536
+ });
2226
2537
 
2227
2538
  await new Promise((resolve, reject) => {
2228
2539
  server.once("error", reject);
2229
2540
  server.listen(port, host, resolve);
2230
2541
  });
2231
2542
 
2232
- console.error(`[${SERVER_NAME}] onboard server listening at http://${host}:${port}`);
2233
- console.error(`[${SERVER_NAME}] public base: ${publicBaseUrl}`);
2543
+ console.error(`[${SERVER_NAME}] gateway listening at http://${host}:${port} | public_base=${publicBaseUrl}`);
2544
+ console.error(`[${SERVER_NAME}] oauth start URL: ${publicBaseUrl}/oauth/start`);
2545
+ console.error(`[${SERVER_NAME}] profile list URL: ${publicBaseUrl}/profiles`);
2234
2546
  console.error(`[${SERVER_NAME}] bootstrap URL: ${publicBaseUrl}/onboard/bootstrap`);
2235
2547
  }
2236
2548