@opengeni/api-router 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +1 -1
- package/dist/{chunk-DQ5TIRDZ.js → chunk-YY6OAEL6.js} +342 -243
- package/dist/chunk-YY6OAEL6.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +9 -9
- package/src/app.ts +3 -0
- package/src/http/auth.ts +6 -0
- package/src/integrations/oauth-client.ts +6 -5
- package/src/integrations/provider-domain.ts +15 -0
- package/src/routes/catalog-assets.ts +105 -0
- package/src/routes/connections.ts +3 -2
- package/dist/chunk-DQ5TIRDZ.js.map +0 -1
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,7 +2409,19 @@ 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
|
|
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;
|
|
2326
2426
|
async function startMcpOAuth(deps, context) {
|
|
2327
2427
|
const { db, settings } = deps;
|
|
@@ -2333,7 +2433,7 @@ async function startMcpOAuth(deps, context) {
|
|
|
2333
2433
|
const metadataUrl = `${baseUrl}/v1/integrations/oauth/client-metadata.json`;
|
|
2334
2434
|
const existing = context.payload.connectionId ? await getConnectionMetadata(db, context.workspaceId, context.payload.connectionId, context.subjectId) : null;
|
|
2335
2435
|
if (context.payload.connectionId && !existing) {
|
|
2336
|
-
throw new
|
|
2436
|
+
throw new HTTPException6(404, { message: "connection not found" });
|
|
2337
2437
|
}
|
|
2338
2438
|
const discovery = await discoverMcpOAuth(resource, settings);
|
|
2339
2439
|
const client = await registerOAuthClient(db, settings, discovery.as, metadataUrl, redirectUri);
|
|
@@ -2376,7 +2476,7 @@ async function startMcpOAuth(deps, context) {
|
|
|
2376
2476
|
async function completeMcpOAuthCallback(deps, input) {
|
|
2377
2477
|
const { db, settings } = deps;
|
|
2378
2478
|
if (!input.state) {
|
|
2379
|
-
throw new
|
|
2479
|
+
throw new HTTPException6(400, { message: "missing OAuth state" });
|
|
2380
2480
|
}
|
|
2381
2481
|
const state = readOAuthState(input.state, settings);
|
|
2382
2482
|
if (!input.code) {
|
|
@@ -2391,7 +2491,7 @@ async function completeMcpOAuthCallback(deps, input) {
|
|
|
2391
2491
|
now: /* @__PURE__ */ new Date()
|
|
2392
2492
|
});
|
|
2393
2493
|
if (!consumed) {
|
|
2394
|
-
throw new
|
|
2494
|
+
throw new HTTPException6(400, { message: "OAuth state has already been used" });
|
|
2395
2495
|
}
|
|
2396
2496
|
try {
|
|
2397
2497
|
const baseUrl = integrationBaseUrl(settings.publicBaseUrl, input.requestUrl);
|
|
@@ -2446,11 +2546,11 @@ async function completeMcpOAuthCallback(deps, input) {
|
|
|
2446
2546
|
createdBySubjectId: state.subjectId
|
|
2447
2547
|
});
|
|
2448
2548
|
if (!connection) {
|
|
2449
|
-
throw new
|
|
2549
|
+
throw new HTTPException6(409, { message: "connection changed during OAuth reconnect; start again" });
|
|
2450
2550
|
}
|
|
2451
|
-
return { redirectTo: callbackReturnPath(state.returnPath, "success", { connectionId: connection.id }) };
|
|
2551
|
+
return { redirectTo: callbackReturnPath(state.returnPath, "success", { connectionId: connection.id, providerDomain: connection.providerDomain }) };
|
|
2452
2552
|
} catch (error) {
|
|
2453
|
-
if (error instanceof
|
|
2553
|
+
if (error instanceof HTTPException6 && error.status >= 400 && error.status < 500) {
|
|
2454
2554
|
throw error;
|
|
2455
2555
|
}
|
|
2456
2556
|
return { redirectTo: callbackReturnPath(state.returnPath, "error", { reason: "oauth_callback_failed" }) };
|
|
@@ -2462,7 +2562,7 @@ function integrationBaseUrl(publicBaseUrl, requestUrl) {
|
|
|
2462
2562
|
function requireIntegrationsStateSecret(settings) {
|
|
2463
2563
|
const secret = settings.integrationsStateSecret?.trim();
|
|
2464
2564
|
if (!secret) {
|
|
2465
|
-
throw new
|
|
2565
|
+
throw new HTTPException6(503, { message: "integrations OAuth requires OPENGENI_INTEGRATIONS_STATE_SECRET" });
|
|
2466
2566
|
}
|
|
2467
2567
|
return secret;
|
|
2468
2568
|
}
|
|
@@ -2471,11 +2571,11 @@ async function discoverMcpOAuth(resource, settings) {
|
|
|
2471
2571
|
const prm = await discoverProtectedResourceMetadata(resource, settings, challenge.resourceMetadata);
|
|
2472
2572
|
const authorizationServer = prm.authorizationServers[0];
|
|
2473
2573
|
if (!authorizationServer) {
|
|
2474
|
-
throw new
|
|
2574
|
+
throw new HTTPException6(422, { message: "MCP protected resource metadata did not advertise an authorization server" });
|
|
2475
2575
|
}
|
|
2476
2576
|
const as = await discoverAuthorizationServerMetadata(authorizationServer, settings);
|
|
2477
2577
|
if (!as.codeChallengeMethodsSupported.includes("S256")) {
|
|
2478
|
-
throw new
|
|
2578
|
+
throw new HTTPException6(422, { message: "authorization server does not support required PKCE S256" });
|
|
2479
2579
|
}
|
|
2480
2580
|
return { challenge, prm, as };
|
|
2481
2581
|
}
|
|
@@ -2496,7 +2596,7 @@ async function discoverProtectedResourceMetadata(resource, settings, advertisedU
|
|
|
2496
2596
|
]);
|
|
2497
2597
|
for (const candidate of candidates) {
|
|
2498
2598
|
const payload = await fetchJsonObject(candidate, settings).catch((error) => {
|
|
2499
|
-
if (error instanceof
|
|
2599
|
+
if (error instanceof HTTPException6) {
|
|
2500
2600
|
throw error;
|
|
2501
2601
|
}
|
|
2502
2602
|
return null;
|
|
@@ -2515,7 +2615,7 @@ async function discoverProtectedResourceMetadata(resource, settings, advertisedU
|
|
|
2515
2615
|
...stringValue(payload.resource) ? { resource: stringValue(payload.resource) } : {}
|
|
2516
2616
|
};
|
|
2517
2617
|
}
|
|
2518
|
-
throw new
|
|
2618
|
+
throw new HTTPException6(422, { message: "could not discover MCP protected resource metadata" });
|
|
2519
2619
|
}
|
|
2520
2620
|
async function discoverAuthorizationServerMetadata(authorizationServer, settings) {
|
|
2521
2621
|
const candidates = uniqueStrings([
|
|
@@ -2525,7 +2625,7 @@ async function discoverAuthorizationServerMetadata(authorizationServer, settings
|
|
|
2525
2625
|
]);
|
|
2526
2626
|
for (const candidate of candidates) {
|
|
2527
2627
|
const payload = await fetchJsonObject(candidate, settings).catch((error) => {
|
|
2528
|
-
if (error instanceof
|
|
2628
|
+
if (error instanceof HTTPException6) {
|
|
2529
2629
|
throw error;
|
|
2530
2630
|
}
|
|
2531
2631
|
return null;
|
|
@@ -2549,7 +2649,7 @@ async function discoverAuthorizationServerMetadata(authorizationServer, settings
|
|
|
2549
2649
|
...stringValue(payload.registration_endpoint) ? { registrationEndpoint: stringValue(payload.registration_endpoint) } : {}
|
|
2550
2650
|
};
|
|
2551
2651
|
}
|
|
2552
|
-
throw new
|
|
2652
|
+
throw new HTTPException6(422, { message: "could not discover OAuth authorization server metadata" });
|
|
2553
2653
|
}
|
|
2554
2654
|
async function registerOAuthClient(db, settings, as, metadataUrl, redirectUri) {
|
|
2555
2655
|
const operator = operatorClientForAs(settings, as);
|
|
@@ -2577,7 +2677,7 @@ async function registerOAuthClient(db, settings, as, metadataUrl, redirectUri) {
|
|
|
2577
2677
|
};
|
|
2578
2678
|
}
|
|
2579
2679
|
if (!as.registrationEndpoint) {
|
|
2580
|
-
throw new
|
|
2680
|
+
throw new HTTPException6(422, {
|
|
2581
2681
|
message: "manual OAuth client credentials are required for this authorization server"
|
|
2582
2682
|
});
|
|
2583
2683
|
}
|
|
@@ -2597,7 +2697,7 @@ async function registerOAuthClient(db, settings, as, metadataUrl, redirectUri) {
|
|
|
2597
2697
|
if (storedWinner.clientId !== dcr.clientId) {
|
|
2598
2698
|
const winner = await loadIntegrationOAuthClient(db, settings, as.issuer);
|
|
2599
2699
|
if (!winner) {
|
|
2600
|
-
throw new
|
|
2700
|
+
throw new HTTPException6(422, { message: "OAuth client registration could not be loaded after a registration race" });
|
|
2601
2701
|
}
|
|
2602
2702
|
return dcrRegistrationFromStored(winner);
|
|
2603
2703
|
}
|
|
@@ -2649,7 +2749,7 @@ function normalizedIssuerKey(value) {
|
|
|
2649
2749
|
}
|
|
2650
2750
|
async function dynamicClientRegistration(settings, as, redirectUri) {
|
|
2651
2751
|
if (!as.registrationEndpoint) {
|
|
2652
|
-
throw new
|
|
2752
|
+
throw new HTTPException6(422, { message: "authorization server does not support dynamic client registration" });
|
|
2653
2753
|
}
|
|
2654
2754
|
await assertOAuthFetchAllowed(as.registrationEndpoint, settings);
|
|
2655
2755
|
const response = await fetchOAuth(as.registrationEndpoint, settings, {
|
|
@@ -2664,12 +2764,12 @@ async function dynamicClientRegistration(settings, as, redirectUri) {
|
|
|
2664
2764
|
})
|
|
2665
2765
|
});
|
|
2666
2766
|
if (!response.ok) {
|
|
2667
|
-
throw new
|
|
2767
|
+
throw new HTTPException6(422, { message: `dynamic client registration failed with HTTP ${response.status}` });
|
|
2668
2768
|
}
|
|
2669
2769
|
const payload = await response.json();
|
|
2670
2770
|
const clientId = stringValue(payload.client_id);
|
|
2671
2771
|
if (!clientId) {
|
|
2672
|
-
throw new
|
|
2772
|
+
throw new HTTPException6(422, { message: "dynamic client registration response did not include client_id" });
|
|
2673
2773
|
}
|
|
2674
2774
|
const clientSecret = stringValue(payload.client_secret);
|
|
2675
2775
|
return {
|
|
@@ -2698,12 +2798,12 @@ function buildAuthorizationUrl(input) {
|
|
|
2698
2798
|
function readOAuthState(state, settings) {
|
|
2699
2799
|
const payload = readSignedState2(state, requireIntegrationsStateSecret(settings));
|
|
2700
2800
|
if (!payload) {
|
|
2701
|
-
throw new
|
|
2801
|
+
throw new HTTPException6(400, { message: "invalid or expired OAuth state" });
|
|
2702
2802
|
}
|
|
2703
2803
|
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
2704
2804
|
const iat = numberValue(payload.iat);
|
|
2705
2805
|
if (iat === void 0 || nowSeconds - iat > oauthStateTtlMs / 1e3 || nowSeconds < iat) {
|
|
2706
|
-
throw new
|
|
2806
|
+
throw new HTTPException6(400, { message: "invalid or expired OAuth state" });
|
|
2707
2807
|
}
|
|
2708
2808
|
const parsed = {
|
|
2709
2809
|
accountId: requiredString(payload.accountId, "state.accountId"),
|
|
@@ -2745,7 +2845,7 @@ async function clientForState(db, settings, state) {
|
|
|
2745
2845
|
if (state.clientRegistrationMethod === "dcr") {
|
|
2746
2846
|
const stored = await loadIntegrationOAuthClient(db, settings, state.issuer);
|
|
2747
2847
|
if (!stored || stored.clientId !== state.clientId) {
|
|
2748
|
-
throw new
|
|
2848
|
+
throw new HTTPException6(400, { message: "OAuth client registration is no longer available" });
|
|
2749
2849
|
}
|
|
2750
2850
|
return {
|
|
2751
2851
|
method: "dcr",
|
|
@@ -2758,7 +2858,7 @@ async function clientForState(db, settings, state) {
|
|
|
2758
2858
|
}
|
|
2759
2859
|
const entry = operatorClientEntryFor(settings, [state.issuer, state.authorizationServer]);
|
|
2760
2860
|
if (!entry || entry.clientId !== state.clientId) {
|
|
2761
|
-
throw new
|
|
2861
|
+
throw new HTTPException6(400, { message: "operator OAuth client credentials are no longer available" });
|
|
2762
2862
|
}
|
|
2763
2863
|
return {
|
|
2764
2864
|
method: "operator",
|
|
@@ -2845,27 +2945,24 @@ function callbackReturnPath(returnPath, status, params) {
|
|
|
2845
2945
|
}
|
|
2846
2946
|
function canonicalMcpResource(value) {
|
|
2847
2947
|
if (!value) {
|
|
2848
|
-
throw new
|
|
2948
|
+
throw new HTTPException6(400, { message: "mcpUrl is required" });
|
|
2849
2949
|
}
|
|
2850
2950
|
let url;
|
|
2851
2951
|
try {
|
|
2852
2952
|
url = new URL(value);
|
|
2853
2953
|
} catch {
|
|
2854
|
-
throw new
|
|
2954
|
+
throw new HTTPException6(422, { message: "MCP resource URL is invalid" });
|
|
2855
2955
|
}
|
|
2856
2956
|
url.hash = "";
|
|
2857
2957
|
return url.toString();
|
|
2858
2958
|
}
|
|
2859
|
-
function canonicalProviderDomain(value) {
|
|
2860
|
-
return value.trim().toLowerCase().replace(/^www\./, "");
|
|
2861
|
-
}
|
|
2862
2959
|
function safeReturnPath(value) {
|
|
2863
2960
|
if (!value.startsWith("/") || value.startsWith("//")) {
|
|
2864
|
-
throw new
|
|
2961
|
+
throw new HTTPException6(400, { message: "OAuth returnPath must be a relative path" });
|
|
2865
2962
|
}
|
|
2866
2963
|
const parsed = new URL(value, "https://opengeni.local");
|
|
2867
2964
|
if (parsed.origin !== "https://opengeni.local") {
|
|
2868
|
-
throw new
|
|
2965
|
+
throw new HTTPException6(400, { message: "OAuth returnPath must be a relative path" });
|
|
2869
2966
|
}
|
|
2870
2967
|
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
2871
2968
|
}
|
|
@@ -2887,39 +2984,39 @@ async function fetchOAuth(rawUrl, settings, init = {}, hop = 0) {
|
|
|
2887
2984
|
return response;
|
|
2888
2985
|
}
|
|
2889
2986
|
if (hop >= 3) {
|
|
2890
|
-
throw new
|
|
2987
|
+
throw new HTTPException6(422, { message: "OAuth fetch exceeded maximum redirect hops" });
|
|
2891
2988
|
}
|
|
2892
2989
|
const location = response.headers.get("location");
|
|
2893
2990
|
if (!location) {
|
|
2894
|
-
throw new
|
|
2991
|
+
throw new HTTPException6(422, { message: "OAuth fetch redirect was missing Location" });
|
|
2895
2992
|
}
|
|
2896
2993
|
let nextUrl;
|
|
2897
2994
|
try {
|
|
2898
2995
|
nextUrl = new URL(location, rawUrl).toString();
|
|
2899
2996
|
} catch {
|
|
2900
|
-
throw new
|
|
2997
|
+
throw new HTTPException6(422, { message: "OAuth fetch redirect Location was invalid" });
|
|
2901
2998
|
}
|
|
2902
2999
|
return await fetchOAuth(nextUrl, settings, init, hop + 1);
|
|
2903
3000
|
}
|
|
2904
3001
|
async function assertOAuthFetchAllowed(rawUrl, settings) {
|
|
2905
3002
|
const url = new URL(rawUrl);
|
|
2906
3003
|
if (!["https:", "http:"].includes(url.protocol)) {
|
|
2907
|
-
throw new
|
|
3004
|
+
throw new HTTPException6(422, { message: "OAuth discovery only supports http and https URLs" });
|
|
2908
3005
|
}
|
|
2909
3006
|
if (settings.integrationsAllowPrivateNetworkTargets || ["local", "test"].includes(settings.environment)) {
|
|
2910
3007
|
return;
|
|
2911
3008
|
}
|
|
2912
3009
|
if (url.protocol !== "https:") {
|
|
2913
|
-
throw new
|
|
3010
|
+
throw new HTTPException6(422, { message: "OAuth discovery targets must use https outside local/test" });
|
|
2914
3011
|
}
|
|
2915
3012
|
const hostname = url.hostname.toLowerCase();
|
|
2916
3013
|
if (hostname === "localhost" || hostname.endsWith(".localhost")) {
|
|
2917
|
-
throw new
|
|
3014
|
+
throw new HTTPException6(422, { message: "OAuth discovery may not target localhost" });
|
|
2918
3015
|
}
|
|
2919
3016
|
const literal2 = isIP(hostname);
|
|
2920
3017
|
const addresses = literal2 ? [hostname] : (await lookup(hostname, { all: true })).map((entry) => entry.address);
|
|
2921
3018
|
if (addresses.some(isPrivateAddress)) {
|
|
2922
|
-
throw new
|
|
3019
|
+
throw new HTTPException6(422, { message: "OAuth discovery may not target private network addresses" });
|
|
2923
3020
|
}
|
|
2924
3021
|
}
|
|
2925
3022
|
function parseWwwAuthenticate(header) {
|
|
@@ -2978,7 +3075,7 @@ function registrationMethod(value) {
|
|
|
2978
3075
|
if (value === "operator" || value === "cimd" || value === "dcr") {
|
|
2979
3076
|
return value;
|
|
2980
3077
|
}
|
|
2981
|
-
throw new
|
|
3078
|
+
throw new HTTPException6(400, { message: "invalid OAuth state" });
|
|
2982
3079
|
}
|
|
2983
3080
|
function expiresAtFromTokenResponse(payload) {
|
|
2984
3081
|
const expiresAt = stringValue(payload.expires_at);
|
|
@@ -3013,7 +3110,7 @@ function numberValue(value) {
|
|
|
3013
3110
|
function requiredString(value, field) {
|
|
3014
3111
|
const result = stringValue(value);
|
|
3015
3112
|
if (!result) {
|
|
3016
|
-
throw new
|
|
3113
|
+
throw new HTTPException6(400, { message: `invalid OAuth state: missing ${field}` });
|
|
3017
3114
|
}
|
|
3018
3115
|
return result;
|
|
3019
3116
|
}
|
|
@@ -3023,7 +3120,7 @@ function registerConnectionRoutes(app, deps) {
|
|
|
3023
3120
|
const { db, settings } = deps;
|
|
3024
3121
|
function assertIntegrationsEnabled() {
|
|
3025
3122
|
if (!settings.integrationsEnabled) {
|
|
3026
|
-
throw new
|
|
3123
|
+
throw new HTTPException7(404, { message: "integrations are not enabled for this deployment" });
|
|
3027
3124
|
}
|
|
3028
3125
|
}
|
|
3029
3126
|
app.get("/v1/workspaces/:workspaceId/connections", async (c) => {
|
|
@@ -3043,7 +3140,7 @@ function registerConnectionRoutes(app, deps) {
|
|
|
3043
3140
|
accountId: grant.accountId,
|
|
3044
3141
|
workspaceId,
|
|
3045
3142
|
subjectId,
|
|
3046
|
-
providerDomain: payload.providerDomain,
|
|
3143
|
+
providerDomain: canonicalProviderDomain(payload.providerDomain),
|
|
3047
3144
|
kind: payload.kind,
|
|
3048
3145
|
credentialEncrypted: encryptCredentialBundle(key, payload.credential),
|
|
3049
3146
|
grantedScopes: payload.grantedScopes,
|
|
@@ -3058,7 +3155,7 @@ function registerConnectionRoutes(app, deps) {
|
|
|
3058
3155
|
const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:read");
|
|
3059
3156
|
const connection = await getConnectionMetadata2(db, workspaceId, c.req.param("connectionId"), grant.subjectId);
|
|
3060
3157
|
if (!connection) {
|
|
3061
|
-
throw new
|
|
3158
|
+
throw new HTTPException7(404, { message: "connection not found" });
|
|
3062
3159
|
}
|
|
3063
3160
|
return c.json(ConnectionResponse.parse({ connection }));
|
|
3064
3161
|
});
|
|
@@ -3068,10 +3165,10 @@ function registerConnectionRoutes(app, deps) {
|
|
|
3068
3165
|
const payload = UpdateConnectionRequest.parse(await c.req.json());
|
|
3069
3166
|
if (payload.status !== void 0) {
|
|
3070
3167
|
if (payload.status !== "active") {
|
|
3071
|
-
throw new
|
|
3168
|
+
throw new HTTPException7(400, { message: 'status can only be set to "active"; use DELETE to revoke' });
|
|
3072
3169
|
}
|
|
3073
3170
|
if (payload.credential === void 0) {
|
|
3074
|
-
throw new
|
|
3171
|
+
throw new HTTPException7(400, { message: "reactivating a connection requires a new credential" });
|
|
3075
3172
|
}
|
|
3076
3173
|
}
|
|
3077
3174
|
const key = payload.credential === void 0 ? null : requireEnvironmentEncryption3(settings);
|
|
@@ -3081,7 +3178,7 @@ function registerConnectionRoutes(app, deps) {
|
|
|
3081
3178
|
connectionId: c.req.param("connectionId"),
|
|
3082
3179
|
visibleToSubjectId: grant.subjectId,
|
|
3083
3180
|
updatedBySubjectId: grant.subjectId,
|
|
3084
|
-
...payload.providerDomain !== void 0 ? { providerDomain: payload.providerDomain } : {},
|
|
3181
|
+
...payload.providerDomain !== void 0 ? { providerDomain: canonicalProviderDomain(payload.providerDomain) } : {},
|
|
3085
3182
|
...subjectId !== void 0 ? { subjectId } : {},
|
|
3086
3183
|
...payload.kind !== void 0 ? { kind: payload.kind } : {},
|
|
3087
3184
|
...payload.status !== void 0 ? { status: payload.status } : {},
|
|
@@ -3091,7 +3188,7 @@ function registerConnectionRoutes(app, deps) {
|
|
|
3091
3188
|
...payload.metadata !== void 0 ? { metadata: payload.metadata } : {}
|
|
3092
3189
|
});
|
|
3093
3190
|
if (!connection) {
|
|
3094
|
-
throw new
|
|
3191
|
+
throw new HTTPException7(404, { message: "connection not found" });
|
|
3095
3192
|
}
|
|
3096
3193
|
return c.json(ConnectionResponse.parse({ connection }));
|
|
3097
3194
|
});
|
|
@@ -3100,7 +3197,7 @@ function registerConnectionRoutes(app, deps) {
|
|
|
3100
3197
|
const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:write");
|
|
3101
3198
|
const connection = await revokeConnection(db, workspaceId, c.req.param("connectionId"), grant.subjectId);
|
|
3102
3199
|
if (!connection) {
|
|
3103
|
-
throw new
|
|
3200
|
+
throw new HTTPException7(404, { message: "connection not found" });
|
|
3104
3201
|
}
|
|
3105
3202
|
return c.json(ConnectionResponse.parse({ connection }));
|
|
3106
3203
|
});
|
|
@@ -3110,7 +3207,7 @@ function registerConnectionRoutes(app, deps) {
|
|
|
3110
3207
|
const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:write");
|
|
3111
3208
|
const parsed = OAuthStartRequest.safeParse(await c.req.json());
|
|
3112
3209
|
if (!parsed.success) {
|
|
3113
|
-
throw new
|
|
3210
|
+
throw new HTTPException7(400, { message: parsed.error.issues[0]?.message ?? "invalid OAuth start request" });
|
|
3114
3211
|
}
|
|
3115
3212
|
const payload = parsed.data;
|
|
3116
3213
|
const result = await startMcpOAuth({ db, settings }, {
|
|
@@ -3149,7 +3246,7 @@ function writableSubjectId(requested, grantSubjectId) {
|
|
|
3149
3246
|
return null;
|
|
3150
3247
|
}
|
|
3151
3248
|
if (requested !== grantSubjectId) {
|
|
3152
|
-
throw new
|
|
3249
|
+
throw new HTTPException7(403, { message: "cannot write a connection for another subject" });
|
|
3153
3250
|
}
|
|
3154
3251
|
return requested;
|
|
3155
3252
|
}
|
|
@@ -3187,7 +3284,7 @@ import {
|
|
|
3187
3284
|
searchDocuments as searchDocuments2
|
|
3188
3285
|
} from "@opengeni/documents";
|
|
3189
3286
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
3190
|
-
import { HTTPException as
|
|
3287
|
+
import { HTTPException as HTTPException8 } from "hono/http-exception";
|
|
3191
3288
|
import { requireAccessGrant as requireAccessGrant4 } from "@opengeni/core";
|
|
3192
3289
|
import { recordWorkspaceUsage as recordWorkspaceUsage2, requireLimit as requireLimit2 } from "@opengeni/core";
|
|
3193
3290
|
|
|
@@ -3341,7 +3438,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
3341
3438
|
await requireAccessGrant4(c, deps, workspaceId, "documents:search");
|
|
3342
3439
|
const base = await getDocumentBase(db, workspaceId, c.req.param("baseId"));
|
|
3343
3440
|
if (!base) {
|
|
3344
|
-
throw new
|
|
3441
|
+
throw new HTTPException8(404, { message: "document base not found" });
|
|
3345
3442
|
}
|
|
3346
3443
|
return c.json(DocumentBase.parse(base));
|
|
3347
3444
|
});
|
|
@@ -3349,7 +3446,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
3349
3446
|
const workspaceId = c.req.param("workspaceId");
|
|
3350
3447
|
const grant = await requireAccessGrant4(c, deps, workspaceId, "documents:manage");
|
|
3351
3448
|
if (!objectStorage) {
|
|
3352
|
-
throw new
|
|
3449
|
+
throw new HTTPException8(503, { message: "object storage is not configured" });
|
|
3353
3450
|
}
|
|
3354
3451
|
await requireLimit2(deps, { accountId: grant.accountId, workspaceId, action: "document:index", quantity: 0 });
|
|
3355
3452
|
const payload = AddDocumentRequest.parse(await c.req.json());
|
|
@@ -3399,19 +3496,19 @@ function registerDocumentRoutes(app, deps) {
|
|
|
3399
3496
|
const workspaceId = c.req.param("workspaceId");
|
|
3400
3497
|
const grant = await requireAccessGrant4(c, deps, workspaceId, "documents:manage");
|
|
3401
3498
|
if (!objectStorage) {
|
|
3402
|
-
throw new
|
|
3499
|
+
throw new HTTPException8(503, { message: "object storage is not configured" });
|
|
3403
3500
|
}
|
|
3404
3501
|
await requireLimit2(deps, { accountId: grant.accountId, workspaceId, action: "document:index", quantity: 0 });
|
|
3405
3502
|
try {
|
|
3406
3503
|
const document = await getDocument(db, workspaceId, c.req.param("documentId"));
|
|
3407
3504
|
if (!document) {
|
|
3408
|
-
throw new
|
|
3505
|
+
throw new HTTPException8(404, { message: "document not found" });
|
|
3409
3506
|
}
|
|
3410
3507
|
if (document.status !== "failed") {
|
|
3411
|
-
throw new
|
|
3508
|
+
throw new HTTPException8(422, { message: "only failed documents can be retried" });
|
|
3412
3509
|
}
|
|
3413
3510
|
if (document.baseId !== c.req.param("baseId")) {
|
|
3414
|
-
throw new
|
|
3511
|
+
throw new HTTPException8(404, { message: "document not found" });
|
|
3415
3512
|
}
|
|
3416
3513
|
const queued = await queueDocumentForReindex(db, workspaceId, document.id);
|
|
3417
3514
|
const indexed = await documentIndexer.indexDocument({ accountId: grant.accountId, workspaceId, documentId: document.id }) ?? queued;
|
|
@@ -3430,7 +3527,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
3430
3527
|
}
|
|
3431
3528
|
return c.json(Document.parse(indexed));
|
|
3432
3529
|
} catch (error) {
|
|
3433
|
-
if (error instanceof
|
|
3530
|
+
if (error instanceof HTTPException8) {
|
|
3434
3531
|
throw error;
|
|
3435
3532
|
}
|
|
3436
3533
|
throw documentHttpException(error);
|
|
@@ -3442,7 +3539,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
3442
3539
|
const payload = DocumentSearchRequest.parse(await c.req.json());
|
|
3443
3540
|
const base = await getDocumentBase(db, workspaceId, c.req.param("baseId"));
|
|
3444
3541
|
if (!base) {
|
|
3445
|
-
throw new
|
|
3542
|
+
throw new HTTPException8(404, { message: "document base not found" });
|
|
3446
3543
|
}
|
|
3447
3544
|
return c.json({
|
|
3448
3545
|
results: await searchDocuments2(db, {
|
|
@@ -3483,7 +3580,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
3483
3580
|
limit: c.req.query("limit") ? Number(c.req.query("limit")) : void 0
|
|
3484
3581
|
});
|
|
3485
3582
|
if (!parsed.success) {
|
|
3486
|
-
throw new
|
|
3583
|
+
throw new HTTPException8(400, { message: "invalid knowledge memory query parameters" });
|
|
3487
3584
|
}
|
|
3488
3585
|
return c.json((await listKnowledgeMemories2(db, workspaceId, parsed.data)).map((memory) => KnowledgeMemory.parse(memory)));
|
|
3489
3586
|
});
|
|
@@ -3492,7 +3589,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
3492
3589
|
await requireAccessGrant4(c, deps, workspaceId, "documents:search");
|
|
3493
3590
|
const memory = await getKnowledgeMemory(db, workspaceId, c.req.param("memoryId"));
|
|
3494
3591
|
if (!memory) {
|
|
3495
|
-
throw new
|
|
3592
|
+
throw new HTTPException8(404, { message: "knowledge memory not found" });
|
|
3496
3593
|
}
|
|
3497
3594
|
return c.json(KnowledgeMemory.parse(memory));
|
|
3498
3595
|
});
|
|
@@ -3533,12 +3630,12 @@ function registerDocumentRoutes(app, deps) {
|
|
|
3533
3630
|
function documentHttpException(error) {
|
|
3534
3631
|
const message = error instanceof Error ? error.message : String(error);
|
|
3535
3632
|
if (message.includes("not found")) {
|
|
3536
|
-
return new
|
|
3633
|
+
return new HTTPException8(404, { message });
|
|
3537
3634
|
}
|
|
3538
3635
|
if (message.includes("pending") || message.includes("failed") || message.includes("deleted")) {
|
|
3539
|
-
return new
|
|
3636
|
+
return new HTTPException8(422, { message });
|
|
3540
3637
|
}
|
|
3541
|
-
return new
|
|
3638
|
+
return new HTTPException8(500, { message });
|
|
3542
3639
|
}
|
|
3543
3640
|
|
|
3544
3641
|
// src/routes/enrollments.ts
|
|
@@ -3564,7 +3661,7 @@ import {
|
|
|
3564
3661
|
listEnrollments,
|
|
3565
3662
|
revokeEnrollment
|
|
3566
3663
|
} from "@opengeni/db";
|
|
3567
|
-
import { HTTPException as
|
|
3664
|
+
import { HTTPException as HTTPException9 } from "hono/http-exception";
|
|
3568
3665
|
import { requireAccessGrant as requireAccessGrant5 } from "@opengeni/core";
|
|
3569
3666
|
|
|
3570
3667
|
// src/sandbox/enrollment.ts
|
|
@@ -3848,7 +3945,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
3848
3945
|
const { settings, db } = deps;
|
|
3849
3946
|
function assertSelfhostedEnabled() {
|
|
3850
3947
|
if (!settings.sandboxSelfhostedEnabled) {
|
|
3851
|
-
throw new
|
|
3948
|
+
throw new HTTPException9(404, { message: "selfhosted enrollment is not enabled for this deployment" });
|
|
3852
3949
|
}
|
|
3853
3950
|
}
|
|
3854
3951
|
const startLimiter = new TokenBucket({ capacity: 10, refillPerSecond: 0.5 });
|
|
@@ -3858,7 +3955,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
3858
3955
|
function rateLimit(c, limiter) {
|
|
3859
3956
|
const ip = clientIp(c);
|
|
3860
3957
|
if (!limiter.take(ip)) {
|
|
3861
|
-
throw new
|
|
3958
|
+
throw new HTTPException9(429, { message: "too many requests; slow down" });
|
|
3862
3959
|
}
|
|
3863
3960
|
}
|
|
3864
3961
|
app.post("/v1/enrollments/device/start", async (c) => {
|
|
@@ -3866,12 +3963,12 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
3866
3963
|
rateLimit(c, startLimiter);
|
|
3867
3964
|
const parsed = DeviceEnrollmentStartRequest.safeParse(await c.req.json().catch(() => null));
|
|
3868
3965
|
if (!parsed.success) {
|
|
3869
|
-
throw new
|
|
3966
|
+
throw new HTTPException9(400, { message: "invalid device-start request" });
|
|
3870
3967
|
}
|
|
3871
3968
|
const body = parsed.data;
|
|
3872
3969
|
const workspace = await getWorkspace(db, body.workspaceId);
|
|
3873
3970
|
if (!workspace) {
|
|
3874
|
-
throw new
|
|
3971
|
+
throw new HTTPException9(404, { message: "workspace not found" });
|
|
3875
3972
|
}
|
|
3876
3973
|
const result = await startDeviceEnrollment({ db, settings }, {
|
|
3877
3974
|
accountId: workspace.accountId,
|
|
@@ -3892,7 +3989,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
3892
3989
|
rateLimit(c, pollLimiter);
|
|
3893
3990
|
const parsed = DeviceEnrollmentPollRequest.safeParse(await c.req.json().catch(() => null));
|
|
3894
3991
|
if (!parsed.success) {
|
|
3895
|
-
throw new
|
|
3992
|
+
throw new HTTPException9(400, { message: "invalid device-poll request" });
|
|
3896
3993
|
}
|
|
3897
3994
|
const result = await pollDeviceEnrollment({ db, settings }, { deviceCode: parsed.data.deviceCode });
|
|
3898
3995
|
return c.json(result, 200);
|
|
@@ -3902,16 +3999,16 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
3902
3999
|
rateLimit(c, lookupLimiter);
|
|
3903
4000
|
const parsed = DeviceEnrollmentLookupRequest.safeParse(await c.req.json().catch(() => null));
|
|
3904
4001
|
if (!parsed.success) {
|
|
3905
|
-
throw new
|
|
4002
|
+
throw new HTTPException9(400, { message: "invalid device-lookup request" });
|
|
3906
4003
|
}
|
|
3907
4004
|
const record3 = await lookupDeviceEnrollment({ db, settings }, { userCode: parsed.data.userCode });
|
|
3908
4005
|
if (!record3) {
|
|
3909
|
-
throw new
|
|
4006
|
+
throw new HTTPException9(404, { message: "no pending enrollment for that code" });
|
|
3910
4007
|
}
|
|
3911
4008
|
try {
|
|
3912
4009
|
await requireAccessGrant5(c, deps, record3.workspaceId, "enrollments:read");
|
|
3913
4010
|
} catch {
|
|
3914
|
-
throw new
|
|
4011
|
+
throw new HTTPException9(404, { message: "no pending enrollment for that code" });
|
|
3915
4012
|
}
|
|
3916
4013
|
return c.json(DeviceEnrollmentLookupResponse.parse(toLookupResponse(record3)), 200);
|
|
3917
4014
|
});
|
|
@@ -3920,7 +4017,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
3920
4017
|
rateLimit(c, exchangeLimiter);
|
|
3921
4018
|
const parsed = EnrollTokenExchangeRequest.safeParse(await c.req.json().catch(() => null));
|
|
3922
4019
|
if (!parsed.success) {
|
|
3923
|
-
throw new
|
|
4020
|
+
throw new HTTPException9(400, { message: "invalid enroll-token-exchange request" });
|
|
3924
4021
|
}
|
|
3925
4022
|
const body = parsed.data;
|
|
3926
4023
|
const result = await exchangeEnrollToken({ db, settings }, {
|
|
@@ -3933,9 +4030,9 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
3933
4030
|
});
|
|
3934
4031
|
if (!result.ok) {
|
|
3935
4032
|
if (result.reason === "disabled") {
|
|
3936
|
-
throw new
|
|
4033
|
+
throw new HTTPException9(503, { message: "enrollment credential plane is not configured" });
|
|
3937
4034
|
}
|
|
3938
|
-
throw new
|
|
4035
|
+
throw new HTTPException9(401, { message: "invalid or expired enroll token" });
|
|
3939
4036
|
}
|
|
3940
4037
|
return c.json(EnrollTokenExchangeResponse.parse({ credentials: result.credentials }), 201);
|
|
3941
4038
|
});
|
|
@@ -3945,7 +4042,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
3945
4042
|
assertSelfhostedEnabled();
|
|
3946
4043
|
const parsed = DeviceEnrollmentApproveRequest.safeParse(await c.req.json().catch(() => null));
|
|
3947
4044
|
if (!parsed.success) {
|
|
3948
|
-
throw new
|
|
4045
|
+
throw new HTTPException9(400, { message: "invalid device-approve request" });
|
|
3949
4046
|
}
|
|
3950
4047
|
const body = parsed.data;
|
|
3951
4048
|
const approved = await approveDeviceEnrollment({ db, settings }, {
|
|
@@ -3958,7 +4055,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
3958
4055
|
approvedBySubjectLabel: grant.subjectLabel ?? null
|
|
3959
4056
|
});
|
|
3960
4057
|
if (!approved) {
|
|
3961
|
-
throw new
|
|
4058
|
+
throw new HTTPException9(404, { message: "no pending enrollment for that code" });
|
|
3962
4059
|
}
|
|
3963
4060
|
return c.json(DeviceEnrollmentApproveResponse.parse({
|
|
3964
4061
|
approved: true,
|
|
@@ -3973,7 +4070,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
3973
4070
|
assertSelfhostedEnabled();
|
|
3974
4071
|
const parsed = DeviceEnrollmentDenyRequest.safeParse(await c.req.json().catch(() => null));
|
|
3975
4072
|
if (!parsed.success) {
|
|
3976
|
-
throw new
|
|
4073
|
+
throw new HTTPException9(400, { message: "invalid device-deny request" });
|
|
3977
4074
|
}
|
|
3978
4075
|
const result = await denyDeviceEnrollment({ db, settings }, {
|
|
3979
4076
|
accountId: grant.accountId,
|
|
@@ -3988,7 +4085,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
3988
4085
|
assertSelfhostedEnabled();
|
|
3989
4086
|
const parsed = MintEnrollTokenRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
3990
4087
|
if (!parsed.success) {
|
|
3991
|
-
throw new
|
|
4088
|
+
throw new HTTPException9(400, { message: "invalid mint-enroll-token request" });
|
|
3992
4089
|
}
|
|
3993
4090
|
const minted = await mintEnrollToken({ db, settings }, {
|
|
3994
4091
|
accountId: grant.accountId,
|
|
@@ -3996,7 +4093,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
3996
4093
|
allowScreenControl: parsed.data.allowScreenControl
|
|
3997
4094
|
});
|
|
3998
4095
|
if (!minted) {
|
|
3999
|
-
throw new
|
|
4096
|
+
throw new HTTPException9(503, { message: "enrollment credential plane is not configured" });
|
|
4000
4097
|
}
|
|
4001
4098
|
return c.json(MintEnrollTokenResponse.parse(minted), 201);
|
|
4002
4099
|
});
|
|
@@ -4080,7 +4177,7 @@ import {
|
|
|
4080
4177
|
getEnrollment as getEnrollment2,
|
|
4081
4178
|
readMachineMetricsSeries
|
|
4082
4179
|
} from "@opengeni/db";
|
|
4083
|
-
import { HTTPException as
|
|
4180
|
+
import { HTTPException as HTTPException10 } from "hono/http-exception";
|
|
4084
4181
|
import { requireAccessGrant as requireAccessGrant6 } from "@opengeni/core";
|
|
4085
4182
|
import { buildFleetContextForSession as buildFleetContextForSession2, swapActiveSandbox as swapActiveSandbox2 } from "@opengeni/core";
|
|
4086
4183
|
|
|
@@ -4255,7 +4352,7 @@ function registerMachineRoutes(app, deps) {
|
|
|
4255
4352
|
const { settings, db, bus } = deps;
|
|
4256
4353
|
function assertSelfhostedEnabled() {
|
|
4257
4354
|
if (!settings.sandboxSelfhostedEnabled) {
|
|
4258
|
-
throw new
|
|
4355
|
+
throw new HTTPException10(404, { message: "selfhosted machines are not enabled for this deployment" });
|
|
4259
4356
|
}
|
|
4260
4357
|
}
|
|
4261
4358
|
app.get("/v1/workspaces/:workspaceId/machines", async (c) => {
|
|
@@ -4273,7 +4370,7 @@ function registerMachineRoutes(app, deps) {
|
|
|
4273
4370
|
const enrollmentId = c.req.param("enrollmentId");
|
|
4274
4371
|
const enrollment = await getEnrollment2(db, workspaceId, enrollmentId);
|
|
4275
4372
|
if (!enrollment) {
|
|
4276
|
-
throw new
|
|
4373
|
+
throw new HTTPException10(404, { message: "machine not found in this workspace" });
|
|
4277
4374
|
}
|
|
4278
4375
|
const windowMs = SERIES_WINDOWS_MS[c.req.query("window") ?? ""] ?? DEFAULT_SERIES_WINDOW_MS;
|
|
4279
4376
|
const since = new Date(Date.now() - windowMs);
|
|
@@ -4318,7 +4415,7 @@ import {
|
|
|
4318
4415
|
setWorkspaceEnvironmentVariable as setWorkspaceEnvironmentVariable2,
|
|
4319
4416
|
updateWorkspaceEnvironment
|
|
4320
4417
|
} from "@opengeni/db";
|
|
4321
|
-
import { HTTPException as
|
|
4418
|
+
import { HTTPException as HTTPException11 } from "hono/http-exception";
|
|
4322
4419
|
import { requireAccessGrant as requireAccessGrant7 } from "@opengeni/core";
|
|
4323
4420
|
import {
|
|
4324
4421
|
assertAllowedEnvironmentVariableName as assertAllowedEnvironmentVariableName2,
|
|
@@ -4342,21 +4439,21 @@ function registerEnvironmentRoutes(app, deps) {
|
|
|
4342
4439
|
const payload = CreateWorkspaceEnvironmentRequest.parse(await c.req.json());
|
|
4343
4440
|
const name = trimmedEnvironmentName(payload.name);
|
|
4344
4441
|
if (payload.variables.length > MAX_VARIABLES_PER_ENVIRONMENT2) {
|
|
4345
|
-
throw new
|
|
4442
|
+
throw new HTTPException11(422, { message: `an environment supports at most ${MAX_VARIABLES_PER_ENVIRONMENT2} variables` });
|
|
4346
4443
|
}
|
|
4347
4444
|
const variableNames = /* @__PURE__ */ new Set();
|
|
4348
4445
|
for (const variable of payload.variables) {
|
|
4349
4446
|
assertAllowedEnvironmentVariableName2(variable.name);
|
|
4350
4447
|
if (variableNames.has(variable.name)) {
|
|
4351
|
-
throw new
|
|
4448
|
+
throw new HTTPException11(422, { message: `duplicate environment variable name: ${variable.name}` });
|
|
4352
4449
|
}
|
|
4353
4450
|
variableNames.add(variable.name);
|
|
4354
4451
|
}
|
|
4355
4452
|
if (await countWorkspaceEnvironments2(db, workspaceId) >= MAX_ENVIRONMENTS_PER_WORKSPACE2) {
|
|
4356
|
-
throw new
|
|
4453
|
+
throw new HTTPException11(422, { message: `a workspace supports at most ${MAX_ENVIRONMENTS_PER_WORKSPACE2} environments` });
|
|
4357
4454
|
}
|
|
4358
4455
|
if (await getWorkspaceEnvironmentByName2(db, workspaceId, name)) {
|
|
4359
|
-
throw new
|
|
4456
|
+
throw new HTTPException11(409, { message: `environment name is already in use: ${name}` });
|
|
4360
4457
|
}
|
|
4361
4458
|
const created = await createWorkspaceEnvironment2(db, {
|
|
4362
4459
|
accountId: grant.accountId,
|
|
@@ -4385,7 +4482,7 @@ function registerEnvironmentRoutes(app, deps) {
|
|
|
4385
4482
|
if (name !== void 0 && name !== environment.name) {
|
|
4386
4483
|
const existing = await getWorkspaceEnvironmentByName2(db, workspaceId, name);
|
|
4387
4484
|
if (existing && existing.id !== environment.id) {
|
|
4388
|
-
throw new
|
|
4485
|
+
throw new HTTPException11(409, { message: `environment name is already in use: ${name}` });
|
|
4389
4486
|
}
|
|
4390
4487
|
}
|
|
4391
4488
|
const updated = await updateWorkspaceEnvironment(db, workspaceId, environment.id, {
|
|
@@ -4401,11 +4498,11 @@ function registerEnvironmentRoutes(app, deps) {
|
|
|
4401
4498
|
const environment = await requireEnvironmentForApi(db, workspaceId, c.req.param("environmentId"));
|
|
4402
4499
|
const attachedTasks = await countScheduledTasksUsingEnvironment(db, workspaceId, environment.id);
|
|
4403
4500
|
if (attachedTasks > 0) {
|
|
4404
|
-
throw new
|
|
4501
|
+
throw new HTTPException11(409, { message: `environment is attached to ${attachedTasks} scheduled task(s); detach first` });
|
|
4405
4502
|
}
|
|
4406
4503
|
const activeSessions = await countActiveSessionsUsingEnvironment(db, workspaceId, environment.id);
|
|
4407
4504
|
if (activeSessions > 0) {
|
|
4408
|
-
throw new
|
|
4505
|
+
throw new HTTPException11(409, { message: `environment is attached to ${activeSessions} active session(s); wait for them to finish or cancel them first` });
|
|
4409
4506
|
}
|
|
4410
4507
|
await deleteWorkspaceEnvironment(db, workspaceId, environment.id);
|
|
4411
4508
|
await recordEnvironmentAuditEvent2(db, { grant, action: "environment.deleted", environmentId: environment.id });
|
|
@@ -4420,7 +4517,7 @@ function registerEnvironmentRoutes(app, deps) {
|
|
|
4420
4517
|
const payload = SetWorkspaceEnvironmentVariableRequest.parse(await c.req.json());
|
|
4421
4518
|
const exists = environment.variables.some((variable) => variable.name === name);
|
|
4422
4519
|
if (!exists && environment.variables.length >= MAX_VARIABLES_PER_ENVIRONMENT2) {
|
|
4423
|
-
throw new
|
|
4520
|
+
throw new HTTPException11(422, { message: `an environment supports at most ${MAX_VARIABLES_PER_ENVIRONMENT2} variables` });
|
|
4424
4521
|
}
|
|
4425
4522
|
const metadata = await setWorkspaceEnvironmentVariable2(db, {
|
|
4426
4523
|
accountId: grant.accountId,
|
|
@@ -4439,7 +4536,7 @@ function registerEnvironmentRoutes(app, deps) {
|
|
|
4439
4536
|
const environment = await requireEnvironmentForApi(db, workspaceId, c.req.param("environmentId"));
|
|
4440
4537
|
const deleted = await deleteWorkspaceEnvironmentVariable(db, workspaceId, environment.id, name);
|
|
4441
4538
|
if (!deleted) {
|
|
4442
|
-
throw new
|
|
4539
|
+
throw new HTTPException11(404, { message: "environment variable not found" });
|
|
4443
4540
|
}
|
|
4444
4541
|
await recordEnvironmentAuditEvent2(db, { grant, action: "environment.variable.deleted", environmentId: environment.id, variableName: name });
|
|
4445
4542
|
return c.json({ ok: true });
|
|
@@ -4448,7 +4545,7 @@ function registerEnvironmentRoutes(app, deps) {
|
|
|
4448
4545
|
function parseVariableName(raw) {
|
|
4449
4546
|
const parsed = WorkspaceEnvironmentVariableName2.safeParse(raw);
|
|
4450
4547
|
if (!parsed.success) {
|
|
4451
|
-
throw new
|
|
4548
|
+
throw new HTTPException11(422, { message: "environment variable names must match ^[A-Z][A-Z0-9_]*$" });
|
|
4452
4549
|
}
|
|
4453
4550
|
assertAllowedEnvironmentVariableName2(parsed.data);
|
|
4454
4551
|
return parsed.data;
|
|
@@ -4456,7 +4553,7 @@ function parseVariableName(raw) {
|
|
|
4456
4553
|
function trimmedEnvironmentName(name) {
|
|
4457
4554
|
const trimmed = name.trim();
|
|
4458
4555
|
if (!trimmed) {
|
|
4459
|
-
throw new
|
|
4556
|
+
throw new HTTPException11(422, { message: "environment name is required" });
|
|
4460
4557
|
}
|
|
4461
4558
|
return trimmed;
|
|
4462
4559
|
}
|
|
@@ -4476,7 +4573,7 @@ import {
|
|
|
4476
4573
|
markFileUploadFailed,
|
|
4477
4574
|
requireFile as requireFile2
|
|
4478
4575
|
} from "@opengeni/db";
|
|
4479
|
-
import { HTTPException as
|
|
4576
|
+
import { HTTPException as HTTPException12 } from "hono/http-exception";
|
|
4480
4577
|
import { requireAccessGrant as requireAccessGrant8 } from "@opengeni/core";
|
|
4481
4578
|
import { recordWorkspaceUsage as recordWorkspaceUsage3, requireLimit as requireLimit3 } from "@opengeni/core";
|
|
4482
4579
|
function registerFileRoutes(app, deps) {
|
|
@@ -4485,12 +4582,12 @@ function registerFileRoutes(app, deps) {
|
|
|
4485
4582
|
const workspaceId = c.req.param("workspaceId");
|
|
4486
4583
|
const grant = await requireAccessGrant8(c, deps, workspaceId, "files:upload");
|
|
4487
4584
|
if (!objectStorage) {
|
|
4488
|
-
throw new
|
|
4585
|
+
throw new HTTPException12(503, { message: "object storage is not configured" });
|
|
4489
4586
|
}
|
|
4490
4587
|
const payload = CreateFileUploadRequest.parse(await c.req.json());
|
|
4491
4588
|
await requireLimit3(deps, { accountId: grant.accountId, workspaceId, action: "file:upload", quantity: payload.sizeBytes });
|
|
4492
4589
|
if (payload.sizeBytes > objectStorage.maxSinglePutSizeBytes) {
|
|
4493
|
-
throw new
|
|
4590
|
+
throw new HTTPException12(413, { message: `file exceeds single PUT limit of ${objectStorage.maxSinglePutSizeBytes} bytes` });
|
|
4494
4591
|
}
|
|
4495
4592
|
const fileId = crypto.randomUUID();
|
|
4496
4593
|
const safeFilename = sanitizeFilename(payload.filename);
|
|
@@ -4526,33 +4623,33 @@ function registerFileRoutes(app, deps) {
|
|
|
4526
4623
|
const workspaceId = c.req.param("workspaceId");
|
|
4527
4624
|
const grant = await requireAccessGrant8(c, deps, workspaceId, "files:upload");
|
|
4528
4625
|
if (!objectStorage) {
|
|
4529
|
-
throw new
|
|
4626
|
+
throw new HTTPException12(503, { message: "object storage is not configured" });
|
|
4530
4627
|
}
|
|
4531
4628
|
const upload = await getFileUpload(db, workspaceId, c.req.param("uploadId"));
|
|
4532
4629
|
if (!upload) {
|
|
4533
|
-
throw new
|
|
4630
|
+
throw new HTTPException12(404, { message: "file upload not found" });
|
|
4534
4631
|
}
|
|
4535
4632
|
if (upload.status !== "pending") {
|
|
4536
|
-
throw new
|
|
4633
|
+
throw new HTTPException12(409, { message: `file upload is ${upload.status}` });
|
|
4537
4634
|
}
|
|
4538
4635
|
if (upload.expiresAt.getTime() < Date.now()) {
|
|
4539
4636
|
await markFileUploadFailed(db, workspaceId, upload.id, upload.file.id);
|
|
4540
|
-
throw new
|
|
4637
|
+
throw new HTTPException12(409, { message: "file upload has expired" });
|
|
4541
4638
|
}
|
|
4542
4639
|
const head = await objectStorage.headFile(upload.file).catch((error) => {
|
|
4543
|
-
throw new
|
|
4640
|
+
throw new HTTPException12(409, { message: `uploaded object is not available: ${error instanceof Error ? error.message : String(error)}` });
|
|
4544
4641
|
});
|
|
4545
4642
|
if (Number(head.ContentLength ?? -1) !== upload.file.sizeBytes) {
|
|
4546
4643
|
await markFileUploadFailed(db, workspaceId, upload.id, upload.file.id);
|
|
4547
|
-
throw new
|
|
4644
|
+
throw new HTTPException12(422, { message: "uploaded object size does not match file metadata" });
|
|
4548
4645
|
}
|
|
4549
4646
|
if (upload.file.contentType && head.ContentType && head.ContentType !== upload.file.contentType) {
|
|
4550
4647
|
await markFileUploadFailed(db, workspaceId, upload.id, upload.file.id);
|
|
4551
|
-
throw new
|
|
4648
|
+
throw new HTTPException12(422, { message: "uploaded object content type does not match file metadata" });
|
|
4552
4649
|
}
|
|
4553
4650
|
if (upload.file.sha256 && head.Metadata?.sha256 !== upload.file.sha256) {
|
|
4554
4651
|
await markFileUploadFailed(db, workspaceId, upload.id, upload.file.id);
|
|
4555
|
-
throw new
|
|
4652
|
+
throw new HTTPException12(422, { message: "uploaded object checksum metadata does not match file metadata" });
|
|
4556
4653
|
}
|
|
4557
4654
|
const file = await completeFileUpload(db, workspaceId, upload.id);
|
|
4558
4655
|
await recordWorkspaceUsage3(deps, {
|
|
@@ -4573,7 +4670,7 @@ function registerFileRoutes(app, deps) {
|
|
|
4573
4670
|
await requireAccessGrant8(c, deps, workspaceId, "files:read");
|
|
4574
4671
|
const file = await requireFile2(db, workspaceId, c.req.param("fileId")).catch(() => null);
|
|
4575
4672
|
if (!file) {
|
|
4576
|
-
throw new
|
|
4673
|
+
throw new HTTPException12(404, { message: "file not found" });
|
|
4577
4674
|
}
|
|
4578
4675
|
return c.json(FileAsset.parse(file));
|
|
4579
4676
|
});
|
|
@@ -4581,14 +4678,14 @@ function registerFileRoutes(app, deps) {
|
|
|
4581
4678
|
const workspaceId = c.req.param("workspaceId");
|
|
4582
4679
|
await requireAccessGrant8(c, deps, workspaceId, "files:read");
|
|
4583
4680
|
if (!objectStorage) {
|
|
4584
|
-
throw new
|
|
4681
|
+
throw new HTTPException12(503, { message: "object storage is not configured" });
|
|
4585
4682
|
}
|
|
4586
4683
|
const file = await requireFile2(db, workspaceId, c.req.param("fileId")).catch(() => null);
|
|
4587
4684
|
if (!file) {
|
|
4588
|
-
throw new
|
|
4685
|
+
throw new HTTPException12(404, { message: "file not found" });
|
|
4589
4686
|
}
|
|
4590
4687
|
if (file.status !== "ready") {
|
|
4591
|
-
throw new
|
|
4688
|
+
throw new HTTPException12(409, { message: `file is ${file.status}` });
|
|
4592
4689
|
}
|
|
4593
4690
|
const signed = await objectStorage.createGetUrl({ key: file.objectKey });
|
|
4594
4691
|
return c.json(FileDownloadUrlResponse.parse({
|
|
@@ -4607,7 +4704,7 @@ function sanitizeFilename(filename) {
|
|
|
4607
4704
|
import { CreateApiKeyRequest, CreateApiKeyResponse } from "@opengeni/contracts";
|
|
4608
4705
|
import { createApiKey, listApiKeys, revokeApiKey } from "@opengeni/db";
|
|
4609
4706
|
import { zValidator } from "@hono/zod-validator";
|
|
4610
|
-
import { HTTPException as
|
|
4707
|
+
import { HTTPException as HTTPException13 } from "hono/http-exception";
|
|
4611
4708
|
import { requireAccessGrant as requireAccessGrant9 } from "@opengeni/core";
|
|
4612
4709
|
import { requireLimit as requireLimit4 } from "@opengeni/core";
|
|
4613
4710
|
function registerApiKeyRoutes(app, deps) {
|
|
@@ -4648,7 +4745,7 @@ function ensureDelegablePermissions(grantPermissions, requested) {
|
|
|
4648
4745
|
}
|
|
4649
4746
|
const missing = requested.filter((permission) => !grantPermissions.includes(permission));
|
|
4650
4747
|
if (missing.length > 0) {
|
|
4651
|
-
throw new
|
|
4748
|
+
throw new HTTPException13(403, { message: `cannot delegate missing permissions: ${missing.join(", ")}` });
|
|
4652
4749
|
}
|
|
4653
4750
|
}
|
|
4654
4751
|
function generateApiKeyToken() {
|
|
@@ -4680,7 +4777,7 @@ import {
|
|
|
4680
4777
|
recordStripeWebhookEvent,
|
|
4681
4778
|
upsertBillingCustomer
|
|
4682
4779
|
} from "@opengeni/db";
|
|
4683
|
-
import { HTTPException as
|
|
4780
|
+
import { HTTPException as HTTPException14 } from "hono/http-exception";
|
|
4684
4781
|
import Stripe from "stripe";
|
|
4685
4782
|
import { requireAccessContext } from "@opengeni/core";
|
|
4686
4783
|
function registerBillingRoutes(app, deps) {
|
|
@@ -4694,7 +4791,7 @@ function registerBillingRoutes(app, deps) {
|
|
|
4694
4791
|
const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
|
|
4695
4792
|
const workspaceId = c.req.query("workspaceId");
|
|
4696
4793
|
if (workspaceId && !context.workspaceGrants.some((grant) => grant.accountId === accountId && grant.workspaceId === workspaceId)) {
|
|
4697
|
-
throw new
|
|
4794
|
+
throw new HTTPException14(403, { message: "missing workspace access for usage query" });
|
|
4698
4795
|
}
|
|
4699
4796
|
return c.json({
|
|
4700
4797
|
balance: await getBillingBalance(deps.db, accountId),
|
|
@@ -4712,12 +4809,12 @@ function registerBillingRoutes(app, deps) {
|
|
|
4712
4809
|
});
|
|
4713
4810
|
app.post("/v1/billing/checkout", async (c) => {
|
|
4714
4811
|
if (deps.settings.billingMode !== "stripe") {
|
|
4715
|
-
throw new
|
|
4812
|
+
throw new HTTPException14(404, { message: "stripe billing is not enabled" });
|
|
4716
4813
|
}
|
|
4717
4814
|
const context = await requireAccessContext(c, deps);
|
|
4718
4815
|
const parsed = CreateCheckoutRequest.safeParse(await c.req.json());
|
|
4719
4816
|
if (!parsed.success) {
|
|
4720
|
-
throw new
|
|
4817
|
+
throw new HTTPException14(400, { message: parsed.error.issues[0]?.message ?? "invalid checkout request" });
|
|
4721
4818
|
}
|
|
4722
4819
|
const body = parsed.data;
|
|
4723
4820
|
const accountId = requireSelectedAccount(context, body.accountId, "billing:manage");
|
|
@@ -4738,7 +4835,7 @@ function registerBillingRoutes(app, deps) {
|
|
|
4738
4835
|
idempotencyKey
|
|
4739
4836
|
}), { idempotencyKey });
|
|
4740
4837
|
if (!session.url) {
|
|
4741
|
-
throw new
|
|
4838
|
+
throw new HTTPException14(502, { message: "Stripe did not return a checkout URL" });
|
|
4742
4839
|
}
|
|
4743
4840
|
return c.json(CreateCheckoutResponse.parse({
|
|
4744
4841
|
checkoutSessionId: session.id,
|
|
@@ -4747,18 +4844,18 @@ function registerBillingRoutes(app, deps) {
|
|
|
4747
4844
|
});
|
|
4748
4845
|
app.post("/v1/webhooks/stripe", async (c) => {
|
|
4749
4846
|
if (deps.settings.billingMode !== "stripe") {
|
|
4750
|
-
throw new
|
|
4847
|
+
throw new HTTPException14(404, { message: "stripe billing is not enabled" });
|
|
4751
4848
|
}
|
|
4752
4849
|
const signature = c.req.header("stripe-signature");
|
|
4753
4850
|
if (!signature) {
|
|
4754
|
-
throw new
|
|
4851
|
+
throw new HTTPException14(400, { message: "missing stripe-signature" });
|
|
4755
4852
|
}
|
|
4756
4853
|
const payload = await c.req.text();
|
|
4757
4854
|
let event;
|
|
4758
4855
|
try {
|
|
4759
4856
|
event = await stripeClient(deps).webhooks.constructEventAsync(payload, signature, deps.settings.stripeWebhookSecret);
|
|
4760
4857
|
} catch (error) {
|
|
4761
|
-
throw new
|
|
4858
|
+
throw new HTTPException14(400, { message: error instanceof Error ? error.message : "invalid stripe signature" });
|
|
4762
4859
|
}
|
|
4763
4860
|
const firstSeen = await recordStripeWebhookEvent(deps.db, {
|
|
4764
4861
|
id: event.id,
|
|
@@ -4776,7 +4873,7 @@ function registerBillingRoutes(app, deps) {
|
|
|
4776
4873
|
await markStripeWebhookProcessed(deps.db, event.id);
|
|
4777
4874
|
return c.json({ received: true });
|
|
4778
4875
|
} catch (error) {
|
|
4779
|
-
throw new
|
|
4876
|
+
throw new HTTPException14(500, { message: error instanceof Error ? error.message : String(error) });
|
|
4780
4877
|
}
|
|
4781
4878
|
});
|
|
4782
4879
|
}
|
|
@@ -4828,7 +4925,7 @@ function stripeCheckoutSessionCreateParams(input) {
|
|
|
4828
4925
|
}
|
|
4829
4926
|
function checkoutReturnUrl(publicBaseUrl, candidate, fallbackPath, field) {
|
|
4830
4927
|
if (!publicBaseUrl) {
|
|
4831
|
-
throw new
|
|
4928
|
+
throw new HTTPException14(500, { message: "OPENGENI_PUBLIC_BASE_URL is required for Stripe checkout" });
|
|
4832
4929
|
}
|
|
4833
4930
|
const base = new URL(publicBaseUrl);
|
|
4834
4931
|
const fallback = new URL(fallbackPath, base).toString();
|
|
@@ -4837,7 +4934,7 @@ function checkoutReturnUrl(publicBaseUrl, candidate, fallbackPath, field) {
|
|
|
4837
4934
|
}
|
|
4838
4935
|
const parsed = new URL(candidate);
|
|
4839
4936
|
if (parsed.origin !== base.origin) {
|
|
4840
|
-
throw new
|
|
4937
|
+
throw new HTTPException14(400, { message: `${field} must use the OpenGeni public origin` });
|
|
4841
4938
|
}
|
|
4842
4939
|
return parsed.toString();
|
|
4843
4940
|
}
|
|
@@ -5069,7 +5166,7 @@ async function getOrCreateStripeCustomer(deps, stripe, context, accountId) {
|
|
|
5069
5166
|
}
|
|
5070
5167
|
const account = await getManagedAccount(deps.db, accountId);
|
|
5071
5168
|
if (!account) {
|
|
5072
|
-
throw new
|
|
5169
|
+
throw new HTTPException14(404, { message: "account not found" });
|
|
5073
5170
|
}
|
|
5074
5171
|
const customer = await stripe.customers.create({
|
|
5075
5172
|
name: account.name,
|
|
@@ -5095,17 +5192,17 @@ function stripeCustomerProvider(input) {
|
|
|
5095
5192
|
function requireSelectedAccount(context, requested, permission) {
|
|
5096
5193
|
const accountId = requested ?? context.defaultAccountId ?? void 0;
|
|
5097
5194
|
if (!accountId) {
|
|
5098
|
-
throw new
|
|
5195
|
+
throw new HTTPException14(409, { message: "account selection is required" });
|
|
5099
5196
|
}
|
|
5100
5197
|
const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId);
|
|
5101
5198
|
if (!grant || !grant.permissions.includes(permission) && !grant.permissions.includes("account:admin")) {
|
|
5102
|
-
throw new
|
|
5199
|
+
throw new HTTPException14(403, { message: `missing permission: ${permission}` });
|
|
5103
5200
|
}
|
|
5104
5201
|
return accountId;
|
|
5105
5202
|
}
|
|
5106
5203
|
function stripeClient(deps) {
|
|
5107
5204
|
if (!deps.settings.stripeSecretKey) {
|
|
5108
|
-
throw new
|
|
5205
|
+
throw new HTTPException14(500, { message: "Stripe secret key is not configured" });
|
|
5109
5206
|
}
|
|
5110
5207
|
return new Stripe(deps.settings.stripeSecretKey);
|
|
5111
5208
|
}
|
|
@@ -5149,7 +5246,7 @@ import {
|
|
|
5149
5246
|
verifySignedState
|
|
5150
5247
|
} from "@opengeni/github";
|
|
5151
5248
|
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
5152
|
-
import { HTTPException as
|
|
5249
|
+
import { HTTPException as HTTPException15 } from "hono/http-exception";
|
|
5153
5250
|
import { requireAccessGrant as requireAccessGrant10 } from "@opengeni/core";
|
|
5154
5251
|
var githubStateCookie = "opengeni_github_state";
|
|
5155
5252
|
function registerGitHubRoutes(app, deps) {
|
|
@@ -5177,15 +5274,15 @@ function registerGitHubRoutes(app, deps) {
|
|
|
5177
5274
|
const workspaceId = c.req.param("workspaceId");
|
|
5178
5275
|
const state = c.req.query("state");
|
|
5179
5276
|
if (!state) {
|
|
5180
|
-
throw new
|
|
5277
|
+
throw new HTTPException15(400, { message: "missing GitHub installation state" });
|
|
5181
5278
|
}
|
|
5182
5279
|
const statePayload = readSignedState3(state, githubStateSecret);
|
|
5183
5280
|
if (!statePayload || statePayload.workspaceId !== workspaceId) {
|
|
5184
|
-
throw new
|
|
5281
|
+
throw new HTTPException15(400, { message: "invalid or expired GitHub installation state" });
|
|
5185
5282
|
}
|
|
5186
5283
|
const slug = settings.githubAppSlug?.trim();
|
|
5187
5284
|
if (!slug) {
|
|
5188
|
-
throw new
|
|
5285
|
+
throw new HTTPException15(409, { message: JSON.stringify({ message: "GitHub App is not configured", missing: githubAppMissingSettings2(settings) }) });
|
|
5189
5286
|
}
|
|
5190
5287
|
setGitHubStateCookie(c, deps, state);
|
|
5191
5288
|
return c.redirect(`https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(state)}`);
|
|
@@ -5197,9 +5294,9 @@ function registerGitHubRoutes(app, deps) {
|
|
|
5197
5294
|
return c.json({ repositories: await listWorkspaceGitHubRepositories(deps, workspaceId) });
|
|
5198
5295
|
} catch (error) {
|
|
5199
5296
|
if (error instanceof GitHubAppConfigurationError2) {
|
|
5200
|
-
throw new
|
|
5297
|
+
throw new HTTPException15(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
|
|
5201
5298
|
}
|
|
5202
|
-
throw new
|
|
5299
|
+
throw new HTTPException15(502, { message: error instanceof Error ? error.message : String(error) });
|
|
5203
5300
|
}
|
|
5204
5301
|
});
|
|
5205
5302
|
app.post("/v1/workspaces/:workspaceId/github/repositories/sync", async (c) => {
|
|
@@ -5209,9 +5306,9 @@ function registerGitHubRoutes(app, deps) {
|
|
|
5209
5306
|
return c.json({ repositories: await listWorkspaceGitHubRepositories(deps, workspaceId) });
|
|
5210
5307
|
} catch (error) {
|
|
5211
5308
|
if (error instanceof GitHubAppConfigurationError2) {
|
|
5212
|
-
throw new
|
|
5309
|
+
throw new HTTPException15(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
|
|
5213
5310
|
}
|
|
5214
|
-
throw new
|
|
5311
|
+
throw new HTTPException15(502, { message: error instanceof Error ? error.message : String(error) });
|
|
5215
5312
|
}
|
|
5216
5313
|
});
|
|
5217
5314
|
app.post("/v1/workspaces/:workspaceId/github/app-manifest", async (c) => {
|
|
@@ -5243,10 +5340,10 @@ function registerGitHubRoutes(app, deps) {
|
|
|
5243
5340
|
const code = c.req.query("code");
|
|
5244
5341
|
const state = c.req.query("state");
|
|
5245
5342
|
if (!code) {
|
|
5246
|
-
throw new
|
|
5343
|
+
throw new HTTPException15(400, { message: "missing GitHub manifest code" });
|
|
5247
5344
|
}
|
|
5248
5345
|
if (!state || !verifySignedState(state, githubStateSecret)) {
|
|
5249
|
-
throw new
|
|
5346
|
+
throw new HTTPException15(400, { message: "invalid or expired GitHub manifest state" });
|
|
5250
5347
|
}
|
|
5251
5348
|
try {
|
|
5252
5349
|
const conversion = await convertGitHubAppManifest(code);
|
|
@@ -5257,7 +5354,7 @@ function registerGitHubRoutes(app, deps) {
|
|
|
5257
5354
|
return c.html(githubSuccessHtml(envLines, installUrl));
|
|
5258
5355
|
} catch (error) {
|
|
5259
5356
|
const message = error instanceof GitHubAppApiError ? error.message : String(error);
|
|
5260
|
-
throw new
|
|
5357
|
+
throw new HTTPException15(502, { message });
|
|
5261
5358
|
}
|
|
5262
5359
|
});
|
|
5263
5360
|
const handleGitHubInstallCallback = async (c) => {
|
|
@@ -5266,28 +5363,28 @@ function registerGitHubRoutes(app, deps) {
|
|
|
5266
5363
|
const installationIdRaw = c.req.query("installation_id");
|
|
5267
5364
|
const setupAction = c.req.query("setup_action") ?? null;
|
|
5268
5365
|
if (!state) {
|
|
5269
|
-
throw new
|
|
5366
|
+
throw new HTTPException15(400, { message: "missing GitHub installation state" });
|
|
5270
5367
|
}
|
|
5271
5368
|
const statePayload = readSignedState3(state, githubStateSecret);
|
|
5272
5369
|
if (!statePayload || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string") {
|
|
5273
|
-
throw new
|
|
5370
|
+
throw new HTTPException15(400, { message: "invalid or expired GitHub installation state" });
|
|
5274
5371
|
}
|
|
5275
5372
|
requireGitHubStateCookie(c, state);
|
|
5276
5373
|
const grant = await requireAccessGrant10(c, deps, statePayload.workspaceId, "github:manage");
|
|
5277
5374
|
if (grant.accountId !== statePayload.accountId) {
|
|
5278
|
-
throw new
|
|
5375
|
+
throw new HTTPException15(403, { message: "GitHub installation state does not match this workspace" });
|
|
5279
5376
|
}
|
|
5280
5377
|
if (setupAction === "request" && !installationIdRaw) {
|
|
5281
5378
|
return c.html(githubSetupPendingHtml());
|
|
5282
5379
|
}
|
|
5283
5380
|
const installationId = parsePositiveInteger(installationIdRaw);
|
|
5284
5381
|
if (installationId === null) {
|
|
5285
|
-
throw new
|
|
5382
|
+
throw new HTTPException15(400, { message: "missing or invalid GitHub installation_id" });
|
|
5286
5383
|
}
|
|
5287
5384
|
if (!code) {
|
|
5288
5385
|
const clientId = settings.githubClientId?.trim();
|
|
5289
5386
|
if (!clientId) {
|
|
5290
|
-
throw new
|
|
5387
|
+
throw new HTTPException15(409, { message: JSON.stringify({ message: "GitHub App is not configured", missing: ["OPENGENI_GITHUB_CLIENT_ID"] }) });
|
|
5291
5388
|
}
|
|
5292
5389
|
const oauthState = createSignedState4(githubStateSecret, {
|
|
5293
5390
|
accountId: grant.accountId,
|
|
@@ -5314,15 +5411,15 @@ function registerGitHubRoutes(app, deps) {
|
|
|
5314
5411
|
const code = c.req.query("code");
|
|
5315
5412
|
const state = c.req.query("state");
|
|
5316
5413
|
if (!code) {
|
|
5317
|
-
throw new
|
|
5414
|
+
throw new HTTPException15(400, { message: "missing GitHub OAuth code" });
|
|
5318
5415
|
}
|
|
5319
5416
|
if (!state) {
|
|
5320
|
-
throw new
|
|
5417
|
+
throw new HTTPException15(400, { message: "missing GitHub OAuth state" });
|
|
5321
5418
|
}
|
|
5322
5419
|
const statePayload = readSignedState3(state, githubStateSecret);
|
|
5323
5420
|
const installationId = parsePositiveInteger(String(statePayload?.installationId ?? ""));
|
|
5324
5421
|
if (!statePayload || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string" || installationId === null) {
|
|
5325
|
-
throw new
|
|
5422
|
+
throw new HTTPException15(400, { message: "invalid or expired GitHub OAuth state" });
|
|
5326
5423
|
}
|
|
5327
5424
|
requireGitHubStateCookie(c, state);
|
|
5328
5425
|
return await completeGitHubInstallationBinding(deps, c, {
|
|
@@ -5335,19 +5432,19 @@ function registerGitHubRoutes(app, deps) {
|
|
|
5335
5432
|
async function completeGitHubInstallationBinding(deps, c, input) {
|
|
5336
5433
|
const { db, settings } = deps;
|
|
5337
5434
|
if (!input.statePayload.workspaceId || !input.statePayload.accountId) {
|
|
5338
|
-
throw new
|
|
5435
|
+
throw new HTTPException15(400, { message: "invalid or expired GitHub installation state" });
|
|
5339
5436
|
}
|
|
5340
5437
|
const grant = await requireAccessGrant10(c, deps, input.statePayload.workspaceId, "github:manage");
|
|
5341
5438
|
if (grant.accountId !== input.statePayload.accountId) {
|
|
5342
|
-
throw new
|
|
5439
|
+
throw new HTTPException15(403, { message: "GitHub installation state does not match this workspace" });
|
|
5343
5440
|
}
|
|
5344
5441
|
try {
|
|
5345
5442
|
const installation = await verifyGitHubInstallationAccessForUser(settings, { code: input.code, installationId: input.installationId });
|
|
5346
5443
|
if (!installation) {
|
|
5347
|
-
throw new
|
|
5444
|
+
throw new HTTPException15(404, { message: "GitHub App installation was not found for this app" });
|
|
5348
5445
|
}
|
|
5349
5446
|
if (installation.suspended) {
|
|
5350
|
-
throw new
|
|
5447
|
+
throw new HTTPException15(409, { message: "GitHub App installation is suspended" });
|
|
5351
5448
|
}
|
|
5352
5449
|
await upsertGitHubInstallation(db, {
|
|
5353
5450
|
accountId: grant.accountId,
|
|
@@ -5360,13 +5457,13 @@ async function completeGitHubInstallationBinding(deps, c, input) {
|
|
|
5360
5457
|
deleteCookie(c, githubStateCookie, { path: "/v1/github" });
|
|
5361
5458
|
return c.html(githubSetupSuccessHtml(installation.accountLogin ?? `installation ${input.installationId}`, returnUrl));
|
|
5362
5459
|
} catch (error) {
|
|
5363
|
-
if (error instanceof
|
|
5460
|
+
if (error instanceof HTTPException15) {
|
|
5364
5461
|
throw error;
|
|
5365
5462
|
}
|
|
5366
5463
|
if (error instanceof GitHubAppConfigurationError2) {
|
|
5367
|
-
throw new
|
|
5464
|
+
throw new HTTPException15(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
|
|
5368
5465
|
}
|
|
5369
|
-
throw new
|
|
5466
|
+
throw new HTTPException15(502, { message: error instanceof Error ? error.message : String(error) });
|
|
5370
5467
|
}
|
|
5371
5468
|
}
|
|
5372
5469
|
function setGitHubStateCookie(c, deps, state) {
|
|
@@ -5380,7 +5477,7 @@ function setGitHubStateCookie(c, deps, state) {
|
|
|
5380
5477
|
}
|
|
5381
5478
|
function requireGitHubStateCookie(c, state) {
|
|
5382
5479
|
if (getCookie(c, githubStateCookie) !== state) {
|
|
5383
|
-
throw new
|
|
5480
|
+
throw new HTTPException15(400, { message: "invalid or expired GitHub installation browser state" });
|
|
5384
5481
|
}
|
|
5385
5482
|
}
|
|
5386
5483
|
function isSecureRequest(c, deps) {
|
|
@@ -5447,7 +5544,7 @@ import {
|
|
|
5447
5544
|
updatePackInstallationStatus
|
|
5448
5545
|
} from "@opengeni/db";
|
|
5449
5546
|
import { getDocumentBase as getDocumentBase2 } from "@opengeni/documents";
|
|
5450
|
-
import { HTTPException as
|
|
5547
|
+
import { HTTPException as HTTPException16 } from "hono/http-exception";
|
|
5451
5548
|
import { requireAccessGrant as requireAccessGrant11 } from "@opengeni/core";
|
|
5452
5549
|
import { requireLimit as requireLimit5 } from "@opengeni/core";
|
|
5453
5550
|
import { validateEnvironmentAttachment } from "@opengeni/core";
|
|
@@ -5478,7 +5575,7 @@ function registerPackRoutes(app, deps) {
|
|
|
5478
5575
|
const grant = await requireAccessGrant11(c, deps, workspaceId, "workspace:admin");
|
|
5479
5576
|
const manifest = RegisterCapabilityPackRequest.parse(await c.req.json());
|
|
5480
5577
|
if (isBuiltInCapabilityPack(manifest.id)) {
|
|
5481
|
-
throw new
|
|
5578
|
+
throw new HTTPException16(409, { message: `pack id ${manifest.id} is a built-in pack and cannot be replaced` });
|
|
5482
5579
|
}
|
|
5483
5580
|
const { pack, created } = await registerWorkspacePack(db, {
|
|
5484
5581
|
accountId: grant.accountId,
|
|
@@ -5492,10 +5589,10 @@ function registerPackRoutes(app, deps) {
|
|
|
5492
5589
|
await requireAccessGrant11(c, deps, workspaceId, "workspace:admin");
|
|
5493
5590
|
const packId = c.req.param("packId");
|
|
5494
5591
|
if (isBuiltInCapabilityPack(packId)) {
|
|
5495
|
-
throw new
|
|
5592
|
+
throw new HTTPException16(409, { message: "built-in packs cannot be unregistered" });
|
|
5496
5593
|
}
|
|
5497
5594
|
if (!await getWorkspacePack(db, workspaceId, packId)) {
|
|
5498
|
-
throw new
|
|
5595
|
+
throw new HTTPException16(404, { message: "pack not found" });
|
|
5499
5596
|
}
|
|
5500
5597
|
const installation = await getPackInstallation(db, workspaceId, packId);
|
|
5501
5598
|
if (installation && installation.status === "active") {
|
|
@@ -5532,13 +5629,13 @@ function registerPackRoutes(app, deps) {
|
|
|
5532
5629
|
const storedEnvironmentId = typeof existing?.metadata.environmentId === "string" ? existing.metadata.environmentId : void 0;
|
|
5533
5630
|
const environmentId = payload.environmentId ?? storedEnvironmentId;
|
|
5534
5631
|
if (pack.environment?.required && !environmentId) {
|
|
5535
|
-
throw new
|
|
5632
|
+
throw new HTTPException16(422, { message: "this pack requires an environment attachment; pass environmentId" });
|
|
5536
5633
|
}
|
|
5537
5634
|
if (environmentId) {
|
|
5538
5635
|
const environment = await validateEnvironmentAttachment({ settings, db }, grant, workspaceId, environmentId, { preauthorized: !payload.environmentId });
|
|
5539
5636
|
const missing = (pack.environment?.requiredVariables ?? []).filter((name) => !environment.variables.some((variable) => variable.name === name));
|
|
5540
5637
|
if (missing.length > 0) {
|
|
5541
|
-
throw new
|
|
5638
|
+
throw new HTTPException16(422, { message: `environment is missing required variable(s): ${missing.join(", ")}` });
|
|
5542
5639
|
}
|
|
5543
5640
|
}
|
|
5544
5641
|
const installation = await enablePackInstallation(db, {
|
|
@@ -5559,13 +5656,13 @@ function registerPackRoutes(app, deps) {
|
|
|
5559
5656
|
const pack = await requirePack(db, workspaceId, MARKETING_SOCIAL_PACK_ID);
|
|
5560
5657
|
const installation = await getPackInstallation(db, workspaceId, pack.id);
|
|
5561
5658
|
if (installation?.status !== "active") {
|
|
5562
|
-
throw new
|
|
5659
|
+
throw new HTTPException16(409, { message: "enable the marketing social pack before creating its scheduled tasks" });
|
|
5563
5660
|
}
|
|
5564
5661
|
const payload = MarketingDailyAnalysisTaskRequest.parse(await c.req.json());
|
|
5565
5662
|
await requireLimit5(deps, { accountId: grant.accountId, workspaceId, action: "schedule:create", quantity: 1 });
|
|
5566
5663
|
const connections = await resolveSocialConnections(db, workspaceId, payload.connectionIds);
|
|
5567
5664
|
if (connections.length === 0) {
|
|
5568
|
-
throw new
|
|
5665
|
+
throw new HTTPException16(422, { message: "at least one connected social account is required" });
|
|
5569
5666
|
}
|
|
5570
5667
|
await validateDocumentBaseIds(db, workspaceId, payload.documentBaseIds);
|
|
5571
5668
|
const agentConfig = buildMarketingDailyAnalysisAgentConfig({
|
|
@@ -5609,7 +5706,7 @@ function registerPackRoutes(app, deps) {
|
|
|
5609
5706
|
async function requirePack(db, workspaceId, packId) {
|
|
5610
5707
|
const pack = await resolveCapabilityPack(db, workspaceId, packId);
|
|
5611
5708
|
if (!pack) {
|
|
5612
|
-
throw new
|
|
5709
|
+
throw new HTTPException16(404, { message: "pack not found" });
|
|
5613
5710
|
}
|
|
5614
5711
|
return pack;
|
|
5615
5712
|
}
|
|
@@ -5618,13 +5715,13 @@ async function resolveSocialConnections(db, workspaceId, connectionIds) {
|
|
|
5618
5715
|
const connections = ids.length > 0 ? await Promise.all(ids.map(async (id) => {
|
|
5619
5716
|
const connection = await getSocialConnection(db, workspaceId, id);
|
|
5620
5717
|
if (!connection) {
|
|
5621
|
-
throw new
|
|
5718
|
+
throw new HTTPException16(422, { message: `unknown social connection: ${id}` });
|
|
5622
5719
|
}
|
|
5623
5720
|
return connection;
|
|
5624
5721
|
})) : (await listSocialConnections2(db, workspaceId, 500)).filter((connection) => connection.status === "connected");
|
|
5625
5722
|
const inactive = connections.find((connection) => connection.status !== "connected");
|
|
5626
5723
|
if (inactive) {
|
|
5627
|
-
throw new
|
|
5724
|
+
throw new HTTPException16(422, { message: `social connection ${inactive.id} is ${inactive.status}` });
|
|
5628
5725
|
}
|
|
5629
5726
|
return connections;
|
|
5630
5727
|
}
|
|
@@ -5632,7 +5729,7 @@ async function validateDocumentBaseIds(db, workspaceId, documentBaseIds) {
|
|
|
5632
5729
|
for (const baseId of [...new Set(documentBaseIds)]) {
|
|
5633
5730
|
const base = await getDocumentBase2(db, workspaceId, baseId);
|
|
5634
5731
|
if (!base) {
|
|
5635
|
-
throw new
|
|
5732
|
+
throw new HTTPException16(422, { message: `unknown document base: ${baseId}` });
|
|
5636
5733
|
}
|
|
5637
5734
|
}
|
|
5638
5735
|
}
|
|
@@ -5816,7 +5913,7 @@ import {
|
|
|
5816
5913
|
releaseLeaseHolder
|
|
5817
5914
|
} from "@opengeni/db";
|
|
5818
5915
|
import { appendAndPublishEvents as appendAndPublishEvents3 } from "@opengeni/events";
|
|
5819
|
-
import { HTTPException as
|
|
5916
|
+
import { HTTPException as HTTPException17 } from "hono/http-exception";
|
|
5820
5917
|
import {
|
|
5821
5918
|
establishSandboxSessionFromEnvelope,
|
|
5822
5919
|
serializeEstablishedSandboxEnvelope,
|
|
@@ -5831,7 +5928,7 @@ async function withChannelA(services, ctx, fn) {
|
|
|
5831
5928
|
const { db, settings, bus } = services;
|
|
5832
5929
|
const { accountId, workspaceId, session, subjectId } = ctx;
|
|
5833
5930
|
if (session.sandboxBackend === "none") {
|
|
5834
|
-
throw new
|
|
5931
|
+
throw new HTTPException17(409, { message: "sandbox not available" });
|
|
5835
5932
|
}
|
|
5836
5933
|
const sandboxGroupId = session.sandboxGroupId;
|
|
5837
5934
|
const viewerId = crypto.randomUUID();
|
|
@@ -5859,7 +5956,7 @@ async function withChannelA(services, ctx, fn) {
|
|
|
5859
5956
|
});
|
|
5860
5957
|
if (acquired.role === "fenced") {
|
|
5861
5958
|
await release();
|
|
5862
|
-
throw new
|
|
5959
|
+
throw new HTTPException17(409, { message: `sandbox lease superseded (epoch ${acquired.lease.leaseEpoch}); retry` });
|
|
5863
5960
|
}
|
|
5864
5961
|
let established;
|
|
5865
5962
|
let leaseSnapshot = acquired.lease;
|
|
@@ -5882,7 +5979,7 @@ async function withChannelA(services, ctx, fn) {
|
|
|
5882
5979
|
});
|
|
5883
5980
|
} catch (error) {
|
|
5884
5981
|
await failWarmingToCold(db, { accountId, workspaceId, sandboxGroupId, expectedEpoch });
|
|
5885
|
-
throw new
|
|
5982
|
+
throw new HTTPException17(409, { message: `sandbox not available (${error instanceof Error ? error.message : "spawn failed"})` });
|
|
5886
5983
|
}
|
|
5887
5984
|
const resumeEnvelope = await serializeEstablishedSandboxEnvelope(established) ?? envelope ?? null;
|
|
5888
5985
|
const committed = await commitWarmingToWarm(db, {
|
|
@@ -5897,7 +5994,7 @@ async function withChannelA(services, ctx, fn) {
|
|
|
5897
5994
|
leaseTtlMs
|
|
5898
5995
|
});
|
|
5899
5996
|
if (!committed.committed || !committed.lease) {
|
|
5900
|
-
throw new
|
|
5997
|
+
throw new HTTPException17(409, { message: `sandbox lease superseded (epoch ${expectedEpoch}); retry` });
|
|
5901
5998
|
}
|
|
5902
5999
|
leaseSnapshot = committed.lease;
|
|
5903
6000
|
} else {
|
|
@@ -5936,11 +6033,11 @@ async function withChannelA(services, ctx, fn) {
|
|
|
5936
6033
|
}
|
|
5937
6034
|
}
|
|
5938
6035
|
function mapChannelAError(error) {
|
|
5939
|
-
if (error instanceof
|
|
5940
|
-
if (error instanceof ChannelAValidationError) return new
|
|
5941
|
-
if (error instanceof ChannelANotFoundError) return new
|
|
5942
|
-
if (error instanceof ChannelAConflictError) return new
|
|
5943
|
-
if (error instanceof ChannelAUnsupportedError) return new
|
|
6036
|
+
if (error instanceof HTTPException17) return error;
|
|
6037
|
+
if (error instanceof ChannelAValidationError) return new HTTPException17(400, { message: error.message });
|
|
6038
|
+
if (error instanceof ChannelANotFoundError) return new HTTPException17(404, { message: error.message });
|
|
6039
|
+
if (error instanceof ChannelAConflictError) return new HTTPException17(409, { message: error.message });
|
|
6040
|
+
if (error instanceof ChannelAUnsupportedError) return new HTTPException17(409, { message: error.message });
|
|
5944
6041
|
return error;
|
|
5945
6042
|
}
|
|
5946
6043
|
async function dropEstablishedHandle(established) {
|
|
@@ -5949,7 +6046,7 @@ async function dropEstablishedHandle(established) {
|
|
|
5949
6046
|
|
|
5950
6047
|
// src/routes/sessions.ts
|
|
5951
6048
|
import { negotiateCapabilities } from "@opengeni/runtime/sandbox";
|
|
5952
|
-
import { HTTPException as
|
|
6049
|
+
import { HTTPException as HTTPException19 } from "hono/http-exception";
|
|
5953
6050
|
import { requireAccessGrant as requireAccessGrant13 } from "@opengeni/core";
|
|
5954
6051
|
|
|
5955
6052
|
// src/sandbox/viewer.ts
|
|
@@ -5971,7 +6068,7 @@ import {
|
|
|
5971
6068
|
SandboxLeaseSupersededError as SandboxLeaseSupersededError2
|
|
5972
6069
|
} from "@opengeni/db";
|
|
5973
6070
|
import { appendAndPublishEvents as appendAndPublishEvents4 } from "@opengeni/events";
|
|
5974
|
-
import { HTTPException as
|
|
6071
|
+
import { HTTPException as HTTPException18 } from "hono/http-exception";
|
|
5975
6072
|
import {
|
|
5976
6073
|
DESKTOP_STREAM_PORT,
|
|
5977
6074
|
ensureDisplayStack,
|
|
@@ -6033,7 +6130,7 @@ async function attachViewer(services, input) {
|
|
|
6033
6130
|
});
|
|
6034
6131
|
if (acquired.role === "fenced") {
|
|
6035
6132
|
await release();
|
|
6036
|
-
throw new
|
|
6133
|
+
throw new HTTPException18(409, { message: `sandbox lease superseded (epoch ${acquired.lease.leaseEpoch}); re-read capabilities and re-attach` });
|
|
6037
6134
|
}
|
|
6038
6135
|
if (acquired.role === "spawner") {
|
|
6039
6136
|
const expectedEpoch = acquired.lease.leaseEpoch;
|
|
@@ -6074,12 +6171,12 @@ async function attachViewer(services, input) {
|
|
|
6074
6171
|
};
|
|
6075
6172
|
} catch (error) {
|
|
6076
6173
|
if (error instanceof SandboxLeaseSupersededError2) {
|
|
6077
|
-
throw new
|
|
6174
|
+
throw new HTTPException18(409, { message: `sandbox lease superseded (epoch ${error.leaseEpoch}); re-read capabilities and re-attach` });
|
|
6078
6175
|
}
|
|
6079
6176
|
await failWarmingToCold2(db, { accountId, workspaceId, sandboxGroupId, expectedEpoch });
|
|
6080
6177
|
await release();
|
|
6081
|
-
if (error instanceof
|
|
6082
|
-
throw new
|
|
6178
|
+
if (error instanceof HTTPException18) throw error;
|
|
6179
|
+
throw new HTTPException18(409, { message: `sandbox not available (${error instanceof Error ? error.message : "spawn failed"})` });
|
|
6083
6180
|
} finally {
|
|
6084
6181
|
await dropEstablishedHandle2(established);
|
|
6085
6182
|
}
|
|
@@ -6546,7 +6643,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
6546
6643
|
await requireAccessGrant13(c, deps, workspaceId, "sessions:read");
|
|
6547
6644
|
const session = await getSession4(db, workspaceId, c.req.param("sessionId"));
|
|
6548
6645
|
if (!session) {
|
|
6549
|
-
throw new
|
|
6646
|
+
throw new HTTPException19(404, { message: "session not found" });
|
|
6550
6647
|
}
|
|
6551
6648
|
return c.json(session);
|
|
6552
6649
|
});
|
|
@@ -6557,12 +6654,12 @@ function registerSessionRoutes(app, deps) {
|
|
|
6557
6654
|
const body = await c.req.json();
|
|
6558
6655
|
const target = typeof body.target === "string" ? body.target : "";
|
|
6559
6656
|
if (!target) {
|
|
6560
|
-
throw new
|
|
6657
|
+
throw new HTTPException19(400, { message: 'target is required ("auto" or an account id)' });
|
|
6561
6658
|
}
|
|
6562
6659
|
const pinned = target === "auto" ? null : target;
|
|
6563
6660
|
const ok = await setSessionCodexPin(db, workspaceId, sessionId, pinned);
|
|
6564
6661
|
if (!ok) {
|
|
6565
|
-
throw new
|
|
6662
|
+
throw new HTTPException19(404, { message: "session or codex account not found" });
|
|
6566
6663
|
}
|
|
6567
6664
|
return c.json({ pinned: target === "auto" ? "auto" : target });
|
|
6568
6665
|
});
|
|
@@ -6575,7 +6672,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
6575
6672
|
await updateSessionTitle2({ db, bus }, workspaceId, sessionId, payload.title, "user");
|
|
6576
6673
|
const session = await getSession4(db, workspaceId, sessionId);
|
|
6577
6674
|
if (!session) {
|
|
6578
|
-
throw new
|
|
6675
|
+
throw new HTTPException19(404, { message: "session not found" });
|
|
6579
6676
|
}
|
|
6580
6677
|
return c.json(session);
|
|
6581
6678
|
});
|
|
@@ -6586,7 +6683,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
6586
6683
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
6587
6684
|
const goal = await getSessionGoal2(db, workspaceId, sessionId);
|
|
6588
6685
|
if (!goal) {
|
|
6589
|
-
throw new
|
|
6686
|
+
throw new HTTPException19(404, { message: "session goal not found" });
|
|
6590
6687
|
}
|
|
6591
6688
|
return c.json(goal);
|
|
6592
6689
|
});
|
|
@@ -6598,10 +6695,10 @@ function registerSessionRoutes(app, deps) {
|
|
|
6598
6695
|
const payload = UpdateSessionGoalRequest.parse(await c.req.json());
|
|
6599
6696
|
const existing = await getSessionGoal2(db, workspaceId, sessionId);
|
|
6600
6697
|
if (!existing) {
|
|
6601
|
-
throw new
|
|
6698
|
+
throw new HTTPException19(404, { message: "session goal not found" });
|
|
6602
6699
|
}
|
|
6603
6700
|
if (existing.status === "completed") {
|
|
6604
|
-
throw new
|
|
6701
|
+
throw new HTTPException19(409, { message: "session goal is completed; set a new goal instead" });
|
|
6605
6702
|
}
|
|
6606
6703
|
if (payload.status === "paused") {
|
|
6607
6704
|
const { goal: goal2, changed: changed2 } = await setSessionGoalStatus2(db, workspaceId, sessionId, {
|
|
@@ -6625,7 +6722,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
6625
6722
|
return c.json(goal2);
|
|
6626
6723
|
}
|
|
6627
6724
|
if (existing.status !== "paused") {
|
|
6628
|
-
throw new
|
|
6725
|
+
throw new HTTPException19(409, { message: `session goal is ${existing.status}; only paused goals can be resumed` });
|
|
6629
6726
|
}
|
|
6630
6727
|
const { goal, changed } = await setSessionGoalStatus2(db, workspaceId, sessionId, { status: "active" });
|
|
6631
6728
|
if (changed) {
|
|
@@ -6650,11 +6747,11 @@ function registerSessionRoutes(app, deps) {
|
|
|
6650
6747
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
6651
6748
|
const clearBody = ClearSessionContextRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
6652
6749
|
if (!clearBody.success) {
|
|
6653
|
-
throw new
|
|
6750
|
+
throw new HTTPException19(400, { message: "context clear requires an explicit { confirm: true }" });
|
|
6654
6751
|
}
|
|
6655
6752
|
const session = await requireSession3(db, workspaceId, sessionId);
|
|
6656
6753
|
if (session.status === "queued" || session.status === "running" || session.status === "requires_action") {
|
|
6657
|
-
throw new
|
|
6754
|
+
throw new HTTPException19(409, { message: `session is ${session.status}; cannot clear context mid-turn \u2014 stop the turn first` });
|
|
6658
6755
|
}
|
|
6659
6756
|
const result = await clearSessionContext(db, { accountId: grant.accountId, workspaceId, sessionId });
|
|
6660
6757
|
await appendAndPublishEvents5(db, bus, workspaceId, sessionId, [{
|
|
@@ -6731,7 +6828,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
6731
6828
|
const resources = payload.resources !== void 0 ? normalizeResources(payload.resources) : existing.resources;
|
|
6732
6829
|
const tools = payload.tools !== void 0 ? validateToolRefs(payload.tools, runtimeSettings) : existing.tools;
|
|
6733
6830
|
if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
6734
|
-
throw new
|
|
6831
|
+
throw new HTTPException19(503, { message: "object storage is not configured" });
|
|
6735
6832
|
}
|
|
6736
6833
|
await validateFileResources(db, workspaceId, resources);
|
|
6737
6834
|
await validateGitHubRepositorySelection(db, workspaceId, [...session.resources, ...resources]);
|
|
@@ -6801,7 +6898,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
6801
6898
|
}
|
|
6802
6899
|
const session = await requireSession3(db, workspaceId, sessionId);
|
|
6803
6900
|
if (event.type === "user.approvalDecision" && session.status !== "requires_action") {
|
|
6804
|
-
throw new
|
|
6901
|
+
throw new HTTPException19(409, { message: `session is ${session.status}; no approval is pending` });
|
|
6805
6902
|
}
|
|
6806
6903
|
const eventsToAppend = [{
|
|
6807
6904
|
type: event.type,
|
|
@@ -6811,7 +6908,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
6811
6908
|
const appended = await appendAndPublishEvents5(db, bus, workspaceId, sessionId, eventsToAppend);
|
|
6812
6909
|
const accepted = appended[0];
|
|
6813
6910
|
if (!accepted) {
|
|
6814
|
-
throw new
|
|
6911
|
+
throw new HTTPException19(500, { message: "failed to append client event" });
|
|
6815
6912
|
}
|
|
6816
6913
|
const workflowId = workflowIdForSession2(sessionId);
|
|
6817
6914
|
if (event.type === "user.approvalDecision") {
|
|
@@ -6829,7 +6926,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
6829
6926
|
});
|
|
6830
6927
|
function assertOwnershipEnabled() {
|
|
6831
6928
|
if (!settings.sandboxOwnershipEnabled) {
|
|
6832
|
-
throw new
|
|
6929
|
+
throw new HTTPException19(404, { message: "sandbox ownership is not enabled for this deployment" });
|
|
6833
6930
|
}
|
|
6834
6931
|
}
|
|
6835
6932
|
async function resolveSharedExposure(workspaceId, session) {
|
|
@@ -6844,7 +6941,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
6844
6941
|
const sessionId = c.req.param("sessionId");
|
|
6845
6942
|
const session = await getSession4(db, workspaceId, sessionId);
|
|
6846
6943
|
if (!session) {
|
|
6847
|
-
throw new
|
|
6944
|
+
throw new HTTPException19(404, { message: "session not found" });
|
|
6848
6945
|
}
|
|
6849
6946
|
const lease = await readGroupLease({ db, settings }, { workspaceId, sandboxGroupId: session.sandboxGroupId });
|
|
6850
6947
|
const { shared, sharedSessionIds } = await resolveSharedExposure(workspaceId, session);
|
|
@@ -6939,11 +7036,11 @@ function registerSessionRoutes(app, deps) {
|
|
|
6939
7036
|
const sessionId = c.req.param("sessionId");
|
|
6940
7037
|
const session = await getSession4(db, workspaceId, sessionId);
|
|
6941
7038
|
if (!session) {
|
|
6942
|
-
throw new
|
|
7039
|
+
throw new HTTPException19(404, { message: "session not found" });
|
|
6943
7040
|
}
|
|
6944
7041
|
const parsed = AcknowledgeStreamRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
6945
7042
|
if (!parsed.success) {
|
|
6946
|
-
throw new
|
|
7043
|
+
throw new HTTPException19(400, { message: "invalid stream acknowledgment request" });
|
|
6947
7044
|
}
|
|
6948
7045
|
const recorded = await recordStreamAcknowledgment(db, {
|
|
6949
7046
|
accountId: grant.accountId,
|
|
@@ -6962,21 +7059,21 @@ function registerSessionRoutes(app, deps) {
|
|
|
6962
7059
|
const sessionId = c.req.param("sessionId");
|
|
6963
7060
|
const session = await getSession4(db, workspaceId, sessionId);
|
|
6964
7061
|
if (!session) {
|
|
6965
|
-
throw new
|
|
7062
|
+
throw new HTTPException19(404, { message: "session not found" });
|
|
6966
7063
|
}
|
|
6967
7064
|
const parsed = AttachViewerRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
6968
7065
|
if (!parsed.success) {
|
|
6969
|
-
throw new
|
|
7066
|
+
throw new HTTPException19(400, { message: "invalid viewer attach request" });
|
|
6970
7067
|
}
|
|
6971
7068
|
const wantDesktop = parsed.data.desktop ?? false;
|
|
6972
7069
|
const { shared } = await resolveSharedExposure(workspaceId, session);
|
|
6973
7070
|
if (wantDesktop) {
|
|
6974
7071
|
const ack = await getStreamAcknowledgment(db, { workspaceId, sandboxGroupId: session.sandboxGroupId, subjectId: grant.subjectId });
|
|
6975
7072
|
if (!ack?.acknowledgedUnredacted) {
|
|
6976
|
-
throw new
|
|
7073
|
+
throw new HTTPException19(409, { message: "stream_acknowledgment_required" });
|
|
6977
7074
|
}
|
|
6978
7075
|
if (shared && !ack.acknowledgedShared) {
|
|
6979
|
-
throw new
|
|
7076
|
+
throw new HTTPException19(409, { message: "shared_acknowledgment_required" });
|
|
6980
7077
|
}
|
|
6981
7078
|
}
|
|
6982
7079
|
const activeSandbox = session.activeSandboxId ? await getSandbox2(db, workspaceId, session.activeSandboxId) : null;
|
|
@@ -7075,11 +7172,11 @@ function registerSessionRoutes(app, deps) {
|
|
|
7075
7172
|
const sessionId = c.req.param("sessionId");
|
|
7076
7173
|
const session = await getSession4(db, workspaceId, sessionId);
|
|
7077
7174
|
if (!session) {
|
|
7078
|
-
throw new
|
|
7175
|
+
throw new HTTPException19(404, { message: "session not found" });
|
|
7079
7176
|
}
|
|
7080
7177
|
const parsed = ViewerHeartbeatRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
7081
7178
|
if (!parsed.success) {
|
|
7082
|
-
throw new
|
|
7179
|
+
throw new HTTPException19(400, { message: "viewer heartbeat requires { leaseEpoch }" });
|
|
7083
7180
|
}
|
|
7084
7181
|
const alive = await heartbeatViewer({ db, settings }, {
|
|
7085
7182
|
accountId: grant.accountId,
|
|
@@ -7097,7 +7194,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
7097
7194
|
const sessionId = c.req.param("sessionId");
|
|
7098
7195
|
const session = await getSession4(db, workspaceId, sessionId);
|
|
7099
7196
|
if (!session) {
|
|
7100
|
-
throw new
|
|
7197
|
+
throw new HTTPException19(404, { message: "session not found" });
|
|
7101
7198
|
}
|
|
7102
7199
|
await detachViewer({ db, settings }, {
|
|
7103
7200
|
accountId: grant.accountId,
|
|
@@ -7114,7 +7211,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
7114
7211
|
const sessionId = c.req.param("sessionId");
|
|
7115
7212
|
const session = await getSession4(db, workspaceId, sessionId);
|
|
7116
7213
|
if (!session) {
|
|
7117
|
-
throw new
|
|
7214
|
+
throw new HTTPException19(404, { message: "session not found" });
|
|
7118
7215
|
}
|
|
7119
7216
|
const result = await revokeViewer(db, {
|
|
7120
7217
|
accountId: grant.accountId,
|
|
@@ -7132,7 +7229,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
7132
7229
|
const sessionId = c.req.param("sessionId") ?? "";
|
|
7133
7230
|
const session = await getSession4(db, workspaceId, sessionId);
|
|
7134
7231
|
if (!session) {
|
|
7135
|
-
throw new
|
|
7232
|
+
throw new HTTPException19(404, { message: "session not found" });
|
|
7136
7233
|
}
|
|
7137
7234
|
return { accountId: grant.accountId, workspaceId, session, subjectId: grant.subjectId };
|
|
7138
7235
|
}
|
|
@@ -7140,7 +7237,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
7140
7237
|
const raw = await c.req.json().catch(() => void 0);
|
|
7141
7238
|
const result = schema.safeParse(raw ?? {});
|
|
7142
7239
|
if (!result.success) {
|
|
7143
|
-
throw new
|
|
7240
|
+
throw new HTTPException19(400, { message: "invalid request body" });
|
|
7144
7241
|
}
|
|
7145
7242
|
return result.data;
|
|
7146
7243
|
}
|
|
@@ -7245,10 +7342,10 @@ function registerSessionRoutes(app, deps) {
|
|
|
7245
7342
|
const req = await parseChannelABody(c, PtyWriteRequest);
|
|
7246
7343
|
const pty = await getOpenPtySession(db, ctx.workspaceId, req.ptyId);
|
|
7247
7344
|
if (!pty) {
|
|
7248
|
-
throw new
|
|
7345
|
+
throw new HTTPException19(404, { message: "pty not found or closed" });
|
|
7249
7346
|
}
|
|
7250
7347
|
if (pty.execSessionId === null) {
|
|
7251
|
-
throw new
|
|
7348
|
+
throw new HTTPException19(409, { message: "interactive terminal unsupported on this backend" });
|
|
7252
7349
|
}
|
|
7253
7350
|
let seq = 1;
|
|
7254
7351
|
await withChannelA({ db, settings, bus }, ctx, async ({ service }) => {
|
|
@@ -7266,7 +7363,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
7266
7363
|
const req = await parseChannelABody(c, PtyResizeRequest);
|
|
7267
7364
|
const pty = await getOpenPtySession(db, ctx.workspaceId, req.ptyId);
|
|
7268
7365
|
if (!pty) {
|
|
7269
|
-
throw new
|
|
7366
|
+
throw new HTTPException19(404, { message: "pty not found or closed" });
|
|
7270
7367
|
}
|
|
7271
7368
|
if (pty.execSessionId !== null) {
|
|
7272
7369
|
await withChannelA({ db, settings, bus }, ctx, ({ service }) => service.ptyResize(req, pty.execSessionId));
|
|
@@ -7336,7 +7433,7 @@ import {
|
|
|
7336
7433
|
listSocialConnections as listSocialConnections3,
|
|
7337
7434
|
listSocialPosts as listSocialPosts2
|
|
7338
7435
|
} from "@opengeni/db";
|
|
7339
|
-
import { HTTPException as
|
|
7436
|
+
import { HTTPException as HTTPException20 } from "hono/http-exception";
|
|
7340
7437
|
import { z as z2 } from "zod";
|
|
7341
7438
|
import { requireAccessGrant as requireAccessGrant14 } from "@opengeni/core";
|
|
7342
7439
|
function registerSocialRoutes(app, deps) {
|
|
@@ -7408,7 +7505,7 @@ function parseSince(raw) {
|
|
|
7408
7505
|
}
|
|
7409
7506
|
const since = new Date(raw);
|
|
7410
7507
|
if (Number.isNaN(since.getTime())) {
|
|
7411
|
-
throw new
|
|
7508
|
+
throw new HTTPException20(422, { message: "since must be an ISO date-time" });
|
|
7412
7509
|
}
|
|
7413
7510
|
return since;
|
|
7414
7511
|
}
|
|
@@ -7419,7 +7516,7 @@ function parseConnectionIds(raw) {
|
|
|
7419
7516
|
const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
7420
7517
|
const parsed = z2.array(z2.string().uuid()).safeParse(values);
|
|
7421
7518
|
if (!parsed.success) {
|
|
7422
|
-
throw new
|
|
7519
|
+
throw new HTTPException20(422, { message: "connectionIds must be a comma-separated list of UUIDs" });
|
|
7423
7520
|
}
|
|
7424
7521
|
const ids = parsed.data;
|
|
7425
7522
|
return [...new Set(ids)];
|
|
@@ -7427,12 +7524,12 @@ function parseConnectionIds(raw) {
|
|
|
7427
7524
|
function socialHttpException(error) {
|
|
7428
7525
|
const message = error instanceof Error ? error.message : String(error);
|
|
7429
7526
|
if (message.includes("not found")) {
|
|
7430
|
-
return new
|
|
7527
|
+
return new HTTPException20(404, { message });
|
|
7431
7528
|
}
|
|
7432
7529
|
if (message.includes("duplicate key")) {
|
|
7433
|
-
return new
|
|
7530
|
+
return new HTTPException20(409, { message: "social connection or post already exists" });
|
|
7434
7531
|
}
|
|
7435
|
-
return new
|
|
7532
|
+
return new HTTPException20(500, { message });
|
|
7436
7533
|
}
|
|
7437
7534
|
|
|
7438
7535
|
// src/routes/workspaces.ts
|
|
@@ -7460,7 +7557,7 @@ import {
|
|
|
7460
7557
|
requireWorkspace,
|
|
7461
7558
|
updateWorkspace
|
|
7462
7559
|
} from "@opengeni/db";
|
|
7463
|
-
import { HTTPException as
|
|
7560
|
+
import { HTTPException as HTTPException21 } from "hono/http-exception";
|
|
7464
7561
|
import { hasPermission as hasPermission3, requireAccessContext as requireAccessContext2, requireAccessGrant as requireAccessGrant15 } from "@opengeni/core";
|
|
7465
7562
|
import { requireLimit as requireLimit7 } from "@opengeni/core";
|
|
7466
7563
|
import { assertWorkspaceDeletable, assertWorkspaceMemberRemovable, resolveMemberSubjectId } from "@opengeni/core";
|
|
@@ -7482,7 +7579,7 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
7482
7579
|
const payload = CreateWorkspaceRequest.parse(await c.req.json());
|
|
7483
7580
|
const accountId = payload.accountId ?? context.defaultAccountId;
|
|
7484
7581
|
if (!accountId) {
|
|
7485
|
-
throw new
|
|
7582
|
+
throw new HTTPException21(409, { message: "account selection is required" });
|
|
7486
7583
|
}
|
|
7487
7584
|
requireAccountPermission(context, accountId, "workspace:create");
|
|
7488
7585
|
await requireLimit7(deps, { accountId, action: "workspace:create", quantity: 1 });
|
|
@@ -7558,7 +7655,7 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
7558
7655
|
const members = await listWorkspaceMembers(deps.db, workspaceId);
|
|
7559
7656
|
const member = members.find((candidate) => candidate.subjectId === subjectId);
|
|
7560
7657
|
if (!member) {
|
|
7561
|
-
throw new
|
|
7658
|
+
throw new HTTPException21(500, { message: "failed to add member" });
|
|
7562
7659
|
}
|
|
7563
7660
|
return c.json(WorkspaceMember.parse(member), 201);
|
|
7564
7661
|
});
|
|
@@ -7570,7 +7667,7 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
7570
7667
|
const existing = await listWorkspaceMembers(deps.db, workspaceId);
|
|
7571
7668
|
const current = existing.find((member2) => member2.subjectId === subjectId);
|
|
7572
7669
|
if (!current) {
|
|
7573
|
-
throw new
|
|
7670
|
+
throw new HTTPException21(404, { message: "member not found" });
|
|
7574
7671
|
}
|
|
7575
7672
|
await grantWorkspaceAccess(deps.db, {
|
|
7576
7673
|
accountId: grant.accountId,
|
|
@@ -7583,7 +7680,7 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
7583
7680
|
const members = await listWorkspaceMembers(deps.db, workspaceId);
|
|
7584
7681
|
const member = members.find((candidate) => candidate.subjectId === subjectId);
|
|
7585
7682
|
if (!member) {
|
|
7586
|
-
throw new
|
|
7683
|
+
throw new HTTPException21(500, { message: "failed to update member" });
|
|
7587
7684
|
}
|
|
7588
7685
|
return c.json(WorkspaceMember.parse(member));
|
|
7589
7686
|
});
|
|
@@ -7607,7 +7704,7 @@ function normalizeAgentInstructions(value) {
|
|
|
7607
7704
|
function requireAccountPermission(context, accountId, permission) {
|
|
7608
7705
|
const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId);
|
|
7609
7706
|
if (!grant || !grant.permissions.includes(permission) && !grant.permissions.includes("account:admin")) {
|
|
7610
|
-
throw new
|
|
7707
|
+
throw new HTTPException21(403, { message: `missing permission: ${permission}` });
|
|
7611
7708
|
}
|
|
7612
7709
|
}
|
|
7613
7710
|
|
|
@@ -7634,7 +7731,7 @@ function createApp(deps) {
|
|
|
7634
7731
|
const documentIndexer = deps.documentIndexer ?? {
|
|
7635
7732
|
indexDocument: async ({ accountId, workspaceId, documentId }) => {
|
|
7636
7733
|
if (!objectStorage) {
|
|
7637
|
-
throw new
|
|
7734
|
+
throw new HTTPException22(503, { message: "object storage is not configured" });
|
|
7638
7735
|
}
|
|
7639
7736
|
return await indexDocumentNow(deps.db, objectStorage, workspaceId, documentId, getDocumentServices(), {
|
|
7640
7737
|
beforeEmbed: async ({ chunkCount }) => {
|
|
@@ -7795,6 +7892,7 @@ function createApp(deps) {
|
|
|
7795
7892
|
registerSocialRoutes(app, routeDeps);
|
|
7796
7893
|
registerConnectionRoutes(app, routeDeps);
|
|
7797
7894
|
registerCapabilityRoutes(app, routeDeps);
|
|
7895
|
+
registerCatalogAssetRoutes(app, routeDeps);
|
|
7798
7896
|
registerEnrollmentRoutes(app, routeDeps);
|
|
7799
7897
|
registerMachineRoutes(app, routeDeps);
|
|
7800
7898
|
registerEnvironmentRoutes(app, routeDeps);
|
|
@@ -7835,7 +7933,7 @@ function allowedCorsOrigin(pattern, origin) {
|
|
|
7835
7933
|
return new RegExp(`^(?:${pattern})$`).test(origin);
|
|
7836
7934
|
}
|
|
7837
7935
|
function httpStatusForError(error) {
|
|
7838
|
-
if (error instanceof
|
|
7936
|
+
if (error instanceof HTTPException22) {
|
|
7839
7937
|
return error.status;
|
|
7840
7938
|
}
|
|
7841
7939
|
return 500;
|
|
@@ -7958,6 +8056,7 @@ var routeLabelPatterns = [
|
|
|
7958
8056
|
{ pattern: /^\/v1\/workspaces\/[^/]+\/connections$/, label: "/v1/workspaces/:workspaceId/connections" },
|
|
7959
8057
|
{ pattern: /^\/v1\/workspaces\/[^/]+\/connections\/oauth\/start$/, label: "/v1/workspaces/:workspaceId/connections/oauth/start" },
|
|
7960
8058
|
{ pattern: /^\/v1\/workspaces\/[^/]+\/connections\/[^/]+$/, label: "/v1/workspaces/:workspaceId/connections/:connectionId" },
|
|
8059
|
+
{ pattern: /^\/v1\/catalog-assets\/.+$/, label: "/v1/catalog-assets/*" },
|
|
7961
8060
|
{ pattern: /^\/v1\/integrations\/oauth\/callback$/, label: "/v1/integrations/oauth/callback" },
|
|
7962
8061
|
{ pattern: /^\/v1\/integrations\/oauth\/client-metadata\.json$/, label: "/v1/integrations/oauth/client-metadata.json" },
|
|
7963
8062
|
{ pattern: /^\/v1\/enrollments\/device\/start$/, label: "/v1/enrollments/device/start" },
|
|
@@ -7997,4 +8096,4 @@ export {
|
|
|
7997
8096
|
withDefaultEnabledCapabilityMcpTools,
|
|
7998
8097
|
workflowIdForSession3 as workflowIdForSession
|
|
7999
8098
|
};
|
|
8000
|
-
//# sourceMappingURL=chunk-
|
|
8099
|
+
//# sourceMappingURL=chunk-YY6OAEL6.js.map
|