@opengeni/api-router 0.5.1 → 0.5.3

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.
@@ -12,7 +12,7 @@ import { createObjectStorage } from "@opengeni/storage";
12
12
  import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
13
13
  import { Hono } from "hono";
14
14
  import { cors } from "hono/cors";
15
- import { HTTPException as HTTPException20 } from "hono/http-exception";
15
+ import { HTTPException as HTTPException22 } from "hono/http-exception";
16
16
  import { hasPermission as hasPermission4, requireAccessGrant as requireAccessGrant16, requirePermission } from "@opengeni/core";
17
17
 
18
18
  // src/auth/managed-auth.ts
@@ -1836,6 +1836,9 @@ function isAuthExempt(c, settings) {
1836
1836
  if (path === "/v1/integrations/oauth/callback" || path === "/v1/integrations/oauth/client-metadata.json") {
1837
1837
  return true;
1838
1838
  }
1839
+ if (path.startsWith("/v1/catalog-assets/")) {
1840
+ return true;
1841
+ }
1839
1842
  if (githubConnectPathPattern.test(path)) {
1840
1843
  return true;
1841
1844
  }
@@ -1964,6 +1967,91 @@ function registerCapabilityRoutes(app, deps) {
1964
1967
  });
1965
1968
  }
1966
1969
 
1970
+ // src/routes/catalog-assets.ts
1971
+ import { HTTPException as HTTPException3 } from "hono/http-exception";
1972
+ var CATALOG_ASSET_PREFIX = "catalog-assets/";
1973
+ var MAX_KEY_LENGTH = 512;
1974
+ var PRINTABLE_ASCII = /^[\x20-\x7e]+$/;
1975
+ var CONTENT_TYPE_BY_EXTENSION = {
1976
+ png: "image/png",
1977
+ jpg: "image/jpeg",
1978
+ jpeg: "image/jpeg",
1979
+ svg: "image/svg+xml",
1980
+ webp: "image/webp",
1981
+ gif: "image/gif",
1982
+ ico: "image/x-icon"
1983
+ };
1984
+ function registerCatalogAssetRoutes(app, deps) {
1985
+ const { settings, objectStorage } = deps;
1986
+ app.get("/v1/catalog-assets/*", async (c) => {
1987
+ if (!settings.integrationsEnabled) {
1988
+ throw new HTTPException3(404, { message: "integrations are not enabled for this deployment" });
1989
+ }
1990
+ if (!objectStorage) {
1991
+ throw new HTTPException3(404, { message: "asset not found" });
1992
+ }
1993
+ const key = catalogAssetKeyFromPath(new URL(c.req.url).pathname);
1994
+ if (!key) {
1995
+ throw new HTTPException3(404, { message: "asset not found" });
1996
+ }
1997
+ const contentType = contentTypeForKey(key);
1998
+ if (!contentType) {
1999
+ throw new HTTPException3(404, { message: "asset not found" });
2000
+ }
2001
+ const object3 = await objectStorage.getObjectBytes(key);
2002
+ if (!object3) {
2003
+ throw new HTTPException3(404, { message: "asset not found" });
2004
+ }
2005
+ const etag = etagForKey(key);
2006
+ const headers = {
2007
+ "Cache-Control": "public, max-age=31536000, immutable",
2008
+ ETag: etag,
2009
+ "X-Content-Type-Options": "nosniff",
2010
+ "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox"
2011
+ };
2012
+ if (ifNoneMatchSatisfied(c.req.header("if-none-match"), etag)) {
2013
+ return c.body(null, 304, headers);
2014
+ }
2015
+ return c.body(new Uint8Array(object3.bytes), 200, { ...headers, "Content-Type": contentType });
2016
+ });
2017
+ }
2018
+ function catalogAssetKeyFromPath(pathname) {
2019
+ const prefix = "/v1/";
2020
+ if (!pathname.startsWith(prefix)) {
2021
+ return null;
2022
+ }
2023
+ let key;
2024
+ try {
2025
+ key = decodeURIComponent(pathname.slice(prefix.length));
2026
+ } catch {
2027
+ return null;
2028
+ }
2029
+ if (key.length === 0 || key.length > MAX_KEY_LENGTH || !key.startsWith(CATALOG_ASSET_PREFIX) || key.includes("..") || key.includes("\\") || key.includes("//") || !PRINTABLE_ASCII.test(key)) {
2030
+ return null;
2031
+ }
2032
+ return key;
2033
+ }
2034
+ function contentTypeForKey(key) {
2035
+ const match = /\.([a-zA-Z0-9]+)$/.exec(key);
2036
+ const ext = match?.[1]?.toLowerCase();
2037
+ return ext ? CONTENT_TYPE_BY_EXTENSION[ext] ?? null : null;
2038
+ }
2039
+ function etagForKey(key) {
2040
+ const filename = key.slice(key.lastIndexOf("/") + 1);
2041
+ const dot = filename.lastIndexOf(".");
2042
+ const digest = dot === -1 ? filename : filename.slice(0, dot);
2043
+ return `"${digest}"`;
2044
+ }
2045
+ function ifNoneMatchSatisfied(header, etag) {
2046
+ if (!header) {
2047
+ return false;
2048
+ }
2049
+ if (header.trim() === "*") {
2050
+ return true;
2051
+ }
2052
+ return header.split(",").map((value) => value.trim()).includes(etag);
2053
+ }
2054
+
1967
2055
  // src/routes/codex.ts
1968
2056
  import { environmentsEncryptionKeyBytes as environmentsEncryptionKeyBytes2 } from "@opengeni/config";
