@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.
@@ -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 HTTPException24 } from "hono/http-exception";
27
+ import { HTTPException as HTTPException26 } from "hono/http-exception";
28
28
  import {
29
29
  hasPermission as hasPermission7,
30
- requireAccessGrant as requireAccessGrant17,
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 getSession(deps.db, grant.workspaceId, sessionId);
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 = environmentsEncryptionKeyBytes(settings);
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
- }) : buildConnectionTokenResolver(input.deps.db, input.deps.settings);
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 HTTPException2 } from "hono/http-exception";
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 HTTPException2(400, { message: "invalid asset name" });
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 HTTPException2(400, { message: "invalid version or asset name" });
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 getSession2 } from "@opengeni/db";
5024
- import { HTTPException as HTTPException3 } from "hono/http-exception";
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 getSession2(db, workspaceId, sessionId)) {
5034
- throw new HTTPException3(404, { message: "session not found" });
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 HTTPException4 } from "hono/http-exception";
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 HTTPException4(404, { message: "integrations are not enabled for this deployment" });
5745
+ throw new HTTPException5(404, { message: "integrations are not enabled for this deployment" });
5124
5746
  }
5125
5747
  if (!objectStorage) {
5126
- throw new HTTPException4(404, { message: "asset not found" });
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 HTTPException4(404, { message: "asset not found" });
5752
+ throw new HTTPException5(404, { message: "asset not found" });
5131
5753
  }
5132
5754
  const contentType = contentTypeForKey(key);
