@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.
- package/dist/app.js +1 -1
- package/dist/{chunk-3JR6L6NJ.js → chunk-SD5GJTZ4.js} +785 -201
- package/dist/chunk-SD5GJTZ4.js.map +1 -0
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/app.ts +10 -2
- package/src/http/auth.ts +2 -1
- package/src/index.ts +8 -3
- package/src/integrations/oauth-client.ts +94 -8
- package/src/integrations/slack-bot.ts +102 -9
- package/src/mcp/files.ts +77 -0
- package/src/mcp/server.ts +170 -47
- package/src/mcp/toolspace.ts +13 -4
- package/src/routes/connections.ts +448 -103
- package/src/routes/files.ts +11 -0
- package/src/routes/scheduled-tasks.ts +1 -0
- package/dist/chunk-3JR6L6NJ.js.map +0 -1
|
@@ -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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
2061
|
-
|
|
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
|
-
|
|
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
|
|
2430
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
5800
|
-
if (!
|
|
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(
|
|
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
|
-
|
|
7225
|
+
recordSlackBotInstallCallbackFailure,
|
|
7058
7226
|
revokeConnection,
|
|
7059
|
-
|
|
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
|
|
7120
|
-
|
|
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:
|
|
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
|
-
|
|
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 ===
|
|
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
|
|
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 =
|
|
8227
|
-
const
|
|
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
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
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:
|
|
8277
|
-
|
|
8278
|
-
|
|
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
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
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
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
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
|
-
|
|
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
|
|
8376
|
-
|
|
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
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
11268
|
-
const connectState = missing.length === 0 && slug && canManage ?
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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" && !
|
|
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
|
|
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 =
|
|
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
|
|
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) =>
|
|
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
|
|
16112
|
-
var WorkspaceInstructionPolicyRevisionId =
|
|
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
|
|
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 (
|
|
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-
|
|
17808
|
+
//# sourceMappingURL=chunk-SD5GJTZ4.js.map
|