1969
2057
  import {
@@ -1999,7 +2087,7 @@ import {
1999
2087
  CODEX_ROTATION_STRATEGIES
2000
2088
  } from "@opengeni/db";
2001
2089
  import { createSignedState as createSignedState2, readSignedState } from "@opengeni/github";
2002
- import { HTTPException as HTTPException3 } from "hono/http-exception";
2090
+ import { HTTPException as HTTPException4 } from "hono/http-exception";
2003
2091
  import { requireAccessGrant as requireAccessGrant2 } from "@opengeni/core";
2004
2092
  var CODEX_PROVIDER_LABEL = "Codex subscription \xB7 no credits";
2005
2093
  function codexAccountJson(row) {
@@ -2043,7 +2131,7 @@ function registerCodexRoutes(app, deps) {
2043
2131
  try {
2044
2132
  start = await startDeviceCode();
2045
2133
  } catch (error) {
2046
- throw new HTTPException3(502, { message: error instanceof CodexDeviceError ? error.message : "failed to start Codex device login" });
2134
+ throw new HTTPException4(502, { message: error instanceof CodexDeviceError ? error.message : "failed to start Codex device login" });
2047
2135
  }
2048
2136
  const state = createSignedState2(githubStateSecret, { workspaceId, deviceAuthId: start.deviceAuthId, userCode: start.userCode });
2049
2137
  return c.json({ userCode: start.userCode, verificationUri: start.verificationUri, intervalSeconds: start.intervalSeconds, state });
@@ -2054,7 +2142,7 @@ function registerCodexRoutes(app, deps) {
2054
2142
  const { state } = await c.req.json();
2055
2143
  const payload = state ? readSignedState(state, githubStateSecret) : null;
2056
2144
  if (!payload || payload.workspaceId !== workspaceId || !payload.deviceAuthId || !payload.userCode) {
2057
- throw new HTTPException3(400, { message: "codex connect state is invalid or expired" });
2145
+ throw new HTTPException4(400, { message: "codex connect state is invalid or expired" });
2058
2146
  }
2059
2147
  if (typeof payload.iat === "number" && Date.now() / 1e3 - payload.iat > CODEX_DEVICE_EXPIRY_SECONDS) {
2060
2148
  return c.json({ status: "expired" });
@@ -2063,7 +2151,7 @@ function registerCodexRoutes(app, deps) {
2063
2151
  try {
2064
2152
  poll = await pollDeviceCode({ deviceAuthId: payload.deviceAuthId, userCode: payload.userCode });
2065
2153
  } catch (error) {
2066
- throw new HTTPException3(502, { message: error instanceof CodexDeviceError ? error.message : "codex device poll failed" });
2154
+ throw new HTTPException4(502, { message: error instanceof CodexDeviceError ? error.message : "codex device poll failed" });
2067
2155
  }
2068
2156
  if (poll.status === "pending") {
2069
2157
  return c.json({ status: "pending" });
@@ -2075,12 +2163,12 @@ function registerCodexRoutes(app, deps) {
2075
2163
  try {
2076
2164
  tokens = await exchangeDeviceCode({ authorizationCode: poll.authorizationCode, codeVerifier: poll.codeVerifier });
2077
2165
  } catch (error) {
2078
- throw new HTTPException3(502, { message: error instanceof CodexDeviceError ? error.message : "codex token exchange failed" });
2166
+ throw new HTTPException4(502, { message: error instanceof CodexDeviceError ? error.message : "codex token exchange failed" });
2079
2167
  }
2080
2168
  const id = parseIdToken(tokens.idToken);
2081
2169
  const key = environmentsEncryptionKeyBytes2(settings);
2082
2170
  if (!key) {
2083
- throw new HTTPException3(500, { message: "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured" });
2171
+ throw new HTTPException4(500, { message: "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured" });
2084
2172
  }
2085
2173
  const upserted = await upsertCodexSubscriptionCredential(db, {
2086
2174
  accountId: grant.accountId,
@@ -2174,7 +2262,7 @@ function registerCodexRoutes(app, deps) {
2174
2262
  const accountId = c.req.param("accountId");
2175
2263
  const activated = await setActiveCodexCredential(db, workspaceId, accountId);
2176
2264
  if (!activated) {
2177
- throw new HTTPException3(404, { message: "codex account not found" });
2265
+ throw new HTTPException4(404, { message: "codex account not found" });
2178
2266
  }
2179
2267
  return c.json({ activated: true, accountId });
2180
2268
  });
@@ -2188,17 +2276,17 @@ function registerCodexRoutes(app, deps) {
2188
2276
  }
2189
2277
  if (typeof body.rotationStrategy === "string") {
2190
2278
  if (!CODEX_ROTATION_STRATEGIES.includes(body.rotationStrategy)) {
2191
- throw new HTTPException3(400, { message: "invalid rotation strategy" });
2279
+ throw new HTTPException4(400, { message: "invalid rotation strategy" });
2192
2280
  }
2193
2281
  patch.rotationStrategy = body.rotationStrategy;
2194
2282
  }
2195
2283
  if (patch.rotationEnabled === void 0 && patch.rotationStrategy === void 0) {
2196
- throw new HTTPException3(400, { message: "no settings to update" });
2284
+ throw new HTTPException4(400, { message: "no settings to update" });
2197
2285
  }
2198
2286
  await ensureCodexRotationSettings(db, grant.accountId, workspaceId);
2199
2287
  const updated = await updateCodexRotationSettings(db, workspaceId, patch);
2200
2288
  if (!updated) {
2201
- throw new HTTPException3(404, { message: "codex rotation settings not found" });
2289
+ throw new HTTPException4(404, { message: "codex rotation settings not found" });
2202
2290
  }
2203
2291
  return c.json({
2204
2292
  rotationEnabled: updated.rotationEnabled,
@@ -2214,12 +2302,12 @@ function registerCodexRoutes(app, deps) {
2214
2302
  const label = typeof body.label === "string" ? body.label : null;
2215
2303
  const renamed = await renameCodexAccount(db, workspaceId, accountId, label);
2216
2304
  if (!renamed) {
2217
- throw new HTTPException3(404, { message: "codex account not found" });
2305
+ throw new HTTPException4(404, { message: "codex account not found" });
2218
2306
  }
2219
2307
  const accounts = await listCodexAccountStatuses(db, workspaceId);
2220
2308
  const row = accounts.find((account) => account.id === accountId);
2221
2309
  if (!row) {
2222
- throw new HTTPException3(404, { message: "codex account not found" });
2310
+ throw new HTTPException4(404, { message: "codex account not found" });
2223
2311
  }
2224
2312
  return c.json(codexAccountJson(row));
2225
2313
  });
@@ -2241,7 +2329,7 @@ function registerCodexRoutes(app, deps) {
2241
2329
  await requireAccessGrant2(c, deps, workspaceId, "workspace:read");
2242
2330
  const status = await getCodexCredentialStatus(db, workspaceId);
2243
2331
  if (!status?.credentialId) {
2244
- throw new HTTPException3(404, { message: "codex subscription is not connected" });
2332
+ throw new HTTPException4(404, { message: "codex subscription is not connected" });
2245
2333
  }
2246
2334
  const payload = await fetchCodexUsageForAccount(db, settings, workspaceId, status.credentialId);
2247
2335
  return c.json(codexUsageJson(payload));
@@ -2252,7 +2340,7 @@ function registerCodexRoutes(app, deps) {
2252
2340
  const accountId = c.req.param("accountId");
2253
2341
  const accounts = await listCodexAccountStatuses(db, workspaceId);
2254
2342
  if (!accounts.some((account) => account.id === accountId)) {
2255
- throw new HTTPException3(404, { message: "codex account not found" });
2343
+ throw new HTTPException4(404, { message: "codex account not found" });
2256
2344
  }
2257
2345
  const payload = await fetchCodexUsageForAccount(db, settings, workspaceId, accountId);
2258
2346
  return c.json(codexUsageJson(payload));
@@ -2297,7 +2385,7 @@ import {
2297
2385
  revokeConnection,
2298
2386
  updateConnection as updateConnection2
2299
2387
  } from "@opengeni/db";
2300
- import { HTTPException as HTTPException5 } from "hono/http-exception";
2388
+ import { HTTPException as HTTPException7 } from "hono/http-exception";
2301
2389
 
2302
2390
  // src/integrations/oauth-client.ts
2303
2391
  import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
@@ -2321,21 +2409,46 @@ import { Buffer } from "buffer";
2321
2409
  import { createHash, randomBytes } from "crypto";
2322
2410
  import { lookup } from "dns/promises";
2323
2411
  import { isIP } from "net";
2324
- import { HTTPException as HTTPException4 } from "hono/http-exception";
2412
+ import { HTTPException as HTTPException6 } from "hono/http-exception";
2413
+
2414
+ // src/integrations/provider-domain.ts
2415
+ import { HTTPException as HTTPException5 } from "hono/http-exception";
2416
+ function canonicalProviderDomain(value) {
2417
+ const canonical = value.trim().toLowerCase().replace(/^www\./, "");
2418
+ if (!canonical) {
2419
+ throw new HTTPException5(400, { message: "providerDomain must not be empty" });
2420
+ }
2421
+ return canonical;
2422
+ }
2423
+
2424
+ // src/integrations/oauth-client.ts
2325
2425
  var oauthStateTtlMs = 10 * 60 * 1e3;
2426
+ var OAuthCallbackStageError = class extends Error {
2427
+ constructor(stage2, reason, cause) {
2428
+ super(errorMessage(cause));
2429
+ this.stage = stage2;
2430
+ this.reason = reason;
2431
+ this.cause = cause;
2432
+ this.name = "OAuthCallbackStageError";
2433
+ }
2434
+ stage;
2435
+ reason;
2436
+ cause;
2437
+ };
2326
2438
  async function startMcpOAuth(deps, context) {
2327
2439
  const { db, settings } = deps;
2328
- const resource = canonicalMcpResource(context.payload.mcpUrl ?? context.payload.resource);
2329
- const providerDomain = canonicalProviderDomain(context.payload.providerDomain ?? new URL(resource).hostname);
2440
+ const mcpUrl = canonicalMcpResource(context.payload.mcpUrl ?? context.payload.resource);
2441
+ const providerDomain = canonicalProviderDomain(context.payload.providerDomain ?? new URL(mcpUrl).hostname);
2330
2442
  const returnPath = safeReturnPath(context.payload.returnPath ?? "/integrations");
2331
2443
  const baseUrl = integrationBaseUrl(settings.publicBaseUrl, context.requestUrl);
2332
2444
  const redirectUri = `${baseUrl}/v1/integrations/oauth/callback`;
2333
2445
  const metadataUrl = `${baseUrl}/v1/integrations/oauth/client-metadata.json`;
2334
2446
  const existing = context.payload.connectionId ? await getConnectionMetadata(db, context.workspaceId, context.payload.connectionId, context.subjectId) : null;
2335
2447
  if (context.payload.connectionId && !existing) {
2336
- throw new HTTPException4(404, { message: "connection not found" });
2448
+ throw new HTTPException6(404, { message: "connection not found" });
2337
2449
  }
2338
- const discovery = await discoverMcpOAuth(resource, settings);
2450
+ const discovery = await discoverMcpOAuth(mcpUrl, settings);
2451
+ const resource = discovery.prm.resource ? canonicalOAuthResource(discovery.prm.resource) : mcpUrl;
2339
2452
  const client = await registerOAuthClient(db, settings, discovery.as, metadataUrl, redirectUri);
2340
2453
  const verifier = randomPkceVerifier();
2341
2454
  const authorizeScopes = chooseAuthorizeScopes(context.payload.requestedScopes, discovery.challenge.scope, discovery.prm.scopesSupported);
@@ -2345,6 +2458,7 @@ async function startMcpOAuth(deps, context) {
2345
2458
  workspaceId: context.workspaceId,
2346
2459
  subjectId: context.subjectId,
2347
2460
  providerDomain,
2461
+ mcpUrl,
2348
2462
  resource,
2349
2463
  requestedScopes: uniqueStrings(context.payload.requestedScopes ?? []),
2350
2464
  authorizeScopes,
@@ -2374,24 +2488,33 @@ async function startMcpOAuth(deps, context) {
2374
2488
  });
2375
2489
  }
2376
2490
  async function completeMcpOAuthCallback(deps, input) {
2377
- const { db, settings } = deps;
2491
+ const { db, settings, observability } = deps;
2492
+ let state = null;
2378
2493
  if (!input.state) {
2379
- throw new HTTPException4(400, { message: "missing OAuth state" });
2380
- }
2381
- const state = readOAuthState(input.state, settings);
2382
- if (!input.code) {
2383
- return { redirectTo: callbackReturnPath(state.returnPath, "error", { reason: "missing_code" }) };
2494
+ const error = new OAuthCallbackStageError("state_verify", "state_invalid", new Error("missing OAuth state"));
2495
+ logOAuthCallbackFailure(observability, error, state);
2496
+ return { redirectTo: callbackReturnPath("/integrations", "error", { reason: error.reason }) };
2384
2497
  }
2385
- const consumed = await consumeIntegrationOAuthStateNonce(db, {
2386
- accountId: state.accountId,
2387
- workspaceId: state.workspaceId,
2388
- subjectId: state.subjectId,
2389
- nonce: state.nonce,
2390
- expiresAt: new Date(state.iat * 1e3 + oauthStateTtlMs),
2391
- now: /* @__PURE__ */ new Date()
2392
- });
2393
- if (!consumed) {
2394
- throw new HTTPException4(400, { message: "OAuth state has already been used" });
2498
+ try {
2499
+ state = readOAuthState(input.state, settings);
2500
+ if (!input.code) {
2501
+ return { redirectTo: callbackReturnPath(state.returnPath, "error", { reason: "missing_code" }) };
2502
+ }
2503
+ const consumed = await consumeIntegrationOAuthStateNonce(db, {
2504
+ accountId: state.accountId,
2505
+ workspaceId: state.workspaceId,
2506
+ subjectId: state.subjectId,
2507
+ nonce: state.nonce,
2508
+ expiresAt: new Date(state.iat * 1e3 + oauthStateTtlMs),
2509
+ now: /* @__PURE__ */ new Date()
2510
+ });
2511
+ if (!consumed) {
2512
+ throw new HTTPException6(400, { message: "OAuth state has already been used" });
2513
+ }
2514
+ } catch (error) {
2515
+ const staged = new OAuthCallbackStageError("state_verify", "state_invalid", error);
2516
+ logOAuthCallbackFailure(observability, staged, state);
2517
+ return { redirectTo: callbackReturnPath(state?.returnPath ?? "/integrations", "error", { reason: staged.reason }) };
2395
2518
  }
2396
2519
  try {
2397
2520
  const baseUrl = integrationBaseUrl(settings.publicBaseUrl, input.requestUrl);
@@ -2399,28 +2522,30 @@ async function completeMcpOAuthCallback(deps, input) {
2399
2522
  const key = requireEnvironmentEncryption2(settings);
2400
2523
  const verifier = decryptEnvironmentValue(key, state.encryptedPkceVerifier);
2401
2524
  const client = await clientForState(db, settings, state);
2402
- const token = await exchangeAuthorizationCode(settings, {
2525
+ const token = await stage("token_exchange", "token_exchange_failed", () => exchangeAuthorizationCode(settings, {
2403
2526
  code: input.code,
2404
2527
  verifier,
2405
2528
  redirectUri,
2406
2529
  resource: state.resource,
2407
2530
  tokenEndpoint: state.tokenEndpoint,
2408
2531
  client
2409
- });
2410
- const tools = await verifyMcpToolsList(settings, state.resource, token);
2532
+ }));
2533
+ const verification = await verifyMcpToolsListNonFatal(observability, settings, state, token);
2411
2534
  const scopes = grantedScopes(token.scopeText, state.authorizeScopes);
2412
2535
  const credential = credentialBundle(token, state, client);
2413
2536
  const metadata = {
2414
2537
  resource: state.resource,
2538
+ mcpUrl: state.mcpUrl,
2415
2539
  authorizationServer: state.authorizationServer,
2416
2540
  authorizationServerIssuer: state.issuer,
2417
2541
  tokenEndpoint: state.tokenEndpoint,
2418
2542
  clientId: client.clientId,
2419
2543
  clientRegistrationMethod: state.clientRegistrationMethod,
2420
- mcpTools: tools
2544
+ mcpToolsVerification: verification.metadata,
2545
+ ...verification.tools ? { mcpTools: verification.tools } : {}
2421
2546
  };
2422
2547
  const credentialEncrypted = encryptEnvironmentValue3(key, JSON.stringify(credential));
2423
- const connection = state.connectionId ? await updateConnection(db, {
2548
+ const connection = await stage("persist", "persist_failed", () => state.connectionId ? updateConnection(db, {
2424
2549
  workspaceId: state.workspaceId,
2425
2550
  connectionId: state.connectionId,
2426
2551
  visibleToSubjectId: state.subjectId,
@@ -2433,7 +2558,7 @@ async function completeMcpOAuthCallback(deps, input) {
2433
2558
  expiresAt: token.expiresAt,
2434
2559
  metadata,
2435
2560
  updatedBySubjectId: state.subjectId
2436
- }) : await createConnection(db, {
2561
+ }) : createConnection(db, {
2437
2562
  accountId: state.accountId,
2438
2563
  workspaceId: state.workspaceId,
2439
2564
  subjectId: null,
@@ -2444,16 +2569,21 @@ async function completeMcpOAuthCallback(deps, input) {
2444
2569
  expiresAt: token.expiresAt,
2445
2570
  metadata,
2446
2571
  createdBySubjectId: state.subjectId
2447
- });
2572
+ }));
2448
2573
  if (!connection) {
2449
- throw new HTTPException4(409, { message: "connection changed during OAuth reconnect; start again" });
2574
+ throw new HTTPException6(409, { message: "connection changed during OAuth reconnect; start again" });
2450
2575
  }
2451
- return { redirectTo: callbackReturnPath(state.returnPath, "success", { connectionId: connection.id }) };
2576
+ return {
2577
+ redirectTo: callbackReturnPath(state.returnPath, "success", {
2578
+ connectionId: connection.id,
2579
+ providerDomain: connection.providerDomain,
2580
+ ...verification.metadata.status === "failed" ? { verification: "failed" } : {}
2581
+ })
2582
+ };
2452
2583
  } catch (error) {
2453
- if (error instanceof HTTPException4 && error.status >= 400 && error.status < 500) {
2454
- throw error;
2455
- }
2456
- return { redirectTo: callbackReturnPath(state.returnPath, "error", { reason: "oauth_callback_failed" }) };
2584
+ const staged = error instanceof OAuthCallbackStageError ? error : new OAuthCallbackStageError("persist", "persist_failed", error);
2585
+ logOAuthCallbackFailure(observability, staged, state);
2586
+ return { redirectTo: callbackReturnPath(state.returnPath, "error", { reason: staged.reason }) };
2457
2587
  }
2458
2588
  }
2459
2589
  function integrationBaseUrl(publicBaseUrl, requestUrl) {
@@ -2462,7 +2592,7 @@ function integrationBaseUrl(publicBaseUrl, requestUrl) {
2462
2592
  function requireIntegrationsStateSecret(settings) {
2463
2593
  const secret = settings.integrationsStateSecret?.trim();
2464
2594
  if (!secret) {
2465
- throw new HTTPException4(503, { message: "integrations OAuth requires OPENGENI_INTEGRATIONS_STATE_SECRET" });
2595
+ throw new HTTPException6(503, { message: "integrations OAuth requires OPENGENI_INTEGRATIONS_STATE_SECRET" });
2466
2596
  }
2467
2597
  return secret;
2468
2598
  }
@@ -2471,11 +2601,11 @@ async function discoverMcpOAuth(resource, settings) {
2471
2601
  const prm = await discoverProtectedResourceMetadata(resource, settings, challenge.resourceMetadata);
2472
2602
  const authorizationServer = prm.authorizationServers[0];
2473
2603
  if (!authorizationServer) {
2474
- throw new HTTPException4(422, { message: "MCP protected resource metadata did not advertise an authorization server" });
2604
+ throw new HTTPException6(422, { message: "MCP protected resource metadata did not advertise an authorization server" });
2475
2605
  }
2476
2606
  const as = await discoverAuthorizationServerMetadata(authorizationServer, settings);
2477
2607
  if (!as.codeChallengeMethodsSupported.includes("S256")) {
2478
- throw new HTTPException4(422, { message: "authorization server does not support required PKCE S256" });
2608
+ throw new HTTPException6(422, { message: "authorization server does not support required PKCE S256" });
2479
2609
  }
2480
2610
  return { challenge, prm, as };
2481
2611
  }
@@ -2496,7 +2626,7 @@ async function discoverProtectedResourceMetadata(resource, settings, advertisedU
2496
2626
  ]);
2497
2627
  for (const candidate of candidates) {
2498
2628
  const payload = await fetchJsonObject(candidate, settings).catch((error) => {
2499
- if (error instanceof HTTPException4) {
2629
+ if (error instanceof HTTPException6) {
2500
2630
  throw error;
2501
2631
  }
2502
2632
  return null;
@@ -2515,7 +2645,7 @@ async function discoverProtectedResourceMetadata(resource, settings, advertisedU
2515
2645
  ...stringValue(payload.resource) ? { resource: stringValue(payload.resource) } : {}
2516
2646
  };
2517
2647
  }
2518
- throw new HTTPException4(422, { message: "could not discover MCP protected resource metadata" });
2648
+ throw new HTTPException6(422, { message: "could not discover MCP protected resource metadata" });
2519
2649
  }
2520
2650
  async function discoverAuthorizationServerMetadata(authorizationServer, settings) {
2521
2651
  const candidates = uniqueStrings([
@@ -2525,7 +2655,7 @@ async function discoverAuthorizationServerMetadata(authorizationServer, settings
2525
2655
  ]);
2526
2656
  for (const candidate of candidates) {
2527
2657
  const payload = await fetchJsonObject(candidate, settings).catch((error) => {
2528
- if (error instanceof HTTPException4) {
2658
+ if (error instanceof HTTPException6) {
2529
2659
  throw error;
2530
2660
  }
2531
2661
  return null;
@@ -2549,7 +2679,7 @@ async function discoverAuthorizationServerMetadata(authorizationServer, settings
2549
2679
  ...stringValue(payload.registration_endpoint) ? { registrationEndpoint: stringValue(payload.registration_endpoint) } : {}
2550
2680
  };
2551
2681
  }
2552
- throw new HTTPException4(422, { message: "could not discover OAuth authorization server metadata" });
2682
+ throw new HTTPException6(422, { message: "could not discover OAuth authorization server metadata" });
2553
2683
  }
2554
2684
  async function registerOAuthClient(db, settings, as, metadataUrl, redirectUri) {
2555
2685
  const operator = operatorClientForAs(settings, as);
@@ -2577,7 +2707,7 @@ async function registerOAuthClient(db, settings, as, metadataUrl, redirectUri) {
2577
2707
  };
2578
2708
  }
2579
2709
  if (!as.registrationEndpoint) {
2580
- throw new HTTPException4(422, {
2710
+ throw new HTTPException6(422, {
2581
2711
  message: "manual OAuth client credentials are required for this authorization server"
2582
2712
  });
2583
2713
  }
@@ -2597,7 +2727,7 @@ async function registerOAuthClient(db, settings, as, metadataUrl, redirectUri) {
2597
2727
  if (storedWinner.clientId !== dcr.clientId) {
2598
2728
  const winner = await loadIntegrationOAuthClient(db, settings, as.issuer);
2599
2729
  if (!winner) {
2600
- throw new HTTPException4(422, { message: "OAuth client registration could not be loaded after a registration race" });
2730
+ throw new HTTPException6(422, { message: "OAuth client registration could not be loaded after a registration race" });
2601
2731
  }
2602
2732
  return dcrRegistrationFromStored(winner);
2603
2733
  }
@@ -2649,7 +2779,7 @@ function normalizedIssuerKey(value) {
2649
2779
  }
2650
2780
  async function dynamicClientRegistration(settings, as, redirectUri) {
2651
2781
  if (!as.registrationEndpoint) {
2652
- throw new HTTPException4(422, { message: "authorization server does not support dynamic client registration" });
2782
+ throw new HTTPException6(422, { message: "authorization server does not support dynamic client registration" });
2653
2783
  }
2654
2784
  await assertOAuthFetchAllowed(as.registrationEndpoint, settings);
2655
2785
  const response = await fetchOAuth(as.registrationEndpoint, settings, {
@@ -2664,12 +2794,12 @@ async function dynamicClientRegistration(settings, as, redirectUri) {
2664
2794
  })
2665
2795
  });
2666
2796
  if (!response.ok) {
2667
- throw new HTTPException4(422, { message: `dynamic client registration failed with HTTP ${response.status}` });
2797
+ throw new HTTPException6(422, { message: `dynamic client registration failed with HTTP ${response.status}` });
2668
2798
  }
2669
2799
  const payload = await response.json();
2670
2800
  const clientId = stringValue(payload.client_id);
2671
2801
  if (!clientId) {
2672
- throw new HTTPException4(422, { message: "dynamic client registration response did not include client_id" });
2802
+ throw new HTTPException6(422, { message: "dynamic client registration response did not include client_id" });
2673
2803
  }
2674
2804
  const clientSecret = stringValue(payload.client_secret);
2675
2805
  return {
@@ -2698,19 +2828,21 @@ function buildAuthorizationUrl(input) {
2698
2828
  function readOAuthState(state, settings) {
2699
2829
  const payload = readSignedState2(state, requireIntegrationsStateSecret(settings));
2700
2830
  if (!payload) {
2701
- throw new HTTPException4(400, { message: "invalid or expired OAuth state" });
2831
+ throw new HTTPException6(400, { message: "invalid or expired OAuth state" });
2702
2832
  }
2703
2833
  const nowSeconds = Math.floor(Date.now() / 1e3);
2704
2834
  const iat = numberValue(payload.iat);
2705
2835
  if (iat === void 0 || nowSeconds - iat > oauthStateTtlMs / 1e3 || nowSeconds < iat) {
2706
- throw new HTTPException4(400, { message: "invalid or expired OAuth state" });
2836
+ throw new HTTPException6(400, { message: "invalid or expired OAuth state" });
2707
2837
  }
2838
+ const resource = requiredString(payload.resource, "state.resource");
2708
2839
  const parsed = {
2709
2840
  accountId: requiredString(payload.accountId, "state.accountId"),
2710
2841
  workspaceId: requiredString(payload.workspaceId, "state.workspaceId"),
2711
2842
  subjectId: requiredString(payload.subjectId, "state.subjectId"),
2712
2843
  providerDomain: requiredString(payload.providerDomain, "state.providerDomain"),
2713
- resource: requiredString(payload.resource, "state.resource"),
2844
+ mcpUrl: stringValue(payload.mcpUrl) ?? resource,
2845
+ resource,
2714
2846
  requestedScopes: stringArray(payload.requestedScopes),
2715
2847
  authorizeScopes: stringArray(payload.authorizeScopes),
2716
2848
  encryptedPkceVerifier: requiredString(payload.encryptedPkceVerifier, "state.encryptedPkceVerifier"),
@@ -2745,7 +2877,7 @@ async function clientForState(db, settings, state) {
2745
2877
  if (state.clientRegistrationMethod === "dcr") {
2746
2878
  const stored = await loadIntegrationOAuthClient(db, settings, state.issuer);
2747
2879
  if (!stored || stored.clientId !== state.clientId) {
2748
- throw new HTTPException4(400, { message: "OAuth client registration is no longer available" });
2880
+ throw new HTTPException6(400, { message: "OAuth client registration is no longer available" });
2749
2881
  }
2750
2882
  return {
2751
2883
  method: "dcr",
@@ -2758,7 +2890,7 @@ async function clientForState(db, settings, state) {
2758
2890
  }
2759
2891
  const entry = operatorClientEntryFor(settings, [state.issuer, state.authorizationServer]);
2760
2892
  if (!entry || entry.clientId !== state.clientId) {
2761
- throw new HTTPException4(400, { message: "operator OAuth client credentials are no longer available" });
2893
+ throw new HTTPException6(400, { message: "operator OAuth client credentials are no longer available" });
2762
2894
  }
2763
2895
  return {
2764
2896
  method: "operator",
@@ -2786,7 +2918,8 @@ async function exchangeAuthorizationCode(settings, input) {
2786
2918
  }
2787
2919
  const response = await fetchOAuth(input.tokenEndpoint, settings, { method: "POST", headers, body });
2788
2920
  if (!response.ok) {
2789
- throw new Error(`OAuth token endpoint returned HTTP ${response.status}`);
2921
+ const oauthError = await oauthErrorFromResponse(response);
2922
+ throw new OAuthCallbackStageError("token_exchange", oauthError ?? "token_exchange_failed", new Error(`OAuth token endpoint returned HTTP ${response.status}`));
2790
2923
  }
2791
2924
  const payload = await response.json();
2792
2925
  const accessToken = stringValue(payload.access_token);
@@ -2802,6 +2935,72 @@ async function exchangeAuthorizationCode(settings, input) {
2802
2935
  ...stringValue(payload.scope) ? { scopeText: stringValue(payload.scope) } : {}
2803
2936
  };
2804
2937
  }
2938
+ async function stage(stage2, fallbackReason, fn) {
2939
+ try {
2940
+ return await fn();
2941
+ } catch (error) {
2942
+ if (error instanceof OAuthCallbackStageError) {
2943
+ throw error;
2944
+ }
2945
+ throw new OAuthCallbackStageError(stage2, fallbackReason, error);
2946
+ }
2947
+ }
2948
+ function logOAuthCallbackFailure(observability, error, state) {
2949
+ observability?.error("MCP OAuth callback failed", {
2950
+ "opengeni.oauth.stage": error.stage,
2951
+ "opengeni.oauth.reason": error.reason,
2952
+ "opengeni.oauth.provider_domain": state?.providerDomain,
2953
+ "opengeni.oauth.resource_host": state ? safeHost(state.resource) : void 0,
2954
+ "opengeni.oauth.authorization_server": state?.authorizationServer,
2955
+ "opengeni.oauth.issuer": state?.issuer,
2956
+ "opengeni.oauth.client_registration_method": state?.clientRegistrationMethod,
2957
+ error: sanitizedError(error.cause)
2958
+ });
2959
+ }
2960
+ function logOAuthVerificationWarning(observability, error, state) {
2961
+ observability?.warn("MCP OAuth tools/list verification failed after token exchange", {
2962
+ "opengeni.oauth.stage": error.stage,
2963
+ "opengeni.oauth.reason": error.reason,
2964
+ "opengeni.oauth.provider_domain": state.providerDomain,
2965
+ "opengeni.oauth.resource_host": safeHost(state.resource),
2966
+ "opengeni.oauth.mcp_host": safeHost(state.mcpUrl),
2967
+ "opengeni.oauth.authorization_server": state.authorizationServer,
2968
+ "opengeni.oauth.issuer": state.issuer,
2969
+ "opengeni.oauth.client_registration_method": state.clientRegistrationMethod,
2970
+ error: sanitizedError(error.cause)
2971
+ });
2972
+ }
2973
+ function sanitizedError(error) {
2974
+ if (error instanceof HTTPException6) {
2975
+ return `HTTPException ${error.status}: ${error.message}`;
2976
+ }
2977
+ if (error instanceof Error) {
2978
+ return `${error.name}: ${error.message}`;
2979
+ }
2980
+ return String(error);
2981
+ }
2982
+ function errorMessage(error) {
2983
+ return error instanceof Error ? error.message : String(error);
2984
+ }
2985
+ function safeHost(rawUrl) {
2986
+ try {
2987
+ return new URL(rawUrl).host;
2988
+ } catch {
2989
+ return void 0;
2990
+ }
2991
+ }
2992
+ async function oauthErrorFromResponse(response) {
2993
+ const contentType = response.headers.get("content-type") ?? "";
2994
+ if (!contentType.toLowerCase().includes("application/json")) {
2995
+ return null;
2996
+ }
2997
+ const payload = await response.clone().json().catch(() => null);
2998
+ const error = stringValue(payload?.error);
2999
+ if (!error || !/^[a-zA-Z0-9_.-]{1,80}$/.test(error)) {
3000
+ return null;
3001
+ }
3002
+ return error;
3003
+ }
2805
3004
  async function verifyMcpToolsList(settings, resource, token) {
2806
3005
  await assertOAuthFetchAllowed(resource, settings);
2807
3006
  const client = new Client2({ name: "opengeni-integration-verify", version: "0.1.0" }, { capabilities: {} });
@@ -2822,6 +3021,29 @@ async function verifyMcpToolsList(settings, resource, token) {
2822
3021
  await client.close().catch(() => void 0);
2823
3022
  }
2824
3023
  }
3024
+ async function verifyMcpToolsListNonFatal(observability, settings, state, token) {
3025
+ try {
3026
+ const tools = await stage("tools_list", "tools_list_failed", () => verifyMcpToolsList(settings, state.mcpUrl, token));
3027
+ return {
3028
+ metadata: {
3029
+ status: "ok",
3030
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
3031
+ toolCount: tools.length
3032
+ },
3033
+ tools
3034
+ };
3035
+ } catch (error) {
3036
+ const staged = error instanceof OAuthCallbackStageError ? error : new OAuthCallbackStageError("tools_list", "tools_list_failed", error);
3037
+ logOAuthVerificationWarning(observability, staged, state);
3038
+ return {
3039
+ metadata: {
3040
+ status: "failed",
3041
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
3042
+ reason: staged.reason
3043
+ }
3044
+ };
3045
+ }
3046
+ }
2825
3047
  function credentialBundle(token, state, client) {
2826
3048
  return {
2827
3049
  access_token: token.accessToken,
@@ -2829,6 +3051,7 @@ function credentialBundle(token, state, client) {
2829
3051
  token_type: token.tokenType,
2830
3052
  ...token.expiresAt ? { expires_at: token.expiresAt.toISOString() } : {},
2831
3053
  resource: state.resource,
3054
+ mcp_url: state.mcpUrl,
2832
3055
  ...token.scopeText ? { scope: token.scopeText } : state.authorizeScopes.length ? { scope: state.authorizeScopes.join(" ") } : {},
2833
3056
  token_endpoint: state.tokenEndpoint,
2834
3057
  client_id: client.clientId,
@@ -2845,27 +3068,40 @@ function callbackReturnPath(returnPath, status, params) {
2845
3068
  }
2846
3069
  function canonicalMcpResource(value) {
2847
3070
  if (!value) {
2848
- throw new HTTPException4(400, { message: "mcpUrl is required" });
3071
+ throw new HTTPException6(400, { message: "mcpUrl is required" });
2849
3072
  }
2850
3073
  let url;
2851
3074
  try {
2852
3075
  url = new URL(value);
2853
3076
  } catch {
2854
- throw new HTTPException4(422, { message: "MCP resource URL is invalid" });
3077
+ throw new HTTPException6(422, { message: "MCP resource URL is invalid" });
2855
3078
  }
2856
3079
  url.hash = "";
2857
3080
  return url.toString();
2858
3081
  }
2859
- function canonicalProviderDomain(value) {
2860
- return value.trim().toLowerCase().replace(/^www\./, "");
3082
+ function canonicalOAuthResource(value) {
3083
+ const trimmed = value.trim();
3084
+ if (!trimmed) {
3085
+ throw new HTTPException6(422, { message: "MCP protected resource metadata advertised an invalid resource" });
3086
+ }
3087
+ try {
3088
+ const url = new URL(trimmed);
3089
+ if (url.protocol === "http:" || url.protocol === "https:") {
3090
+ url.hash = "";
3091
+ return url.toString();
3092
+ }
3093
+ return trimmed;
3094
+ } catch {
3095
+ throw new HTTPException6(422, { message: "MCP protected resource metadata advertised an invalid resource" });
3096
+ }
2861
3097
  }
2862
3098
  function safeReturnPath(value) {
2863
3099
  if (!value.startsWith("/") || value.startsWith("//")) {
2864
- throw new HTTPException4(400, { message: "OAuth returnPath must be a relative path" });
3100
+ throw new HTTPException6(400, { message: "OAuth returnPath must be a relative path" });
2865
3101
  }
2866
3102
  const parsed = new URL(value, "https://opengeni.local");
2867
3103
  if (parsed.origin !== "https://opengeni.local") {
2868
- throw new HTTPException4(400, { message: "OAuth returnPath must be a relative path" });
3104
+ throw new HTTPException6(400, { message: "OAuth returnPath must be a relative path" });
2869
3105
  }
2870
3106
  return `${parsed.pathname}${parsed.search}${parsed.hash}`;
2871
3107
  }
@@ -2887,39 +3123,39 @@ async function fetchOAuth(rawUrl, settings, init = {}, hop = 0) {
2887
3123
  return response;
2888
3124
  }
2889
3125
  if (hop >= 3) {
2890
- throw new HTTPException4(422, { message: "OAuth fetch exceeded maximum redirect hops" });
3126
+ throw new HTTPException6(422, { message: "OAuth fetch exceeded maximum redirect hops" });
2891
3127
  }
2892
3128
  const location = response.headers.get("location");
2893
3129
  if (!location) {
2894
- throw new HTTPException4(422, { message: "OAuth fetch redirect was missing Location" });
3130
+ throw new HTTPException6(422, { message: "OAuth fetch redirect was missing Location" });
2895
3131
  }
2896
3132
  let nextUrl;
2897
3133
  try {
2898
3134
  nextUrl = new URL(location, rawUrl).toString();
2899
3135
  } catch {
2900
- throw new HTTPException4(422, { message: "OAuth fetch redirect Location was invalid" });
3136
+ throw new HTTPException6(422, { message: "OAuth fetch redirect Location was invalid" });
2901
3137
  }
2902
3138
  return await fetchOAuth(nextUrl, settings, init, hop + 1);
2903
3139
  }
2904
3140
  async function assertOAuthFetchAllowed(rawUrl, settings) {
2905
3141
  const url = new URL(rawUrl);
2906
3142
  if (!["https:", "http:"].includes(url.protocol)) {
2907
- throw new HTTPException4(422, { message: "OAuth discovery only supports http and https URLs" });
3143
+ throw new HTTPException6(422, { message: "OAuth discovery only supports http and https URLs" });
2908
3144
  }
2909
3145
  if (settings.integrationsAllowPrivateNetworkTargets || ["local", "test"].includes(settings.environment)) {
2910
3146
  return;
2911
3147
  }
2912
3148
  if (url.protocol !== "https:") {
2913
- throw new HTTPException4(422, { message: "OAuth discovery targets must use https outside local/test" });
3149
+ throw new HTTPException6(422, { message: "OAuth discovery targets must use https outside local/test" });
2914
3150
  }
2915
3151
  const hostname = url.hostname.toLowerCase();
2916
3152
  if (hostname === "localhost" || hostname.endsWith(".localhost")) {
2917
- throw new HTTPException4(422, { message: "OAuth discovery may not target localhost" });
3153
+ throw new HTTPException6(422, { message: "OAuth discovery may not target localhost" });
2918
3154
  }
2919
3155
  const literal2 = isIP(hostname);
2920
3156
  const addresses = literal2 ? [hostname] : (await lookup(hostname, { all: true })).map((entry) => entry.address);
2921
3157
  if (addresses.some(isPrivateAddress)) {
2922
- throw new HTTPException4(422, { message: "OAuth discovery may not target private network addresses" });
3158
+ throw new HTTPException6(422, { message: "OAuth discovery may not target private network addresses" });
2923
3159
  }
2924
3160
  }
2925
3161
  function parseWwwAuthenticate(header) {
@@ -2978,7 +3214,7 @@ function registrationMethod(value) {
2978
3214
  if (value === "operator" || value === "cimd" || value === "dcr") {
2979
3215
  return value;
2980
3216
  }
2981
- throw new HTTPException4(400, { message: "invalid OAuth state" });
3217
+ throw new HTTPException6(400, { message: "invalid OAuth state" });
2982
3218
  }
2983
3219
  function expiresAtFromTokenResponse(payload) {
2984
3220
  const expiresAt = stringValue(payload.expires_at);
@@ -3013,17 +3249,17 @@ function numberValue(value) {
3013
3249
  function requiredString(value, field) {
3014
3250
  const result = stringValue(value);
3015
3251
  if (!result) {
3016
- throw new HTTPException4(400, { message: `invalid OAuth state: missing ${field}` });
3252
+ throw new HTTPException6(400, { message: `invalid OAuth state: missing ${field}` });
3017
3253
  }
3018
3254
  return result;
3019
3255
  }
3020
3256
 
3021
3257
  // src/routes/connections.ts
3022
3258
  function registerConnectionRoutes(app, deps) {
3023
- const { db, settings } = deps;
3259
+ const { db, settings, observability } = deps;
3024
3260
  function assertIntegrationsEnabled() {
3025
3261
  if (!settings.integrationsEnabled) {
3026
- throw new HTTPException5(404, { message: "integrations are not enabled for this deployment" });
3262
+ throw new HTTPException7(404, { message: "integrations are not enabled for this deployment" });
3027
3263
  }
3028
3264
  }
3029
3265
  app.get("/v1/workspaces/:workspaceId/connections", async (c) => {
@@ -3043,7 +3279,7 @@ function registerConnectionRoutes(app, deps) {
3043
3279
  accountId: grant.accountId,
3044
3280
  workspaceId,
3045
3281
  subjectId,
3046
- providerDomain: payload.providerDomain,
3282
+ providerDomain: canonicalProviderDomain(payload.providerDomain),
3047
3283
  kind: payload.kind,
3048
3284
  credentialEncrypted: encryptCredentialBundle(key, payload.credential),
3049
3285
  grantedScopes: payload.grantedScopes,
@@ -3058,7 +3294,7 @@ function registerConnectionRoutes(app, deps) {
3058
3294
  const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:read");
3059
3295
  const connection = await getConnectionMetadata2(db, workspaceId, c.req.param("connectionId"), grant.subjectId);
3060
3296
  if (!connection) {
3061
- throw new HTTPException5(404, { message: "connection not found" });
3297
+ throw new HTTPException7(404, { message: "connection not found" });
3062
3298
  }
3063
3299
  return c.json(ConnectionResponse.parse({ connection }));
3064
3300
  });
@@ -3068,10 +3304,10 @@ function registerConnectionRoutes(app, deps) {
3068
3304
  const payload = UpdateConnectionRequest.parse(await c.req.json());
3069
3305
  if (payload.status !== void 0) {
3070
3306
  if (payload.status !== "active") {
3071
- throw new HTTPException5(400, { message: 'status can only be set to "active"; use DELETE to revoke' });
3307
+ throw new HTTPException7(400, { message: 'status can only be set to "active"; use DELETE to revoke' });
3072
3308
  }
3073
3309
  if (payload.credential === void 0) {
3074
- throw new HTTPException5(400, { message: "reactivating a connection requires a new credential" });
3310
+ throw new HTTPException7(400, { message: "reactivating a connection requires a new credential" });
3075
3311
  }
3076
3312
  }
3077
3313
  const key = payload.credential === void 0 ? null : requireEnvironmentEncryption3(settings);
@@ -3081,7 +3317,7 @@ function registerConnectionRoutes(app, deps) {
3081
3317
  connectionId: c.req.param("connectionId"),
3082
3318
  visibleToSubjectId: grant.subjectId,
3083
3319
  updatedBySubjectId: grant.subjectId,
3084
- ...payload.providerDomain !== void 0 ? { providerDomain: payload.providerDomain } : {},
3320
+ ...payload.providerDomain !== void 0 ? { providerDomain: canonicalProviderDomain(payload.providerDomain) } : {},
3085
3321
  ...subjectId !== void 0 ? { subjectId } : {},
3086
3322
  ...payload.kind !== void 0 ? { kind: payload.kind } : {},
3087
3323
  ...payload.status !== void 0 ? { status: payload.status } : {},
@@ -3091,7 +3327,7 @@ function registerConnectionRoutes(app, deps) {
3091
3327
  ...payload.metadata !== void 0 ? { metadata: payload.metadata } : {}
3092
3328
  });
3093
3329
  if (!connection) {
3094
- throw new HTTPException5(404, { message: "connection not found" });
3330
+ throw new HTTPException7(404, { message: "connection not found" });
3095
3331
  }
3096
3332
  return c.json(ConnectionResponse.parse({ connection }));
3097
3333
  });
@@ -3100,7 +3336,7 @@ function registerConnectionRoutes(app, deps) {
3100
3336
  const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:write");
3101
3337
  const connection = await revokeConnection(db, workspaceId, c.req.param("connectionId"), grant.subjectId);
3102
3338
  if (!connection) {
3103
- throw new HTTPException5(404, { message: "connection not found" });
3339
+ throw new HTTPException7(404, { message: "connection not found" });
3104
3340
  }
3105
3341
  return c.json(ConnectionResponse.parse({ connection }));
3106
3342
  });
@@ -3110,10 +3346,10 @@ function registerConnectionRoutes(app, deps) {
3110
3346
  const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:write");
3111
3347
  const parsed = OAuthStartRequest.safeParse(await c.req.json());
3112
3348
  if (!parsed.success) {
3113
- throw new HTTPException5(400, { message: parsed.error.issues[0]?.message ?? "invalid OAuth start request" });
3349
+ throw new HTTPException7(400, { message: parsed.error.issues[0]?.message ?? "invalid OAuth start request" });
3114
3350
  }
3115
3351
  const payload = parsed.data;
3116
- const result = await startMcpOAuth({ db, settings }, {
3352
+ const result = await startMcpOAuth({ db, settings, observability }, {
3117
3353
  accountId: grant.accountId,
3118
3354
  workspaceId,
3119
3355
  subjectId: grant.subjectId,
@@ -3124,7 +3360,7 @@ function registerConnectionRoutes(app, deps) {
3124
3360
  });
3125
3361
  app.get("/v1/integrations/oauth/callback", async (c) => {
3126
3362
  assertIntegrationsEnabled();
3127
- const result = await completeMcpOAuthCallback({ db, settings }, {
3363
+ const result = await completeMcpOAuthCallback({ db, settings, observability }, {
3128
3364
  code: c.req.query("code"),
3129
3365
  state: c.req.query("state"),
3130
3366
  requestUrl: c.req.url
@@ -3149,7 +3385,7 @@ function writableSubjectId(requested, grantSubjectId) {
3149
3385
  return null;
3150
3386
  }
3151
3387
  if (requested !== grantSubjectId) {
3152
- throw new HTTPException5(403, { message: "cannot write a connection for another subject" });
3388
+ throw new HTTPException7(403, { message: "cannot write a connection for another subject" });
3153
3389
  }
3154
3390
  return requested;
3155
3391
  }
@@ -3187,7 +3423,7 @@ import {
3187
3423
  searchDocuments as searchDocuments2
3188
3424
  } from "@opengeni/documents";
3189
3425
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3190
- import { HTTPException as HTTPException6 } from "hono/http-exception";
3426
+ import { HTTPException as HTTPException8 } from "hono/http-exception";
3191
3427
  import { requireAccessGrant as requireAccessGrant4 } from "@opengeni/core";
3192
3428
  import { recordWorkspaceUsage as recordWorkspaceUsage2, requireLimit as requireLimit2 } from "@opengeni/core";
3193
3429
 
@@ -3341,7 +3577,7 @@ function registerDocumentRoutes(app, deps) {
3341
3577
  await requireAccessGrant4(c, deps, workspaceId, "documents:search");
3342
3578
  const base = await getDocumentBase(db, workspaceId, c.req.param("baseId"));
3343
3579
  if (!base) {
3344
- throw new HTTPException6(404, { message: "document base not found" });
3580
+ throw new HTTPException8(404, { message: "document base not found" });
3345
3581
  }
3346
3582
  return c.json(DocumentBase.parse(base));
3347
3583
  });
@@ -3349,7 +3585,7 @@ function registerDocumentRoutes(app, deps) {
3349
3585
  const workspaceId = c.req.param("workspaceId");
3350
3586
  const grant = await requireAccessGrant4(c, deps, workspaceId, "documents:manage");
3351
3587
  if (!objectStorage) {
3352
- throw new HTTPException6(503, { message: "object storage is not configured" });
3588
+ throw new HTTPException8(503, { message: "object storage is not configured" });
3353
3589
  }
3354
3590
  await requireLimit2(deps, { accountId: grant.accountId, workspaceId, action: "document:index", quantity: 0 });
3355
3591
  const payload = AddDocumentRequest.parse(await c.req.json());
@@ -3399,19 +3635,19 @@ function registerDocumentRoutes(app, deps) {
3399
3635
  const workspaceId = c.req.param("workspaceId");
3400
3636
  const grant = await requireAccessGrant4(c, deps, workspaceId, "documents:manage");
3401
3637
  if (!objectStorage) {
3402
- throw new HTTPException6(503, { message: "object storage is not configured" });
3638
+ throw new HTTPException8(503, { message: "object storage is not configured" });
3403
3639
  }
3404
3640
  await requireLimit2(deps, { accountId: grant.accountId, workspaceId, action: "document:index", quantity: 0 });
3405
3641
  try {
3406
3642
  const document = await getDocument(db, workspaceId, c.req.param("documentId"));
3407
3643
  if (!document) {
3408
- throw new HTTPException6(404, { message: "document not found" });
3644
+ throw new HTTPException8(404, { message: "document not found" });
3409
3645
  }
3410
3646
  if (document.status !== "failed") {
3411
- throw new HTTPException6(422, { message: "only failed documents can be retried" });
3647
+ throw new HTTPException8(422, { message: "only failed documents can be retried" });
3412
3648
  }
3413
3649
  if (document.baseId !== c.req.param("baseId")) {
3414
- throw new HTTPException6(404, { message: "document not found" });
3650
+ throw new HTTPException8(404, { message: "document not found" });
3415
3651
  }
3416
3652
  const queued = await queueDocumentForReindex(db, workspaceId, document.id);
3417
3653
  const indexed = await documentIndexer.indexDocument({ accountId: grant.accountId, workspaceId, documentId: document.id }) ?? queued;
@@ -3430,7 +3666,7 @@ function registerDocumentRoutes(app, deps) {
3430
3666
  }
3431
3667
  return c.json(Document.parse(indexed));
3432
3668
  } catch (error) {
3433
- if (error instanceof HTTPException6) {
3669
+ if (error instanceof HTTPException8) {
3434
3670
  throw error;
3435
3671
  }
3436
3672
  throw documentHttpException(error);
@@ -3442,7 +3678,7 @@ function registerDocumentRoutes(app, deps) {
3442
3678
  const payload = DocumentSearchRequest.parse(await c.req.json());
3443
3679
  const base = await getDocumentBase(db, workspaceId, c.req.param("baseId"));
3444
3680
  if (!base) {
3445
- throw new HTTPException6(404, { message: "document base not found" });
3681
+ throw new HTTPException8(404, { message: "document base not found" });
3446
3682
  }
3447
3683
  return c.json({
3448
3684
  results: await searchDocuments2(db, {
@@ -3483,7 +3719,7 @@ function registerDocumentRoutes(app, deps) {
3483
3719
  limit: c.req.query("limit") ? Number(c.req.query("limit")) : void 0
3484
3720
  });
3485
3721
  if (!parsed.success) {
3486
- throw new HTTPException6(400, { message: "invalid knowledge memory query parameters" });
3722
+ throw new HTTPException8(400, { message: "invalid knowledge memory query parameters" });
3487
3723
  }
3488
3724
  return c.json((await listKnowledgeMemories2(db, workspaceId, parsed.data)).map((memory) => KnowledgeMemory.parse(memory)));
3489
3725
  });
@@ -3492,7 +3728,7 @@ function registerDocumentRoutes(app, deps) {
3492
3728
  await requireAccessGrant4(c, deps, workspaceId, "documents:search");
3493
3729
  const memory = await getKnowledgeMemory(db, workspaceId, c.req.param("memoryId"));
3494
3730
  if (!memory) {
3495
- throw new HTTPException6(404, { message: "knowledge memory not found" });
3731
+ throw new HTTPException8(404, { message: "knowledge memory not found" });
3496
3732
  }
3497
3733
  return c.json(KnowledgeMemory.parse(memory));
3498
3734
  });
@@ -3533,12 +3769,12 @@ function registerDocumentRoutes(app, deps) {
3533
3769
  function documentHttpException(error) {
3534
3770
  const message = error instanceof Error ? error.message : String(error);
3535
3771
  if (message.includes("not found")) {
3536
- return new HTTPException6(404, { message });
3772
+ return new HTTPException8(404, { message });
3537
3773
  }
3538
3774
  if (message.includes("pending") || message.includes("failed") || message.includes("deleted")) {
3539
- return new HTTPException6(422, { message });
3775
+ return new HTTPException8(422, { message });
3540
3776
  }
3541
- return new HTTPException6(500, { message });
3777
+ return new HTTPException8(500, { message });
3542
3778
  }
3543
3779
 
3544
3780
  // src/routes/enrollments.ts
@@ -3564,7 +3800,7 @@ import {
3564
3800
  listEnrollments,
3565
3801
  revokeEnrollment
3566
3802
  } from "@opengeni/db";
3567
- import { HTTPException as HTTPException7 } from "hono/http-exception";
3803
+ import { HTTPException as HTTPException9 } from "hono/http-exception";
3568
3804
  import { requireAccessGrant as requireAccessGrant5 } from "@opengeni/core";
3569
3805
 
3570
3806
  // src/sandbox/enrollment.ts
@@ -3848,7 +4084,7 @@ function registerEnrollmentRoutes(app, deps) {
3848
4084
  const { settings, db } = deps;
3849
4085
  function assertSelfhostedEnabled() {
3850
4086
  if (!settings.sandboxSelfhostedEnabled) {
3851
- throw new HTTPException7(404, { message: "selfhosted enrollment is not enabled for this deployment" });
4087
+ throw new HTTPException9(404, { message: "selfhosted enrollment is not enabled for this deployment" });
3852
4088
  }
3853
4089
  }
3854
4090
  const startLimiter = new TokenBucket({ capacity: 10, refillPerSecond: 0.5 });
@@ -3858,7 +4094,7 @@ function registerEnrollmentRoutes(app, deps) {
3858
4094
  function rateLimit(c, limiter) {
3859
4095
  const ip = clientIp(c);
3860
4096
  if (!limiter.take(ip)) {
3861
- throw new HTTPException7(429, { message: "too many requests; slow down" });
4097
+ throw new HTTPException9(429, { message: "too many requests; slow down" });
3862
4098
  }
3863
4099
  }
3864
4100
  app.post("/v1/enrollments/device/start", async (c) => {
@@ -3866,12 +4102,12 @@ function registerEnrollmentRoutes(app, deps) {
3866
4102
  rateLimit(c, startLimiter);
3867
4103
  const parsed = DeviceEnrollmentStartRequest.safeParse(await c.req.json().catch(() => null));
3868
4104
  if (!parsed.success) {
3869
- throw new HTTPException7(400, { message: "invalid device-start request" });
4105
+ throw new HTTPException9(400, { message: "invalid device-start request" });
3870
4106
  }
3871
4107
  const body = parsed.data;
3872
4108
  const workspace = await getWorkspace(db, body.workspaceId);
3873
4109
  if (!workspace) {
3874
- throw new HTTPException7(404, { message: "workspace not found" });
4110
+ throw new HTTPException9(404, { message: "workspace not found" });
3875
4111
  }
3876
4112
  const result = await startDeviceEnrollment({ db, settings }, {
3877
4113
  accountId: workspace.accountId,
@@ -3892,7 +4128,7 @@ function registerEnrollmentRoutes(app, deps) {
3892
4128
  rateLimit(c, pollLimiter);
3893
4129
  const parsed = DeviceEnrollmentPollRequest.safeParse(await c.req.json().catch(() => null));
3894
4130
  if (!parsed.success) {
3895
- throw new HTTPException7(400, { message: "invalid device-poll request" });
4131
+ throw new HTTPException9(400, { message: "invalid device-poll request" });
3896
4132
  }
3897
4133
  const result = await pollDeviceEnrollment({ db, settings }, { deviceCode: parsed.data.deviceCode });
3898
4134
  return c.json(result, 200);
@@ -3902,16 +4138,16 @@ function registerEnrollmentRoutes(app, deps) {
3902
4138
  rateLimit(c, lookupLimiter);
3903
4139
  const parsed = DeviceEnrollmentLookupRequest.safeParse(await c.req.json().catch(() => null));
3904
4140
  if (!parsed.success) {
3905
- throw new HTTPException7(400, { message: "invalid device-lookup request" });
4141
+ throw new HTTPException9(400, { message: "invalid device-lookup request" });
3906
4142
  }
3907
4143
  const record3 = await lookupDeviceEnrollment({ db, settings }, { userCode: parsed.data.userCode });
3908
4144
  if (!record3) {
3909
- throw new HTTPException7(404, { message: "no pending enrollment for that code" });
4145
+ throw new HTTPException9(404, { message: "no pending enrollment for that code" });
3910
4146
  }
3911
4147
  try {
3912
4148
  await requireAccessGrant5(c, deps, record3.workspaceId, "enrollments:read");
3913
4149
  } catch {
3914
- throw new HTTPException7(404, { message: "no pending enrollment for that code" });
4150
+ throw new HTTPException9(404, { message: "no pending enrollment for that code" });
3915
4151
  }
3916
4152
  return c.json(DeviceEnrollmentLookupResponse.parse(toLookupResponse(record3)), 200);
3917
4153
  });
@@ -3920,7 +4156,7 @@ function registerEnrollmentRoutes(app, deps) {
3920
4156
  rateLimit(c, exchangeLimiter);
3921
4157
  const parsed = EnrollTokenExchangeRequest.safeParse(await c.req.json().catch(() => null));
3922
4158
  if (!parsed.success) {
3923
- throw new HTTPException7(400, { message: "invalid enroll-token-exchange request" });
4159
+ throw new HTTPException9(400, { message: "invalid enroll-token-exchange request" });
3924
4160
  }
3925
4161
  const body = parsed.data;
3926
4162
  const result = await exchangeEnrollToken({ db, settings }, {
@@ -3933,9 +4169,9 @@ function registerEnrollmentRoutes(app, deps) {
3933
4169
  });
3934
4170
  if (!result.ok) {
3935
4171
  if (result.reason === "disabled") {
3936
- throw new HTTPException7(503, { message: "enrollment credential plane is not configured" });
4172
+ throw new HTTPException9(503, { message: "enrollment credential plane is not configured" });
3937
4173
  }
3938
- throw new HTTPException7(401, { message: "invalid or expired enroll token" });
4174
+ throw new HTTPException9(401, { message: "invalid or expired enroll token" });
3939
4175
  }
3940
4176
  return c.json(EnrollTokenExchangeResponse.parse({ credentials: result.credentials }), 201);
3941
4177
  });
@@ -3945,7 +4181,7 @@ function registerEnrollmentRoutes(app, deps) {
3945
4181
  assertSelfhostedEnabled();
3946
4182
  const parsed = DeviceEnrollmentApproveRequest.safeParse(await c.req.json().catch(() => null));
3947
4183
  if (!parsed.success) {
3948
- throw new HTTPException7(400, { message: "invalid device-approve request" });
4184
+ throw new HTTPException9(400, { message: "invalid device-approve request" });
3949
4185
  }
3950
4186
  const body = parsed.data;
3951
4187
  const approved = await approveDeviceEnrollment({ db, settings }, {
@@ -3958,7 +4194,7 @@ function registerEnrollmentRoutes(app, deps) {
3958
4194
  approvedBySubjectLabel: grant.subjectLabel ?? null
3959
4195
  });
3960
4196
  if (!approved) {
3961
- throw new HTTPException7(404, { message: "no pending enrollment for that code" });
4197
+ throw new HTTPException9(404, { message: "no pending enrollment for that code" });
3962
4198
  }
3963
4199
  return c.json(DeviceEnrollmentApproveResponse.parse({
3964
4200
  approved: true,
@@ -3973,7 +4209,7 @@ function registerEnrollmentRoutes(app, deps) {
3973
4209
  assertSelfhostedEnabled();
3974
4210
  const parsed = DeviceEnrollmentDenyRequest.safeParse(await c.req.json().catch(() => null));
3975
4211
  if (!parsed.success) {
3976
- throw new HTTPException7(400, { message: "invalid device-deny request" });
4212
+ throw new HTTPException9(400, { message: "invalid device-deny request" });
3977
4213
  }
3978
4214
  const result = await denyDeviceEnrollment({ db, settings }, {
3979
4215
  accountId: grant.accountId,
@@ -3988,7 +4224,7 @@ function registerEnrollmentRoutes(app, deps) {
3988
4224
  assertSelfhostedEnabled();
3989
4225
  const parsed = MintEnrollTokenRequest.safeParse(await c.req.json().catch(() => ({})));
3990
4226
  if (!parsed.success) {
3991
- throw new HTTPException7(400, { message: "invalid mint-enroll-token request" });
4227
+ throw new HTTPException9(400, { message: "invalid mint-enroll-token request" });
3992
4228
  }
3993
4229
  const minted = await mintEnrollToken({ db, settings }, {
3994
4230
  accountId: grant.accountId,
@@ -3996,7 +4232,7 @@ function registerEnrollmentRoutes(app, deps) {
3996
4232
  allowScreenControl: parsed.data.allowScreenControl
3997
4233
  });
3998
4234
  if (!minted) {
3999
- throw new HTTPException7(503, { message: "enrollment credential plane is not configured" });
4235
+ throw new HTTPException9(503, { message: "enrollment credential plane is not configured" });
4000
4236
  }
4001
4237
  return c.json(MintEnrollTokenResponse.parse(minted), 201);
4002
4238
  });
@@ -4080,7 +4316,7 @@ import {
4080
4316
  getEnrollment as getEnrollment2,
4081
4317
  readMachineMetricsSeries
4082
4318
  } from "@opengeni/db";
4083
- import { HTTPException as HTTPException8 } from "hono/http-exception";
4319
+ import { HTTPException as HTTPException10 } from "hono/http-exception";
4084
4320
  import { requireAccessGrant as requireAccessGrant6 } from "@opengeni/core";
4085
4321
  import { buildFleetContextForSession as buildFleetContextForSession2, swapActiveSandbox as swapActiveSandbox2 } from "@opengeni/core";
4086
4322
 
@@ -4255,7 +4491,7 @@ function registerMachineRoutes(app, deps) {
4255
4491
  const { settings, db, bus } = deps;
4256
4492
  function assertSelfhostedEnabled() {
4257
4493
  if (!settings.sandboxSelfhostedEnabled) {
4258
- throw new HTTPException8(404, { message: "selfhosted machines are not enabled for this deployment" });
4494
+ throw new HTTPException10(404, { message: "selfhosted machines are not enabled for this deployment" });
4259
4495
  }
4260
4496
  }
4261
4497
  app.get("/v1/workspaces/:workspaceId/machines", async (c) => {
@@ -4273,7 +4509,7 @@ function registerMachineRoutes(app, deps) {
4273
4509
  const enrollmentId = c.req.param("enrollmentId");
4274
4510
  const enrollment = await getEnrollment2(db, workspaceId, enrollmentId);
4275
4511
  if (!enrollment) {
4276
- throw new HTTPException8(404, { message: "machine not found in this workspace" });
4512
+ throw new HTTPException10(404, { message: "machine not found in this workspace" });
4277
4513
  }
4278
4514
  const windowMs = SERIES_WINDOWS_MS[c.req.query("window") ?? ""] ?? DEFAULT_SERIES_WINDOW_MS;
4279
4515
  const since = new Date(Date.now() - windowMs);
@@ -4318,7 +4554,7 @@ import {
4318
4554
  setWorkspaceEnvironmentVariable as setWorkspaceEnvironmentVariable2,
4319
4555
  updateWorkspaceEnvironment
4320
4556
  } from "@opengeni/db";
4321
- import { HTTPException as HTTPException9 } from "hono/http-exception";
4557
+ import { HTTPException as HTTPException11 } from "hono/http-exception";
4322
4558
  import { requireAccessGrant as requireAccessGrant7 } from "@opengeni/core";
4323
4559
  import {
4324
4560
  assertAllowedEnvironmentVariableName as assertAllowedEnvironmentVariableName2,
@@ -4342,21 +4578,21 @@ function registerEnvironmentRoutes(app, deps) {
4342
4578
  const payload = CreateWorkspaceEnvironmentRequest.parse(await c.req.json());
4343
4579
  const name = trimmedEnvironmentName(payload.name);
4344
4580
  if (payload.variables.length > MAX_VARIABLES_PER_ENVIRONMENT2) {
4345
- throw new HTTPException9(422, { message: `an environment supports at most ${MAX_VARIABLES_PER_ENVIRONMENT2} variables` });
4581
+ throw new HTTPException11(422, { message: `an environment supports at most ${MAX_VARIABLES_PER_ENVIRONMENT2} variables` });
4346
4582
  }
4347
4583
  const variableNames = /* @__PURE__ */ new Set();
4348
4584
  for (const variable of payload.variables) {
4349
4585
  assertAllowedEnvironmentVariableName2(variable.name);
4350
4586
  if (variableNames.has(variable.name)) {
4351
- throw new HTTPException9(422, { message: `duplicate environment variable name: ${variable.name}` });
4587
+ throw new HTTPException11(422, { message: `duplicate environment variable name: ${variable.name}` });
4352
4588
  }
4353
4589
  variableNames.add(variable.name);
4354
4590
  }
4355
4591
  if (await countWorkspaceEnvironments2(db, workspaceId) >= MAX_ENVIRONMENTS_PER_WORKSPACE2) {
4356
- throw new HTTPException9(422, { message: `a workspace supports at most ${MAX_ENVIRONMENTS_PER_WORKSPACE2} environments` });
4592
+ throw new HTTPException11(422, { message: `a workspace supports at most ${MAX_ENVIRONMENTS_PER_WORKSPACE2} environments` });
4357
4593
  }
4358
4594
  if (await getWorkspaceEnvironmentByName2(db, workspaceId, name)) {
4359
- throw new HTTPException9(409, { message: `environment name is already in use: ${name}` });
4595
+ throw new HTTPException11(409, { message: `environment name is already in use: ${name}` });
4360
4596
  }
4361
4597
  const created = await createWorkspaceEnvironment2(db, {
4362
4598
  accountId: grant.accountId,
@@ -4385,7 +4621,7 @@ function registerEnvironmentRoutes(app, deps) {
4385
4621
  if (name !== void 0 && name !== environment.name) {
4386
4622
  const existing = await getWorkspaceEnvironmentByName2(db, workspaceId, name);
4387
4623
  if (existing && existing.id !== environment.id) {
4388
- throw new HTTPException9(409, { message: `environment name is already in use: ${name}` });
4624
+ throw new HTTPException11(409, { message: `environment name is already in use: ${name}` });
4389
4625
  }
4390
4626
  }
4391
4627
  const updated = await updateWorkspaceEnvironment(db, workspaceId, environment.id, {
@@ -4401,11 +4637,11 @@ function registerEnvironmentRoutes(app, deps) {
4401
4637
  const environment = await requireEnvironmentForApi(db, workspaceId, c.req.param("environmentId"));
4402
4638
  const attachedTasks = await countScheduledTasksUsingEnvironment(db, workspaceId, environment.id);
4403
4639
  if (attachedTasks > 0) {
4404
- throw new HTTPException9(409, { message: `environment is attached to ${attachedTasks} scheduled task(s); detach first` });
4640
+ throw new HTTPException11(409, { message: `environment is attached to ${attachedTasks} scheduled task(s); detach first` });
4405
4641
  }
4406
4642
  const activeSessions = await countActiveSessionsUsingEnvironment(db, workspaceId, environment.id);
4407
4643
  if (activeSessions > 0) {
4408
- throw new HTTPException9(409, { message: `environment is attached to ${activeSessions} active session(s); wait for them to finish or cancel them first` });
4644
+ throw new HTTPException11(409, { message: `environment is attached to ${activeSessions} active session(s); wait for them to finish or cancel them first` });
4409
4645
  }
4410
4646
  await deleteWorkspaceEnvironment(db, workspaceId, environment.id);
4411
4647
  await recordEnvironmentAuditEvent2(db, { grant, action: "environment.deleted", environmentId: environment.id });
@@ -4420,7 +4656,7 @@ function registerEnvironmentRoutes(app, deps) {
4420
4656
  const payload = SetWorkspaceEnvironmentVariableRequest.parse(await c.req.json());
4421
4657
  const exists = environment.variables.some((variable) => variable.name === name);
4422
4658
  if (!exists && environment.variables.length >= MAX_VARIABLES_PER_ENVIRONMENT2) {
4423
- throw new HTTPException9(422, { message: `an environment supports at most ${MAX_VARIABLES_PER_ENVIRONMENT2} variables` });
4659
+ throw new HTTPException11(422, { message: `an environment supports at most ${MAX_VARIABLES_PER_ENVIRONMENT2} variables` });
4424
4660
  }
4425
4661
  const metadata = await setWorkspaceEnvironmentVariable2(db, {
4426
4662
  accountId: grant.accountId,
@@ -4439,7 +4675,7 @@ function registerEnvironmentRoutes(app, deps) {
4439
4675
  const environment = await requireEnvironmentForApi(db, workspaceId, c.req.param("environmentId"));
4440
4676
  const deleted = await deleteWorkspaceEnvironmentVariable(db, workspaceId, environment.id, name);
4441
4677
  if (!deleted) {
4442
- throw new HTTPException9(404, { message: "environment variable not found" });
4678
+ throw new HTTPException11(404, { message: "environment variable not found" });
4443
4679
  }
4444
4680
  await recordEnvironmentAuditEvent2(db, { grant, action: "environment.variable.deleted", environmentId: environment.id, variableName: name });
4445
4681
  return c.json({ ok: true });
@@ -4448,7 +4684,7 @@ function registerEnvironmentRoutes(app, deps) {
4448
4684
  function parseVariableName(raw) {
4449
4685
  const parsed = WorkspaceEnvironmentVariableName2.safeParse(raw);
4450
4686
  if (!parsed.success) {
4451
- throw new HTTPException9(422, { message: "environment variable names must match ^[A-Z][A-Z0-9_]*$" });
4687
+ throw new HTTPException11(422, { message: "environment variable names must match ^[A-Z][A-Z0-9_]*$" });
4452
4688
  }
4453
4689
  assertAllowedEnvironmentVariableName2(parsed.data);
4454
4690
  return parsed.data;
@@ -4456,7 +4692,7 @@ function parseVariableName(raw) {
4456
4692
  function trimmedEnvironmentName(name) {
4457
4693
  const trimmed = name.trim();
4458
4694
  if (!trimmed) {
4459
- throw new HTTPException9(422, { message: "environment name is required" });
4695
+ throw new HTTPException11(422, { message: "environment name is required" });
4460
4696
  }
4461
4697
  return trimmed;
4462
4698
  }
@@ -4476,7 +4712,7 @@ import {
4476
4712
  markFileUploadFailed,
4477
4713
  requireFile as requireFile2
4478
4714
  } from "@opengeni/db";
4479
- import { HTTPException as HTTPException10 } from "hono/http-exception";
4715
+ import { HTTPException as HTTPException12 } from "hono/http-exception";
4480
4716
  import { requireAccessGrant as requireAccessGrant8 } from "@opengeni/core";
4481
4717
  import { recordWorkspaceUsage as recordWorkspaceUsage3, requireLimit as requireLimit3 } from "@opengeni/core";
4482
4718
  function registerFileRoutes(app, deps) {
@@ -4485,12 +4721,12 @@ function registerFileRoutes(app, deps) {
4485
4721
  const workspaceId = c.req.param("workspaceId");
4486
4722
  const grant = await requireAccessGrant8(c, deps, workspaceId, "files:upload");
4487
4723
  if (!objectStorage) {
4488
- throw new HTTPException10(503, { message: "object storage is not configured" });
4724
+ throw new HTTPException12(503, { message: "object storage is not configured" });
4489
4725
  }
4490
4726
  const payload = CreateFileUploadRequest.parse(await c.req.json());
4491
4727
  await requireLimit3(deps, { accountId: grant.accountId, workspaceId, action: "file:upload", quantity: payload.sizeBytes });
4492
4728
  if (payload.sizeBytes > objectStorage.maxSinglePutSizeBytes) {
4493
- throw new HTTPException10(413, { message: `file exceeds single PUT limit of ${objectStorage.maxSinglePutSizeBytes} bytes` });
4729
+ throw new HTTPException12(413, { message: `file exceeds single PUT limit of ${objectStorage.maxSinglePutSizeBytes} bytes` });
4494
4730
  }
4495
4731
  const fileId = crypto.randomUUID();
4496
4732
  const safeFilename = sanitizeFilename(payload.filename);
@@ -4526,33 +4762,33 @@ function registerFileRoutes(app, deps) {
4526
4762
  const workspaceId = c.req.param("workspaceId");
4527
4763
  const grant = await requireAccessGrant8(c, deps, workspaceId, "files:upload");
4528
4764
  if (!objectStorage) {
4529
- throw new HTTPException10(503, { message: "object storage is not configured" });
4765
+ throw new HTTPException12(503, { message: "object storage is not configured" });
4530
4766
  }
4531
4767
  const upload = await getFileUpload(db, workspaceId, c.req.param("uploadId"));
4532
4768
  if (!upload) {
4533
- throw new HTTPException10(404, { message: "file upload not found" });
4769
+ throw new HTTPException12(404, { message: "file upload not found" });
4534
4770
  }
4535
4771
  if (upload.status !== "pending") {
4536
- throw new HTTPException10(409, { message: `file upload is ${upload.status}` });
4772
+ throw new HTTPException12(409, { message: `file upload is ${upload.status}` });
4537
4773
  }
4538
4774
  if (upload.expiresAt.getTime() < Date.now()) {
4539
4775
  await markFileUploadFailed(db, workspaceId, upload.id, upload.file.id);
4540
- throw new HTTPException10(409, { message: "file upload has expired" });
4776
+ throw new HTTPException12(409, { message: "file upload has expired" });
4541
4777
  }
4542
4778
  const head = await objectStorage.headFile(upload.file).catch((error) => {
4543
- throw new HTTPException10(409, { message: `uploaded object is not available: ${error instanceof Error ? error.message : String(error)}` });
4779
+ throw new HTTPException12(409, { message: `uploaded object is not available: ${error instanceof Error ? error.message : String(error)}` });
4544
4780
  });
4545
4781
  if (Number(head.ContentLength ?? -1) !== upload.file.sizeBytes) {
4546
4782
  await markFileUploadFailed(db, workspaceId, upload.id, upload.file.id);
4547
- throw new HTTPException10(422, { message: "uploaded object size does not match file metadata" });
4783
+ throw new HTTPException12(422, { message: "uploaded object size does not match file metadata" });
4548
4784
  }
4549
4785
  if (upload.file.contentType && head.ContentType && head.ContentType !== upload.file.contentType) {
4550
4786
  await markFileUploadFailed(db, workspaceId, upload.id, upload.file.id);
4551
- throw new HTTPException10(422, { message: "uploaded object content type does not match file metadata" });
4787
+ throw new HTTPException12(422, { message: "uploaded object content type does not match file metadata" });
4552
4788
  }
4553
4789
  if (upload.file.sha256 && head.Metadata?.sha256 !== upload.file.sha256) {
4554
4790
  await markFileUploadFailed(db, workspaceId, upload.id, upload.file.id);
4555
- throw new HTTPException10(422, { message: "uploaded object checksum metadata does not match file metadata" });
4791
+ throw new HTTPException12(422, { message: "uploaded object checksum metadata does not match file metadata" });
4556
4792
  }
4557
4793
  const file = await completeFileUpload(db, workspaceId, upload.id);
4558
4794
  await recordWorkspaceUsage3(deps, {
@@ -4573,7 +4809,7 @@ function registerFileRoutes(app, deps) {
4573
4809
  await requireAccessGrant8(c, deps, workspaceId, "files:read");
4574
4810
  const file = await requireFile2(db, workspaceId, c.req.param("fileId")).catch(() => null);
4575
4811
  if (!file) {
4576
- throw new HTTPException10(404, { message: "file not found" });
4812
+ throw new HTTPException12(404, { message: "file not found" });
4577
4813
  }
4578
4814
  return c.json(FileAsset.parse(file));
4579
4815
  });
@@ -4581,14 +4817,14 @@ function registerFileRoutes(app, deps) {
4581
4817
  const workspaceId = c.req.param("workspaceId");
4582
4818
  await requireAccessGrant8(c, deps, workspaceId, "files:read");
4583
4819
  if (!objectStorage) {
4584
- throw new HTTPException10(503, { message: "object storage is not configured" });
4820
+ throw new HTTPException12(503, { message: "object storage is not configured" });
4585
4821
  }
4586
4822
  const file = await requireFile2(db, workspaceId, c.req.param("fileId")).catch(() => null);
4587
4823
  if (!file) {
4588
- throw new HTTPException10(404, { message: "file not found" });
4824
+ throw new HTTPException12(404, { message: "file not found" });
4589
4825
  }
4590
4826
  if (file.status !== "ready") {
4591
- throw new HTTPException10(409, { message: `file is ${file.status}` });
4827
+ throw new HTTPException12(409, { message: `file is ${file.status}` });
4592
4828
  }
4593
4829
  const signed = await objectStorage.createGetUrl({ key: file.objectKey });
4594
4830
  return c.json(FileDownloadUrlResponse.parse({
@@ -4607,7 +4843,7 @@ function sanitizeFilename(filename) {
4607
4843
  import { CreateApiKeyRequest, CreateApiKeyResponse } from "@opengeni/contracts";
4608
4844
  import { createApiKey, listApiKeys, revokeApiKey } from "@opengeni/db";
4609
4845
  import { zValidator } from "@hono/zod-validator";
4610
- import { HTTPException as HTTPException11 } from "hono/http-exception";
4846
+ import { HTTPException as HTTPException13 } from "hono/http-exception";
4611
4847
  import { requireAccessGrant as requireAccessGrant9 } from "@opengeni/core";
4612
4848
  import { requireLimit as requireLimit4 } from "@opengeni/core";
4613
4849
  function registerApiKeyRoutes(app, deps) {
@@ -4648,7 +4884,7 @@ function ensureDelegablePermissions(grantPermissions, requested) {
4648
4884
  }
4649
4885
  const missing = requested.filter((permission) => !grantPermissions.includes(permission));
4650
4886
  if (missing.length > 0) {
4651
- throw new HTTPException11(403, { message: `cannot delegate missing permissions: ${missing.join(", ")}` });
4887
+ throw new HTTPException13(403, { message: `cannot delegate missing permissions: ${missing.join(", ")}` });
4652
4888
  }
4653
4889
  }
4654
4890
  function generateApiKeyToken() {
@@ -4680,7 +4916,7 @@ import {
4680
4916
  recordStripeWebhookEvent,
4681
4917
  upsertBillingCustomer
4682
4918
  } from "@opengeni/db";
4683
- import { HTTPException as HTTPException12 } from "hono/http-exception";
4919
+ import { HTTPException as HTTPException14 } from "hono/http-exception";
4684
4920
  import Stripe from "stripe";
4685
4921
  import { requireAccessContext } from "@opengeni/core";
4686
4922
  function registerBillingRoutes(app, deps) {
@@ -4694,7 +4930,7 @@ function registerBillingRoutes(app, deps) {
4694
4930
  const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
4695
4931
  const workspaceId = c.req.query("workspaceId");
4696
4932
  if (workspaceId && !context.workspaceGrants.some((grant) => grant.accountId === accountId && grant.workspaceId === workspaceId)) {
4697
- throw new HTTPException12(403, { message: "missing workspace access for usage query" });
4933
+ throw new HTTPException14(403, { message: "missing workspace access for usage query" });
4698
4934
  }
4699
4935
  return c.json({
4700
4936
  balance: await getBillingBalance(deps.db, accountId),
@@ -4712,12 +4948,12 @@ function registerBillingRoutes(app, deps) {
4712
4948
  });
4713
4949
  app.post("/v1/billing/checkout", async (c) => {
4714
4950
  if (deps.settings.billingMode !== "stripe") {
4715
- throw new HTTPException12(404, { message: "stripe billing is not enabled" });
4951
+ throw new HTTPException14(404, { message: "stripe billing is not enabled" });
4716
4952
  }
4717
4953
  const context = await requireAccessContext(c, deps);
4718
4954
  const parsed = CreateCheckoutRequest.safeParse(await c.req.json());
4719
4955
  if (!parsed.success) {
4720
- throw new HTTPException12(400, { message: parsed.error.issues[0]?.message ?? "invalid checkout request" });
4956
+ throw new HTTPException14(400, { message: parsed.error.issues[0]?.message ?? "invalid checkout request" });
4721
4957
  }
4722
4958
  const body = parsed.data;
4723
4959
  const accountId = requireSelectedAccount(context, body.accountId, "billing:manage");
@@ -4738,7 +4974,7 @@ function registerBillingRoutes(app, deps) {
4738
4974
  idempotencyKey
4739
4975
  }), { idempotencyKey });
4740
4976
  if (!session.url) {
4741
- throw new HTTPException12(502, { message: "Stripe did not return a checkout URL" });
4977
+ throw new HTTPException14(502, { message: "Stripe did not return a checkout URL" });
4742
4978
  }
4743
4979
  return c.json(CreateCheckoutResponse.parse({
4744
4980
  checkoutSessionId: session.id,
@@ -4747,18 +4983,18 @@ function registerBillingRoutes(app, deps) {
4747
4983
  });
4748
4984
  app.post("/v1/webhooks/stripe", async (c) => {
4749
4985
  if (deps.settings.billingMode !== "stripe") {
4750
- throw new HTTPException12(404, { message: "stripe billing is not enabled" });
4986
+ throw new HTTPException14(404, { message: "stripe billing is not enabled" });
4751
4987
  }
4752
4988
  const signature = c.req.header("stripe-signature");
4753
4989
  if (!signature) {
4754
- throw new HTTPException12(400, { message: "missing stripe-signature" });
4990
+ throw new HTTPException14(400, { message: "missing stripe-signature" });
4755
4991
  }
4756
4992
  const payload = await c.req.text();
4757
4993
  let event;
4758
4994
  try {
4759
4995
  event = await stripeClient(deps).webhooks.constructEventAsync(payload, signature, deps.settings.stripeWebhookSecret);
4760
4996
  } catch (error) {
4761
- throw new HTTPException12(400, { message: error instanceof Error ? error.message : "invalid stripe signature" });
4997
+ throw new HTTPException14(400, { message: error instanceof Error ? error.message : "invalid stripe signature" });
4762
4998
  }
4763
4999
  const firstSeen = await recordStripeWebhookEvent(deps.db, {
4764
5000
  id: event.id,
@@ -4776,7 +5012,7 @@ function registerBillingRoutes(app, deps) {
4776
5012
  await markStripeWebhookProcessed(deps.db, event.id);
4777
5013
  return c.json({ received: true });
4778
5014
  } catch (error) {
4779
- throw new HTTPException12(500, { message: error instanceof Error ? error.message : String(error) });
5015
+ throw new HTTPException14(500, { message: error instanceof Error ? error.message : String(error) });
4780
5016
  }
4781
5017
  });
4782
5018
  }
@@ -4828,7 +5064,7 @@ function stripeCheckoutSessionCreateParams(input) {
4828
5064
  }
4829
5065
  function checkoutReturnUrl(publicBaseUrl, candidate, fallbackPath, field) {
4830
5066
  if (!publicBaseUrl) {
4831
- throw new HTTPException12(500, { message: "OPENGENI_PUBLIC_BASE_URL is required for Stripe checkout" });
5067
+ throw new HTTPException14(500, { message: "OPENGENI_PUBLIC_BASE_URL is required for Stripe checkout" });
4832
5068
  }
4833
5069
  const base = new URL(publicBaseUrl);
4834
5070
  const fallback = new URL(fallbackPath, base).toString();
@@ -4837,7 +5073,7 @@ function checkoutReturnUrl(publicBaseUrl, candidate, fallbackPath, field) {
4837
5073
  }
4838
5074
  const parsed = new URL(candidate);
4839
5075
  if (parsed.origin !== base.origin) {
4840
- throw new HTTPException12(400, { message: `${field} must use the OpenGeni public origin` });
5076
+ throw new HTTPException14(400, { message: `${field} must use the OpenGeni public origin` });
4841
5077
  }
4842
5078
  return parsed.toString();
4843
5079
  }
@@ -5069,7 +5305,7 @@ async function getOrCreateStripeCustomer(deps, stripe, context, accountId) {
5069
5305
  }
5070
5306
  const account = await getManagedAccount(deps.db, accountId);
5071
5307
  if (!account) {
5072
- throw new HTTPException12(404, { message: "account not found" });
5308
+ throw new HTTPException14(404, { message: "account not found" });
5073
5309
  }
5074
5310
  const customer = await stripe.customers.create({
5075
5311
  name: account.name,
@@ -5095,17 +5331,17 @@ function stripeCustomerProvider(input) {
5095
5331
  function requireSelectedAccount(context, requested, permission) {
5096
5332
  const accountId = requested ?? context.defaultAccountId ?? void 0;
5097
5333
  if (!accountId) {
5098
- throw new HTTPException12(409, { message: "account selection is required" });
5334
+ throw new HTTPException14(409, { message: "account selection is required" });
5099
5335
  }
5100
5336
  const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId);
5101
5337
  if (!grant || !grant.permissions.includes(permission) && !grant.permissions.includes("account:admin")) {
5102
- throw new HTTPException12(403, { message: `missing permission: ${permission}` });
5338
+ throw new HTTPException14(403, { message: `missing permission: ${permission}` });
5103
5339
  }
5104
5340
  return accountId;
5105
5341
  }
5106
5342
  function stripeClient(deps) {
5107
5343
  if (!deps.settings.stripeSecretKey) {
5108
- throw new HTTPException12(500, { message: "Stripe secret key is not configured" });
5344
+ throw new HTTPException14(500, { message: "Stripe secret key is not configured" });
5109
5345
  }
5110
5346
  return new Stripe(deps.settings.stripeSecretKey);
5111
5347
  }
@@ -5149,7 +5385,7 @@ import {
5149
5385
  verifySignedState
5150
5386
  } from "@opengeni/github";
5151
5387
  import { deleteCookie, getCookie, setCookie } from "hono/cookie";
5152
- import { HTTPException as HTTPException13 } from "hono/http-exception";
5388
+ import { HTTPException as HTTPException15 } from "hono/http-exception";
5153
5389
  import { requireAccessGrant as requireAccessGrant10 } from "@opengeni/core";
5154
5390
  var githubStateCookie = "opengeni_github_state";
5155
5391
  function registerGitHubRoutes(app, deps) {
@@ -5177,15 +5413,15 @@ function registerGitHubRoutes(app, deps) {
5177
5413
  const workspaceId = c.req.param("workspaceId");
5178
5414
  const state = c.req.query("state");
5179
5415
  if (!state) {
5180
- throw new HTTPException13(400, { message: "missing GitHub installation state" });
5416
+ throw new HTTPException15(400, { message: "missing GitHub installation state" });
5181
5417
  }
5182
5418
  const statePayload = readSignedState3(state, githubStateSecret);
5183
5419
  if (!statePayload || statePayload.workspaceId !== workspaceId) {
5184
- throw new HTTPException13(400, { message: "invalid or expired GitHub installation state" });
5420
+ throw new HTTPException15(400, { message: "invalid or expired GitHub installation state" });
5185
5421
  }
5186
5422
  const slug = settings.githubAppSlug?.trim();
5187
5423
  if (!slug) {
5188
- throw new HTTPException13(409, { message: JSON.stringify({ message: "GitHub App is not configured", missing: githubAppMissingSettings2(settings) }) });
5424
+ throw new HTTPException15(409, { message: JSON.stringify({ message: "GitHub App is not configured", missing: githubAppMissingSettings2(settings) }) });
5189
5425
  }
5190
5426
  setGitHubStateCookie(c, deps, state);
5191
5427
  return c.redirect(`https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(state)}`);
@@ -5197,9 +5433,9 @@ function registerGitHubRoutes(app, deps) {
5197
5433
  return c.json({ repositories: await listWorkspaceGitHubRepositories(deps, workspaceId) });
5198
5434
  } catch (error) {
5199
5435
  if (error instanceof GitHubAppConfigurationError2) {
5200
- throw new HTTPException13(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
5436
+ throw new HTTPException15(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
5201
5437
  }
5202
- throw new HTTPException13(502, { message: error instanceof Error ? error.message : String(error) });
5438
+ throw new HTTPException15(502, { message: error instanceof Error ? error.message : String(error) });
5203
5439
  }
5204
5440
  });
5205
5441
  app.post("/v1/workspaces/:workspaceId/github/repositories/sync", async (c) => {
@@ -5209,9 +5445,9 @@ function registerGitHubRoutes(app, deps) {
5209
5445
  return c.json({ repositories: await listWorkspaceGitHubRepositories(deps, workspaceId) });
5210
5446
  } catch (error) {
5211
5447
  if (error instanceof GitHubAppConfigurationError2) {
5212
- throw new HTTPException13(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
5448
+ throw new HTTPException15(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
5213
5449
  }
5214
- throw new HTTPException13(502, { message: error instanceof Error ? error.message : String(error) });
5450
+ throw new HTTPException15(502, { message: error instanceof Error ? error.message : String(error) });
5215
5451
  }
5216
5452
  });
5217
5453
  app.post("/v1/workspaces/:workspaceId/github/app-manifest", async (c) => {
@@ -5243,10 +5479,10 @@ function registerGitHubRoutes(app, deps) {
5243
5479
  const code = c.req.query("code");
5244
5480
  const state = c.req.query("state");
5245
5481
  if (!code) {
5246
- throw new HTTPException13(400, { message: "missing GitHub manifest code" });
5482
+ throw new HTTPException15(400, { message: "missing GitHub manifest code" });
5247
5483
  }
5248
5484
  if (!state || !verifySignedState(state, githubStateSecret)) {
5249
- throw new HTTPException13(400, { message: "invalid or expired GitHub manifest state" });
5485
+ throw new HTTPException15(400, { message: "invalid or expired GitHub manifest state" });
5250
5486
  }
5251
5487
  try {
5252
5488
  const conversion = await convertGitHubAppManifest(code);
@@ -5257,7 +5493,7 @@ function registerGitHubRoutes(app, deps) {
5257
5493
  return c.html(githubSuccessHtml(envLines, installUrl));
5258
5494
  } catch (error) {
5259
5495
  const message = error instanceof GitHubAppApiError ? error.message : String(error);
5260
- throw new HTTPException13(502, { message });
5496
+ throw new HTTPException15(502, { message });
5261
5497
  }
5262
5498
  });
5263
5499
  const handleGitHubInstallCallback = async (c) => {
@@ -5266,28 +5502,28 @@ function registerGitHubRoutes(app, deps) {
5266
5502
  const installationIdRaw = c.req.query("installation_id");
5267
5503
  const setupAction = c.req.query("setup_action") ?? null;
5268
5504
  if (!state) {
5269
- throw new HTTPException13(400, { message: "missing GitHub installation state" });
5505
+ throw new HTTPException15(400, { message: "missing GitHub installation state" });
5270
5506
  }
5271
5507
  const statePayload = readSignedState3(state, githubStateSecret);
5272
5508
  if (!statePayload || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string") {
5273
- throw new HTTPException13(400, { message: "invalid or expired GitHub installation state" });
5509
+ throw new HTTPException15(400, { message: "invalid or expired GitHub installation state" });
5274
5510
  }
5275
5511
  requireGitHubStateCookie(c, state);
5276
5512
  const grant = await requireAccessGrant10(c, deps, statePayload.workspaceId, "github:manage");
5277
5513
  if (grant.accountId !== statePayload.accountId) {
5278
- throw new HTTPException13(403, { message: "GitHub installation state does not match this workspace" });
5514
+ throw new HTTPException15(403, { message: "GitHub installation state does not match this workspace" });
5279
5515
  }
5280
5516
  if (setupAction === "request" && !installationIdRaw) {
5281
5517
  return c.html(githubSetupPendingHtml());
5282
5518
  }
5283
5519
  const installationId = parsePositiveInteger(installationIdRaw);
5284
5520
  if (installationId === null) {
5285
- throw new HTTPException13(400, { message: "missing or invalid GitHub installation_id" });
5521
+ throw new HTTPException15(400, { message: "missing or invalid GitHub installation_id" });
5286
5522
  }
5287
5523
  if (!code) {
5288
5524
  const clientId = settings.githubClientId?.trim();
5289
5525
  if (!clientId) {
5290
- throw new HTTPException13(409, { message: JSON.stringify({ message: "GitHub App is not configured", missing: ["OPENGENI_GITHUB_CLIENT_ID"] }) });
5526
+ throw new HTTPException15(409, { message: JSON.stringify({ message: "GitHub App is not configured", missing: ["OPENGENI_GITHUB_CLIENT_ID"] }) });
5291
5527
  }
5292
5528
  const oauthState = createSignedState4(githubStateSecret, {
5293
5529
  accountId: grant.accountId,
@@ -5314,15 +5550,15 @@ function registerGitHubRoutes(app, deps) {
5314
5550
  const code = c.req.query("code");
5315
5551
  const state = c.req.query("state");
5316
5552
  if (!code) {
5317
- throw new HTTPException13(400, { message: "missing GitHub OAuth code" });
5553
+ throw new HTTPException15(400, { message: "missing GitHub OAuth code" });
5318
5554
  }
5319
5555
  if (!state) {
5320
- throw new HTTPException13(400, { message: "missing GitHub OAuth state" });
5556
+ throw new HTTPException15(400, { message: "missing GitHub OAuth state" });
5321
5557
  }
5322
5558
  const statePayload = readSignedState3(state, githubStateSecret);
5323
5559
  const installationId = parsePositiveInteger(String(statePayload?.installationId ?? ""));
5324
5560
  if (!statePayload || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string" || installationId === null) {
5325
- throw new HTTPException13(400, { message: "invalid or expired GitHub OAuth state" });
5561
+ throw new HTTPException15(400, { message: "invalid or expired GitHub OAuth state" });
5326
5562
  }
5327
5563
  requireGitHubStateCookie(c, state);
5328
5564
  return await completeGitHubInstallationBinding(deps, c, {
@@ -5335,19 +5571,19 @@ function registerGitHubRoutes(app, deps) {
5335
5571
  async function completeGitHubInstallationBinding(deps, c, input) {
5336
5572
  const { db, settings } = deps;
5337
5573
  if (!input.statePayload.workspaceId || !input.statePayload.accountId) {
5338
- throw new HTTPException13(400, { message: "invalid or expired GitHub installation state" });
5574
+ throw new HTTPException15(400, { message: "invalid or expired GitHub installation state" });
5339
5575
  }
5340
5576
  const grant = await requireAccessGrant10(c, deps, input.statePayload.workspaceId, "github:manage");
5341
5577
  if (grant.accountId !== input.statePayload.accountId) {
5342
- throw new HTTPException13(403, { message: "GitHub installation state does not match this workspace" });
5578
+ throw new HTTPException15(403, { message: "GitHub installation state does not match this workspace" });
5343
5579
  }
5344
5580
  try {
5345
5581
  const installation = await verifyGitHubInstallationAccessForUser(settings, { code: input.code, installationId: input.installationId });
5346
5582
  if (!installation) {
5347
- throw new HTTPException13(404, { message: "GitHub App installation was not found for this app" });
5583
+ throw new HTTPException15(404, { message: "GitHub App installation was not found for this app" });
5348
5584
  }
5349
5585
  if (installation.suspended) {
5350
- throw new HTTPException13(409, { message: "GitHub App installation is suspended" });
5586
+ throw new HTTPException15(409, { message: "GitHub App installation is suspended" });
5351
5587
  }
5352
5588
  await upsertGitHubInstallation(db, {
5353
5589
  accountId: grant.accountId,
@@ -5360,13 +5596,13 @@ async function completeGitHubInstallationBinding(deps, c, input) {
5360
5596
  deleteCookie(c, githubStateCookie, { path: "/v1/github" });
5361
5597
  return c.html(githubSetupSuccessHtml(installation.accountLogin ?? `installation ${input.installationId}`, returnUrl));
5362
5598
  } catch (error) {
5363
- if (error instanceof HTTPException13) {
5599
+ if (error instanceof HTTPException15) {
5364
5600
  throw error;
5365
5601
  }
5366
5602
  if (error instanceof GitHubAppConfigurationError2) {
5367
- throw new HTTPException13(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
5603
+ throw new HTTPException15(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
5368
5604
  }
5369
- throw new HTTPException13(502, { message: error instanceof Error ? error.message : String(error) });
5605
+ throw new HTTPException15(502, { message: error instanceof Error ? error.message : String(error) });
5370
5606
  }
5371
5607
  }
5372
5608
  function setGitHubStateCookie(c, deps, state) {
@@ -5380,7 +5616,7 @@ function setGitHubStateCookie(c, deps, state) {
5380
5616
  }
5381
5617
  function requireGitHubStateCookie(c, state) {
5382
5618
  if (getCookie(c, githubStateCookie) !== state) {
5383
- throw new HTTPException13(400, { message: "invalid or expired GitHub installation browser state" });
5619
+ throw new HTTPException15(400, { message: "invalid or expired GitHub installation browser state" });
5384
5620
  }
5385
5621
  }
5386
5622
  function isSecureRequest(c, deps) {
@@ -5447,7 +5683,7 @@ import {
5447
5683
  updatePackInstallationStatus
5448
5684
  } from "@opengeni/db";
5449
5685
  import { getDocumentBase as getDocumentBase2 } from "@opengeni/documents";
5450
- import { HTTPException as HTTPException14 } from "hono/http-exception";
5686
+ import { HTTPException as HTTPException16 } from "hono/http-exception";
5451
5687
  import { requireAccessGrant as requireAccessGrant11 } from "@opengeni/core";
5452
5688
  import { requireLimit as requireLimit5 } from "@opengeni/core";
5453
5689
  import { validateEnvironmentAttachment } from "@opengeni/core";
@@ -5478,7 +5714,7 @@ function registerPackRoutes(app, deps) {
5478
5714
  const grant = await requireAccessGrant11(c, deps, workspaceId, "workspace:admin");
5479
5715
  const manifest = RegisterCapabilityPackRequest.parse(await c.req.json());
5480
5716
  if (isBuiltInCapabilityPack(manifest.id)) {
5481
- throw new HTTPException14(409, { message: `pack id ${manifest.id} is a built-in pack and cannot be replaced` });
5717
+ throw new HTTPException16(409, { message: `pack id ${manifest.id} is a built-in pack and cannot be replaced` });
5482
5718
  }
5483
5719
  const { pack, created } = await registerWorkspacePack(db, {
5484
5720
  accountId: grant.accountId,
@@ -5492,10 +5728,10 @@ function registerPackRoutes(app, deps) {
5492
5728
  await requireAccessGrant11(c, deps, workspaceId, "workspace:admin");
5493
5729
  const packId = c.req.param("packId");
5494
5730
  if (isBuiltInCapabilityPack(packId)) {
5495
- throw new HTTPException14(409, { message: "built-in packs cannot be unregistered" });
5731
+ throw new HTTPException16(409, { message: "built-in packs cannot be unregistered" });
5496
5732
  }
5497
5733
  if (!await getWorkspacePack(db, workspaceId, packId)) {
5498
- throw new HTTPException14(404, { message: "pack not found" });
5734
+ throw new HTTPException16(404, { message: "pack not found" });
5499
5735
  }
5500
5736
  const installation = await getPackInstallation(db, workspaceId, packId);
5501
5737
  if (installation && installation.status === "active") {
@@ -5532,13 +5768,13 @@ function registerPackRoutes(app, deps) {
5532
5768
  const storedEnvironmentId = typeof existing?.metadata.environmentId === "string" ? existing.metadata.environmentId : void 0;
5533
5769
  const environmentId = payload.environmentId ?? storedEnvironmentId;
5534
5770
  if (pack.environment?.required && !environmentId) {
5535
- throw new HTTPException14(422, { message: "this pack requires an environment attachment; pass environmentId" });
5771
+ throw new HTTPException16(422, { message: "this pack requires an environment attachment; pass environmentId" });
5536
5772
  }
5537
5773
  if (environmentId) {
5538
5774
  const environment = await validateEnvironmentAttachment({ settings, db }, grant, workspaceId, environmentId, { preauthorized: !payload.environmentId });
5539
5775
  const missing = (pack.environment?.requiredVariables ?? []).filter((name) => !environment.variables.some((variable) => variable.name === name));
5540
5776
  if (missing.length > 0) {
5541
- throw new HTTPException14(422, { message: `environment is missing required variable(s): ${missing.join(", ")}` });
5777
+ throw new HTTPException16(422, { message: `environment is missing required variable(s): ${missing.join(", ")}` });
5542
5778
  }
5543
5779
  }
5544
5780
  const installation = await enablePackInstallation(db, {
@@ -5559,13 +5795,13 @@ function registerPackRoutes(app, deps) {
5559
5795
  const pack = await requirePack(db, workspaceId, MARKETING_SOCIAL_PACK_ID);
5560
5796
  const installation = await getPackInstallation(db, workspaceId, pack.id);
5561
5797
  if (installation?.status !== "active") {
5562
- throw new HTTPException14(409, { message: "enable the marketing social pack before creating its scheduled tasks" });
5798
+ throw new HTTPException16(409, { message: "enable the marketing social pack before creating its scheduled tasks" });
5563
5799
  }
5564
5800
  const payload = MarketingDailyAnalysisTaskRequest.parse(await c.req.json());
5565
5801
  await requireLimit5(deps, { accountId: grant.accountId, workspaceId, action: "schedule:create", quantity: 1 });
5566
5802
  const connections = await resolveSocialConnections(db, workspaceId, payload.connectionIds);
5567
5803
  if (connections.length === 0) {
5568
- throw new HTTPException14(422, { message: "at least one connected social account is required" });
5804
+ throw new HTTPException16(422, { message: "at least one connected social account is required" });
5569
5805
  }
5570
5806
  await validateDocumentBaseIds(db, workspaceId, payload.documentBaseIds);
5571
5807
  const agentConfig = buildMarketingDailyAnalysisAgentConfig({
@@ -5609,7 +5845,7 @@ function registerPackRoutes(app, deps) {
5609
5845
  async function requirePack(db, workspaceId, packId) {
5610
5846
  const pack = await resolveCapabilityPack(db, workspaceId, packId);
5611
5847
  if (!pack) {
5612
- throw new HTTPException14(404, { message: "pack not found" });
5848
+ throw new HTTPException16(404, { message: "pack not found" });
5613
5849
  }
5614
5850
  return pack;
5615
5851
  }
@@ -5618,13 +5854,13 @@ async function resolveSocialConnections(db, workspaceId, connectionIds) {
5618
5854
  const connections = ids.length > 0 ? await Promise.all(ids.map(async (id) => {
5619
5855
  const connection = await getSocialConnection(db, workspaceId, id);
5620
5856
  if (!connection) {
5621
- throw new HTTPException14(422, { message: `unknown social connection: ${id}` });
5857
+ throw new HTTPException16(422, { message: `unknown social connection: ${id}` });
5622
5858
  }
5623
5859
  return connection;
5624
5860
  })) : (await listSocialConnections2(db, workspaceId, 500)).filter((connection) => connection.status === "connected");
5625
5861
  const inactive = connections.find((connection) => connection.status !== "connected");
5626
5862
  if (inactive) {
5627
- throw new HTTPException14(422, { message: `social connection ${inactive.id} is ${inactive.status}` });
5863
+ throw new HTTPException16(422, { message: `social connection ${inactive.id} is ${inactive.status}` });
5628
5864
  }
5629
5865
  return connections;
5630
5866
  }
@@ -5632,7 +5868,7 @@ async function validateDocumentBaseIds(db, workspaceId, documentBaseIds) {
5632
5868
  for (const baseId of [...new Set(documentBaseIds)]) {
5633
5869
  const base = await getDocumentBase2(db, workspaceId, baseId);
5634
5870
  if (!base) {
5635
- throw new HTTPException14(422, { message: `unknown document base: ${baseId}` });
5871
+ throw new HTTPException16(422, { message: `unknown document base: ${baseId}` });
5636
5872
  }
5637
5873
  }
5638
5874
  }
@@ -5816,7 +6052,7 @@ import {
5816
6052
  releaseLeaseHolder
5817
6053
  } from "@opengeni/db";
5818
6054
  import { appendAndPublishEvents as appendAndPublishEvents3 } from "@opengeni/events";
5819
- import { HTTPException as HTTPException15 } from "hono/http-exception";
6055
+ import { HTTPException as HTTPException17 } from "hono/http-exception";
5820
6056
  import {
5821
6057
  establishSandboxSessionFromEnvelope,
5822
6058
  serializeEstablishedSandboxEnvelope,
@@ -5831,7 +6067,7 @@ async function withChannelA(services, ctx, fn) {
5831
6067
  const { db, settings, bus } = services;
5832
6068
  const { accountId, workspaceId, session, subjectId } = ctx;
5833
6069
  if (session.sandboxBackend === "none") {
5834
- throw new HTTPException15(409, { message: "sandbox not available" });
6070
+ throw new HTTPException17(409, { message: "sandbox not available" });
5835
6071
  }
5836
6072
  const sandboxGroupId = session.sandboxGroupId;
5837
6073
  const viewerId = crypto.randomUUID();
@@ -5859,7 +6095,7 @@ async function withChannelA(services, ctx, fn) {
5859
6095
  });
5860
6096
  if (acquired.role === "fenced") {
5861
6097
  await release();
5862
- throw new HTTPException15(409, { message: `sandbox lease superseded (epoch ${acquired.lease.leaseEpoch}); retry` });
6098
+ throw new HTTPException17(409, { message: `sandbox lease superseded (epoch ${acquired.lease.leaseEpoch}); retry` });
5863
6099
  }
5864
6100
  let established;
5865
6101
  let leaseSnapshot = acquired.lease;
@@ -5882,7 +6118,7 @@ async function withChannelA(services, ctx, fn) {
5882
6118
  });
5883
6119
  } catch (error) {
5884
6120
  await failWarmingToCold(db, { accountId, workspaceId, sandboxGroupId, expectedEpoch });
5885
- throw new HTTPException15(409, { message: `sandbox not available (${error instanceof Error ? error.message : "spawn failed"})` });
6121
+ throw new HTTPException17(409, { message: `sandbox not available (${error instanceof Error ? error.message : "spawn failed"})` });
5886
6122
  }
5887
6123
  const resumeEnvelope = await serializeEstablishedSandboxEnvelope(established) ?? envelope ?? null;
5888
6124
  const committed = await commitWarmingToWarm(db, {
@@ -5897,7 +6133,7 @@ async function withChannelA(services, ctx, fn) {
5897
6133
  leaseTtlMs
5898
6134
  });
5899
6135
  if (!committed.committed || !committed.lease) {
5900
- throw new HTTPException15(409, { message: `sandbox lease superseded (epoch ${expectedEpoch}); retry` });
6136
+ throw new HTTPException17(409, { message: `sandbox lease superseded (epoch ${expectedEpoch}); retry` });
5901
6137
  }
5902
6138
  leaseSnapshot = committed.lease;
5903
6139
  } else {
@@ -5936,11 +6172,11 @@ async function withChannelA(services, ctx, fn) {
5936
6172
  }
5937
6173
  }
5938
6174
  function mapChannelAError(error) {
5939
- if (error instanceof HTTPException15) return error;
5940
- if (error instanceof ChannelAValidationError) return new HTTPException15(400, { message: error.message });
5941
- if (error instanceof ChannelANotFoundError) return new HTTPException15(404, { message: error.message });
5942
- if (error instanceof ChannelAConflictError) return new HTTPException15(409, { message: error.message });
5943
- if (error instanceof ChannelAUnsupportedError) return new HTTPException15(409, { message: error.message });
6175
+ if (error instanceof HTTPException17) return error;
6176
+ if (error instanceof ChannelAValidationError) return new HTTPException17(400, { message: error.message });
6177
+ if (error instanceof ChannelANotFoundError) return new HTTPException17(404, { message: error.message });
6178
+ if (error instanceof ChannelAConflictError) return new HTTPException17(409, { message: error.message });
6179
+ if (error instanceof ChannelAUnsupportedError) return new HTTPException17(409, { message: error.message });
5944
6180
  return error;
5945
6181
  }
5946
6182
  async function dropEstablishedHandle(established) {
@@ -5949,7 +6185,7 @@ async function dropEstablishedHandle(established) {
5949
6185
 
5950
6186
  // src/routes/sessions.ts
5951
6187
  import { negotiateCapabilities } from "@opengeni/runtime/sandbox";
5952
- import { HTTPException as HTTPException17 } from "hono/http-exception";
6188
+ import { HTTPException as HTTPException19 } from "hono/http-exception";
5953
6189
  import { requireAccessGrant as requireAccessGrant13 } from "@opengeni/core";
5954
6190
 
5955
6191
  // src/sandbox/viewer.ts
@@ -5971,7 +6207,7 @@ import {
5971
6207
  SandboxLeaseSupersededError as SandboxLeaseSupersededError2
5972
6208
  } from "@opengeni/db";
5973
6209
  import { appendAndPublishEvents as appendAndPublishEvents4 } from "@opengeni/events";
5974
- import { HTTPException as HTTPException16 } from "hono/http-exception";
6210
+ import { HTTPException as HTTPException18 } from "hono/http-exception";
5975
6211
  import {
5976
6212
  DESKTOP_STREAM_PORT,
5977
6213
  ensureDisplayStack,
@@ -6033,7 +6269,7 @@ async function attachViewer(services, input) {
6033
6269
  });
6034
6270
  if (acquired.role === "fenced") {
6035
6271
  await release();
6036
- throw new HTTPException16(409, { message: `sandbox lease superseded (epoch ${acquired.lease.leaseEpoch}); re-read capabilities and re-attach` });
6272
+ throw new HTTPException18(409, { message: `sandbox lease superseded (epoch ${acquired.lease.leaseEpoch}); re-read capabilities and re-attach` });
6037
6273
  }
6038
6274
  if (acquired.role === "spawner") {
6039
6275
  const expectedEpoch = acquired.lease.leaseEpoch;
@@ -6074,12 +6310,12 @@ async function attachViewer(services, input) {
6074
6310
  };
6075
6311
  } catch (error) {
6076
6312
  if (error instanceof SandboxLeaseSupersededError2) {
6077
- throw new HTTPException16(409, { message: `sandbox lease superseded (epoch ${error.leaseEpoch}); re-read capabilities and re-attach` });
6313
+ throw new HTTPException18(409, { message: `sandbox lease superseded (epoch ${error.leaseEpoch}); re-read capabilities and re-attach` });
6078
6314
  }
6079
6315
  await failWarmingToCold2(db, { accountId, workspaceId, sandboxGroupId, expectedEpoch });
6080
6316
  await release();
6081
- if (error instanceof HTTPException16) throw error;
6082
- throw new HTTPException16(409, { message: `sandbox not available (${error instanceof Error ? error.message : "spawn failed"})` });
6317
+ if (error instanceof HTTPException18) throw error;
6318
+ throw new HTTPException18(409, { message: `sandbox not available (${error instanceof Error ? error.message : "spawn failed"})` });
6083
6319
  } finally {
6084
6320
  await dropEstablishedHandle2(established);
6085
6321
  }
@@ -6546,7 +6782,7 @@ function registerSessionRoutes(app, deps) {
6546
6782
  await requireAccessGrant13(c, deps, workspaceId, "sessions:read");
6547
6783
  const session = await getSession4(db, workspaceId, c.req.param("sessionId"));
6548
6784
  if (!session) {
6549
- throw new HTTPException17(404, { message: "session not found" });
6785
+ throw new HTTPException19(404, { message: "session not found" });
6550
6786
  }
6551
6787
  return c.json(session);
6552
6788
  });
@@ -6557,12 +6793,12 @@ function registerSessionRoutes(app, deps) {
6557
6793
  const body = await c.req.json();
6558
6794
  const target = typeof body.target === "string" ? body.target : "";
6559
6795
  if (!target) {
6560
- throw new HTTPException17(400, { message: 'target is required ("auto" or an account id)' });
6796
+ throw new HTTPException19(400, { message: 'target is required ("auto" or an account id)' });
6561
6797
  }
6562
6798
  const pinned = target === "auto" ? null : target;
6563
6799
  const ok = await setSessionCodexPin(db, workspaceId, sessionId, pinned);
6564
6800
  if (!ok) {
6565
- throw new HTTPException17(404, { message: "session or codex account not found" });
6801
+ throw new HTTPException19(404, { message: "session or codex account not found" });
6566
6802
  }
6567
6803
  return c.json({ pinned: target === "auto" ? "auto" : target });
6568
6804
  });
@@ -6575,7 +6811,7 @@ function registerSessionRoutes(app, deps) {
6575
6811
  await updateSessionTitle2({ db, bus }, workspaceId, sessionId, payload.title, "user");
6576
6812
  const session = await getSession4(db, workspaceId, sessionId);
6577
6813
  if (!session) {
6578
- throw new HTTPException17(404, { message: "session not found" });
6814
+ throw new HTTPException19(404, { message: "session not found" });
6579
6815
  }
6580
6816
  return c.json(session);
6581
6817
  });
@@ -6586,7 +6822,7 @@ function registerSessionRoutes(app, deps) {
6586
6822
  await assertSessionExists(db, workspaceId, sessionId);
6587
6823
  const goal = await getSessionGoal2(db, workspaceId, sessionId);
6588
6824
  if (!goal) {
6589
- throw new HTTPException17(404, { message: "session goal not found" });
6825
+ throw new HTTPException19(404, { message: "session goal not found" });
6590
6826
  }
6591
6827
  return c.json(goal);
6592
6828
  });
@@ -6598,10 +6834,10 @@ function registerSessionRoutes(app, deps) {
6598
6834
  const payload = UpdateSessionGoalRequest.parse(await c.req.json());
6599
6835
  const existing = await getSessionGoal2(db, workspaceId, sessionId);
6600
6836
  if (!existing) {
6601
- throw new HTTPException17(404, { message: "session goal not found" });
6837
+ throw new HTTPException19(404, { message: "session goal not found" });
6602
6838
  }
6603
6839
  if (existing.status === "completed") {
6604
- throw new HTTPException17(409, { message: "session goal is completed; set a new goal instead" });
6840
+ throw new HTTPException19(409, { message: "session goal is completed; set a new goal instead" });
6605
6841
  }
6606
6842
  if (payload.status === "paused") {
6607
6843
  const { goal: goal2, changed: changed2 } = await setSessionGoalStatus2(db, workspaceId, sessionId, {
@@ -6625,7 +6861,7 @@ function registerSessionRoutes(app, deps) {
6625
6861
  return c.json(goal2);
6626
6862
  }
6627
6863
  if (existing.status !== "paused") {
6628
- throw new HTTPException17(409, { message: `session goal is ${existing.status}; only paused goals can be resumed` });
6864
+ throw new HTTPException19(409, { message: `session goal is ${existing.status}; only paused goals can be resumed` });
6629
6865
  }
6630
6866
  const { goal, changed } = await setSessionGoalStatus2(db, workspaceId, sessionId, { status: "active" });
6631
6867
  if (changed) {
@@ -6650,11 +6886,11 @@ function registerSessionRoutes(app, deps) {
6650
6886
  await assertSessionExists(db, workspaceId, sessionId);
6651
6887
  const clearBody = ClearSessionContextRequest.safeParse(await c.req.json().catch(() => ({})));
6652
6888
  if (!clearBody.success) {
6653
- throw new HTTPException17(400, { message: "context clear requires an explicit { confirm: true }" });
6889
+ throw new HTTPException19(400, { message: "context clear requires an explicit { confirm: true }" });
6654
6890
  }
6655
6891
  const session = await requireSession3(db, workspaceId, sessionId);
6656
6892
  if (session.status === "queued" || session.status === "running" || session.status === "requires_action") {
6657
- throw new HTTPException17(409, { message: `session is ${session.status}; cannot clear context mid-turn \u2014 stop the turn first` });
6893
+ throw new HTTPException19(409, { message: `session is ${session.status}; cannot clear context mid-turn \u2014 stop the turn first` });
6658
6894
  }
6659
6895
  const result = await clearSessionContext(db, { accountId: grant.accountId, workspaceId, sessionId });
6660
6896
  await appendAndPublishEvents5(db, bus, workspaceId, sessionId, [{
@@ -6731,7 +6967,7 @@ function registerSessionRoutes(app, deps) {
6731
6967
  const resources = payload.resources !== void 0 ? normalizeResources(payload.resources) : existing.resources;
6732
6968
  const tools = payload.tools !== void 0 ? validateToolRefs(payload.tools, runtimeSettings) : existing.tools;
6733
6969
  if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
6734
- throw new HTTPException17(503, { message: "object storage is not configured" });
6970
+ throw new HTTPException19(503, { message: "object storage is not configured" });
6735
6971
  }
6736
6972
  await validateFileResources(db, workspaceId, resources);
6737
6973
  await validateGitHubRepositorySelection(db, workspaceId, [...session.resources, ...resources]);
@@ -6801,7 +7037,7 @@ function registerSessionRoutes(app, deps) {
6801
7037
  }
6802
7038
  const session = await requireSession3(db, workspaceId, sessionId);
6803
7039
  if (event.type === "user.approvalDecision" && session.status !== "requires_action") {
6804
- throw new HTTPException17(409, { message: `session is ${session.status}; no approval is pending` });
7040
+ throw new HTTPException19(409, { message: `session is ${session.status}; no approval is pending` });
6805
7041
  }
6806
7042
  const eventsToAppend = [{
6807
7043
  type: event.type,
@@ -6811,7 +7047,7 @@ function registerSessionRoutes(app, deps) {
6811
7047
  const appended = await appendAndPublishEvents5(db, bus, workspaceId, sessionId, eventsToAppend);
6812
7048
  const accepted = appended[0];
6813
7049
  if (!accepted) {
6814
- throw new HTTPException17(500, { message: "failed to append client event" });
7050
+ throw new HTTPException19(500, { message: "failed to append client event" });
6815
7051
  }
6816
7052
  const workflowId = workflowIdForSession2(sessionId);
6817
7053
  if (event.type === "user.approvalDecision") {
@@ -6829,7 +7065,7 @@ function registerSessionRoutes(app, deps) {
6829
7065
  });
6830
7066
  function assertOwnershipEnabled() {
6831
7067
  if (!settings.sandboxOwnershipEnabled) {
6832
- throw new HTTPException17(404, { message: "sandbox ownership is not enabled for this deployment" });
7068
+ throw new HTTPException19(404, { message: "sandbox ownership is not enabled for this deployment" });
6833
7069
  }
6834
7070
  }
6835
7071
  async function resolveSharedExposure(workspaceId, session) {
@@ -6844,7 +7080,7 @@ function registerSessionRoutes(app, deps) {
6844
7080
  const sessionId = c.req.param("sessionId");
6845
7081
  const session = await getSession4(db, workspaceId, sessionId);
6846
7082
  if (!session) {
6847
- throw new HTTPException17(404, { message: "session not found" });
7083
+ throw new HTTPException19(404, { message: "session not found" });
6848
7084
  }
6849
7085
  const lease = await readGroupLease({ db, settings }, { workspaceId, sandboxGroupId: session.sandboxGroupId });
6850
7086
  const { shared, sharedSessionIds } = await resolveSharedExposure(workspaceId, session);
@@ -6939,11 +7175,11 @@ function registerSessionRoutes(app, deps) {
6939
7175
  const sessionId = c.req.param("sessionId");
6940
7176
  const session = await getSession4(db, workspaceId, sessionId);
6941
7177
  if (!session) {
6942
- throw new HTTPException17(404, { message: "session not found" });
7178
+ throw new HTTPException19(404, { message: "session not found" });
6943
7179
  }
6944
7180
  const parsed = AcknowledgeStreamRequest.safeParse(await c.req.json().catch(() => ({})));
6945
7181
  if (!parsed.success) {
6946
- throw new HTTPException17(400, { message: "invalid stream acknowledgment request" });
7182
+ throw new HTTPException19(400, { message: "invalid stream acknowledgment request" });
6947
7183
  }
6948
7184
  const recorded = await recordStreamAcknowledgment(db, {
6949
7185
  accountId: grant.accountId,
@@ -6962,21 +7198,21 @@ function registerSessionRoutes(app, deps) {
6962
7198
  const sessionId = c.req.param("sessionId");
6963
7199
  const session = await getSession4(db, workspaceId, sessionId);
6964
7200
  if (!session) {
6965
- throw new HTTPException17(404, { message: "session not found" });
7201
+ throw new HTTPException19(404, { message: "session not found" });
6966
7202
  }
6967
7203
  const parsed = AttachViewerRequest.safeParse(await c.req.json().catch(() => ({})));
6968
7204
  if (!parsed.success) {
6969
- throw new HTTPException17(400, { message: "invalid viewer attach request" });
7205
+ throw new HTTPException19(400, { message: "invalid viewer attach request" });
6970
7206
  }
6971
7207
  const wantDesktop = parsed.data.desktop ?? false;
6972
7208
  const { shared } = await resolveSharedExposure(workspaceId, session);
6973
7209
  if (wantDesktop) {
6974
7210
  const ack = await getStreamAcknowledgment(db, { workspaceId, sandboxGroupId: session.sandboxGroupId, subjectId: grant.subjectId });
6975
7211
  if (!ack?.acknowledgedUnredacted) {
6976
- throw new HTTPException17(409, { message: "stream_acknowledgment_required" });
7212
+ throw new HTTPException19(409, { message: "stream_acknowledgment_required" });
6977
7213
  }
6978
7214
  if (shared && !ack.acknowledgedShared) {
6979
- throw new HTTPException17(409, { message: "shared_acknowledgment_required" });
7215
+ throw new HTTPException19(409, { message: "shared_acknowledgment_required" });
6980
7216
  }
6981
7217
  }
6982
7218
  const activeSandbox = session.activeSandboxId ? await getSandbox2(db, workspaceId, session.activeSandboxId) : null;
@@ -7075,11 +7311,11 @@ function registerSessionRoutes(app, deps) {
7075
7311
  const sessionId = c.req.param("sessionId");
7076
7312
  const session = await getSession4(db, workspaceId, sessionId);
7077
7313
  if (!session) {
7078
- throw new HTTPException17(404, { message: "session not found" });
7314
+ throw new HTTPException19(404, { message: "session not found" });
7079
7315
  }
7080
7316
  const parsed = ViewerHeartbeatRequest.safeParse(await c.req.json().catch(() => ({})));
7081
7317
  if (!parsed.success) {
7082
- throw new HTTPException17(400, { message: "viewer heartbeat requires { leaseEpoch }" });
7318
+ throw new HTTPException19(400, { message: "viewer heartbeat requires { leaseEpoch }" });
7083
7319
  }
7084
7320
  const alive = await heartbeatViewer({ db, settings }, {
7085
7321
  accountId: grant.accountId,
@@ -7097,7 +7333,7 @@ function registerSessionRoutes(app, deps) {
7097
7333
  const sessionId = c.req.param("sessionId");
7098
7334
  const session = await getSession4(db, workspaceId, sessionId);
7099
7335
  if (!session) {
7100
- throw new HTTPException17(404, { message: "session not found" });
7336
+ throw new HTTPException19(404, { message: "session not found" });
7101
7337
  }
7102
7338
  await detachViewer({ db, settings }, {
7103
7339
  accountId: grant.accountId,
@@ -7114,7 +7350,7 @@ function registerSessionRoutes(app, deps) {
7114
7350
  const sessionId = c.req.param("sessionId");
7115
7351
  const session = await getSession4(db, workspaceId, sessionId);
7116
7352
  if (!session) {
7117
- throw new HTTPException17(404, { message: "session not found" });
7353
+ throw new HTTPException19(404, { message: "session not found" });
7118
7354
  }
7119
7355
  const result = await revokeViewer(db, {
7120
7356
  accountId: grant.accountId,
@@ -7132,7 +7368,7 @@ function registerSessionRoutes(app, deps) {
7132
7368
  const sessionId = c.req.param("sessionId") ?? "";
7133
7369
  const session = await getSession4(db, workspaceId, sessionId);
7134
7370
  if (!session) {
7135
- throw new HTTPException17(404, { message: "session not found" });
7371
+ throw new HTTPException19(404, { message: "session not found" });
7136
7372
  }
7137
7373
  return { accountId: grant.accountId, workspaceId, session, subjectId: grant.subjectId };
7138
7374
  }
@@ -7140,7 +7376,7 @@ function registerSessionRoutes(app, deps) {
7140
7376
  const raw = await c.req.json().catch(() => void 0);
7141
7377
  const result = schema.safeParse(raw ?? {});
7142
7378
  if (!result.success) {
7143
- throw new HTTPException17(400, { message: "invalid request body" });
7379
+ throw new HTTPException19(400, { message: "invalid request body" });
7144
7380
  }
7145
7381
  return result.data;
7146
7382
  }
@@ -7245,10 +7481,10 @@ function registerSessionRoutes(app, deps) {
7245
7481
  const req = await parseChannelABody(c, PtyWriteRequest);
7246
7482
  const pty = await getOpenPtySession(db, ctx.workspaceId, req.ptyId);
7247
7483
  if (!pty) {
7248
- throw new HTTPException17(404, { message: "pty not found or closed" });
7484
+ throw new HTTPException19(404, { message: "pty not found or closed" });
7249
7485
  }
7250
7486
  if (pty.execSessionId === null) {
7251
- throw new HTTPException17(409, { message: "interactive terminal unsupported on this backend" });
7487
+ throw new HTTPException19(409, { message: "interactive terminal unsupported on this backend" });
7252
7488
  }
7253
7489
  let seq = 1;
7254
7490
  await withChannelA({ db, settings, bus }, ctx, async ({ service }) => {
@@ -7266,7 +7502,7 @@ function registerSessionRoutes(app, deps) {
7266
7502
  const req = await parseChannelABody(c, PtyResizeRequest);
7267
7503
  const pty = await getOpenPtySession(db, ctx.workspaceId, req.ptyId);
7268
7504
  if (!pty) {
7269
- throw new HTTPException17(404, { message: "pty not found or closed" });
7505
+ throw new HTTPException19(404, { message: "pty not found or closed" });
7270
7506
  }
7271
7507
  if (pty.execSessionId !== null) {
7272
7508
  await withChannelA({ db, settings, bus }, ctx, ({ service }) => service.ptyResize(req, pty.execSessionId));
@@ -7336,7 +7572,7 @@ import {
7336
7572
  listSocialConnections as listSocialConnections3,
7337
7573
  listSocialPosts as listSocialPosts2
7338
7574
  } from "@opengeni/db";
7339
- import { HTTPException as HTTPException18 } from "hono/http-exception";
7575
+ import { HTTPException as HTTPException20 } from "hono/http-exception";
7340
7576
  import { z as z2 } from "zod";
7341
7577
  import { requireAccessGrant as requireAccessGrant14 } from "@opengeni/core";
7342
7578
  function registerSocialRoutes(app, deps) {
@@ -7408,7 +7644,7 @@ function parseSince(raw) {
7408
7644
  }
7409
7645
  const since = new Date(raw);
7410
7646
  if (Number.isNaN(since.getTime())) {
7411
- throw new HTTPException18(422, { message: "since must be an ISO date-time" });
7647
+ throw new HTTPException20(422, { message: "since must be an ISO date-time" });
7412
7648
  }
7413
7649
  return since;
7414
7650
  }
@@ -7419,7 +7655,7 @@ function parseConnectionIds(raw) {
7419
7655
  const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
7420
7656
  const parsed = z2.array(z2.string().uuid()).safeParse(values);
7421
7657
  if (!parsed.success) {
7422
- throw new HTTPException18(422, { message: "connectionIds must be a comma-separated list of UUIDs" });
7658
+ throw new HTTPException20(422, { message: "connectionIds must be a comma-separated list of UUIDs" });
7423
7659
  }
7424
7660
  const ids = parsed.data;
7425
7661
  return [...new Set(ids)];
@@ -7427,12 +7663,12 @@ function parseConnectionIds(raw) {
7427
7663
  function socialHttpException(error) {
7428
7664
  const message = error instanceof Error ? error.message : String(error);
7429
7665
  if (message.includes("not found")) {
7430
- return new HTTPException18(404, { message });
7666
+ return new HTTPException20(404, { message });
7431
7667
  }
7432
7668
  if (message.includes("duplicate key")) {
7433
- return new HTTPException18(409, { message: "social connection or post already exists" });
7669
+ return new HTTPException20(409, { message: "social connection or post already exists" });
7434
7670
  }
7435
- return new HTTPException18(500, { message });
7671
+ return new HTTPException20(500, { message });
7436
7672
  }
7437
7673
 
7438
7674
  // src/routes/workspaces.ts
@@ -7460,7 +7696,7 @@ import {
7460
7696
  requireWorkspace,
7461
7697
  updateWorkspace
7462
7698
  } from "@opengeni/db";
7463
- import { HTTPException as HTTPException19 } from "hono/http-exception";
7699
+ import { HTTPException as HTTPException21 } from "hono/http-exception";
7464
7700
  import { hasPermission as hasPermission3, requireAccessContext as requireAccessContext2, requireAccessGrant as requireAccessGrant15 } from "@opengeni/core";
7465
7701
  import { requireLimit as requireLimit7 } from "@opengeni/core";
7466
7702
  import { assertWorkspaceDeletable, assertWorkspaceMemberRemovable, resolveMemberSubjectId } from "@opengeni/core";
@@ -7482,7 +7718,7 @@ function registerWorkspaceRoutes(app, deps) {
7482
7718
  const payload = CreateWorkspaceRequest.parse(await c.req.json());
7483
7719
  const accountId = payload.accountId ?? context.defaultAccountId;
7484
7720
  if (!accountId) {
7485
- throw new HTTPException19(409, { message: "account selection is required" });
7721
+ throw new HTTPException21(409, { message: "account selection is required" });
7486
7722
  }
7487
7723
  requireAccountPermission(context, accountId, "workspace:create");
7488
7724
  await requireLimit7(deps, { accountId, action: "workspace:create", quantity: 1 });
@@ -7558,7 +7794,7 @@ function registerWorkspaceRoutes(app, deps) {
7558
7794
  const members = await listWorkspaceMembers(deps.db, workspaceId);
7559
7795
  const member = members.find((candidate) => candidate.subjectId === subjectId);
7560
7796
  if (!member) {
7561
- throw new HTTPException19(500, { message: "failed to add member" });
7797
+ throw new HTTPException21(500, { message: "failed to add member" });
7562
7798
  }
7563
7799
  return c.json(WorkspaceMember.parse(member), 201);
7564
7800
  });
@@ -7570,7 +7806,7 @@ function registerWorkspaceRoutes(app, deps) {
7570
7806
  const existing = await listWorkspaceMembers(deps.db, workspaceId);
7571
7807
  const current = existing.find((member2) => member2.subjectId === subjectId);
7572
7808
  if (!current) {
7573
- throw new HTTPException19(404, { message: "member not found" });
7809
+ throw new HTTPException21(404, { message: "member not found" });
7574
7810
  }
7575
7811
  await grantWorkspaceAccess(deps.db, {
7576
7812
  accountId: grant.accountId,
@@ -7583,7 +7819,7 @@ function registerWorkspaceRoutes(app, deps) {
7583
7819
  const members = await listWorkspaceMembers(deps.db, workspaceId);
7584
7820
  const member = members.find((candidate) => candidate.subjectId === subjectId);
7585
7821
  if (!member) {
7586
- throw new HTTPException19(500, { message: "failed to update member" });
7822
+ throw new HTTPException21(500, { message: "failed to update member" });
7587
7823
  }
7588
7824
  return c.json(WorkspaceMember.parse(member));
7589
7825
  });
@@ -7607,7 +7843,7 @@ function normalizeAgentInstructions(value) {
7607
7843
  function requireAccountPermission(context, accountId, permission) {
7608
7844
  const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId);
7609
7845
  if (!grant || !grant.permissions.includes(permission) && !grant.permissions.includes("account:admin")) {
7610
- throw new HTTPException19(403, { message: `missing permission: ${permission}` });
7846
+ throw new HTTPException21(403, { message: `missing permission: ${permission}` });
7611
7847
  }
7612
7848
  }
7613
7849
 
@@ -7634,7 +7870,7 @@ function createApp(deps) {
7634
7870
  const documentIndexer = deps.documentIndexer ?? {
7635
7871
  indexDocument: async ({ accountId, workspaceId, documentId }) => {
7636
7872
  if (!objectStorage) {
7637
- throw new HTTPException20(503, { message: "object storage is not configured" });
7873
+ throw new HTTPException22(503, { message: "object storage is not configured" });
7638
7874
  }
7639
7875
  return await indexDocumentNow(deps.db, objectStorage, workspaceId, documentId, getDocumentServices(), {
7640
7876
  beforeEmbed: async ({ chunkCount }) => {
@@ -7795,6 +8031,7 @@ function createApp(deps) {
7795
8031
  registerSocialRoutes(app, routeDeps);
7796
8032
  registerConnectionRoutes(app, routeDeps);
7797
8033
  registerCapabilityRoutes(app, routeDeps);
8034
+ registerCatalogAssetRoutes(app, routeDeps);
7798
8035
  registerEnrollmentRoutes(app, routeDeps);
7799
8036
  registerMachineRoutes(app, routeDeps);
7800
8037
  registerEnvironmentRoutes(app, routeDeps);
@@ -7835,7 +8072,7 @@ function allowedCorsOrigin(pattern, origin) {
7835
8072
  return new RegExp(`^(?:${pattern})$`).test(origin);
7836
8073
  }
7837
8074
  function httpStatusForError(error) {
7838
- if (error instanceof HTTPException20) {
8075
+ if (error instanceof HTTPException22) {
7839
8076
  return error.status;
7840
8077
  }
7841
8078
  return 500;
@@ -7958,6 +8195,7 @@ var routeLabelPatterns = [
7958
8195
  { pattern: /^\/v1\/workspaces\/[^/]+\/connections$/, label: "/v1/workspaces/:workspaceId/connections" },
7959
8196
  { pattern: /^\/v1\/workspaces\/[^/]+\/connections\/oauth\/start$/, label: "/v1/workspaces/:workspaceId/connections/oauth/start" },
7960
8197
  { pattern: /^\/v1\/workspaces\/[^/]+\/connections\/[^/]+$/, label: "/v1/workspaces/:workspaceId/connections/:connectionId" },
8198
+ { pattern: /^\/v1\/catalog-assets\/.+$/, label: "/v1/catalog-assets/*" },
7961
8199
  { pattern: /^\/v1\/integrations\/oauth\/callback$/, label: "/v1/integrations/oauth/callback" },
7962
8200
  { pattern: /^\/v1\/integrations\/oauth\/client-metadata\.json$/, label: "/v1/integrations/oauth/client-metadata.json" },
7963
8201
  { pattern: /^\/v1\/enrollments\/device\/start$/, label: "/v1/enrollments/device/start" },
@@ -7997,4 +8235,4 @@ export {
7997
8235
  withDefaultEnabledCapabilityMcpTools,
7998
8236
  workflowIdForSession3 as workflowIdForSession
7999
8237
  };
8000
- //# sourceMappingURL=chunk-DQ5TIRDZ.js.map
8238
+ //# sourceMappingURL=chunk-3HIA43CC.js.map