5133
5755
  if (!contentType) {
5134
- throw new HTTPException4(404, { message: "asset not found" });
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 HTTPException4(404, { message: "asset not found" });
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 environmentsEncryptionKeyBytes2 } from "@opengeni/config";
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 HTTPException5 } from "hono/http-exception";
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 HTTPException5(403, {
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 HTTPException5(503, {
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 HTTPException5(403, {
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 HTTPException5(403, {
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 HTTPException5(403, {
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 HTTPException5(403, {
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 HTTPException5(401, {
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 HTTPException5(403, {
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 HTTPException5(502, {
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 HTTPException5(400, {
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 HTTPException5(502, {
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 HTTPException5(502, {
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 = environmentsEncryptionKeyBytes2(settings);
6302
+ const key = environmentsEncryptionKeyBytes3(settings);
5681
6303
  if (!key) {
5682
- throw new HTTPException5(500, {
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 HTTPException5(409, {
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 HTTPException5(404, { message: "codex account not found" });
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 HTTPException5(400, { message: "no settings to update" });
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 HTTPException5(404, {
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 HTTPException5(404, { message: "codex account not found" });
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 HTTPException5(404, { message: "codex account not found" });
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 HTTPException5(400, {
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 HTTPException5(404, { message: "codex account not found" });
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 HTTPException5(409, {
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 HTTPException5(409, {
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 HTTPException5(404, {
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 HTTPException5(404, { message: "codex account not found" });
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 HTTPException5(400, {
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 HTTPException5(404, { message: "codex account not found" });
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 HTTPException5(403, {
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 HTTPException5(409, {
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 HTTPException5(409, {
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 HTTPException5(409, { message: "redemption recovery state changed" });
6747
+ throw new HTTPException6(409, { message: "redemption recovery state changed" });
6126
6748
  }
6127
6749
  if (adoption.kind === "forbidden") {
6128
- throw new HTTPException5(403, { message: "redemption owner is unavailable" });
6750
+ throw new HTTPException6(403, { message: "redemption owner is unavailable" });
6129
6751
  }
6130
6752
  if (adoption.kind === "conflict") {
6131
- throw new HTTPException5(409, {
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 HTTPException5(403, { message: "redemption credential is unavailable" });
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 HTTPException5(503, {
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 HTTPException5(400, {
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 HTTPException5(503, {
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 HTTPException5(403, {
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 HTTPException5(404, { message: "codex account not found" });
6829
+ throw new HTTPException6(404, { message: "codex account not found" });
6208
6830
  }
6209
6831
  if (claimed.kind === "forbidden") {
6210
- throw new HTTPException5(403, {
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 HTTPException5(409, {
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 { requireAccessGrant as requireAccessGrant3, requireEnvironmentEncryption as requireEnvironmentEncryption2 } from "@opengeni/core";
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 HTTPException8 } from "hono/http-exception";
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 HTTPException7 } from "hono/http-exception";
7051
+ import { HTTPException as HTTPException8 } from "hono/http-exception";
6422
7052
 
6423
7053
  // src/integrations/provider-domain.ts
6424
- import { HTTPException as HTTPException6 } from "hono/http-exception";
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 HTTPException6(400, { message: "providerDomain must not be empty" });
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 HTTPException7(404, { message: "connection not found" });
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 HTTPException7(400, { message: "OAuth state has already been used" });
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 HTTPException7(409, {
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 HTTPException7(503, {
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 HTTPException7(422, {
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 HTTPException7(422, {
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 HTTPException7) {
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 HTTPException7(422, { message: "could not discover MCP protected resource metadata" });
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 HTTPException7) {
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 HTTPException7(422, {
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 HTTPException7(422, {
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 HTTPException7(422, {
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 HTTPException7(422, {
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 HTTPException7(422, {
7556
+ throw new HTTPException8(422, {
6927
7557
  message: `dynamic client registration failed with HTTP ${response.status}`
6928
7558
  });
6929
7559
  }
6930
- const payload = await readResponseJsonBounded(
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 HTTPException7(422, {
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 HTTPException7(400, { message: "invalid or expired OAuth state" });
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 HTTPException7(400, { message: "invalid or expired OAuth state" });
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 HTTPException7(400, { message: "OAuth client registration is no longer available" });
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 HTTPException7(400, {
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 readResponseJsonBounded(
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 HTTPException7) {
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 readResponseJsonBounded(
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 HTTPException7(400, { message: "mcpUrl is required" });
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 HTTPException7(422, { message: "MCP resource URL is invalid" });
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 HTTPException7(422, {
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 HTTPException7(422, {
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 HTTPException7(422, { message: error.message });
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 HTTPException7(400, { message: "OAuth returnPath must be a relative path" });
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 HTTPException7(400, { message: "OAuth returnPath must be a relative path" });
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 readResponseJsonBounded(
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 HTTPException7(422, { message: error.message });
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 HTTPException7(422, {
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 HTTPException7(422, { message: "OAuth fetch exceeded maximum redirect hops" });
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 HTTPException7(422, { message: "OAuth fetch redirect was missing Location" });
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 HTTPException7(422, { message: "OAuth fetch redirect Location was invalid" });
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 HTTPException7(400, { message: "invalid OAuth state" });
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 HTTPException7(400, { message: `invalid OAuth state: missing ${field}` });
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 HTTPException8(404, { message: "integrations are not enabled for this deployment" });
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 HTTPException8(404, { message: "connection not found" });
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 HTTPException8(400, {
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 HTTPException8(400, {
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 HTTPException8(404, { message: "connection not found" });
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 HTTPException8(404, { message: "connection not found" });
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 HTTPException8(400, {
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 HTTPException8(403, { message: "cannot write a connection for another subject" });
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 HTTPException10 } from "hono/http-exception";
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 HTTPException9 } from "hono/http-exception";
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 HTTPException9(503, { message: "object storage is not configured" });
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 HTTPException9(413, {
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 HTTPException9(503, { message: "object storage is not configured" });
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 HTTPException9(404, { message: "file upload not found" });
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 HTTPException9(409, {
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 HTTPException9(409, {
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 HTTPException9(status, { message });
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 HTTPException9(409, { message: "file upload cleanup claim was superseded" });
8840
+ throw new HTTPException10(409, { message: "file upload cleanup claim was superseded" });
8078
8841
  }
8079
- throw new HTTPException9(status, { message });
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 HTTPException9(409, {
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 HTTPException9(409, {
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 HTTPException9(404, { message: "file not found" });
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 HTTPException9(502, { message: "object storage returned an invalid byte range" });
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 HTTPException9(503, { message: "object storage is not configured" });
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 HTTPException9(404, { message: "file not found" });
8984
+ throw new HTTPException10(404, { message: "file not found" });
8222
8985
  }
8223
8986
  if (file.status !== "ready") {
8224
- throw new HTTPException9(409, { message: `file is ${file.status}` });
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 HTTPException9(404, { message: "artifact not found" });
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 HTTPException10(404, { message: "document base not found" });
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 HTTPException10(503, { message: "object storage is not configured" });
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 HTTPException10) {
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 HTTPException10(503, { message: "object storage is not configured" });
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 HTTPException10(404, { message: "document not found" });
9177
+ throw new HTTPException11(404, { message: "document not found" });
8415
9178
  }
8416
9179
  if (document.status !== "failed") {
8417
- throw new HTTPException10(422, { message: "only failed documents can be retried" });
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 HTTPException10(404, { message: "document not found" });
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 HTTPException10) {
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 HTTPException10(404, { message: "document base not found" });
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 HTTPException10(503, { message: "object storage is not configured" });
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 HTTPException10(413, {
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 HTTPException10) {
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 HTTPException10(404, { message: "document not found" });
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 HTTPException10) {
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 HTTPException10(400, { message: "invalid knowledge memory query parameters" });
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 HTTPException10(404, { message: "knowledge memory not found" });
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 HTTPException10(400, { message: "invalid workspace memory search request" });
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 HTTPException10(400, { message: "invalid knowledge memory request" });
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 HTTPException10(404, { message });
9537
+ return new HTTPException11(404, { message });
8775
9538
  }
8776
9539
  if (message.includes("already exists")) {
8777
- return new HTTPException10(409, { message });
9540
+ return new HTTPException11(409, { message });
8778
9541
  }
8779
9542
  if (message.includes("no suggested base")) {
8780
- return new HTTPException10(422, { message });
9543
+ return new HTTPException11(422, { message });
8781
9544
  }
8782
9545
  if (message.includes("pending") || message.includes("failed") || message.includes("deleted")) {
8783
- return new HTTPException10(422, { message });
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 HTTPException10(400, { message });
9549
+ return new HTTPException11(400, { message });
8787
9550
  }
8788
- return new HTTPException10(500, { message });
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 HTTPException11 } from "hono/http-exception";
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 HTTPException11(404, {
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 HTTPException11(429, { message: "too many requests; slow down" });
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 HTTPException11(400, { message: "invalid device-start request" });
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 HTTPException11(404, { message: "workspace not found" });
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 HTTPException11(400, { message: "invalid device-poll request" });
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 HTTPException11(400, { message: "invalid device-lookup request" });
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 HTTPException11(404, { message: "no pending enrollment for that code" });
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 HTTPException11(404, { message: "no pending enrollment for that code" });
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 HTTPException11(400, { message: "invalid enroll-token-exchange request" });
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 HTTPException11(503, { message: "enrollment credential plane is not configured" });
9964
+ throw new HTTPException12(503, { message: "enrollment credential plane is not configured" });
9202
9965
  }
9203
- throw new HTTPException11(401, { message: "invalid or expired enroll token" });
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 HTTPException11(400, { message: "invalid device-approve request" });
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 HTTPException11(404, { message: "no pending enrollment for that code" });
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 HTTPException11(400, { message: "invalid device-deny request" });
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 HTTPException11(400, { message: "invalid mint-enroll-token request" });
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 HTTPException11(503, { message: "enrollment credential plane is not configured" });
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 HTTPException12 } from "hono/http-exception";
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 getSession3,
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 getSession3(db, workspaceId, input.sessionId);
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 HTTPException12(404, {
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 HTTPException12(404, { message: "machine not found in this workspace" });
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 HTTPException13 } from "hono/http-exception";
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 HTTPException13(422, {
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 HTTPException13(422, {
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 HTTPException13(422, {
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 HTTPException13(409, { message: `variable set name is already in use: ${name}` });
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 HTTPException13(409, { message: `variable set name is already in use: ${name}` });
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 HTTPException13(409, {
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 HTTPException13(409, {
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 HTTPException13(422, {
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 HTTPException13(404, { message: "variable set variable not found" });
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 HTTPException13(422, {
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 HTTPException13(422, { message: "variable set name is required" });
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 HTTPException14 } from "hono/http-exception";
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 HTTPException14(403, {
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 HTTPException15 } from "hono/http-exception";
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 HTTPException15(403, { message: "missing workspace access for usage query" });
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 HTTPException15(404, { message: "stripe billing is not enabled" });
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 HTTPException15(400, {
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 HTTPException15(502, { message: "Stripe did not return a checkout URL" });
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 HTTPException15(404, { message: "stripe billing is not enabled" });
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 HTTPException15(400, { message: "missing stripe-signature" });
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 HTTPException15(400, {
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 HTTPException15(500, {
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 HTTPException15(500, {
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 HTTPException15(400, { message: `${field} must use the OpenGeni public origin` });
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 HTTPException15(404, { message: "account not found" });
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 HTTPException15(409, { message: "account selection is required" });
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 HTTPException15(403, { message: `missing permission: ${permission}` });
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 HTTPException15(500, { message: "Stripe secret key is not configured" });
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 HTTPException16 } from "hono/http-exception";
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 HTTPException16(400, { message: "missing GitHub installation state" });
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 HTTPException16(400, { message: "invalid or expired GitHub installation state" });
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 HTTPException16(409, {
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 HTTPException16(409, {
11277
+ throw new HTTPException17(409, {
10515
11278
  message: JSON.stringify({ message: error.message, missing: error.missing })
10516
11279
  });
10517
11280
  }
10518
- throw new HTTPException16(502, {
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 HTTPException16(409, {
11293
+ throw new HTTPException17(409, {
10531
11294
  message: JSON.stringify({ message: error.message, missing: error.missing })
10532
11295
  });
10533
11296
  }
10534
- throw new HTTPException16(502, {
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 HTTPException16(400, { message: "invalid GitHub installation id" });
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 HTTPException16(404, { message: "GitHub installation binding not found" });
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 HTTPException16(400, { message: "missing GitHub manifest code" });
11351
+ throw new HTTPException17(400, { message: "missing GitHub manifest code" });
10589
11352
  }
10590
11353
  if (!state || !verifySignedState(state, githubStateSecret)) {
10591
- throw new HTTPException16(400, { message: "invalid or expired GitHub manifest state" });
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 HTTPException16(502, { message });
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 HTTPException16(400, { message: "missing GitHub installation state" });
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 HTTPException16(400, { message: "invalid or expired GitHub installation state" });
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 HTTPException16(403, {
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 HTTPException16(400, { message: "unsupported GitHub setup action" });
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 HTTPException16(400, { message: "missing or invalid GitHub installation_id" });
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 HTTPException16(409, {
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 HTTPException16(400, { message: "missing GitHub OAuth code" });
11424
+ throw new HTTPException17(400, { message: "missing GitHub OAuth code" });
10662
11425
  }
10663
11426
  if (!state) {
10664
- throw new HTTPException16(400, { message: "missing GitHub OAuth state" });
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 HTTPException16(400, { message: "invalid or expired GitHub OAuth state" });
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 HTTPException16(400, { message: "invalid GitHub installation id" });
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 HTTPException16(403, {
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 HTTPException16(409, {
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 HTTPException16(409, { message: "GitHub installation proof is stale or invalid" });
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 HTTPException16(409, { message: "GitHub returned duplicate repository identities" });
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 HTTPException16(409, { message: error.message });
11484
+ throw new HTTPException17(409, { message: error.message });
10722
11485
  }
10723
11486
  throw error;
10724
11487
  }
10725
11488
  if (!bound) {
10726
- throw new HTTPException16(409, {
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 HTTPException16(400, { message: "missing GitHub OAuth state" });
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 HTTPException16(400, { message: "invalid or expired GitHub OAuth state" });
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 HTTPException16(410, { message: legacyInstallationChooserDisabledMessage });
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 HTTPException16(400, {
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 HTTPException16) || error.status !== 401) {
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 HTTPException16) {
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 HTTPException16(403, { message: error.message });
11565
+ return new HTTPException17(403, { message: error.message });
10803
11566
  }
10804
11567
  if (error.reason === "installation_missing") {
10805
- return new HTTPException16(404, { message: error.message });
11568
+ return new HTTPException17(404, { message: error.message });
10806
11569
  }
10807
- return new HTTPException16(409, { message: error.message });
11570
+ return new HTTPException17(409, { message: error.message });
10808
11571
  }
10809
11572
  if (error instanceof GitHubAppConfigurationError2) {
10810
- return new HTTPException16(409, {
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 HTTPException16(502, { message: error.message });
11578
+ return new HTTPException17(502, { message: error.message });
10816
11579
  }
10817
- return new HTTPException16(502, { message: "GitHub authority verification failed" });
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 HTTPException17 } from "hono/http-exception";
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 HTTPException17(409, {
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 HTTPException17(409, { message: "built-in packs cannot be unregistered" });
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 HTTPException17(404, { message: "pack not found" });
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 HTTPException17(422, {
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 HTTPException17(422, {
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 HTTPException17(409, {
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 HTTPException17(422, {
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 HTTPException17(404, { message: "pack not found" });
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 HTTPException17(422, { message: `unknown social connection: ${id}` });
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 HTTPException17(422, {
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 HTTPException17(422, { message: `unknown document base: ${baseId}` });
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 HTTPException18 } from "hono/http-exception";
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 HTTPException18(409, { message: error.message });
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 getSandbox2,
11495
- getSession as getSession4,
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 HTTPException19 } from "hono/http-exception";
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 HTTPException19(409, { message: "sandbox not available" });
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 HTTPException19(409, {
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 HTTPException19(409, {
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 HTTPException19(409, {
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 HTTPException19(409, {
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 HTTPException19(409, {
12543
+ throw new HTTPException20(409, {
11700
12544
  message: `sandbox instance was lost; retry to restore it`
11701
12545
  });
11702
12546
  }
11703
12547
  }
11704
- const emit = async (events) => {
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
- ).session;
11731
- const credentialSession = withRunCredentialsSession(routedSession, session.id);
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 HTTPException19) return error;
12573
+ if (error instanceof HTTPException20) return error;
11751
12574
  if (error instanceof ChannelAUnavailableError)
11752
- return new HTTPException19(503, { message: error.message });
12575
+ return new HTTPException20(503, { message: error.message });
11753
12576
  if (error instanceof ChannelAValidationError)
11754
- return new HTTPException19(400, { message: error.message });
12577
+ return new HTTPException20(400, { message: error.message });
11755
12578
  if (error instanceof ChannelANotFoundError)
11756
- return new HTTPException19(404, { message: error.message });
12579
+ return new HTTPException20(404, { message: error.message });
11757
12580
  if (error instanceof ChannelAConflictError)
11758
- return new HTTPException19(409, { message: error.message });
12581
+ return new HTTPException20(409, { message: error.message });
11759
12582
  if (error instanceof ChannelAUnsupportedError)
11760
- return new HTTPException19(409, { message: error.message });
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 HTTPException21 } from "hono/http-exception";
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 HTTPException20 } from "hono/http-exception";
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 HTTPException20(404, { message: "capture not found" });
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 HTTPException20(404, { message: "path not in capture" });
13202
+ throw new HTTPException21(404, { message: "path not in capture" });
12380
13203
  }
12381
13204
  if (file.deleted) {
12382
- throw new HTTPException20(404, { message: "file was deleted" });
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 HTTPException21(409, {
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 HTTPException21(403, { message: error.message });
13474
+ throw new HTTPException22(403, { message: error.message });
12647
13475
  }
12648
13476
  if (error instanceof SessionListCursorExpiredError) {
12649
- throw new HTTPException21(410, { message: error.message });
13477
+ throw new HTTPException22(410, { message: error.message });
12650
13478
  }
12651
13479
  if (error instanceof SessionListCursorError) {
12652
- throw new HTTPException21(400, { message: error.message });
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 HTTPException21(429, { message: error.message });
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 HTTPException21(404, { message: "session not found" });
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 HTTPException21(404, { message: "session not found" });
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 HTTPException21(404, { message: "session not found" });
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 HTTPException21(400, { message: "invalid session pin request" });
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 HTTPException21(404, { message: "session not found" });
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 HTTPException21(403, { message: error.message });
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 HTTPException21(400, {
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 HTTPException21(404, {
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 HTTPException21(404, { message: "session not found" });
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 HTTPException21(400, { message: "invalid MCP approval-policy request" });
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 HTTPException21(404, { message: "session goal not found" });
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 HTTPException21(404, { message: "session goal not found" });
13715
+ throw new HTTPException22(404, { message: "session goal not found" });
12888
13716
  }
12889
13717
  if (existing.status === "completed") {
12890
- throw new HTTPException21(409, {
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 HTTPException21(409, {
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 HTTPException21(400, {
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 HTTPException21(409, { message: error.message });
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 HTTPException21(400, {
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 HTTPException21(400, {
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 HTTPException21(404, { message: "session not found" });
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 HTTPException21(400, { message: "workspace-control actor is too large" });
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 HTTPException21(400, { message: "invalid session control request" });
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 HTTPException21(409, {
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 HTTPException21(error.code === "SKIP_NOT_ALLOWED" ? 409 : 422, {
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 HTTPException21(404, { message: "human-input request not found" });
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 HTTPException21(409, {
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 HTTPException21(400, { message: "invalid human-input request status" });
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 HTTPException21(404, { message: "human-input request not found" });
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 HTTPException21(404, {
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 getSession4(db, workspaceId, sessionId);
14379
+ const session = await getSession5(db, workspaceId, sessionId);
13552
14380
  if (!session) {
13553
- throw new HTTPException21(404, { message: "session not found" });
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 getSandbox2(db, workspaceId, session.activeSandboxId) : null;
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 getSession4(db, workspaceId, sessionId);
14496
+ const session = await getSession5(db, workspaceId, sessionId);
13669
14497
  if (!session) {
13670
- throw new HTTPException21(404, { message: "session not found" });
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 HTTPException21(400, {
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 getSession4(db, workspaceId, sessionId);
14525
+ const session = await getSession5(db, workspaceId, sessionId);
13698
14526
  if (!session) {
13699
- throw new HTTPException21(404, { message: "session not found" });
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 HTTPException21(400, {
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 HTTPException21(409, {
14544
+ throw new HTTPException22(409, {
13717
14545
  message: "stream_acknowledgment_required"
13718
14546
  });
13719
14547
  }
13720
14548
  if (shared && !ack.acknowledgedShared) {
13721
- throw new HTTPException21(409, {
14549
+ throw new HTTPException22(409, {
13722
14550
  message: "shared_acknowledgment_required"
13723
14551
  });
13724
14552
  }
13725
14553
  }
13726
- const activeSandbox = session.activeSandboxId ? await getSandbox2(db, workspaceId, session.activeSandboxId) : null;
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 getSession4(db, workspaceId, sessionId);
14656
+ const session = await getSession5(db, workspaceId, sessionId);
13829
14657
  if (!session) {
13830
- throw new HTTPException21(404, { message: "session not found" });
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 HTTPException21(400, {
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 getSession4(db, workspaceId, sessionId);
14684
+ const session = await getSession5(db, workspaceId, sessionId);
13857
14685
  if (!session) {
13858
- throw new HTTPException21(404, { message: "session not found" });
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 getSession4(db, workspaceId, sessionId);
14706
+ const session = await getSession5(db, workspaceId, sessionId);
13879
14707
  if (!session) {
13880
- throw new HTTPException21(404, { message: "session not found" });
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 getSession4(db, workspaceId, sessionId);
14728
+ const session = await getSession5(db, workspaceId, sessionId);
13901
14729
  if (!session) {
13902
- throw new HTTPException21(404, { message: "session not found" });
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 HTTPException21(400, { message: "invalid request body" });
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 getSession4(db, workspaceId, sessionId);
14851
+ const session = await getSession5(db, workspaceId, sessionId);
14024
14852
  if (!session) {
14025
- throw new HTTPException21(404, { message: "session not found" });
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 HTTPException21(400, {
14867
+ throw new HTTPException22(400, {
14040
14868
  message: "path query parameter is required"
14041
14869
  });
14042
14870
  }
14043
- const session = await getSession4(db, workspaceId, sessionId);
14871
+ const session = await getSession5(db, workspaceId, sessionId);
14044
14872
  if (!session) {
14045
- throw new HTTPException21(404, { message: "session not found" });
14873
+ throw new HTTPException22(404, { message: "session not found" });
14046
14874
  }
14047
14875
  if (!objectStorage) {
14048
- throw new HTTPException21(404, { message: "capture not found" });
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 HTTPException21(400, {
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 HTTPException21(409, {
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 HTTPException21(409, {
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 HTTPException21(404, { message: "pty not found or closed" });
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 HTTPException21(409, {
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 HTTPException21(404, { message: "pty not found or closed" });
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 HTTPException21(409, {
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 HTTPException21(409, {
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 HTTPException21(404, { message: "session not found" });
15195
+ return new HTTPException22(404, { message: "session not found" });
14363
15196
  }
14364
15197
  if (error instanceof SessionAuthorizationUnavailableError) {
14365
- return new HTTPException21(503, { message: "session authorization is unavailable" });
15198
+ return new HTTPException22(503, { message: "session authorization is unavailable" });
14366
15199
  }
14367
- if (error instanceof HTTPException21) return error;
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 HTTPException21(400, { message: `${name} is invalid` });
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 HTTPException21(400, { message: `${name} accepts at most 100 values` });
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 HTTPException21(400, { message: `${name} contains an invalid value` });
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 HTTPException21(400, {
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 HTTPException21(400, { message: "cursor is invalid" });
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 HTTPException21(400, {
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 HTTPException21(400, { message: 'pinsOnly must be the literal "true"' });
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 HTTPException21(400, { message: 'pinsOnly requires view="page"' });
15248
+ throw new HTTPException22(400, { message: 'pinsOnly requires view="page"' });
14416
15249
  }
14417
15250
  if (pinsOnly && (rawCursor || parentSessionId !== void 0 || search)) {
14418
- throw new HTTPException21(400, {
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 HTTPException21 && error.status === 422) {
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 HTTPException22 } from "hono/http-exception";
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 HTTPException22(422, { message: "since must be an ISO date-time" });
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 HTTPException22(422, {
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 HTTPException22(404, { message });
15485
+ return new HTTPException23(404, { message });
14653
15486
  }
14654
15487
  if (message.includes("duplicate key")) {
14655
- return new HTTPException22(409, { message: "social connection or post already exists" });
15488
+ return new HTTPException23(409, { message: "social connection or post already exists" });
14656
15489
  }
14657
- return new HTTPException22(500, { message });
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 HTTPException23 } from "hono/http-exception";
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 HTTPException23(409, { message: "account selection is required" });
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 HTTPException23(400, { message: "invalid workspace settings patch" });
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 HTTPException23(400, { message: "workspace-control actor is too large" });
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 HTTPException23(400, { message: "invalid workspace inference-control request" });
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 HTTPException23(422, { message: `unknown rigId: ${payload.rigId}` });
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 HTTPException23(500, { message: "failed to add member" });
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 HTTPException23(404, { message: "member not found" });
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 HTTPException23(500, { message: "failed to update member" });
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 HTTPException23(403, { message: `missing permission: ${permission}` });
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 HTTPException24(503, {
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 HTTPException24(413, { message: "MCP request body exceeds the safety limit" });
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 HTTPException24(404, { message: "session not found" });
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 HTTPException24(404, { message: "session not found" });
16549
+ throw new HTTPException26(404, { message: "session not found" });
15481
16550
  }
15482
16551
  if (error instanceof SessionAuthorizationUnavailableError2) {
15483
- throw new HTTPException24(503, { message: "session authorization is unavailable" });
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 HTTPException24(413, { message: "MCP tool list exceeds the safety limit" });
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 requireAccessGrant17(c, deps, workspaceId);
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 HTTPException24) {
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 HTTPException24) {
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
- async ([name, check]) => {
15676
- try {
15677
- await withTimeout(Promise.resolve().then(check), timeoutMs);
15678
- return [name, { ok: true }];
15679
- } catch (error) {
15680
- return [
15681
- name,
15682
- {
15683
- ok: false,
15684
- error: error instanceof Error ? error.message : String(error)
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-DQWFAIPE.js.map
17182
+ //# sourceMappingURL=chunk-S2N4252E.js.map