@opengeni/api-router 0.12.1 → 0.12.5
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-S2N4252E.js → chunk-3BHOMOSD.js} +631 -169
- package/dist/chunk-3BHOMOSD.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +10 -10
- package/src/app.ts +13 -5
- package/src/http/auth.ts +2 -1
- package/src/integrations/oauth-client.ts +94 -8
- package/src/integrations/slack-bot.ts +102 -9
- package/src/mcp/server.ts +20 -0
- package/src/mcp/toolspace.ts +57 -7
- package/src/routes/connections.ts +448 -103
- package/dist/chunk-S2N4252E.js.map +0 -1
|
@@ -3,7 +3,8 @@ import {
|
|
|
3
3
|
canonicalizeConfiguredModelId as canonicalizeConfiguredModelId2,
|
|
4
4
|
configuredAllowedModels,
|
|
5
5
|
configuredAllowedReasoningEfforts,
|
|
6
|
-
configuredModels as configuredModels2
|
|
6
|
+
configuredModels as configuredModels2,
|
|
7
|
+
withCodexCatalogProvider as withCodexCatalogProvider2
|
|
7
8
|
} from "@opengeni/config";
|
|
8
9
|
import {
|
|
9
10
|
ClientConfig,
|
|
@@ -26,7 +27,7 @@ import { bodyLimit } from "hono/body-limit";
|
|
|
26
27
|
import { cors } from "hono/cors";
|
|
27
28
|
import { HTTPException as HTTPException26 } from "hono/http-exception";
|
|
28
29
|
import {
|
|
29
|
-
hasPermission as
|
|
30
|
+
hasPermission as hasPermission9,
|
|
30
31
|
requireAccessGrant as requireAccessGrant18,
|
|
31
32
|
requirePermission,
|
|
32
33
|
requireSessionAuthorization as requireSessionAuthorization3,
|
|
@@ -2018,7 +2019,8 @@ import {
|
|
|
2018
2019
|
completeSlackBotPostOperation,
|
|
2019
2020
|
getSession,
|
|
2020
2021
|
recordAuditEvent,
|
|
2021
|
-
releaseSlackBotPostOperationClaim
|
|
2022
|
+
releaseSlackBotPostOperationClaim,
|
|
2023
|
+
setConnectionStatus
|
|
2022
2024
|
} from "@opengeni/db";
|
|
2023
2025
|
import { readResponseJsonBounded } from "@opengeni/network";
|
|
2024
2026
|
import { HTTPException as HTTPException2 } from "hono/http-exception";
|
|
@@ -2030,6 +2032,46 @@ var MAX_HISTORY_PAGE = 100;
|
|
|
2030
2032
|
var MAX_USER_PAGE = 200;
|
|
2031
2033
|
var MAX_PROJECTED_TEXT = 4e3;
|
|
2032
2034
|
var SLACK_POST_CLAIM_LEASE_MS = 3e4;
|
|
2035
|
+
async function exchangeOpenGeniSlackAuthorizationCode(input, fetchImpl = fetch) {
|
|
2036
|
+
const body = new URLSearchParams({
|
|
2037
|
+
code: input.code,
|
|
2038
|
+
client_id: input.clientId,
|
|
2039
|
+
client_secret: input.clientSecret,
|
|
2040
|
+
redirect_uri: input.redirectUri
|
|
2041
|
+
});
|
|
2042
|
+
let response;
|
|
2043
|
+
try {
|
|
2044
|
+
response = await fetchImpl(`${SLACK_API_BASE}oauth.v2.access`, {
|
|
2045
|
+
method: "POST",
|
|
2046
|
+
headers: {
|
|
2047
|
+
accept: "application/json",
|
|
2048
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
2049
|
+
},
|
|
2050
|
+
body: body.toString(),
|
|
2051
|
+
redirect: "error",
|
|
2052
|
+
signal: AbortSignal.timeout(SLACK_TIMEOUT_MS)
|
|
2053
|
+
});
|
|
2054
|
+
} catch {
|
|
2055
|
+
throw new HTTPException2(502, { message: "Slack installation token exchange failed" });
|
|
2056
|
+
}
|
|
2057
|
+
if (!response.ok) {
|
|
2058
|
+
throw new HTTPException2(502, { message: "Slack installation token exchange failed" });
|
|
2059
|
+
}
|
|
2060
|
+
const payload = await readResponseJsonBounded(
|
|
2061
|
+
response,
|
|
2062
|
+
SLACK_RESPONSE_MAX_BYTES,
|
|
2063
|
+
"Slack OAuth response"
|
|
2064
|
+
);
|
|
2065
|
+
const record3 = slackRecord(payload);
|
|
2066
|
+
if (!record3 || record3.ok !== true) {
|
|
2067
|
+
throw new SlackBotProviderError(slackString(record3?.error) || "oauth_exchange_failed");
|
|
2068
|
+
}
|
|
2069
|
+
const accessToken = slackString(record3.access_token);
|
|
2070
|
+
if (!accessToken?.startsWith("xoxb-")) {
|
|
2071
|
+
throw new HTTPException2(502, { message: "Slack installation did not return a bot token" });
|
|
2072
|
+
}
|
|
2073
|
+
return accessToken;
|
|
2074
|
+
}
|
|
2033
2075
|
var SlackBotProviderError = class extends Error {
|
|
2034
2076
|
constructor(code) {
|
|
2035
2077
|
super(`Slack bot request failed: ${safeSlackCode(code)}`);
|
|
@@ -2037,6 +2079,23 @@ var SlackBotProviderError = class extends Error {
|
|
|
2037
2079
|
this.name = "SlackBotProviderError";
|
|
2038
2080
|
}
|
|
2039
2081
|
};
|
|
2082
|
+
var SLACK_CREDENTIAL_REJECTION_CODES = /* @__PURE__ */ new Set([
|
|
2083
|
+
"account_inactive",
|
|
2084
|
+
"invalid_auth",
|
|
2085
|
+
"not_authed",
|
|
2086
|
+
"token_expired",
|
|
2087
|
+
"token_revoked"
|
|
2088
|
+
]);
|
|
2089
|
+
function slackCredentialRejected(error) {
|
|
2090
|
+
return error instanceof SlackBotProviderError && SLACK_CREDENTIAL_REJECTION_CODES.has(error.code);
|
|
2091
|
+
}
|
|
2092
|
+
var SlackBotCredentialVerificationError = class extends HTTPException2 {
|
|
2093
|
+
constructor(failureReason, message) {
|
|
2094
|
+
super(422, { message });
|
|
2095
|
+
this.failureReason = failureReason;
|
|
2096
|
+
this.name = "SlackBotCredentialVerificationError";
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2040
2099
|
async function verifyOpenGeniSlackBotCredential(token, fetchImpl = fetch, now = /* @__PURE__ */ new Date()) {
|
|
2041
2100
|
const authResponse = await slackApiFetch(fetchImpl, "auth.test", token, {});
|
|
2042
2101
|
const grantedScopes2 = parseGrantedScopes(authResponse.response.headers.get("x-oauth-scopes"));
|
|
@@ -2051,14 +2110,18 @@ async function verifyOpenGeniSlackBotCredential(token, fetchImpl = fetch, now =
|
|
|
2051
2110
|
});
|
|
2052
2111
|
const user = slackRecord(userResponse.payload.user);
|
|
2053
2112
|
if (!user || user.is_bot !== true || user.deleted === true) {
|
|
2054
|
-
throw new
|
|
2113
|
+
throw new SlackBotCredentialVerificationError(
|
|
2114
|
+
"identity_mismatch",
|
|
2115
|
+
"Slack credential must identify an active bot user"
|
|
2116
|
+
);
|
|
2055
2117
|
}
|
|
2056
2118
|
const profile = slackRecord(user.profile);
|
|
2057
2119
|
const displayName = slackString(profile?.display_name) || slackString(profile?.real_name);
|
|
2058
2120
|
if (displayName !== "OpenGeni") {
|
|
2059
|
-
throw new
|
|
2060
|
-
|
|
2061
|
-
|
|
2121
|
+
throw new SlackBotCredentialVerificationError(
|
|
2122
|
+
"identity_mismatch",
|
|
2123
|
+
'Slack bot display name must be exactly "OpenGeni"'
|
|
2124
|
+
);
|
|
2062
2125
|
}
|
|
2063
2126
|
return {
|
|
2064
2127
|
grantedScopes: grantedScopes2,
|
|
@@ -2269,7 +2332,18 @@ var OpenGeniSlackBotClient = class {
|
|
|
2269
2332
|
return projected;
|
|
2270
2333
|
}
|
|
2271
2334
|
async call(headers, method, params) {
|
|
2272
|
-
|
|
2335
|
+
try {
|
|
2336
|
+
return (await slackApiFetchWithHeaders(this.fetchImpl, method, headers, params)).payload;
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
if (slackCredentialRejected(error)) {
|
|
2339
|
+
await setConnectionStatus(this.db, this.context.workspaceId, "needs_reauth", error.code, {
|
|
2340
|
+
id: this.connection.id,
|
|
2341
|
+
version: this.connection.version,
|
|
2342
|
+
subjectId: null
|
|
2343
|
+
}).catch(() => false);
|
|
2344
|
+
}
|
|
2345
|
+
throw error;
|
|
2346
|
+
}
|
|
2273
2347
|
}
|
|
2274
2348
|
async withAudit(operation, run) {
|
|
2275
2349
|
try {
|
|
@@ -2425,14 +2499,18 @@ function assertExactOpenGeniSlackBotScopes(grantedScopes2) {
|
|
|
2425
2499
|
...forbidden.length ? [`forbidden: ${forbidden.join(", ")}`] : [],
|
|
2426
2500
|
...unsupported.length ? [`unsupported: ${unsupported.join(", ")}`] : []
|
|
2427
2501
|
];
|
|
2428
|
-
throw new
|
|
2429
|
-
|
|
2430
|
-
|
|
2502
|
+
throw new SlackBotCredentialVerificationError(
|
|
2503
|
+
"scope_mismatch",
|
|
2504
|
+
`Slack bot scopes must exactly match the OpenGeni manifest (${facts.join("; ")})`
|
|
2505
|
+
);
|
|
2431
2506
|
}
|
|
2432
2507
|
}
|
|
2433
2508
|
function parseGrantedScopes(header) {
|
|
2434
2509
|
if (!header) {
|
|
2435
|
-
throw new
|
|
2510
|
+
throw new SlackBotCredentialVerificationError(
|
|
2511
|
+
"scope_mismatch",
|
|
2512
|
+
"Slack did not report granted bot scopes"
|
|
2513
|
+
);
|
|
2436
2514
|
}
|
|
2437
2515
|
return [
|
|
2438
2516
|
...new Set(
|
|
@@ -3049,6 +3127,19 @@ function registerToolspaceProxyTools(server, surface) {
|
|
|
3049
3127
|
if (!surface) {
|
|
3050
3128
|
return;
|
|
3051
3129
|
}
|
|
3130
|
+
if (surface.tools.length === 0) {
|
|
3131
|
+
server.registerTool(
|
|
3132
|
+
"__opengeni_empty_toolspace_surface__",
|
|
3133
|
+
{
|
|
3134
|
+
description: "Internal disabled placeholder for an empty Toolspace surface.",
|
|
3135
|
+
inputSchema: z4.object({})
|
|
3136
|
+
},
|
|
3137
|
+
async () => ({
|
|
3138
|
+
content: [{ type: "text", text: '{"unavailable":true}' }]
|
|
3139
|
+
})
|
|
3140
|
+
).disable();
|
|
3141
|
+
return;
|
|
3142
|
+
}
|
|
3052
3143
|
for (const tool of surface.tools) {
|
|
3053
3144
|
server.registerTool(
|
|
3054
3145
|
tool.name,
|
|
@@ -4887,13 +4978,25 @@ async function resolveToolListing(input) {
|
|
|
4887
4978
|
sessionId,
|
|
4888
4979
|
rootSessionId,
|
|
4889
4980
|
turn: activeTurn
|
|
4890
|
-
}).catch(() =>
|
|
4981
|
+
}).catch((error) => {
|
|
4982
|
+
deps.observability?.warn("toolspace upstream connection failed", {
|
|
4983
|
+
serverId,
|
|
4984
|
+
...toolspaceErrorAttributes(error)
|
|
4985
|
+
});
|
|
4986
|
+
return null;
|
|
4987
|
+
});
|
|
4891
4988
|
if (!connection) {
|
|
4892
4989
|
aggregateBudget.replace(serverId, []);
|
|
4893
4990
|
return;
|
|
4894
4991
|
}
|
|
4895
4992
|
try {
|
|
4896
|
-
const listed = await connection.client.listTools(void 0, toolspaceRequestOptions(config)).catch(() =>
|
|
4993
|
+
const listed = await connection.client.listTools(void 0, toolspaceRequestOptions(config)).catch((error) => {
|
|
4994
|
+
deps.observability?.warn("toolspace upstream tool list failed", {
|
|
4995
|
+
serverId,
|
|
4996
|
+
...toolspaceErrorAttributes(error)
|
|
4997
|
+
});
|
|
4998
|
+
return { tools: [] };
|
|
4999
|
+
});
|
|
4897
5000
|
let boundedTools;
|
|
4898
5001
|
try {
|
|
4899
5002
|
boundedTools = assertMcpToolListWithinBounds(listed.tools);
|
|
@@ -4980,7 +5083,11 @@ async function settingsWithSessionMcpServersForToolspace(deps, workspaceId, sess
|
|
|
4980
5083
|
};
|
|
4981
5084
|
}
|
|
4982
5085
|
async function connectToolspaceServer(input) {
|
|
4983
|
-
const
|
|
5086
|
+
const useBunNativeFetch = !!process.versions.bun;
|
|
5087
|
+
const mcpFetchImpl = useBunNativeFetch ? globalThis.fetch.bind(globalThis) : undiciFetch;
|
|
5088
|
+
const guardedFetch = guardedMcpFetch(input.deps.settings, mcpFetchImpl, {
|
|
5089
|
+
...useBunNativeFetch ? { pinResolvedDestination: false } : {}
|
|
5090
|
+
});
|
|
4984
5091
|
const baseFetch = input.config.connectionRef ? connectionBrokerFetch(guardedFetch, input) : guardedFetch;
|
|
4985
5092
|
const client = new Client(
|
|
4986
5093
|
{ name: `opengeni-toolspace-${input.config.id}`, version: "1.0.0" },
|
|
@@ -5186,6 +5293,17 @@ function toolspaceAuditSummary(value) {
|
|
|
5186
5293
|
sha256: createHash2("sha256").update(serialized).digest("hex")
|
|
5187
5294
|
};
|
|
5188
5295
|
}
|
|
5296
|
+
function toolspaceErrorAttributes(error) {
|
|
5297
|
+
if (!(error instanceof Error)) {
|
|
5298
|
+
return { errorClass: typeof error };
|
|
5299
|
+
}
|
|
5300
|
+
const code = error.code;
|
|
5301
|
+
return {
|
|
5302
|
+
errorClass: error.name,
|
|
5303
|
+
...typeof code === "string" ? { errorCode: code } : {},
|
|
5304
|
+
...error.message ? { errorMessage: error.message.replaceAll(/[\r\n]+/gu, " ").slice(0, 512) } : {}
|
|
5305
|
+
};
|
|
5306
|
+
}
|
|
5189
5307
|
async function reserveExactAttemptCall(deps, grant, authority) {
|
|
5190
5308
|
const reservation = await reserveToolspaceCallForAttempt(deps.db, {
|
|
5191
5309
|
accountId: grant.accountId,
|
|
@@ -5249,7 +5367,15 @@ function connectionBrokerFetch(baseFetch, input) {
|
|
|
5249
5367
|
if (!connectionRef) {
|
|
5250
5368
|
return baseFetch;
|
|
5251
5369
|
}
|
|
5252
|
-
const
|
|
5370
|
+
const credentialSubjectId = input.turn.initiator.kind === "subject" ? input.turn.initiator.subjectId : void 0;
|
|
5371
|
+
if (connectionRef.subjectScope === "subject" && !credentialSubjectId) {
|
|
5372
|
+
throw new Error(
|
|
5373
|
+
`subject-owned connection for MCP server ${input.config.id} requires a human turn initiator`
|
|
5374
|
+
);
|
|
5375
|
+
}
|
|
5376
|
+
const hostCredentialPort = input.deps.connectionCredentials?.mcpCredentials;
|
|
5377
|
+
const resolverSubjectId = hostCredentialPort ? input.grant.subjectId : credentialSubjectId;
|
|
5378
|
+
const resolveCredential = hostCredentialPort ? buildHostConnectionTokenResolver(hostCredentialPort, {
|
|
5253
5379
|
accountId: input.grant.accountId,
|
|
5254
5380
|
workspaceId: input.grant.workspaceId,
|
|
5255
5381
|
sessionId: input.sessionId,
|
|
@@ -5271,7 +5397,7 @@ function connectionBrokerFetch(baseFetch, input) {
|
|
|
5271
5397
|
destinationUrl,
|
|
5272
5398
|
forceRefresh: false,
|
|
5273
5399
|
...request.toolName ? { toolName: request.toolName } : {},
|
|
5274
|
-
subjectId:
|
|
5400
|
+
...resolverSubjectId ? { subjectId: resolverSubjectId } : {}
|
|
5275
5401
|
});
|
|
5276
5402
|
if (first.status === "auth_needed") {
|
|
5277
5403
|
return await authNeededFetchResponse(input, request, first);
|
|
@@ -5289,7 +5415,7 @@ function connectionBrokerFetch(baseFetch, input) {
|
|
|
5289
5415
|
destinationUrl,
|
|
5290
5416
|
forceRefresh: true,
|
|
5291
5417
|
...request.toolName ? { toolName: request.toolName } : {},
|
|
5292
|
-
subjectId:
|
|
5418
|
+
...resolverSubjectId ? { subjectId: resolverSubjectId } : {}
|
|
5293
5419
|
});
|
|
5294
5420
|
if (refreshed.status === "auth_needed") {
|
|
5295
5421
|
return await authNeededFetchResponse(input, request, refreshed);
|
|
@@ -5573,7 +5699,7 @@ function isAuthExempt(c, settings) {
|
|
|
5573
5699
|
if (path === "/v1/github/setup" || path === "/v1/github/install/callback" || path === "/v1/github/oauth/callback" || path === "/v1/github/app-manifest/callback") {
|
|
5574
5700
|
return true;
|
|
5575
5701
|
}
|
|
5576
|
-
if (path === "/v1/integrations/oauth/callback" || path === "/v1/integrations/oauth/client-metadata.json") {
|
|
5702
|
+
if (path === "/v1/integrations/oauth/callback" || path === "/v1/integrations/oauth/client-metadata.json" || path === "/v1/integrations/slack/callback") {
|
|
5577
5703
|
return true;
|
|
5578
5704
|
}
|
|
5579
5705
|
if (path.startsWith("/v1/catalog-assets/")) {
|
|
@@ -6991,17 +7117,20 @@ function registerCodexRoutes(app, deps) {
|
|
|
6991
7117
|
}
|
|
6992
7118
|
|
|
6993
7119
|
// src/routes/connections.ts
|
|
7120
|
+
import { createHash as createHash4 } from "crypto";
|
|
6994
7121
|
import {
|
|
6995
|
-
ConnectOpenGeniSlackBotRequest,
|
|
6996
7122
|
ConnectionResponse,
|
|
6997
7123
|
CreateConnectionRequest,
|
|
6998
7124
|
IntegrationClientMetadata,
|
|
6999
7125
|
ListConnectionsResponse,
|
|
7126
|
+
OpenGeniSlackBotInstallRequest,
|
|
7127
|
+
OpenGeniSlackBotInstallStart,
|
|
7000
7128
|
OAuthStartRequest,
|
|
7001
7129
|
OAuthStartResponse as OAuthStartResponse2,
|
|
7002
7130
|
UpdateConnectionRequest
|
|
7003
7131
|
} from "@opengeni/contracts";
|
|
7004
7132
|
import {
|
|
7133
|
+
hasPermission as hasPermission6,
|
|
7005
7134
|
hasReservedOpenGeniSlackBotMetadata,
|
|
7006
7135
|
isOpenGeniSlackBotConnection,
|
|
7007
7136
|
openGeniSlackBotMetadata as openGeniSlackBotMetadata2,
|
|
@@ -7009,13 +7138,19 @@ import {
|
|
|
7009
7138
|
requireEnvironmentEncryption as requireEnvironmentEncryption2
|
|
7010
7139
|
} from "@opengeni/core";
|
|
7011
7140
|
import {
|
|
7141
|
+
consumeIntegrationOAuthStateNonce as consumeIntegrationOAuthStateNonce2,
|
|
7012
7142
|
createConnection as createConnection2,
|
|
7143
|
+
createConnectionWithSlackBotSuccessAudit,
|
|
7013
7144
|
encryptEnvironmentValue as encryptEnvironmentValue3,
|
|
7014
7145
|
getConnectionMetadata as getConnectionMetadata2,
|
|
7146
|
+
getWorkspaceGrant as getWorkspaceGrant2,
|
|
7015
7147
|
listConnectionsMetadata as listConnectionsMetadata2,
|
|
7016
|
-
|
|
7148
|
+
recordSlackBotInstallCallbackFailure,
|
|
7017
7149
|
revokeConnection,
|
|
7018
|
-
|
|
7150
|
+
revokeConnectionWithSlackBotSuccessAudit,
|
|
7151
|
+
SlackBotLifecycleSuccessAuditError,
|
|
7152
|
+
updateConnection as updateConnection2,
|
|
7153
|
+
updateConnectionWithSlackBotSuccessAudit
|
|
7019
7154
|
} from "@opengeni/db";
|
|
7020
7155
|
import { HTTPException as HTTPException9 } from "hono/http-exception";
|
|
7021
7156
|
|
|
@@ -7024,13 +7159,14 @@ import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
7024
7159
|
import { StreamableHTTPClientTransport as StreamableHTTPClientTransport2 } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
7025
7160
|
import { parseIntegrationsOauthClientsJson } from "@opengeni/config";
|
|
7026
7161
|
import { OAuthStartResponse } from "@opengeni/contracts";
|
|
7027
|
-
import { requireEnvironmentEncryption } from "@opengeni/core";
|
|
7162
|
+
import { hasPermission as hasPermission5, requireEnvironmentEncryption } from "@opengeni/core";
|
|
7028
7163
|
import {
|
|
7029
7164
|
consumeIntegrationOAuthStateNonce,
|
|
7030
7165
|
createConnection,
|
|
7031
7166
|
decryptEnvironmentValue,
|
|
7032
7167
|
encryptEnvironmentValue as encryptEnvironmentValue2,
|
|
7033
7168
|
getConnectionMetadata,
|
|
7169
|
+
getWorkspaceGrant,
|
|
7034
7170
|
listConnectionsMetadata,
|
|
7035
7171
|
loadIntegrationOAuthClient,
|
|
7036
7172
|
normalizeBearerScheme,
|
|
@@ -7063,6 +7199,8 @@ function canonicalProviderDomain(value) {
|
|
|
7063
7199
|
// src/integrations/oauth-client.ts
|
|
7064
7200
|
import { OAUTH_MAX_RESPONSE_BYTES as OAUTH_MAX_RESPONSE_BYTES2 } from "@opengeni/network";
|
|
7065
7201
|
var oauthStateTtlMs = 10 * 60 * 1e3;
|
|
7202
|
+
var OFFICIAL_SLACK_MCP_URL = "https://mcp.slack.com/mcp";
|
|
7203
|
+
var SLACK_OAUTH_ORIGIN = "https://slack.com";
|
|
7066
7204
|
var OAuthCallbackStageError = class extends Error {
|
|
7067
7205
|
constructor(stage, reason, cause) {
|
|
7068
7206
|
super(errorMessage(cause));
|
|
@@ -7075,9 +7213,10 @@ var OAuthCallbackStageError = class extends Error {
|
|
|
7075
7213
|
async function startMcpOAuth(deps, context) {
|
|
7076
7214
|
const { db, settings } = deps;
|
|
7077
7215
|
const mcpUrl = canonicalMcpResource(context.payload.mcpUrl ?? context.payload.resource);
|
|
7078
|
-
const
|
|
7079
|
-
|
|
7080
|
-
|
|
7216
|
+
const officialSlackResource = mcpUrl === OFFICIAL_SLACK_MCP_URL;
|
|
7217
|
+
const providerDomain = officialSlackResource ? "slack.com" : canonicalProviderDomain(context.payload.providerDomain ?? new URL(mcpUrl).hostname);
|
|
7218
|
+
const personalSlack = officialSlackResource || providerDomain === "slack.com";
|
|
7219
|
+
assertPersonalSlackOAuthStart(settings, context.payload, mcpUrl, personalSlack);
|
|
7081
7220
|
const returnPath = safeReturnPath(context.payload.returnPath ?? "/integrations");
|
|
7082
7221
|
const baseUrl = integrationBaseUrl(settings.publicBaseUrl, context.requestUrl);
|
|
7083
7222
|
const redirectUri = `${baseUrl}/v1/integrations/oauth/callback`;
|
|
@@ -7092,6 +7231,9 @@ async function startMcpOAuth(deps, context) {
|
|
|
7092
7231
|
throw new HTTPException8(404, { message: "connection not found" });
|
|
7093
7232
|
}
|
|
7094
7233
|
const discovery = await discoverMcpOAuth(mcpUrl, settings);
|
|
7234
|
+
if (personalSlack && !isLocalTestEnvironment(settings.environment)) {
|
|
7235
|
+
assertSlackAuthorizationServer(discovery.as);
|
|
7236
|
+
}
|
|
7095
7237
|
const resource = discovery.prm.resource ? canonicalOAuthResource(discovery.prm.resource) : mcpUrl;
|
|
7096
7238
|
const verifier = randomPkceVerifier();
|
|
7097
7239
|
const authorizeScopes = chooseAuthorizeScopes(
|
|
@@ -7159,6 +7301,7 @@ async function completeMcpOAuthCallback(deps, input) {
|
|
|
7159
7301
|
}
|
|
7160
7302
|
try {
|
|
7161
7303
|
state = readOAuthState(input.state, settings);
|
|
7304
|
+
await requireOAuthCallbackGrant(db, state);
|
|
7162
7305
|
if (!input.code) {
|
|
7163
7306
|
return {
|
|
7164
7307
|
redirectTo: callbackReturnPath(state.returnPath, "error", { reason: "missing_code" })
|
|
@@ -7217,6 +7360,7 @@ async function completeMcpOAuthCallback(deps, input) {
|
|
|
7217
7360
|
...verification.tools ? { mcpTools: verification.tools } : {}
|
|
7218
7361
|
};
|
|
7219
7362
|
const credentialEncrypted = encryptEnvironmentValue2(key, JSON.stringify(credential));
|
|
7363
|
+
await requireOAuthCallbackGrant(db, state);
|
|
7220
7364
|
const connection = await runCallbackStage(
|
|
7221
7365
|
"persist",
|
|
7222
7366
|
"persist_failed",
|
|
@@ -7225,6 +7369,7 @@ async function completeMcpOAuthCallback(deps, input) {
|
|
|
7225
7369
|
connectionId: state.connectionId,
|
|
7226
7370
|
visibleToSubjectId: state.subjectId,
|
|
7227
7371
|
expectedVersion: state.connectionVersion,
|
|
7372
|
+
subjectId: state.subjectId,
|
|
7228
7373
|
providerDomain: state.providerDomain,
|
|
7229
7374
|
kind: "oauth2",
|
|
7230
7375
|
status: "active",
|
|
@@ -7236,7 +7381,7 @@ async function completeMcpOAuthCallback(deps, input) {
|
|
|
7236
7381
|
}) : createConnection(db, {
|
|
7237
7382
|
accountId: state.accountId,
|
|
7238
7383
|
workspaceId: state.workspaceId,
|
|
7239
|
-
subjectId:
|
|
7384
|
+
subjectId: state.subjectId,
|
|
7240
7385
|
providerDomain: state.providerDomain,
|
|
7241
7386
|
kind: "oauth2",
|
|
7242
7387
|
credentialEncrypted,
|
|
@@ -7276,6 +7421,43 @@ function requireIntegrationsStateSecret(settings) {
|
|
|
7276
7421
|
}
|
|
7277
7422
|
return secret;
|
|
7278
7423
|
}
|
|
7424
|
+
async function requireOAuthCallbackGrant(db, state) {
|
|
7425
|
+
const grant = await getWorkspaceGrant(db, state.subjectId, state.workspaceId);
|
|
7426
|
+
if (!grant || grant.accountId !== state.accountId || !hasPermission5(grant.permissions, "connections:write")) {
|
|
7427
|
+
throw new HTTPException8(403, {
|
|
7428
|
+
message: "OAuth subject no longer has permission to write this workspace connection"
|
|
7429
|
+
});
|
|
7430
|
+
}
|
|
7431
|
+
}
|
|
7432
|
+
function assertPersonalSlackOAuthStart(settings, payload, mcpUrl, personalSlack) {
|
|
7433
|
+
if (!personalSlack) return;
|
|
7434
|
+
if (payload.oauthClient) {
|
|
7435
|
+
throw new HTTPException8(422, {
|
|
7436
|
+
message: "Slack OAuth client credentials are deployment-managed"
|
|
7437
|
+
});
|
|
7438
|
+
}
|
|
7439
|
+
if (payload.providerDomain && canonicalProviderDomain(payload.providerDomain) !== "slack.com") {
|
|
7440
|
+
throw new HTTPException8(422, { message: "Slack provider identity does not match slack.com" });
|
|
7441
|
+
}
|
|
7442
|
+
if (!isLocalTestEnvironment(settings.environment) && mcpUrl !== OFFICIAL_SLACK_MCP_URL) {
|
|
7443
|
+
throw new HTTPException8(422, {
|
|
7444
|
+
message: `personal Slack OAuth must use ${OFFICIAL_SLACK_MCP_URL}`
|
|
7445
|
+
});
|
|
7446
|
+
}
|
|
7447
|
+
if (!settings.slackClientId?.trim() || !settings.slackClientSecret?.trim()) {
|
|
7448
|
+
throw new HTTPException8(503, {
|
|
7449
|
+
message: "personal Slack OAuth requires OPENGENI_SLACK_CLIENT_ID and OPENGENI_SLACK_CLIENT_SECRET"
|
|
7450
|
+
});
|
|
7451
|
+
}
|
|
7452
|
+
}
|
|
7453
|
+
function assertSlackAuthorizationServer(as) {
|
|
7454
|
+
const urls = [as.issuer, as.authorizationServer, as.authorizationEndpoint, as.tokenEndpoint];
|
|
7455
|
+
if (urls.some((value) => new URL(value).origin !== SLACK_OAUTH_ORIGIN)) {
|
|
7456
|
+
throw new HTTPException8(422, {
|
|
7457
|
+
message: "Slack MCP authorization metadata did not remain bound to slack.com"
|
|
7458
|
+
});
|
|
7459
|
+
}
|
|
7460
|
+
}
|
|
7279
7461
|
async function discoverMcpOAuth(resource, settings) {
|
|
7280
7462
|
const challenge = await probeMcpChallenge(resource, settings);
|
|
7281
7463
|
const prm = await discoverProtectedResourceMetadata(
|
|
@@ -7512,6 +7694,14 @@ function operatorClientForAs(settings, as) {
|
|
|
7512
7694
|
};
|
|
7513
7695
|
}
|
|
7514
7696
|
function operatorClientEntryFor(settings, candidates) {
|
|
7697
|
+
const normalizedCandidates = new Set(candidates.map(normalizedIssuerKey));
|
|
7698
|
+
if (normalizedCandidates.has(SLACK_OAUTH_ORIGIN) && settings.slackClientId?.trim() && settings.slackClientSecret?.trim()) {
|
|
7699
|
+
return {
|
|
7700
|
+
clientId: settings.slackClientId.trim(),
|
|
7701
|
+
clientSecret: settings.slackClientSecret.trim(),
|
|
7702
|
+
tokenEndpointAuthMethod: "client_secret_post"
|
|
7703
|
+
};
|
|
7704
|
+
}
|
|
7515
7705
|
const configured = parseIntegrationsOauthClientsJson(settings.integrationsOauthClientsJson);
|
|
7516
7706
|
const exactKeys = uniqueStrings(
|
|
7517
7707
|
candidates.flatMap((candidate) => [candidate, normalizedIssuerKey(candidate)])
|
|
@@ -7522,7 +7712,6 @@ function operatorClientEntryFor(settings, candidates) {
|
|
|
7522
7712
|
return entry;
|
|
7523
7713
|
}
|
|
7524
7714
|
}
|
|
7525
|
-
const normalizedCandidates = new Set(candidates.map(normalizedIssuerKey));
|
|
7526
7715
|
for (const [key, entry] of Object.entries(configured)) {
|
|
7527
7716
|
if (normalizedCandidates.has(normalizedIssuerKey(key))) {
|
|
7528
7717
|
return entry;
|
|
@@ -7583,11 +7772,17 @@ async function dynamicClientRegistration(settings, as, redirectUri, scopes) {
|
|
|
7583
7772
|
}
|
|
7584
7773
|
async function existingOAuthConnectionForStart(db, input) {
|
|
7585
7774
|
if (input.connectionId) {
|
|
7586
|
-
|
|
7775
|
+
const connection = await getConnectionMetadata(
|
|
7776
|
+
db,
|
|
7777
|
+
input.workspaceId,
|
|
7778
|
+
input.connectionId,
|
|
7779
|
+
input.subjectId
|
|
7780
|
+
);
|
|
7781
|
+
return connection?.subjectId === input.subjectId && connection.kind === "oauth2" && connection.providerDomain === input.providerDomain ? connection : null;
|
|
7587
7782
|
}
|
|
7588
7783
|
const visible = await listConnectionsMetadata(db, input.workspaceId, input.subjectId);
|
|
7589
7784
|
return visible.find(
|
|
7590
|
-
(connection) => connection.subjectId ===
|
|
7785
|
+
(connection) => connection.subjectId === input.subjectId && connection.kind === "oauth2" && connection.status === "active" && connection.providerDomain === input.providerDomain
|
|
7591
7786
|
) ?? null;
|
|
7592
7787
|
}
|
|
7593
7788
|
function buildAuthorizationUrl(input) {
|
|
@@ -7654,6 +7849,9 @@ function readOAuthState(state, settings) {
|
|
|
7654
7849
|
};
|
|
7655
7850
|
const connectionId = stringValue(payload.connectionId);
|
|
7656
7851
|
const connectionVersion = numberValue(payload.connectionVersion);
|
|
7852
|
+
if (Boolean(connectionId) !== Boolean(connectionVersion)) {
|
|
7853
|
+
throw new HTTPException8(400, { message: "invalid OAuth reconnect state" });
|
|
7854
|
+
}
|
|
7657
7855
|
return {
|
|
7658
7856
|
...parsed,
|
|
7659
7857
|
...connectionId ? { connectionId } : {},
|
|
@@ -8140,8 +8338,10 @@ function requiredString(value, field) {
|
|
|
8140
8338
|
// src/routes/connections.ts
|
|
8141
8339
|
import {
|
|
8142
8340
|
OPENGENI_SLACK_BOT_CREDENTIAL_LABEL as OPENGENI_SLACK_BOT_CREDENTIAL_LABEL2,
|
|
8143
|
-
OPENGENI_SLACK_BOT_CREDENTIAL_ROLE as OPENGENI_SLACK_BOT_CREDENTIAL_ROLE2
|
|
8341
|
+
OPENGENI_SLACK_BOT_CREDENTIAL_ROLE as OPENGENI_SLACK_BOT_CREDENTIAL_ROLE2,
|
|
8342
|
+
OPENGENI_SLACK_BOT_REQUIRED_SCOPES as OPENGENI_SLACK_BOT_REQUIRED_SCOPES2
|
|
8144
8343
|
} from "@opengeni/contracts";
|
|
8344
|
+
import { createSignedState as createSignedState4, readSignedState as readSignedState3 } from "@opengeni/github";
|
|
8145
8345
|
function registerConnectionRoutes(app, deps) {
|
|
8146
8346
|
const { db, settings, observability } = deps;
|
|
8147
8347
|
function assertIntegrationsEnabled() {
|
|
@@ -8165,11 +8365,13 @@ function registerConnectionRoutes(app, deps) {
|
|
|
8165
8365
|
assertNotReservedSlackBotMetadata(payload.metadata);
|
|
8166
8366
|
const key = requireEnvironmentEncryption2(settings);
|
|
8167
8367
|
const subjectId = writableSubjectId(payload.subjectId, grant.subjectId);
|
|
8368
|
+
const providerDomain = canonicalProviderDomain(payload.providerDomain);
|
|
8369
|
+
assertNotDirectPersonalSlackOAuth(providerDomain, payload.kind);
|
|
8168
8370
|
const connection = await createConnection2(db, {
|
|
8169
8371
|
accountId: grant.accountId,
|
|
8170
8372
|
workspaceId,
|
|
8171
8373
|
subjectId,
|
|
8172
|
-
providerDomain
|
|
8374
|
+
providerDomain,
|
|
8173
8375
|
kind: payload.kind,
|
|
8174
8376
|
credentialEncrypted: encryptCredentialBundle(key, payload.credential),
|
|
8175
8377
|
grantedScopes: payload.grantedScopes,
|
|
@@ -8179,19 +8381,11 @@ function registerConnectionRoutes(app, deps) {
|
|
|
8179
8381
|
});
|
|
8180
8382
|
return c.json(ConnectionResponse.parse({ connection }), 201);
|
|
8181
8383
|
});
|
|
8182
|
-
app.post("/v1/workspaces/:workspaceId/connections/slack-bot", async (c) => {
|
|
8384
|
+
app.post("/v1/workspaces/:workspaceId/connections/slack-bot/install", async (c) => {
|
|
8183
8385
|
const workspaceId = c.req.param("workspaceId");
|
|
8184
8386
|
const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:write");
|
|
8185
|
-
const payload =
|
|
8186
|
-
const
|
|
8187
|
-
payload.token,
|
|
8188
|
-
deps.slackFetch ?? fetch
|
|
8189
|
-
);
|
|
8190
|
-
const key = requireEnvironmentEncryption2(settings);
|
|
8191
|
-
const credentialEncrypted = encryptCredentialBundle(
|
|
8192
|
-
key,
|
|
8193
|
-
slackBotCredentialBundle(payload.token)
|
|
8194
|
-
);
|
|
8387
|
+
const payload = OpenGeniSlackBotInstallRequest.parse(await c.req.json());
|
|
8388
|
+
const slack = requireOpenGeniSlackOAuthSettings(settings);
|
|
8195
8389
|
const existing = payload.connectionId ? await getConnectionMetadata2(db, workspaceId, payload.connectionId, grant.subjectId) : null;
|
|
8196
8390
|
if (payload.connectionId && !existing) {
|
|
8197
8391
|
throw new HTTPException9(404, { message: "connection not found" });
|
|
@@ -8201,69 +8395,119 @@ function registerConnectionRoutes(app, deps) {
|
|
|
8201
8395
|
message: "connectionId is not an OpenGeni Slack bot connection"
|
|
8202
8396
|
});
|
|
8203
8397
|
}
|
|
8204
|
-
const
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
});
|
|
8209
|
-
}
|
|
8210
|
-
if (existingMetadata && (existingMetadata.botId !== verified.metadata.botId || existingMetadata.botUserId !== verified.metadata.botUserId)) {
|
|
8211
|
-
throw new HTTPException9(409, {
|
|
8212
|
-
message: "a different Slack bot requires a new connection and explicit scheduled-task rebinding"
|
|
8213
|
-
});
|
|
8214
|
-
}
|
|
8215
|
-
const verifiedInstallAt = new Date(verified.metadata.verifiedAt);
|
|
8216
|
-
const connection = existing ? await updateConnection2(db, {
|
|
8217
|
-
workspaceId,
|
|
8218
|
-
connectionId: existing.id,
|
|
8219
|
-
visibleToSubjectId: grant.subjectId,
|
|
8220
|
-
expectedVersion: existing.version,
|
|
8221
|
-
subjectId: null,
|
|
8222
|
-
providerDomain: "slack.com",
|
|
8223
|
-
kind: "app_install",
|
|
8224
|
-
status: "active",
|
|
8225
|
-
credentialEncrypted,
|
|
8226
|
-
grantedScopes: verified.grantedScopes,
|
|
8227
|
-
expiresAt: null,
|
|
8228
|
-
verifiedInstallAt,
|
|
8229
|
-
verifiedInstallVersion: existing.version + 1,
|
|
8230
|
-
metadata: verified.metadata,
|
|
8231
|
-
updatedBySubjectId: grant.subjectId
|
|
8232
|
-
}) : await createConnection2(db, {
|
|
8398
|
+
const baseUrl = integrationBaseUrl(settings.publicBaseUrl, c.req.url);
|
|
8399
|
+
const redirectUri = `${baseUrl}/v1/integrations/slack/callback`;
|
|
8400
|
+
const returnPath = `/workspaces/${workspaceId}/capabilities`;
|
|
8401
|
+
const state = createSignedState4(requireIntegrationsStateSecret(settings), {
|
|
8233
8402
|
accountId: grant.accountId,
|
|
8234
8403
|
workspaceId,
|
|
8235
|
-
subjectId:
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
credentialEncrypted,
|
|
8239
|
-
grantedScopes: verified.grantedScopes,
|
|
8240
|
-
expiresAt: null,
|
|
8241
|
-
verifiedInstallAt,
|
|
8242
|
-
verifiedInstallVersion: 1,
|
|
8243
|
-
metadata: verified.metadata,
|
|
8244
|
-
createdBySubjectId: grant.subjectId
|
|
8404
|
+
subjectId: grant.subjectId,
|
|
8405
|
+
returnPath,
|
|
8406
|
+
...existing ? { connectionId: existing.id, connectionVersion: existing.version } : {}
|
|
8245
8407
|
});
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8408
|
+
const authorizationUrl = new URL("https://slack.com/oauth/v2/authorize");
|
|
8409
|
+
authorizationUrl.searchParams.set("client_id", slack.clientId);
|
|
8410
|
+
authorizationUrl.searchParams.set("scope", OPENGENI_SLACK_BOT_REQUIRED_SCOPES2.join(","));
|
|
8411
|
+
authorizationUrl.searchParams.set("redirect_uri", redirectUri);
|
|
8412
|
+
authorizationUrl.searchParams.set("state", state);
|
|
8413
|
+
return c.json(
|
|
8414
|
+
OpenGeniSlackBotInstallStart.parse({
|
|
8415
|
+
authorizationUrl: authorizationUrl.toString(),
|
|
8416
|
+
expiresAt: new Date(Date.now() + oauthStateTtlMs).toISOString()
|
|
8417
|
+
})
|
|
8418
|
+
);
|
|
8419
|
+
});
|
|
8420
|
+
app.get("/v1/integrations/slack/callback", async (c) => {
|
|
8421
|
+
const baseUrl = integrationBaseUrl(settings.publicBaseUrl, c.req.url);
|
|
8422
|
+
let state = null;
|
|
8423
|
+
let stage = "permission_check";
|
|
8424
|
+
try {
|
|
8425
|
+
state = readOpenGeniSlackInstallState(c.req.query("state"), settings);
|
|
8426
|
+
await requireSlackInstallCallbackGrant(db, state);
|
|
8427
|
+
stage = "nonce_consume";
|
|
8428
|
+
const consumed = await consumeIntegrationOAuthStateNonce2(db, {
|
|
8429
|
+
accountId: state.accountId,
|
|
8430
|
+
workspaceId: state.workspaceId,
|
|
8431
|
+
subjectId: state.subjectId,
|
|
8432
|
+
nonce: state.nonce,
|
|
8433
|
+
expiresAt: new Date(state.iat * 1e3 + oauthStateTtlMs),
|
|
8434
|
+
now: /* @__PURE__ */ new Date()
|
|
8249
8435
|
});
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
targetType: "connection",
|
|
8257
|
-
targetId: connection.id,
|
|
8258
|
-
metadata: {
|
|
8259
|
-
credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE2,
|
|
8260
|
-
credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL2,
|
|
8261
|
-
connectionId: connection.id,
|
|
8262
|
-
slackTeamId: verified.metadata.slackTeamId,
|
|
8263
|
-
outcome: "succeeded"
|
|
8436
|
+
if (!consumed) {
|
|
8437
|
+
throw new SlackInstallCallbackError(
|
|
8438
|
+
400,
|
|
8439
|
+
"state_replayed",
|
|
8440
|
+
"Slack installation state has already been used"
|
|
8441
|
+
);
|
|
8264
8442
|
}
|
|
8265
|
-
|
|
8266
|
-
|
|
8443
|
+
if (c.req.query("error")) {
|
|
8444
|
+
stage = "provider_denial";
|
|
8445
|
+
throw new SlackInstallCallbackError(
|
|
8446
|
+
400,
|
|
8447
|
+
"provider_denied",
|
|
8448
|
+
"Slack installation authorization was denied"
|
|
8449
|
+
);
|
|
8450
|
+
}
|
|
8451
|
+
stage = "code_exchange";
|
|
8452
|
+
const code = c.req.query("code");
|
|
8453
|
+
if (!code) {
|
|
8454
|
+
throw new SlackInstallCallbackError(
|
|
8455
|
+
400,
|
|
8456
|
+
"missing_code",
|
|
8457
|
+
"Slack installation callback is missing code"
|
|
8458
|
+
);
|
|
8459
|
+
}
|
|
8460
|
+
const slack = requireOpenGeniSlackOAuthSettings(settings);
|
|
8461
|
+
const redirectUri = `${baseUrl}/v1/integrations/slack/callback`;
|
|
8462
|
+
const token = await exchangeOpenGeniSlackAuthorizationCode(
|
|
8463
|
+
{
|
|
8464
|
+
code,
|
|
8465
|
+
clientId: slack.clientId,
|
|
8466
|
+
clientSecret: slack.clientSecret,
|
|
8467
|
+
redirectUri
|
|
8468
|
+
},
|
|
8469
|
+
deps.slackFetch ?? fetch
|
|
8470
|
+
);
|
|
8471
|
+
stage = "credential_verification";
|
|
8472
|
+
const verified = await verifyOpenGeniSlackBotCredential(token, deps.slackFetch ?? fetch);
|
|
8473
|
+
stage = "permission_recheck";
|
|
8474
|
+
await requireSlackInstallCallbackGrant(db, state);
|
|
8475
|
+
stage = "persistence";
|
|
8476
|
+
const connection = await persistOpenGeniSlackBotConnection({
|
|
8477
|
+
deps,
|
|
8478
|
+
state,
|
|
8479
|
+
token,
|
|
8480
|
+
verified
|
|
8481
|
+
});
|
|
8482
|
+
return c.redirect(
|
|
8483
|
+
slackInstallReturnUrl(baseUrl, state.returnPath, "connected", connection.id),
|
|
8484
|
+
302
|
|
8485
|
+
);
|
|
8486
|
+
} catch (error) {
|
|
8487
|
+
if (state) {
|
|
8488
|
+
const failure = slackInstallCallbackFailure(stage, error);
|
|
8489
|
+
try {
|
|
8490
|
+
await recordSlackBotInstallCallbackFailure(db, {
|
|
8491
|
+
accountId: state.accountId,
|
|
8492
|
+
workspaceId: state.workspaceId,
|
|
8493
|
+
subjectId: state.subjectId,
|
|
8494
|
+
callbackDigest: createHash4("sha256").update(state.nonce).digest("hex"),
|
|
8495
|
+
installMode: state.connectionId ? "reinstall" : "connect",
|
|
8496
|
+
...failure
|
|
8497
|
+
});
|
|
8498
|
+
} catch {
|
|
8499
|
+
return c.redirect(
|
|
8500
|
+
slackInstallReturnUrl(baseUrl, state.returnPath, "error", "installation_failed"),
|
|
8501
|
+
302
|
|
8502
|
+
);
|
|
8503
|
+
}
|
|
8504
|
+
}
|
|
8505
|
+
const reason = slackInstallErrorReason(error);
|
|
8506
|
+
return c.redirect(
|
|
8507
|
+
slackInstallReturnUrl(baseUrl, state?.returnPath ?? "/integrations", "error", reason),
|
|
8508
|
+
302
|
|
8509
|
+
);
|
|
8510
|
+
}
|
|
8267
8511
|
});
|
|
8268
8512
|
app.get("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
|
|
8269
8513
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -8295,6 +8539,12 @@ function registerConnectionRoutes(app, deps) {
|
|
|
8295
8539
|
message: "use the dedicated OpenGeni Slack bot reinstall flow to update this connection"
|
|
8296
8540
|
});
|
|
8297
8541
|
}
|
|
8542
|
+
if (existing) {
|
|
8543
|
+
assertNotDirectPersonalSlackOAuth(
|
|
8544
|
+
canonicalProviderDomain(payload.providerDomain ?? existing.providerDomain),
|
|
8545
|
+
payload.kind ?? existing.kind
|
|
8546
|
+
);
|
|
8547
|
+
}
|
|
8298
8548
|
if (payload.status !== void 0) {
|
|
8299
8549
|
if (payload.status !== "active") {
|
|
8300
8550
|
throw new HTTPException9(400, {
|
|
@@ -8330,33 +8580,24 @@ function registerConnectionRoutes(app, deps) {
|
|
|
8330
8580
|
});
|
|
8331
8581
|
app.delete("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
|
|
8332
8582
|
const workspaceId = c.req.param("workspaceId");
|
|
8583
|
+
const connectionId = c.req.param("connectionId");
|
|
8333
8584
|
const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:write");
|
|
8334
|
-
const
|
|
8335
|
-
|
|
8336
|
-
workspaceId,
|
|
8337
|
-
c.req.param("connectionId"),
|
|
8338
|
-
grant.subjectId
|
|
8339
|
-
);
|
|
8340
|
-
if (!connection) {
|
|
8585
|
+
const existing = await getConnectionMetadata2(db, workspaceId, connectionId, grant.subjectId);
|
|
8586
|
+
if (!existing) {
|
|
8341
8587
|
throw new HTTPException9(404, { message: "connection not found" });
|
|
8342
8588
|
}
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
connectionId: connection.id,
|
|
8356
|
-
slackTeamId: metadata.slackTeamId,
|
|
8357
|
-
outcome: "succeeded"
|
|
8358
|
-
}
|
|
8359
|
-
});
|
|
8589
|
+
const connection = isOpenGeniSlackBotConnection(existing) ? await revokeConnectionWithSlackBotSuccessAudit(db, {
|
|
8590
|
+
accountId: grant.accountId,
|
|
8591
|
+
workspaceId,
|
|
8592
|
+
subjectId: grant.subjectId,
|
|
8593
|
+
connectionId,
|
|
8594
|
+
expectedVersion: existing.version,
|
|
8595
|
+
credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE2,
|
|
8596
|
+
credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL2,
|
|
8597
|
+
slackTeamId: openGeniSlackBotMetadata2(existing.metadata).slackTeamId
|
|
8598
|
+
}) : await revokeConnection(db, workspaceId, connectionId, grant.subjectId);
|
|
8599
|
+
if (!connection) {
|
|
8600
|
+
throw new HTTPException9(409, { message: "connection changed during disconnect; try again" });
|
|
8360
8601
|
}
|
|
8361
8602
|
return c.json(ConnectionResponse.parse({ connection }));
|
|
8362
8603
|
});
|
|
@@ -8410,6 +8651,222 @@ function registerConnectionRoutes(app, deps) {
|
|
|
8410
8651
|
);
|
|
8411
8652
|
});
|
|
8412
8653
|
}
|
|
8654
|
+
async function persistOpenGeniSlackBotConnection(input) {
|
|
8655
|
+
const { db, settings } = input.deps;
|
|
8656
|
+
const key = requireEnvironmentEncryption2(settings);
|
|
8657
|
+
const credentialEncrypted = encryptCredentialBundle(key, slackBotCredentialBundle(input.token));
|
|
8658
|
+
const existing = input.state.connectionId ? await getConnectionMetadata2(
|
|
8659
|
+
db,
|
|
8660
|
+
input.state.workspaceId,
|
|
8661
|
+
input.state.connectionId,
|
|
8662
|
+
input.state.subjectId
|
|
8663
|
+
) : null;
|
|
8664
|
+
if (input.state.connectionId && !existing) {
|
|
8665
|
+
throw new SlackInstallCallbackError(
|
|
8666
|
+
404,
|
|
8667
|
+
"connection_conflict",
|
|
8668
|
+
"connection not found",
|
|
8669
|
+
"principal_validation"
|
|
8670
|
+
);
|
|
8671
|
+
}
|
|
8672
|
+
if (existing && !isOpenGeniSlackBotConnection(existing)) {
|
|
8673
|
+
throw new SlackInstallCallbackError(
|
|
8674
|
+
422,
|
|
8675
|
+
"connection_conflict",
|
|
8676
|
+
"connectionId is not an OpenGeni Slack bot connection",
|
|
8677
|
+
"principal_validation"
|
|
8678
|
+
);
|
|
8679
|
+
}
|
|
8680
|
+
if (existing?.version !== input.state.connectionVersion) {
|
|
8681
|
+
throw new SlackInstallCallbackError(
|
|
8682
|
+
409,
|
|
8683
|
+
"connection_conflict",
|
|
8684
|
+
"Slack bot connection changed during reinstall; start again",
|
|
8685
|
+
"principal_validation"
|
|
8686
|
+
);
|
|
8687
|
+
}
|
|
8688
|
+
const existingMetadata = existing ? openGeniSlackBotMetadata2(existing.metadata) : null;
|
|
8689
|
+
if (existingMetadata && existingMetadata.slackTeamId !== input.verified.metadata.slackTeamId) {
|
|
8690
|
+
throw new SlackInstallCallbackError(
|
|
8691
|
+
409,
|
|
8692
|
+
"principal_mismatch",
|
|
8693
|
+
"a Slack bot connection can only be reinstalled for its original Slack workspace",
|
|
8694
|
+
"principal_validation"
|
|
8695
|
+
);
|
|
8696
|
+
}
|
|
8697
|
+
if (existingMetadata && (existingMetadata.botId !== input.verified.metadata.botId || existingMetadata.botUserId !== input.verified.metadata.botUserId)) {
|
|
8698
|
+
throw new SlackInstallCallbackError(
|
|
8699
|
+
409,
|
|
8700
|
+
"principal_mismatch",
|
|
8701
|
+
"a different Slack bot requires a new connection and explicit scheduled-task rebinding",
|
|
8702
|
+
"principal_validation"
|
|
8703
|
+
);
|
|
8704
|
+
}
|
|
8705
|
+
const verifiedInstallAt = new Date(input.verified.metadata.verifiedAt);
|
|
8706
|
+
const lifecycleAudit = {
|
|
8707
|
+
accountId: input.state.accountId,
|
|
8708
|
+
workspaceId: input.state.workspaceId,
|
|
8709
|
+
subjectId: input.state.subjectId,
|
|
8710
|
+
credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE2,
|
|
8711
|
+
credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL2,
|
|
8712
|
+
slackTeamId: input.verified.metadata.slackTeamId
|
|
8713
|
+
};
|
|
8714
|
+
const connection = existing ? await updateConnectionWithSlackBotSuccessAudit(db, {
|
|
8715
|
+
...lifecycleAudit,
|
|
8716
|
+
connection: {
|
|
8717
|
+
workspaceId: input.state.workspaceId,
|
|
8718
|
+
connectionId: existing.id,
|
|
8719
|
+
visibleToSubjectId: input.state.subjectId,
|
|
8720
|
+
expectedVersion: existing.version,
|
|
8721
|
+
subjectId: null,
|
|
8722
|
+
providerDomain: "slack.com",
|
|
8723
|
+
kind: "app_install",
|
|
8724
|
+
status: "active",
|
|
8725
|
+
credentialEncrypted,
|
|
8726
|
+
grantedScopes: input.verified.grantedScopes,
|
|
8727
|
+
expiresAt: null,
|
|
8728
|
+
verifiedInstallAt,
|
|
8729
|
+
verifiedInstallVersion: existing.version + 1,
|
|
8730
|
+
metadata: input.verified.metadata,
|
|
8731
|
+
updatedBySubjectId: input.state.subjectId
|
|
8732
|
+
}
|
|
8733
|
+
}) : await createConnectionWithSlackBotSuccessAudit(db, {
|
|
8734
|
+
...lifecycleAudit,
|
|
8735
|
+
connection: {
|
|
8736
|
+
accountId: input.state.accountId,
|
|
8737
|
+
workspaceId: input.state.workspaceId,
|
|
8738
|
+
subjectId: null,
|
|
8739
|
+
providerDomain: "slack.com",
|
|
8740
|
+
kind: "app_install",
|
|
8741
|
+
credentialEncrypted,
|
|
8742
|
+
grantedScopes: input.verified.grantedScopes,
|
|
8743
|
+
expiresAt: null,
|
|
8744
|
+
verifiedInstallAt,
|
|
8745
|
+
verifiedInstallVersion: 1,
|
|
8746
|
+
metadata: input.verified.metadata,
|
|
8747
|
+
createdBySubjectId: input.state.subjectId
|
|
8748
|
+
}
|
|
8749
|
+
});
|
|
8750
|
+
if (!connection) {
|
|
8751
|
+
throw new SlackInstallCallbackError(
|
|
8752
|
+
409,
|
|
8753
|
+
"connection_conflict",
|
|
8754
|
+
"Slack bot connection changed during reinstall; start again",
|
|
8755
|
+
"principal_validation"
|
|
8756
|
+
);
|
|
8757
|
+
}
|
|
8758
|
+
return connection;
|
|
8759
|
+
}
|
|
8760
|
+
function requireOpenGeniSlackOAuthSettings(settings) {
|
|
8761
|
+
const clientId = settings.slackClientId?.trim();
|
|
8762
|
+
const clientSecret = settings.slackClientSecret?.trim();
|
|
8763
|
+
if (!clientId || !clientSecret) {
|
|
8764
|
+
throw new HTTPException9(503, {
|
|
8765
|
+
message: "OpenGeni Slack installation requires OPENGENI_SLACK_CLIENT_ID and OPENGENI_SLACK_CLIENT_SECRET"
|
|
8766
|
+
});
|
|
8767
|
+
}
|
|
8768
|
+
return { clientId, clientSecret };
|
|
8769
|
+
}
|
|
8770
|
+
function readOpenGeniSlackInstallState(rawState, settings) {
|
|
8771
|
+
if (!rawState) {
|
|
8772
|
+
throw new HTTPException9(400, { message: "missing Slack installation state" });
|
|
8773
|
+
}
|
|
8774
|
+
const payload = readSignedState3(rawState, requireIntegrationsStateSecret(settings));
|
|
8775
|
+
if (!payload) {
|
|
8776
|
+
throw new HTTPException9(400, { message: "invalid or expired Slack installation state" });
|
|
8777
|
+
}
|
|
8778
|
+
const requiredString2 = (value, label) => {
|
|
8779
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
8780
|
+
throw new HTTPException9(400, { message: `invalid Slack installation ${label}` });
|
|
8781
|
+
}
|
|
8782
|
+
return value;
|
|
8783
|
+
};
|
|
8784
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
8785
|
+
if (typeof payload.iat !== "number" || nowSeconds < payload.iat || nowSeconds - payload.iat > oauthStateTtlMs / 1e3) {
|
|
8786
|
+
throw new HTTPException9(400, { message: "invalid or expired Slack installation state" });
|
|
8787
|
+
}
|
|
8788
|
+
const accountId = requiredString2(payload.accountId, "account");
|
|
8789
|
+
const workspaceId = requiredString2(payload.workspaceId, "workspace");
|
|
8790
|
+
const subjectId = requiredString2(payload.subjectId, "subject");
|
|
8791
|
+
const returnPath = requiredString2(payload.returnPath, "return path");
|
|
8792
|
+
if (returnPath !== `/workspaces/${workspaceId}/capabilities`) {
|
|
8793
|
+
throw new HTTPException9(400, { message: "invalid Slack installation return path" });
|
|
8794
|
+
}
|
|
8795
|
+
const connectionId = typeof payload.connectionId === "string" ? payload.connectionId : void 0;
|
|
8796
|
+
const connectionVersion = typeof payload.connectionVersion === "number" && Number.isInteger(payload.connectionVersion) ? payload.connectionVersion : void 0;
|
|
8797
|
+
if (Boolean(connectionId) !== Boolean(connectionVersion)) {
|
|
8798
|
+
throw new HTTPException9(400, { message: "invalid Slack reinstall state" });
|
|
8799
|
+
}
|
|
8800
|
+
return {
|
|
8801
|
+
accountId,
|
|
8802
|
+
workspaceId,
|
|
8803
|
+
subjectId,
|
|
8804
|
+
returnPath,
|
|
8805
|
+
...connectionId ? { connectionId, connectionVersion } : {},
|
|
8806
|
+
nonce: requiredString2(payload.nonce, "nonce"),
|
|
8807
|
+
iat: typeof payload.iat === "number" ? payload.iat : (() => {
|
|
8808
|
+
throw new HTTPException9(400, { message: "invalid Slack installation timestamp" });
|
|
8809
|
+
})()
|
|
8810
|
+
};
|
|
8811
|
+
}
|
|
8812
|
+
function slackInstallReturnUrl(baseUrl, returnPath, status, detail) {
|
|
8813
|
+
const url = new URL(returnPath, `${baseUrl}/`);
|
|
8814
|
+
url.searchParams.set("slack", status);
|
|
8815
|
+
url.searchParams.set(status === "connected" ? "connectionId" : "reason", detail.slice(0, 128));
|
|
8816
|
+
return url.toString();
|
|
8817
|
+
}
|
|
8818
|
+
var SlackInstallCallbackError = class extends HTTPException9 {
|
|
8819
|
+
constructor(status, failureReason, message, failureStage) {
|
|
8820
|
+
super(status, { message });
|
|
8821
|
+
this.failureReason = failureReason;
|
|
8822
|
+
this.failureStage = failureStage;
|
|
8823
|
+
this.name = "SlackInstallCallbackError";
|
|
8824
|
+
}
|
|
8825
|
+
};
|
|
8826
|
+
function slackInstallCallbackFailure(stage, error) {
|
|
8827
|
+
if (error instanceof SlackInstallCallbackError) {
|
|
8828
|
+
return { stage: error.failureStage ?? stage, reason: error.failureReason };
|
|
8829
|
+
}
|
|
8830
|
+
if (error instanceof SlackBotCredentialVerificationError) {
|
|
8831
|
+
return { stage: "credential_verification", reason: error.failureReason };
|
|
8832
|
+
}
|
|
8833
|
+
if (error instanceof SlackBotLifecycleSuccessAuditError) {
|
|
8834
|
+
return { stage: "persistence", reason: "success_audit_failed" };
|
|
8835
|
+
}
|
|
8836
|
+
if (stage === "code_exchange") {
|
|
8837
|
+
return { stage, reason: "exchange_failed" };
|
|
8838
|
+
}
|
|
8839
|
+
if (stage === "credential_verification") {
|
|
8840
|
+
return { stage, reason: "credential_verification_failed" };
|
|
8841
|
+
}
|
|
8842
|
+
return { stage, reason: "persistence_failed" };
|
|
8843
|
+
}
|
|
8844
|
+
function slackInstallErrorReason(error) {
|
|
8845
|
+
if (error instanceof SlackInstallCallbackError && error.failureReason === "provider_denied") {
|
|
8846
|
+
return "provider_denied";
|
|
8847
|
+
}
|
|
8848
|
+
if (error instanceof HTTPException9) {
|
|
8849
|
+
return `http_${error.status}`;
|
|
8850
|
+
}
|
|
8851
|
+
return "installation_failed";
|
|
8852
|
+
}
|
|
8853
|
+
async function requireSlackInstallCallbackGrant(db, state) {
|
|
8854
|
+
const grant = await getWorkspaceGrant2(db, state.subjectId, state.workspaceId);
|
|
8855
|
+
if (!grant || grant.accountId !== state.accountId || !hasPermission6(grant.permissions, "connections:write")) {
|
|
8856
|
+
throw new SlackInstallCallbackError(
|
|
8857
|
+
403,
|
|
8858
|
+
"permission_lost",
|
|
8859
|
+
"Slack installation subject no longer has permission for this workspace"
|
|
8860
|
+
);
|
|
8861
|
+
}
|
|
8862
|
+
}
|
|
8863
|
+
function assertNotDirectPersonalSlackOAuth(providerDomain, kind) {
|
|
8864
|
+
if (providerDomain === "slack.com" && kind === "oauth2") {
|
|
8865
|
+
throw new HTTPException9(422, {
|
|
8866
|
+
message: "personal Slack credentials must use the hosted MCP OAuth flow"
|
|
8867
|
+
});
|
|
8868
|
+
}
|
|
8869
|
+
}
|
|
8413
8870
|
function assertNotReservedSlackBotMetadata(metadata) {
|
|
8414
8871
|
if (hasReservedOpenGeniSlackBotMetadata(metadata)) {
|
|
8415
8872
|
throw new HTTPException9(422, {
|
|
@@ -11195,7 +11652,7 @@ import {
|
|
|
11195
11652
|
authorizeGitHubInstallationBinding,
|
|
11196
11653
|
buildGitHubAppManifest,
|
|
11197
11654
|
convertGitHubAppManifest,
|
|
11198
|
-
createSignedState as
|
|
11655
|
+
createSignedState as createSignedState5,
|
|
11199
11656
|
envLinesFromGitHubManifestConversion,
|
|
11200
11657
|
GitHubAppApiError,
|
|
11201
11658
|
GitHubAppConfigurationError as GitHubAppConfigurationError2,
|
|
@@ -11204,13 +11661,13 @@ import {
|
|
|
11204
11661
|
githubOAuthAuthorizeUrl,
|
|
11205
11662
|
organizationAppManifestUrl,
|
|
11206
11663
|
personalAppManifestUrl,
|
|
11207
|
-
readSignedState as
|
|
11664
|
+
readSignedState as readSignedState4,
|
|
11208
11665
|
stateMaxAgeSeconds,
|
|
11209
11666
|
verifySignedState
|
|
11210
11667
|
} from "@opengeni/github";
|
|
11211
11668
|
import { deleteCookie, setCookie } from "hono/cookie";
|
|
11212
11669
|
import { HTTPException as HTTPException17 } from "hono/http-exception";
|
|
11213
|
-
import { hasPermission as
|
|
11670
|
+
import { hasPermission as hasPermission7, requireAccessGrant as requireAccessGrant10 } from "@opengeni/core";
|
|
11214
11671
|
var githubStateCookie = "opengeni_github_state";
|
|
11215
11672
|
var githubBindingStateMaxAgeSeconds = 10 * 60;
|
|
11216
11673
|
var legacyInstallationChooserDisabledMessage = "The legacy repository-admin GitHub installation chooser is disabled; use the GitHub owner-consent connect flow";
|
|
@@ -11223,8 +11680,8 @@ function registerGitHubRoutes(app, deps) {
|
|
|
11223
11680
|
const slug = settings.githubAppSlug?.trim() || null;
|
|
11224
11681
|
const installations = missing.length === 0 ? await listWorkspaceGitHubInstallationBindings(deps, grant.workspaceId) : [];
|
|
11225
11682
|
const status = githubBindingStatus(missing.length === 0, installations);
|
|
11226
|
-
const canManage =
|
|
11227
|
-
const connectState = missing.length === 0 && slug && canManage ?
|
|
11683
|
+
const canManage = hasPermission7(grant.permissions, "github:manage");
|
|
11684
|
+
const connectState = missing.length === 0 && slug && canManage ? createSignedState5(githubStateSecret, {
|
|
11228
11685
|
accountId: grant.accountId,
|
|
11229
11686
|
workspaceId: grant.workspaceId,
|
|
11230
11687
|
intent: "installation_authority",
|
|
@@ -11249,7 +11706,7 @@ function registerGitHubRoutes(app, deps) {
|
|
|
11249
11706
|
if (!state) {
|
|
11250
11707
|
throw new HTTPException17(400, { message: "missing GitHub installation state" });
|
|
11251
11708
|
}
|
|
11252
|
-
const statePayload =
|
|
11709
|
+
const statePayload = readSignedState4(state, githubStateSecret);
|
|
11253
11710
|
if (!statePayload || statePayload.intent !== "installation_authority" || statePayload.workspaceId !== workspaceId || typeof statePayload.accountId !== "string" || !isFreshGitHubBindingState(statePayload)) {
|
|
11254
11711
|
throw new HTTPException17(400, { message: "invalid or expired GitHub installation state" });
|
|
11255
11712
|
}
|
|
@@ -11324,7 +11781,7 @@ function registerGitHubRoutes(app, deps) {
|
|
|
11324
11781
|
/\/+$/,
|
|
11325
11782
|
""
|
|
11326
11783
|
);
|
|
11327
|
-
const state =
|
|
11784
|
+
const state = createSignedState5(githubStateSecret, {
|
|
11328
11785
|
accountId: grant.accountId,
|
|
11329
11786
|
workspaceId: grant.workspaceId
|
|
11330
11787
|
});
|
|
@@ -11368,7 +11825,7 @@ function registerGitHubRoutes(app, deps) {
|
|
|
11368
11825
|
if (!state) {
|
|
11369
11826
|
throw new HTTPException17(400, { message: "missing GitHub installation state" });
|
|
11370
11827
|
}
|
|
11371
|
-
const statePayload =
|
|
11828
|
+
const statePayload = readSignedState4(state, githubStateSecret);
|
|
11372
11829
|
if (!statePayload || statePayload.intent !== "installation_authority" || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string" || !isFreshGitHubBindingState(statePayload)) {
|
|
11373
11830
|
throw new HTTPException17(400, { message: "invalid or expired GitHub installation state" });
|
|
11374
11831
|
}
|
|
@@ -11399,7 +11856,7 @@ function registerGitHubRoutes(app, deps) {
|
|
|
11399
11856
|
})
|
|
11400
11857
|
});
|
|
11401
11858
|
}
|
|
11402
|
-
const oauthState =
|
|
11859
|
+
const oauthState = createSignedState5(githubStateSecret, {
|
|
11403
11860
|
accountId: grant.accountId,
|
|
11404
11861
|
workspaceId: grant.workspaceId,
|
|
11405
11862
|
installationId,
|
|
@@ -11426,7 +11883,7 @@ function registerGitHubRoutes(app, deps) {
|
|
|
11426
11883
|
if (!state) {
|
|
11427
11884
|
throw new HTTPException17(400, { message: "missing GitHub OAuth state" });
|
|
11428
11885
|
}
|
|
11429
|
-
const statePayload =
|
|
11886
|
+
const statePayload = readSignedState4(state, githubStateSecret);
|
|
11430
11887
|
if (!statePayload || statePayload.intent !== "installation_authority_oauth" || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string" || !isFreshGitHubBindingState(statePayload)) {
|
|
11431
11888
|
throw new HTTPException17(400, { message: "invalid or expired GitHub OAuth state" });
|
|
11432
11889
|
}
|
|
@@ -11505,7 +11962,7 @@ function registerGitHubRoutes(app, deps) {
|
|
|
11505
11962
|
if (!state) {
|
|
11506
11963
|
throw new HTTPException17(400, { message: "missing GitHub OAuth state" });
|
|
11507
11964
|
}
|
|
11508
|
-
const statePayload =
|
|
11965
|
+
const statePayload = readSignedState4(state, githubStateSecret);
|
|
11509
11966
|
if (!statePayload || typeof statePayload.accountId !== "string" || statePayload.accountId.length === 0 || statePayload.workspaceId !== workspaceId) {
|
|
11510
11967
|
throw new HTTPException17(400, { message: "invalid or expired GitHub OAuth state" });
|
|
11511
11968
|
}
|
|
@@ -13270,34 +13727,34 @@ function registerSessionRoutes(app, deps) {
|
|
|
13270
13727
|
message: "durable interactive terminals require a session-home provider lease"
|
|
13271
13728
|
});
|
|
13272
13729
|
}
|
|
13273
|
-
const
|
|
13730
|
+
const process2 = await getRetainedProcess(db, {
|
|
13274
13731
|
workspaceId: ctx.workspaceId,
|
|
13275
13732
|
sessionId: ctx.session.id,
|
|
13276
13733
|
processId: pty.retainedProcessId
|
|
13277
13734
|
});
|
|
13278
|
-
if (!
|
|
13735
|
+
if (!process2 || process2.state !== "active" || process2.ownerActorKind !== "direct" || process2.accountId !== ctx.accountId || process2.leaseId !== pty.leaseId || process2.sandboxGroupId !== pty.sandboxGroupId || process2.parentAdmissionId !== pty.openAdmissionId || process2.leaseEpoch !== pty.leaseEpoch || process2.providerBackend !== pty.providerBackend || process2.providerInstanceId !== pty.providerInstanceId || process2.routeKind !== pty.routeKind || process2.routeTargetId !== pty.routeTargetId || process2.routeEpoch !== pty.routeEpoch || process2.providerSessionId !== pty.execSessionId || // Only a persistable home backend can currently be reconstructed by an
|
|
13279
13736
|
// API request without consulting the mutable active pointer.
|
|
13280
|
-
|
|
13737
|
+
process2.routeTargetId !== null || handle.lease.id !== process2.leaseId || handle.lease.sandboxGroupId !== process2.sandboxGroupId || handle.lease.leaseEpoch !== process2.leaseEpoch || handle.lease.backend !== process2.providerBackend || handle.lease.instanceId !== process2.providerInstanceId) {
|
|
13281
13738
|
throw new HTTPException22(409, {
|
|
13282
13739
|
message: "pty retained-process identity is stale; reopen the terminal"
|
|
13283
13740
|
});
|
|
13284
13741
|
}
|
|
13285
13742
|
handle.routingSession.adoptRetainedProcess({
|
|
13286
|
-
process: { id:
|
|
13743
|
+
process: { id: process2.id, providerSessionId: process2.providerSessionId },
|
|
13287
13744
|
backend: {
|
|
13288
13745
|
sandboxId: null,
|
|
13289
|
-
leaseEpoch:
|
|
13290
|
-
providerInstanceId:
|
|
13291
|
-
activeEpoch:
|
|
13746
|
+
leaseEpoch: process2.leaseEpoch,
|
|
13747
|
+
providerInstanceId: process2.providerInstanceId,
|
|
13748
|
+
activeEpoch: process2.routeEpoch
|
|
13292
13749
|
}
|
|
13293
13750
|
});
|
|
13294
|
-
return
|
|
13751
|
+
return process2;
|
|
13295
13752
|
};
|
|
13296
|
-
const emitPtyExited = async (ctx, ptyId,
|
|
13753
|
+
const emitPtyExited = async (ctx, ptyId, process2) => {
|
|
13297
13754
|
const exited = {
|
|
13298
13755
|
ptyId,
|
|
13299
|
-
exitCode:
|
|
13300
|
-
reason:
|
|
13756
|
+
exitCode: process2.exitCode,
|
|
13757
|
+
reason: process2.state === "exited" ? "exit" : "lost"
|
|
13301
13758
|
};
|
|
13302
13759
|
await appendAndPublishEvents5(db, bus, ctx.workspaceId, ctx.session.id, [
|
|
13303
13760
|
{ type: "terminal.pty.exited", payload: exited }
|
|
@@ -14919,12 +15376,12 @@ function registerSessionRoutes(app, deps) {
|
|
|
14919
15376
|
const opened = await service.ptyOpen(req, ptyId);
|
|
14920
15377
|
const execSessionId = opened.execSessionId;
|
|
14921
15378
|
const retained = execSessionId === null ? null : handle.routingSession.retainedProcessIdentity(execSessionId);
|
|
14922
|
-
const
|
|
15379
|
+
const process2 = retained ? await getRetainedProcess(db, {
|
|
14923
15380
|
workspaceId: ctx.workspaceId,
|
|
14924
15381
|
sessionId: ctx.session.id,
|
|
14925
15382
|
processId: retained.id
|
|
14926
15383
|
}) : null;
|
|
14927
|
-
if (execSessionId === null || !retained || !
|
|
15384
|
+
if (execSessionId === null || !retained || !process2 || process2.state !== "active" || process2.ownerActorKind !== "direct" || process2.providerSessionId !== execSessionId || process2.routeTargetId !== null || process2.leaseId !== handle.lease.id || process2.sandboxGroupId !== handle.lease.sandboxGroupId || process2.leaseEpoch !== handle.lease.leaseEpoch || process2.providerBackend !== handle.lease.backend || process2.providerInstanceId !== handle.lease.instanceId) {
|
|
14928
15385
|
if (execSessionId !== null && handle.routingSession.hasRetainedProcess(execSessionId)) {
|
|
14929
15386
|
await drainOpenedPty(handle, execSessionId);
|
|
14930
15387
|
}
|
|
@@ -14933,17 +15390,17 @@ function registerSessionRoutes(app, deps) {
|
|
|
14933
15390
|
});
|
|
14934
15391
|
}
|
|
14935
15392
|
const identity = {
|
|
14936
|
-
leaseId:
|
|
14937
|
-
sandboxGroupId:
|
|
14938
|
-
retainedProcessId:
|
|
14939
|
-
openAdmissionId:
|
|
14940
|
-
execSessionId:
|
|
14941
|
-
leaseEpoch:
|
|
14942
|
-
providerBackend:
|
|
14943
|
-
providerInstanceId:
|
|
14944
|
-
routeKind:
|
|
14945
|
-
routeTargetId:
|
|
14946
|
-
routeEpoch:
|
|
15393
|
+
leaseId: process2.leaseId,
|
|
15394
|
+
sandboxGroupId: process2.sandboxGroupId,
|
|
15395
|
+
retainedProcessId: process2.id,
|
|
15396
|
+
openAdmissionId: process2.parentAdmissionId,
|
|
15397
|
+
execSessionId: process2.providerSessionId,
|
|
15398
|
+
leaseEpoch: process2.leaseEpoch,
|
|
15399
|
+
providerBackend: process2.providerBackend,
|
|
15400
|
+
providerInstanceId: process2.providerInstanceId,
|
|
15401
|
+
routeKind: process2.routeKind,
|
|
15402
|
+
routeTargetId: process2.routeTargetId,
|
|
15403
|
+
routeEpoch: process2.routeEpoch
|
|
14947
15404
|
};
|
|
14948
15405
|
try {
|
|
14949
15406
|
await insertPtySession(db, {
|
|
@@ -15531,7 +15988,7 @@ import {
|
|
|
15531
15988
|
} from "@opengeni/db";
|
|
15532
15989
|
import { boundWorkspaceControlHttpPage } from "@opengeni/events";
|
|
15533
15990
|
import { HTTPException as HTTPException24 } from "hono/http-exception";
|
|
15534
|
-
import { hasPermission as
|
|
15991
|
+
import { hasPermission as hasPermission8, requireAccessContext as requireAccessContext2, requireAccessGrant as requireAccessGrant16 } from "@opengeni/core";
|
|
15535
15992
|
import { requireLimit as requireLimit7 } from "@opengeni/core";
|
|
15536
15993
|
import {
|
|
15537
15994
|
assertWorkspaceDeletable,
|
|
@@ -15778,7 +16235,7 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
15778
16235
|
const context = await requireAccessContext2(c, deps);
|
|
15779
16236
|
const readableWorkspaceIds = [
|
|
15780
16237
|
...new Set(
|
|
15781
|
-
context.workspaceGrants.filter((grant) =>
|
|
16238
|
+
context.workspaceGrants.filter((grant) => hasPermission8(grant.permissions, "workspace:read")).map((grant) => grant.workspaceId)
|
|
15782
16239
|
)
|
|
15783
16240
|
];
|
|
15784
16241
|
if (readableWorkspaceIds.length > 0) {
|
|
@@ -16489,18 +16946,19 @@ function createApp(deps) {
|
|
|
16489
16946
|
);
|
|
16490
16947
|
app.get("/v1/config/client", (c) => {
|
|
16491
16948
|
c.header("cache-control", "no-store");
|
|
16949
|
+
const catalogSettings = deps.settings.codexSubscriptionEnabled ? withCodexCatalogProvider2(deps.settings) : deps.settings;
|
|
16492
16950
|
return c.json(
|
|
16493
16951
|
ClientConfig.parse({
|
|
16494
16952
|
deploymentRevision: deps.settings.deploymentRevision,
|
|
16495
16953
|
apiContractRevision: OPENGENI_API_CONTRACT_REVISION,
|
|
16496
16954
|
...deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {},
|
|
16497
|
-
defaultModel: canonicalizeConfiguredModelId2(
|
|
16498
|
-
allowedModels: configuredAllowedModels(
|
|
16955
|
+
defaultModel: canonicalizeConfiguredModelId2(catalogSettings, catalogSettings.openaiModel),
|
|
16956
|
+
allowedModels: configuredAllowedModels(catalogSettings),
|
|
16499
16957
|
// Provider-grouped model list for the picker. configuredModels() carries the
|
|
16500
16958
|
// union of the built-in allow-list and every registry provider's models, in
|
|
16501
16959
|
// selection order (default model first); project each to the client-safe
|
|
16502
16960
|
// ClientModel shape (ConfiguredModel.providerId → ClientModel.provider).
|
|
16503
|
-
models: configuredModels2(
|
|
16961
|
+
models: configuredModels2(catalogSettings).map(projectClientModel),
|
|
16504
16962
|
defaultReasoningEffort: deps.settings.openaiReasoningEffort,
|
|
16505
16963
|
allowedReasoningEfforts: configuredAllowedReasoningEfforts(deps.settings),
|
|
16506
16964
|
mcpServers: deps.settings.mcpServers.map((server) => ({
|
|
@@ -16641,7 +17099,7 @@ function createApp(deps) {
|
|
|
16641
17099
|
}
|
|
16642
17100
|
async function requireMcpAccessGrant(c, deps, workspaceId) {
|
|
16643
17101
|
const grant = await requireAccessGrant18(c, deps, workspaceId);
|
|
16644
|
-
if (
|
|
17102
|
+
if (hasPermission9(grant.permissions, "workspace:read")) {
|
|
16645
17103
|
return grant;
|
|
16646
17104
|
}
|
|
16647
17105
|
if (isToolspaceGrant(deps.settings, grant)) {
|
|
@@ -17079,8 +17537,8 @@ var routeLabelPatterns = [
|
|
|
17079
17537
|
label: "/v1/workspaces/:workspaceId/connections/oauth/start"
|
|
17080
17538
|
},
|
|
17081
17539
|
{
|
|
17082
|
-
pattern: /^\/v1\/workspaces\/[^/]+\/connections\/slack-bot$/,
|
|
17083
|
-
label: "/v1/workspaces/:workspaceId/connections/slack-bot"
|
|
17540
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/connections\/slack-bot\/install$/,
|
|
17541
|
+
label: "/v1/workspaces/:workspaceId/connections/slack-bot/install"
|
|
17084
17542
|
},
|
|
17085
17543
|
{
|
|
17086
17544
|
pattern: /^\/v1\/workspaces\/[^/]+\/connections\/[^/]+$/,
|
|
@@ -17095,6 +17553,10 @@ var routeLabelPatterns = [
|
|
|
17095
17553
|
pattern: /^\/v1\/integrations\/oauth\/client-metadata\.json$/,
|
|
17096
17554
|
label: "/v1/integrations/oauth/client-metadata.json"
|
|
17097
17555
|
},
|
|
17556
|
+
{
|
|
17557
|
+
pattern: /^\/v1\/integrations\/slack\/callback$/,
|
|
17558
|
+
label: "/v1/integrations/slack/callback"
|
|
17559
|
+
},
|
|
17098
17560
|
{
|
|
17099
17561
|
pattern: /^\/v1\/enrollments\/device\/start$/,
|
|
17100
17562
|
label: "/v1/enrollments/device/start"
|
|
@@ -17179,4 +17641,4 @@ export {
|
|
|
17179
17641
|
withDefaultEnabledCapabilityMcpTools,
|
|
17180
17642
|
workflowIdForSession2 as workflowIdForSession
|
|
17181
17643
|
};
|
|
17182
|
-
//# sourceMappingURL=chunk-
|
|
17644
|
+
//# sourceMappingURL=chunk-3BHOMOSD.js.map
|