@opengeni/api-router 0.12.2 → 0.12.7

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.
@@ -21,13 +21,13 @@ import {
21
21
  import { dbSql, getWorkspace as getWorkspace2 } from "@opengeni/db";
22
22
  import { createObservability } from "@opengeni/observability";
23
23
  import { createObjectStorage } from "@opengeni/storage";
24
- import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
24
+ import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport3 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
25
25
  import { Hono } from "hono";
26
26
  import { bodyLimit } from "hono/body-limit";
27
27
  import { cors } from "hono/cors";
28
28
  import { HTTPException as HTTPException26 } from "hono/http-exception";
29
29
  import {
30
- hasPermission as hasPermission7,
30
+ hasPermission as hasPermission10,
31
31
  requireAccessGrant as requireAccessGrant18,
32
32
  requirePermission,
33
33
  requireSessionAuthorization as requireSessionAuthorization3,
@@ -291,6 +291,8 @@ import { requireLimit as requireLimit8 } from "@opengeni/core";
291
291
  // src/mcp/server.ts
292
292
  import {
293
293
  CreateScheduledTaskRequest,
294
+ DEFAULT_FIRST_PARTY_MCP_TOOLS,
295
+ FIRST_PARTY_MCP_TOOL_NAMES,
294
296
  defaultRepositoryMountPath,
295
297
  SESSION_EVENT_RAW_DELTA_TYPES,
296
298
  SessionEventLatestClass,
@@ -335,7 +337,6 @@ import {
335
337
  MEMORY_CORRECT_TOOL_DESCRIPTION,
336
338
  MEMORY_SAVE_TOOL_DESCRIPTION,
337
339
  MEMORY_SEARCH_TOOL_DESCRIPTION,
338
- requireFile,
339
340
  requireScheduledTask,
340
341
  requireSession,
341
342
  saveWorkspaceMemory,
@@ -356,7 +357,9 @@ import {
356
357
  GitHubAppConfigurationError,
357
358
  githubAppMissingSettings
358
359
  } from "@opengeni/github";
359
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
360
+ import {
361
+ McpServer
362
+ } from "@modelcontextprotocol/sdk/server/mcp.js";
360
363
  import * as z4 from "zod/v4";
361
364
  import {
362
365
  hasPermission as hasPermission2,
@@ -2019,7 +2022,8 @@ import {
2019
2022
  completeSlackBotPostOperation,
2020
2023
  getSession,
2021
2024
  recordAuditEvent,
2022
- releaseSlackBotPostOperationClaim
2025
+ releaseSlackBotPostOperationClaim,
2026
+ setConnectionStatus
2023
2027
  } from "@opengeni/db";
2024
2028
  import { readResponseJsonBounded } from "@opengeni/network";
2025
2029
  import { HTTPException as HTTPException2 } from "hono/http-exception";
@@ -2031,6 +2035,46 @@ var MAX_HISTORY_PAGE = 100;
2031
2035
  var MAX_USER_PAGE = 200;
2032
2036
  var MAX_PROJECTED_TEXT = 4e3;
2033
2037
  var SLACK_POST_CLAIM_LEASE_MS = 3e4;
2038
+ async function exchangeOpenGeniSlackAuthorizationCode(input, fetchImpl = fetch) {
2039
+ const body = new URLSearchParams({
2040
+ code: input.code,
2041
+ client_id: input.clientId,
2042
+ client_secret: input.clientSecret,
2043
+ redirect_uri: input.redirectUri
2044
+ });
2045
+ let response;
2046
+ try {
2047
+ response = await fetchImpl(`${SLACK_API_BASE}oauth.v2.access`, {
2048
+ method: "POST",
2049
+ headers: {
2050
+ accept: "application/json",
2051
+ "content-type": "application/x-www-form-urlencoded"
2052
+ },
2053
+ body: body.toString(),
2054
+ redirect: "error",
2055
+ signal: AbortSignal.timeout(SLACK_TIMEOUT_MS)
2056
+ });
2057
+ } catch {
2058
+ throw new HTTPException2(502, { message: "Slack installation token exchange failed" });
2059
+ }
2060
+ if (!response.ok) {
2061
+ throw new HTTPException2(502, { message: "Slack installation token exchange failed" });
2062
+ }
2063
+ const payload = await readResponseJsonBounded(
2064
+ response,
2065
+ SLACK_RESPONSE_MAX_BYTES,
2066
+ "Slack OAuth response"
2067
+ );
2068
+ const record3 = slackRecord(payload);
2069
+ if (!record3 || record3.ok !== true) {
2070
+ throw new SlackBotProviderError(slackString(record3?.error) || "oauth_exchange_failed");
2071
+ }
2072
+ const accessToken = slackString(record3.access_token);
2073
+ if (!accessToken?.startsWith("xoxb-")) {
2074
+ throw new HTTPException2(502, { message: "Slack installation did not return a bot token" });
2075
+ }
2076
+ return accessToken;
2077
+ }
2034
2078
  var SlackBotProviderError = class extends Error {
2035
2079
  constructor(code) {
2036
2080
  super(`Slack bot request failed: ${safeSlackCode(code)}`);
@@ -2038,6 +2082,23 @@ var SlackBotProviderError = class extends Error {
2038
2082
  this.name = "SlackBotProviderError";
2039
2083
  }
2040
2084
  };
2085
+ var SLACK_CREDENTIAL_REJECTION_CODES = /* @__PURE__ */ new Set([
2086
+ "account_inactive",
2087
+ "invalid_auth",
2088
+ "not_authed",
2089
+ "token_expired",
2090
+ "token_revoked"
2091
+ ]);
2092
+ function slackCredentialRejected(error) {
2093
+ return error instanceof SlackBotProviderError && SLACK_CREDENTIAL_REJECTION_CODES.has(error.code);
2094
+ }
2095
+ var SlackBotCredentialVerificationError = class extends HTTPException2 {
2096
+ constructor(failureReason, message) {
2097
+ super(422, { message });
2098
+ this.failureReason = failureReason;
2099
+ this.name = "SlackBotCredentialVerificationError";
2100
+ }
2101
+ };
2041
2102
  async function verifyOpenGeniSlackBotCredential(token, fetchImpl = fetch, now = /* @__PURE__ */ new Date()) {
2042
2103
  const authResponse = await slackApiFetch(fetchImpl, "auth.test", token, {});
2043
2104
  const grantedScopes2 = parseGrantedScopes(authResponse.response.headers.get("x-oauth-scopes"));
@@ -2052,14 +2113,18 @@ async function verifyOpenGeniSlackBotCredential(token, fetchImpl = fetch, now =
2052
2113
  });
2053
2114
  const user = slackRecord(userResponse.payload.user);
2054
2115
  if (!user || user.is_bot !== true || user.deleted === true) {
2055
- throw new HTTPException2(422, { message: "Slack credential must identify an active bot user" });
2116
+ throw new SlackBotCredentialVerificationError(
2117
+ "identity_mismatch",
2118
+ "Slack credential must identify an active bot user"
2119
+ );
2056
2120
  }
2057
2121
  const profile = slackRecord(user.profile);
2058
2122
  const displayName = slackString(profile?.display_name) || slackString(profile?.real_name);
2059
2123
  if (displayName !== "OpenGeni") {
2060
- throw new HTTPException2(422, {
2061
- message: 'Slack bot display name must be exactly "OpenGeni"'
2062
- });
2124
+ throw new SlackBotCredentialVerificationError(
2125
+ "identity_mismatch",
2126
+ 'Slack bot display name must be exactly "OpenGeni"'
2127
+ );
2063
2128
  }
2064
2129
  return {
2065
2130
  grantedScopes: grantedScopes2,
@@ -2270,7 +2335,18 @@ var OpenGeniSlackBotClient = class {
2270
2335
  return projected;
2271
2336
  }
2272
2337
  async call(headers, method, params) {
2273
- return (await slackApiFetchWithHeaders(this.fetchImpl, method, headers, params)).payload;
2338
+ try {
2339
+ return (await slackApiFetchWithHeaders(this.fetchImpl, method, headers, params)).payload;
2340
+ } catch (error) {
2341
+ if (slackCredentialRejected(error)) {
2342
+ await setConnectionStatus(this.db, this.context.workspaceId, "needs_reauth", error.code, {
2343
+ id: this.connection.id,
2344
+ version: this.connection.version,
2345
+ subjectId: null
2346
+ }).catch(() => false);
2347
+ }
2348
+ throw error;
2349
+ }
2274
2350
  }
2275
2351
  async withAudit(operation, run) {
2276
2352
  try {
@@ -2426,14 +2502,18 @@ function assertExactOpenGeniSlackBotScopes(grantedScopes2) {
2426
2502
  ...forbidden.length ? [`forbidden: ${forbidden.join(", ")}`] : [],
2427
2503
  ...unsupported.length ? [`unsupported: ${unsupported.join(", ")}`] : []
2428
2504
  ];
2429
- throw new HTTPException2(422, {
2430
- message: `Slack bot scopes must exactly match the OpenGeni manifest (${facts.join("; ")})`
2431
- });
2505
+ throw new SlackBotCredentialVerificationError(
2506
+ "scope_mismatch",
2507
+ `Slack bot scopes must exactly match the OpenGeni manifest (${facts.join("; ")})`
2508
+ );
2432
2509
  }
2433
2510
  }
2434
2511
  function parseGrantedScopes(header) {
2435
2512
  if (!header) {
2436
- throw new HTTPException2(422, { message: "Slack did not report granted bot scopes" });
2513
+ throw new SlackBotCredentialVerificationError(
2514
+ "scope_mismatch",
2515
+ "Slack did not report granted bot scopes"
2516
+ );
2437
2517
  }
2438
2518
  return [
2439
2519
  ...new Set(
@@ -2530,17 +2610,124 @@ function slackPostOutcomeMayBeAmbiguous(error) {
2530
2610
  }
2531
2611
 
2532
2612
  // src/mcp/server.ts
2613
+ var FIRST_PARTY_TOOL_AUTHORIZATION = {
2614
+ set_session_title: { sessionRequired: true, allOf: ["sessions:control"] },
2615
+ goal_set: { sessionRequired: true, allOf: ["goals:manage"] },
2616
+ goal_update: { sessionRequired: true, allOf: ["goals:manage"] },
2617
+ goal_complete: { sessionRequired: true, allOf: ["goals:manage"] },
2618
+ goal_pause: { sessionRequired: true, allOf: ["goals:manage"] },
2619
+ memory_search: { sessionRequired: true, allOf: ["documents:search"] },
2620
+ memory_save: { sessionRequired: true, allOf: ["documents:search"] },
2621
+ memory_correct: { sessionRequired: true, allOf: ["documents:search"] },
2622
+ sandboxes_list: { sessionRequired: true, allOf: ["sessions:read"] },
2623
+ sandbox_attach: { sessionRequired: true, allOf: ["sessions:control"] },
2624
+ sandbox_swap: { sessionRequired: true, allOf: ["sessions:control"] },
2625
+ run_on: { sessionRequired: true, allOf: ["sessions:control"] },
2626
+ sandbox_provision: { sessionRequired: true, allOf: ["sessions:control"] },
2627
+ rig_list: { allOf: ["rigs:use"] },
2628
+ rig_get: { allOf: ["rigs:use"] },
2629
+ rig_propose_change: { allOf: ["rigs:use"] },
2630
+ rig_verify: { allOf: ["rigs:use"] },
2631
+ rig_promote: { allOf: ["rigs:manage"] },
2632
+ sessions_list: { allOf: ["sessions:read"] },
2633
+ session_get: { allOf: ["sessions:read"] },
2634
+ session_events: { allOf: ["sessions:read"] },
2635
+ session_create: { allOf: ["sessions:create"] },
2636
+ session_send_message: { allOf: ["sessions:control"] },
2637
+ session_pause: { allOf: ["sessions:control"] },
2638
+ session_resume: { allOf: ["sessions:control"] },
2639
+ session_steer: { sessionRequired: true, allOf: ["sessions:control"] },
2640
+ set_other_session_title: { allOf: ["sessions:control"] },
2641
+ variable_set_list: { allOf: ["variable-sets:use"] },
2642
+ environment_list: { allOf: ["variable-sets:use"] },
2643
+ variable_set_set_variable: { allOf: ["variable-sets:manage"] },
2644
+ environment_set_variable: { allOf: ["variable-sets:manage"] },
2645
+ github_connect_link: { allOf: ["github:use"] },
2646
+ github_token: { sessionRequired: true, allOf: ["github:use"] },
2647
+ github_repositories_list: { allOf: ["github:use"] },
2648
+ social_connections_list: { allOf: ["connections:read"] },
2649
+ social_posts_recent: { allOf: ["connections:read"] },
2650
+ social_daily_analysis_context: { allOf: ["connections:read"] },
2651
+ scheduled_tasks_list: { anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"] },
2652
+ scheduled_tasks_get: { anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"] },
2653
+ scheduled_tasks_create: { allOf: ["scheduled_tasks:manage"] },
2654
+ scheduled_tasks_update: { allOf: ["scheduled_tasks:manage"] },
2655
+ scheduled_tasks_pause: { allOf: ["scheduled_tasks:manage"] },
2656
+ scheduled_tasks_resume: { allOf: ["scheduled_tasks:manage"] },
2657
+ scheduled_tasks_trigger: { allOf: ["scheduled_tasks:run"] },
2658
+ scheduled_tasks_delete: { allOf: ["scheduled_tasks:manage"] },
2659
+ scheduled_task_runs_list: { anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"] },
2660
+ slack_bot_list_channels: { allOf: ["connections:read"] },
2661
+ slack_bot_channel_history: { allOf: ["connections:read"] },
2662
+ slack_bot_list_users: { allOf: ["connections:read"] },
2663
+ slack_bot_post_message: { allOf: ["connections:read"] }
2664
+ };
2665
+ var FIRST_PARTY_MCP_TOOL_NAME_SET = new Set(FIRST_PARTY_MCP_TOOL_NAMES);
2666
+ var PolicyMcpServer = class extends McpServer {
2667
+ constructor(grant, sessionId, selectedTools, allowUncatalogued) {
2668
+ super({ name: "opengeni", version: "1.0.0" });
2669
+ this.grant = grant;
2670
+ this.sessionId = sessionId;
2671
+ this.selectedTools = selectedTools;
2672
+ this.allowUncatalogued = allowUncatalogued;
2673
+ }
2674
+ registeredToolCount = 0;
2675
+ registerTool(name, config, cb) {
2676
+ const catalogued = FIRST_PARTY_MCP_TOOL_NAME_SET.has(name);
2677
+ let admitted = this.allowUncatalogued && !catalogued;
2678
+ if (catalogued) {
2679
+ const toolName = name;
2680
+ const policy = FIRST_PARTY_TOOL_AUTHORIZATION[toolName];
2681
+ const authorized = (!policy.sessionRequired || this.sessionId !== null) && (policy.allOf?.every((permission) => hasPermission2(this.grant.permissions, permission)) ?? true) && (policy.anyOf?.some((permission) => hasPermission2(this.grant.permissions, permission)) ?? true);
2682
+ const selected = this.selectedTools === null || this.selectedTools.has(toolName);
2683
+ admitted = authorized && selected;
2684
+ }
2685
+ if (!admitted) {
2686
+ return {
2687
+ ...config.title ? { title: config.title } : {},
2688
+ ...config.description ? { description: config.description } : {},
2689
+ ...config.annotations ? { annotations: config.annotations } : {},
2690
+ ...config._meta ? { _meta: config._meta } : {},
2691
+ handler: cb,
2692
+ enabled: false,
2693
+ enable() {
2694
+ },
2695
+ disable() {
2696
+ },
2697
+ update() {
2698
+ },
2699
+ remove() {
2700
+ }
2701
+ };
2702
+ }
2703
+ this.registeredToolCount += 1;
2704
+ return super.registerTool(name, config, cb);
2705
+ }
2706
+ ensureToolsListHandler() {
2707
+ if (this.registeredToolCount > 0) return;
2708
+ super.registerTool(
2709
+ "__opengeni_empty_first_party_surface__",
2710
+ {
2711
+ description: "Internal disabled placeholder for an empty first-party surface.",
2712
+ inputSchema: z4.object({})
2713
+ },
2714
+ async () => ({
2715
+ content: [{ type: "text", text: '{"unavailable":true}' }]
2716
+ })
2717
+ ).disable();
2718
+ }
2719
+ };
2533
2720
  function buildOpenGeniMcpServer(deps, grant, options = {}) {
2534
- const server = new McpServer({
2535
- name: "opengeni",
2536
- version: "1.0.0"
2537
- });
2538
2721
  const json = (value) => ({
2539
2722
  content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
2540
2723
  });
2541
2724
  const can = (permission) => hasPermission2(grant.permissions, permission);
2542
2725
  const toolspaceMode = options.toolspace != null;
2543
2726
  const sessionId = typeof grant.metadata?.["sessionId"] === "string" ? grant.metadata["sessionId"] : null;
2727
+ const selectedTools = sessionId !== null && !toolspaceMode ? new Set(
2728
+ grant.metadata?.["firstPartyMcpTools"] ?? DEFAULT_FIRST_PARTY_MCP_TOOLS
2729
+ ) : null;
2730
+ const server = new PolicyMcpServer(grant, sessionId, selectedTools, toolspaceMode);
2544
2731
  if (sessionId !== null && (!toolspaceMode || can("sessions:control"))) {
2545
2732
  server.registerTool(
2546
2733
  "set_session_title",
@@ -2580,44 +2767,6 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
2580
2767
  registerGitHubTokenTool(server, deps, grant, sessionId, json);
2581
2768
  }
2582
2769
  }
2583
- if (!toolspaceMode || can("files:read")) {
2584
- server.registerTool(
2585
- "files_get_download_url",
2586
- {
2587
- description: "Create a short-lived download URL for a ready file asset.",
2588
- inputSchema: { fileId: z4.string().uuid() }
2589
- },
2590
- async ({ fileId }) => {
2591
- if (!deps.objectStorage) {
2592
- throw new Error("object storage is not configured");
2593
- }
2594
- const file = await requireFile(deps.db, grant.workspaceId, fileId);
2595
- if (file.status !== "ready") {
2596
- throw new Error(`file is ${file.status}`);
2597
- }
2598
- const signed = await deps.objectStorage.createGetUrl({
2599
- key: file.objectKey
2600
- });
2601
- return json({
2602
- file: {
2603
- id: file.id,
2604
- filename: file.filename,
2605
- safeFilename: file.safeFilename,
2606
- contentType: file.contentType,
2607
- sizeBytes: file.sizeBytes,
2608
- sha256: file.sha256,
2609
- status: file.status,
2610
- createdAt: file.createdAt,
2611
- updatedAt: file.updatedAt
2612
- },
2613
- downloadUrl: {
2614
- url: signed.url,
2615
- expiresAt: signed.expiresAt.toISOString()
2616
- }
2617
- });
2618
- }
2619
- );
2620
- }
2621
2770
  if (!toolspaceMode || can("github:use")) {
2622
2771
  server.registerTool(
2623
2772
  "github_repositories_list",
@@ -2908,7 +3057,8 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
2908
3057
  await deps.workflowClient.triggerScheduledTask({
2909
3058
  task,
2910
3059
  agentRunUsageIdempotencyKey,
2911
- triggerWorkflowId
3060
+ triggerWorkflowId,
3061
+ initiator: { kind: "subject", subjectId: grant.subjectId }
2912
3062
  });
2913
3063
  await recordWorkspaceUsage(deps, {
2914
3064
  accountId: grant.accountId,
@@ -2954,6 +3104,7 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
2954
3104
  );
2955
3105
  }
2956
3106
  registerToolspaceProxyTools(server, options.toolspace ?? null);
3107
+ server.ensureToolsListHandler();
2957
3108
  return server;
2958
3109
  }
2959
3110
  function registerSlackBotTools(server, deps, grant, sessionId, json) {
@@ -3819,6 +3970,9 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
3819
3970
  firstPartyMcpPermissions: z4.array(z4.string()).optional().describe(
3820
3971
  "Optional first-party capability set for the child. Omit to inherit this session's effective permissions. An explicit set may only narrow capabilities held by this session. A goal-bearing child requires goals:manage in the resulting set; creation fails rather than adding it implicitly."
3821
3972
  ),
3973
+ firstPartyMcpTools: z4.array(z4.enum(FIRST_PARTY_MCP_TOOL_NAMES)).optional().describe(
3974
+ "Exact model-visible first-party tool selection for the child. Omit to inherit this session's effective selection. This does not grant permissions."
3975
+ ),
3822
3976
  // Shared-sandbox placement (addendum 05 §D). OMIT (default) to SHARE the
3823
3977
  // creator's box — one filesystem/repo/desktop, N independent conversations;
3824
3978
  // this is the SAFE DEFAULT. Pass "new" for a fresh isolated box (a different
@@ -3872,7 +4026,7 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
3872
4026
  server.registerTool(
3873
4027
  "session_send_message",
3874
4028
  {
3875
- description: "Send information to another session. From an OpenGeni worker this becomes a coalescible internal update, never a visible prompt-queue row; pending updates are delivered together on the target's next inference. A sessionless operator call appends one visible prompt.",
4029
+ description: "Send information to another session. From an OpenGeni worker this becomes a canonical coalescible machine input: it appears in the target's compact incoming queue group, is durably added to model history when claimed, and remains visible in the timeline. A sessionless operator call appends one human/API prompt.",
3876
4030
  inputSchema: {
3877
4031
  sessionId: z4.string().uuid(),
3878
4032
  text: z4.string().min(1),
@@ -4052,7 +4206,7 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
4052
4206
  server.registerTool(
4053
4207
  "session_steer",
4054
4208
  {
4055
- description: "Atomically replace another session's current direction and resume it. The instruction is an internal update, never a human queue row.",
4209
+ description: "Atomically replace another session's current direction and resume it. The Agent Steer is a typed machine input shown in the target queue/timeline and durably retained in model history; it never impersonates a human prompt.",
4056
4210
  inputSchema: {
4057
4211
  sessionId: z4.string().uuid(),
4058
4212
  instruction: z4.string().min(1),
@@ -5290,7 +5444,15 @@ function connectionBrokerFetch(baseFetch, input) {
5290
5444
  if (!connectionRef) {
5291
5445
  return baseFetch;
5292
5446
  }
5293
- const resolveCredential = input.deps.connectionCredentials?.mcpCredentials ? buildHostConnectionTokenResolver(input.deps.connectionCredentials.mcpCredentials, {
5447
+ const credentialSubjectId = input.turn.initiator.kind === "subject" ? input.turn.initiator.subjectId : void 0;
5448
+ if (connectionRef.subjectScope === "subject" && !credentialSubjectId) {
5449
+ throw new Error(
5450
+ `subject-owned connection for MCP server ${input.config.id} requires a human turn initiator`
5451
+ );
5452
+ }
5453
+ const hostCredentialPort = input.deps.connectionCredentials?.mcpCredentials;
5454
+ const resolverSubjectId = hostCredentialPort ? input.grant.subjectId : credentialSubjectId;
5455
+ const resolveCredential = hostCredentialPort ? buildHostConnectionTokenResolver(hostCredentialPort, {
5294
5456
  accountId: input.grant.accountId,
5295
5457
  workspaceId: input.grant.workspaceId,
5296
5458
  sessionId: input.sessionId,
@@ -5312,7 +5474,7 @@ function connectionBrokerFetch(baseFetch, input) {
5312
5474
  destinationUrl,
5313
5475
  forceRefresh: false,
5314
5476
  ...request.toolName ? { toolName: request.toolName } : {},
5315
- subjectId: input.grant.subjectId
5477
+ ...resolverSubjectId ? { subjectId: resolverSubjectId } : {}
5316
5478
  });
5317
5479
  if (first.status === "auth_needed") {
5318
5480
  return await authNeededFetchResponse(input, request, first);
@@ -5330,7 +5492,7 @@ function connectionBrokerFetch(baseFetch, input) {
5330
5492
  destinationUrl,
5331
5493
  forceRefresh: true,
5332
5494
  ...request.toolName ? { toolName: request.toolName } : {},
5333
- subjectId: input.grant.subjectId
5495
+ ...resolverSubjectId ? { subjectId: resolverSubjectId } : {}
5334
5496
  });
5335
5497
  if (refreshed.status === "auth_needed") {
5336
5498
  return await authNeededFetchResponse(input, request, refreshed);
@@ -5614,7 +5776,7 @@ function isAuthExempt(c, settings) {
5614
5776
  if (path === "/v1/github/setup" || path === "/v1/github/install/callback" || path === "/v1/github/oauth/callback" || path === "/v1/github/app-manifest/callback") {
5615
5777
  return true;
5616
5778
  }
5617
- if (path === "/v1/integrations/oauth/callback" || path === "/v1/integrations/oauth/client-metadata.json") {
5779
+ if (path === "/v1/integrations/oauth/callback" || path === "/v1/integrations/oauth/client-metadata.json" || path === "/v1/integrations/slack/callback") {
5618
5780
  return true;
5619
5781
  }
5620
5782
  if (path.startsWith("/v1/catalog-assets/")) {
@@ -5796,8 +5958,8 @@ function registerCatalogAssetRoutes(app, deps) {
5796
5958
  if (!contentType) {
5797
5959
  throw new HTTPException5(404, { message: "asset not found" });
5798
5960
  }
5799
- const object4 = await objectStorage.getObjectBytes(key);
5800
- if (!object4) {
5961
+ const object5 = await objectStorage.getObjectBytes(key);
5962
+ if (!object5) {
5801
5963
  throw new HTTPException5(404, { message: "asset not found" });
5802
5964
  }
5803
5965
  const etag = etagForKey(key);
@@ -5810,7 +5972,7 @@ function registerCatalogAssetRoutes(app, deps) {
5810
5972
  if (ifNoneMatchSatisfied(c.req.header("if-none-match"), etag)) {
5811
5973
  return c.body(null, 304, headers);
5812
5974
  }
5813
- return c.body(new Uint8Array(object4.bytes), 200, { ...headers, "Content-Type": contentType });
5975
+ return c.body(new Uint8Array(object5.bytes), 200, { ...headers, "Content-Type": contentType });
5814
5976
  });
5815
5977
  }
5816
5978
  function catalogAssetKeyFromPath(pathname) {
@@ -7032,17 +7194,20 @@ function registerCodexRoutes(app, deps) {
7032
7194
  }
7033
7195
 
7034
7196
  // src/routes/connections.ts
7197
+ import { createHash as createHash4 } from "crypto";
7035
7198
  import {
7036
- ConnectOpenGeniSlackBotRequest,
7037
7199
  ConnectionResponse,
7038
7200
  CreateConnectionRequest,
7039
7201
  IntegrationClientMetadata,
7040
7202
  ListConnectionsResponse,
7203
+ OpenGeniSlackBotInstallRequest,
7204
+ OpenGeniSlackBotInstallStart,
7041
7205
  OAuthStartRequest,
7042
7206
  OAuthStartResponse as OAuthStartResponse2,
7043
7207
  UpdateConnectionRequest
7044
7208
  } from "@opengeni/contracts";
7045
7209
  import {
7210
+ hasPermission as hasPermission6,
7046
7211
  hasReservedOpenGeniSlackBotMetadata,
7047
7212
  isOpenGeniSlackBotConnection,
7048
7213
  openGeniSlackBotMetadata as openGeniSlackBotMetadata2,
@@ -7050,13 +7215,19 @@ import {
7050
7215
  requireEnvironmentEncryption as requireEnvironmentEncryption2
7051
7216
  } from "@opengeni/core";
7052
7217
  import {
7218
+ consumeIntegrationOAuthStateNonce as consumeIntegrationOAuthStateNonce2,
7053
7219
  createConnection as createConnection2,
7220
+ createConnectionWithSlackBotSuccessAudit,
7054
7221
  encryptEnvironmentValue as encryptEnvironmentValue3,
7055
7222
  getConnectionMetadata as getConnectionMetadata2,
7223
+ getWorkspaceGrant as getWorkspaceGrant2,
7056
7224
  listConnectionsMetadata as listConnectionsMetadata2,
7057
- recordAuditEvent as recordAuditEvent2,
7225
+ recordSlackBotInstallCallbackFailure,
7058
7226
  revokeConnection,
7059
- updateConnection as updateConnection2
7227
+ revokeConnectionWithSlackBotSuccessAudit,
7228
+ SlackBotLifecycleSuccessAuditError,
7229
+ updateConnection as updateConnection2,
7230
+ updateConnectionWithSlackBotSuccessAudit
7060
7231
  } from "@opengeni/db";
7061
7232
  import { HTTPException as HTTPException9 } from "hono/http-exception";
7062
7233
 
@@ -7065,13 +7236,14 @@ import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
7065
7236
  import { StreamableHTTPClientTransport as StreamableHTTPClientTransport2 } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
7066
7237
  import { parseIntegrationsOauthClientsJson } from "@opengeni/config";
7067
7238
  import { OAuthStartResponse } from "@opengeni/contracts";
7068
- import { requireEnvironmentEncryption } from "@opengeni/core";
7239
+ import { hasPermission as hasPermission5, requireEnvironmentEncryption } from "@opengeni/core";
7069
7240
  import {
7070
7241
  consumeIntegrationOAuthStateNonce,
7071
7242
  createConnection,
7072
7243
  decryptEnvironmentValue,
7073
7244
  encryptEnvironmentValue as encryptEnvironmentValue2,
7074
7245
  getConnectionMetadata,
7246
+ getWorkspaceGrant,
7075
7247
  listConnectionsMetadata,
7076
7248
  loadIntegrationOAuthClient,
7077
7249
  normalizeBearerScheme,
@@ -7104,6 +7276,8 @@ function canonicalProviderDomain(value) {
7104
7276
  // src/integrations/oauth-client.ts
7105
7277
  import { OAUTH_MAX_RESPONSE_BYTES as OAUTH_MAX_RESPONSE_BYTES2 } from "@opengeni/network";
7106
7278
  var oauthStateTtlMs = 10 * 60 * 1e3;
7279
+ var OFFICIAL_SLACK_MCP_URL = "https://mcp.slack.com/mcp";
7280
+ var SLACK_OAUTH_ORIGIN = "https://slack.com";
7107
7281
  var OAuthCallbackStageError = class extends Error {
7108
7282
  constructor(stage, reason, cause) {
7109
7283
  super(errorMessage(cause));
@@ -7116,9 +7290,10 @@ var OAuthCallbackStageError = class extends Error {
7116
7290
  async function startMcpOAuth(deps, context) {
7117
7291
  const { db, settings } = deps;
7118
7292
  const mcpUrl = canonicalMcpResource(context.payload.mcpUrl ?? context.payload.resource);
7119
- const providerDomain = canonicalProviderDomain(
7120
- context.payload.providerDomain ?? new URL(mcpUrl).hostname
7121
- );
7293
+ const officialSlackResource = mcpUrl === OFFICIAL_SLACK_MCP_URL;
7294
+ const providerDomain = officialSlackResource ? "slack.com" : canonicalProviderDomain(context.payload.providerDomain ?? new URL(mcpUrl).hostname);
7295
+ const personalSlack = officialSlackResource || providerDomain === "slack.com";
7296
+ assertPersonalSlackOAuthStart(settings, context.payload, mcpUrl, personalSlack);
7122
7297
  const returnPath = safeReturnPath(context.payload.returnPath ?? "/integrations");
7123
7298
  const baseUrl = integrationBaseUrl(settings.publicBaseUrl, context.requestUrl);
7124
7299
  const redirectUri = `${baseUrl}/v1/integrations/oauth/callback`;
@@ -7133,6 +7308,9 @@ async function startMcpOAuth(deps, context) {
7133
7308
  throw new HTTPException8(404, { message: "connection not found" });
7134
7309
  }
7135
7310
  const discovery = await discoverMcpOAuth(mcpUrl, settings);
7311
+ if (personalSlack && !isLocalTestEnvironment(settings.environment)) {
7312
+ assertSlackAuthorizationServer(discovery.as);
7313
+ }
7136
7314
  const resource = discovery.prm.resource ? canonicalOAuthResource(discovery.prm.resource) : mcpUrl;
7137
7315
  const verifier = randomPkceVerifier();
7138
7316
  const authorizeScopes = chooseAuthorizeScopes(
@@ -7200,6 +7378,7 @@ async function completeMcpOAuthCallback(deps, input) {
7200
7378
  }
7201
7379
  try {
7202
7380
  state = readOAuthState(input.state, settings);
7381
+ await requireOAuthCallbackGrant(db, state);
7203
7382
  if (!input.code) {
7204
7383
  return {
7205
7384
  redirectTo: callbackReturnPath(state.returnPath, "error", { reason: "missing_code" })
@@ -7258,6 +7437,7 @@ async function completeMcpOAuthCallback(deps, input) {
7258
7437
  ...verification.tools ? { mcpTools: verification.tools } : {}
7259
7438
  };
7260
7439
  const credentialEncrypted = encryptEnvironmentValue2(key, JSON.stringify(credential));
7440
+ await requireOAuthCallbackGrant(db, state);
7261
7441
  const connection = await runCallbackStage(
7262
7442
  "persist",
7263
7443
  "persist_failed",
@@ -7266,6 +7446,7 @@ async function completeMcpOAuthCallback(deps, input) {
7266
7446
  connectionId: state.connectionId,
7267
7447
  visibleToSubjectId: state.subjectId,
7268
7448
  expectedVersion: state.connectionVersion,
7449
+ subjectId: state.subjectId,
7269
7450
  providerDomain: state.providerDomain,
7270
7451
  kind: "oauth2",
7271
7452
  status: "active",
@@ -7277,7 +7458,7 @@ async function completeMcpOAuthCallback(deps, input) {
7277
7458
  }) : createConnection(db, {
7278
7459
  accountId: state.accountId,
7279
7460
  workspaceId: state.workspaceId,
7280
- subjectId: null,
7461
+ subjectId: state.subjectId,
7281
7462
  providerDomain: state.providerDomain,
7282
7463
  kind: "oauth2",
7283
7464
  credentialEncrypted,
@@ -7317,6 +7498,43 @@ function requireIntegrationsStateSecret(settings) {
7317
7498
  }
7318
7499
  return secret;
7319
7500
  }
7501
+ async function requireOAuthCallbackGrant(db, state) {
7502
+ const grant = await getWorkspaceGrant(db, state.subjectId, state.workspaceId);
7503
+ if (!grant || grant.accountId !== state.accountId || !hasPermission5(grant.permissions, "connections:write")) {
7504
+ throw new HTTPException8(403, {
7505
+ message: "OAuth subject no longer has permission to write this workspace connection"
7506
+ });
7507
+ }
7508
+ }
7509
+ function assertPersonalSlackOAuthStart(settings, payload, mcpUrl, personalSlack) {
7510
+ if (!personalSlack) return;
7511
+ if (payload.oauthClient) {
7512
+ throw new HTTPException8(422, {
7513
+ message: "Slack OAuth client credentials are deployment-managed"
7514
+ });
7515
+ }
7516
+ if (payload.providerDomain && canonicalProviderDomain(payload.providerDomain) !== "slack.com") {
7517
+ throw new HTTPException8(422, { message: "Slack provider identity does not match slack.com" });
7518
+ }
7519
+ if (!isLocalTestEnvironment(settings.environment) && mcpUrl !== OFFICIAL_SLACK_MCP_URL) {
7520
+ throw new HTTPException8(422, {
7521
+ message: `personal Slack OAuth must use ${OFFICIAL_SLACK_MCP_URL}`
7522
+ });
7523
+ }
7524
+ if (!settings.slackClientId?.trim() || !settings.slackClientSecret?.trim()) {
7525
+ throw new HTTPException8(503, {
7526
+ message: "personal Slack OAuth requires OPENGENI_SLACK_CLIENT_ID and OPENGENI_SLACK_CLIENT_SECRET"
7527
+ });
7528
+ }
7529
+ }
7530
+ function assertSlackAuthorizationServer(as) {
7531
+ const urls = [as.issuer, as.authorizationServer, as.authorizationEndpoint, as.tokenEndpoint];
7532
+ if (urls.some((value) => new URL(value).origin !== SLACK_OAUTH_ORIGIN)) {
7533
+ throw new HTTPException8(422, {
7534
+ message: "Slack MCP authorization metadata did not remain bound to slack.com"
7535
+ });
7536
+ }
7537
+ }
7320
7538
  async function discoverMcpOAuth(resource, settings) {
7321
7539
  const challenge = await probeMcpChallenge(resource, settings);
7322
7540
  const prm = await discoverProtectedResourceMetadata(
@@ -7553,6 +7771,14 @@ function operatorClientForAs(settings, as) {
7553
7771
  };
7554
7772
  }
7555
7773
  function operatorClientEntryFor(settings, candidates) {
7774
+ const normalizedCandidates = new Set(candidates.map(normalizedIssuerKey));
7775
+ if (normalizedCandidates.has(SLACK_OAUTH_ORIGIN) && settings.slackClientId?.trim() && settings.slackClientSecret?.trim()) {
7776
+ return {
7777
+ clientId: settings.slackClientId.trim(),
7778
+ clientSecret: settings.slackClientSecret.trim(),
7779
+ tokenEndpointAuthMethod: "client_secret_post"
7780
+ };
7781
+ }
7556
7782
  const configured = parseIntegrationsOauthClientsJson(settings.integrationsOauthClientsJson);
7557
7783
  const exactKeys = uniqueStrings(
7558
7784
  candidates.flatMap((candidate) => [candidate, normalizedIssuerKey(candidate)])
@@ -7563,7 +7789,6 @@ function operatorClientEntryFor(settings, candidates) {
7563
7789
  return entry;
7564
7790
  }
7565
7791
  }
7566
- const normalizedCandidates = new Set(candidates.map(normalizedIssuerKey));
7567
7792
  for (const [key, entry] of Object.entries(configured)) {
7568
7793
  if (normalizedCandidates.has(normalizedIssuerKey(key))) {
7569
7794
  return entry;
@@ -7624,11 +7849,17 @@ async function dynamicClientRegistration(settings, as, redirectUri, scopes) {
7624
7849
  }
7625
7850
  async function existingOAuthConnectionForStart(db, input) {
7626
7851
  if (input.connectionId) {
7627
- return await getConnectionMetadata(db, input.workspaceId, input.connectionId, input.subjectId);
7852
+ const connection = await getConnectionMetadata(
7853
+ db,
7854
+ input.workspaceId,
7855
+ input.connectionId,
7856
+ input.subjectId
7857
+ );
7858
+ return connection?.subjectId === input.subjectId && connection.kind === "oauth2" && connection.providerDomain === input.providerDomain ? connection : null;
7628
7859
  }
7629
7860
  const visible = await listConnectionsMetadata(db, input.workspaceId, input.subjectId);
7630
7861
  return visible.find(
7631
- (connection) => connection.subjectId === null && connection.kind === "oauth2" && connection.status === "active" && connection.providerDomain === input.providerDomain
7862
+ (connection) => connection.subjectId === input.subjectId && connection.kind === "oauth2" && connection.status === "active" && connection.providerDomain === input.providerDomain
7632
7863
  ) ?? null;
7633
7864
  }
7634
7865
  function buildAuthorizationUrl(input) {
@@ -7695,6 +7926,9 @@ function readOAuthState(state, settings) {
7695
7926
  };
7696
7927
  const connectionId = stringValue(payload.connectionId);
7697
7928
  const connectionVersion = numberValue(payload.connectionVersion);
7929
+ if (Boolean(connectionId) !== Boolean(connectionVersion)) {
7930
+ throw new HTTPException8(400, { message: "invalid OAuth reconnect state" });
7931
+ }
7698
7932
  return {
7699
7933
  ...parsed,
7700
7934
  ...connectionId ? { connectionId } : {},
@@ -8181,8 +8415,10 @@ function requiredString(value, field) {
8181
8415
  // src/routes/connections.ts
8182
8416
  import {
8183
8417
  OPENGENI_SLACK_BOT_CREDENTIAL_LABEL as OPENGENI_SLACK_BOT_CREDENTIAL_LABEL2,
8184
- OPENGENI_SLACK_BOT_CREDENTIAL_ROLE as OPENGENI_SLACK_BOT_CREDENTIAL_ROLE2
8418
+ OPENGENI_SLACK_BOT_CREDENTIAL_ROLE as OPENGENI_SLACK_BOT_CREDENTIAL_ROLE2,
8419
+ OPENGENI_SLACK_BOT_REQUIRED_SCOPES as OPENGENI_SLACK_BOT_REQUIRED_SCOPES2
8185
8420
  } from "@opengeni/contracts";
8421
+ import { createSignedState as createSignedState4, readSignedState as readSignedState3 } from "@opengeni/github";
8186
8422
  function registerConnectionRoutes(app, deps) {
8187
8423
  const { db, settings, observability } = deps;
8188
8424
  function assertIntegrationsEnabled() {
@@ -8206,11 +8442,13 @@ function registerConnectionRoutes(app, deps) {
8206
8442
  assertNotReservedSlackBotMetadata(payload.metadata);
8207
8443
  const key = requireEnvironmentEncryption2(settings);
8208
8444
  const subjectId = writableSubjectId(payload.subjectId, grant.subjectId);
8445
+ const providerDomain = canonicalProviderDomain(payload.providerDomain);
8446
+ assertNotDirectPersonalSlackOAuth(providerDomain, payload.kind);
8209
8447
  const connection = await createConnection2(db, {
8210
8448
  accountId: grant.accountId,
8211
8449
  workspaceId,
8212
8450
  subjectId,
8213
- providerDomain: canonicalProviderDomain(payload.providerDomain),
8451
+ providerDomain,
8214
8452
  kind: payload.kind,
8215
8453
  credentialEncrypted: encryptCredentialBundle(key, payload.credential),
8216
8454
  grantedScopes: payload.grantedScopes,
@@ -8220,19 +8458,11 @@ function registerConnectionRoutes(app, deps) {
8220
8458
  });
8221
8459
  return c.json(ConnectionResponse.parse({ connection }), 201);
8222
8460
  });
8223
- app.post("/v1/workspaces/:workspaceId/connections/slack-bot", async (c) => {
8461
+ app.post("/v1/workspaces/:workspaceId/connections/slack-bot/install", async (c) => {
8224
8462
  const workspaceId = c.req.param("workspaceId");
8225
8463
  const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:write");
8226
- const payload = ConnectOpenGeniSlackBotRequest.parse(await c.req.json());
8227
- const verified = await verifyOpenGeniSlackBotCredential(
8228
- payload.token,
8229
- deps.slackFetch ?? fetch
8230
- );
8231
- const key = requireEnvironmentEncryption2(settings);
8232
- const credentialEncrypted = encryptCredentialBundle(
8233
- key,
8234
- slackBotCredentialBundle(payload.token)
8235
- );
8464
+ const payload = OpenGeniSlackBotInstallRequest.parse(await c.req.json());
8465
+ const slack = requireOpenGeniSlackOAuthSettings(settings);
8236
8466
  const existing = payload.connectionId ? await getConnectionMetadata2(db, workspaceId, payload.connectionId, grant.subjectId) : null;
8237
8467
  if (payload.connectionId && !existing) {
8238
8468
  throw new HTTPException9(404, { message: "connection not found" });
@@ -8242,69 +8472,119 @@ function registerConnectionRoutes(app, deps) {
8242
8472
  message: "connectionId is not an OpenGeni Slack bot connection"
8243
8473
  });
8244
8474
  }
8245
- const existingMetadata = existing ? openGeniSlackBotMetadata2(existing.metadata) : null;
8246
- if (existingMetadata && existingMetadata.slackTeamId !== verified.metadata.slackTeamId) {
8247
- throw new HTTPException9(409, {
8248
- message: "a Slack bot connection can only be reinstalled for its original Slack workspace"
8249
- });
8250
- }
8251
- if (existingMetadata && (existingMetadata.botId !== verified.metadata.botId || existingMetadata.botUserId !== verified.metadata.botUserId)) {
8252
- throw new HTTPException9(409, {
8253
- message: "a different Slack bot requires a new connection and explicit scheduled-task rebinding"
8254
- });
8255
- }
8256
- const verifiedInstallAt = new Date(verified.metadata.verifiedAt);
8257
- const connection = existing ? await updateConnection2(db, {
8258
- workspaceId,
8259
- connectionId: existing.id,
8260
- visibleToSubjectId: grant.subjectId,
8261
- expectedVersion: existing.version,
8262
- subjectId: null,
8263
- providerDomain: "slack.com",
8264
- kind: "app_install",
8265
- status: "active",
8266
- credentialEncrypted,
8267
- grantedScopes: verified.grantedScopes,
8268
- expiresAt: null,
8269
- verifiedInstallAt,
8270
- verifiedInstallVersion: existing.version + 1,
8271
- metadata: verified.metadata,
8272
- updatedBySubjectId: grant.subjectId
8273
- }) : await createConnection2(db, {
8475
+ const baseUrl = integrationBaseUrl(settings.publicBaseUrl, c.req.url);
8476
+ const redirectUri = `${baseUrl}/v1/integrations/slack/callback`;
8477
+ const returnPath = `/workspaces/${workspaceId}/capabilities`;
8478
+ const state = createSignedState4(requireIntegrationsStateSecret(settings), {
8274
8479
  accountId: grant.accountId,
8275
8480
  workspaceId,
8276
- subjectId: null,
8277
- providerDomain: "slack.com",
8278
- kind: "app_install",
8279
- credentialEncrypted,
8280
- grantedScopes: verified.grantedScopes,
8281
- expiresAt: null,
8282
- verifiedInstallAt,
8283
- verifiedInstallVersion: 1,
8284
- metadata: verified.metadata,
8285
- createdBySubjectId: grant.subjectId
8481
+ subjectId: grant.subjectId,
8482
+ returnPath,
8483
+ ...existing ? { connectionId: existing.id, connectionVersion: existing.version } : {}
8286
8484
  });
8287
- if (!connection) {
8288
- throw new HTTPException9(409, {
8289
- message: "Slack bot connection changed during reinstall; retry with the current connection"
8485
+ const authorizationUrl = new URL("https://slack.com/oauth/v2/authorize");
8486
+ authorizationUrl.searchParams.set("client_id", slack.clientId);
8487
+ authorizationUrl.searchParams.set("scope", OPENGENI_SLACK_BOT_REQUIRED_SCOPES2.join(","));
8488
+ authorizationUrl.searchParams.set("redirect_uri", redirectUri);
8489
+ authorizationUrl.searchParams.set("state", state);
8490
+ return c.json(
8491
+ OpenGeniSlackBotInstallStart.parse({
8492
+ authorizationUrl: authorizationUrl.toString(),
8493
+ expiresAt: new Date(Date.now() + oauthStateTtlMs).toISOString()
8494
+ })
8495
+ );
8496
+ });
8497
+ app.get("/v1/integrations/slack/callback", async (c) => {
8498
+ const baseUrl = integrationBaseUrl(settings.publicBaseUrl, c.req.url);
8499
+ let state = null;
8500
+ let stage = "permission_check";
8501
+ try {
8502
+ state = readOpenGeniSlackInstallState(c.req.query("state"), settings);
8503
+ await requireSlackInstallCallbackGrant(db, state);
8504
+ stage = "nonce_consume";
8505
+ const consumed = await consumeIntegrationOAuthStateNonce2(db, {
8506
+ accountId: state.accountId,
8507
+ workspaceId: state.workspaceId,
8508
+ subjectId: state.subjectId,
8509
+ nonce: state.nonce,
8510
+ expiresAt: new Date(state.iat * 1e3 + oauthStateTtlMs),
8511
+ now: /* @__PURE__ */ new Date()
8290
8512
  });
8291
- }
8292
- await recordAuditEvent2(db, {
8293
- accountId: grant.accountId,
8294
- workspaceId,
8295
- subjectId: grant.subjectId,
8296
- action: existing ? "slack_bot.reinstalled" : "slack_bot.connected",
8297
- targetType: "connection",
8298
- targetId: connection.id,
8299
- metadata: {
8300
- credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE2,
8301
- credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL2,
8302
- connectionId: connection.id,
8303
- slackTeamId: verified.metadata.slackTeamId,
8304
- outcome: "succeeded"
8513
+ if (!consumed) {
8514
+ throw new SlackInstallCallbackError(
8515
+ 400,
8516
+ "state_replayed",
8517
+ "Slack installation state has already been used"
8518
+ );
8305
8519
  }
8306
- });
8307
- return c.json(ConnectionResponse.parse({ connection }), existing ? 200 : 201);
8520
+ if (c.req.query("error")) {
8521
+ stage = "provider_denial";
8522
+ throw new SlackInstallCallbackError(
8523
+ 400,
8524
+ "provider_denied",
8525
+ "Slack installation authorization was denied"
8526
+ );
8527
+ }
8528
+ stage = "code_exchange";
8529
+ const code = c.req.query("code");
8530
+ if (!code) {
8531
+ throw new SlackInstallCallbackError(
8532
+ 400,
8533
+ "missing_code",
8534
+ "Slack installation callback is missing code"
8535
+ );
8536
+ }
8537
+ const slack = requireOpenGeniSlackOAuthSettings(settings);
8538
+ const redirectUri = `${baseUrl}/v1/integrations/slack/callback`;
8539
+ const token = await exchangeOpenGeniSlackAuthorizationCode(
8540
+ {
8541
+ code,
8542
+ clientId: slack.clientId,
8543
+ clientSecret: slack.clientSecret,
8544
+ redirectUri
8545
+ },
8546
+ deps.slackFetch ?? fetch
8547
+ );
8548
+ stage = "credential_verification";
8549
+ const verified = await verifyOpenGeniSlackBotCredential(token, deps.slackFetch ?? fetch);
8550
+ stage = "permission_recheck";
8551
+ await requireSlackInstallCallbackGrant(db, state);
8552
+ stage = "persistence";
8553
+ const connection = await persistOpenGeniSlackBotConnection({
8554
+ deps,
8555
+ state,
8556
+ token,
8557
+ verified
8558
+ });
8559
+ return c.redirect(
8560
+ slackInstallReturnUrl(baseUrl, state.returnPath, "connected", connection.id),
8561
+ 302
8562
+ );
8563
+ } catch (error) {
8564
+ if (state) {
8565
+ const failure = slackInstallCallbackFailure(stage, error);
8566
+ try {
8567
+ await recordSlackBotInstallCallbackFailure(db, {
8568
+ accountId: state.accountId,
8569
+ workspaceId: state.workspaceId,
8570
+ subjectId: state.subjectId,
8571
+ callbackDigest: createHash4("sha256").update(state.nonce).digest("hex"),
8572
+ installMode: state.connectionId ? "reinstall" : "connect",
8573
+ ...failure
8574
+ });
8575
+ } catch {
8576
+ return c.redirect(
8577
+ slackInstallReturnUrl(baseUrl, state.returnPath, "error", "installation_failed"),
8578
+ 302
8579
+ );
8580
+ }
8581
+ }
8582
+ const reason = slackInstallErrorReason(error);
8583
+ return c.redirect(
8584
+ slackInstallReturnUrl(baseUrl, state?.returnPath ?? "/integrations", "error", reason),
8585
+ 302
8586
+ );
8587
+ }
8308
8588
  });
8309
8589
  app.get("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
8310
8590
  const workspaceId = c.req.param("workspaceId");
@@ -8336,6 +8616,12 @@ function registerConnectionRoutes(app, deps) {
8336
8616
  message: "use the dedicated OpenGeni Slack bot reinstall flow to update this connection"
8337
8617
  });
8338
8618
  }
8619
+ if (existing) {
8620
+ assertNotDirectPersonalSlackOAuth(
8621
+ canonicalProviderDomain(payload.providerDomain ?? existing.providerDomain),
8622
+ payload.kind ?? existing.kind
8623
+ );
8624
+ }
8339
8625
  if (payload.status !== void 0) {
8340
8626
  if (payload.status !== "active") {
8341
8627
  throw new HTTPException9(400, {
@@ -8371,33 +8657,24 @@ function registerConnectionRoutes(app, deps) {
8371
8657
  });
8372
8658
  app.delete("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
8373
8659
  const workspaceId = c.req.param("workspaceId");
8660
+ const connectionId = c.req.param("connectionId");
8374
8661
  const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:write");
8375
- const connection = await revokeConnection(
8376
- db,
8377
- workspaceId,
8378
- c.req.param("connectionId"),
8379
- grant.subjectId
8380
- );
8381
- if (!connection) {
8662
+ const existing = await getConnectionMetadata2(db, workspaceId, connectionId, grant.subjectId);
8663
+ if (!existing) {
8382
8664
  throw new HTTPException9(404, { message: "connection not found" });
8383
8665
  }
8384
- if (isOpenGeniSlackBotConnection(connection)) {
8385
- const metadata = openGeniSlackBotMetadata2(connection.metadata);
8386
- await recordAuditEvent2(db, {
8387
- accountId: grant.accountId,
8388
- workspaceId,
8389
- subjectId: grant.subjectId,
8390
- action: "slack_bot.disconnected",
8391
- targetType: "connection",
8392
- targetId: connection.id,
8393
- metadata: {
8394
- credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE2,
8395
- credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL2,
8396
- connectionId: connection.id,
8397
- slackTeamId: metadata.slackTeamId,
8398
- outcome: "succeeded"
8399
- }
8400
- });
8666
+ const connection = isOpenGeniSlackBotConnection(existing) ? await revokeConnectionWithSlackBotSuccessAudit(db, {
8667
+ accountId: grant.accountId,
8668
+ workspaceId,
8669
+ subjectId: grant.subjectId,
8670
+ connectionId,
8671
+ expectedVersion: existing.version,
8672
+ credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE2,
8673
+ credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL2,
8674
+ slackTeamId: openGeniSlackBotMetadata2(existing.metadata).slackTeamId
8675
+ }) : await revokeConnection(db, workspaceId, connectionId, grant.subjectId);
8676
+ if (!connection) {
8677
+ throw new HTTPException9(409, { message: "connection changed during disconnect; try again" });
8401
8678
  }
8402
8679
  return c.json(ConnectionResponse.parse({ connection }));
8403
8680
  });
@@ -8451,6 +8728,222 @@ function registerConnectionRoutes(app, deps) {
8451
8728
  );
8452
8729
  });
8453
8730
  }
8731
+ async function persistOpenGeniSlackBotConnection(input) {
8732
+ const { db, settings } = input.deps;
8733
+ const key = requireEnvironmentEncryption2(settings);
8734
+ const credentialEncrypted = encryptCredentialBundle(key, slackBotCredentialBundle(input.token));
8735
+ const existing = input.state.connectionId ? await getConnectionMetadata2(
8736
+ db,
8737
+ input.state.workspaceId,
8738
+ input.state.connectionId,
8739
+ input.state.subjectId
8740
+ ) : null;
8741
+ if (input.state.connectionId && !existing) {
8742
+ throw new SlackInstallCallbackError(
8743
+ 404,
8744
+ "connection_conflict",
8745
+ "connection not found",
8746
+ "principal_validation"
8747
+ );
8748
+ }
8749
+ if (existing && !isOpenGeniSlackBotConnection(existing)) {
8750
+ throw new SlackInstallCallbackError(
8751
+ 422,
8752
+ "connection_conflict",
8753
+ "connectionId is not an OpenGeni Slack bot connection",
8754
+ "principal_validation"
8755
+ );
8756
+ }
8757
+ if (existing?.version !== input.state.connectionVersion) {
8758
+ throw new SlackInstallCallbackError(
8759
+ 409,
8760
+ "connection_conflict",
8761
+ "Slack bot connection changed during reinstall; start again",
8762
+ "principal_validation"
8763
+ );
8764
+ }
8765
+ const existingMetadata = existing ? openGeniSlackBotMetadata2(existing.metadata) : null;
8766
+ if (existingMetadata && existingMetadata.slackTeamId !== input.verified.metadata.slackTeamId) {
8767
+ throw new SlackInstallCallbackError(
8768
+ 409,
8769
+ "principal_mismatch",
8770
+ "a Slack bot connection can only be reinstalled for its original Slack workspace",
8771
+ "principal_validation"
8772
+ );
8773
+ }
8774
+ if (existingMetadata && (existingMetadata.botId !== input.verified.metadata.botId || existingMetadata.botUserId !== input.verified.metadata.botUserId)) {
8775
+ throw new SlackInstallCallbackError(
8776
+ 409,
8777
+ "principal_mismatch",
8778
+ "a different Slack bot requires a new connection and explicit scheduled-task rebinding",
8779
+ "principal_validation"
8780
+ );
8781
+ }
8782
+ const verifiedInstallAt = new Date(input.verified.metadata.verifiedAt);
8783
+ const lifecycleAudit = {
8784
+ accountId: input.state.accountId,
8785
+ workspaceId: input.state.workspaceId,
8786
+ subjectId: input.state.subjectId,
8787
+ credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE2,
8788
+ credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL2,
8789
+ slackTeamId: input.verified.metadata.slackTeamId
8790
+ };
8791
+ const connection = existing ? await updateConnectionWithSlackBotSuccessAudit(db, {
8792
+ ...lifecycleAudit,
8793
+ connection: {
8794
+ workspaceId: input.state.workspaceId,
8795
+ connectionId: existing.id,
8796
+ visibleToSubjectId: input.state.subjectId,
8797
+ expectedVersion: existing.version,
8798
+ subjectId: null,
8799
+ providerDomain: "slack.com",
8800
+ kind: "app_install",
8801
+ status: "active",
8802
+ credentialEncrypted,
8803
+ grantedScopes: input.verified.grantedScopes,
8804
+ expiresAt: null,
8805
+ verifiedInstallAt,
8806
+ verifiedInstallVersion: existing.version + 1,
8807
+ metadata: input.verified.metadata,
8808
+ updatedBySubjectId: input.state.subjectId
8809
+ }
8810
+ }) : await createConnectionWithSlackBotSuccessAudit(db, {
8811
+ ...lifecycleAudit,
8812
+ connection: {
8813
+ accountId: input.state.accountId,
8814
+ workspaceId: input.state.workspaceId,
8815
+ subjectId: null,
8816
+ providerDomain: "slack.com",
8817
+ kind: "app_install",
8818
+ credentialEncrypted,
8819
+ grantedScopes: input.verified.grantedScopes,
8820
+ expiresAt: null,
8821
+ verifiedInstallAt,
8822
+ verifiedInstallVersion: 1,
8823
+ metadata: input.verified.metadata,
8824
+ createdBySubjectId: input.state.subjectId
8825
+ }
8826
+ });
8827
+ if (!connection) {
8828
+ throw new SlackInstallCallbackError(
8829
+ 409,
8830
+ "connection_conflict",
8831
+ "Slack bot connection changed during reinstall; start again",
8832
+ "principal_validation"
8833
+ );
8834
+ }
8835
+ return connection;
8836
+ }
8837
+ function requireOpenGeniSlackOAuthSettings(settings) {
8838
+ const clientId = settings.slackClientId?.trim();
8839
+ const clientSecret = settings.slackClientSecret?.trim();
8840
+ if (!clientId || !clientSecret) {
8841
+ throw new HTTPException9(503, {
8842
+ message: "OpenGeni Slack installation requires OPENGENI_SLACK_CLIENT_ID and OPENGENI_SLACK_CLIENT_SECRET"
8843
+ });
8844
+ }
8845
+ return { clientId, clientSecret };
8846
+ }
8847
+ function readOpenGeniSlackInstallState(rawState, settings) {
8848
+ if (!rawState) {
8849
+ throw new HTTPException9(400, { message: "missing Slack installation state" });
8850
+ }
8851
+ const payload = readSignedState3(rawState, requireIntegrationsStateSecret(settings));
8852
+ if (!payload) {
8853
+ throw new HTTPException9(400, { message: "invalid or expired Slack installation state" });
8854
+ }
8855
+ const requiredString2 = (value, label) => {
8856
+ if (typeof value !== "string" || value.length === 0) {
8857
+ throw new HTTPException9(400, { message: `invalid Slack installation ${label}` });
8858
+ }
8859
+ return value;
8860
+ };
8861
+ const nowSeconds = Math.floor(Date.now() / 1e3);
8862
+ if (typeof payload.iat !== "number" || nowSeconds < payload.iat || nowSeconds - payload.iat > oauthStateTtlMs / 1e3) {
8863
+ throw new HTTPException9(400, { message: "invalid or expired Slack installation state" });
8864
+ }
8865
+ const accountId = requiredString2(payload.accountId, "account");
8866
+ const workspaceId = requiredString2(payload.workspaceId, "workspace");
8867
+ const subjectId = requiredString2(payload.subjectId, "subject");
8868
+ const returnPath = requiredString2(payload.returnPath, "return path");
8869
+ if (returnPath !== `/workspaces/${workspaceId}/capabilities`) {
8870
+ throw new HTTPException9(400, { message: "invalid Slack installation return path" });
8871
+ }
8872
+ const connectionId = typeof payload.connectionId === "string" ? payload.connectionId : void 0;
8873
+ const connectionVersion = typeof payload.connectionVersion === "number" && Number.isInteger(payload.connectionVersion) ? payload.connectionVersion : void 0;
8874
+ if (Boolean(connectionId) !== Boolean(connectionVersion)) {
8875
+ throw new HTTPException9(400, { message: "invalid Slack reinstall state" });
8876
+ }
8877
+ return {
8878
+ accountId,
8879
+ workspaceId,
8880
+ subjectId,
8881
+ returnPath,
8882
+ ...connectionId ? { connectionId, connectionVersion } : {},
8883
+ nonce: requiredString2(payload.nonce, "nonce"),
8884
+ iat: typeof payload.iat === "number" ? payload.iat : (() => {
8885
+ throw new HTTPException9(400, { message: "invalid Slack installation timestamp" });
8886
+ })()
8887
+ };
8888
+ }
8889
+ function slackInstallReturnUrl(baseUrl, returnPath, status, detail) {
8890
+ const url = new URL(returnPath, `${baseUrl}/`);
8891
+ url.searchParams.set("slack", status);
8892
+ url.searchParams.set(status === "connected" ? "connectionId" : "reason", detail.slice(0, 128));
8893
+ return url.toString();
8894
+ }
8895
+ var SlackInstallCallbackError = class extends HTTPException9 {
8896
+ constructor(status, failureReason, message, failureStage) {
8897
+ super(status, { message });
8898
+ this.failureReason = failureReason;
8899
+ this.failureStage = failureStage;
8900
+ this.name = "SlackInstallCallbackError";
8901
+ }
8902
+ };
8903
+ function slackInstallCallbackFailure(stage, error) {
8904
+ if (error instanceof SlackInstallCallbackError) {
8905
+ return { stage: error.failureStage ?? stage, reason: error.failureReason };
8906
+ }
8907
+ if (error instanceof SlackBotCredentialVerificationError) {
8908
+ return { stage: "credential_verification", reason: error.failureReason };
8909
+ }
8910
+ if (error instanceof SlackBotLifecycleSuccessAuditError) {
8911
+ return { stage: "persistence", reason: "success_audit_failed" };
8912
+ }
8913
+ if (stage === "code_exchange") {
8914
+ return { stage, reason: "exchange_failed" };
8915
+ }
8916
+ if (stage === "credential_verification") {
8917
+ return { stage, reason: "credential_verification_failed" };
8918
+ }
8919
+ return { stage, reason: "persistence_failed" };
8920
+ }
8921
+ function slackInstallErrorReason(error) {
8922
+ if (error instanceof SlackInstallCallbackError && error.failureReason === "provider_denied") {
8923
+ return "provider_denied";
8924
+ }
8925
+ if (error instanceof HTTPException9) {
8926
+ return `http_${error.status}`;
8927
+ }
8928
+ return "installation_failed";
8929
+ }
8930
+ async function requireSlackInstallCallbackGrant(db, state) {
8931
+ const grant = await getWorkspaceGrant2(db, state.subjectId, state.workspaceId);
8932
+ if (!grant || grant.accountId !== state.accountId || !hasPermission6(grant.permissions, "connections:write")) {
8933
+ throw new SlackInstallCallbackError(
8934
+ 403,
8935
+ "permission_lost",
8936
+ "Slack installation subject no longer has permission for this workspace"
8937
+ );
8938
+ }
8939
+ }
8940
+ function assertNotDirectPersonalSlackOAuth(providerDomain, kind) {
8941
+ if (providerDomain === "slack.com" && kind === "oauth2") {
8942
+ throw new HTTPException9(422, {
8943
+ message: "personal Slack credentials must use the hosted MCP OAuth flow"
8944
+ });
8945
+ }
8946
+ }
8454
8947
  function assertNotReservedSlackBotMetadata(metadata) {
8455
8948
  if (hasReservedOpenGeniSlackBotMetadata(metadata)) {
8456
8949
  throw new HTTPException9(422, {
@@ -8515,7 +9008,7 @@ import {
8515
9008
  queueDocumentForReindex,
8516
9009
  searchDocuments as searchDocuments2
8517
9010
  } from "@opengeni/documents";
8518
- import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
9011
+ import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
8519
9012
  import { HTTPException as HTTPException11 } from "hono/http-exception";
8520
9013
  import { requireAccessGrant as requireAccessGrant5 } from "@opengeni/core";
8521
9014
  import { recordWorkspaceUsage as recordWorkspaceUsage3, requireLimit as requireLimit3 } from "@opengeni/core";
@@ -8746,8 +9239,90 @@ import {
8746
9239
  import { HTTPException as HTTPException10 } from "hono/http-exception";
8747
9240
  import { requireAccessGrant as requireAccessGrant4 } from "@opengeni/core";
8748
9241
  import { recordWorkspaceUsage as recordWorkspaceUsage2, requireLimit as requireLimit2 } from "@opengeni/core";
9242
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
9243
+
9244
+ // src/mcp/files.ts
9245
+ import { requireFile } from "@opengeni/db";
9246
+ import { hasPermission as hasPermission7 } from "@opengeni/core";
9247
+ import { McpServer as McpServer3 } from "@modelcontextprotocol/sdk/server/mcp.js";
9248
+ import * as z3 from "zod/v4";
9249
+ function buildFilesMcpServer(deps, grant) {
9250
+ const server = new McpServer3({
9251
+ name: "opengeni-files",
9252
+ version: "1.0.0"
9253
+ });
9254
+ if (!hasPermission7(grant.permissions, "files:read")) {
9255
+ server.registerTool(
9256
+ "__opengeni_empty_files_surface__",
9257
+ {
9258
+ description: "Internal disabled placeholder for an unauthorized files surface.",
9259
+ inputSchema: z3.object({})
9260
+ },
9261
+ async () => ({
9262
+ content: [{ type: "text", text: '{"unavailable":true}' }]
9263
+ })
9264
+ ).disable();
9265
+ return server;
9266
+ }
9267
+ server.registerTool(
9268
+ "files_get_download_url",
9269
+ {
9270
+ description: "Create a short-lived download URL for a ready file asset.",
9271
+ inputSchema: { fileId: z3.string().uuid() }
9272
+ },
9273
+ async ({ fileId }) => {
9274
+ if (!deps.objectStorage) {
9275
+ throw new Error("object storage is not configured");
9276
+ }
9277
+ const file = await requireFile(deps.db, grant.workspaceId, fileId);
9278
+ if (file.status !== "ready") {
9279
+ throw new Error(`file is ${file.status}`);
9280
+ }
9281
+ const signed = await deps.objectStorage.createGetUrl({ key: file.objectKey });
9282
+ return {
9283
+ content: [
9284
+ {
9285
+ type: "text",
9286
+ text: JSON.stringify(
9287
+ {
9288
+ file: {
9289
+ id: file.id,
9290
+ filename: file.filename,
9291
+ safeFilename: file.safeFilename,
9292
+ contentType: file.contentType,
9293
+ sizeBytes: file.sizeBytes,
9294
+ sha256: file.sha256,
9295
+ status: file.status,
9296
+ createdAt: file.createdAt,
9297
+ updatedAt: file.updatedAt
9298
+ },
9299
+ downloadUrl: {
9300
+ url: signed.url,
9301
+ expiresAt: signed.expiresAt.toISOString()
9302
+ }
9303
+ },
9304
+ null,
9305
+ 2
9306
+ )
9307
+ }
9308
+ ]
9309
+ };
9310
+ }
9311
+ );
9312
+ return server;
9313
+ }
9314
+
9315
+ // src/routes/files.ts
8749
9316
  function registerFileRoutes(app, deps) {
8750
9317
  const { db, objectStorage } = deps;
9318
+ app.all("/v1/workspaces/:workspaceId/mcp/files", async (c) => {
9319
+ const workspaceId = c.req.param("workspaceId");
9320
+ const grant = await requireAccessGrant4(c, deps, workspaceId, "files:read");
9321
+ const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
9322
+ const server = buildFilesMcpServer(deps, grant);
9323
+ await server.connect(transport);
9324
+ return await transport.handleRequest(c.req.raw);
9325
+ });
8751
9326
  app.post("/v1/workspaces/:workspaceId/files/uploads", async (c) => {
8752
9327
  const workspaceId = c.req.param("workspaceId");
8753
9328
  const grant = await requireAccessGrant4(c, deps, workspaceId, "files:upload");
@@ -9556,7 +10131,7 @@ function registerDocumentRoutes(app, deps) {
9556
10131
  const workspaceId = c.req.param("workspaceId");
9557
10132
  const grant = await requireAccessGrant5(c, deps, workspaceId, "documents:search");
9558
10133
  const sessionId = typeof grant.metadata?.sessionId === "string" ? grant.metadata.sessionId : void 0;
9559
- const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
10134
+ const transport = new WebStandardStreamableHTTPServerTransport2({ enableJsonResponse: true });
9560
10135
  const server = buildDocumentsMcpServer(
9561
10136
  db,
9562
10137
  grant.accountId,
@@ -11236,7 +11811,7 @@ import {
11236
11811
  authorizeGitHubInstallationBinding,
11237
11812
  buildGitHubAppManifest,
11238
11813
  convertGitHubAppManifest,
11239
- createSignedState as createSignedState4,
11814
+ createSignedState as createSignedState5,
11240
11815
  envLinesFromGitHubManifestConversion,
11241
11816
  GitHubAppApiError,
11242
11817
  GitHubAppConfigurationError as GitHubAppConfigurationError2,
@@ -11245,13 +11820,13 @@ import {
11245
11820
  githubOAuthAuthorizeUrl,
11246
11821
  organizationAppManifestUrl,
11247
11822
  personalAppManifestUrl,
11248
- readSignedState as readSignedState3,
11823
+ readSignedState as readSignedState4,
11249
11824
  stateMaxAgeSeconds,
11250
11825
  verifySignedState
11251
11826
  } from "@opengeni/github";
11252
11827
  import { deleteCookie, setCookie } from "hono/cookie";
11253
11828
  import { HTTPException as HTTPException17 } from "hono/http-exception";
11254
- import { hasPermission as hasPermission5, requireAccessGrant as requireAccessGrant10 } from "@opengeni/core";
11829
+ import { hasPermission as hasPermission8, requireAccessGrant as requireAccessGrant10 } from "@opengeni/core";
11255
11830
  var githubStateCookie = "opengeni_github_state";
11256
11831
  var githubBindingStateMaxAgeSeconds = 10 * 60;
11257
11832
  var legacyInstallationChooserDisabledMessage = "The legacy repository-admin GitHub installation chooser is disabled; use the GitHub owner-consent connect flow";
@@ -11264,8 +11839,8 @@ function registerGitHubRoutes(app, deps) {
11264
11839
  const slug = settings.githubAppSlug?.trim() || null;
11265
11840
  const installations = missing.length === 0 ? await listWorkspaceGitHubInstallationBindings(deps, grant.workspaceId) : [];
11266
11841
  const status = githubBindingStatus(missing.length === 0, installations);
11267
- const canManage = hasPermission5(grant.permissions, "github:manage");
11268
- const connectState = missing.length === 0 && slug && canManage ? createSignedState4(githubStateSecret, {
11842
+ const canManage = hasPermission8(grant.permissions, "github:manage");
11843
+ const connectState = missing.length === 0 && slug && canManage ? createSignedState5(githubStateSecret, {
11269
11844
  accountId: grant.accountId,
11270
11845
  workspaceId: grant.workspaceId,
11271
11846
  intent: "installation_authority",
@@ -11290,7 +11865,7 @@ function registerGitHubRoutes(app, deps) {
11290
11865
  if (!state) {
11291
11866
  throw new HTTPException17(400, { message: "missing GitHub installation state" });
11292
11867
  }
11293
- const statePayload = readSignedState3(state, githubStateSecret);
11868
+ const statePayload = readSignedState4(state, githubStateSecret);
11294
11869
  if (!statePayload || statePayload.intent !== "installation_authority" || statePayload.workspaceId !== workspaceId || typeof statePayload.accountId !== "string" || !isFreshGitHubBindingState(statePayload)) {
11295
11870
  throw new HTTPException17(400, { message: "invalid or expired GitHub installation state" });
11296
11871
  }
@@ -11365,7 +11940,7 @@ function registerGitHubRoutes(app, deps) {
11365
11940
  /\/+$/,
11366
11941
  ""
11367
11942
  );
11368
- const state = createSignedState4(githubStateSecret, {
11943
+ const state = createSignedState5(githubStateSecret, {
11369
11944
  accountId: grant.accountId,
11370
11945
  workspaceId: grant.workspaceId
11371
11946
  });
@@ -11409,7 +11984,7 @@ function registerGitHubRoutes(app, deps) {
11409
11984
  if (!state) {
11410
11985
  throw new HTTPException17(400, { message: "missing GitHub installation state" });
11411
11986
  }
11412
- const statePayload = readSignedState3(state, githubStateSecret);
11987
+ const statePayload = readSignedState4(state, githubStateSecret);
11413
11988
  if (!statePayload || statePayload.intent !== "installation_authority" || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string" || !isFreshGitHubBindingState(statePayload)) {
11414
11989
  throw new HTTPException17(400, { message: "invalid or expired GitHub installation state" });
11415
11990
  }
@@ -11440,7 +12015,7 @@ function registerGitHubRoutes(app, deps) {
11440
12015
  })
11441
12016
  });
11442
12017
  }
11443
- const oauthState = createSignedState4(githubStateSecret, {
12018
+ const oauthState = createSignedState5(githubStateSecret, {
11444
12019
  accountId: grant.accountId,
11445
12020
  workspaceId: grant.workspaceId,
11446
12021
  installationId,
@@ -11467,7 +12042,7 @@ function registerGitHubRoutes(app, deps) {
11467
12042
  if (!state) {
11468
12043
  throw new HTTPException17(400, { message: "missing GitHub OAuth state" });
11469
12044
  }
11470
- const statePayload = readSignedState3(state, githubStateSecret);
12045
+ const statePayload = readSignedState4(state, githubStateSecret);
11471
12046
  if (!statePayload || statePayload.intent !== "installation_authority_oauth" || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string" || !isFreshGitHubBindingState(statePayload)) {
11472
12047
  throw new HTTPException17(400, { message: "invalid or expired GitHub OAuth state" });
11473
12048
  }
@@ -11546,7 +12121,7 @@ function registerGitHubRoutes(app, deps) {
11546
12121
  if (!state) {
11547
12122
  throw new HTTPException17(400, { message: "missing GitHub OAuth state" });
11548
12123
  }
11549
- const statePayload = readSignedState3(state, githubStateSecret);
12124
+ const statePayload = readSignedState4(state, githubStateSecret);
11550
12125
  if (!statePayload || typeof statePayload.accountId !== "string" || statePayload.accountId.length === 0 || statePayload.workspaceId !== workspaceId) {
11551
12126
  throw new HTTPException17(400, { message: "invalid or expired GitHub OAuth state" });
11552
12127
  }
@@ -12202,7 +12777,8 @@ function registerScheduledTaskRoutes(app, deps) {
12202
12777
  await workflowClient.triggerScheduledTask({
12203
12778
  task,
12204
12779
  agentRunUsageIdempotencyKey,
12205
- triggerWorkflowId
12780
+ triggerWorkflowId,
12781
+ initiator: { kind: "subject", subjectId: grant.subjectId }
12206
12782
  });
12207
12783
  await recordWorkspaceUsage4(deps, {
12208
12784
  accountId: grant.accountId,
@@ -12342,7 +12918,7 @@ import {
12342
12918
  coalesceSessionEventDeltas,
12343
12919
  publishDurableSessionEvents as publishDurableSessionEvents2
12344
12920
  } from "@opengeni/events";
12345
- import { z as z3, ZodError } from "zod";
12921
+ import { z as z5, ZodError } from "zod";
12346
12922
 
12347
12923
  // src/sandbox/channel-a.ts
12348
12924
  import {
@@ -13546,7 +14122,7 @@ function registerSessionRoutes(app, deps) {
13546
14122
  const workspaceId = c.req.param("workspaceId");
13547
14123
  const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
13548
14124
  const sessionId = c.req.param("sessionId");
13549
- if (!z3.string().uuid().safeParse(sessionId).success) {
14125
+ if (!z5.string().uuid().safeParse(sessionId).success) {
13550
14126
  throw new HTTPException22(404, { message: "session not found" });
13551
14127
  }
13552
14128
  const session = await getSessionForSubject(
@@ -13565,7 +14141,7 @@ function registerSessionRoutes(app, deps) {
13565
14141
  const workspaceId = c.req.param("workspaceId");
13566
14142
  const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
13567
14143
  const sessionId = c.req.param("sessionId");
13568
- if (!z3.string().uuid().safeParse(sessionId).success) {
14144
+ if (!z5.string().uuid().safeParse(sessionId).success) {
13569
14145
  throw new HTTPException22(404, { message: "session not found" });
13570
14146
  }
13571
14147
  const parsed = UpdateSessionPinRequest.safeParse(await c.req.json().catch(() => null));
@@ -15265,7 +15841,7 @@ function eventEnumList(raw, schema, name) {
15265
15841
  }
15266
15842
  function sessionListQuery(query, allowCursor = true) {
15267
15843
  const parentSessionId = query.parentSessionId;
15268
- if (parentSessionId !== void 0 && parentSessionId !== "null" && !z3.string().uuid().safeParse(parentSessionId).success) {
15844
+ if (parentSessionId !== void 0 && parentSessionId !== "null" && !z5.string().uuid().safeParse(parentSessionId).success) {
15269
15845
  throw new HTTPException22(400, {
15270
15846
  message: 'parentSessionId must be a session id or the literal "null"'
15271
15847
  });
@@ -15421,7 +15997,7 @@ import {
15421
15997
  listSocialPosts as listSocialPosts2
15422
15998
  } from "@opengeni/db";
15423
15999
  import { HTTPException as HTTPException23 } from "hono/http-exception";
15424
- import { z as z5 } from "zod";
16000
+ import { z as z6 } from "zod";
15425
16001
  import { requireAccessGrant as requireAccessGrant15 } from "@opengeni/core";
15426
16002
  function registerSocialRoutes(app, deps) {
15427
16003
  const { db } = deps;
@@ -15511,7 +16087,7 @@ function parseConnectionIds(raw) {
15511
16087
  return void 0;
15512
16088
  }
15513
16089
  const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
15514
- const parsed = z5.array(z5.string().uuid()).safeParse(values);
16090
+ const parsed = z6.array(z6.string().uuid()).safeParse(values);
15515
16091
  if (!parsed.success) {
15516
16092
  throw new HTTPException23(422, {
15517
16093
  message: "connectionIds must be a comma-separated list of UUIDs"
@@ -15572,7 +16148,7 @@ import {
15572
16148
  } from "@opengeni/db";
15573
16149
  import { boundWorkspaceControlHttpPage } from "@opengeni/events";
15574
16150
  import { HTTPException as HTTPException24 } from "hono/http-exception";
15575
- import { hasPermission as hasPermission6, requireAccessContext as requireAccessContext2, requireAccessGrant as requireAccessGrant16 } from "@opengeni/core";
16151
+ import { hasPermission as hasPermission9, requireAccessContext as requireAccessContext2, requireAccessGrant as requireAccessGrant16 } from "@opengeni/core";
15576
16152
  import { requireLimit as requireLimit7 } from "@opengeni/core";
15577
16153
  import {
15578
16154
  assertWorkspaceDeletable,
@@ -15819,7 +16395,7 @@ function registerWorkspaceRoutes(app, deps) {
15819
16395
  const context = await requireAccessContext2(c, deps);
15820
16396
  const readableWorkspaceIds = [
15821
16397
  ...new Set(
15822
- context.workspaceGrants.filter((grant) => hasPermission6(grant.permissions, "workspace:read")).map((grant) => grant.workspaceId)
16398
+ context.workspaceGrants.filter((grant) => hasPermission9(grant.permissions, "workspace:read")).map((grant) => grant.workspaceId)
15823
16399
  )
15824
16400
  ];
15825
16401
  if (readableWorkspaceIds.length > 0) {
@@ -16108,8 +16684,8 @@ import {
16108
16684
  WorkspaceInstructionPolicyNotFoundError
16109
16685
  } from "@opengeni/db";
16110
16686
  import { HTTPException as HTTPException25 } from "hono/http-exception";
16111
- import { z as z6 } from "zod";
16112
- var WorkspaceInstructionPolicyRevisionId = z6.string().uuid();
16687
+ import { z as z7 } from "zod";
16688
+ var WorkspaceInstructionPolicyRevisionId = z7.string().uuid();
16113
16689
  async function parseBody(context, schema) {
16114
16690
  const parsed = schema.safeParse(await context.req.json().catch(() => null));
16115
16691
  if (!parsed.success) {
@@ -16609,7 +17185,7 @@ function createApp(deps) {
16609
17185
  }
16610
17186
  const workspace = await getWorkspace2(routeDeps.db, workspaceId);
16611
17187
  const workspaceMemoryEnabled = resolveWorkspaceMemoryEnabled(workspace?.settings);
16612
- const transport = new WebStandardStreamableHTTPServerTransport2({
17188
+ const transport = new WebStandardStreamableHTTPServerTransport3({
16613
17189
  enableJsonResponse: true
16614
17190
  });
16615
17191
  const mcp = buildOpenGeniMcpServer(routeDeps, grant, {
@@ -16683,7 +17259,7 @@ function createApp(deps) {
16683
17259
  }
16684
17260
  async function requireMcpAccessGrant(c, deps, workspaceId) {
16685
17261
  const grant = await requireAccessGrant18(c, deps, workspaceId);
16686
- if (hasPermission7(grant.permissions, "workspace:read")) {
17262
+ if (hasPermission10(grant.permissions, "workspace:read")) {
16687
17263
  return grant;
16688
17264
  }
16689
17265
  if (isToolspaceGrant(deps.settings, grant)) {
@@ -16864,6 +17440,10 @@ var routeLabelPatterns = [
16864
17440
  pattern: /^\/v1\/workspaces\/[^/]+\/mcp\/docs$/,
16865
17441
  label: "/v1/workspaces/:workspaceId/mcp/docs"
16866
17442
  },
17443
+ {
17444
+ pattern: /^\/v1\/workspaces\/[^/]+\/mcp\/files$/,
17445
+ label: "/v1/workspaces/:workspaceId/mcp/files"
17446
+ },
16867
17447
  {
16868
17448
  pattern: /^\/v1\/workspaces\/[^/]+\/default-rig$/,
16869
17449
  label: "/v1/workspaces/:workspaceId/default-rig"
@@ -17121,8 +17701,8 @@ var routeLabelPatterns = [
17121
17701
  label: "/v1/workspaces/:workspaceId/connections/oauth/start"
17122
17702
  },
17123
17703
  {
17124
- pattern: /^\/v1\/workspaces\/[^/]+\/connections\/slack-bot$/,
17125
- label: "/v1/workspaces/:workspaceId/connections/slack-bot"
17704
+ pattern: /^\/v1\/workspaces\/[^/]+\/connections\/slack-bot\/install$/,
17705
+ label: "/v1/workspaces/:workspaceId/connections/slack-bot/install"
17126
17706
  },
17127
17707
  {
17128
17708
  pattern: /^\/v1\/workspaces\/[^/]+\/connections\/[^/]+$/,
@@ -17137,6 +17717,10 @@ var routeLabelPatterns = [
17137
17717
  pattern: /^\/v1\/integrations\/oauth\/client-metadata\.json$/,
17138
17718
  label: "/v1/integrations/oauth/client-metadata.json"
17139
17719
  },
17720
+ {
17721
+ pattern: /^\/v1\/integrations\/slack\/callback$/,
17722
+ label: "/v1/integrations/slack/callback"
17723
+ },
17140
17724
  {
17141
17725
  pattern: /^\/v1\/enrollments\/device\/start$/,
17142
17726
  label: "/v1/enrollments/device/start"
@@ -17221,4 +17805,4 @@ export {
17221
17805
  withDefaultEnabledCapabilityMcpTools,
17222
17806
  workflowIdForSession2 as workflowIdForSession
17223
17807
  };
17224
- //# sourceMappingURL=chunk-3JR6L6NJ.js.map
17808
+ //# sourceMappingURL=chunk-SD5GJTZ4.js.map