@opengeni/api-router 0.11.8 → 0.12.1
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-DQWFAIPE.js → chunk-S2N4252E.js} +1526 -453
- package/dist/chunk-S2N4252E.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +10 -10
- package/src/app.ts +34 -24
- package/src/http/auth.ts +4 -1
- package/src/integrations/slack-bot.ts +694 -0
- package/src/mcp/server.ts +120 -0
- package/src/routes/connections.ts +155 -1
- package/src/routes/sessions.ts +10 -0
- package/src/routes/workspace-instruction-policies.ts +243 -0
- package/src/sandbox/channel-a.ts +131 -70
- package/dist/chunk-DQWFAIPE.js.map +0 -1
|
@@ -24,10 +24,10 @@ import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPSe
|
|
|
24
24
|
import { Hono } from "hono";
|
|
25
25
|
import { bodyLimit } from "hono/body-limit";
|
|
26
26
|
import { cors } from "hono/cors";
|
|
27
|
-
import { HTTPException as
|
|
27
|
+
import { HTTPException as HTTPException26 } from "hono/http-exception";
|
|
28
28
|
import {
|
|
29
29
|
hasPermission as hasPermission7,
|
|
30
|
-
requireAccessGrant as
|
|
30
|
+
requireAccessGrant as requireAccessGrant18,
|
|
31
31
|
requirePermission,
|
|
32
32
|
requireSessionAuthorization as requireSessionAuthorization3,
|
|
33
33
|
SessionAuthorizationDeniedError as SessionAuthorizationDeniedError2,
|
|
@@ -312,7 +312,7 @@ import {
|
|
|
312
312
|
createVariableSet,
|
|
313
313
|
deleteScheduledTask,
|
|
314
314
|
encryptVariableSetValue,
|
|
315
|
-
getSession,
|
|
315
|
+
getSession as getSession2,
|
|
316
316
|
getSessionGoal,
|
|
317
317
|
getSessionQueueSnapshot,
|
|
318
318
|
getSessionTurn,
|
|
@@ -1997,6 +1997,537 @@ function viewerIdAsUuid(rawViewerId) {
|
|
|
1997
1997
|
return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
|
|
1998
1998
|
}
|
|
1999
1999
|
|
|
2000
|
+
// src/integrations/slack-bot.ts
|
|
2001
|
+
import { createHmac } from "crypto";
|
|
2002
|
+
import { environmentsEncryptionKeyBytes } from "@opengeni/config";
|
|
2003
|
+
import {
|
|
2004
|
+
OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
|
|
2005
|
+
OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
2006
|
+
OPENGENI_SLACK_BOT_FORBIDDEN_SCOPES,
|
|
2007
|
+
OPENGENI_SLACK_BOT_REQUIRED_SCOPES
|
|
2008
|
+
} from "@opengeni/contracts";
|
|
2009
|
+
import {
|
|
2010
|
+
isTrustedScheduledSlackBotSession,
|
|
2011
|
+
openGeniSlackBotMetadata,
|
|
2012
|
+
requireOpenGeniSlackBotConnection,
|
|
2013
|
+
scheduledSlackBotConnectionId
|
|
2014
|
+
} from "@opengeni/core";
|
|
2015
|
+
import {
|
|
2016
|
+
buildConnectionTokenResolver,
|
|
2017
|
+
claimSlackBotPostOperation,
|
|
2018
|
+
completeSlackBotPostOperation,
|
|
2019
|
+
getSession,
|
|
2020
|
+
recordAuditEvent,
|
|
2021
|
+
releaseSlackBotPostOperationClaim
|
|
2022
|
+
} from "@opengeni/db";
|
|
2023
|
+
import { readResponseJsonBounded } from "@opengeni/network";
|
|
2024
|
+
import { HTTPException as HTTPException2 } from "hono/http-exception";
|
|
2025
|
+
var SLACK_API_BASE = "https://slack.com/api/";
|
|
2026
|
+
var SLACK_RESPONSE_MAX_BYTES = 2 * 1024 * 1024;
|
|
2027
|
+
var SLACK_TIMEOUT_MS = 1e4;
|
|
2028
|
+
var MAX_CHANNEL_PAGE = 200;
|
|
2029
|
+
var MAX_HISTORY_PAGE = 100;
|
|
2030
|
+
var MAX_USER_PAGE = 200;
|
|
2031
|
+
var MAX_PROJECTED_TEXT = 4e3;
|
|
2032
|
+
var SLACK_POST_CLAIM_LEASE_MS = 3e4;
|
|
2033
|
+
var SlackBotProviderError = class extends Error {
|
|
2034
|
+
constructor(code) {
|
|
2035
|
+
super(`Slack bot request failed: ${safeSlackCode(code)}`);
|
|
2036
|
+
this.code = code;
|
|
2037
|
+
this.name = "SlackBotProviderError";
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
async function verifyOpenGeniSlackBotCredential(token, fetchImpl = fetch, now = /* @__PURE__ */ new Date()) {
|
|
2041
|
+
const authResponse = await slackApiFetch(fetchImpl, "auth.test", token, {});
|
|
2042
|
+
const grantedScopes2 = parseGrantedScopes(authResponse.response.headers.get("x-oauth-scopes"));
|
|
2043
|
+
assertExactOpenGeniSlackBotScopes(grantedScopes2);
|
|
2044
|
+
const auth = authResponse.payload;
|
|
2045
|
+
const slackTeamId = requiredSlackString(auth.team_id, "team_id");
|
|
2046
|
+
const slackTeamName = requiredSlackString(auth.team, "team");
|
|
2047
|
+
const botUserId = requiredSlackString(auth.user_id, "user_id");
|
|
2048
|
+
const botId = requiredSlackString(auth.bot_id, "bot_id");
|
|
2049
|
+
const userResponse = await slackApiFetch(fetchImpl, "users.info", token, {
|
|
2050
|
+
user: botUserId
|
|
2051
|
+
});
|
|
2052
|
+
const user = slackRecord(userResponse.payload.user);
|
|
2053
|
+
if (!user || user.is_bot !== true || user.deleted === true) {
|
|
2054
|
+
throw new HTTPException2(422, { message: "Slack credential must identify an active bot user" });
|
|
2055
|
+
}
|
|
2056
|
+
const profile = slackRecord(user.profile);
|
|
2057
|
+
const displayName = slackString(profile?.display_name) || slackString(profile?.real_name);
|
|
2058
|
+
if (displayName !== "OpenGeni") {
|
|
2059
|
+
throw new HTTPException2(422, {
|
|
2060
|
+
message: 'Slack bot display name must be exactly "OpenGeni"'
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
return {
|
|
2064
|
+
grantedScopes: grantedScopes2,
|
|
2065
|
+
metadata: {
|
|
2066
|
+
credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
2067
|
+
credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
|
|
2068
|
+
slackTeamId,
|
|
2069
|
+
slackTeamName,
|
|
2070
|
+
botUserId,
|
|
2071
|
+
botId,
|
|
2072
|
+
botDisplayName: "OpenGeni",
|
|
2073
|
+
verifiedAt: now.toISOString()
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
async function resolveSlackBotConnectionForTool(input) {
|
|
2078
|
+
const session = input.sessionId ? await getSession(input.db, input.grant.workspaceId, input.sessionId) : null;
|
|
2079
|
+
if (input.sessionId && !session) {
|
|
2080
|
+
throw new Error("signed Slack bot session was not found");
|
|
2081
|
+
}
|
|
2082
|
+
const boundConnectionId = scheduledSlackBotConnectionId(session?.metadata);
|
|
2083
|
+
if (boundConnectionId && (!session || !isTrustedScheduledSlackBotSession(session))) {
|
|
2084
|
+
throw new Error("OpenGeni Slack bot routing metadata is not scheduler-authorized");
|
|
2085
|
+
}
|
|
2086
|
+
if (boundConnectionId && input.requestedConnectionId && input.requestedConnectionId !== boundConnectionId) {
|
|
2087
|
+
throw new Error("this scheduled session is bound to a different OpenGeni Slack bot connection");
|
|
2088
|
+
}
|
|
2089
|
+
const connectionId = boundConnectionId ?? input.requestedConnectionId;
|
|
2090
|
+
if (!connectionId) {
|
|
2091
|
+
throw new Error("connectionId is required outside a Slack-bot-bound scheduled session");
|
|
2092
|
+
}
|
|
2093
|
+
if (!boundConnectionId && !input.grant.permissions.includes("connections:read")) {
|
|
2094
|
+
throw new Error("connections:read is required to select an OpenGeni Slack bot connection");
|
|
2095
|
+
}
|
|
2096
|
+
const connection = await requireOpenGeniSlackBotConnection(
|
|
2097
|
+
input.db,
|
|
2098
|
+
input.grant.workspaceId,
|
|
2099
|
+
connectionId
|
|
2100
|
+
);
|
|
2101
|
+
const metadata = openGeniSlackBotMetadata(connection.metadata);
|
|
2102
|
+
if (!metadata) {
|
|
2103
|
+
throw new Error("OpenGeni Slack bot connection metadata is invalid");
|
|
2104
|
+
}
|
|
2105
|
+
return {
|
|
2106
|
+
connection,
|
|
2107
|
+
metadata,
|
|
2108
|
+
context: {
|
|
2109
|
+
accountId: input.grant.accountId,
|
|
2110
|
+
workspaceId: input.grant.workspaceId,
|
|
2111
|
+
subjectId: input.grant.subjectId,
|
|
2112
|
+
sessionId: input.sessionId,
|
|
2113
|
+
scheduledTaskId: typeof session?.metadata.scheduledTaskId === "string" ? session.metadata.scheduledTaskId : null
|
|
2114
|
+
}
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
var OpenGeniSlackBotClient = class {
|
|
2118
|
+
constructor(db, settings, connection, metadata, context, fetchImpl = fetch) {
|
|
2119
|
+
this.db = db;
|
|
2120
|
+
this.settings = settings;
|
|
2121
|
+
this.connection = connection;
|
|
2122
|
+
this.metadata = metadata;
|
|
2123
|
+
this.context = context;
|
|
2124
|
+
this.fetchImpl = fetchImpl;
|
|
2125
|
+
this.resolveCredential = buildConnectionTokenResolver(db, settings);
|
|
2126
|
+
}
|
|
2127
|
+
resolveCredential;
|
|
2128
|
+
async listChannels(input = {}) {
|
|
2129
|
+
return await this.withAudit("channels.list", async (headers) => {
|
|
2130
|
+
const payload = await this.call(headers, "conversations.list", {
|
|
2131
|
+
types: "public_channel,private_channel",
|
|
2132
|
+
exclude_archived: "true",
|
|
2133
|
+
limit: String(boundedInt(input.limit, MAX_CHANNEL_PAGE, 100)),
|
|
2134
|
+
...input.cursor ? { cursor: input.cursor } : {}
|
|
2135
|
+
});
|
|
2136
|
+
return {
|
|
2137
|
+
channels: slackArray(payload.channels).map(projectChannel).filter((channel) => channel !== null),
|
|
2138
|
+
nextCursor: responseCursor(payload)
|
|
2139
|
+
};
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
async channelHistory(input) {
|
|
2143
|
+
return await this.withAudit("channel_history.read", async (headers) => {
|
|
2144
|
+
const info = await this.requireMemberChannel(headers, input.channelId);
|
|
2145
|
+
const payload = await this.call(headers, "conversations.history", {
|
|
2146
|
+
channel: input.channelId,
|
|
2147
|
+
limit: String(boundedInt(input.limit, MAX_HISTORY_PAGE, 50)),
|
|
2148
|
+
...input.cursor ? { cursor: input.cursor } : {}
|
|
2149
|
+
});
|
|
2150
|
+
return {
|
|
2151
|
+
channel: info,
|
|
2152
|
+
messages: slackArray(payload.messages).map(projectMessage),
|
|
2153
|
+
nextCursor: responseCursor(payload)
|
|
2154
|
+
};
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
async listUsers(input = {}) {
|
|
2158
|
+
return await this.withAudit("users.list", async (headers) => {
|
|
2159
|
+
const payload = await this.call(headers, "users.list", {
|
|
2160
|
+
limit: String(boundedInt(input.limit, MAX_USER_PAGE, 100)),
|
|
2161
|
+
...input.cursor ? { cursor: input.cursor } : {}
|
|
2162
|
+
});
|
|
2163
|
+
return {
|
|
2164
|
+
users: slackArray(payload.members).map(projectUser).filter((user) => user !== null),
|
|
2165
|
+
nextCursor: responseCursor(payload)
|
|
2166
|
+
};
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
async postMessage(input) {
|
|
2170
|
+
const operation = "message.post";
|
|
2171
|
+
const claimHolderId = crypto.randomUUID();
|
|
2172
|
+
let claimAcquired = false;
|
|
2173
|
+
let providerCallStarted = false;
|
|
2174
|
+
try {
|
|
2175
|
+
const headers = await this.headersFor(operation);
|
|
2176
|
+
let channelId = input.channelId;
|
|
2177
|
+
if (input.userId) {
|
|
2178
|
+
const opened = await this.call(headers, "conversations.open", { users: input.userId });
|
|
2179
|
+
channelId = requiredSlackString(slackRecord(opened.channel)?.id, "channel.id");
|
|
2180
|
+
} else if (channelId) {
|
|
2181
|
+
await this.requireMemberChannel(headers, channelId);
|
|
2182
|
+
}
|
|
2183
|
+
if (!channelId) {
|
|
2184
|
+
throw new Error("exactly one of channelId or userId is required");
|
|
2185
|
+
}
|
|
2186
|
+
const targetKind = input.userId ? "user" : "channel";
|
|
2187
|
+
const targetId = input.userId ?? input.channelId;
|
|
2188
|
+
const requestDigest = this.postRequestDigest({
|
|
2189
|
+
operationId: input.operationId,
|
|
2190
|
+
targetKind,
|
|
2191
|
+
targetId,
|
|
2192
|
+
text: input.text
|
|
2193
|
+
});
|
|
2194
|
+
const claim = await claimSlackBotPostOperation(this.db, {
|
|
2195
|
+
accountId: this.context.accountId,
|
|
2196
|
+
workspaceId: this.context.workspaceId,
|
|
2197
|
+
connectionId: this.connection.id,
|
|
2198
|
+
operationId: input.operationId,
|
|
2199
|
+
targetKind,
|
|
2200
|
+
targetId,
|
|
2201
|
+
requestDigest,
|
|
2202
|
+
claimHolderId,
|
|
2203
|
+
claimLeaseMs: SLACK_POST_CLAIM_LEASE_MS
|
|
2204
|
+
});
|
|
2205
|
+
if (claim.kind === "connection_not_found") {
|
|
2206
|
+
throw new Error("OpenGeni Slack bot connection no longer exists");
|
|
2207
|
+
}
|
|
2208
|
+
if (claim.kind === "conflict") {
|
|
2209
|
+
throw new Error("operationId is already bound to a different Slack post request");
|
|
2210
|
+
}
|
|
2211
|
+
if (claim.kind === "in_progress") {
|
|
2212
|
+
throw new Error("Slack post operation is already in progress; retry the same operationId");
|
|
2213
|
+
}
|
|
2214
|
+
if (claim.kind === "completed") {
|
|
2215
|
+
return this.completedPostResult(claim.operation, input.operationId);
|
|
2216
|
+
}
|
|
2217
|
+
claimAcquired = true;
|
|
2218
|
+
providerCallStarted = true;
|
|
2219
|
+
const posted = await this.call(headers, "chat.postMessage", {
|
|
2220
|
+
channel: channelId,
|
|
2221
|
+
text: input.text,
|
|
2222
|
+
client_msg_id: input.operationId
|
|
2223
|
+
});
|
|
2224
|
+
const slackChannelId = requiredSlackString(posted.channel, "channel");
|
|
2225
|
+
const slackMessageTimestamp = requiredSlackString(posted.ts, "ts");
|
|
2226
|
+
const completed = await completeSlackBotPostOperation(this.db, {
|
|
2227
|
+
accountId: this.context.accountId,
|
|
2228
|
+
workspaceId: this.context.workspaceId,
|
|
2229
|
+
connectionId: this.connection.id,
|
|
2230
|
+
operationId: input.operationId,
|
|
2231
|
+
claimHolderId,
|
|
2232
|
+
slackChannelId,
|
|
2233
|
+
slackMessageTimestamp,
|
|
2234
|
+
subjectId: this.context.subjectId,
|
|
2235
|
+
auditMetadata: this.auditMetadata(operation, "succeeded", void 0, input.operationId)
|
|
2236
|
+
});
|
|
2237
|
+
if (completed.kind !== "completed") {
|
|
2238
|
+
throw new Error("Slack post completion lost its durable operation claim");
|
|
2239
|
+
}
|
|
2240
|
+
claimAcquired = false;
|
|
2241
|
+
return this.completedPostResult(completed.operation, input.operationId);
|
|
2242
|
+
} catch (error) {
|
|
2243
|
+
const failureCode = safeFailureCode(error);
|
|
2244
|
+
if (claimAcquired) {
|
|
2245
|
+
await releaseSlackBotPostOperationClaim(this.db, {
|
|
2246
|
+
accountId: this.context.accountId,
|
|
2247
|
+
workspaceId: this.context.workspaceId,
|
|
2248
|
+
connectionId: this.connection.id,
|
|
2249
|
+
operationId: input.operationId,
|
|
2250
|
+
claimHolderId,
|
|
2251
|
+
failureCode
|
|
2252
|
+
}).catch(() => void 0);
|
|
2253
|
+
}
|
|
2254
|
+
await this.recordAudit(
|
|
2255
|
+
operation,
|
|
2256
|
+
providerCallStarted && slackPostOutcomeMayBeAmbiguous(error) ? "ambiguous" : "failed",
|
|
2257
|
+
failureCode,
|
|
2258
|
+
input.operationId
|
|
2259
|
+
);
|
|
2260
|
+
throw error;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
async requireMemberChannel(headers, channelId) {
|
|
2264
|
+
const payload = await this.call(headers, "conversations.info", { channel: channelId });
|
|
2265
|
+
const projected = projectChannel(payload.channel);
|
|
2266
|
+
if (!projected || projected.isMember !== true) {
|
|
2267
|
+
throw new SlackBotProviderError("not_in_channel");
|
|
2268
|
+
}
|
|
2269
|
+
return projected;
|
|
2270
|
+
}
|
|
2271
|
+
async call(headers, method, params) {
|
|
2272
|
+
return (await slackApiFetchWithHeaders(this.fetchImpl, method, headers, params)).payload;
|
|
2273
|
+
}
|
|
2274
|
+
async withAudit(operation, run) {
|
|
2275
|
+
try {
|
|
2276
|
+
const headers = await this.headersFor(operation);
|
|
2277
|
+
const result = await run(headers);
|
|
2278
|
+
await this.recordAudit(operation, "succeeded");
|
|
2279
|
+
return { ...result, receipt: this.receipt(operation) };
|
|
2280
|
+
} catch (error) {
|
|
2281
|
+
await this.recordAudit(operation, "failed", safeFailureCode(error));
|
|
2282
|
+
throw error;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
async headersFor(operation) {
|
|
2286
|
+
const result = await this.resolveCredential({
|
|
2287
|
+
workspaceId: this.context.workspaceId,
|
|
2288
|
+
serverId: "opengeni-slack-bot",
|
|
2289
|
+
toolName: `slack_bot_${operation.replaceAll(".", "_")}`,
|
|
2290
|
+
connectionRef: {
|
|
2291
|
+
connectionId: this.connection.id,
|
|
2292
|
+
providerDomain: "slack.com",
|
|
2293
|
+
kind: "app_install",
|
|
2294
|
+
scopes: [...OPENGENI_SLACK_BOT_REQUIRED_SCOPES],
|
|
2295
|
+
subjectScope: "workspace"
|
|
2296
|
+
},
|
|
2297
|
+
destinationUrl: `${SLACK_API_BASE}${slackMethodForOperation(operation)}`
|
|
2298
|
+
});
|
|
2299
|
+
if (result.status !== "ok" || result.connectionId !== this.connection.id) {
|
|
2300
|
+
throw new Error("OpenGeni Slack bot connection needs to be reinstalled");
|
|
2301
|
+
}
|
|
2302
|
+
return result.headers;
|
|
2303
|
+
}
|
|
2304
|
+
receipt(operation, operationId) {
|
|
2305
|
+
return {
|
|
2306
|
+
credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
2307
|
+
credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
|
|
2308
|
+
connectionId: this.connection.id,
|
|
2309
|
+
slackTeamId: this.metadata.slackTeamId,
|
|
2310
|
+
operation,
|
|
2311
|
+
...operationId ? { operationId, clientMessageId: operationId } : {}
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
completedPostResult(operation, operationId) {
|
|
2315
|
+
if (!operation.slackChannelId || !operation.slackMessageTimestamp) {
|
|
2316
|
+
throw new Error("completed Slack post operation is missing its provider result");
|
|
2317
|
+
}
|
|
2318
|
+
return {
|
|
2319
|
+
channelId: operation.slackChannelId,
|
|
2320
|
+
timestamp: operation.slackMessageTimestamp,
|
|
2321
|
+
receipt: this.receipt("message.post", operationId)
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
postRequestDigest(input) {
|
|
2325
|
+
const key = environmentsEncryptionKeyBytes(this.settings);
|
|
2326
|
+
if (!key) throw new Error("connection encryption is not configured");
|
|
2327
|
+
return createHmac("sha256", key).update(
|
|
2328
|
+
JSON.stringify({
|
|
2329
|
+
operationId: input.operationId,
|
|
2330
|
+
connectionId: this.connection.id,
|
|
2331
|
+
targetKind: input.targetKind,
|
|
2332
|
+
targetId: input.targetId,
|
|
2333
|
+
text: input.text
|
|
2334
|
+
})
|
|
2335
|
+
).digest("hex");
|
|
2336
|
+
}
|
|
2337
|
+
async recordAudit(operation, outcome, failureCode, operationId) {
|
|
2338
|
+
await recordAuditEvent(this.db, {
|
|
2339
|
+
accountId: this.context.accountId,
|
|
2340
|
+
workspaceId: this.context.workspaceId,
|
|
2341
|
+
subjectId: this.context.subjectId,
|
|
2342
|
+
action: `slack_bot.${operation}`,
|
|
2343
|
+
targetType: "connection",
|
|
2344
|
+
targetId: this.connection.id,
|
|
2345
|
+
metadata: this.auditMetadata(operation, outcome, failureCode, operationId)
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
auditMetadata(operation, outcome, failureCode, operationId) {
|
|
2349
|
+
return {
|
|
2350
|
+
...this.receipt(operation, operationId),
|
|
2351
|
+
outcome,
|
|
2352
|
+
...failureCode ? { failureCode } : {},
|
|
2353
|
+
...this.context.sessionId ? { sessionId: this.context.sessionId } : {},
|
|
2354
|
+
...this.context.scheduledTaskId ? { scheduledTaskId: this.context.scheduledTaskId } : {}
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
};
|
|
2358
|
+
function createOpenGeniSlackBotClient(deps, resolved) {
|
|
2359
|
+
return new OpenGeniSlackBotClient(
|
|
2360
|
+
deps.db,
|
|
2361
|
+
deps.settings,
|
|
2362
|
+
resolved.connection,
|
|
2363
|
+
resolved.metadata,
|
|
2364
|
+
resolved.context,
|
|
2365
|
+
deps.slackFetch
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2368
|
+
async function slackApiFetch(fetchImpl, method, token, params) {
|
|
2369
|
+
return await slackApiFetchWithHeaders(
|
|
2370
|
+
fetchImpl,
|
|
2371
|
+
method,
|
|
2372
|
+
{ authorization: `Bearer ${token}` },
|
|
2373
|
+
params
|
|
2374
|
+
);
|
|
2375
|
+
}
|
|
2376
|
+
async function slackApiFetchWithHeaders(fetchImpl, method, credentialHeaders, params) {
|
|
2377
|
+
if (!/^[a-z]+\.[a-z]+$/i.test(method)) {
|
|
2378
|
+
throw new Error("invalid Slack API method");
|
|
2379
|
+
}
|
|
2380
|
+
const url = new URL(method, SLACK_API_BASE);
|
|
2381
|
+
const body = new URLSearchParams(params);
|
|
2382
|
+
let response;
|
|
2383
|
+
try {
|
|
2384
|
+
response = await fetchImpl(url, {
|
|
2385
|
+
method: "POST",
|
|
2386
|
+
headers: {
|
|
2387
|
+
...credentialHeaders,
|
|
2388
|
+
accept: "application/json",
|
|
2389
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
2390
|
+
},
|
|
2391
|
+
body,
|
|
2392
|
+
signal: AbortSignal.timeout(SLACK_TIMEOUT_MS)
|
|
2393
|
+
});
|
|
2394
|
+
} catch {
|
|
2395
|
+
throw new SlackBotProviderError("transport_error");
|
|
2396
|
+
}
|
|
2397
|
+
if (!response.ok) {
|
|
2398
|
+
await response.body?.cancel().catch(() => void 0);
|
|
2399
|
+
throw new SlackBotProviderError(`http_${response.status}`);
|
|
2400
|
+
}
|
|
2401
|
+
let payload;
|
|
2402
|
+
try {
|
|
2403
|
+
payload = await readResponseJsonBounded(
|
|
2404
|
+
response,
|
|
2405
|
+
SLACK_RESPONSE_MAX_BYTES,
|
|
2406
|
+
`Slack ${method} response`
|
|
2407
|
+
);
|
|
2408
|
+
} catch {
|
|
2409
|
+
throw new SlackBotProviderError("invalid_response");
|
|
2410
|
+
}
|
|
2411
|
+
if (payload.ok !== true) {
|
|
2412
|
+
throw new SlackBotProviderError(slackString(payload.error) || "unknown_error");
|
|
2413
|
+
}
|
|
2414
|
+
return { response, payload };
|
|
2415
|
+
}
|
|
2416
|
+
function assertExactOpenGeniSlackBotScopes(grantedScopes2) {
|
|
2417
|
+
const required = new Set(OPENGENI_SLACK_BOT_REQUIRED_SCOPES);
|
|
2418
|
+
const granted = new Set(grantedScopes2);
|
|
2419
|
+
const missing = [...required].filter((scope) => !granted.has(scope));
|
|
2420
|
+
const forbidden = OPENGENI_SLACK_BOT_FORBIDDEN_SCOPES.filter((scope) => granted.has(scope));
|
|
2421
|
+
const unsupported = grantedScopes2.filter((scope) => !required.has(scope));
|
|
2422
|
+
if (missing.length || forbidden.length || unsupported.length) {
|
|
2423
|
+
const facts = [
|
|
2424
|
+
...missing.length ? [`missing: ${missing.join(", ")}`] : [],
|
|
2425
|
+
...forbidden.length ? [`forbidden: ${forbidden.join(", ")}`] : [],
|
|
2426
|
+
...unsupported.length ? [`unsupported: ${unsupported.join(", ")}`] : []
|
|
2427
|
+
];
|
|
2428
|
+
throw new HTTPException2(422, {
|
|
2429
|
+
message: `Slack bot scopes must exactly match the OpenGeni manifest (${facts.join("; ")})`
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
function parseGrantedScopes(header) {
|
|
2434
|
+
if (!header) {
|
|
2435
|
+
throw new HTTPException2(422, { message: "Slack did not report granted bot scopes" });
|
|
2436
|
+
}
|
|
2437
|
+
return [
|
|
2438
|
+
...new Set(
|
|
2439
|
+
header.split(",").map((scope) => scope.trim()).filter(Boolean)
|
|
2440
|
+
)
|
|
2441
|
+
].sort();
|
|
2442
|
+
}
|
|
2443
|
+
function projectChannel(value) {
|
|
2444
|
+
const channel = slackRecord(value);
|
|
2445
|
+
const id = slackString(channel?.id);
|
|
2446
|
+
if (!channel || !id) return null;
|
|
2447
|
+
return {
|
|
2448
|
+
id,
|
|
2449
|
+
name: boundedSlackString(channel.name, 256),
|
|
2450
|
+
isPrivate: channel.is_private === true,
|
|
2451
|
+
isMember: channel.is_member === true,
|
|
2452
|
+
isArchived: channel.is_archived === true,
|
|
2453
|
+
topic: boundedSlackString(slackRecord(channel.topic)?.value, 1024),
|
|
2454
|
+
purpose: boundedSlackString(slackRecord(channel.purpose)?.value, 1024),
|
|
2455
|
+
numMembers: typeof channel.num_members === "number" && Number.isSafeInteger(channel.num_members) ? channel.num_members : null
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
function projectMessage(value) {
|
|
2459
|
+
const message = slackRecord(value) ?? {};
|
|
2460
|
+
return {
|
|
2461
|
+
timestamp: boundedSlackString(message.ts, 64),
|
|
2462
|
+
userId: boundedSlackString(message.user, 64),
|
|
2463
|
+
botId: boundedSlackString(message.bot_id, 64),
|
|
2464
|
+
threadTimestamp: boundedSlackString(message.thread_ts, 64),
|
|
2465
|
+
text: boundedSlackString(message.text, MAX_PROJECTED_TEXT)
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
function projectUser(value) {
|
|
2469
|
+
const user = slackRecord(value);
|
|
2470
|
+
const id = slackString(user?.id);
|
|
2471
|
+
if (!user || !id) return null;
|
|
2472
|
+
const profile = slackRecord(user.profile);
|
|
2473
|
+
return {
|
|
2474
|
+
id,
|
|
2475
|
+
name: boundedSlackString(user.name, 256),
|
|
2476
|
+
displayName: boundedSlackString(profile?.display_name, 256),
|
|
2477
|
+
realName: boundedSlackString(profile?.real_name, 256),
|
|
2478
|
+
isBot: user.is_bot === true,
|
|
2479
|
+
deleted: user.deleted === true
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
function responseCursor(payload) {
|
|
2483
|
+
return boundedSlackString(slackRecord(payload.response_metadata)?.next_cursor, 1024) || null;
|
|
2484
|
+
}
|
|
2485
|
+
function slackMethodForOperation(operation) {
|
|
2486
|
+
switch (operation) {
|
|
2487
|
+
case "channels.list":
|
|
2488
|
+
return "conversations.list";
|
|
2489
|
+
case "channel_history.read":
|
|
2490
|
+
return "conversations.history";
|
|
2491
|
+
case "users.list":
|
|
2492
|
+
return "users.list";
|
|
2493
|
+
case "message.post":
|
|
2494
|
+
return "chat.postMessage";
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
function boundedInt(value, max, fallback) {
|
|
2498
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? Math.min(value, max) : fallback;
|
|
2499
|
+
}
|
|
2500
|
+
function slackArray(value) {
|
|
2501
|
+
return Array.isArray(value) ? value : [];
|
|
2502
|
+
}
|
|
2503
|
+
function slackRecord(value) {
|
|
2504
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2505
|
+
}
|
|
2506
|
+
function slackString(value) {
|
|
2507
|
+
return typeof value === "string" ? value : "";
|
|
2508
|
+
}
|
|
2509
|
+
function requiredSlackString(value, field) {
|
|
2510
|
+
const result = slackString(value);
|
|
2511
|
+
if (!result || result.length > 256) {
|
|
2512
|
+
throw new SlackBotProviderError(`invalid_${field.replaceAll(".", "_")}`);
|
|
2513
|
+
}
|
|
2514
|
+
return result;
|
|
2515
|
+
}
|
|
2516
|
+
function boundedSlackString(value, max) {
|
|
2517
|
+
return slackString(value).slice(0, max);
|
|
2518
|
+
}
|
|
2519
|
+
function safeSlackCode(value) {
|
|
2520
|
+
return /^[a-z0-9_.:-]{1,128}$/i.test(value) ? value : "unknown_error";
|
|
2521
|
+
}
|
|
2522
|
+
function safeFailureCode(error) {
|
|
2523
|
+
if (error instanceof SlackBotProviderError) return safeSlackCode(error.code);
|
|
2524
|
+
return "local_validation_failed";
|
|
2525
|
+
}
|
|
2526
|
+
function slackPostOutcomeMayBeAmbiguous(error) {
|
|
2527
|
+
if (!(error instanceof SlackBotProviderError)) return true;
|
|
2528
|
+
return error.code === "transport_error" || error.code === "invalid_response" || error.code.startsWith("http_");
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2000
2531
|
// src/mcp/server.ts
|
|
2001
2532
|
function buildOpenGeniMcpServer(deps, grant, options = {}) {
|
|
2002
2533
|
const server = new McpServer({
|
|
@@ -2038,6 +2569,7 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
|
|
|
2038
2569
|
}
|
|
2039
2570
|
if (!toolspaceMode) {
|
|
2040
2571
|
registerRigTools(server, deps, grant, can, sessionId, json);
|
|
2572
|
+
registerSlackBotTools(server, deps, grant, sessionId, json);
|
|
2041
2573
|
}
|
|
2042
2574
|
registerWorkspaceOrchestrationTools(server, deps, grant, can, sessionId, toolspaceMode, json);
|
|
2043
2575
|
registerVariableSetTools(server, deps, grant, can, json);
|
|
@@ -2423,6 +2955,96 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
|
|
|
2423
2955
|
registerToolspaceProxyTools(server, options.toolspace ?? null);
|
|
2424
2956
|
return server;
|
|
2425
2957
|
}
|
|
2958
|
+
function registerSlackBotTools(server, deps, grant, sessionId, json) {
|
|
2959
|
+
const clientFor = async (connectionId) => {
|
|
2960
|
+
const resolved = await resolveSlackBotConnectionForTool({
|
|
2961
|
+
db: deps.db,
|
|
2962
|
+
grant,
|
|
2963
|
+
sessionId,
|
|
2964
|
+
...connectionId ? { requestedConnectionId: connectionId } : {}
|
|
2965
|
+
});
|
|
2966
|
+
return createOpenGeniSlackBotClient(deps, resolved);
|
|
2967
|
+
};
|
|
2968
|
+
server.registerTool(
|
|
2969
|
+
"slack_bot_list_channels",
|
|
2970
|
+
{
|
|
2971
|
+
description: "List public and bot-visible private Slack channels through the workspace-shared OpenGeni bot. isMember identifies channels the bot may read/post in; the bot never joins channels automatically.",
|
|
2972
|
+
inputSchema: {
|
|
2973
|
+
connectionId: z4.string().uuid().optional(),
|
|
2974
|
+
cursor: z4.string().max(1024).optional(),
|
|
2975
|
+
limit: z4.number().int().min(1).max(200).optional()
|
|
2976
|
+
}
|
|
2977
|
+
},
|
|
2978
|
+
async ({ connectionId, cursor, limit }) => json(
|
|
2979
|
+
await (await clientFor(connectionId)).listChannels({
|
|
2980
|
+
...cursor ? { cursor } : {},
|
|
2981
|
+
...limit !== void 0 ? { limit } : {}
|
|
2982
|
+
})
|
|
2983
|
+
)
|
|
2984
|
+
);
|
|
2985
|
+
server.registerTool(
|
|
2986
|
+
"slack_bot_channel_history",
|
|
2987
|
+
{
|
|
2988
|
+
description: "Read Slack channel history as the workspace-shared OpenGeni bot. Public and private channels both require bot membership; invite the bot to private channels first.",
|
|
2989
|
+
inputSchema: {
|
|
2990
|
+
connectionId: z4.string().uuid().optional(),
|
|
2991
|
+
channelId: z4.string().min(1).max(64),
|
|
2992
|
+
cursor: z4.string().max(1024).optional(),
|
|
2993
|
+
limit: z4.number().int().min(1).max(100).optional()
|
|
2994
|
+
}
|
|
2995
|
+
},
|
|
2996
|
+
async ({ connectionId, channelId, cursor, limit }) => json(
|
|
2997
|
+
await (await clientFor(connectionId)).channelHistory({
|
|
2998
|
+
channelId,
|
|
2999
|
+
...cursor ? { cursor } : {},
|
|
3000
|
+
...limit !== void 0 ? { limit } : {}
|
|
3001
|
+
})
|
|
3002
|
+
)
|
|
3003
|
+
);
|
|
3004
|
+
server.registerTool(
|
|
3005
|
+
"slack_bot_list_users",
|
|
3006
|
+
{
|
|
3007
|
+
description: "List Slack workspace users through the workspace-shared OpenGeni bot.",
|
|
3008
|
+
inputSchema: {
|
|
3009
|
+
connectionId: z4.string().uuid().optional(),
|
|
3010
|
+
cursor: z4.string().max(1024).optional(),
|
|
3011
|
+
limit: z4.number().int().min(1).max(200).optional()
|
|
3012
|
+
}
|
|
3013
|
+
},
|
|
3014
|
+
async ({ connectionId, cursor, limit }) => json(
|
|
3015
|
+
await (await clientFor(connectionId)).listUsers({
|
|
3016
|
+
...cursor ? { cursor } : {},
|
|
3017
|
+
...limit !== void 0 ? { limit } : {}
|
|
3018
|
+
})
|
|
3019
|
+
)
|
|
3020
|
+
);
|
|
3021
|
+
server.registerTool(
|
|
3022
|
+
"slack_bot_post_message",
|
|
3023
|
+
{
|
|
3024
|
+
description: "Post as the workspace-shared OpenGeni bot. Pass channelId for a channel where the bot is already a member, or userId to open/post a DM; pass exactly one. Generate one operationId UUID per intended message and reuse that same UUID on every retry.",
|
|
3025
|
+
inputSchema: {
|
|
3026
|
+
connectionId: z4.string().uuid().optional(),
|
|
3027
|
+
operationId: z4.string().uuid(),
|
|
3028
|
+
channelId: z4.string().min(1).max(64).optional(),
|
|
3029
|
+
userId: z4.string().min(1).max(64).optional(),
|
|
3030
|
+
text: z4.string().min(1).max(4e4)
|
|
3031
|
+
}
|
|
3032
|
+
},
|
|
3033
|
+
async ({ connectionId, operationId, channelId, userId, text }) => {
|
|
3034
|
+
if (Boolean(channelId) === Boolean(userId)) {
|
|
3035
|
+
throw new Error("exactly one of channelId or userId is required");
|
|
3036
|
+
}
|
|
3037
|
+
return json(
|
|
3038
|
+
await (await clientFor(connectionId)).postMessage({
|
|
3039
|
+
operationId,
|
|
3040
|
+
...channelId ? { channelId } : {},
|
|
3041
|
+
...userId ? { userId } : {},
|
|
3042
|
+
text
|
|
3043
|
+
})
|
|
3044
|
+
);
|
|
3045
|
+
}
|
|
3046
|
+
);
|
|
3047
|
+
}
|
|
2426
3048
|
function registerToolspaceProxyTools(server, surface) {
|
|
2427
3049
|
if (!surface) {
|
|
2428
3050
|
return;
|
|
@@ -3018,7 +3640,7 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
3018
3640
|
sessionId,
|
|
3019
3641
|
"session.read"
|
|
3020
3642
|
);
|
|
3021
|
-
const session = await
|
|
3643
|
+
const session = await getSession2(deps.db, grant.workspaceId, sessionId);
|
|
3022
3644
|
if (!session) {
|
|
3023
3645
|
throw new Error("session not found");
|
|
3024
3646
|
}
|
|
@@ -4025,7 +4647,7 @@ async function withMcpEffectivePolicy(deps, workspaceId, session) {
|
|
|
4025
4647
|
// src/mcp/toolspace.ts
|
|
4026
4648
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4027
4649
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4028
|
-
import { environmentsEncryptionKeyBytes } from "@opengeni/config";
|
|
4650
|
+
import { environmentsEncryptionKeyBytes as environmentsEncryptionKeyBytes2 } from "@opengeni/config";
|
|
4029
4651
|
import {
|
|
4030
4652
|
prefixedMcpToolName
|
|
4031
4653
|
} from "@opengeni/contracts";
|
|
@@ -4034,7 +4656,7 @@ import {
|
|
|
4034
4656
|
settingsWithEnabledCapabilityMcpServers
|
|
4035
4657
|
} from "@opengeni/core";
|
|
4036
4658
|
import {
|
|
4037
|
-
buildConnectionTokenResolver,
|
|
4659
|
+
buildConnectionTokenResolver as buildConnectionTokenResolver2,
|
|
4038
4660
|
buildHostConnectionTokenResolver,
|
|
4039
4661
|
clearPendingSessionToolspaceCall,
|
|
4040
4662
|
getActiveSessionTurnForExecution,
|
|
@@ -4316,7 +4938,7 @@ function writeToolListCache(key, entries) {
|
|
|
4316
4938
|
toolListCache.write(key, entries);
|
|
4317
4939
|
}
|
|
4318
4940
|
async function settingsWithSessionMcpServersForToolspace(deps, workspaceId, sessionId, attemptId, settings) {
|
|
4319
|
-
const encryptionKey =
|
|
4941
|
+
const encryptionKey = environmentsEncryptionKeyBytes2(settings);
|
|
4320
4942
|
if (!encryptionKey) {
|
|
4321
4943
|
const metadata = await listSessionMcpServerMetadata(deps.db, workspaceId, sessionId);
|
|
4322
4944
|
if (metadata.length === 0) {
|
|
@@ -4638,7 +5260,7 @@ function connectionBrokerFetch(baseFetch, input) {
|
|
|
4638
5260
|
initiator: input.turn.initiator,
|
|
4639
5261
|
initiatorContext: input.turn.initiatorContext,
|
|
4640
5262
|
surface: "toolspace"
|
|
4641
|
-
}) :
|
|
5263
|
+
}) : buildConnectionTokenResolver2(input.deps.db, input.deps.settings);
|
|
4642
5264
|
return async (requestInput, init) => {
|
|
4643
5265
|
const request = await mcpRequestInfo(requestInput, init);
|
|
4644
5266
|
const destinationUrl = mcpRequestDestinationUrl(requestInput);
|
|
@@ -4802,7 +5424,7 @@ import { boundedMcpRequest, McpPayloadTooLargeError as McpPayloadTooLargeError2
|
|
|
4802
5424
|
|
|
4803
5425
|
// src/routes/install.ts
|
|
4804
5426
|
import { readFile, stat } from "fs/promises";
|
|
4805
|
-
import { HTTPException as
|
|
5427
|
+
import { HTTPException as HTTPException3 } from "hono/http-exception";
|
|
4806
5428
|
var INSTALL_DIR = new URL("../../../../agent/install/", import.meta.url);
|
|
4807
5429
|
var BAKED_DIR = new URL("baked/", INSTALL_DIR);
|
|
4808
5430
|
var TEXT_ASSETS = {
|
|
@@ -4900,7 +5522,7 @@ function registerInstallRoutes(app, deps) {
|
|
|
4900
5522
|
app.get("/agent/latest/:asset", async (c) => {
|
|
4901
5523
|
const asset = c.req.param("asset");
|
|
4902
5524
|
if (!ASSET_NAME.test(asset)) {
|
|
4903
|
-
throw new
|
|
5525
|
+
throw new HTTPException3(400, { message: "invalid asset name" });
|
|
4904
5526
|
}
|
|
4905
5527
|
return serveAsset(asset, `${releasesBase}/download/${stableAgentTag}/${asset}`);
|
|
4906
5528
|
});
|
|
@@ -4908,7 +5530,7 @@ function registerInstallRoutes(app, deps) {
|
|
|
4908
5530
|
const versionSeg = c.req.param("versionSeg");
|
|
4909
5531
|
const asset = c.req.param("asset");
|
|
4910
5532
|
if (!VERSION_SEG.test(versionSeg) || !ASSET_NAME.test(asset)) {
|
|
4911
|
-
throw new
|
|
5533
|
+
throw new HTTPException3(400, { message: "invalid version or asset name" });
|
|
4912
5534
|
}
|
|
4913
5535
|
return serveAsset(asset, `${releasesBase}/download/agent-${versionSeg}/${asset}`);
|
|
4914
5536
|
});
|
|
@@ -4966,7 +5588,7 @@ function isAuthExempt(c, settings) {
|
|
|
4966
5588
|
if (installExactPaths.has(path) || isInstallRedirectPath(path)) {
|
|
4967
5589
|
return true;
|
|
4968
5590
|
}
|
|
4969
|
-
if (settings.authAllowHealth && (path === "/healthz" || path === "/readyz")) {
|
|
5591
|
+
if (settings.authAllowHealth && (path === "/healthz" || path === "/readyz" || path === "/traffic-readyz")) {
|
|
4970
5592
|
return true;
|
|
4971
5593
|
}
|
|
4972
5594
|
if (settings.authAllowMetrics && path === "/metrics") {
|
|
@@ -5020,8 +5642,8 @@ import {
|
|
|
5020
5642
|
} from "@opengeni/core";
|
|
5021
5643
|
|
|
5022
5644
|
// src/http/common.ts
|
|
5023
|
-
import { getSession as
|
|
5024
|
-
import { HTTPException as
|
|
5645
|
+
import { getSession as getSession3 } from "@opengeni/db";
|
|
5646
|
+
import { HTTPException as HTTPException4 } from "hono/http-exception";
|
|
5025
5647
|
function boundedLimit(raw) {
|
|
5026
5648
|
const limit = Number(raw ?? 100);
|
|
5027
5649
|
if (!Number.isFinite(limit)) {
|
|
@@ -5030,8 +5652,8 @@ function boundedLimit(raw) {
|
|
|
5030
5652
|
return Math.min(500, Math.max(1, Math.floor(limit)));
|
|
5031
5653
|
}
|
|
5032
5654
|
async function assertSessionExists(db, workspaceId, sessionId) {
|
|
5033
|
-
if (!await
|
|
5034
|
-
throw new
|
|
5655
|
+
if (!await getSession3(db, workspaceId, sessionId)) {
|
|
5656
|
+
throw new HTTPException4(404, { message: "session not found" });
|
|
5035
5657
|
}
|
|
5036
5658
|
}
|
|
5037
5659
|
|
|
@@ -5103,7 +5725,7 @@ function registerCapabilityRoutes(app, deps) {
|
|
|
5103
5725
|
}
|
|
5104
5726
|
|
|
5105
5727
|
// src/routes/catalog-assets.ts
|
|
5106
|
-
import { HTTPException as
|
|
5728
|
+
import { HTTPException as HTTPException5 } from "hono/http-exception";
|
|
5107
5729
|
var CATALOG_ASSET_PREFIX = "catalog-assets/";
|
|
5108
5730
|
var MAX_KEY_LENGTH = 512;
|
|
5109
5731
|
var PRINTABLE_ASCII = /^[\x20-\x7e]+$/;
|
|
@@ -5120,22 +5742,22 @@ function registerCatalogAssetRoutes(app, deps) {
|
|
|
5120
5742
|
const { settings, objectStorage } = deps;
|
|
5121
5743
|
app.get("/v1/catalog-assets/*", async (c) => {
|
|
5122
5744
|
if (!settings.integrationsEnabled) {
|
|
5123
|
-
throw new
|
|
5745
|
+
throw new HTTPException5(404, { message: "integrations are not enabled for this deployment" });
|
|
5124
5746
|
}
|
|
5125
5747
|
if (!objectStorage) {
|
|
5126
|
-
throw new
|
|
5748
|
+
throw new HTTPException5(404, { message: "asset not found" });
|
|
5127
5749
|
}
|
|
5128
5750
|
const key = catalogAssetKeyFromPath(new URL(c.req.url).pathname);
|
|
5129
5751
|
if (!key) {
|
|
5130
|
-
throw new
|
|
5752
|
+
throw new HTTPException5(404, { message: "asset not found" });
|
|
5131
5753
|
}
|
|
5132
5754
|
const contentType = contentTypeForKey(key);
|
|
5133
5755
|
if (!contentType) {
|
|
5134
|
-
throw new
|
|
5756
|
+
throw new HTTPException5(404, { message: "asset not found" });
|
|
5135
5757
|
}
|
|
5136
5758
|
const object4 = await objectStorage.getObjectBytes(key);
|
|
5137
5759
|
if (!object4) {
|
|
5138
|
-
throw new
|
|
5760
|
+
throw new HTTPException5(404, { message: "asset not found" });
|
|
5139
5761
|
}
|
|
5140
5762
|
const etag = etagForKey(key);
|
|
5141
5763
|
const headers = {
|
|
@@ -5188,7 +5810,7 @@ function ifNoneMatchSatisfied(header, etag) {
|
|
|
5188
5810
|
}
|
|
5189
5811
|
|
|
5190
5812
|
// src/routes/codex.ts
|
|
5191
|
-
import { environmentsEncryptionKeyBytes as
|
|
5813
|
+
import { environmentsEncryptionKeyBytes as environmentsEncryptionKeyBytes3 } from "@opengeni/config";
|
|
5192
5814
|
import {
|
|
5193
5815
|
accessTokenExpiry,
|
|
5194
5816
|
buildCodexUsageWindowFromCache,
|
|
@@ -5237,7 +5859,7 @@ import {
|
|
|
5237
5859
|
} from "@opengeni/db";
|
|
5238
5860
|
import { createSignedState as createSignedState2, readSignedState } from "@opengeni/github";
|
|
5239
5861
|
import { hasPermission as hasPermission4, requireAccessGrant as requireAccessGrant2 } from "@opengeni/core";
|
|
5240
|
-
import { HTTPException as
|
|
5862
|
+
import { HTTPException as HTTPException6 } from "hono/http-exception";
|
|
5241
5863
|
import * as z from "zod/v4";
|
|
5242
5864
|
|
|
5243
5865
|
// src/codex-redemption-security.ts
|
|
@@ -5377,48 +5999,48 @@ async function managedCookieHuman(c, deps) {
|
|
|
5377
5999
|
function requireSameOriginBrowserMutation(c, deps) {
|
|
5378
6000
|
const contentType = c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
5379
6001
|
if (contentType !== "application/json") {
|
|
5380
|
-
throw new
|
|
6002
|
+
throw new HTTPException6(403, {
|
|
5381
6003
|
message: "JSON browser request required"
|
|
5382
6004
|
});
|
|
5383
6005
|
}
|
|
5384
6006
|
if (!deps.settings.publicBaseUrl) {
|
|
5385
|
-
throw new
|
|
6007
|
+
throw new HTTPException6(503, {
|
|
5386
6008
|
message: "managed browser origin is not configured"
|
|
5387
6009
|
});
|
|
5388
6010
|
}
|
|
5389
6011
|
const expectedOrigin = new URL(deps.settings.publicBaseUrl).origin;
|
|
5390
6012
|
if (c.req.header("origin") !== expectedOrigin) {
|
|
5391
|
-
throw new
|
|
6013
|
+
throw new HTTPException6(403, {
|
|
5392
6014
|
message: "same-origin browser request required"
|
|
5393
6015
|
});
|
|
5394
6016
|
}
|
|
5395
6017
|
if (c.req.header("sec-fetch-site")?.toLowerCase() !== "same-origin") {
|
|
5396
|
-
throw new
|
|
6018
|
+
throw new HTTPException6(403, {
|
|
5397
6019
|
message: "same-origin fetch metadata required"
|
|
5398
6020
|
});
|
|
5399
6021
|
}
|
|
5400
6022
|
}
|
|
5401
6023
|
async function requireRedemptionHuman(c, deps, workspaceId) {
|
|
5402
6024
|
if (deps.settings.productAccessMode !== "managed") {
|
|
5403
|
-
throw new
|
|
6025
|
+
throw new HTTPException6(403, {
|
|
5404
6026
|
message: "reset redemption requires managed product mode"
|
|
5405
6027
|
});
|
|
5406
6028
|
}
|
|
5407
6029
|
if (c.req.header("authorization")) {
|
|
5408
|
-
throw new
|
|
6030
|
+
throw new HTTPException6(403, {
|
|
5409
6031
|
message: "authorization bearer is not allowed for redemption"
|
|
5410
6032
|
});
|
|
5411
6033
|
}
|
|
5412
6034
|
requireSameOriginBrowserMutation(c, deps);
|
|
5413
6035
|
const human = await managedCookieHuman(c, deps);
|
|
5414
6036
|
if (!human) {
|
|
5415
|
-
throw new
|
|
6037
|
+
throw new HTTPException6(401, {
|
|
5416
6038
|
message: "managed browser session required"
|
|
5417
6039
|
});
|
|
5418
6040
|
}
|
|
5419
6041
|
const grant = await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
|
|
5420
6042
|
if (grant.subjectId !== human.subjectId) {
|
|
5421
|
-
throw new
|
|
6043
|
+
throw new HTTPException6(403, {
|
|
5422
6044
|
message: "managed browser identity mismatch"
|
|
5423
6045
|
});
|
|
5424
6046
|
}
|
|
@@ -5618,7 +6240,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
5618
6240
|
try {
|
|
5619
6241
|
start = await startDeviceCode();
|
|
5620
6242
|
} catch (error) {
|
|
5621
|
-
throw new
|
|
6243
|
+
throw new HTTPException6(502, {
|
|
5622
6244
|
message: error instanceof CodexDeviceError ? error.message : "failed to start Codex device login"
|
|
5623
6245
|
});
|
|
5624
6246
|
}
|
|
@@ -5640,7 +6262,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
5640
6262
|
const { state } = await c.req.json();
|
|
5641
6263
|
const payload = state ? readSignedState(state, githubStateSecret) : null;
|
|
5642
6264
|
if (!payload || payload.workspaceId !== workspaceId || !payload.deviceAuthId || !payload.userCode) {
|
|
5643
|
-
throw new
|
|
6265
|
+
throw new HTTPException6(400, {
|
|
5644
6266
|
message: "codex connect state is invalid or expired"
|
|
5645
6267
|
});
|
|
5646
6268
|
}
|
|
@@ -5654,7 +6276,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
5654
6276
|
userCode: payload.userCode
|
|
5655
6277
|
});
|
|
5656
6278
|
} catch (error) {
|
|
5657
|
-
throw new
|
|
6279
|
+
throw new HTTPException6(502, {
|
|
5658
6280
|
message: error instanceof CodexDeviceError ? error.message : "codex device poll failed"
|
|
5659
6281
|
});
|
|
5660
6282
|
}
|
|
@@ -5671,15 +6293,15 @@ function registerCodexRoutes(app, deps) {
|
|
|
5671
6293
|
codeVerifier: poll.codeVerifier
|
|
5672
6294
|
});
|
|
5673
6295
|
} catch (error) {
|
|
5674
|
-
throw new
|
|
6296
|
+
throw new HTTPException6(502, {
|
|
5675
6297
|
message: error instanceof CodexDeviceError ? error.message : "codex token exchange failed"
|
|
5676
6298
|
});
|
|
5677
6299
|
}
|
|
5678
6300
|
const id = parseIdToken(tokens.idToken);
|
|
5679
6301
|
const connectingHuman = await managedCookieHuman(c, deps);
|
|
5680
|
-
const key =
|
|
6302
|
+
const key = environmentsEncryptionKeyBytes3(settings);
|
|
5681
6303
|
if (!key) {
|
|
5682
|
-
throw new
|
|
6304
|
+
throw new HTTPException6(500, {
|
|
5683
6305
|
message: "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured"
|
|
5684
6306
|
});
|
|
5685
6307
|
}
|
|
@@ -5715,7 +6337,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
5715
6337
|
);
|
|
5716
6338
|
const upserted = mutation.result;
|
|
5717
6339
|
if (upserted.kind === "unresolved_redemption") {
|
|
5718
|
-
throw new
|
|
6340
|
+
throw new HTTPException6(409, {
|
|
5719
6341
|
message: "this subscription has an unresolved reset redemption; recover it before changing ownership"
|
|
5720
6342
|
});
|
|
5721
6343
|
}
|
|
@@ -5811,7 +6433,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
5811
6433
|
);
|
|
5812
6434
|
const activated = mutation.result;
|
|
5813
6435
|
if (!activated) {
|
|
5814
|
-
throw new
|
|
6436
|
+
throw new HTTPException6(404, { message: "codex account not found" });
|
|
5815
6437
|
}
|
|
5816
6438
|
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
5817
6439
|
return c.json({ activated: true, accountId });
|
|
@@ -5825,7 +6447,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
5825
6447
|
patch.rotationEnabled = body.rotationEnabled;
|
|
5826
6448
|
}
|
|
5827
6449
|
if (patch.rotationEnabled === void 0 && body.rotationStrategy === void 0) {
|
|
5828
|
-
throw new
|
|
6450
|
+
throw new HTTPException6(400, { message: "no settings to update" });
|
|
5829
6451
|
}
|
|
5830
6452
|
if (patch.rotationEnabled === void 0) {
|
|
5831
6453
|
return c.json({ rotationStrategy: "sharded", rotationStrategyDeprecated: true });
|
|
@@ -5841,7 +6463,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
5841
6463
|
);
|
|
5842
6464
|
const updated = mutation.result;
|
|
5843
6465
|
if (!updated) {
|
|
5844
|
-
throw new
|
|
6466
|
+
throw new HTTPException6(404, {
|
|
5845
6467
|
message: "codex rotation settings not found"
|
|
5846
6468
|
});
|
|
5847
6469
|
}
|
|
@@ -5861,12 +6483,12 @@ function registerCodexRoutes(app, deps) {
|
|
|
5861
6483
|
const label = typeof body.label === "string" ? body.label : null;
|
|
5862
6484
|
const renamed = await renameCodexAccount(db, workspaceId, accountId, label);
|
|
5863
6485
|
if (!renamed) {
|
|
5864
|
-
throw new
|
|
6486
|
+
throw new HTTPException6(404, { message: "codex account not found" });
|
|
5865
6487
|
}
|
|
5866
6488
|
const accounts = await listCodexAccountStatuses(db, workspaceId);
|
|
5867
6489
|
const row = accounts.find((account) => account.id === accountId);
|
|
5868
6490
|
if (!row) {
|
|
5869
|
-
throw new
|
|
6491
|
+
throw new HTTPException6(404, { message: "codex account not found" });
|
|
5870
6492
|
}
|
|
5871
6493
|
return c.json(codexAccountJson(row));
|
|
5872
6494
|
});
|
|
@@ -5878,7 +6500,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
5878
6500
|
expectedVersion: z.number().int().positive()
|
|
5879
6501
|
}).safeParse(await c.req.json().catch(() => null));
|
|
5880
6502
|
if (!parsed.success) {
|
|
5881
|
-
throw new
|
|
6503
|
+
throw new HTTPException6(400, {
|
|
5882
6504
|
message: "enabled and expectedVersion are required"
|
|
5883
6505
|
});
|
|
5884
6506
|
}
|
|
@@ -5892,7 +6514,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
5892
6514
|
});
|
|
5893
6515
|
const result = mutation.result;
|
|
5894
6516
|
if (result.kind === "not_found") {
|
|
5895
|
-
throw new
|
|
6517
|
+
throw new HTTPException6(404, { message: "codex account not found" });
|
|
5896
6518
|
}
|
|
5897
6519
|
const response = {
|
|
5898
6520
|
allocatorEnabled: result.allocatorEnabled,
|
|
@@ -5917,7 +6539,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
5917
6539
|
);
|
|
5918
6540
|
const result = mutation.result;
|
|
5919
6541
|
if (result.blockedByUnresolvedRedemption) {
|
|
5920
|
-
throw new
|
|
6542
|
+
throw new HTTPException6(409, {
|
|
5921
6543
|
message: "this subscription has an unresolved reset redemption; recover it before disconnecting"
|
|
5922
6544
|
});
|
|
5923
6545
|
}
|
|
@@ -5937,7 +6559,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
5937
6559
|
);
|
|
5938
6560
|
const result = mutation.result;
|
|
5939
6561
|
if (result.blockedCredentialIds.length > 0) {
|
|
5940
|
-
throw new
|
|
6562
|
+
throw new HTTPException6(409, {
|
|
5941
6563
|
message: "one or more subscriptions have unresolved reset redemptions; recover them before disconnecting"
|
|
5942
6564
|
});
|
|
5943
6565
|
}
|
|
@@ -5949,7 +6571,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
5949
6571
|
await requireAccessGrant2(c, deps, workspaceId, "workspace:read");
|
|
5950
6572
|
const status = await getCodexCredentialStatus(db, workspaceId);
|
|
5951
6573
|
if (!status?.credentialId) {
|
|
5952
|
-
throw new
|
|
6574
|
+
throw new HTTPException6(404, {
|
|
5953
6575
|
message: "codex subscription is not connected"
|
|
5954
6576
|
});
|
|
5955
6577
|
}
|
|
@@ -5963,7 +6585,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
5963
6585
|
const accountId = c.req.param("accountId");
|
|
5964
6586
|
const accounts = await listCodexAccountStatuses(db, workspaceId);
|
|
5965
6587
|
if (!accounts.some((account) => account.id === accountId)) {
|
|
5966
|
-
throw new
|
|
6588
|
+
throw new HTTPException6(404, { message: "codex account not found" });
|
|
5967
6589
|
}
|
|
5968
6590
|
const payload = await fetchCodexUsageForAccount(db, settings, workspaceId, accountId);
|
|
5969
6591
|
await signalPendingCodexCapacityTargets(deps, workspaceId);
|
|
@@ -6088,21 +6710,21 @@ function registerCodexRoutes(app, deps) {
|
|
|
6088
6710
|
c.header("cache-control", "no-store");
|
|
6089
6711
|
const parsed = redemptionPrepareBody.safeParse(await c.req.json().catch(() => null));
|
|
6090
6712
|
if (!parsed.success) {
|
|
6091
|
-
throw new
|
|
6713
|
+
throw new HTTPException6(400, {
|
|
6092
6714
|
message: "attemptId and creditId are required"
|
|
6093
6715
|
});
|
|
6094
6716
|
}
|
|
6095
6717
|
const accounts = await listCodexAccountStatuses(db, workspaceId);
|
|
6096
6718
|
const account = accounts.find((candidate) => candidate.id === credentialId);
|
|
6097
|
-
if (!account) throw new
|
|
6719
|
+
if (!account) throw new HTTPException6(404, { message: "codex account not found" });
|
|
6098
6720
|
let existing = await getCodexResetRedemptionAttempt(db, workspaceId, parsed.data.attemptId);
|
|
6099
6721
|
if (account.connectedBySubjectId !== human.subjectId) {
|
|
6100
|
-
throw new
|
|
6722
|
+
throw new HTTPException6(403, {
|
|
6101
6723
|
message: "only the human who connected this subscription may redeem its reset credits"
|
|
6102
6724
|
});
|
|
6103
6725
|
}
|
|
6104
6726
|
if (existing && (existing.credentialId !== credentialId || existing.creditId !== parsed.data.creditId || existing.subjectId !== human.subjectId)) {
|
|
6105
|
-
throw new
|
|
6727
|
+
throw new HTTPException6(409, {
|
|
6106
6728
|
message: "logical redemption attempt identity mismatch"
|
|
6107
6729
|
});
|
|
6108
6730
|
}
|
|
@@ -6117,29 +6739,29 @@ function registerCodexRoutes(app, deps) {
|
|
|
6117
6739
|
browserSessionHash: human.browserSessionHash
|
|
6118
6740
|
});
|
|
6119
6741
|
if (adoption.kind === "in_progress") {
|
|
6120
|
-
throw new
|
|
6742
|
+
throw new HTTPException6(409, {
|
|
6121
6743
|
message: "this redemption is still in progress in another browser request"
|
|
6122
6744
|
});
|
|
6123
6745
|
}
|
|
6124
6746
|
if (adoption.kind === "not_found") {
|
|
6125
|
-
throw new
|
|
6747
|
+
throw new HTTPException6(409, { message: "redemption recovery state changed" });
|
|
6126
6748
|
}
|
|
6127
6749
|
if (adoption.kind === "forbidden") {
|
|
6128
|
-
throw new
|
|
6750
|
+
throw new HTTPException6(403, { message: "redemption owner is unavailable" });
|
|
6129
6751
|
}
|
|
6130
6752
|
if (adoption.kind === "conflict") {
|
|
6131
|
-
throw new
|
|
6753
|
+
throw new HTTPException6(409, {
|
|
6132
6754
|
message: "logical redemption attempt identity mismatch"
|
|
6133
6755
|
});
|
|
6134
6756
|
}
|
|
6135
6757
|
existing = adoption.attempt;
|
|
6136
6758
|
}
|
|
6137
6759
|
if (account.status !== "active" && existing?.status !== "completed") {
|
|
6138
|
-
throw new
|
|
6760
|
+
throw new HTTPException6(403, { message: "redemption credential is unavailable" });
|
|
6139
6761
|
}
|
|
6140
6762
|
const secret = settings.betterAuthSecret;
|
|
6141
6763
|
if (!secret) {
|
|
6142
|
-
throw new
|
|
6764
|
+
throw new HTTPException6(503, {
|
|
6143
6765
|
message: "managed browser confirmation is unavailable"
|
|
6144
6766
|
});
|
|
6145
6767
|
}
|
|
@@ -6175,19 +6797,19 @@ function registerCodexRoutes(app, deps) {
|
|
|
6175
6797
|
c.header("cache-control", "no-store");
|
|
6176
6798
|
const parsed = redemptionBody.safeParse(await c.req.json().catch(() => null));
|
|
6177
6799
|
if (!parsed.success) {
|
|
6178
|
-
throw new
|
|
6800
|
+
throw new HTTPException6(400, {
|
|
6179
6801
|
message: "explicit redemption confirmation is required"
|
|
6180
6802
|
});
|
|
6181
6803
|
}
|
|
6182
6804
|
const secret = settings.betterAuthSecret;
|
|
6183
6805
|
if (!secret) {
|
|
6184
|
-
throw new
|
|
6806
|
+
throw new HTTPException6(503, {
|
|
6185
6807
|
message: "managed browser confirmation is unavailable"
|
|
6186
6808
|
});
|
|
6187
6809
|
}
|
|
6188
6810
|
const claims = await verifyCodexRedemptionConfirmation(secret, parsed.data.confirmationToken);
|
|
6189
6811
|
if (!claims || claims.attemptId !== parsed.data.attemptId || claims.workspaceId !== workspaceId || claims.credentialId !== credentialId || claims.creditId !== parsed.data.creditId || claims.subjectId !== human.subjectId || claims.browserSessionHash !== human.browserSessionHash) {
|
|
6190
|
-
throw new
|
|
6812
|
+
throw new HTTPException6(403, {
|
|
6191
6813
|
message: "redemption confirmation is invalid or expired"
|
|
6192
6814
|
});
|
|
6193
6815
|
}
|
|
@@ -6204,15 +6826,15 @@ function registerCodexRoutes(app, deps) {
|
|
|
6204
6826
|
claimHolderId
|
|
6205
6827
|
});
|
|
6206
6828
|
if (claimed.kind === "not_found") {
|
|
6207
|
-
throw new
|
|
6829
|
+
throw new HTTPException6(404, { message: "codex account not found" });
|
|
6208
6830
|
}
|
|
6209
6831
|
if (claimed.kind === "forbidden") {
|
|
6210
|
-
throw new
|
|
6832
|
+
throw new HTTPException6(403, {
|
|
6211
6833
|
message: "redemption owner or credential is unavailable"
|
|
6212
6834
|
});
|
|
6213
6835
|
}
|
|
6214
6836
|
if (claimed.kind === "conflict") {
|
|
6215
|
-
throw new
|
|
6837
|
+
throw new HTTPException6(409, {
|
|
6216
6838
|
message: "logical redemption attempt identity mismatch"
|
|
6217
6839
|
});
|
|
6218
6840
|
}
|
|
@@ -6370,6 +6992,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
6370
6992
|
|
|
6371
6993
|
// src/routes/connections.ts
|
|
6372
6994
|
import {
|
|
6995
|
+
ConnectOpenGeniSlackBotRequest,
|
|
6373
6996
|
ConnectionResponse,
|
|
6374
6997
|
CreateConnectionRequest,
|
|
6375
6998
|
IntegrationClientMetadata,
|
|
@@ -6378,16 +7001,23 @@ import {
|
|
|
6378
7001
|
OAuthStartResponse as OAuthStartResponse2,
|
|
6379
7002
|
UpdateConnectionRequest
|
|
6380
7003
|
} from "@opengeni/contracts";
|
|
6381
|
-
import {
|
|
7004
|
+
import {
|
|
7005
|
+
hasReservedOpenGeniSlackBotMetadata,
|
|
7006
|
+
isOpenGeniSlackBotConnection,
|
|
7007
|
+
openGeniSlackBotMetadata as openGeniSlackBotMetadata2,
|
|
7008
|
+
requireAccessGrant as requireAccessGrant3,
|
|
7009
|
+
requireEnvironmentEncryption as requireEnvironmentEncryption2
|
|
7010
|
+
} from "@opengeni/core";
|
|
6382
7011
|
import {
|
|
6383
7012
|
createConnection as createConnection2,
|
|
6384
7013
|
encryptEnvironmentValue as encryptEnvironmentValue3,
|
|
6385
7014
|
getConnectionMetadata as getConnectionMetadata2,
|
|
6386
7015
|
listConnectionsMetadata as listConnectionsMetadata2,
|
|
7016
|
+
recordAuditEvent as recordAuditEvent2,
|
|
6387
7017
|
revokeConnection,
|
|
6388
7018
|
updateConnection as updateConnection2
|
|
6389
7019
|
} from "@opengeni/db";
|
|
6390
|
-
import { HTTPException as
|
|
7020
|
+
import { HTTPException as HTTPException9 } from "hono/http-exception";
|
|
6391
7021
|
|
|
6392
7022
|
// src/integrations/oauth-client.ts
|
|
6393
7023
|
import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
|
|
@@ -6413,19 +7043,19 @@ import {
|
|
|
6413
7043
|
OAUTH_MAX_RESPONSE_BYTES,
|
|
6414
7044
|
isLocalTestEnvironment,
|
|
6415
7045
|
pinnedFetch,
|
|
6416
|
-
readResponseJsonBounded,
|
|
7046
|
+
readResponseJsonBounded as readResponseJsonBounded2,
|
|
6417
7047
|
validateHttpUrl
|
|
6418
7048
|
} from "@opengeni/network";
|
|
6419
7049
|
import { Buffer as Buffer3 } from "buffer";
|
|
6420
7050
|
import { createHash as createHash3, randomBytes } from "crypto";
|
|
6421
|
-
import { HTTPException as
|
|
7051
|
+
import { HTTPException as HTTPException8 } from "hono/http-exception";
|
|
6422
7052
|
|
|
6423
7053
|
// src/integrations/provider-domain.ts
|
|
6424
|
-
import { HTTPException as
|
|
7054
|
+
import { HTTPException as HTTPException7 } from "hono/http-exception";
|
|
6425
7055
|
function canonicalProviderDomain(value) {
|
|
6426
7056
|
const canonical = value.trim().toLowerCase().replace(/^www\./, "");
|
|
6427
7057
|
if (!canonical) {
|
|
6428
|
-
throw new
|
|
7058
|
+
throw new HTTPException7(400, { message: "providerDomain must not be empty" });
|
|
6429
7059
|
}
|
|
6430
7060
|
return canonical;
|
|
6431
7061
|
}
|
|
@@ -6459,7 +7089,7 @@ async function startMcpOAuth(deps, context) {
|
|
|
6459
7089
|
connectionId: context.payload.connectionId
|
|
6460
7090
|
});
|
|
6461
7091
|
if (context.payload.connectionId && !existing) {
|
|
6462
|
-
throw new
|
|
7092
|
+
throw new HTTPException8(404, { message: "connection not found" });
|
|
6463
7093
|
}
|
|
6464
7094
|
const discovery = await discoverMcpOAuth(mcpUrl, settings);
|
|
6465
7095
|
const resource = discovery.prm.resource ? canonicalOAuthResource(discovery.prm.resource) : mcpUrl;
|
|
@@ -6543,7 +7173,7 @@ async function completeMcpOAuthCallback(deps, input) {
|
|
|
6543
7173
|
now: /* @__PURE__ */ new Date()
|
|
6544
7174
|
});
|
|
6545
7175
|
if (!consumed) {
|
|
6546
|
-
throw new
|
|
7176
|
+
throw new HTTPException8(400, { message: "OAuth state has already been used" });
|
|
6547
7177
|
}
|
|
6548
7178
|
} catch (error) {
|
|
6549
7179
|
const staged = new OAuthCallbackStageError("state_verify", "state_invalid", error);
|
|
@@ -6617,7 +7247,7 @@ async function completeMcpOAuthCallback(deps, input) {
|
|
|
6617
7247
|
})
|
|
6618
7248
|
);
|
|
6619
7249
|
if (!connection) {
|
|
6620
|
-
throw new
|
|
7250
|
+
throw new HTTPException8(409, {
|
|
6621
7251
|
message: "connection changed during OAuth reconnect; start again"
|
|
6622
7252
|
});
|
|
6623
7253
|
}
|
|
@@ -6640,7 +7270,7 @@ function integrationBaseUrl(publicBaseUrl, requestUrl) {
|
|
|
6640
7270
|
function requireIntegrationsStateSecret(settings) {
|
|
6641
7271
|
const secret = settings.integrationsStateSecret?.trim();
|
|
6642
7272
|
if (!secret) {
|
|
6643
|
-
throw new
|
|
7273
|
+
throw new HTTPException8(503, {
|
|
6644
7274
|
message: "integrations OAuth requires OPENGENI_INTEGRATIONS_STATE_SECRET"
|
|
6645
7275
|
});
|
|
6646
7276
|
}
|
|
@@ -6655,13 +7285,13 @@ async function discoverMcpOAuth(resource, settings) {
|
|
|
6655
7285
|
);
|
|
6656
7286
|
const authorizationServer = prm.authorizationServers[0];
|
|
6657
7287
|
if (!authorizationServer) {
|
|
6658
|
-
throw new
|
|
7288
|
+
throw new HTTPException8(422, {
|
|
6659
7289
|
message: "MCP protected resource metadata did not advertise an authorization server"
|
|
6660
7290
|
});
|
|
6661
7291
|
}
|
|
6662
7292
|
const as = await discoverAuthorizationServerMetadata(authorizationServer, settings);
|
|
6663
7293
|
if (!as.codeChallengeMethodsSupported.includes("S256")) {
|
|
6664
|
-
throw new
|
|
7294
|
+
throw new HTTPException8(422, {
|
|
6665
7295
|
message: "authorization server does not support required PKCE S256"
|
|
6666
7296
|
});
|
|
6667
7297
|
}
|
|
@@ -6688,7 +7318,7 @@ async function discoverProtectedResourceMetadata(resource, settings, advertisedU
|
|
|
6688
7318
|
]);
|
|
6689
7319
|
for (const candidate of candidates) {
|
|
6690
7320
|
const payload = await fetchJsonObject(candidate, settings).catch((error) => {
|
|
6691
|
-
if (error instanceof
|
|
7321
|
+
if (error instanceof HTTPException8) {
|
|
6692
7322
|
throw error;
|
|
6693
7323
|
}
|
|
6694
7324
|
return null;
|
|
@@ -6707,7 +7337,7 @@ async function discoverProtectedResourceMetadata(resource, settings, advertisedU
|
|
|
6707
7337
|
...stringValue(payload.resource) ? { resource: stringValue(payload.resource) } : {}
|
|
6708
7338
|
};
|
|
6709
7339
|
}
|
|
6710
|
-
throw new
|
|
7340
|
+
throw new HTTPException8(422, { message: "could not discover MCP protected resource metadata" });
|
|
6711
7341
|
}
|
|
6712
7342
|
async function discoverAuthorizationServerMetadata(authorizationServer, settings) {
|
|
6713
7343
|
const safeAuthorizationServer = oauthEndpointUrl(
|
|
@@ -6722,7 +7352,7 @@ async function discoverAuthorizationServerMetadata(authorizationServer, settings
|
|
|
6722
7352
|
]);
|
|
6723
7353
|
for (const candidate of candidates) {
|
|
6724
7354
|
const payload = await fetchJsonObject(candidate, settings).catch((error) => {
|
|
6725
|
-
if (error instanceof
|
|
7355
|
+
if (error instanceof HTTPException8) {
|
|
6726
7356
|
throw error;
|
|
6727
7357
|
}
|
|
6728
7358
|
return null;
|
|
@@ -6760,7 +7390,7 @@ async function discoverAuthorizationServerMetadata(authorizationServer, settings
|
|
|
6760
7390
|
...safeRegistrationEndpoint ? { registrationEndpoint: safeRegistrationEndpoint } : {}
|
|
6761
7391
|
};
|
|
6762
7392
|
}
|
|
6763
|
-
throw new
|
|
7393
|
+
throw new HTTPException8(422, {
|
|
6764
7394
|
message: "could not discover OAuth authorization server metadata"
|
|
6765
7395
|
});
|
|
6766
7396
|
}
|
|
@@ -6809,7 +7439,7 @@ async function getOrCreateDynamicClientRegistration(db, settings, as, redirectUr
|
|
|
6809
7439
|
};
|
|
6810
7440
|
}
|
|
6811
7441
|
if (!as.registrationEndpoint) {
|
|
6812
|
-
throw new
|
|
7442
|
+
throw new HTTPException8(422, {
|
|
6813
7443
|
message: "manual OAuth client credentials are required for this authorization server"
|
|
6814
7444
|
});
|
|
6815
7445
|
}
|
|
@@ -6827,7 +7457,7 @@ async function getOrCreateDynamicClientRegistration(db, settings, as, redirectUr
|
|
|
6827
7457
|
if (storedWinner.clientId !== dcr.clientId) {
|
|
6828
7458
|
const winner = await loadIntegrationOAuthClient(db, settings, as.issuer);
|
|
6829
7459
|
if (!winner) {
|
|
6830
|
-
throw new
|
|
7460
|
+
throw new HTTPException8(422, {
|
|
6831
7461
|
message: "OAuth client registration could not be loaded after a registration race"
|
|
6832
7462
|
});
|
|
6833
7463
|
}
|
|
@@ -6905,7 +7535,7 @@ function normalizedIssuerKey(value) {
|
|
|
6905
7535
|
}
|
|
6906
7536
|
async function dynamicClientRegistration(settings, as, redirectUri, scopes) {
|
|
6907
7537
|
if (!as.registrationEndpoint) {
|
|
6908
|
-
throw new
|
|
7538
|
+
throw new HTTPException8(422, {
|
|
6909
7539
|
message: "authorization server does not support dynamic client registration"
|
|
6910
7540
|
});
|
|
6911
7541
|
}
|
|
@@ -6923,18 +7553,18 @@ async function dynamicClientRegistration(settings, as, redirectUri, scopes) {
|
|
|
6923
7553
|
});
|
|
6924
7554
|
if (!response.ok) {
|
|
6925
7555
|
await cancelResponseBody(response);
|
|
6926
|
-
throw new
|
|
7556
|
+
throw new HTTPException8(422, {
|
|
6927
7557
|
message: `dynamic client registration failed with HTTP ${response.status}`
|
|
6928
7558
|
});
|
|
6929
7559
|
}
|
|
6930
|
-
const payload = await
|
|
7560
|
+
const payload = await readResponseJsonBounded2(
|
|
6931
7561
|
response,
|
|
6932
7562
|
OAUTH_MAX_RESPONSE_BYTES,
|
|
6933
7563
|
"OAuth dynamic registration response"
|
|
6934
7564
|
);
|
|
6935
7565
|
const clientId = stringValue(payload.client_id);
|
|
6936
7566
|
if (!clientId) {
|
|
6937
|
-
throw new
|
|
7567
|
+
throw new HTTPException8(422, {
|
|
6938
7568
|
message: "dynamic client registration response did not include client_id"
|
|
6939
7569
|
});
|
|
6940
7570
|
}
|
|
@@ -6978,12 +7608,12 @@ function buildAuthorizationUrl(input) {
|
|
|
6978
7608
|
function readOAuthState(state, settings) {
|
|
6979
7609
|
const payload = readSignedState2(state, requireIntegrationsStateSecret(settings));
|
|
6980
7610
|
if (!payload) {
|
|
6981
|
-
throw new
|
|
7611
|
+
throw new HTTPException8(400, { message: "invalid or expired OAuth state" });
|
|
6982
7612
|
}
|
|
6983
7613
|
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
6984
7614
|
const iat = numberValue(payload.iat);
|
|
6985
7615
|
if (iat === void 0 || nowSeconds - iat > oauthStateTtlMs / 1e3 || nowSeconds < iat) {
|
|
6986
|
-
throw new
|
|
7616
|
+
throw new HTTPException8(400, { message: "invalid or expired OAuth state" });
|
|
6987
7617
|
}
|
|
6988
7618
|
const resource = requiredString(payload.resource, "state.resource");
|
|
6989
7619
|
const parsed = {
|
|
@@ -7054,7 +7684,7 @@ async function clientForState(db, settings, state) {
|
|
|
7054
7684
|
if (state.clientRegistrationMethod === "dcr") {
|
|
7055
7685
|
const stored = await loadIntegrationOAuthClient(db, settings, state.issuer);
|
|
7056
7686
|
if (!stored || stored.clientId !== state.clientId || stored.issuer !== state.issuer || stored.authorizationServer !== state.authorizationServer) {
|
|
7057
|
-
throw new
|
|
7687
|
+
throw new HTTPException8(400, { message: "OAuth client registration is no longer available" });
|
|
7058
7688
|
}
|
|
7059
7689
|
return {
|
|
7060
7690
|
method: "dcr",
|
|
@@ -7070,7 +7700,7 @@ async function clientForState(db, settings, state) {
|
|
|
7070
7700
|
}
|
|
7071
7701
|
const entry = operatorClientEntryFor(settings, [state.issuer, state.authorizationServer]);
|
|
7072
7702
|
if (!entry || entry.clientId !== state.clientId) {
|
|
7073
|
-
throw new
|
|
7703
|
+
throw new HTTPException8(400, {
|
|
7074
7704
|
message: "operator OAuth client credentials are no longer available"
|
|
7075
7705
|
});
|
|
7076
7706
|
}
|
|
@@ -7118,7 +7748,7 @@ async function exchangeAuthorizationCode(settings, input) {
|
|
|
7118
7748
|
new Error(`OAuth token endpoint returned HTTP ${response.status}`)
|
|
7119
7749
|
);
|
|
7120
7750
|
}
|
|
7121
|
-
const payload = await
|
|
7751
|
+
const payload = await readResponseJsonBounded2(
|
|
7122
7752
|
response,
|
|
7123
7753
|
OAUTH_MAX_RESPONSE_BYTES,
|
|
7124
7754
|
"OAuth token response"
|
|
@@ -7172,7 +7802,7 @@ function logOAuthVerificationWarning(observability, error, state) {
|
|
|
7172
7802
|
});
|
|
7173
7803
|
}
|
|
7174
7804
|
function sanitizedError(error) {
|
|
7175
|
-
if (error instanceof
|
|
7805
|
+
if (error instanceof HTTPException8) {
|
|
7176
7806
|
return `HTTPException ${error.status}: ${error.message}`;
|
|
7177
7807
|
}
|
|
7178
7808
|
if (error instanceof Error) {
|
|
@@ -7196,7 +7826,7 @@ async function oauthErrorFromResponse(response) {
|
|
|
7196
7826
|
await cancelResponseBody(response);
|
|
7197
7827
|
return null;
|
|
7198
7828
|
}
|
|
7199
|
-
const payload = await
|
|
7829
|
+
const payload = await readResponseJsonBounded2(
|
|
7200
7830
|
response,
|
|
7201
7831
|
OAUTH_MAX_RESPONSE_BYTES,
|
|
7202
7832
|
"OAuth token error response"
|
|
@@ -7288,13 +7918,13 @@ function callbackReturnPath(returnPath, status, params) {
|
|
|
7288
7918
|
}
|
|
7289
7919
|
function canonicalMcpResource(value) {
|
|
7290
7920
|
if (!value) {
|
|
7291
|
-
throw new
|
|
7921
|
+
throw new HTTPException8(400, { message: "mcpUrl is required" });
|
|
7292
7922
|
}
|
|
7293
7923
|
let url;
|
|
7294
7924
|
try {
|
|
7295
7925
|
url = new URL(value);
|
|
7296
7926
|
} catch {
|
|
7297
|
-
throw new
|
|
7927
|
+
throw new HTTPException8(422, { message: "MCP resource URL is invalid" });
|
|
7298
7928
|
}
|
|
7299
7929
|
url.hash = "";
|
|
7300
7930
|
return url.toString();
|
|
@@ -7302,7 +7932,7 @@ function canonicalMcpResource(value) {
|
|
|
7302
7932
|
function canonicalOAuthResource(value) {
|
|
7303
7933
|
const trimmed = value.trim();
|
|
7304
7934
|
if (!trimmed) {
|
|
7305
|
-
throw new
|
|
7935
|
+
throw new HTTPException8(422, {
|
|
7306
7936
|
message: "MCP protected resource metadata advertised an invalid resource"
|
|
7307
7937
|
});
|
|
7308
7938
|
}
|
|
@@ -7314,7 +7944,7 @@ function canonicalOAuthResource(value) {
|
|
|
7314
7944
|
}
|
|
7315
7945
|
return trimmed;
|
|
7316
7946
|
} catch {
|
|
7317
|
-
throw new
|
|
7947
|
+
throw new HTTPException8(422, {
|
|
7318
7948
|
message: "MCP protected resource metadata advertised an invalid resource"
|
|
7319
7949
|
});
|
|
7320
7950
|
}
|
|
@@ -7327,18 +7957,18 @@ function oauthEndpointUrl(rawUrl, settings, label) {
|
|
|
7327
7957
|
});
|
|
7328
7958
|
} catch (error) {
|
|
7329
7959
|
if (error instanceof DestinationPolicyError) {
|
|
7330
|
-
throw new
|
|
7960
|
+
throw new HTTPException8(422, { message: error.message });
|
|
7331
7961
|
}
|
|
7332
7962
|
throw error;
|
|
7333
7963
|
}
|
|
7334
7964
|
}
|
|
7335
7965
|
function safeReturnPath(value) {
|
|
7336
7966
|
if (!value.startsWith("/") || value.startsWith("//")) {
|
|
7337
|
-
throw new
|
|
7967
|
+
throw new HTTPException8(400, { message: "OAuth returnPath must be a relative path" });
|
|
7338
7968
|
}
|
|
7339
7969
|
const parsed = new URL(value, "https://opengeni.local");
|
|
7340
7970
|
if (parsed.origin !== "https://opengeni.local") {
|
|
7341
|
-
throw new
|
|
7971
|
+
throw new HTTPException8(400, { message: "OAuth returnPath must be a relative path" });
|
|
7342
7972
|
}
|
|
7343
7973
|
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
7344
7974
|
}
|
|
@@ -7348,7 +7978,7 @@ async function fetchJsonObject(url, settings) {
|
|
|
7348
7978
|
await cancelResponseBody(response);
|
|
7349
7979
|
throw new Error(`HTTP ${response.status}`);
|
|
7350
7980
|
}
|
|
7351
|
-
const payload = await
|
|
7981
|
+
const payload = await readResponseJsonBounded2(
|
|
7352
7982
|
response,
|
|
7353
7983
|
OAUTH_MAX_RESPONSE_BYTES,
|
|
7354
7984
|
"OAuth metadata response"
|
|
@@ -7368,7 +7998,7 @@ async function fetchOAuth(rawUrl, settings, init = {}, hop = 0) {
|
|
|
7368
7998
|
});
|
|
7369
7999
|
} catch (error) {
|
|
7370
8000
|
if (error instanceof DestinationPolicyError) {
|
|
7371
|
-
throw new
|
|
8001
|
+
throw new HTTPException8(422, { message: error.message });
|
|
7372
8002
|
}
|
|
7373
8003
|
throw error;
|
|
7374
8004
|
}
|
|
@@ -7377,25 +8007,25 @@ async function fetchOAuth(rawUrl, settings, init = {}, hop = 0) {
|
|
|
7377
8007
|
}
|
|
7378
8008
|
if (!oauthRequestMayFollowRedirect(init)) {
|
|
7379
8009
|
await cancelResponseBody(response);
|
|
7380
|
-
throw new
|
|
8010
|
+
throw new HTTPException8(422, {
|
|
7381
8011
|
message: "OAuth credential-bearing requests may not follow redirects"
|
|
7382
8012
|
});
|
|
7383
8013
|
}
|
|
7384
8014
|
if (hop >= 3) {
|
|
7385
8015
|
await cancelResponseBody(response);
|
|
7386
|
-
throw new
|
|
8016
|
+
throw new HTTPException8(422, { message: "OAuth fetch exceeded maximum redirect hops" });
|
|
7387
8017
|
}
|
|
7388
8018
|
const location = response.headers.get("location");
|
|
7389
8019
|
if (!location) {
|
|
7390
8020
|
await cancelResponseBody(response);
|
|
7391
|
-
throw new
|
|
8021
|
+
throw new HTTPException8(422, { message: "OAuth fetch redirect was missing Location" });
|
|
7392
8022
|
}
|
|
7393
8023
|
let nextUrl;
|
|
7394
8024
|
try {
|
|
7395
8025
|
nextUrl = new URL(location, rawUrl).toString();
|
|
7396
8026
|
} catch {
|
|
7397
8027
|
await cancelResponseBody(response);
|
|
7398
|
-
throw new
|
|
8028
|
+
throw new HTTPException8(422, { message: "OAuth fetch redirect Location was invalid" });
|
|
7399
8029
|
}
|
|
7400
8030
|
await cancelResponseBody(response);
|
|
7401
8031
|
return await fetchOAuth(nextUrl, settings, init, hop + 1);
|
|
@@ -7467,7 +8097,7 @@ function registrationMethod(value) {
|
|
|
7467
8097
|
if (value === "operator" || value === "manual" || value === "cimd" || value === "dcr") {
|
|
7468
8098
|
return value;
|
|
7469
8099
|
}
|
|
7470
|
-
throw new
|
|
8100
|
+
throw new HTTPException8(400, { message: "invalid OAuth state" });
|
|
7471
8101
|
}
|
|
7472
8102
|
function expiresAtFromTokenResponse(payload) {
|
|
7473
8103
|
const expiresAt = stringValue(payload.expires_at);
|
|
@@ -7502,17 +8132,21 @@ function numberValue(value) {
|
|
|
7502
8132
|
function requiredString(value, field) {
|
|
7503
8133
|
const result = stringValue(value);
|
|
7504
8134
|
if (!result) {
|
|
7505
|
-
throw new
|
|
8135
|
+
throw new HTTPException8(400, { message: `invalid OAuth state: missing ${field}` });
|
|
7506
8136
|
}
|
|
7507
8137
|
return result;
|
|
7508
8138
|
}
|
|
7509
8139
|
|
|
7510
8140
|
// src/routes/connections.ts
|
|
8141
|
+
import {
|
|
8142
|
+
OPENGENI_SLACK_BOT_CREDENTIAL_LABEL as OPENGENI_SLACK_BOT_CREDENTIAL_LABEL2,
|
|
8143
|
+
OPENGENI_SLACK_BOT_CREDENTIAL_ROLE as OPENGENI_SLACK_BOT_CREDENTIAL_ROLE2
|
|
8144
|
+
} from "@opengeni/contracts";
|
|
7511
8145
|
function registerConnectionRoutes(app, deps) {
|
|
7512
8146
|
const { db, settings, observability } = deps;
|
|
7513
8147
|
function assertIntegrationsEnabled() {
|
|
7514
8148
|
if (!settings.integrationsEnabled) {
|
|
7515
|
-
throw new
|
|
8149
|
+
throw new HTTPException9(404, { message: "integrations are not enabled for this deployment" });
|
|
7516
8150
|
}
|
|
7517
8151
|
}
|
|
7518
8152
|
app.get("/v1/workspaces/:workspaceId/connections", async (c) => {
|
|
@@ -7528,6 +8162,7 @@ function registerConnectionRoutes(app, deps) {
|
|
|
7528
8162
|
const workspaceId = c.req.param("workspaceId");
|
|
7529
8163
|
const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:write");
|
|
7530
8164
|
const payload = CreateConnectionRequest.parse(await c.req.json());
|
|
8165
|
+
assertNotReservedSlackBotMetadata(payload.metadata);
|
|
7531
8166
|
const key = requireEnvironmentEncryption2(settings);
|
|
7532
8167
|
const subjectId = writableSubjectId(payload.subjectId, grant.subjectId);
|
|
7533
8168
|
const connection = await createConnection2(db, {
|
|
@@ -7544,6 +8179,92 @@ function registerConnectionRoutes(app, deps) {
|
|
|
7544
8179
|
});
|
|
7545
8180
|
return c.json(ConnectionResponse.parse({ connection }), 201);
|
|
7546
8181
|
});
|
|
8182
|
+
app.post("/v1/workspaces/:workspaceId/connections/slack-bot", async (c) => {
|
|
8183
|
+
const workspaceId = c.req.param("workspaceId");
|
|
8184
|
+
const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:write");
|
|
8185
|
+
const payload = ConnectOpenGeniSlackBotRequest.parse(await c.req.json());
|
|
8186
|
+
const verified = await verifyOpenGeniSlackBotCredential(
|
|
8187
|
+
payload.token,
|
|
8188
|
+
deps.slackFetch ?? fetch
|
|
8189
|
+
);
|
|
8190
|
+
const key = requireEnvironmentEncryption2(settings);
|
|
8191
|
+
const credentialEncrypted = encryptCredentialBundle(
|
|
8192
|
+
key,
|
|
8193
|
+
slackBotCredentialBundle(payload.token)
|
|
8194
|
+
);
|
|
8195
|
+
const existing = payload.connectionId ? await getConnectionMetadata2(db, workspaceId, payload.connectionId, grant.subjectId) : null;
|
|
8196
|
+
if (payload.connectionId && !existing) {
|
|
8197
|
+
throw new HTTPException9(404, { message: "connection not found" });
|
|
8198
|
+
}
|
|
8199
|
+
if (existing && !isOpenGeniSlackBotConnection(existing)) {
|
|
8200
|
+
throw new HTTPException9(422, {
|
|
8201
|
+
message: "connectionId is not an OpenGeni Slack bot connection"
|
|
8202
|
+
});
|
|
8203
|
+
}
|
|
8204
|
+
const existingMetadata = existing ? openGeniSlackBotMetadata2(existing.metadata) : null;
|
|
8205
|
+
if (existingMetadata && existingMetadata.slackTeamId !== verified.metadata.slackTeamId) {
|
|
8206
|
+
throw new HTTPException9(409, {
|
|
8207
|
+
message: "a Slack bot connection can only be reinstalled for its original Slack workspace"
|
|
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, {
|
|
8233
|
+
accountId: grant.accountId,
|
|
8234
|
+
workspaceId,
|
|
8235
|
+
subjectId: null,
|
|
8236
|
+
providerDomain: "slack.com",
|
|
8237
|
+
kind: "app_install",
|
|
8238
|
+
credentialEncrypted,
|
|
8239
|
+
grantedScopes: verified.grantedScopes,
|
|
8240
|
+
expiresAt: null,
|
|
8241
|
+
verifiedInstallAt,
|
|
8242
|
+
verifiedInstallVersion: 1,
|
|
8243
|
+
metadata: verified.metadata,
|
|
8244
|
+
createdBySubjectId: grant.subjectId
|
|
8245
|
+
});
|
|
8246
|
+
if (!connection) {
|
|
8247
|
+
throw new HTTPException9(409, {
|
|
8248
|
+
message: "Slack bot connection changed during reinstall; retry with the current connection"
|
|
8249
|
+
});
|
|
8250
|
+
}
|
|
8251
|
+
await recordAuditEvent2(db, {
|
|
8252
|
+
accountId: grant.accountId,
|
|
8253
|
+
workspaceId,
|
|
8254
|
+
subjectId: grant.subjectId,
|
|
8255
|
+
action: existing ? "slack_bot.reinstalled" : "slack_bot.connected",
|
|
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"
|
|
8264
|
+
}
|
|
8265
|
+
});
|
|
8266
|
+
return c.json(ConnectionResponse.parse({ connection }), existing ? 200 : 201);
|
|
8267
|
+
});
|
|
7547
8268
|
app.get("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
|
|
7548
8269
|
const workspaceId = c.req.param("workspaceId");
|
|
7549
8270
|
const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:read");
|
|
@@ -7554,7 +8275,7 @@ function registerConnectionRoutes(app, deps) {
|
|
|
7554
8275
|
grant.subjectId
|
|
7555
8276
|
);
|
|
7556
8277
|
if (!connection) {
|
|
7557
|
-
throw new
|
|
8278
|
+
throw new HTTPException9(404, { message: "connection not found" });
|
|
7558
8279
|
}
|
|
7559
8280
|
return c.json(ConnectionResponse.parse({ connection }));
|
|
7560
8281
|
});
|
|
@@ -7562,14 +8283,26 @@ function registerConnectionRoutes(app, deps) {
|
|
|
7562
8283
|
const workspaceId = c.req.param("workspaceId");
|
|
7563
8284
|
const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:write");
|
|
7564
8285
|
const payload = UpdateConnectionRequest.parse(await c.req.json());
|
|
8286
|
+
assertNotReservedSlackBotMetadata(payload.metadata);
|
|
8287
|
+
const existing = await getConnectionMetadata2(
|
|
8288
|
+
db,
|
|
8289
|
+
workspaceId,
|
|
8290
|
+
c.req.param("connectionId"),
|
|
8291
|
+
grant.subjectId
|
|
8292
|
+
);
|
|
8293
|
+
if (existing && isOpenGeniSlackBotConnection(existing)) {
|
|
8294
|
+
throw new HTTPException9(422, {
|
|
8295
|
+
message: "use the dedicated OpenGeni Slack bot reinstall flow to update this connection"
|
|
8296
|
+
});
|
|
8297
|
+
}
|
|
7565
8298
|
if (payload.status !== void 0) {
|
|
7566
8299
|
if (payload.status !== "active") {
|
|
7567
|
-
throw new
|
|
8300
|
+
throw new HTTPException9(400, {
|
|
7568
8301
|
message: 'status can only be set to "active"; use DELETE to revoke'
|
|
7569
8302
|
});
|
|
7570
8303
|
}
|
|
7571
8304
|
if (payload.credential === void 0) {
|
|
7572
|
-
throw new
|
|
8305
|
+
throw new HTTPException9(400, {
|
|
7573
8306
|
message: "reactivating a connection requires a new credential"
|
|
7574
8307
|
});
|
|
7575
8308
|
}
|
|
@@ -7591,7 +8324,7 @@ function registerConnectionRoutes(app, deps) {
|
|
|
7591
8324
|
...payload.metadata !== void 0 ? { metadata: payload.metadata } : {}
|
|
7592
8325
|
});
|
|
7593
8326
|
if (!connection) {
|
|
7594
|
-
throw new
|
|
8327
|
+
throw new HTTPException9(404, { message: "connection not found" });
|
|
7595
8328
|
}
|
|
7596
8329
|
return c.json(ConnectionResponse.parse({ connection }));
|
|
7597
8330
|
});
|
|
@@ -7605,7 +8338,25 @@ function registerConnectionRoutes(app, deps) {
|
|
|
7605
8338
|
grant.subjectId
|
|
7606
8339
|
);
|
|
7607
8340
|
if (!connection) {
|
|
7608
|
-
throw new
|
|
8341
|
+
throw new HTTPException9(404, { message: "connection not found" });
|
|
8342
|
+
}
|
|
8343
|
+
if (isOpenGeniSlackBotConnection(connection)) {
|
|
8344
|
+
const metadata = openGeniSlackBotMetadata2(connection.metadata);
|
|
8345
|
+
await recordAuditEvent2(db, {
|
|
8346
|
+
accountId: grant.accountId,
|
|
8347
|
+
workspaceId,
|
|
8348
|
+
subjectId: grant.subjectId,
|
|
8349
|
+
action: "slack_bot.disconnected",
|
|
8350
|
+
targetType: "connection",
|
|
8351
|
+
targetId: connection.id,
|
|
8352
|
+
metadata: {
|
|
8353
|
+
credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE2,
|
|
8354
|
+
credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL2,
|
|
8355
|
+
connectionId: connection.id,
|
|
8356
|
+
slackTeamId: metadata.slackTeamId,
|
|
8357
|
+
outcome: "succeeded"
|
|
8358
|
+
}
|
|
8359
|
+
});
|
|
7609
8360
|
}
|
|
7610
8361
|
return c.json(ConnectionResponse.parse({ connection }));
|
|
7611
8362
|
});
|
|
@@ -7615,7 +8366,7 @@ function registerConnectionRoutes(app, deps) {
|
|
|
7615
8366
|
const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:write");
|
|
7616
8367
|
const parsed = OAuthStartRequest.safeParse(await c.req.json());
|
|
7617
8368
|
if (!parsed.success) {
|
|
7618
|
-
throw new
|
|
8369
|
+
throw new HTTPException9(400, {
|
|
7619
8370
|
message: parsed.error.issues[0]?.message ?? "invalid OAuth start request"
|
|
7620
8371
|
});
|
|
7621
8372
|
}
|
|
@@ -7659,18 +8410,30 @@ function registerConnectionRoutes(app, deps) {
|
|
|
7659
8410
|
);
|
|
7660
8411
|
});
|
|
7661
8412
|
}
|
|
8413
|
+
function assertNotReservedSlackBotMetadata(metadata) {
|
|
8414
|
+
if (hasReservedOpenGeniSlackBotMetadata(metadata)) {
|
|
8415
|
+
throw new HTTPException9(422, {
|
|
8416
|
+
message: "OpenGeni Slack bot metadata is reserved for the dedicated connection flow"
|
|
8417
|
+
});
|
|
8418
|
+
}
|
|
8419
|
+
}
|
|
7662
8420
|
function writableSubjectId(requested, grantSubjectId) {
|
|
7663
8421
|
if (requested == null) {
|
|
7664
8422
|
return null;
|
|
7665
8423
|
}
|
|
7666
8424
|
if (requested !== grantSubjectId) {
|
|
7667
|
-
throw new
|
|
8425
|
+
throw new HTTPException9(403, { message: "cannot write a connection for another subject" });
|
|
7668
8426
|
}
|
|
7669
8427
|
return requested;
|
|
7670
8428
|
}
|
|
7671
8429
|
function encryptCredentialBundle(key, credential) {
|
|
7672
8430
|
return encryptEnvironmentValue3(key, JSON.stringify(credential));
|
|
7673
8431
|
}
|
|
8432
|
+
function slackBotCredentialBundle(token) {
|
|
8433
|
+
const headerName = ["author", "ization"].join("");
|
|
8434
|
+
const scheme = ["Bear", "er"].join("");
|
|
8435
|
+
return { headers: { [headerName]: `${scheme} ${token}` } };
|
|
8436
|
+
}
|
|
7674
8437
|
|
|
7675
8438
|
// src/routes/documents.ts
|
|
7676
8439
|
import {
|
|
@@ -7712,7 +8475,7 @@ import {
|
|
|
7712
8475
|
searchDocuments as searchDocuments2
|
|
7713
8476
|
} from "@opengeni/documents";
|
|
7714
8477
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
7715
|
-
import { HTTPException as
|
|
8478
|
+
import { HTTPException as HTTPException11 } from "hono/http-exception";
|
|
7716
8479
|
import { requireAccessGrant as requireAccessGrant5 } from "@opengeni/core";
|
|
7717
8480
|
import { recordWorkspaceUsage as recordWorkspaceUsage3, requireLimit as requireLimit3 } from "@opengeni/core";
|
|
7718
8481
|
|
|
@@ -7939,7 +8702,7 @@ import {
|
|
|
7939
8702
|
getRetainedFileArtifact,
|
|
7940
8703
|
requireFile as requireFile2
|
|
7941
8704
|
} from "@opengeni/db";
|
|
7942
|
-
import { HTTPException as
|
|
8705
|
+
import { HTTPException as HTTPException10 } from "hono/http-exception";
|
|
7943
8706
|
import { requireAccessGrant as requireAccessGrant4 } from "@opengeni/core";
|
|
7944
8707
|
import { recordWorkspaceUsage as recordWorkspaceUsage2, requireLimit as requireLimit2 } from "@opengeni/core";
|
|
7945
8708
|
function registerFileRoutes(app, deps) {
|
|
@@ -7948,7 +8711,7 @@ function registerFileRoutes(app, deps) {
|
|
|
7948
8711
|
const workspaceId = c.req.param("workspaceId");
|
|
7949
8712
|
const grant = await requireAccessGrant4(c, deps, workspaceId, "files:upload");
|
|
7950
8713
|
if (!objectStorage) {
|
|
7951
|
-
throw new
|
|
8714
|
+
throw new HTTPException10(503, { message: "object storage is not configured" });
|
|
7952
8715
|
}
|
|
7953
8716
|
const payload = CreateFileUploadRequest.parse(await c.req.json());
|
|
7954
8717
|
await requireLimit2(deps, {
|
|
@@ -7958,7 +8721,7 @@ function registerFileRoutes(app, deps) {
|
|
|
7958
8721
|
quantity: payload.sizeBytes
|
|
7959
8722
|
});
|
|
7960
8723
|
if (payload.sizeBytes > objectStorage.maxSinglePutSizeBytes) {
|
|
7961
|
-
throw new
|
|
8724
|
+
throw new HTTPException10(413, {
|
|
7962
8725
|
message: `file exceeds single PUT limit of ${objectStorage.maxSinglePutSizeBytes} bytes`
|
|
7963
8726
|
});
|
|
7964
8727
|
}
|
|
@@ -7999,11 +8762,11 @@ function registerFileRoutes(app, deps) {
|
|
|
7999
8762
|
const workspaceId = c.req.param("workspaceId");
|
|
8000
8763
|
const grant = await requireAccessGrant4(c, deps, workspaceId, "files:upload");
|
|
8001
8764
|
if (!objectStorage) {
|
|
8002
|
-
throw new
|
|
8765
|
+
throw new HTTPException10(503, { message: "object storage is not configured" });
|
|
8003
8766
|
}
|
|
8004
8767
|
const upload = await getFileUpload(db, workspaceId, c.req.param("uploadId"));
|
|
8005
8768
|
if (!upload) {
|
|
8006
|
-
throw new
|
|
8769
|
+
throw new HTTPException10(404, { message: "file upload not found" });
|
|
8007
8770
|
}
|
|
8008
8771
|
const recordUploadedFileUsage = async (file2) => {
|
|
8009
8772
|
await recordWorkspaceUsage2(deps, {
|
|
@@ -8027,7 +8790,7 @@ function registerFileRoutes(app, deps) {
|
|
|
8027
8790
|
if (current?.status === "completed" && current.file.status === "ready") {
|
|
8028
8791
|
file2 = current.file;
|
|
8029
8792
|
} else if (current && current.status !== "pending") {
|
|
8030
|
-
throw new
|
|
8793
|
+
throw new HTTPException10(409, {
|
|
8031
8794
|
message: `file upload is ${publicFileUploadStatus(current.status)}`
|
|
8032
8795
|
});
|
|
8033
8796
|
} else {
|
|
@@ -8048,7 +8811,7 @@ function registerFileRoutes(app, deps) {
|
|
|
8048
8811
|
return claim.file;
|
|
8049
8812
|
}
|
|
8050
8813
|
if (claim.outcome === "unavailable") {
|
|
8051
|
-
throw new
|
|
8814
|
+
throw new HTTPException10(409, {
|
|
8052
8815
|
message: `file upload is ${publicFileUploadStatus(claim.status)}`
|
|
8053
8816
|
});
|
|
8054
8817
|
}
|
|
@@ -8064,7 +8827,7 @@ function registerFileRoutes(app, deps) {
|
|
|
8064
8827
|
error: error instanceof Error ? error.message : String(error)
|
|
8065
8828
|
}
|
|
8066
8829
|
);
|
|
8067
|
-
throw new
|
|
8830
|
+
throw new HTTPException10(status, { message });
|
|
8068
8831
|
}
|
|
8069
8832
|
const settled = await completeFileUploadCleanup(db, {
|
|
8070
8833
|
accountId: grant.accountId,
|
|
@@ -8074,16 +8837,16 @@ function registerFileRoutes(app, deps) {
|
|
|
8074
8837
|
terminalStatus
|
|
8075
8838
|
});
|
|
8076
8839
|
if (!settled) {
|
|
8077
|
-
throw new
|
|
8840
|
+
throw new HTTPException10(409, { message: "file upload cleanup claim was superseded" });
|
|
8078
8841
|
}
|
|
8079
|
-
throw new
|
|
8842
|
+
throw new HTTPException10(status, { message });
|
|
8080
8843
|
};
|
|
8081
8844
|
if (upload.status === "completed" && upload.file.status === "ready") {
|
|
8082
8845
|
const file2 = await completeAndRecordUsage();
|
|
8083
8846
|
return c.json(CompleteFileUploadResponse.parse({ file: file2 }));
|
|
8084
8847
|
}
|
|
8085
8848
|
if (upload.status !== "pending") {
|
|
8086
|
-
throw new
|
|
8849
|
+
throw new HTTPException10(409, {
|
|
8087
8850
|
message: `file upload is ${publicFileUploadStatus(upload.status)}`
|
|
8088
8851
|
});
|
|
8089
8852
|
}
|
|
@@ -8092,7 +8855,7 @@ function registerFileRoutes(app, deps) {
|
|
|
8092
8855
|
return c.json(CompleteFileUploadResponse.parse({ file: file2 }));
|
|
8093
8856
|
}
|
|
8094
8857
|
const head = await objectStorage.headFile(upload.file).catch((error) => {
|
|
8095
|
-
throw new
|
|
8858
|
+
throw new HTTPException10(409, {
|
|
8096
8859
|
message: `uploaded object is not available: ${error instanceof Error ? error.message : String(error)}`
|
|
8097
8860
|
});
|
|
8098
8861
|
});
|
|
@@ -8128,7 +8891,7 @@ function registerFileRoutes(app, deps) {
|
|
|
8128
8891
|
await requireAccessGrant4(c, deps, workspaceId, "files:read");
|
|
8129
8892
|
const file = await requireFile2(db, workspaceId, c.req.param("fileId")).catch(() => null);
|
|
8130
8893
|
if (!file) {
|
|
8131
|
-
throw new
|
|
8894
|
+
throw new HTTPException10(404, { message: "file not found" });
|
|
8132
8895
|
}
|
|
8133
8896
|
return c.json(FileAsset.parse(file));
|
|
8134
8897
|
});
|
|
@@ -8206,7 +8969,7 @@ function registerFileRoutes(app, deps) {
|
|
|
8206
8969
|
return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
|
|
8207
8970
|
}
|
|
8208
8971
|
if (bytes.byteLength !== range.length) {
|
|
8209
|
-
throw new
|
|
8972
|
+
throw new HTTPException10(502, { message: "object storage returned an invalid byte range" });
|
|
8210
8973
|
}
|
|
8211
8974
|
return c.body(new Uint8Array(bytes), range.status, headers);
|
|
8212
8975
|
});
|
|
@@ -8214,14 +8977,14 @@ function registerFileRoutes(app, deps) {
|
|
|
8214
8977
|
const workspaceId = c.req.param("workspaceId");
|
|
8215
8978
|
await requireAccessGrant4(c, deps, workspaceId, "files:read");
|
|
8216
8979
|
if (!objectStorage) {
|
|
8217
|
-
throw new
|
|
8980
|
+
throw new HTTPException10(503, { message: "object storage is not configured" });
|
|
8218
8981
|
}
|
|
8219
8982
|
const file = await requireFile2(db, workspaceId, c.req.param("fileId")).catch(() => null);
|
|
8220
8983
|
if (!file) {
|
|
8221
|
-
throw new
|
|
8984
|
+
throw new HTTPException10(404, { message: "file not found" });
|
|
8222
8985
|
}
|
|
8223
8986
|
if (file.status !== "ready") {
|
|
8224
|
-
throw new
|
|
8987
|
+
throw new HTTPException10(409, { message: `file is ${file.status}` });
|
|
8225
8988
|
}
|
|
8226
8989
|
const signed = await objectStorage.createGetUrl({ key: file.objectKey });
|
|
8227
8990
|
return c.json(
|
|
@@ -8243,7 +9006,7 @@ function publicFileUploadStatus(status) {
|
|
|
8243
9006
|
function retainedArtifactId(value) {
|
|
8244
9007
|
const parsed = FileAsset.shape.id.safeParse(value);
|
|
8245
9008
|
if (!parsed.success) {
|
|
8246
|
-
throw new
|
|
9009
|
+
throw new HTTPException10(404, { message: "artifact not found" });
|
|
8247
9010
|
}
|
|
8248
9011
|
return parsed.data;
|
|
8249
9012
|
}
|
|
@@ -8311,7 +9074,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
8311
9074
|
await requireAccessGrant5(c, deps, workspaceId, "documents:search");
|
|
8312
9075
|
const base = await getDocumentBase(db, workspaceId, c.req.param("baseId"));
|
|
8313
9076
|
if (!base) {
|
|
8314
|
-
throw new
|
|
9077
|
+
throw new HTTPException11(404, { message: "document base not found" });
|
|
8315
9078
|
}
|
|
8316
9079
|
return c.json(DocumentBase.parse(base));
|
|
8317
9080
|
});
|
|
@@ -8319,7 +9082,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
8319
9082
|
const workspaceId = c.req.param("workspaceId");
|
|
8320
9083
|
const grant = await requireAccessGrant5(c, deps, workspaceId, "documents:manage");
|
|
8321
9084
|
if (!objectStorage) {
|
|
8322
|
-
throw new
|
|
9085
|
+
throw new HTTPException11(503, { message: "object storage is not configured" });
|
|
8323
9086
|
}
|
|
8324
9087
|
await requireLimit3(deps, {
|
|
8325
9088
|
accountId: grant.accountId,
|
|
@@ -8385,7 +9148,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
8385
9148
|
});
|
|
8386
9149
|
return c.body(null, 204);
|
|
8387
9150
|
} catch (error) {
|
|
8388
|
-
if (error instanceof
|
|
9151
|
+
if (error instanceof HTTPException11) {
|
|
8389
9152
|
throw error;
|
|
8390
9153
|
}
|
|
8391
9154
|
throw documentHttpException(error);
|
|
@@ -8398,7 +9161,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
8398
9161
|
const workspaceId = c.req.param("workspaceId");
|
|
8399
9162
|
const grant = await requireAccessGrant5(c, deps, workspaceId, "documents:manage");
|
|
8400
9163
|
if (!objectStorage) {
|
|
8401
|
-
throw new
|
|
9164
|
+
throw new HTTPException11(503, { message: "object storage is not configured" });
|
|
8402
9165
|
}
|
|
8403
9166
|
await requireLimit3(deps, {
|
|
8404
9167
|
accountId: grant.accountId,
|
|
@@ -8411,13 +9174,13 @@ function registerDocumentRoutes(app, deps) {
|
|
|
8411
9174
|
viewerSubjectId: grant.subjectId
|
|
8412
9175
|
});
|
|
8413
9176
|
if (!document) {
|
|
8414
|
-
throw new
|
|
9177
|
+
throw new HTTPException11(404, { message: "document not found" });
|
|
8415
9178
|
}
|
|
8416
9179
|
if (document.status !== "failed") {
|
|
8417
|
-
throw new
|
|
9180
|
+
throw new HTTPException11(422, { message: "only failed documents can be retried" });
|
|
8418
9181
|
}
|
|
8419
9182
|
if (document.baseId !== c.req.param("baseId")) {
|
|
8420
|
-
throw new
|
|
9183
|
+
throw new HTTPException11(404, { message: "document not found" });
|
|
8421
9184
|
}
|
|
8422
9185
|
const queued = await queueDocumentForReindex(db, workspaceId, document.id, {
|
|
8423
9186
|
viewerSubjectId: grant.subjectId
|
|
@@ -8442,7 +9205,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
8442
9205
|
}
|
|
8443
9206
|
return c.json(Document.parse(indexed));
|
|
8444
9207
|
} catch (error) {
|
|
8445
|
-
if (error instanceof
|
|
9208
|
+
if (error instanceof HTTPException11) {
|
|
8446
9209
|
throw error;
|
|
8447
9210
|
}
|
|
8448
9211
|
throw documentHttpException(error);
|
|
@@ -8455,7 +9218,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
8455
9218
|
const payload = DocumentSearchRequest.parse(await c.req.json());
|
|
8456
9219
|
const base = await getDocumentBase(db, workspaceId, c.req.param("baseId"));
|
|
8457
9220
|
if (!base) {
|
|
8458
|
-
throw new
|
|
9221
|
+
throw new HTTPException11(404, { message: "document base not found" });
|
|
8459
9222
|
}
|
|
8460
9223
|
return c.json({
|
|
8461
9224
|
results: await searchDocuments2(
|
|
@@ -8499,7 +9262,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
8499
9262
|
const workspaceId = c.req.param("workspaceId");
|
|
8500
9263
|
const grant = await requireAccessGrant5(c, deps, workspaceId, "documents:manage");
|
|
8501
9264
|
if (!objectStorage) {
|
|
8502
|
-
throw new
|
|
9265
|
+
throw new HTTPException11(503, { message: "object storage is not configured" });
|
|
8503
9266
|
}
|
|
8504
9267
|
await requireLimit3(deps, {
|
|
8505
9268
|
accountId: grant.accountId,
|
|
@@ -8519,7 +9282,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
8519
9282
|
quantity: bytes.length
|
|
8520
9283
|
});
|
|
8521
9284
|
if (bytes.length > objectStorage.maxSinglePutSizeBytes) {
|
|
8522
|
-
throw new
|
|
9285
|
+
throw new HTTPException11(413, {
|
|
8523
9286
|
message: `drop exceeds single PUT limit of ${objectStorage.maxSinglePutSizeBytes} bytes`
|
|
8524
9287
|
});
|
|
8525
9288
|
}
|
|
@@ -8598,7 +9361,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
8598
9361
|
}
|
|
8599
9362
|
return c.json(Document.parse(indexed), wasCreated ? 201 : 200);
|
|
8600
9363
|
} catch (error) {
|
|
8601
|
-
if (error instanceof
|
|
9364
|
+
if (error instanceof HTTPException11) {
|
|
8602
9365
|
throw error;
|
|
8603
9366
|
}
|
|
8604
9367
|
throw documentHttpException(error);
|
|
@@ -8613,7 +9376,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
8613
9376
|
viewerSubjectId: grant.subjectId
|
|
8614
9377
|
});
|
|
8615
9378
|
if (!document) {
|
|
8616
|
-
throw new
|
|
9379
|
+
throw new HTTPException11(404, { message: "document not found" });
|
|
8617
9380
|
}
|
|
8618
9381
|
return c.json(
|
|
8619
9382
|
Document.parse(
|
|
@@ -8627,7 +9390,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
8627
9390
|
)
|
|
8628
9391
|
);
|
|
8629
9392
|
} catch (error) {
|
|
8630
|
-
if (error instanceof
|
|
9393
|
+
if (error instanceof HTTPException11) {
|
|
8631
9394
|
throw error;
|
|
8632
9395
|
}
|
|
8633
9396
|
throw documentHttpException(error);
|
|
@@ -8644,7 +9407,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
8644
9407
|
limit: c.req.query("limit") ? Number(c.req.query("limit")) : void 0
|
|
8645
9408
|
});
|
|
8646
9409
|
if (!parsed.success) {
|
|
8647
|
-
throw new
|
|
9410
|
+
throw new HTTPException11(400, { message: "invalid knowledge memory query parameters" });
|
|
8648
9411
|
}
|
|
8649
9412
|
return c.json(
|
|
8650
9413
|
(await listKnowledgeMemories2(db, workspaceId, parsed.data)).map(
|
|
@@ -8657,7 +9420,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
8657
9420
|
await requireAccessGrant5(c, deps, workspaceId, "documents:search");
|
|
8658
9421
|
const memory = await getKnowledgeMemory(db, workspaceId, c.req.param("memoryId"));
|
|
8659
9422
|
if (!memory) {
|
|
8660
|
-
throw new
|
|
9423
|
+
throw new HTTPException11(404, { message: "knowledge memory not found" });
|
|
8661
9424
|
}
|
|
8662
9425
|
return c.json(KnowledgeMemory.parse(memory));
|
|
8663
9426
|
});
|
|
@@ -8666,7 +9429,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
8666
9429
|
await requireAccessGrant5(c, deps, workspaceId, "documents:search");
|
|
8667
9430
|
const parsed = WorkspaceMemorySearchRequest.safeParse(await c.req.json());
|
|
8668
9431
|
if (!parsed.success) {
|
|
8669
|
-
throw new
|
|
9432
|
+
throw new HTTPException11(400, { message: "invalid workspace memory search request" });
|
|
8670
9433
|
}
|
|
8671
9434
|
const results = await searchWorkspaceMemories2(
|
|
8672
9435
|
db,
|
|
@@ -8688,7 +9451,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
8688
9451
|
const grant = await requireAccessGrant5(c, deps, workspaceId, "documents:manage");
|
|
8689
9452
|
const parsedBody = CreateKnowledgeMemoryRequest.safeParse(await c.req.json());
|
|
8690
9453
|
if (!parsedBody.success) {
|
|
8691
|
-
throw new
|
|
9454
|
+
throw new HTTPException11(400, { message: "invalid knowledge memory request" });
|
|
8692
9455
|
}
|
|
8693
9456
|
const payload = parsedBody.data;
|
|
8694
9457
|
if (payload.status === "active") {
|
|
@@ -8771,21 +9534,21 @@ function dropFilename(preferred) {
|
|
|
8771
9534
|
function documentHttpException(error) {
|
|
8772
9535
|
const message = error instanceof Error ? error.message : String(error);
|
|
8773
9536
|
if (message.includes("not found")) {
|
|
8774
|
-
return new
|
|
9537
|
+
return new HTTPException11(404, { message });
|
|
8775
9538
|
}
|
|
8776
9539
|
if (message.includes("already exists")) {
|
|
8777
|
-
return new
|
|
9540
|
+
return new HTTPException11(409, { message });
|
|
8778
9541
|
}
|
|
8779
9542
|
if (message.includes("no suggested base")) {
|
|
8780
|
-
return new
|
|
9543
|
+
return new HTTPException11(422, { message });
|
|
8781
9544
|
}
|
|
8782
9545
|
if (message.includes("pending") || message.includes("failed") || message.includes("deleted")) {
|
|
8783
|
-
return new
|
|
9546
|
+
return new HTTPException11(422, { message });
|
|
8784
9547
|
}
|
|
8785
9548
|
if (message.includes("too long") || message.includes("visible memory is full") || message.includes("empty after sanitization") || message.includes("does not match") || message.includes("Ambiguous memory id")) {
|
|
8786
|
-
return new
|
|
9549
|
+
return new HTTPException11(400, { message });
|
|
8787
9550
|
}
|
|
8788
|
-
return new
|
|
9551
|
+
return new HTTPException11(500, { message });
|
|
8789
9552
|
}
|
|
8790
9553
|
|
|
8791
9554
|
// src/routes/enrollments.ts
|
|
@@ -8807,7 +9570,7 @@ import {
|
|
|
8807
9570
|
RevokeEnrollmentResponse
|
|
8808
9571
|
} from "@opengeni/contracts";
|
|
8809
9572
|
import { getWorkspace, listEnrollments, revokeEnrollment } from "@opengeni/db";
|
|
8810
|
-
import { HTTPException as
|
|
9573
|
+
import { HTTPException as HTTPException12 } from "hono/http-exception";
|
|
8811
9574
|
import { requireAccessGrant as requireAccessGrant6 } from "@opengeni/core";
|
|
8812
9575
|
|
|
8813
9576
|
// src/sandbox/enrollment.ts
|
|
@@ -9099,7 +9862,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
9099
9862
|
const { settings, db } = deps;
|
|
9100
9863
|
function assertSelfhostedEnabled() {
|
|
9101
9864
|
if (!settings.sandboxSelfhostedEnabled) {
|
|
9102
|
-
throw new
|
|
9865
|
+
throw new HTTPException12(404, {
|
|
9103
9866
|
message: "selfhosted enrollment is not enabled for this deployment"
|
|
9104
9867
|
});
|
|
9105
9868
|
}
|
|
@@ -9111,7 +9874,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
9111
9874
|
function rateLimit(c, limiter) {
|
|
9112
9875
|
const ip = clientIp(c);
|
|
9113
9876
|
if (!limiter.take(ip)) {
|
|
9114
|
-
throw new
|
|
9877
|
+
throw new HTTPException12(429, { message: "too many requests; slow down" });
|
|
9115
9878
|
}
|
|
9116
9879
|
}
|
|
9117
9880
|
app.post("/v1/enrollments/device/start", async (c) => {
|
|
@@ -9119,12 +9882,12 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
9119
9882
|
rateLimit(c, startLimiter);
|
|
9120
9883
|
const parsed = DeviceEnrollmentStartRequest.safeParse(await c.req.json().catch(() => null));
|
|
9121
9884
|
if (!parsed.success) {
|
|
9122
|
-
throw new
|
|
9885
|
+
throw new HTTPException12(400, { message: "invalid device-start request" });
|
|
9123
9886
|
}
|
|
9124
9887
|
const body = parsed.data;
|
|
9125
9888
|
const workspace = await getWorkspace(db, body.workspaceId);
|
|
9126
9889
|
if (!workspace) {
|
|
9127
|
-
throw new
|
|
9890
|
+
throw new HTTPException12(404, { message: "workspace not found" });
|
|
9128
9891
|
}
|
|
9129
9892
|
const result = await startDeviceEnrollment(
|
|
9130
9893
|
{ db, settings },
|
|
@@ -9148,7 +9911,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
9148
9911
|
rateLimit(c, pollLimiter);
|
|
9149
9912
|
const parsed = DeviceEnrollmentPollRequest.safeParse(await c.req.json().catch(() => null));
|
|
9150
9913
|
if (!parsed.success) {
|
|
9151
|
-
throw new
|
|
9914
|
+
throw new HTTPException12(400, { message: "invalid device-poll request" });
|
|
9152
9915
|
}
|
|
9153
9916
|
const result = await pollDeviceEnrollment(
|
|
9154
9917
|
{ db, settings },
|
|
@@ -9161,19 +9924,19 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
9161
9924
|
rateLimit(c, lookupLimiter);
|
|
9162
9925
|
const parsed = DeviceEnrollmentLookupRequest.safeParse(await c.req.json().catch(() => null));
|
|
9163
9926
|
if (!parsed.success) {
|
|
9164
|
-
throw new
|
|
9927
|
+
throw new HTTPException12(400, { message: "invalid device-lookup request" });
|
|
9165
9928
|
}
|
|
9166
9929
|
const record3 = await lookupDeviceEnrollment(
|
|
9167
9930
|
{ db, settings },
|
|
9168
9931
|
{ userCode: parsed.data.userCode }
|
|
9169
9932
|
);
|
|
9170
9933
|
if (!record3) {
|
|
9171
|
-
throw new
|
|
9934
|
+
throw new HTTPException12(404, { message: "no pending enrollment for that code" });
|
|
9172
9935
|
}
|
|
9173
9936
|
try {
|
|
9174
9937
|
await requireAccessGrant6(c, deps, record3.workspaceId, "enrollments:read");
|
|
9175
9938
|
} catch {
|
|
9176
|
-
throw new
|
|
9939
|
+
throw new HTTPException12(404, { message: "no pending enrollment for that code" });
|
|
9177
9940
|
}
|
|
9178
9941
|
return c.json(DeviceEnrollmentLookupResponse.parse(toLookupResponse(record3)), 200);
|
|
9179
9942
|
});
|
|
@@ -9182,7 +9945,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
9182
9945
|
rateLimit(c, exchangeLimiter);
|
|
9183
9946
|
const parsed = EnrollTokenExchangeRequest.safeParse(await c.req.json().catch(() => null));
|
|
9184
9947
|
if (!parsed.success) {
|
|
9185
|
-
throw new
|
|
9948
|
+
throw new HTTPException12(400, { message: "invalid enroll-token-exchange request" });
|
|
9186
9949
|
}
|
|
9187
9950
|
const body = parsed.data;
|
|
9188
9951
|
const result = await exchangeEnrollToken(
|
|
@@ -9198,9 +9961,9 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
9198
9961
|
);
|
|
9199
9962
|
if (!result.ok) {
|
|
9200
9963
|
if (result.reason === "disabled") {
|
|
9201
|
-
throw new
|
|
9964
|
+
throw new HTTPException12(503, { message: "enrollment credential plane is not configured" });
|
|
9202
9965
|
}
|
|
9203
|
-
throw new
|
|
9966
|
+
throw new HTTPException12(401, { message: "invalid or expired enroll token" });
|
|
9204
9967
|
}
|
|
9205
9968
|
return c.json(EnrollTokenExchangeResponse.parse({ credentials: result.credentials }), 201);
|
|
9206
9969
|
});
|
|
@@ -9210,7 +9973,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
9210
9973
|
assertSelfhostedEnabled();
|
|
9211
9974
|
const parsed = DeviceEnrollmentApproveRequest.safeParse(await c.req.json().catch(() => null));
|
|
9212
9975
|
if (!parsed.success) {
|
|
9213
|
-
throw new
|
|
9976
|
+
throw new HTTPException12(400, { message: "invalid device-approve request" });
|
|
9214
9977
|
}
|
|
9215
9978
|
const body = parsed.data;
|
|
9216
9979
|
const approved = await approveDeviceEnrollment(
|
|
@@ -9226,7 +9989,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
9226
9989
|
}
|
|
9227
9990
|
);
|
|
9228
9991
|
if (!approved) {
|
|
9229
|
-
throw new
|
|
9992
|
+
throw new HTTPException12(404, { message: "no pending enrollment for that code" });
|
|
9230
9993
|
}
|
|
9231
9994
|
return c.json(
|
|
9232
9995
|
DeviceEnrollmentApproveResponse.parse({
|
|
@@ -9244,7 +10007,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
9244
10007
|
assertSelfhostedEnabled();
|
|
9245
10008
|
const parsed = DeviceEnrollmentDenyRequest.safeParse(await c.req.json().catch(() => null));
|
|
9246
10009
|
if (!parsed.success) {
|
|
9247
|
-
throw new
|
|
10010
|
+
throw new HTTPException12(400, { message: "invalid device-deny request" });
|
|
9248
10011
|
}
|
|
9249
10012
|
const result = await denyDeviceEnrollment(
|
|
9250
10013
|
{ db, settings },
|
|
@@ -9262,7 +10025,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
9262
10025
|
assertSelfhostedEnabled();
|
|
9263
10026
|
const parsed = MintEnrollTokenRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
9264
10027
|
if (!parsed.success) {
|
|
9265
|
-
throw new
|
|
10028
|
+
throw new HTTPException12(400, { message: "invalid mint-enroll-token request" });
|
|
9266
10029
|
}
|
|
9267
10030
|
const minted = await mintEnrollToken(
|
|
9268
10031
|
{ db, settings },
|
|
@@ -9273,7 +10036,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
9273
10036
|
}
|
|
9274
10037
|
);
|
|
9275
10038
|
if (!minted) {
|
|
9276
|
-
throw new
|
|
10039
|
+
throw new HTTPException12(503, { message: "enrollment credential plane is not configured" });
|
|
9277
10040
|
}
|
|
9278
10041
|
return c.json(MintEnrollTokenResponse.parse(minted), 201);
|
|
9279
10042
|
});
|
|
@@ -9362,13 +10125,13 @@ import {
|
|
|
9362
10125
|
SwapActiveSandboxResponse
|
|
9363
10126
|
} from "@opengeni/contracts";
|
|
9364
10127
|
import { getEnrollment as getEnrollment2, readMachineMetricsSeries, requireSession as requireSession3 } from "@opengeni/db";
|
|
9365
|
-
import { HTTPException as
|
|
10128
|
+
import { HTTPException as HTTPException13 } from "hono/http-exception";
|
|
9366
10129
|
import { requireAccessGrant as requireAccessGrant7 } from "@opengeni/core";
|
|
9367
10130
|
import { buildFleetContextForSession as buildFleetContextForSession2, swapActiveSandbox as swapActiveSandbox2 } from "@opengeni/core";
|
|
9368
10131
|
|
|
9369
10132
|
// src/sandbox/machines.ts
|
|
9370
10133
|
import {
|
|
9371
|
-
getSession as
|
|
10134
|
+
getSession as getSession4,
|
|
9372
10135
|
listEnrollments as listEnrollments2,
|
|
9373
10136
|
listSandboxes,
|
|
9374
10137
|
readActiveSandbox,
|
|
@@ -9454,7 +10217,7 @@ async function listMachines(services, input) {
|
|
|
9454
10217
|
let activeEpoch = 0;
|
|
9455
10218
|
let session = null;
|
|
9456
10219
|
if (input.sessionId) {
|
|
9457
|
-
session = await
|
|
10220
|
+
session = await getSession4(db, workspaceId, input.sessionId);
|
|
9458
10221
|
if (session) {
|
|
9459
10222
|
const pointer = await readActiveSandbox(db, workspaceId, input.sessionId);
|
|
9460
10223
|
activeSandboxId = pointer?.activeSandboxId ?? null;
|
|
@@ -9550,7 +10313,7 @@ function registerMachineRoutes(app, deps) {
|
|
|
9550
10313
|
const { settings, db, bus } = deps;
|
|
9551
10314
|
function assertSelfhostedEnabled() {
|
|
9552
10315
|
if (!settings.sandboxSelfhostedEnabled) {
|
|
9553
|
-
throw new
|
|
10316
|
+
throw new HTTPException13(404, {
|
|
9554
10317
|
message: "selfhosted machines are not enabled for this deployment"
|
|
9555
10318
|
});
|
|
9556
10319
|
}
|
|
@@ -9570,7 +10333,7 @@ function registerMachineRoutes(app, deps) {
|
|
|
9570
10333
|
const enrollmentId = c.req.param("enrollmentId");
|
|
9571
10334
|
const enrollment = await getEnrollment2(db, workspaceId, enrollmentId);
|
|
9572
10335
|
if (!enrollment) {
|
|
9573
|
-
throw new
|
|
10336
|
+
throw new HTTPException13(404, { message: "machine not found in this workspace" });
|
|
9574
10337
|
}
|
|
9575
10338
|
const windowMs = SERIES_WINDOWS_MS[c.req.query("window") ?? ""] ?? DEFAULT_SERIES_WINDOW_MS;
|
|
9576
10339
|
const since = new Date(Date.now() - windowMs);
|
|
@@ -9636,7 +10399,7 @@ import {
|
|
|
9636
10399
|
setVariableSetVariable as setVariableSetVariable2,
|
|
9637
10400
|
updateVariableSet
|
|
9638
10401
|
} from "@opengeni/db";
|
|
9639
|
-
import { HTTPException as
|
|
10402
|
+
import { HTTPException as HTTPException14 } from "hono/http-exception";
|
|
9640
10403
|
import { requireAccessGrant as requireAccessGrant8 } from "@opengeni/core";
|
|
9641
10404
|
import {
|
|
9642
10405
|
assertAllowedVariableSetVariableName as assertAllowedVariableSetVariableName2,
|
|
@@ -9665,7 +10428,7 @@ function registerVariableSetRoutes(app, deps) {
|
|
|
9665
10428
|
const payload = CreateVariableSetRequest.parse(await c.req.json());
|
|
9666
10429
|
const name = trimmedVariableSetName(payload.name);
|
|
9667
10430
|
if (payload.variables.length > MAX_VARIABLES_PER_ENVIRONMENT2) {
|
|
9668
|
-
throw new
|
|
10431
|
+
throw new HTTPException14(422, {
|
|
9669
10432
|
message: `a variable set supports at most ${MAX_VARIABLES_PER_ENVIRONMENT2} variables`
|
|
9670
10433
|
});
|
|
9671
10434
|
}
|
|
@@ -9673,19 +10436,19 @@ function registerVariableSetRoutes(app, deps) {
|
|
|
9673
10436
|
for (const variable of payload.variables) {
|
|
9674
10437
|
assertAllowedVariableSetVariableName2(variable.name);
|
|
9675
10438
|
if (variableNames.has(variable.name)) {
|
|
9676
|
-
throw new
|
|
10439
|
+
throw new HTTPException14(422, {
|
|
9677
10440
|
message: `duplicate variable set variable name: ${variable.name}`
|
|
9678
10441
|
});
|
|
9679
10442
|
}
|
|
9680
10443
|
variableNames.add(variable.name);
|
|
9681
10444
|
}
|
|
9682
10445
|
if (await countVariableSets2(db, workspaceId) >= MAX_ENVIRONMENTS_PER_WORKSPACE2) {
|
|
9683
|
-
throw new
|
|
10446
|
+
throw new HTTPException14(422, {
|
|
9684
10447
|
message: `a workspace supports at most ${MAX_ENVIRONMENTS_PER_WORKSPACE2} variable sets`
|
|
9685
10448
|
});
|
|
9686
10449
|
}
|
|
9687
10450
|
if (await getVariableSetByName2(db, workspaceId, name)) {
|
|
9688
|
-
throw new
|
|
10451
|
+
throw new HTTPException14(409, { message: `variable set name is already in use: ${name}` });
|
|
9689
10452
|
}
|
|
9690
10453
|
const created = await createVariableSet2(db, {
|
|
9691
10454
|
accountId: grant.accountId,
|
|
@@ -9722,7 +10485,7 @@ function registerVariableSetRoutes(app, deps) {
|
|
|
9722
10485
|
if (name !== void 0 && name !== variableSet.name) {
|
|
9723
10486
|
const existing = await getVariableSetByName2(db, workspaceId, name);
|
|
9724
10487
|
if (existing && existing.id !== variableSet.id) {
|
|
9725
|
-
throw new
|
|
10488
|
+
throw new HTTPException14(409, { message: `variable set name is already in use: ${name}` });
|
|
9726
10489
|
}
|
|
9727
10490
|
}
|
|
9728
10491
|
const updated = await updateVariableSet(db, workspaceId, variableSet.id, {
|
|
@@ -9750,7 +10513,7 @@ function registerVariableSetRoutes(app, deps) {
|
|
|
9750
10513
|
variableSet.id
|
|
9751
10514
|
);
|
|
9752
10515
|
if (attachedTasks > 0) {
|
|
9753
|
-
throw new
|
|
10516
|
+
throw new HTTPException14(409, {
|
|
9754
10517
|
message: `variable set is attached to ${attachedTasks} scheduled task(s); detach first`
|
|
9755
10518
|
});
|
|
9756
10519
|
}
|
|
@@ -9760,7 +10523,7 @@ function registerVariableSetRoutes(app, deps) {
|
|
|
9760
10523
|
variableSet.id
|
|
9761
10524
|
);
|
|
9762
10525
|
if (activeSessions > 0) {
|
|
9763
|
-
throw new
|
|
10526
|
+
throw new HTTPException14(409, {
|
|
9764
10527
|
message: `variable set is attached to ${activeSessions} active session(s); wait for them to finish or cancel them first`
|
|
9765
10528
|
});
|
|
9766
10529
|
}
|
|
@@ -9785,7 +10548,7 @@ function registerVariableSetRoutes(app, deps) {
|
|
|
9785
10548
|
const payload = SetVariableSetVariableRequest.parse(await c.req.json());
|
|
9786
10549
|
const exists = variableSet.variables.some((variable) => variable.name === name);
|
|
9787
10550
|
if (!exists && variableSet.variables.length >= MAX_VARIABLES_PER_ENVIRONMENT2) {
|
|
9788
|
-
throw new
|
|
10551
|
+
throw new HTTPException14(422, {
|
|
9789
10552
|
message: `a variable set supports at most ${MAX_VARIABLES_PER_ENVIRONMENT2} variables`
|
|
9790
10553
|
});
|
|
9791
10554
|
}
|
|
@@ -9815,7 +10578,7 @@ function registerVariableSetRoutes(app, deps) {
|
|
|
9815
10578
|
);
|
|
9816
10579
|
const deleted = await deleteVariableSetVariable(db, workspaceId, variableSet.id, name);
|
|
9817
10580
|
if (!deleted) {
|
|
9818
|
-
throw new
|
|
10581
|
+
throw new HTTPException14(404, { message: "variable set variable not found" });
|
|
9819
10582
|
}
|
|
9820
10583
|
await recordVariableSetAuditEvent2(db, {
|
|
9821
10584
|
grant,
|
|
@@ -9831,7 +10594,7 @@ var registerEnvironmentRoutes = registerVariableSetRoutes;
|
|
|
9831
10594
|
function parseVariableName(raw) {
|
|
9832
10595
|
const parsed = VariableSetVariableName2.safeParse(raw);
|
|
9833
10596
|
if (!parsed.success) {
|
|
9834
|
-
throw new
|
|
10597
|
+
throw new HTTPException14(422, {
|
|
9835
10598
|
message: "variable set variable names must match ^[A-Z][A-Z0-9_]*$"
|
|
9836
10599
|
});
|
|
9837
10600
|
}
|
|
@@ -9841,7 +10604,7 @@ function parseVariableName(raw) {
|
|
|
9841
10604
|
function trimmedVariableSetName(name) {
|
|
9842
10605
|
const trimmed = name.trim();
|
|
9843
10606
|
if (!trimmed) {
|
|
9844
|
-
throw new
|
|
10607
|
+
throw new HTTPException14(422, { message: "variable set name is required" });
|
|
9845
10608
|
}
|
|
9846
10609
|
return trimmed;
|
|
9847
10610
|
}
|
|
@@ -9850,7 +10613,7 @@ function trimmedVariableSetName(name) {
|
|
|
9850
10613
|
import { CreateApiKeyRequest, CreateApiKeyResponse } from "@opengeni/contracts";
|
|
9851
10614
|
import { createApiKey, listApiKeys, revokeApiKey } from "@opengeni/db";
|
|
9852
10615
|
import { zValidator } from "@hono/zod-validator";
|
|
9853
|
-
import { HTTPException as
|
|
10616
|
+
import { HTTPException as HTTPException15 } from "hono/http-exception";
|
|
9854
10617
|
import { requireAccessGrant as requireAccessGrant9 } from "@opengeni/core";
|
|
9855
10618
|
import { requireLimit as requireLimit4 } from "@opengeni/core";
|
|
9856
10619
|
function registerApiKeyRoutes(app, deps) {
|
|
@@ -9900,7 +10663,7 @@ function ensureDelegablePermissions(grantPermissions, requested) {
|
|
|
9900
10663
|
}
|
|
9901
10664
|
const missing = requested.filter((permission) => !grantPermissions.includes(permission));
|
|
9902
10665
|
if (missing.length > 0) {
|
|
9903
|
-
throw new
|
|
10666
|
+
throw new HTTPException15(403, {
|
|
9904
10667
|
message: `cannot delegate missing permissions: ${missing.join(", ")}`
|
|
9905
10668
|
});
|
|
9906
10669
|
}
|
|
@@ -9934,7 +10697,7 @@ import {
|
|
|
9934
10697
|
recordStripeWebhookEvent,
|
|
9935
10698
|
upsertBillingCustomer
|
|
9936
10699
|
} from "@opengeni/db";
|
|
9937
|
-
import { HTTPException as
|
|
10700
|
+
import { HTTPException as HTTPException16 } from "hono/http-exception";
|
|
9938
10701
|
import Stripe from "stripe";
|
|
9939
10702
|
import { requireAccessContext } from "@opengeni/core";
|
|
9940
10703
|
function registerBillingRoutes(app, deps) {
|
|
@@ -9953,7 +10716,7 @@ function registerBillingRoutes(app, deps) {
|
|
|
9953
10716
|
if (workspaceId && !context.workspaceGrants.some(
|
|
9954
10717
|
(grant) => grant.accountId === accountId && grant.workspaceId === workspaceId
|
|
9955
10718
|
)) {
|
|
9956
|
-
throw new
|
|
10719
|
+
throw new HTTPException16(403, { message: "missing workspace access for usage query" });
|
|
9957
10720
|
}
|
|
9958
10721
|
return c.json({
|
|
9959
10722
|
balance: await getBillingBalance(deps.db, accountId),
|
|
@@ -9975,12 +10738,12 @@ function registerBillingRoutes(app, deps) {
|
|
|
9975
10738
|
});
|
|
9976
10739
|
app.post("/v1/billing/checkout", async (c) => {
|
|
9977
10740
|
if (deps.settings.billingMode !== "stripe") {
|
|
9978
|
-
throw new
|
|
10741
|
+
throw new HTTPException16(404, { message: "stripe billing is not enabled" });
|
|
9979
10742
|
}
|
|
9980
10743
|
const context = await requireAccessContext(c, deps);
|
|
9981
10744
|
const parsed = CreateCheckoutRequest.safeParse(await c.req.json());
|
|
9982
10745
|
if (!parsed.success) {
|
|
9983
|
-
throw new
|
|
10746
|
+
throw new HTTPException16(400, {
|
|
9984
10747
|
message: parsed.error.issues[0]?.message ?? "invalid checkout request"
|
|
9985
10748
|
});
|
|
9986
10749
|
}
|
|
@@ -10006,7 +10769,7 @@ function registerBillingRoutes(app, deps) {
|
|
|
10006
10769
|
{ idempotencyKey }
|
|
10007
10770
|
);
|
|
10008
10771
|
if (!session.url) {
|
|
10009
|
-
throw new
|
|
10772
|
+
throw new HTTPException16(502, { message: "Stripe did not return a checkout URL" });
|
|
10010
10773
|
}
|
|
10011
10774
|
return c.json(
|
|
10012
10775
|
CreateCheckoutResponse.parse({
|
|
@@ -10017,11 +10780,11 @@ function registerBillingRoutes(app, deps) {
|
|
|
10017
10780
|
});
|
|
10018
10781
|
app.post("/v1/webhooks/stripe", async (c) => {
|
|
10019
10782
|
if (deps.settings.billingMode !== "stripe") {
|
|
10020
|
-
throw new
|
|
10783
|
+
throw new HTTPException16(404, { message: "stripe billing is not enabled" });
|
|
10021
10784
|
}
|
|
10022
10785
|
const signature = c.req.header("stripe-signature");
|
|
10023
10786
|
if (!signature) {
|
|
10024
|
-
throw new
|
|
10787
|
+
throw new HTTPException16(400, { message: "missing stripe-signature" });
|
|
10025
10788
|
}
|
|
10026
10789
|
const payload = await c.req.text();
|
|
10027
10790
|
let event;
|
|
@@ -10032,7 +10795,7 @@ function registerBillingRoutes(app, deps) {
|
|
|
10032
10795
|
deps.settings.stripeWebhookSecret
|
|
10033
10796
|
);
|
|
10034
10797
|
} catch (error) {
|
|
10035
|
-
throw new
|
|
10798
|
+
throw new HTTPException16(400, {
|
|
10036
10799
|
message: error instanceof Error ? error.message : "invalid stripe signature"
|
|
10037
10800
|
});
|
|
10038
10801
|
}
|
|
@@ -10052,7 +10815,7 @@ function registerBillingRoutes(app, deps) {
|
|
|
10052
10815
|
await markStripeWebhookProcessed(deps.db, event.id);
|
|
10053
10816
|
return c.json({ received: true });
|
|
10054
10817
|
} catch (error) {
|
|
10055
|
-
throw new
|
|
10818
|
+
throw new HTTPException16(500, {
|
|
10056
10819
|
message: error instanceof Error ? error.message : String(error)
|
|
10057
10820
|
});
|
|
10058
10821
|
}
|
|
@@ -10118,7 +10881,7 @@ function stripeCheckoutSessionCreateParams(input) {
|
|
|
10118
10881
|
}
|
|
10119
10882
|
function checkoutReturnUrl(publicBaseUrl, candidate, fallbackPath, field) {
|
|
10120
10883
|
if (!publicBaseUrl) {
|
|
10121
|
-
throw new
|
|
10884
|
+
throw new HTTPException16(500, {
|
|
10122
10885
|
message: "OPENGENI_PUBLIC_BASE_URL is required for Stripe checkout"
|
|
10123
10886
|
});
|
|
10124
10887
|
}
|
|
@@ -10129,7 +10892,7 @@ function checkoutReturnUrl(publicBaseUrl, candidate, fallbackPath, field) {
|
|
|
10129
10892
|
}
|
|
10130
10893
|
const parsed = new URL(candidate);
|
|
10131
10894
|
if (parsed.origin !== base.origin) {
|
|
10132
|
-
throw new
|
|
10895
|
+
throw new HTTPException16(400, { message: `${field} must use the OpenGeni public origin` });
|
|
10133
10896
|
}
|
|
10134
10897
|
return parsed.toString();
|
|
10135
10898
|
}
|
|
@@ -10363,7 +11126,7 @@ async function getOrCreateStripeCustomer(deps, stripe, context, accountId) {
|
|
|
10363
11126
|
}
|
|
10364
11127
|
const account = await getManagedAccount(deps.db, accountId);
|
|
10365
11128
|
if (!account) {
|
|
10366
|
-
throw new
|
|
11129
|
+
throw new HTTPException16(404, { message: "account not found" });
|
|
10367
11130
|
}
|
|
10368
11131
|
const customer = await stripe.customers.create({
|
|
10369
11132
|
name: account.name,
|
|
@@ -10389,17 +11152,17 @@ function stripeCustomerProvider(input) {
|
|
|
10389
11152
|
function requireSelectedAccount(context, requested, permission) {
|
|
10390
11153
|
const accountId = requested ?? context.defaultAccountId ?? void 0;
|
|
10391
11154
|
if (!accountId) {
|
|
10392
|
-
throw new
|
|
11155
|
+
throw new HTTPException16(409, { message: "account selection is required" });
|
|
10393
11156
|
}
|
|
10394
11157
|
const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId);
|
|
10395
11158
|
if (!grant || !grant.permissions.includes(permission) && !grant.permissions.includes("account:admin")) {
|
|
10396
|
-
throw new
|
|
11159
|
+
throw new HTTPException16(403, { message: `missing permission: ${permission}` });
|
|
10397
11160
|
}
|
|
10398
11161
|
return accountId;
|
|
10399
11162
|
}
|
|
10400
11163
|
function stripeClient(deps) {
|
|
10401
11164
|
if (!deps.settings.stripeSecretKey) {
|
|
10402
|
-
throw new
|
|
11165
|
+
throw new HTTPException16(500, { message: "Stripe secret key is not configured" });
|
|
10403
11166
|
}
|
|
10404
11167
|
return new Stripe(deps.settings.stripeSecretKey);
|
|
10405
11168
|
}
|
|
@@ -10446,7 +11209,7 @@ import {
|
|
|
10446
11209
|
verifySignedState
|
|
10447
11210
|
} from "@opengeni/github";
|
|
10448
11211
|
import { deleteCookie, setCookie } from "hono/cookie";
|
|
10449
|
-
import { HTTPException as
|
|
11212
|
+
import { HTTPException as HTTPException17 } from "hono/http-exception";
|
|
10450
11213
|
import { hasPermission as hasPermission5, requireAccessGrant as requireAccessGrant10 } from "@opengeni/core";
|
|
10451
11214
|
var githubStateCookie = "opengeni_github_state";
|
|
10452
11215
|
var githubBindingStateMaxAgeSeconds = 10 * 60;
|
|
@@ -10484,15 +11247,15 @@ function registerGitHubRoutes(app, deps) {
|
|
|
10484
11247
|
const workspaceId = c.req.param("workspaceId");
|
|
10485
11248
|
const state = c.req.query("state");
|
|
10486
11249
|
if (!state) {
|
|
10487
|
-
throw new
|
|
11250
|
+
throw new HTTPException17(400, { message: "missing GitHub installation state" });
|
|
10488
11251
|
}
|
|
10489
11252
|
const statePayload = readSignedState3(state, githubStateSecret);
|
|
10490
11253
|
if (!statePayload || statePayload.intent !== "installation_authority" || statePayload.workspaceId !== workspaceId || typeof statePayload.accountId !== "string" || !isFreshGitHubBindingState(statePayload)) {
|
|
10491
|
-
throw new
|
|
11254
|
+
throw new HTTPException17(400, { message: "invalid or expired GitHub installation state" });
|
|
10492
11255
|
}
|
|
10493
11256
|
const slug = settings.githubAppSlug?.trim();
|
|
10494
11257
|
if (!slug || githubAppMissingSettings2(settings).length > 0) {
|
|
10495
|
-
throw new
|
|
11258
|
+
throw new HTTPException17(409, {
|
|
10496
11259
|
message: JSON.stringify({
|
|
10497
11260
|
message: "GitHub App is not configured",
|
|
10498
11261
|
missing: githubAppMissingSettings2(settings)
|
|
@@ -10511,11 +11274,11 @@ function registerGitHubRoutes(app, deps) {
|
|
|
10511
11274
|
return c.json({ repositories: await listWorkspaceGitHubRepositories(deps, workspaceId) });
|
|
10512
11275
|
} catch (error) {
|
|
10513
11276
|
if (error instanceof GitHubAppConfigurationError2) {
|
|
10514
|
-
throw new
|
|
11277
|
+
throw new HTTPException17(409, {
|
|
10515
11278
|
message: JSON.stringify({ message: error.message, missing: error.missing })
|
|
10516
11279
|
});
|
|
10517
11280
|
}
|
|
10518
|
-
throw new
|
|
11281
|
+
throw new HTTPException17(502, {
|
|
10519
11282
|
message: error instanceof Error ? error.message : String(error)
|
|
10520
11283
|
});
|
|
10521
11284
|
}
|
|
@@ -10527,11 +11290,11 @@ function registerGitHubRoutes(app, deps) {
|
|
|
10527
11290
|
return c.json({ repositories: await listWorkspaceGitHubRepositories(deps, workspaceId) });
|
|
10528
11291
|
} catch (error) {
|
|
10529
11292
|
if (error instanceof GitHubAppConfigurationError2) {
|
|
10530
|
-
throw new
|
|
11293
|
+
throw new HTTPException17(409, {
|
|
10531
11294
|
message: JSON.stringify({ message: error.message, missing: error.missing })
|
|
10532
11295
|
});
|
|
10533
11296
|
}
|
|
10534
|
-
throw new
|
|
11297
|
+
throw new HTTPException17(502, {
|
|
10535
11298
|
message: error instanceof Error ? error.message : String(error)
|
|
10536
11299
|
});
|
|
10537
11300
|
}
|
|
@@ -10541,7 +11304,7 @@ function registerGitHubRoutes(app, deps) {
|
|
|
10541
11304
|
const grant = await requireAccessGrant10(c, deps, workspaceId, "github:manage");
|
|
10542
11305
|
const installationId = parsePositiveInteger(c.req.param("installationId"));
|
|
10543
11306
|
if (installationId === null) {
|
|
10544
|
-
throw new
|
|
11307
|
+
throw new HTTPException17(400, { message: "invalid GitHub installation id" });
|
|
10545
11308
|
}
|
|
10546
11309
|
const deleted = await deleteGitHubInstallationBinding(db, {
|
|
10547
11310
|
accountId: grant.accountId,
|
|
@@ -10549,7 +11312,7 @@ function registerGitHubRoutes(app, deps) {
|
|
|
10549
11312
|
installationId
|
|
10550
11313
|
});
|
|
10551
11314
|
if (!deleted) {
|
|
10552
|
-
throw new
|
|
11315
|
+
throw new HTTPException17(404, { message: "GitHub installation binding not found" });
|
|
10553
11316
|
}
|
|
10554
11317
|
return c.body(null, 204);
|
|
10555
11318
|
});
|
|
@@ -10585,10 +11348,10 @@ function registerGitHubRoutes(app, deps) {
|
|
|
10585
11348
|
const code = c.req.query("code");
|
|
10586
11349
|
const state = c.req.query("state");
|
|
10587
11350
|
if (!code) {
|
|
10588
|
-
throw new
|
|
11351
|
+
throw new HTTPException17(400, { message: "missing GitHub manifest code" });
|
|
10589
11352
|
}
|
|
10590
11353
|
if (!state || !verifySignedState(state, githubStateSecret)) {
|
|
10591
|
-
throw new
|
|
11354
|
+
throw new HTTPException17(400, { message: "invalid or expired GitHub manifest state" });
|
|
10592
11355
|
}
|
|
10593
11356
|
try {
|
|
10594
11357
|
const conversion = await convertGitHubAppManifest(code);
|
|
@@ -10597,22 +11360,22 @@ function registerGitHubRoutes(app, deps) {
|
|
|
10597
11360
|
return c.html(githubSuccessHtml(envLines));
|
|
10598
11361
|
} catch (error) {
|
|
10599
11362
|
const message = error instanceof GitHubAppApiError ? error.message : String(error);
|
|
10600
|
-
throw new
|
|
11363
|
+
throw new HTTPException17(502, { message });
|
|
10601
11364
|
}
|
|
10602
11365
|
});
|
|
10603
11366
|
const handleGitHubInstallCallback = async (c) => {
|
|
10604
11367
|
const state = c.req.query("state");
|
|
10605
11368
|
if (!state) {
|
|
10606
|
-
throw new
|
|
11369
|
+
throw new HTTPException17(400, { message: "missing GitHub installation state" });
|
|
10607
11370
|
}
|
|
10608
11371
|
const statePayload = readSignedState3(state, githubStateSecret);
|
|
10609
11372
|
if (!statePayload || statePayload.intent !== "installation_authority" || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string" || !isFreshGitHubBindingState(statePayload)) {
|
|
10610
|
-
throw new
|
|
11373
|
+
throw new HTTPException17(400, { message: "invalid or expired GitHub installation state" });
|
|
10611
11374
|
}
|
|
10612
11375
|
requireGitHubStateCookie(c, state);
|
|
10613
11376
|
const grant = await requireGitHubManageGrant(c, deps, statePayload.workspaceId, statePayload);
|
|
10614
11377
|
if (grant.accountId !== statePayload.accountId) {
|
|
10615
|
-
throw new
|
|
11378
|
+
throw new HTTPException17(403, {
|
|
10616
11379
|
message: "GitHub installation state does not match this workspace"
|
|
10617
11380
|
});
|
|
10618
11381
|
}
|
|
@@ -10621,15 +11384,15 @@ function registerGitHubRoutes(app, deps) {
|
|
|
10621
11384
|
return c.html(githubSetupPendingHtml());
|
|
10622
11385
|
}
|
|
10623
11386
|
if (setupAction !== "install" && setupAction !== "update") {
|
|
10624
|
-
throw new
|
|
11387
|
+
throw new HTTPException17(400, { message: "unsupported GitHub setup action" });
|
|
10625
11388
|
}
|
|
10626
11389
|
const installationId = parsePositiveInteger(c.req.query("installation_id"));
|
|
10627
11390
|
if (installationId === null) {
|
|
10628
|
-
throw new
|
|
11391
|
+
throw new HTTPException17(400, { message: "missing or invalid GitHub installation_id" });
|
|
10629
11392
|
}
|
|
10630
11393
|
const clientId = settings.githubClientId?.trim();
|
|
10631
11394
|
if (!clientId) {
|
|
10632
|
-
throw new
|
|
11395
|
+
throw new HTTPException17(409, {
|
|
10633
11396
|
message: JSON.stringify({
|
|
10634
11397
|
message: "GitHub App is not configured",
|
|
10635
11398
|
missing: ["OPENGENI_GITHUB_CLIENT_ID"]
|
|
@@ -10658,23 +11421,23 @@ function registerGitHubRoutes(app, deps) {
|
|
|
10658
11421
|
const code = c.req.query("code");
|
|
10659
11422
|
const state = c.req.query("state");
|
|
10660
11423
|
if (!code) {
|
|
10661
|
-
throw new
|
|
11424
|
+
throw new HTTPException17(400, { message: "missing GitHub OAuth code" });
|
|
10662
11425
|
}
|
|
10663
11426
|
if (!state) {
|
|
10664
|
-
throw new
|
|
11427
|
+
throw new HTTPException17(400, { message: "missing GitHub OAuth state" });
|
|
10665
11428
|
}
|
|
10666
11429
|
const statePayload = readSignedState3(state, githubStateSecret);
|
|
10667
11430
|
if (!statePayload || statePayload.intent !== "installation_authority_oauth" || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string" || !isFreshGitHubBindingState(statePayload)) {
|
|
10668
|
-
throw new
|
|
11431
|
+
throw new HTTPException17(400, { message: "invalid or expired GitHub OAuth state" });
|
|
10669
11432
|
}
|
|
10670
11433
|
const installationId = parsePositiveInteger(String(statePayload.installationId ?? ""));
|
|
10671
11434
|
if (installationId === null) {
|
|
10672
|
-
throw new
|
|
11435
|
+
throw new HTTPException17(400, { message: "invalid GitHub installation id" });
|
|
10673
11436
|
}
|
|
10674
11437
|
requireGitHubStateCookie(c, state);
|
|
10675
11438
|
const grant = await requireGitHubManageGrant(c, deps, statePayload.workspaceId, statePayload);
|
|
10676
11439
|
if (grant.accountId !== statePayload.accountId) {
|
|
10677
|
-
throw new
|
|
11440
|
+
throw new HTTPException17(403, {
|
|
10678
11441
|
message: "GitHub OAuth state does not match this workspace"
|
|
10679
11442
|
});
|
|
10680
11443
|
}
|
|
@@ -10685,16 +11448,16 @@ function registerGitHubRoutes(app, deps) {
|
|
|
10685
11448
|
throw githubAuthorityHttpError(error);
|
|
10686
11449
|
}
|
|
10687
11450
|
if (!proof) {
|
|
10688
|
-
throw new
|
|
11451
|
+
throw new HTTPException17(409, {
|
|
10689
11452
|
message: "The configured GitHub provider cannot prove personal-owner or organization-owner authority"
|
|
10690
11453
|
});
|
|
10691
11454
|
}
|
|
10692
11455
|
if (!isConsistentGitHubBindingProof(proof, installationId)) {
|
|
10693
|
-
throw new
|
|
11456
|
+
throw new HTTPException17(409, { message: "GitHub installation proof is stale or invalid" });
|
|
10694
11457
|
}
|
|
10695
11458
|
const repositoryIds = [...new Set(proof.repositories.map((repository) => repository.id))];
|
|
10696
11459
|
if (repositoryIds.length !== proof.repositories.length) {
|
|
10697
|
-
throw new
|
|
11460
|
+
throw new HTTPException17(409, { message: "GitHub returned duplicate repository identities" });
|
|
10698
11461
|
}
|
|
10699
11462
|
const authorityCheckedAt = /* @__PURE__ */ new Date();
|
|
10700
11463
|
const expiresAt = new Date((statePayload.iat + githubBindingStateMaxAgeSeconds) * 1e3);
|
|
@@ -10718,12 +11481,12 @@ function registerGitHubRoutes(app, deps) {
|
|
|
10718
11481
|
});
|
|
10719
11482
|
} catch (error) {
|
|
10720
11483
|
if (error instanceof GitHubInstallationAuthorityCommitError) {
|
|
10721
|
-
throw new
|
|
11484
|
+
throw new HTTPException17(409, { message: error.message });
|
|
10722
11485
|
}
|
|
10723
11486
|
throw error;
|
|
10724
11487
|
}
|
|
10725
11488
|
if (!bound) {
|
|
10726
|
-
throw new
|
|
11489
|
+
throw new HTTPException17(409, {
|
|
10727
11490
|
message: "GitHub installation authorization was already used"
|
|
10728
11491
|
});
|
|
10729
11492
|
}
|
|
@@ -10740,17 +11503,17 @@ function registerGitHubRoutes(app, deps) {
|
|
|
10740
11503
|
const form = new URLSearchParams(await c.req.text());
|
|
10741
11504
|
const state = form.get("oauth_state");
|
|
10742
11505
|
if (!state) {
|
|
10743
|
-
throw new
|
|
11506
|
+
throw new HTTPException17(400, { message: "missing GitHub OAuth state" });
|
|
10744
11507
|
}
|
|
10745
11508
|
const statePayload = readSignedState3(state, githubStateSecret);
|
|
10746
11509
|
if (!statePayload || typeof statePayload.accountId !== "string" || statePayload.accountId.length === 0 || statePayload.workspaceId !== workspaceId) {
|
|
10747
|
-
throw new
|
|
11510
|
+
throw new HTTPException17(400, { message: "invalid or expired GitHub OAuth state" });
|
|
10748
11511
|
}
|
|
10749
11512
|
throw legacyInstallationChooserDisabled();
|
|
10750
11513
|
});
|
|
10751
11514
|
}
|
|
10752
11515
|
function legacyInstallationChooserDisabled() {
|
|
10753
|
-
return new
|
|
11516
|
+
return new HTTPException17(410, { message: legacyInstallationChooserDisabledMessage });
|
|
10754
11517
|
}
|
|
10755
11518
|
function setGitHubStateCookie(c, deps, state) {
|
|
10756
11519
|
setCookie(c, githubStateCookie, state, {
|
|
@@ -10763,7 +11526,7 @@ function setGitHubStateCookie(c, deps, state) {
|
|
|
10763
11526
|
}
|
|
10764
11527
|
function requireGitHubStateCookie(c, state) {
|
|
10765
11528
|
if (!allCookieValues(c, githubStateCookie).includes(state)) {
|
|
10766
|
-
throw new
|
|
11529
|
+
throw new HTTPException17(400, {
|
|
10767
11530
|
message: "invalid or expired GitHub installation browser state"
|
|
10768
11531
|
});
|
|
10769
11532
|
}
|
|
@@ -10772,7 +11535,7 @@ async function requireGitHubManageGrant(c, deps, workspaceId, expectedState) {
|
|
|
10772
11535
|
try {
|
|
10773
11536
|
return await requireAccessGrant10(c, deps, workspaceId, "github:manage");
|
|
10774
11537
|
} catch (error) {
|
|
10775
|
-
if (!(error instanceof
|
|
11538
|
+
if (!(error instanceof HTTPException17) || error.status !== 401) {
|
|
10776
11539
|
throw error;
|
|
10777
11540
|
}
|
|
10778
11541
|
const grant = githubBrowserGrantFromState(deps.settings, expectedState, workspaceId);
|
|
@@ -10794,27 +11557,27 @@ function allCookieValues(c, name) {
|
|
|
10794
11557
|
});
|
|
10795
11558
|
}
|
|
10796
11559
|
function githubAuthorityHttpError(error) {
|
|
10797
|
-
if (error instanceof
|
|
11560
|
+
if (error instanceof HTTPException17) {
|
|
10798
11561
|
return error;
|
|
10799
11562
|
}
|
|
10800
11563
|
if (error instanceof GitHubInstallationAuthorityError) {
|
|
10801
11564
|
if (error.reason === "authority_denied") {
|
|
10802
|
-
return new
|
|
11565
|
+
return new HTTPException17(403, { message: error.message });
|
|
10803
11566
|
}
|
|
10804
11567
|
if (error.reason === "installation_missing") {
|
|
10805
|
-
return new
|
|
11568
|
+
return new HTTPException17(404, { message: error.message });
|
|
10806
11569
|
}
|
|
10807
|
-
return new
|
|
11570
|
+
return new HTTPException17(409, { message: error.message });
|
|
10808
11571
|
}
|
|
10809
11572
|
if (error instanceof GitHubAppConfigurationError2) {
|
|
10810
|
-
return new
|
|
11573
|
+
return new HTTPException17(409, {
|
|
10811
11574
|
message: JSON.stringify({ message: error.message, missing: error.missing })
|
|
10812
11575
|
});
|
|
10813
11576
|
}
|
|
10814
11577
|
if (error instanceof GitHubAppApiError) {
|
|
10815
|
-
return new
|
|
11578
|
+
return new HTTPException17(502, { message: error.message });
|
|
10816
11579
|
}
|
|
10817
|
-
return new
|
|
11580
|
+
return new HTTPException17(502, { message: "GitHub authority verification failed" });
|
|
10818
11581
|
}
|
|
10819
11582
|
function isSecureRequest(c, deps) {
|
|
10820
11583
|
return deps.settings.publicBaseUrl?.startsWith("https://") || c.req.header("x-forwarded-proto") === "https" || new URL(c.req.url).protocol === "https:";
|
|
@@ -10894,7 +11657,7 @@ import {
|
|
|
10894
11657
|
updatePackInstallationStatus
|
|
10895
11658
|
} from "@opengeni/db";
|
|
10896
11659
|
import { getDocumentBase as getDocumentBase2 } from "@opengeni/documents";
|
|
10897
|
-
import { HTTPException as
|
|
11660
|
+
import { HTTPException as HTTPException18 } from "hono/http-exception";
|
|
10898
11661
|
import { requireAccessGrant as requireAccessGrant11 } from "@opengeni/core";
|
|
10899
11662
|
import { requireLimit as requireLimit5 } from "@opengeni/core";
|
|
10900
11663
|
import { validateVariableSetAttachment } from "@opengeni/core";
|
|
@@ -10922,7 +11685,7 @@ function registerPackRoutes(app, deps) {
|
|
|
10922
11685
|
const grant = await requireAccessGrant11(c, deps, workspaceId, "workspace:admin");
|
|
10923
11686
|
const manifest = RegisterCapabilityPackRequest.parse(await c.req.json());
|
|
10924
11687
|
if (isBuiltInCapabilityPack(manifest.id)) {
|
|
10925
|
-
throw new
|
|
11688
|
+
throw new HTTPException18(409, {
|
|
10926
11689
|
message: `pack id ${manifest.id} is a built-in pack and cannot be replaced`
|
|
10927
11690
|
});
|
|
10928
11691
|
}
|
|
@@ -10938,10 +11701,10 @@ function registerPackRoutes(app, deps) {
|
|
|
10938
11701
|
await requireAccessGrant11(c, deps, workspaceId, "workspace:admin");
|
|
10939
11702
|
const packId = c.req.param("packId");
|
|
10940
11703
|
if (isBuiltInCapabilityPack(packId)) {
|
|
10941
|
-
throw new
|
|
11704
|
+
throw new HTTPException18(409, { message: "built-in packs cannot be unregistered" });
|
|
10942
11705
|
}
|
|
10943
11706
|
if (!await getWorkspacePack(db, workspaceId, packId)) {
|
|
10944
|
-
throw new
|
|
11707
|
+
throw new HTTPException18(404, { message: "pack not found" });
|
|
10945
11708
|
}
|
|
10946
11709
|
const installation = await getPackInstallation(db, workspaceId, packId);
|
|
10947
11710
|
if (installation && installation.status === "active") {
|
|
@@ -10982,7 +11745,7 @@ function registerPackRoutes(app, deps) {
|
|
|
10982
11745
|
const storedVariableSetId = typeof existing?.metadata.variableSetId === "string" ? existing.metadata.variableSetId : typeof existing?.metadata.environmentId === "string" ? existing.metadata.environmentId : void 0;
|
|
10983
11746
|
const variableSetId = payload.variableSetId ?? storedVariableSetId;
|
|
10984
11747
|
if (pack.variableSet?.required && !variableSetId) {
|
|
10985
|
-
throw new
|
|
11748
|
+
throw new HTTPException18(422, {
|
|
10986
11749
|
message: "this pack requires a variableSet attachment; pass variableSetId"
|
|
10987
11750
|
});
|
|
10988
11751
|
}
|
|
@@ -10998,7 +11761,7 @@ function registerPackRoutes(app, deps) {
|
|
|
10998
11761
|
(name) => !variableSet.variables.some((variable) => variable.name === name)
|
|
10999
11762
|
);
|
|
11000
11763
|
if (missing.length > 0) {
|
|
11001
|
-
throw new
|
|
11764
|
+
throw new HTTPException18(422, {
|
|
11002
11765
|
message: `variableSet is missing required variable(s): ${missing.join(", ")}`
|
|
11003
11766
|
});
|
|
11004
11767
|
}
|
|
@@ -11023,7 +11786,7 @@ function registerPackRoutes(app, deps) {
|
|
|
11023
11786
|
const pack = await requirePack(db, workspaceId, MARKETING_SOCIAL_PACK_ID);
|
|
11024
11787
|
const installation = await getPackInstallation(db, workspaceId, pack.id);
|
|
11025
11788
|
if (installation?.status !== "active") {
|
|
11026
|
-
throw new
|
|
11789
|
+
throw new HTTPException18(409, {
|
|
11027
11790
|
message: "enable the marketing social pack before creating its scheduled tasks"
|
|
11028
11791
|
});
|
|
11029
11792
|
}
|
|
@@ -11036,7 +11799,7 @@ function registerPackRoutes(app, deps) {
|
|
|
11036
11799
|
});
|
|
11037
11800
|
const connections = await resolveSocialConnections(db, workspaceId, payload.connectionIds);
|
|
11038
11801
|
if (connections.length === 0) {
|
|
11039
|
-
throw new
|
|
11802
|
+
throw new HTTPException18(422, {
|
|
11040
11803
|
message: "at least one connected social account is required"
|
|
11041
11804
|
});
|
|
11042
11805
|
}
|
|
@@ -11083,7 +11846,7 @@ function registerPackRoutes(app, deps) {
|
|
|
11083
11846
|
async function requirePack(db, workspaceId, packId) {
|
|
11084
11847
|
const pack = await resolveCapabilityPack(db, workspaceId, packId);
|
|
11085
11848
|
if (!pack) {
|
|
11086
|
-
throw new
|
|
11849
|
+
throw new HTTPException18(404, { message: "pack not found" });
|
|
11087
11850
|
}
|
|
11088
11851
|
return pack;
|
|
11089
11852
|
}
|
|
@@ -11093,7 +11856,7 @@ async function resolveSocialConnections(db, workspaceId, connectionIds) {
|
|
|
11093
11856
|
ids.map(async (id) => {
|
|
11094
11857
|
const connection = await getSocialConnection(db, workspaceId, id);
|
|
11095
11858
|
if (!connection) {
|
|
11096
|
-
throw new
|
|
11859
|
+
throw new HTTPException18(422, { message: `unknown social connection: ${id}` });
|
|
11097
11860
|
}
|
|
11098
11861
|
return connection;
|
|
11099
11862
|
})
|
|
@@ -11102,7 +11865,7 @@ async function resolveSocialConnections(db, workspaceId, connectionIds) {
|
|
|
11102
11865
|
);
|
|
11103
11866
|
const inactive = connections.find((connection) => connection.status !== "connected");
|
|
11104
11867
|
if (inactive) {
|
|
11105
|
-
throw new
|
|
11868
|
+
throw new HTTPException18(422, {
|
|
11106
11869
|
message: `social connection ${inactive.id} is ${inactive.status}`
|
|
11107
11870
|
});
|
|
11108
11871
|
}
|
|
@@ -11112,7 +11875,7 @@ async function validateDocumentBaseIds(db, workspaceId, documentBaseIds) {
|
|
|
11112
11875
|
for (const baseId of [...new Set(documentBaseIds)]) {
|
|
11113
11876
|
const base = await getDocumentBase2(db, workspaceId, baseId);
|
|
11114
11877
|
if (!base) {
|
|
11115
|
-
throw new
|
|
11878
|
+
throw new HTTPException18(422, { message: `unknown document base: ${baseId}` });
|
|
11116
11879
|
}
|
|
11117
11880
|
}
|
|
11118
11881
|
}
|
|
@@ -11130,7 +11893,7 @@ import {
|
|
|
11130
11893
|
RigChangeAlreadyVerifyingError as RigChangeAlreadyVerifyingError2,
|
|
11131
11894
|
RigChangeTransitionError as RigChangeTransitionError2
|
|
11132
11895
|
} from "@opengeni/db";
|
|
11133
|
-
import { HTTPException as
|
|
11896
|
+
import { HTTPException as HTTPException19 } from "hono/http-exception";
|
|
11134
11897
|
import { requireAccessGrant as requireAccessGrant12 } from "@opengeni/core";
|
|
11135
11898
|
import {
|
|
11136
11899
|
activateRigVersionForApi,
|
|
@@ -11154,7 +11917,7 @@ function registerRigRoutes(app, deps) {
|
|
|
11154
11917
|
change = await beginRigChangeVerificationAttempt2(db, workspaceId, changeId, { startedAt });
|
|
11155
11918
|
} catch (error) {
|
|
11156
11919
|
if (error instanceof RigChangeAlreadyVerifyingError2 || error instanceof RigChangeTransitionError2) {
|
|
11157
|
-
throw new
|
|
11920
|
+
throw new HTTPException19(409, { message: error.message });
|
|
11158
11921
|
}
|
|
11159
11922
|
throw error;
|
|
11160
11923
|
}
|
|
@@ -11491,8 +12254,8 @@ import {
|
|
|
11491
12254
|
clearSessionContext,
|
|
11492
12255
|
getOpenPtySession,
|
|
11493
12256
|
getRetainedProcess,
|
|
11494
|
-
getSandbox as
|
|
11495
|
-
getSession as
|
|
12257
|
+
getSandbox as getSandbox3,
|
|
12258
|
+
getSession as getSession5,
|
|
11496
12259
|
getSessionForSubject,
|
|
11497
12260
|
getSessionGoal as getSessionGoal2,
|
|
11498
12261
|
getSessionHumanInputRequest,
|
|
@@ -11551,17 +12314,21 @@ import { githubAppBotIdentity as githubAppBotIdentity2 } from "@opengeni/github"
|
|
|
11551
12314
|
import {
|
|
11552
12315
|
acquireLease as acquireLease2,
|
|
11553
12316
|
getSandboxSessionEnvelope as getSandboxSessionEnvelope2,
|
|
12317
|
+
getSandbox as getSandbox2,
|
|
11554
12318
|
loadWorkspaceEnvironmentForRun as loadWorkspaceEnvironmentForRun2,
|
|
11555
12319
|
markWarmLeaseInstanceLost as markWarmLeaseInstanceLost2,
|
|
12320
|
+
readActiveSandbox as readActiveSandbox2,
|
|
11556
12321
|
readLease as readLease3,
|
|
11557
12322
|
releaseLeaseHolder as releaseLeaseHolder2
|
|
11558
12323
|
} from "@opengeni/db";
|
|
11559
12324
|
import { appendAndPublishEvents as appendAndPublishEvents4 } from "@opengeni/events";
|
|
11560
|
-
import { HTTPException as
|
|
12325
|
+
import { HTTPException as HTTPException20 } from "hono/http-exception";
|
|
11561
12326
|
import {
|
|
12327
|
+
buildSelfhostedBackendSession,
|
|
11562
12328
|
establishSandboxSessionFromEnvelope as establishSandboxSessionFromEnvelope3,
|
|
11563
12329
|
isProviderSandboxNotFoundError as isProviderSandboxNotFoundError3,
|
|
11564
12330
|
SandboxChannelAService,
|
|
12331
|
+
NatsControlRpc as NatsControlRpc3,
|
|
11565
12332
|
ChannelAConflictError,
|
|
11566
12333
|
ChannelANotFoundError,
|
|
11567
12334
|
ChannelAUnsupportedError,
|
|
@@ -11571,17 +12338,112 @@ import {
|
|
|
11571
12338
|
withToolspaceTokenSession,
|
|
11572
12339
|
withRunCredentialsSession
|
|
11573
12340
|
} from "@opengeni/runtime/sandbox";
|
|
11574
|
-
import { wrapChannelABoxWithRouting } from "@opengeni/core";
|
|
12341
|
+
import { relayConfigFromSettings as relayConfigFromSettings3, wrapChannelABoxWithRouting } from "@opengeni/core";
|
|
11575
12342
|
async function withChannelA(services, ctx, fn) {
|
|
11576
12343
|
const { db, settings, bus } = services;
|
|
11577
12344
|
const { accountId, workspaceId, session } = ctx;
|
|
11578
12345
|
if (session.sandboxBackend === "none") {
|
|
11579
|
-
throw new
|
|
12346
|
+
throw new HTTPException20(409, { message: "sandbox not available" });
|
|
11580
12347
|
}
|
|
11581
12348
|
const sandboxGroupId = session.sandboxGroupId;
|
|
11582
12349
|
const requestId = crypto.randomUUID();
|
|
11583
12350
|
const holderId = `direct:${requestId}`;
|
|
11584
12351
|
const leaseTtlMs = settings.sandboxLeaseTtlMs;
|
|
12352
|
+
const workspaceEnvironment = await loadWorkspaceEnvironmentForRun2(
|
|
12353
|
+
db,
|
|
12354
|
+
settings,
|
|
12355
|
+
workspaceId,
|
|
12356
|
+
session.environmentId
|
|
12357
|
+
);
|
|
12358
|
+
const settingsForSession = session.sandboxBackend !== settings.sandboxBackend ? { ...settings, sandboxBackend: session.sandboxBackend } : settings;
|
|
12359
|
+
const environment = stableSandboxEnvironmentForRun2(
|
|
12360
|
+
settingsForSession,
|
|
12361
|
+
workspaceEnvironment?.values ?? {},
|
|
12362
|
+
{ workspaceId }
|
|
12363
|
+
);
|
|
12364
|
+
if (hasGitCredentialRepositorySelection2(session.resources)) {
|
|
12365
|
+
applyGitAuthPointerEnvironment2(
|
|
12366
|
+
environment,
|
|
12367
|
+
hasGitHubRepositorySelection2(session.resources) ? githubAppBotIdentity2(settings) : null
|
|
12368
|
+
);
|
|
12369
|
+
}
|
|
12370
|
+
const runEstablished = async (routed, lease) => {
|
|
12371
|
+
const emit = async (events) => {
|
|
12372
|
+
await appendAndPublishEvents4(
|
|
12373
|
+
db,
|
|
12374
|
+
bus,
|
|
12375
|
+
workspaceId,
|
|
12376
|
+
session.id,
|
|
12377
|
+
events.map((e) => ({ type: e.type, payload: e.payload }))
|
|
12378
|
+
);
|
|
12379
|
+
};
|
|
12380
|
+
const routingSession = routed.session;
|
|
12381
|
+
const credentialSession = withRunCredentialsSession(routingSession, session.id);
|
|
12382
|
+
const scopedSession = environment.OPENGENI_TOOLSPACE_TOKEN_FILE ? withToolspaceTokenSession(
|
|
12383
|
+
credentialSession,
|
|
12384
|
+
toolspaceTokenFileFromEnvironment(environment, session.id)
|
|
12385
|
+
) : credentialSession;
|
|
12386
|
+
const service = new SandboxChannelAService({
|
|
12387
|
+
session: scopedSession,
|
|
12388
|
+
leaseEpoch: lease?.leaseEpoch ?? session.activeEpoch,
|
|
12389
|
+
emit
|
|
12390
|
+
});
|
|
12391
|
+
return await fn({ service, lease, routingSession, requestId });
|
|
12392
|
+
};
|
|
12393
|
+
if (session.sandboxBackend === "selfhosted") {
|
|
12394
|
+
let established2;
|
|
12395
|
+
try {
|
|
12396
|
+
const pointer = await readActiveSandbox2(db, workspaceId, session.id);
|
|
12397
|
+
if (!pointer?.activeSandboxId) {
|
|
12398
|
+
throw new HTTPException20(409, {
|
|
12399
|
+
message: "machine-home session has no active Connected Machine"
|
|
12400
|
+
});
|
|
12401
|
+
}
|
|
12402
|
+
const sandbox = await getSandbox2(db, workspaceId, pointer.activeSandboxId);
|
|
12403
|
+
if (sandbox?.kind !== "selfhosted" || !sandbox.enrollmentId) {
|
|
12404
|
+
throw new HTTPException20(409, {
|
|
12405
|
+
message: "machine-home session points to an unavailable Connected Machine"
|
|
12406
|
+
});
|
|
12407
|
+
}
|
|
12408
|
+
const built = await buildSelfhostedBackendSession({
|
|
12409
|
+
workspaceId,
|
|
12410
|
+
agentId: sandbox.enrollmentId,
|
|
12411
|
+
relay: relayConfigFromSettings3(settings),
|
|
12412
|
+
controlRpcFactory: () => new NatsControlRpc3(async () => bus.getRequestConnection()),
|
|
12413
|
+
epoch: pointer.activeEpoch,
|
|
12414
|
+
environment,
|
|
12415
|
+
workingDir: pointer.workingDir,
|
|
12416
|
+
timeoutMs: settings.sandboxSelfhostedControlTimeoutMs,
|
|
12417
|
+
execTimeoutMs: settings.sandboxSelfhostedExecTimeoutMs
|
|
12418
|
+
});
|
|
12419
|
+
established2 = {
|
|
12420
|
+
client: built.client,
|
|
12421
|
+
session: built.session,
|
|
12422
|
+
sessionState: { agentId: sandbox.enrollmentId },
|
|
12423
|
+
instanceId: sandbox.enrollmentId,
|
|
12424
|
+
backendId: "selfhosted"
|
|
12425
|
+
};
|
|
12426
|
+
const routed = wrapChannelABoxWithRouting(
|
|
12427
|
+
{ db, settings, bus },
|
|
12428
|
+
{
|
|
12429
|
+
accountId,
|
|
12430
|
+
workspaceId,
|
|
12431
|
+
sessionId: session.id,
|
|
12432
|
+
pinnedSelfhosted: {
|
|
12433
|
+
sandboxId: sandbox.id,
|
|
12434
|
+
epoch: pointer.activeEpoch
|
|
12435
|
+
},
|
|
12436
|
+
directRequest: { requestId, holderId }
|
|
12437
|
+
},
|
|
12438
|
+
established2
|
|
12439
|
+
);
|
|
12440
|
+
return await runEstablished(routed, null);
|
|
12441
|
+
} catch (error) {
|
|
12442
|
+
throw mapChannelAError(error);
|
|
12443
|
+
} finally {
|
|
12444
|
+
await dropEstablishedHandle2(established2);
|
|
12445
|
+
}
|
|
12446
|
+
}
|
|
11585
12447
|
const release = async () => {
|
|
11586
12448
|
await releaseLeaseHolder2(db, {
|
|
11587
12449
|
accountId,
|
|
@@ -11606,13 +12468,13 @@ async function withChannelA(services, ctx, fn) {
|
|
|
11606
12468
|
});
|
|
11607
12469
|
if (acquired.role === "blocked") {
|
|
11608
12470
|
await release();
|
|
11609
|
-
throw new
|
|
12471
|
+
throw new HTTPException20(409, {
|
|
11610
12472
|
message: `sandbox recovery ${acquired.lease.recovery.restore.status} at epoch ${acquired.lease.leaseEpoch}`
|
|
11611
12473
|
});
|
|
11612
12474
|
}
|
|
11613
12475
|
if (acquired.role === "fenced") {
|
|
11614
12476
|
await release();
|
|
11615
|
-
throw new
|
|
12477
|
+
throw new HTTPException20(409, {
|
|
11616
12478
|
message: `sandbox lease superseded (epoch ${acquired.lease.leaseEpoch}); retry`
|
|
11617
12479
|
});
|
|
11618
12480
|
}
|
|
@@ -11620,24 +12482,6 @@ async function withChannelA(services, ctx, fn) {
|
|
|
11620
12482
|
let leaseSnapshot = acquired.lease;
|
|
11621
12483
|
try {
|
|
11622
12484
|
const envelope = await getSandboxSessionEnvelope2(db, workspaceId, session.id);
|
|
11623
|
-
const workspaceEnvironment = await loadWorkspaceEnvironmentForRun2(
|
|
11624
|
-
db,
|
|
11625
|
-
settings,
|
|
11626
|
-
workspaceId,
|
|
11627
|
-
session.environmentId
|
|
11628
|
-
);
|
|
11629
|
-
const settingsForSession = session.sandboxBackend !== settings.sandboxBackend ? { ...settings, sandboxBackend: session.sandboxBackend } : settings;
|
|
11630
|
-
const environment = stableSandboxEnvironmentForRun2(
|
|
11631
|
-
settingsForSession,
|
|
11632
|
-
workspaceEnvironment?.values ?? {},
|
|
11633
|
-
{ workspaceId }
|
|
11634
|
-
);
|
|
11635
|
-
if (hasGitCredentialRepositorySelection2(session.resources)) {
|
|
11636
|
-
applyGitAuthPointerEnvironment2(
|
|
11637
|
-
environment,
|
|
11638
|
-
hasGitHubRepositorySelection2(session.resources) ? githubAppBotIdentity2(settings) : null
|
|
11639
|
-
);
|
|
11640
|
-
}
|
|
11641
12485
|
if (acquired.role === "spawner") {
|
|
11642
12486
|
const expectedEpoch = acquired.lease.leaseEpoch;
|
|
11643
12487
|
try {
|
|
@@ -11658,14 +12502,14 @@ async function withChannelA(services, ctx, fn) {
|
|
|
11658
12502
|
established = result.established;
|
|
11659
12503
|
leaseSnapshot = result.lease;
|
|
11660
12504
|
} catch (error) {
|
|
11661
|
-
throw new
|
|
12505
|
+
throw new HTTPException20(409, {
|
|
11662
12506
|
message: `sandbox not available (${error instanceof Error ? error.message : "spawn failed"})`
|
|
11663
12507
|
});
|
|
11664
12508
|
}
|
|
11665
12509
|
} else {
|
|
11666
12510
|
const live = await readLease3(db, workspaceId, sandboxGroupId);
|
|
11667
12511
|
if (!live || live.liveness !== "warm" || live.leaseEpoch !== acquired.lease.leaseEpoch || live.instanceId === null) {
|
|
11668
|
-
throw new
|
|
12512
|
+
throw new HTTPException20(409, {
|
|
11669
12513
|
message: `sandbox lease is not attachable; retry`
|
|
11670
12514
|
});
|
|
11671
12515
|
}
|
|
@@ -11696,23 +12540,12 @@ async function withChannelA(services, ctx, fn) {
|
|
|
11696
12540
|
}
|
|
11697
12541
|
]);
|
|
11698
12542
|
}
|
|
11699
|
-
throw new
|
|
12543
|
+
throw new HTTPException20(409, {
|
|
11700
12544
|
message: `sandbox instance was lost; retry to restore it`
|
|
11701
12545
|
});
|
|
11702
12546
|
}
|
|
11703
12547
|
}
|
|
11704
|
-
const
|
|
11705
|
-
await appendAndPublishEvents4(
|
|
11706
|
-
db,
|
|
11707
|
-
bus,
|
|
11708
|
-
workspaceId,
|
|
11709
|
-
session.id,
|
|
11710
|
-
// SessionEventType is a string enum at the contract; the producer parses
|
|
11711
|
-
// the payload, so this cast is the same shape the worker emits.
|
|
11712
|
-
events.map((e) => ({ type: e.type, payload: e.payload }))
|
|
11713
|
-
);
|
|
11714
|
-
};
|
|
11715
|
-
const routedSession = wrapChannelABoxWithRouting(
|
|
12548
|
+
const routed = wrapChannelABoxWithRouting(
|
|
11716
12549
|
{ db, settings, bus },
|
|
11717
12550
|
{
|
|
11718
12551
|
accountId,
|
|
@@ -11727,18 +12560,8 @@ async function withChannelA(services, ctx, fn) {
|
|
|
11727
12560
|
directRequest: { requestId, holderId }
|
|
11728
12561
|
},
|
|
11729
12562
|
established
|
|
11730
|
-
)
|
|
11731
|
-
|
|
11732
|
-
const scopedSession = environment.OPENGENI_TOOLSPACE_TOKEN_FILE ? withToolspaceTokenSession(
|
|
11733
|
-
credentialSession,
|
|
11734
|
-
toolspaceTokenFileFromEnvironment(environment, session.id)
|
|
11735
|
-
) : credentialSession;
|
|
11736
|
-
const service = new SandboxChannelAService({
|
|
11737
|
-
session: scopedSession,
|
|
11738
|
-
leaseEpoch: leaseSnapshot.leaseEpoch,
|
|
11739
|
-
emit
|
|
11740
|
-
});
|
|
11741
|
-
return await fn({ service, lease: leaseSnapshot, routingSession: routedSession, requestId });
|
|
12563
|
+
);
|
|
12564
|
+
return await runEstablished(routed, leaseSnapshot);
|
|
11742
12565
|
} catch (error) {
|
|
11743
12566
|
throw mapChannelAError(error);
|
|
11744
12567
|
} finally {
|
|
@@ -11747,17 +12570,17 @@ async function withChannelA(services, ctx, fn) {
|
|
|
11747
12570
|
}
|
|
11748
12571
|
}
|
|
11749
12572
|
function mapChannelAError(error) {
|
|
11750
|
-
if (error instanceof
|
|
12573
|
+
if (error instanceof HTTPException20) return error;
|
|
11751
12574
|
if (error instanceof ChannelAUnavailableError)
|
|
11752
|
-
return new
|
|
12575
|
+
return new HTTPException20(503, { message: error.message });
|
|
11753
12576
|
if (error instanceof ChannelAValidationError)
|
|
11754
|
-
return new
|
|
12577
|
+
return new HTTPException20(400, { message: error.message });
|
|
11755
12578
|
if (error instanceof ChannelANotFoundError)
|
|
11756
|
-
return new
|
|
12579
|
+
return new HTTPException20(404, { message: error.message });
|
|
11757
12580
|
if (error instanceof ChannelAConflictError)
|
|
11758
|
-
return new
|
|
12581
|
+
return new HTTPException20(409, { message: error.message });
|
|
11759
12582
|
if (error instanceof ChannelAUnsupportedError)
|
|
11760
|
-
return new
|
|
12583
|
+
return new HTTPException20(409, { message: error.message });
|
|
11761
12584
|
return error;
|
|
11762
12585
|
}
|
|
11763
12586
|
async function dropEstablishedHandle2(established) {
|
|
@@ -11766,7 +12589,7 @@ async function dropEstablishedHandle2(established) {
|
|
|
11766
12589
|
|
|
11767
12590
|
// src/routes/sessions.ts
|
|
11768
12591
|
import { negotiateCapabilities } from "@opengeni/runtime/sandbox";
|
|
11769
|
-
import { HTTPException as
|
|
12592
|
+
import { HTTPException as HTTPException22 } from "hono/http-exception";
|
|
11770
12593
|
import {
|
|
11771
12594
|
requireAccessGrant as requireAccessGrant14,
|
|
11772
12595
|
requireSessionAuthorization as requireSessionAuthorization2,
|
|
@@ -12276,7 +13099,7 @@ import {
|
|
|
12276
13099
|
WorkspaceCaptureManifest,
|
|
12277
13100
|
WorkspaceCaptureStats
|
|
12278
13101
|
} from "@opengeni/contracts";
|
|
12279
|
-
import { HTTPException as
|
|
13102
|
+
import { HTTPException as HTTPException21 } from "hono/http-exception";
|
|
12280
13103
|
var CAPTURE_INLINE_MANIFEST_MAX_BYTES = 2 * 1024 * 1024;
|
|
12281
13104
|
var CAPTURE_INLINE_FILE_MAX_BYTES = 256 * 1024;
|
|
12282
13105
|
var CAPTURE_SIGNED_URL_TTL_SECONDS = 300;
|
|
@@ -12371,15 +13194,15 @@ async function serveWorkspaceCapture(row, storage) {
|
|
|
12371
13194
|
async function serveWorkspaceCaptureFile(row, path, storage) {
|
|
12372
13195
|
const loaded = row ? await loadManifest(row, storage) : null;
|
|
12373
13196
|
if (!loaded) {
|
|
12374
|
-
throw new
|
|
13197
|
+
throw new HTTPException21(404, { message: "capture not found" });
|
|
12375
13198
|
}
|
|
12376
13199
|
const { manifest } = loaded;
|
|
12377
13200
|
const file = manifest.files.find((f) => f.path === path);
|
|
12378
13201
|
if (!file) {
|
|
12379
|
-
throw new
|
|
13202
|
+
throw new HTTPException21(404, { message: "path not in capture" });
|
|
12380
13203
|
}
|
|
12381
13204
|
if (file.deleted) {
|
|
12382
|
-
throw new
|
|
13205
|
+
throw new HTTPException21(404, { message: "file was deleted" });
|
|
12383
13206
|
}
|
|
12384
13207
|
const base = {
|
|
12385
13208
|
path: file.path,
|
|
@@ -12442,6 +13265,11 @@ function registerSessionRoutes(app, deps) {
|
|
|
12442
13265
|
routeEpoch: pty.routeEpoch
|
|
12443
13266
|
});
|
|
12444
13267
|
const adoptPtyProcess = async (ctx, handle, pty) => {
|
|
13268
|
+
if (!handle.lease) {
|
|
13269
|
+
throw new HTTPException22(409, {
|
|
13270
|
+
message: "durable interactive terminals require a session-home provider lease"
|
|
13271
|
+
});
|
|
13272
|
+
}
|
|
12445
13273
|
const process = await getRetainedProcess(db, {
|
|
12446
13274
|
workspaceId: ctx.workspaceId,
|
|
12447
13275
|
sessionId: ctx.session.id,
|
|
@@ -12450,7 +13278,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
12450
13278
|
if (!process || process.state !== "active" || process.ownerActorKind !== "direct" || process.accountId !== ctx.accountId || process.leaseId !== pty.leaseId || process.sandboxGroupId !== pty.sandboxGroupId || process.parentAdmissionId !== pty.openAdmissionId || process.leaseEpoch !== pty.leaseEpoch || process.providerBackend !== pty.providerBackend || process.providerInstanceId !== pty.providerInstanceId || process.routeKind !== pty.routeKind || process.routeTargetId !== pty.routeTargetId || process.routeEpoch !== pty.routeEpoch || process.providerSessionId !== pty.execSessionId || // Only a persistable home backend can currently be reconstructed by an
|
|
12451
13279
|
// API request without consulting the mutable active pointer.
|
|
12452
13280
|
process.routeTargetId !== null || handle.lease.id !== process.leaseId || handle.lease.sandboxGroupId !== process.sandboxGroupId || handle.lease.leaseEpoch !== process.leaseEpoch || handle.lease.backend !== process.providerBackend || handle.lease.instanceId !== process.providerInstanceId) {
|
|
12453
|
-
throw new
|
|
13281
|
+
throw new HTTPException22(409, {
|
|
12454
13282
|
message: "pty retained-process identity is stale; reopen the terminal"
|
|
12455
13283
|
});
|
|
12456
13284
|
}
|
|
@@ -12643,17 +13471,17 @@ function registerSessionRoutes(app, deps) {
|
|
|
12643
13471
|
});
|
|
12644
13472
|
} catch (error) {
|
|
12645
13473
|
if (error instanceof SessionListAccessError) {
|
|
12646
|
-
throw new
|
|
13474
|
+
throw new HTTPException22(403, { message: error.message });
|
|
12647
13475
|
}
|
|
12648
13476
|
if (error instanceof SessionListCursorExpiredError) {
|
|
12649
|
-
throw new
|
|
13477
|
+
throw new HTTPException22(410, { message: error.message });
|
|
12650
13478
|
}
|
|
12651
13479
|
if (error instanceof SessionListCursorError) {
|
|
12652
|
-
throw new
|
|
13480
|
+
throw new HTTPException22(400, { message: error.message });
|
|
12653
13481
|
}
|
|
12654
13482
|
if (error instanceof SessionListSnapshotLimitError) {
|
|
12655
13483
|
c.header("Retry-After", "5");
|
|
12656
|
-
throw new
|
|
13484
|
+
throw new HTTPException22(429, { message: error.message });
|
|
12657
13485
|
}
|
|
12658
13486
|
throw error;
|
|
12659
13487
|
}
|
|
@@ -12678,7 +13506,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
12678
13506
|
const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
|
|
12679
13507
|
const sessionId = c.req.param("sessionId");
|
|
12680
13508
|
if (!z3.string().uuid().safeParse(sessionId).success) {
|
|
12681
|
-
throw new
|
|
13509
|
+
throw new HTTPException22(404, { message: "session not found" });
|
|
12682
13510
|
}
|
|
12683
13511
|
const session = await getSessionForSubject(
|
|
12684
13512
|
db,
|
|
@@ -12688,7 +13516,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
12688
13516
|
relatedSessionAccessFor(c)
|
|
12689
13517
|
);
|
|
12690
13518
|
if (!session) {
|
|
12691
|
-
throw new
|
|
13519
|
+
throw new HTTPException22(404, { message: "session not found" });
|
|
12692
13520
|
}
|
|
12693
13521
|
return c.json(await withEffectivePolicy(deps, workspaceId, session));
|
|
12694
13522
|
});
|
|
@@ -12697,11 +13525,11 @@ function registerSessionRoutes(app, deps) {
|
|
|
12697
13525
|
const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
|
|
12698
13526
|
const sessionId = c.req.param("sessionId");
|
|
12699
13527
|
if (!z3.string().uuid().safeParse(sessionId).success) {
|
|
12700
|
-
throw new
|
|
13528
|
+
throw new HTTPException22(404, { message: "session not found" });
|
|
12701
13529
|
}
|
|
12702
13530
|
const parsed = UpdateSessionPinRequest.safeParse(await c.req.json().catch(() => null));
|
|
12703
13531
|
if (!parsed.success) {
|
|
12704
|
-
throw new
|
|
13532
|
+
throw new HTTPException22(400, { message: "invalid session pin request" });
|
|
12705
13533
|
}
|
|
12706
13534
|
try {
|
|
12707
13535
|
const session = await setSessionPin(db, {
|
|
@@ -12711,7 +13539,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
12711
13539
|
...parsed.data
|
|
12712
13540
|
});
|
|
12713
13541
|
if (!session) {
|
|
12714
|
-
throw new
|
|
13542
|
+
throw new HTTPException22(404, { message: "session not found" });
|
|
12715
13543
|
}
|
|
12716
13544
|
return c.json(
|
|
12717
13545
|
await withEffectivePolicy(
|
|
@@ -12722,7 +13550,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
12722
13550
|
);
|
|
12723
13551
|
} catch (error) {
|
|
12724
13552
|
if (error instanceof SessionPinAccessError) {
|
|
12725
|
-
throw new
|
|
13553
|
+
throw new HTTPException22(403, { message: error.message });
|
|
12726
13554
|
}
|
|
12727
13555
|
if (error instanceof SessionPinVersionConflictError) {
|
|
12728
13556
|
return c.json(
|
|
@@ -12760,7 +13588,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
12760
13588
|
const body = await c.req.json();
|
|
12761
13589
|
const target = typeof body.target === "string" ? body.target : "";
|
|
12762
13590
|
if (!target) {
|
|
12763
|
-
throw new
|
|
13591
|
+
throw new HTTPException22(400, {
|
|
12764
13592
|
message: 'target is required ("auto" or an account id)'
|
|
12765
13593
|
});
|
|
12766
13594
|
}
|
|
@@ -12775,7 +13603,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
12775
13603
|
);
|
|
12776
13604
|
const ok = mutation.result;
|
|
12777
13605
|
if (!ok) {
|
|
12778
|
-
throw new
|
|
13606
|
+
throw new HTTPException22(404, {
|
|
12779
13607
|
message: "session or codex account not found"
|
|
12780
13608
|
});
|
|
12781
13609
|
}
|
|
@@ -12814,7 +13642,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
12814
13642
|
titleUpdate.relatedSessionAccess
|
|
12815
13643
|
);
|
|
12816
13644
|
if (!session) {
|
|
12817
|
-
throw new
|
|
13645
|
+
throw new HTTPException22(404, { message: "session not found" });
|
|
12818
13646
|
}
|
|
12819
13647
|
return c.json(await withEffectivePolicy(deps, workspaceId, session));
|
|
12820
13648
|
});
|
|
@@ -12829,7 +13657,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
12829
13657
|
await c.req.json().catch(() => null)
|
|
12830
13658
|
);
|
|
12831
13659
|
if (!parsedServerId.success || !payload.success) {
|
|
12832
|
-
throw new
|
|
13660
|
+
throw new HTTPException22(400, { message: "invalid MCP approval-policy request" });
|
|
12833
13661
|
}
|
|
12834
13662
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
12835
13663
|
return c.json(
|
|
@@ -12872,7 +13700,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
12872
13700
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
12873
13701
|
const goal = await getSessionGoalWithContinuation(db, workspaceId, sessionId);
|
|
12874
13702
|
if (!goal) {
|
|
12875
|
-
throw new
|
|
13703
|
+
throw new HTTPException22(404, { message: "session goal not found" });
|
|
12876
13704
|
}
|
|
12877
13705
|
return c.json(goal);
|
|
12878
13706
|
});
|
|
@@ -12884,10 +13712,10 @@ function registerSessionRoutes(app, deps) {
|
|
|
12884
13712
|
const payload = UpdateSessionGoalRequest.parse(await c.req.json());
|
|
12885
13713
|
const existing = await getSessionGoal2(db, workspaceId, sessionId);
|
|
12886
13714
|
if (!existing) {
|
|
12887
|
-
throw new
|
|
13715
|
+
throw new HTTPException22(404, { message: "session goal not found" });
|
|
12888
13716
|
}
|
|
12889
13717
|
if (existing.status === "completed") {
|
|
12890
|
-
throw new
|
|
13718
|
+
throw new HTTPException22(409, {
|
|
12891
13719
|
message: "session goal is completed; set a new goal instead"
|
|
12892
13720
|
});
|
|
12893
13721
|
}
|
|
@@ -12909,7 +13737,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
12909
13737
|
return c.json(await getSessionGoalWithContinuation(db, workspaceId, sessionId) ?? goal2);
|
|
12910
13738
|
}
|
|
12911
13739
|
if (existing.status !== "paused") {
|
|
12912
|
-
throw new
|
|
13740
|
+
throw new HTTPException22(409, {
|
|
12913
13741
|
message: `session goal is ${existing.status}; only paused goals can be resumed`
|
|
12914
13742
|
});
|
|
12915
13743
|
}
|
|
@@ -12963,7 +13791,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
12963
13791
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
12964
13792
|
const clearBody = ClearSessionContextRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
12965
13793
|
if (!clearBody.success) {
|
|
12966
|
-
throw new
|
|
13794
|
+
throw new HTTPException22(400, {
|
|
12967
13795
|
message: "context clear requires an explicit { confirm: true }"
|
|
12968
13796
|
});
|
|
12969
13797
|
}
|
|
@@ -12973,7 +13801,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
12973
13801
|
sessionId
|
|
12974
13802
|
}).catch((error) => {
|
|
12975
13803
|
if (error instanceof SessionContextBusyError) {
|
|
12976
|
-
throw new
|
|
13804
|
+
throw new HTTPException22(409, { message: error.message });
|
|
12977
13805
|
}
|
|
12978
13806
|
throw error;
|
|
12979
13807
|
});
|
|
@@ -13039,14 +13867,14 @@ function registerSessionRoutes(app, deps) {
|
|
|
13039
13867
|
"events"
|
|
13040
13868
|
);
|
|
13041
13869
|
if (resultMode === "compact" && latestClass === void 0) {
|
|
13042
|
-
throw new
|
|
13870
|
+
throw new HTTPException22(400, {
|
|
13043
13871
|
message: "resultMode=compact requires latest"
|
|
13044
13872
|
});
|
|
13045
13873
|
}
|
|
13046
13874
|
if (latestClass && ["includeTypes", "excludeTypes", "includeClasses", "excludeClasses"].some(
|
|
13047
13875
|
(name) => c.req.query(name) !== void 0
|
|
13048
13876
|
)) {
|
|
13049
|
-
throw new
|
|
13877
|
+
throw new HTTPException22(400, {
|
|
13050
13878
|
message: "latest cannot be combined with event filters"
|
|
13051
13879
|
});
|
|
13052
13880
|
}
|
|
@@ -13199,7 +14027,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
13199
14027
|
await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
|
|
13200
14028
|
const sessionId = c.req.param("sessionId");
|
|
13201
14029
|
const snapshot = await getSessionQueueSnapshot2(db, workspaceId, sessionId);
|
|
13202
|
-
if (!snapshot) throw new
|
|
14030
|
+
if (!snapshot) throw new HTTPException22(404, { message: "session not found" });
|
|
13203
14031
|
return c.json(projectQueueSnapshot(snapshot, sessionId, relatedSessionAccessFor(c)));
|
|
13204
14032
|
});
|
|
13205
14033
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/queue/:turnId/move", async (c) => {
|
|
@@ -13345,12 +14173,12 @@ function registerSessionRoutes(app, deps) {
|
|
|
13345
14173
|
const workspaceId = c.req.param("workspaceId");
|
|
13346
14174
|
const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:control");
|
|
13347
14175
|
if (workspaceControlUtf8Bytes(grant.subjectId) > WORKSPACE_CONTROL_ACTOR_MAX_BYTES) {
|
|
13348
|
-
throw new
|
|
14176
|
+
throw new HTTPException22(400, { message: "workspace-control actor is too large" });
|
|
13349
14177
|
}
|
|
13350
14178
|
const sessionId = c.req.param("sessionId");
|
|
13351
14179
|
const parsed = SessionControlRequest.safeParse(await c.req.json().catch(() => null));
|
|
13352
14180
|
if (!parsed.success) {
|
|
13353
|
-
throw new
|
|
14181
|
+
throw new HTTPException22(400, { message: "invalid session control request" });
|
|
13354
14182
|
}
|
|
13355
14183
|
try {
|
|
13356
14184
|
const response = await controlHumanSessionWorkstream2(
|
|
@@ -13442,7 +14270,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
13442
14270
|
clientEventId: event.clientEventId ?? null
|
|
13443
14271
|
});
|
|
13444
14272
|
if (accepted.action === "conflict") {
|
|
13445
|
-
throw new
|
|
14273
|
+
throw new HTTPException22(409, {
|
|
13446
14274
|
message: `session is ${accepted.sessionStatus}; no unhandled approval is pending`
|
|
13447
14275
|
});
|
|
13448
14276
|
}
|
|
@@ -13472,14 +14300,14 @@ function registerSessionRoutes(app, deps) {
|
|
|
13472
14300
|
});
|
|
13473
14301
|
} catch (error) {
|
|
13474
14302
|
if (error instanceof HumanInputResponseValidationError) {
|
|
13475
|
-
throw new
|
|
14303
|
+
throw new HTTPException22(error.code === "SKIP_NOT_ALLOWED" ? 409 : 422, {
|
|
13476
14304
|
message: error.message
|
|
13477
14305
|
});
|
|
13478
14306
|
}
|
|
13479
14307
|
throw error;
|
|
13480
14308
|
}
|
|
13481
14309
|
if (accepted.action === "not_found") {
|
|
13482
|
-
throw new
|
|
14310
|
+
throw new HTTPException22(404, { message: "human-input request not found" });
|
|
13483
14311
|
}
|
|
13484
14312
|
await publishDurableSessionEvents2(bus, workspaceId, sessionId, accepted.events);
|
|
13485
14313
|
if (accepted.workflowWakeRevision !== null) {
|
|
@@ -13493,7 +14321,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
13493
14321
|
});
|
|
13494
14322
|
}
|
|
13495
14323
|
if (accepted.action === "conflict") {
|
|
13496
|
-
throw new
|
|
14324
|
+
throw new HTTPException22(409, {
|
|
13497
14325
|
message: `human-input request is ${accepted.request.status}`
|
|
13498
14326
|
});
|
|
13499
14327
|
}
|
|
@@ -13508,7 +14336,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
13508
14336
|
const rawStatus = c.req.query("status");
|
|
13509
14337
|
const status = rawStatus ? HumanInputRequestStatus.safeParse(rawStatus) : null;
|
|
13510
14338
|
if (status && !status.success) {
|
|
13511
|
-
throw new
|
|
14339
|
+
throw new HTTPException22(400, { message: "invalid human-input request status" });
|
|
13512
14340
|
}
|
|
13513
14341
|
const requests = await listSessionHumanInputRequests(db, workspaceId, sessionId, {
|
|
13514
14342
|
...status?.success ? { status: status.data } : {}
|
|
@@ -13527,13 +14355,13 @@ function registerSessionRoutes(app, deps) {
|
|
|
13527
14355
|
sessionId,
|
|
13528
14356
|
c.req.param("requestId")
|
|
13529
14357
|
);
|
|
13530
|
-
if (!request) throw new
|
|
14358
|
+
if (!request) throw new HTTPException22(404, { message: "human-input request not found" });
|
|
13531
14359
|
return c.json(request);
|
|
13532
14360
|
}
|
|
13533
14361
|
);
|
|
13534
14362
|
function assertOwnershipEnabled() {
|
|
13535
14363
|
if (!settings.sandboxOwnershipEnabled) {
|
|
13536
|
-
throw new
|
|
14364
|
+
throw new HTTPException22(404, {
|
|
13537
14365
|
message: "sandbox ownership is not enabled for this deployment"
|
|
13538
14366
|
});
|
|
13539
14367
|
}
|
|
@@ -13548,9 +14376,9 @@ function registerSessionRoutes(app, deps) {
|
|
|
13548
14376
|
const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
|
|
13549
14377
|
assertOwnershipEnabled();
|
|
13550
14378
|
const sessionId = c.req.param("sessionId");
|
|
13551
|
-
const session = await
|
|
14379
|
+
const session = await getSession5(db, workspaceId, sessionId);
|
|
13552
14380
|
if (!session) {
|
|
13553
|
-
throw new
|
|
14381
|
+
throw new HTTPException22(404, { message: "session not found" });
|
|
13554
14382
|
}
|
|
13555
14383
|
const lease = await readGroupLease(
|
|
13556
14384
|
{ db, settings },
|
|
@@ -13646,7 +14474,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
13646
14474
|
});
|
|
13647
14475
|
let responseCapabilities = capabilities;
|
|
13648
14476
|
if (capabilities.DesktopStream.transport !== null) {
|
|
13649
|
-
const activeSandbox = session.activeSandboxId ? await
|
|
14477
|
+
const activeSandbox = session.activeSandboxId ? await getSandbox3(db, workspaceId, session.activeSandboxId) : null;
|
|
13650
14478
|
const wire = resolveActiveDesktopTransport(
|
|
13651
14479
|
activeSandbox?.kind === "selfhosted",
|
|
13652
14480
|
settings.sandboxDesktopInteractive !== false
|
|
@@ -13665,13 +14493,13 @@ function registerSessionRoutes(app, deps) {
|
|
|
13665
14493
|
const grant = await requireAccessGrant14(c, deps, workspaceId, "stream:acknowledge");
|
|
13666
14494
|
assertOwnershipEnabled();
|
|
13667
14495
|
const sessionId = c.req.param("sessionId");
|
|
13668
|
-
const session = await
|
|
14496
|
+
const session = await getSession5(db, workspaceId, sessionId);
|
|
13669
14497
|
if (!session) {
|
|
13670
|
-
throw new
|
|
14498
|
+
throw new HTTPException22(404, { message: "session not found" });
|
|
13671
14499
|
}
|
|
13672
14500
|
const parsed = AcknowledgeStreamRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
13673
14501
|
if (!parsed.success) {
|
|
13674
|
-
throw new
|
|
14502
|
+
throw new HTTPException22(400, {
|
|
13675
14503
|
message: "invalid stream acknowledgment request"
|
|
13676
14504
|
});
|
|
13677
14505
|
}
|
|
@@ -13694,13 +14522,13 @@ function registerSessionRoutes(app, deps) {
|
|
|
13694
14522
|
const grant = await requireAccessGrant14(c, deps, workspaceId, "stream:view");
|
|
13695
14523
|
assertOwnershipEnabled();
|
|
13696
14524
|
const sessionId = c.req.param("sessionId");
|
|
13697
|
-
const session = await
|
|
14525
|
+
const session = await getSession5(db, workspaceId, sessionId);
|
|
13698
14526
|
if (!session) {
|
|
13699
|
-
throw new
|
|
14527
|
+
throw new HTTPException22(404, { message: "session not found" });
|
|
13700
14528
|
}
|
|
13701
14529
|
const parsed = AttachViewerRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
13702
14530
|
if (!parsed.success) {
|
|
13703
|
-
throw new
|
|
14531
|
+
throw new HTTPException22(400, {
|
|
13704
14532
|
message: "invalid viewer attach request"
|
|
13705
14533
|
});
|
|
13706
14534
|
}
|
|
@@ -13713,17 +14541,17 @@ function registerSessionRoutes(app, deps) {
|
|
|
13713
14541
|
subjectId: grant.subjectId
|
|
13714
14542
|
});
|
|
13715
14543
|
if (!ack?.acknowledgedUnredacted) {
|
|
13716
|
-
throw new
|
|
14544
|
+
throw new HTTPException22(409, {
|
|
13717
14545
|
message: "stream_acknowledgment_required"
|
|
13718
14546
|
});
|
|
13719
14547
|
}
|
|
13720
14548
|
if (shared && !ack.acknowledgedShared) {
|
|
13721
|
-
throw new
|
|
14549
|
+
throw new HTTPException22(409, {
|
|
13722
14550
|
message: "shared_acknowledgment_required"
|
|
13723
14551
|
});
|
|
13724
14552
|
}
|
|
13725
14553
|
}
|
|
13726
|
-
const activeSandbox = session.activeSandboxId ? await
|
|
14554
|
+
const activeSandbox = session.activeSandboxId ? await getSandbox3(db, workspaceId, session.activeSandboxId) : null;
|
|
13727
14555
|
const selfhostedActive = activeSandbox?.kind === "selfhosted";
|
|
13728
14556
|
let stream = null;
|
|
13729
14557
|
let terminal = null;
|
|
@@ -13825,13 +14653,13 @@ function registerSessionRoutes(app, deps) {
|
|
|
13825
14653
|
const grant = await requireAccessGrant14(c, deps, workspaceId, "stream:view");
|
|
13826
14654
|
assertOwnershipEnabled();
|
|
13827
14655
|
const sessionId = c.req.param("sessionId");
|
|
13828
|
-
const session = await
|
|
14656
|
+
const session = await getSession5(db, workspaceId, sessionId);
|
|
13829
14657
|
if (!session) {
|
|
13830
|
-
throw new
|
|
14658
|
+
throw new HTTPException22(404, { message: "session not found" });
|
|
13831
14659
|
}
|
|
13832
14660
|
const parsed = ViewerHeartbeatRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
13833
14661
|
if (!parsed.success) {
|
|
13834
|
-
throw new
|
|
14662
|
+
throw new HTTPException22(400, {
|
|
13835
14663
|
message: "viewer heartbeat requires { leaseEpoch }"
|
|
13836
14664
|
});
|
|
13837
14665
|
}
|
|
@@ -13853,9 +14681,9 @@ function registerSessionRoutes(app, deps) {
|
|
|
13853
14681
|
const grant = await requireAccessGrant14(c, deps, workspaceId, "stream:view");
|
|
13854
14682
|
assertOwnershipEnabled();
|
|
13855
14683
|
const sessionId = c.req.param("sessionId");
|
|
13856
|
-
const session = await
|
|
14684
|
+
const session = await getSession5(db, workspaceId, sessionId);
|
|
13857
14685
|
if (!session) {
|
|
13858
|
-
throw new
|
|
14686
|
+
throw new HTTPException22(404, { message: "session not found" });
|
|
13859
14687
|
}
|
|
13860
14688
|
await detachViewer(
|
|
13861
14689
|
{ db, settings },
|
|
@@ -13875,9 +14703,9 @@ function registerSessionRoutes(app, deps) {
|
|
|
13875
14703
|
const grant = await requireAccessGrant14(c, deps, workspaceId, "stream:view");
|
|
13876
14704
|
assertOwnershipEnabled();
|
|
13877
14705
|
const sessionId = c.req.param("sessionId");
|
|
13878
|
-
const session = await
|
|
14706
|
+
const session = await getSession5(db, workspaceId, sessionId);
|
|
13879
14707
|
if (!session) {
|
|
13880
|
-
throw new
|
|
14708
|
+
throw new HTTPException22(404, { message: "session not found" });
|
|
13881
14709
|
}
|
|
13882
14710
|
const result = await revokeViewer(db, {
|
|
13883
14711
|
accountId: grant.accountId,
|
|
@@ -13897,9 +14725,9 @@ function registerSessionRoutes(app, deps) {
|
|
|
13897
14725
|
const grant = await requireAccessGrant14(c, deps, workspaceId, permission);
|
|
13898
14726
|
assertOwnershipEnabled();
|
|
13899
14727
|
const sessionId = c.req.param("sessionId") ?? "";
|
|
13900
|
-
const session = await
|
|
14728
|
+
const session = await getSession5(db, workspaceId, sessionId);
|
|
13901
14729
|
if (!session) {
|
|
13902
|
-
throw new
|
|
14730
|
+
throw new HTTPException22(404, { message: "session not found" });
|
|
13903
14731
|
}
|
|
13904
14732
|
return {
|
|
13905
14733
|
accountId: grant.accountId,
|
|
@@ -13912,7 +14740,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
13912
14740
|
const raw = await c.req.json().catch(() => void 0);
|
|
13913
14741
|
const result = schema.safeParse(raw ?? {});
|
|
13914
14742
|
if (!result.success) {
|
|
13915
|
-
throw new
|
|
14743
|
+
throw new HTTPException22(400, { message: "invalid request body" });
|
|
13916
14744
|
}
|
|
13917
14745
|
return result.data;
|
|
13918
14746
|
}
|
|
@@ -14020,9 +14848,9 @@ function registerSessionRoutes(app, deps) {
|
|
|
14020
14848
|
const workspaceId = c.req.param("workspaceId") ?? "";
|
|
14021
14849
|
await requireAccessGrant14(c, deps, workspaceId, "files:read");
|
|
14022
14850
|
const sessionId = c.req.param("sessionId") ?? "";
|
|
14023
|
-
const session = await
|
|
14851
|
+
const session = await getSession5(db, workspaceId, sessionId);
|
|
14024
14852
|
if (!session) {
|
|
14025
|
-
throw new
|
|
14853
|
+
throw new HTTPException22(404, { message: "session not found" });
|
|
14026
14854
|
}
|
|
14027
14855
|
if (!objectStorage) {
|
|
14028
14856
|
return c.json({ available: false });
|
|
@@ -14036,23 +14864,23 @@ function registerSessionRoutes(app, deps) {
|
|
|
14036
14864
|
const sessionId = c.req.param("sessionId") ?? "";
|
|
14037
14865
|
const path = c.req.query("path");
|
|
14038
14866
|
if (!path) {
|
|
14039
|
-
throw new
|
|
14867
|
+
throw new HTTPException22(400, {
|
|
14040
14868
|
message: "path query parameter is required"
|
|
14041
14869
|
});
|
|
14042
14870
|
}
|
|
14043
|
-
const session = await
|
|
14871
|
+
const session = await getSession5(db, workspaceId, sessionId);
|
|
14044
14872
|
if (!session) {
|
|
14045
|
-
throw new
|
|
14873
|
+
throw new HTTPException22(404, { message: "session not found" });
|
|
14046
14874
|
}
|
|
14047
14875
|
if (!objectStorage) {
|
|
14048
|
-
throw new
|
|
14876
|
+
throw new HTTPException22(404, { message: "capture not found" });
|
|
14049
14877
|
}
|
|
14050
14878
|
const revisionParam = c.req.query("revision");
|
|
14051
14879
|
let row;
|
|
14052
14880
|
if (revisionParam !== void 0 && revisionParam !== "") {
|
|
14053
14881
|
const revision = Number(revisionParam);
|
|
14054
14882
|
if (!Number.isInteger(revision) || revision < 0) {
|
|
14055
|
-
throw new
|
|
14883
|
+
throw new HTTPException22(400, {
|
|
14056
14884
|
message: "revision must be a non-negative integer"
|
|
14057
14885
|
});
|
|
14058
14886
|
}
|
|
@@ -14076,12 +14904,17 @@ function registerSessionRoutes(app, deps) {
|
|
|
14076
14904
|
const ctx = await channelAPreamble(c, "terminal:attach");
|
|
14077
14905
|
const req = await parseChannelABody(c, PtyOpenRequest);
|
|
14078
14906
|
if (ctx.session.sandboxBackend === "selfhosted" || ctx.session.activeSandboxId !== null) {
|
|
14079
|
-
throw new
|
|
14907
|
+
throw new HTTPException22(409, {
|
|
14080
14908
|
message: "durable interactive terminals require the session-home provider route and are unavailable on active swaps or non-persistable routes; use synchronous exec or attach the session home sandbox"
|
|
14081
14909
|
});
|
|
14082
14910
|
}
|
|
14083
14911
|
const ptyId = crypto.randomUUID();
|
|
14084
14912
|
const out = await withChannelA({ db, settings, bus }, ctx, async (handle) => {
|
|
14913
|
+
if (!handle.lease) {
|
|
14914
|
+
throw new HTTPException22(409, {
|
|
14915
|
+
message: "durable interactive terminals require a session-home provider lease"
|
|
14916
|
+
});
|
|
14917
|
+
}
|
|
14085
14918
|
const { service } = handle;
|
|
14086
14919
|
const opened = await service.ptyOpen(req, ptyId);
|
|
14087
14920
|
const execSessionId = opened.execSessionId;
|
|
@@ -14095,7 +14928,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
14095
14928
|
if (execSessionId !== null && handle.routingSession.hasRetainedProcess(execSessionId)) {
|
|
14096
14929
|
await drainOpenedPty(handle, execSessionId);
|
|
14097
14930
|
}
|
|
14098
|
-
throw new
|
|
14931
|
+
throw new HTTPException22(409, {
|
|
14099
14932
|
message: "interactive terminal did not acquire durable process authority"
|
|
14100
14933
|
});
|
|
14101
14934
|
}
|
|
@@ -14164,7 +14997,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
14164
14997
|
ptyId: req.ptyId
|
|
14165
14998
|
});
|
|
14166
14999
|
if (!pty) {
|
|
14167
|
-
throw new
|
|
15000
|
+
throw new HTTPException22(404, { message: "pty not found or closed" });
|
|
14168
15001
|
}
|
|
14169
15002
|
let seq = 1;
|
|
14170
15003
|
await withChannelA({ db, settings, bus }, ctx, async (handle) => {
|
|
@@ -14200,7 +15033,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
14200
15033
|
await emitPtyExited(ctx, req.ptyId, terminal);
|
|
14201
15034
|
return;
|
|
14202
15035
|
}
|
|
14203
|
-
throw new
|
|
15036
|
+
throw new HTTPException22(409, {
|
|
14204
15037
|
message: "pty identity changed while input was in flight; reopen the terminal"
|
|
14205
15038
|
});
|
|
14206
15039
|
}
|
|
@@ -14227,7 +15060,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
14227
15060
|
ptyId: req.ptyId
|
|
14228
15061
|
});
|
|
14229
15062
|
if (!pty) {
|
|
14230
|
-
throw new
|
|
15063
|
+
throw new HTTPException22(404, { message: "pty not found or closed" });
|
|
14231
15064
|
}
|
|
14232
15065
|
await withChannelA({ db, settings, bus }, ctx, async (handle) => {
|
|
14233
15066
|
await adoptPtyProcess(ctx, handle, pty);
|
|
@@ -14242,7 +15075,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
14242
15075
|
rows: req.rows
|
|
14243
15076
|
});
|
|
14244
15077
|
if (!updated) {
|
|
14245
|
-
throw new
|
|
15078
|
+
throw new HTTPException22(409, {
|
|
14246
15079
|
message: "pty identity changed while resize was in flight; reopen the terminal"
|
|
14247
15080
|
});
|
|
14248
15081
|
}
|
|
@@ -14267,7 +15100,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
14267
15100
|
processId: pty.retainedProcessId
|
|
14268
15101
|
});
|
|
14269
15102
|
if (!terminal || terminal.state === "active") {
|
|
14270
|
-
throw new
|
|
15103
|
+
throw new HTTPException22(409, {
|
|
14271
15104
|
message: "pty close is pending exact provider exit proof; retry"
|
|
14272
15105
|
});
|
|
14273
15106
|
}
|
|
@@ -14359,19 +15192,19 @@ function sessionAuthorizationOperationForHttp(method, pathname, sessionId) {
|
|
|
14359
15192
|
}
|
|
14360
15193
|
function sessionAuthorizationHttpError(error) {
|
|
14361
15194
|
if (error instanceof SessionAuthorizationDeniedError) {
|
|
14362
|
-
return new
|
|
15195
|
+
return new HTTPException22(404, { message: "session not found" });
|
|
14363
15196
|
}
|
|
14364
15197
|
if (error instanceof SessionAuthorizationUnavailableError) {
|
|
14365
|
-
return new
|
|
15198
|
+
return new HTTPException22(503, { message: "session authorization is unavailable" });
|
|
14366
15199
|
}
|
|
14367
|
-
if (error instanceof
|
|
15200
|
+
if (error instanceof HTTPException22) return error;
|
|
14368
15201
|
throw error;
|
|
14369
15202
|
}
|
|
14370
15203
|
function eventEnumValue(raw, schema, name, fallback) {
|
|
14371
15204
|
if (raw === void 0) return fallback;
|
|
14372
15205
|
const parsed = schema.safeParse(raw);
|
|
14373
15206
|
if (!parsed.success) {
|
|
14374
|
-
throw new
|
|
15207
|
+
throw new HTTPException22(400, { message: `${name} is invalid` });
|
|
14375
15208
|
}
|
|
14376
15209
|
return parsed.data;
|
|
14377
15210
|
}
|
|
@@ -14379,12 +15212,12 @@ function eventEnumList(raw, schema, name) {
|
|
|
14379
15212
|
if (raw === void 0 || raw.trim() === "") return [];
|
|
14380
15213
|
const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
14381
15214
|
if (values.length > 100) {
|
|
14382
|
-
throw new
|
|
15215
|
+
throw new HTTPException22(400, { message: `${name} accepts at most 100 values` });
|
|
14383
15216
|
}
|
|
14384
15217
|
return values.map((value) => {
|
|
14385
15218
|
const parsed = schema.safeParse(value);
|
|
14386
15219
|
if (!parsed.success) {
|
|
14387
|
-
throw new
|
|
15220
|
+
throw new HTTPException22(400, { message: `${name} contains an invalid value` });
|
|
14388
15221
|
}
|
|
14389
15222
|
return parsed.data;
|
|
14390
15223
|
});
|
|
@@ -14392,30 +15225,30 @@ function eventEnumList(raw, schema, name) {
|
|
|
14392
15225
|
function sessionListQuery(query, allowCursor = true) {
|
|
14393
15226
|
const parentSessionId = query.parentSessionId;
|
|
14394
15227
|
if (parentSessionId !== void 0 && parentSessionId !== "null" && !z3.string().uuid().safeParse(parentSessionId).success) {
|
|
14395
|
-
throw new
|
|
15228
|
+
throw new HTTPException22(400, {
|
|
14396
15229
|
message: 'parentSessionId must be a session id or the literal "null"'
|
|
14397
15230
|
});
|
|
14398
15231
|
}
|
|
14399
15232
|
const rawCursor = allowCursor ? query.cursor : void 0;
|
|
14400
15233
|
const cursor = rawCursor ? decodeSessionListCursor(rawCursor) : void 0;
|
|
14401
15234
|
if (rawCursor && !cursor) {
|
|
14402
|
-
throw new
|
|
15235
|
+
throw new HTTPException22(400, { message: "cursor is invalid" });
|
|
14403
15236
|
}
|
|
14404
15237
|
const search = query.search?.trim();
|
|
14405
15238
|
if (search && search.length > 200) {
|
|
14406
|
-
throw new
|
|
15239
|
+
throw new HTTPException22(400, {
|
|
14407
15240
|
message: "search must be at most 200 characters"
|
|
14408
15241
|
});
|
|
14409
15242
|
}
|
|
14410
15243
|
if (query.pinsOnly !== void 0 && query.pinsOnly !== "true") {
|
|
14411
|
-
throw new
|
|
15244
|
+
throw new HTTPException22(400, { message: 'pinsOnly must be the literal "true"' });
|
|
14412
15245
|
}
|
|
14413
15246
|
const pinsOnly = query.pinsOnly === "true";
|
|
14414
15247
|
if (pinsOnly && !allowCursor) {
|
|
14415
|
-
throw new
|
|
15248
|
+
throw new HTTPException22(400, { message: 'pinsOnly requires view="page"' });
|
|
14416
15249
|
}
|
|
14417
15250
|
if (pinsOnly && (rawCursor || parentSessionId !== void 0 || search)) {
|
|
14418
|
-
throw new
|
|
15251
|
+
throw new HTTPException22(400, {
|
|
14419
15252
|
message: "pinsOnly cannot be combined with cursor, parentSessionId, or search"
|
|
14420
15253
|
});
|
|
14421
15254
|
}
|
|
@@ -14475,7 +15308,7 @@ function sessionCreateErrorResponse(c, error) {
|
|
|
14475
15308
|
422
|
|
14476
15309
|
);
|
|
14477
15310
|
}
|
|
14478
|
-
if (error instanceof
|
|
15311
|
+
if (error instanceof HTTPException22 && error.status === 422) {
|
|
14479
15312
|
return c.json(
|
|
14480
15313
|
{
|
|
14481
15314
|
code: "SESSION_CREATE_REJECTED",
|
|
@@ -14546,7 +15379,7 @@ import {
|
|
|
14546
15379
|
listSocialConnections as listSocialConnections3,
|
|
14547
15380
|
listSocialPosts as listSocialPosts2
|
|
14548
15381
|
} from "@opengeni/db";
|
|
14549
|
-
import { HTTPException as
|
|
15382
|
+
import { HTTPException as HTTPException23 } from "hono/http-exception";
|
|
14550
15383
|
import { z as z5 } from "zod";
|
|
14551
15384
|
import { requireAccessGrant as requireAccessGrant15 } from "@opengeni/core";
|
|
14552
15385
|
function registerSocialRoutes(app, deps) {
|
|
@@ -14628,7 +15461,7 @@ function parseSince(raw) {
|
|
|
14628
15461
|
}
|
|
14629
15462
|
const since = new Date(raw);
|
|
14630
15463
|
if (Number.isNaN(since.getTime())) {
|
|
14631
|
-
throw new
|
|
15464
|
+
throw new HTTPException23(422, { message: "since must be an ISO date-time" });
|
|
14632
15465
|
}
|
|
14633
15466
|
return since;
|
|
14634
15467
|
}
|
|
@@ -14639,7 +15472,7 @@ function parseConnectionIds(raw) {
|
|
|
14639
15472
|
const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
14640
15473
|
const parsed = z5.array(z5.string().uuid()).safeParse(values);
|
|
14641
15474
|
if (!parsed.success) {
|
|
14642
|
-
throw new
|
|
15475
|
+
throw new HTTPException23(422, {
|
|
14643
15476
|
message: "connectionIds must be a comma-separated list of UUIDs"
|
|
14644
15477
|
});
|
|
14645
15478
|
}
|
|
@@ -14649,12 +15482,12 @@ function parseConnectionIds(raw) {
|
|
|
14649
15482
|
function socialHttpException(error) {
|
|
14650
15483
|
const message = error instanceof Error ? error.message : String(error);
|
|
14651
15484
|
if (message.includes("not found")) {
|
|
14652
|
-
return new
|
|
15485
|
+
return new HTTPException23(404, { message });
|
|
14653
15486
|
}
|
|
14654
15487
|
if (message.includes("duplicate key")) {
|
|
14655
|
-
return new
|
|
15488
|
+
return new HTTPException23(409, { message: "social connection or post already exists" });
|
|
14656
15489
|
}
|
|
14657
|
-
return new
|
|
15490
|
+
return new HTTPException23(500, { message });
|
|
14658
15491
|
}
|
|
14659
15492
|
|
|
14660
15493
|
// src/routes/workspaces.ts
|
|
@@ -14697,7 +15530,7 @@ import {
|
|
|
14697
15530
|
workspaceCodexSubscriptionActive
|
|
14698
15531
|
} from "@opengeni/db";
|
|
14699
15532
|
import { boundWorkspaceControlHttpPage } from "@opengeni/events";
|
|
14700
|
-
import { HTTPException as
|
|
15533
|
+
import { HTTPException as HTTPException24 } from "hono/http-exception";
|
|
14701
15534
|
import { hasPermission as hasPermission6, requireAccessContext as requireAccessContext2, requireAccessGrant as requireAccessGrant16 } from "@opengeni/core";
|
|
14702
15535
|
import { requireLimit as requireLimit7 } from "@opengeni/core";
|
|
14703
15536
|
import {
|
|
@@ -14965,7 +15798,7 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
14965
15798
|
const payload = CreateWorkspaceRequest.parse(await c.req.json());
|
|
14966
15799
|
const accountId = payload.accountId ?? context.defaultAccountId;
|
|
14967
15800
|
if (!accountId) {
|
|
14968
|
-
throw new
|
|
15801
|
+
throw new HTTPException24(409, { message: "account selection is required" });
|
|
14969
15802
|
}
|
|
14970
15803
|
requireAccountPermission(context, accountId, "workspace:create");
|
|
14971
15804
|
await requireLimit7(deps, { accountId, action: "workspace:create", quantity: 1 });
|
|
@@ -15008,7 +15841,7 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
15008
15841
|
await requireAccessGrant16(c, deps, workspaceId, "workspace:admin");
|
|
15009
15842
|
const parsed = UpdateWorkspaceSettingsRequest.safeParse(await c.req.json());
|
|
15010
15843
|
if (!parsed.success) {
|
|
15011
|
-
throw new
|
|
15844
|
+
throw new HTTPException24(400, { message: "invalid workspace settings patch" });
|
|
15012
15845
|
}
|
|
15013
15846
|
const workspace = await updateWorkspaceSettings(deps.db, workspaceId, parsed.data);
|
|
15014
15847
|
return c.json(Workspace.parse(workspace));
|
|
@@ -15056,11 +15889,11 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
15056
15889
|
const workspaceId = c.req.param("workspaceId");
|
|
15057
15890
|
const grant = await requireAccessGrant16(c, deps, workspaceId, "workspace:admin");
|
|
15058
15891
|
if (workspaceControlUtf8Bytes2(grant.subjectId) > WORKSPACE_CONTROL_ACTOR_MAX_BYTES2) {
|
|
15059
|
-
throw new
|
|
15892
|
+
throw new HTTPException24(400, { message: "workspace-control actor is too large" });
|
|
15060
15893
|
}
|
|
15061
15894
|
const parsed = WorkspaceInferenceControlRequest.safeParse(await c.req.json().catch(() => null));
|
|
15062
15895
|
if (!parsed.success) {
|
|
15063
|
-
throw new
|
|
15896
|
+
throw new HTTPException24(400, { message: "invalid workspace inference-control request" });
|
|
15064
15897
|
}
|
|
15065
15898
|
return c.json(
|
|
15066
15899
|
await controlHumanWorkspace(
|
|
@@ -15106,7 +15939,7 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
15106
15939
|
if (payload.rigId) {
|
|
15107
15940
|
const rig = await getRig(deps.db, workspaceId, payload.rigId);
|
|
15108
15941
|
if (!rig) {
|
|
15109
|
-
throw new
|
|
15942
|
+
throw new HTTPException24(422, { message: `unknown rigId: ${payload.rigId}` });
|
|
15110
15943
|
}
|
|
15111
15944
|
}
|
|
15112
15945
|
const workspace = await setWorkspaceDefaultRig(deps.db, workspaceId, payload.rigId);
|
|
@@ -15152,7 +15985,7 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
15152
15985
|
const members = await listWorkspaceMembers(deps.db, workspaceId);
|
|
15153
15986
|
const member = members.find((candidate) => candidate.subjectId === subjectId);
|
|
15154
15987
|
if (!member) {
|
|
15155
|
-
throw new
|
|
15988
|
+
throw new HTTPException24(500, { message: "failed to add member" });
|
|
15156
15989
|
}
|
|
15157
15990
|
return c.json(WorkspaceMember.parse(member), 201);
|
|
15158
15991
|
});
|
|
@@ -15164,7 +15997,7 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
15164
15997
|
const existing = await listWorkspaceMembers(deps.db, workspaceId);
|
|
15165
15998
|
const current = existing.find((member2) => member2.subjectId === subjectId);
|
|
15166
15999
|
if (!current) {
|
|
15167
|
-
throw new
|
|
16000
|
+
throw new HTTPException24(404, { message: "member not found" });
|
|
15168
16001
|
}
|
|
15169
16002
|
await grantWorkspaceAccess(deps.db, {
|
|
15170
16003
|
accountId: grant.accountId,
|
|
@@ -15177,7 +16010,7 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
15177
16010
|
const members = await listWorkspaceMembers(deps.db, workspaceId);
|
|
15178
16011
|
const member = members.find((candidate) => candidate.subjectId === subjectId);
|
|
15179
16012
|
if (!member) {
|
|
15180
|
-
throw new
|
|
16013
|
+
throw new HTTPException24(500, { message: "failed to update member" });
|
|
15181
16014
|
}
|
|
15182
16015
|
return c.json(WorkspaceMember.parse(member));
|
|
15183
16016
|
});
|
|
@@ -15201,10 +16034,241 @@ function normalizeAgentInstructions(value) {
|
|
|
15201
16034
|
function requireAccountPermission(context, accountId, permission) {
|
|
15202
16035
|
const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId);
|
|
15203
16036
|
if (!grant || !grant.permissions.includes(permission) && !grant.permissions.includes("account:admin")) {
|
|
15204
|
-
throw new
|
|
16037
|
+
throw new HTTPException24(403, { message: `missing permission: ${permission}` });
|
|
15205
16038
|
}
|
|
15206
16039
|
}
|
|
15207
16040
|
|
|
16041
|
+
// src/routes/workspace-instruction-policies.ts
|
|
16042
|
+
import {
|
|
16043
|
+
ActivateWorkspaceInstructionPolicyRequest,
|
|
16044
|
+
CreateWorkspaceInstructionPolicyDraftRequest,
|
|
16045
|
+
ImportLegacyWorkspaceInstructionPolicyDraftRequest,
|
|
16046
|
+
RollbackWorkspaceInstructionPolicyRequest,
|
|
16047
|
+
WorkspaceInstructionPolicyActivationResponse,
|
|
16048
|
+
WorkspaceInstructionPolicyConflictResponse,
|
|
16049
|
+
WorkspaceInstructionPolicyDiffRequest,
|
|
16050
|
+
WorkspaceInstructionPolicyDiffResponse,
|
|
16051
|
+
WorkspaceInstructionPolicyListQuery,
|
|
16052
|
+
WorkspaceInstructionPolicyListResponse,
|
|
16053
|
+
WorkspaceInstructionPolicyRevision
|
|
16054
|
+
} from "@opengeni/contracts";
|
|
16055
|
+
import { requireAccessGrant as requireAccessGrant17 } from "@opengeni/core";
|
|
16056
|
+
import {
|
|
16057
|
+
activateWorkspaceInstructionPolicyRevision,
|
|
16058
|
+
createWorkspaceInstructionPolicyDraft,
|
|
16059
|
+
diffWorkspaceInstructionPolicyRevisions,
|
|
16060
|
+
getWorkspaceInstructionPolicyRevision,
|
|
16061
|
+
importLegacyWorkspaceInstructionPolicyDraft,
|
|
16062
|
+
listWorkspaceInstructionPolicyRevisions,
|
|
16063
|
+
rollbackWorkspaceInstructionPolicyRevision,
|
|
16064
|
+
WorkspaceInstructionPolicyConflictError,
|
|
16065
|
+
WorkspaceInstructionPolicyInvalidOperationError,
|
|
16066
|
+
WorkspaceInstructionPolicyLegacyUnavailableError,
|
|
16067
|
+
WorkspaceInstructionPolicyNotFoundError
|
|
16068
|
+
} from "@opengeni/db";
|
|
16069
|
+
import { HTTPException as HTTPException25 } from "hono/http-exception";
|
|
16070
|
+
import { z as z6 } from "zod";
|
|
16071
|
+
var WorkspaceInstructionPolicyRevisionId = z6.string().uuid();
|
|
16072
|
+
async function parseBody(context, schema) {
|
|
16073
|
+
const parsed = schema.safeParse(await context.req.json().catch(() => null));
|
|
16074
|
+
if (!parsed.success) {
|
|
16075
|
+
throw new HTTPException25(422, { message: "Invalid workspace instruction-policy request" });
|
|
16076
|
+
}
|
|
16077
|
+
return parsed.data;
|
|
16078
|
+
}
|
|
16079
|
+
function policyErrorResponse(context, error) {
|
|
16080
|
+
if (error instanceof WorkspaceInstructionPolicyConflictError) {
|
|
16081
|
+
return context.json(
|
|
16082
|
+
WorkspaceInstructionPolicyConflictResponse.parse({
|
|
16083
|
+
code: error.code,
|
|
16084
|
+
message: error.message,
|
|
16085
|
+
currentHead: error.currentHead
|
|
16086
|
+
}),
|
|
16087
|
+
409
|
|
16088
|
+
);
|
|
16089
|
+
}
|
|
16090
|
+
if (error instanceof WorkspaceInstructionPolicyNotFoundError) {
|
|
16091
|
+
return context.json(
|
|
16092
|
+
{ code: "WORKSPACE_INSTRUCTION_POLICY_NOT_FOUND", message: error.message },
|
|
16093
|
+
404
|
|
16094
|
+
);
|
|
16095
|
+
}
|
|
16096
|
+
if (error instanceof WorkspaceInstructionPolicyLegacyUnavailableError) {
|
|
16097
|
+
return context.json(
|
|
16098
|
+
{ code: "WORKSPACE_INSTRUCTION_POLICY_LEGACY_UNAVAILABLE", message: error.message },
|
|
16099
|
+
409
|
|
16100
|
+
);
|
|
16101
|
+
}
|
|
16102
|
+
if (error instanceof WorkspaceInstructionPolicyInvalidOperationError) {
|
|
16103
|
+
return context.json(
|
|
16104
|
+
{ code: "INVALID_WORKSPACE_INSTRUCTION_POLICY_OPERATION", message: error.message },
|
|
16105
|
+
422
|
|
16106
|
+
);
|
|
16107
|
+
}
|
|
16108
|
+
throw error;
|
|
16109
|
+
}
|
|
16110
|
+
function assertBoundedActor(subjectId) {
|
|
16111
|
+
if (subjectId.trim().length < 1 || subjectId.length > 1024) {
|
|
16112
|
+
throw new HTTPException25(400, { message: "Workspace instruction-policy actor is invalid" });
|
|
16113
|
+
}
|
|
16114
|
+
}
|
|
16115
|
+
function parseRevisionId(context) {
|
|
16116
|
+
const parsed = WorkspaceInstructionPolicyRevisionId.safeParse(context.req.param("revisionId"));
|
|
16117
|
+
if (!parsed.success) {
|
|
16118
|
+
throw new HTTPException25(422, { message: "Invalid workspace instruction-policy revision id" });
|
|
16119
|
+
}
|
|
16120
|
+
return parsed.data;
|
|
16121
|
+
}
|
|
16122
|
+
function registerWorkspaceInstructionPolicyRoutes(app, deps) {
|
|
16123
|
+
const base = "/v1/workspaces/:workspaceId/instruction-policies";
|
|
16124
|
+
app.get(base, async (context) => {
|
|
16125
|
+
const workspaceId = context.req.param("workspaceId");
|
|
16126
|
+
await requireAccessGrant17(context, deps, workspaceId, "workspace:read");
|
|
16127
|
+
const parsed = WorkspaceInstructionPolicyListQuery.safeParse({
|
|
16128
|
+
kind: context.req.query("kind"),
|
|
16129
|
+
scope: context.req.query("scope"),
|
|
16130
|
+
roleKey: context.req.query("roleKey"),
|
|
16131
|
+
afterRevision: context.req.query("afterRevision"),
|
|
16132
|
+
limit: context.req.query("limit")
|
|
16133
|
+
});
|
|
16134
|
+
if (!parsed.success) {
|
|
16135
|
+
throw new HTTPException25(422, { message: "Invalid workspace instruction-policy query" });
|
|
16136
|
+
}
|
|
16137
|
+
return context.json(
|
|
16138
|
+
WorkspaceInstructionPolicyListResponse.parse(
|
|
16139
|
+
await listWorkspaceInstructionPolicyRevisions(deps.db, workspaceId, parsed.data)
|
|
16140
|
+
)
|
|
16141
|
+
);
|
|
16142
|
+
});
|
|
16143
|
+
app.post(`${base}/drafts`, async (context) => {
|
|
16144
|
+
const workspaceId = context.req.param("workspaceId");
|
|
16145
|
+
const grant = await requireAccessGrant17(context, deps, workspaceId, "workspace:admin");
|
|
16146
|
+
assertBoundedActor(grant.subjectId);
|
|
16147
|
+
const request = await parseBody(context, CreateWorkspaceInstructionPolicyDraftRequest);
|
|
16148
|
+
try {
|
|
16149
|
+
return context.json(
|
|
16150
|
+
WorkspaceInstructionPolicyRevision.parse(
|
|
16151
|
+
await createWorkspaceInstructionPolicyDraft(deps.db, {
|
|
16152
|
+
accountId: grant.accountId,
|
|
16153
|
+
workspaceId,
|
|
16154
|
+
createdBySubjectId: grant.subjectId,
|
|
16155
|
+
kind: request.kind,
|
|
16156
|
+
scope: request.scope,
|
|
16157
|
+
roleKey: request.roleKey,
|
|
16158
|
+
content: request.content,
|
|
16159
|
+
provenanceSource: request.provenanceSource,
|
|
16160
|
+
provenanceSourceId: request.provenanceSourceId,
|
|
16161
|
+
supersedesRevisionId: request.supersedesRevisionId
|
|
16162
|
+
})
|
|
16163
|
+
),
|
|
16164
|
+
201
|
|
16165
|
+
);
|
|
16166
|
+
} catch (error) {
|
|
16167
|
+
return policyErrorResponse(context, error);
|
|
16168
|
+
}
|
|
16169
|
+
});
|
|
16170
|
+
app.post(`${base}/import-legacy`, async (context) => {
|
|
16171
|
+
const workspaceId = context.req.param("workspaceId");
|
|
16172
|
+
const grant = await requireAccessGrant17(context, deps, workspaceId, "workspace:admin");
|
|
16173
|
+
assertBoundedActor(grant.subjectId);
|
|
16174
|
+
const request = await parseBody(context, ImportLegacyWorkspaceInstructionPolicyDraftRequest);
|
|
16175
|
+
try {
|
|
16176
|
+
return context.json(
|
|
16177
|
+
WorkspaceInstructionPolicyRevision.parse(
|
|
16178
|
+
await importLegacyWorkspaceInstructionPolicyDraft(deps.db, {
|
|
16179
|
+
accountId: grant.accountId,
|
|
16180
|
+
workspaceId,
|
|
16181
|
+
createdBySubjectId: grant.subjectId,
|
|
16182
|
+
supersedesRevisionId: request.supersedesRevisionId
|
|
16183
|
+
})
|
|
16184
|
+
),
|
|
16185
|
+
201
|
|
16186
|
+
);
|
|
16187
|
+
} catch (error) {
|
|
16188
|
+
return policyErrorResponse(context, error);
|
|
16189
|
+
}
|
|
16190
|
+
});
|
|
16191
|
+
app.get(`${base}/diff`, async (context) => {
|
|
16192
|
+
const workspaceId = context.req.param("workspaceId");
|
|
16193
|
+
await requireAccessGrant17(context, deps, workspaceId, "workspace:read");
|
|
16194
|
+
const parsed = WorkspaceInstructionPolicyDiffRequest.safeParse({
|
|
16195
|
+
fromRevisionId: context.req.query("fromRevisionId"),
|
|
16196
|
+
toRevisionId: context.req.query("toRevisionId")
|
|
16197
|
+
});
|
|
16198
|
+
if (!parsed.success) {
|
|
16199
|
+
throw new HTTPException25(422, { message: "Invalid workspace instruction-policy diff query" });
|
|
16200
|
+
}
|
|
16201
|
+
try {
|
|
16202
|
+
return context.json(
|
|
16203
|
+
WorkspaceInstructionPolicyDiffResponse.parse(
|
|
16204
|
+
await diffWorkspaceInstructionPolicyRevisions(deps.db, workspaceId, parsed.data)
|
|
16205
|
+
)
|
|
16206
|
+
);
|
|
16207
|
+
} catch (error) {
|
|
16208
|
+
return policyErrorResponse(context, error);
|
|
16209
|
+
}
|
|
16210
|
+
});
|
|
16211
|
+
app.post(`${base}/rollback`, async (context) => {
|
|
16212
|
+
const workspaceId = context.req.param("workspaceId");
|
|
16213
|
+
const grant = await requireAccessGrant17(context, deps, workspaceId, "workspace:admin");
|
|
16214
|
+
assertBoundedActor(grant.subjectId);
|
|
16215
|
+
const request = await parseBody(context, RollbackWorkspaceInstructionPolicyRequest);
|
|
16216
|
+
try {
|
|
16217
|
+
return context.json(
|
|
16218
|
+
WorkspaceInstructionPolicyActivationResponse.parse(
|
|
16219
|
+
await rollbackWorkspaceInstructionPolicyRevision(deps.db, {
|
|
16220
|
+
accountId: grant.accountId,
|
|
16221
|
+
workspaceId,
|
|
16222
|
+
targetRevisionId: request.targetRevisionId,
|
|
16223
|
+
expectedCurrentRevisionId: request.expectedCurrentRevisionId,
|
|
16224
|
+
actorSubjectId: grant.subjectId,
|
|
16225
|
+
reason: request.reason
|
|
16226
|
+
})
|
|
16227
|
+
)
|
|
16228
|
+
);
|
|
16229
|
+
} catch (error) {
|
|
16230
|
+
return policyErrorResponse(context, error);
|
|
16231
|
+
}
|
|
16232
|
+
});
|
|
16233
|
+
app.get(`${base}/:revisionId`, async (context) => {
|
|
16234
|
+
const workspaceId = context.req.param("workspaceId");
|
|
16235
|
+
await requireAccessGrant17(context, deps, workspaceId, "workspace:read");
|
|
16236
|
+
const revisionId = parseRevisionId(context);
|
|
16237
|
+
try {
|
|
16238
|
+
return context.json(
|
|
16239
|
+
WorkspaceInstructionPolicyRevision.parse(
|
|
16240
|
+
await getWorkspaceInstructionPolicyRevision(deps.db, workspaceId, revisionId)
|
|
16241
|
+
)
|
|
16242
|
+
);
|
|
16243
|
+
} catch (error) {
|
|
16244
|
+
return policyErrorResponse(context, error);
|
|
16245
|
+
}
|
|
16246
|
+
});
|
|
16247
|
+
app.post(`${base}/:revisionId/activate`, async (context) => {
|
|
16248
|
+
const workspaceId = context.req.param("workspaceId");
|
|
16249
|
+
const grant = await requireAccessGrant17(context, deps, workspaceId, "workspace:admin");
|
|
16250
|
+
assertBoundedActor(grant.subjectId);
|
|
16251
|
+
const revisionId = parseRevisionId(context);
|
|
16252
|
+
const request = await parseBody(context, ActivateWorkspaceInstructionPolicyRequest);
|
|
16253
|
+
try {
|
|
16254
|
+
return context.json(
|
|
16255
|
+
WorkspaceInstructionPolicyActivationResponse.parse(
|
|
16256
|
+
await activateWorkspaceInstructionPolicyRevision(deps.db, {
|
|
16257
|
+
accountId: grant.accountId,
|
|
16258
|
+
workspaceId,
|
|
16259
|
+
revisionId,
|
|
16260
|
+
expectedCurrentRevisionId: request.expectedCurrentRevisionId,
|
|
16261
|
+
actorSubjectId: grant.subjectId,
|
|
16262
|
+
reason: request.reason
|
|
16263
|
+
})
|
|
16264
|
+
)
|
|
16265
|
+
);
|
|
16266
|
+
} catch (error) {
|
|
16267
|
+
return policyErrorResponse(context, error);
|
|
16268
|
+
}
|
|
16269
|
+
});
|
|
16270
|
+
}
|
|
16271
|
+
|
|
15208
16272
|
// src/app.ts
|
|
15209
16273
|
import {
|
|
15210
16274
|
mergeResourceRefs,
|
|
@@ -15235,7 +16299,7 @@ function createApp(deps) {
|
|
|
15235
16299
|
documentId
|
|
15236
16300
|
}) => {
|
|
15237
16301
|
if (!objectStorage) {
|
|
15238
|
-
throw new
|
|
16302
|
+
throw new HTTPException26(503, {
|
|
15239
16303
|
message: "object storage is not configured"
|
|
15240
16304
|
});
|
|
15241
16305
|
}
|
|
@@ -15412,6 +16476,11 @@ function createApp(deps) {
|
|
|
15412
16476
|
const result = await runReadinessChecks(readinessChecks(deps), 2e3);
|
|
15413
16477
|
return c.json(result, result.ok ? 200 : 503);
|
|
15414
16478
|
});
|
|
16479
|
+
app.get("/traffic-readyz", async (c) => {
|
|
16480
|
+
const { db } = readinessChecks(deps);
|
|
16481
|
+
const result = await runReadinessChecks({ db }, 2e3);
|
|
16482
|
+
return c.json(result, result.ok ? 200 : 503);
|
|
16483
|
+
});
|
|
15415
16484
|
app.get(
|
|
15416
16485
|
"/metrics",
|
|
15417
16486
|
async (c) => c.text(await observability.prometheusMetrics(), 200, {
|
|
@@ -15458,7 +16527,7 @@ function createApp(deps) {
|
|
|
15458
16527
|
boundedRequest = await boundedMcpRequest(c.req.raw);
|
|
15459
16528
|
} catch (error) {
|
|
15460
16529
|
if (error instanceof McpPayloadTooLargeError2) {
|
|
15461
|
-
throw new
|
|
16530
|
+
throw new HTTPException26(413, { message: "MCP request body exceeds the safety limit" });
|
|
15462
16531
|
}
|
|
15463
16532
|
throw error;
|
|
15464
16533
|
}
|
|
@@ -15467,7 +16536,7 @@ function createApp(deps) {
|
|
|
15467
16536
|
const boundSessionId = grant.metadata?.sessionId;
|
|
15468
16537
|
if (toolspaceGrant || typeof boundSessionId === "string") {
|
|
15469
16538
|
if (typeof boundSessionId !== "string") {
|
|
15470
|
-
throw new
|
|
16539
|
+
throw new HTTPException26(404, { message: "session not found" });
|
|
15471
16540
|
}
|
|
15472
16541
|
try {
|
|
15473
16542
|
await requireSessionAuthorization3(routeDeps, grant, {
|
|
@@ -15477,10 +16546,10 @@ function createApp(deps) {
|
|
|
15477
16546
|
});
|
|
15478
16547
|
} catch (error) {
|
|
15479
16548
|
if (error instanceof SessionAuthorizationDeniedError2) {
|
|
15480
|
-
throw new
|
|
16549
|
+
throw new HTTPException26(404, { message: "session not found" });
|
|
15481
16550
|
}
|
|
15482
16551
|
if (error instanceof SessionAuthorizationUnavailableError2) {
|
|
15483
|
-
throw new
|
|
16552
|
+
throw new HTTPException26(503, { message: "session authorization is unavailable" });
|
|
15484
16553
|
}
|
|
15485
16554
|
throw error;
|
|
15486
16555
|
}
|
|
@@ -15491,7 +16560,7 @@ function createApp(deps) {
|
|
|
15491
16560
|
toolspace = await prepareToolspaceMcpSurface({ deps: routeDeps, grant });
|
|
15492
16561
|
} catch (error) {
|
|
15493
16562
|
if (error instanceof McpPayloadTooLargeError2) {
|
|
15494
|
-
throw new
|
|
16563
|
+
throw new HTTPException26(413, { message: "MCP tool list exceeds the safety limit" });
|
|
15495
16564
|
}
|
|
15496
16565
|
throw error;
|
|
15497
16566
|
}
|
|
@@ -15520,6 +16589,7 @@ function createApp(deps) {
|
|
|
15520
16589
|
registerGitHubRoutes(app, routeDeps);
|
|
15521
16590
|
registerInstallRoutes(app, routeDeps);
|
|
15522
16591
|
registerWorkspaceRoutes(app, routeDeps);
|
|
16592
|
+
registerWorkspaceInstructionPolicyRoutes(app, routeDeps);
|
|
15523
16593
|
registerSocialRoutes(app, routeDeps);
|
|
15524
16594
|
registerConnectionRoutes(app, routeDeps);
|
|
15525
16595
|
registerCapabilityRoutes(app, routeDeps);
|
|
@@ -15570,7 +16640,7 @@ function createApp(deps) {
|
|
|
15570
16640
|
return app;
|
|
15571
16641
|
}
|
|
15572
16642
|
async function requireMcpAccessGrant(c, deps, workspaceId) {
|
|
15573
|
-
const grant = await
|
|
16643
|
+
const grant = await requireAccessGrant18(c, deps, workspaceId);
|
|
15574
16644
|
if (hasPermission7(grant.permissions, "workspace:read")) {
|
|
15575
16645
|
return grant;
|
|
15576
16646
|
}
|
|
@@ -15607,7 +16677,7 @@ function allowedCorsOrigin(pattern, origin) {
|
|
|
15607
16677
|
return new RegExp(`^(?:${pattern})$`).test(origin);
|
|
15608
16678
|
}
|
|
15609
16679
|
function httpStatusForError(error) {
|
|
15610
|
-
if (error instanceof
|
|
16680
|
+
if (error instanceof HTTPException26) {
|
|
15611
16681
|
return error.status;
|
|
15612
16682
|
}
|
|
15613
16683
|
if (error instanceof McpPayloadTooLargeError2) {
|
|
@@ -15635,7 +16705,7 @@ function publicErrorMessage(error, status) {
|
|
|
15635
16705
|
if (status >= 500) {
|
|
15636
16706
|
return "OpenGeni could not complete the request.";
|
|
15637
16707
|
}
|
|
15638
|
-
if (error instanceof
|
|
16708
|
+
if (error instanceof HTTPException26) {
|
|
15639
16709
|
return boundedPublicMessage(error.message) ?? "Request failed.";
|
|
15640
16710
|
}
|
|
15641
16711
|
if (error instanceof McpPayloadTooLargeError2) {
|
|
@@ -15671,22 +16741,20 @@ function readinessChecks(deps) {
|
|
|
15671
16741
|
}
|
|
15672
16742
|
async function runReadinessChecks(checks, timeoutMs) {
|
|
15673
16743
|
const entries = await Promise.all(
|
|
15674
|
-
Object.entries(checks).map(
|
|
15675
|
-
|
|
15676
|
-
|
|
15677
|
-
|
|
15678
|
-
|
|
15679
|
-
|
|
15680
|
-
|
|
15681
|
-
|
|
15682
|
-
|
|
15683
|
-
|
|
15684
|
-
|
|
15685
|
-
|
|
15686
|
-
];
|
|
15687
|
-
}
|
|
16744
|
+
Object.entries(checks).map(async ([name, check]) => {
|
|
16745
|
+
try {
|
|
16746
|
+
await withTimeout(Promise.resolve().then(check), timeoutMs);
|
|
16747
|
+
return [name, { ok: true }];
|
|
16748
|
+
} catch (error) {
|
|
16749
|
+
return [
|
|
16750
|
+
name,
|
|
16751
|
+
{
|
|
16752
|
+
ok: false,
|
|
16753
|
+
error: error instanceof Error ? error.message : String(error)
|
|
16754
|
+
}
|
|
16755
|
+
];
|
|
15688
16756
|
}
|
|
15689
|
-
)
|
|
16757
|
+
})
|
|
15690
16758
|
);
|
|
15691
16759
|
const result = Object.fromEntries(entries);
|
|
15692
16760
|
return {
|
|
@@ -15715,6 +16783,7 @@ async function withTimeout(promise, timeoutMs) {
|
|
|
15715
16783
|
var routeLabelPatterns = [
|
|
15716
16784
|
{ pattern: /^\/healthz$/, label: "/healthz" },
|
|
15717
16785
|
{ pattern: /^\/readyz$/, label: "/readyz" },
|
|
16786
|
+
{ pattern: /^\/traffic-readyz$/, label: "/traffic-readyz" },
|
|
15718
16787
|
{
|
|
15719
16788
|
pattern: /^\/v1\/workspaces\/[^/]+\/codex\/connect\/start$/,
|
|
15720
16789
|
label: "/v1/workspaces/:workspaceId/codex/connect/start"
|
|
@@ -16009,6 +17078,10 @@ var routeLabelPatterns = [
|
|
|
16009
17078
|
pattern: /^\/v1\/workspaces\/[^/]+\/connections\/oauth\/start$/,
|
|
16010
17079
|
label: "/v1/workspaces/:workspaceId/connections/oauth/start"
|
|
16011
17080
|
},
|
|
17081
|
+
{
|
|
17082
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/connections\/slack-bot$/,
|
|
17083
|
+
label: "/v1/workspaces/:workspaceId/connections/slack-bot"
|
|
17084
|
+
},
|
|
16012
17085
|
{
|
|
16013
17086
|
pattern: /^\/v1\/workspaces\/[^/]+\/connections\/[^/]+$/,
|
|
16014
17087
|
label: "/v1/workspaces/:workspaceId/connections/:connectionId"
|
|
@@ -16106,4 +17179,4 @@ export {
|
|
|
16106
17179
|
withDefaultEnabledCapabilityMcpTools,
|
|
16107
17180
|
workflowIdForSession2 as workflowIdForSession
|
|
16108
17181
|
};
|
|
16109
|
-
//# sourceMappingURL=chunk-
|
|
17182
|
+
//# sourceMappingURL=chunk-S2N4252E.js.map
|