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