@opengeni/api-router 2.5.0 → 2.6.4-canary.0
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.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/auth/managed-auth-attempt-context.d.ts +5 -1
- package/dist/auth/managed-auth.d.ts +24 -1
- package/dist/{chunk-QESX7HDK.js → chunk-UOI7KAP3.js} +3240 -791
- package/dist/chunk-UOI7KAP3.js.map +1 -0
- package/dist/http/sse.d.ts +9 -0
- package/dist/index.js +117 -5
- package/dist/index.js.map +1 -1
- package/dist/interaction-metrics.d.ts +2 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +1 -1
- package/dist/mcp/company-profile-agent-admin.d.ts +6 -6
- package/dist/mcp/remember.d.ts +2 -2
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/session-view.d.ts +1 -0
- package/dist/mcp/session-wait.d.ts +19 -0
- package/dist/model-catalog.d.ts +5 -31
- package/dist/routes/api-keys.d.ts +2 -0
- package/dist/routes/browser-sessions.d.ts +1 -0
- package/dist/routes/computer-sessions.d.ts +12 -0
- package/dist/routes/managed-auth-session-sets.d.ts +7 -0
- package/dist/routes/workspaces.d.ts +1 -1
- package/dist/sandbox/metrics-ingestion.d.ts +16 -0
- package/dist/workspace-delete-observability.d.ts +10 -0
- package/package.json +19 -19
- package/src/app.ts +206 -29
- package/src/auth/managed-auth-attempt-context.ts +40 -3
- package/src/auth/managed-auth-session-adapter.ts +1 -0
- package/src/auth/managed-auth.ts +164 -4
- package/src/http/sse.ts +279 -45
- package/src/index.ts +13 -1
- package/src/integrations/oauth-client.ts +8 -1
- package/src/integrations/provider-oauth.ts +12 -2
- package/src/integrations/slack-interactions.ts +77 -37
- package/src/interaction-metrics.ts +30 -0
- package/src/mcp/company-brain-governed-writes.ts +38 -23
- package/src/mcp/company-profile-agent-admin.ts +7 -7
- package/src/mcp/remember.ts +19 -8
- package/src/mcp/server.ts +328 -27
- package/src/mcp/session-wait.ts +56 -8
- package/src/model-catalog.ts +45 -337
- package/src/routes/api-integrations.ts +2 -2
- package/src/routes/api-keys.ts +149 -7
- package/src/routes/automations.ts +186 -19
- package/src/routes/browser-sessions.ts +10 -2
- package/src/routes/capabilities.ts +3 -3
- package/src/routes/codex.ts +483 -74
- package/src/routes/company-profile.ts +64 -0
- package/src/routes/computer-sessions.ts +103 -1
- package/src/routes/connections.ts +271 -16
- package/src/routes/github.ts +0 -8
- package/src/routes/integration-facets.ts +8 -5
- package/src/routes/interaction-resources.ts +7 -1
- package/src/routes/managed-auth-session-sets.ts +199 -2
- package/src/routes/organization-memberships.ts +28 -8
- package/src/routes/packs.ts +5 -5
- package/src/routes/plugins.ts +2 -2
- package/src/routes/pr-review.ts +80 -4
- package/src/routes/scheduled-tasks.ts +47 -11
- package/src/routes/sessions.ts +75 -53
- package/src/routes/skills.ts +3 -3
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspaces.ts +665 -56
- package/src/sandbox/channel-a.ts +10 -4
- package/src/sandbox/machines.ts +13 -6
- package/src/sandbox/metrics-ingestion.ts +157 -3
- package/src/sandbox/viewer.ts +20 -1
- package/src/workspace-delete-observability.ts +75 -0
- package/dist/chunk-QESX7HDK.js.map +0 -1
|
@@ -58,7 +58,11 @@ import {
|
|
|
58
58
|
import { publishDurableSessionEvents } from "@opengeni/events";
|
|
59
59
|
import type { Context, Hono } from "hono";
|
|
60
60
|
import { HTTPException } from "hono/http-exception";
|
|
61
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
browserSseDeliveryOptions,
|
|
63
|
+
sseWorkspaceInteractionRevisionStream,
|
|
64
|
+
sseWorkspaceLiveStream,
|
|
65
|
+
} from "../http/sse";
|
|
62
66
|
import { observeInterventionMutation } from "../interaction-metrics";
|
|
63
67
|
|
|
64
68
|
export function registerInteractionResourceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
@@ -74,6 +78,7 @@ export function registerInteractionResourceRoutes(app: Hono, deps: ApiRouteDeps)
|
|
|
74
78
|
nonnegativeSafeIntegerQuery(context, "interactionAfter", 0),
|
|
75
79
|
context.req.raw.signal,
|
|
76
80
|
{
|
|
81
|
+
...browserSseDeliveryOptions(context.req.query("transport")),
|
|
77
82
|
observability: deps.observability,
|
|
78
83
|
actorEpoch: getManagedAuthRequestActorEpoch(context.req.raw) ?? undefined,
|
|
79
84
|
reauthorize: async () => {
|
|
@@ -99,6 +104,7 @@ export function registerInteractionResourceRoutes(app: Hono, deps: ApiRouteDeps)
|
|
|
99
104
|
after,
|
|
100
105
|
context.req.raw.signal,
|
|
101
106
|
{
|
|
107
|
+
...browserSseDeliveryOptions(context.req.query("transport")),
|
|
102
108
|
observability: deps.observability,
|
|
103
109
|
actorEpoch: getManagedAuthRequestActorEpoch(context.req.raw) ?? undefined,
|
|
104
110
|
reauthorize: async () => {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
CompleteManagedAuthLoginTransactionResponse,
|
|
8
8
|
LogoutManagedAuthLoginSlotRequest,
|
|
9
9
|
LogoutManagedAuthSessionSetRequest,
|
|
10
|
+
MANAGED_AUTH_TRANSACTION_TTL_SECONDS,
|
|
10
11
|
MANAGED_AUTH_SESSION_SET_API_CONTRACT_HEADER,
|
|
11
12
|
MANAGED_AUTH_SESSION_SET_API_CONTRACT_REVISION,
|
|
12
13
|
ManagedAuthDeepLinkResolution,
|
|
@@ -15,6 +16,8 @@ import {
|
|
|
15
16
|
ManagedAuthSessionSetErrorCode,
|
|
16
17
|
ResolveManagedAuthDeepLinkRequest,
|
|
17
18
|
SelectManagedAuthLoginSlotRequest,
|
|
19
|
+
StartManagedAuthSocialTransactionRequest,
|
|
20
|
+
StartManagedAuthSocialTransactionResponse,
|
|
18
21
|
type ManagedAuthSessionSetProjection as ManagedAuthSessionSetProjectionType,
|
|
19
22
|
type ManagedAuthSessionSetErrorCode as ManagedAuthSessionSetErrorCodeType,
|
|
20
23
|
} from "@opengeni/contracts/managed-auth-session-sets";
|
|
@@ -57,11 +60,21 @@ import {
|
|
|
57
60
|
mutateManagedAuthSessionSet,
|
|
58
61
|
} from "@opengeni/db/managed-auth-session-sets";
|
|
59
62
|
import { ensureManagedAccessForUser, getSession } from "@opengeni/db";
|
|
63
|
+
import { sql } from "drizzle-orm";
|
|
60
64
|
import type { Context, Hono } from "hono";
|
|
61
65
|
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
62
66
|
import { HTTPException } from "hono/http-exception";
|
|
63
67
|
import { ApiHttpError } from "../http/api-error";
|
|
64
|
-
import
|
|
68
|
+
import { z } from "zod";
|
|
69
|
+
|
|
70
|
+
const ManagedAuthSocialStartReceipt = z
|
|
71
|
+
.object({
|
|
72
|
+
version: z.literal(1),
|
|
73
|
+
requestDigest: z.string().regex(/^[0-9a-f]{64}$/u),
|
|
74
|
+
url: z.string().url().max(4_096),
|
|
75
|
+
stateCookie: z.string().min(1).max(4_096),
|
|
76
|
+
})
|
|
77
|
+
.strict();
|
|
65
78
|
|
|
66
79
|
export function registerManagedAuthSessionSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
67
80
|
const noStore = async (context: Context, next: () => Promise<void>) => {
|
|
@@ -304,6 +317,129 @@ export function registerManagedAuthSessionSetRoutes(app: Hono, deps: ApiRouteDep
|
|
|
304
317
|
}
|
|
305
318
|
});
|
|
306
319
|
|
|
320
|
+
app.post("/v1/auth/session-set/transactions/social", async (context) => {
|
|
321
|
+
const body = await bodyAs(context, StartManagedAuthSocialTransactionRequest);
|
|
322
|
+
const { authority, actorEpoch } = await requireMutation(context, deps, body.expectedGeneration);
|
|
323
|
+
const available = requireAvailable(deps);
|
|
324
|
+
if (!managedAuthSocialProviderConfigured(deps, body.provider)) {
|
|
325
|
+
throw managedAuthApiError(404, "managed_authentication_unavailable");
|
|
326
|
+
}
|
|
327
|
+
const transactionSecret = requireTransactionSecret(context, body.transactionId);
|
|
328
|
+
const publicBaseUrl = deps.settings.publicBaseUrl;
|
|
329
|
+
if (!publicBaseUrl) {
|
|
330
|
+
throw managedAuthApiError(503, "managed_authentication_unavailable", {
|
|
331
|
+
retryable: true,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
const callbackURL = new URL("/account-auth", publicBaseUrl);
|
|
335
|
+
callbackURL.searchParams.set("transaction", body.transactionId);
|
|
336
|
+
callbackURL.searchParams.set("social", "complete");
|
|
337
|
+
const errorCallbackURL = new URL(callbackURL);
|
|
338
|
+
errorCallbackURL.searchParams.set("social", "error");
|
|
339
|
+
try {
|
|
340
|
+
const authorityHash = managedAuthSha256(authority);
|
|
341
|
+
const transactionSecretHash = managedAuthSha256(transactionSecret);
|
|
342
|
+
const requestDigest = digest(deps, {
|
|
343
|
+
operation: "social_start",
|
|
344
|
+
authorityHash,
|
|
345
|
+
actorEpoch,
|
|
346
|
+
transactionSecretHash,
|
|
347
|
+
request: body,
|
|
348
|
+
});
|
|
349
|
+
const receiptIdentifier = `opengeni-managed-social-start:${authorityHash}:${body.operationId}`;
|
|
350
|
+
const receipt = await deps.db.transaction(async (transaction) => {
|
|
351
|
+
await transaction.execute(
|
|
352
|
+
sql`select pg_advisory_xact_lock(hashtextextended(${receiptIdentifier}, 0))`,
|
|
353
|
+
);
|
|
354
|
+
const authContext = await available.managedAuth.$context;
|
|
355
|
+
const stored = await authContext.internalAdapter.findVerificationValue(receiptIdentifier);
|
|
356
|
+
if (stored && new Date(stored.expiresAt).getTime() > Date.now()) {
|
|
357
|
+
let parsed: ReturnType<typeof ManagedAuthSocialStartReceipt.safeParse>;
|
|
358
|
+
try {
|
|
359
|
+
parsed = ManagedAuthSocialStartReceipt.safeParse(JSON.parse(stored.value));
|
|
360
|
+
} catch (error) {
|
|
361
|
+
throw new ManagedAuthCompletionOutcomeUnknownError({
|
|
362
|
+
cause: error,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
if (!parsed.success) {
|
|
366
|
+
throw new ManagedAuthCompletionOutcomeUnknownError({
|
|
367
|
+
cause: parsed.error,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
if (parsed.data.requestDigest !== requestDigest) {
|
|
371
|
+
throw new ManagedAuthSessionSetOperationReuseError();
|
|
372
|
+
}
|
|
373
|
+
return parsed.data;
|
|
374
|
+
}
|
|
375
|
+
if (stored) {
|
|
376
|
+
await authContext.internalAdapter.deleteVerificationByIdentifier(receiptIdentifier);
|
|
377
|
+
}
|
|
378
|
+
const result = await available.managedAuth.api.signInSocial({
|
|
379
|
+
body: {
|
|
380
|
+
provider: body.provider,
|
|
381
|
+
disableRedirect: true,
|
|
382
|
+
callbackURL: callbackURL.toString(),
|
|
383
|
+
errorCallbackURL: errorCallbackURL.toString(),
|
|
384
|
+
additionalData: {
|
|
385
|
+
opengeniManagedAuth: {
|
|
386
|
+
version: 1,
|
|
387
|
+
operationId: body.operationId,
|
|
388
|
+
provider: body.provider,
|
|
389
|
+
transactionId: body.transactionId,
|
|
390
|
+
authorityHash,
|
|
391
|
+
transactionSecretHash,
|
|
392
|
+
expectedGeneration: body.expectedGeneration,
|
|
393
|
+
expectedActorEpoch: actorEpoch,
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
headers: isolatedManagedAuthHeaders(context.req.raw),
|
|
398
|
+
returnHeaders: true,
|
|
399
|
+
});
|
|
400
|
+
const url = validatedManagedAuthSocialAuthorizationUrl({
|
|
401
|
+
provider: body.provider,
|
|
402
|
+
rawUrl: result.response?.url,
|
|
403
|
+
expectedClientId:
|
|
404
|
+
body.provider === "google"
|
|
405
|
+
? deps.settings.managedAuthGoogleClientId!
|
|
406
|
+
: deps.settings.managedAuthGithubClientId!,
|
|
407
|
+
expectedCallbackUrl: new URL(
|
|
408
|
+
`/v1/auth/callback/${body.provider}`,
|
|
409
|
+
publicBaseUrl,
|
|
410
|
+
).toString(),
|
|
411
|
+
});
|
|
412
|
+
const stateCookieName = authContext.createAuthCookie("state").name;
|
|
413
|
+
const stateCookie = setCookieHeaders(result.headers).find((cookie) =>
|
|
414
|
+
cookie.startsWith(`${stateCookieName}=`),
|
|
415
|
+
);
|
|
416
|
+
const parsed = ManagedAuthSocialStartReceipt.parse({
|
|
417
|
+
version: 1,
|
|
418
|
+
requestDigest,
|
|
419
|
+
url,
|
|
420
|
+
stateCookie,
|
|
421
|
+
});
|
|
422
|
+
const created = await authContext.internalAdapter.createVerificationValue({
|
|
423
|
+
identifier: receiptIdentifier,
|
|
424
|
+
value: JSON.stringify(parsed),
|
|
425
|
+
expiresAt: new Date(Date.now() + MANAGED_AUTH_TRANSACTION_TTL_SECONDS * 1_000),
|
|
426
|
+
});
|
|
427
|
+
if (!created) {
|
|
428
|
+
throw new ManagedAuthCompletionOutcomeUnknownError();
|
|
429
|
+
}
|
|
430
|
+
return parsed;
|
|
431
|
+
});
|
|
432
|
+
context.header("set-cookie", receipt.stateCookie, { append: true });
|
|
433
|
+
return jsonWithActorEpoch(
|
|
434
|
+
context,
|
|
435
|
+
actorEpoch,
|
|
436
|
+
StartManagedAuthSocialTransactionResponse.parse({ url: receipt.url }),
|
|
437
|
+
);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
throwHttp(error);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
307
443
|
app.delete("/v1/auth/session-set/transactions/:transactionId", async (context) => {
|
|
308
444
|
const body = await bodyAs(context, CancelManagedAuthLoginTransactionRequest);
|
|
309
445
|
if (body.transactionId !== context.req.param("transactionId")) invalid();
|
|
@@ -449,6 +585,46 @@ export function registerManagedAuthSessionSetRoutes(app: Hono, deps: ApiRouteDep
|
|
|
449
585
|
});
|
|
450
586
|
}
|
|
451
587
|
|
|
588
|
+
export function validatedManagedAuthSocialAuthorizationUrl(input: {
|
|
589
|
+
provider: "google" | "github";
|
|
590
|
+
rawUrl: unknown;
|
|
591
|
+
expectedClientId: string;
|
|
592
|
+
expectedCallbackUrl: string;
|
|
593
|
+
}): string {
|
|
594
|
+
if (typeof input.rawUrl !== "string") {
|
|
595
|
+
throw new ManagedAuthCompletionOutcomeUnknownError();
|
|
596
|
+
}
|
|
597
|
+
let url: URL;
|
|
598
|
+
try {
|
|
599
|
+
url = new URL(input.rawUrl);
|
|
600
|
+
} catch (error) {
|
|
601
|
+
throw new ManagedAuthCompletionOutcomeUnknownError({ cause: error });
|
|
602
|
+
}
|
|
603
|
+
const expectedEndpoint =
|
|
604
|
+
input.provider === "google"
|
|
605
|
+
? { origin: "https://accounts.google.com", pathname: "/o/oauth2/v2/auth" }
|
|
606
|
+
: { origin: "https://github.com", pathname: "/login/oauth/authorize" };
|
|
607
|
+
const exactlyOne = (name: string, expected?: string): boolean => {
|
|
608
|
+
const values = url.searchParams.getAll(name);
|
|
609
|
+
return (
|
|
610
|
+
values.length === 1 && values[0] !== "" && (expected === undefined || values[0] === expected)
|
|
611
|
+
);
|
|
612
|
+
};
|
|
613
|
+
if (
|
|
614
|
+
url.origin !== expectedEndpoint.origin ||
|
|
615
|
+
url.pathname !== expectedEndpoint.pathname ||
|
|
616
|
+
url.username ||
|
|
617
|
+
url.password ||
|
|
618
|
+
url.hash ||
|
|
619
|
+
!exactlyOne("client_id", input.expectedClientId) ||
|
|
620
|
+
!exactlyOne("redirect_uri", input.expectedCallbackUrl) ||
|
|
621
|
+
!exactlyOne("state")
|
|
622
|
+
) {
|
|
623
|
+
throw new ManagedAuthCompletionOutcomeUnknownError();
|
|
624
|
+
}
|
|
625
|
+
return input.rawUrl;
|
|
626
|
+
}
|
|
627
|
+
|
|
452
628
|
/**
|
|
453
629
|
* Better Auth 1.6.26 provider lifecycle routes retained while session-set mode
|
|
454
630
|
* owns all selected-session reads, enumeration, revocation, and user mutation.
|
|
@@ -470,21 +646,42 @@ export function requireManagedAuthProviderRouteAllowed(method: string, pathname:
|
|
|
470
646
|
(pathname === "/v1/auth/verify-email" ||
|
|
471
647
|
pathname === "/v1/auth/error" ||
|
|
472
648
|
pathname === "/v1/auth/ok" ||
|
|
649
|
+
/^\/v1\/auth\/callback\/(?:google|github)$/.test(pathname) ||
|
|
473
650
|
/^\/v1\/auth\/reset-password\/[^/]+$/.test(pathname)));
|
|
474
651
|
if (!allowed) throw managedAuthApiError(409, "provider_route_blocked");
|
|
475
652
|
}
|
|
476
653
|
|
|
654
|
+
function managedAuthSocialProviderConfigured(
|
|
655
|
+
deps: ApiRouteDeps,
|
|
656
|
+
provider: "google" | "github",
|
|
657
|
+
): boolean {
|
|
658
|
+
return provider === "google"
|
|
659
|
+
? Boolean(
|
|
660
|
+
deps.settings.managedAuthGoogleClientId && deps.settings.managedAuthGoogleClientSecret,
|
|
661
|
+
)
|
|
662
|
+
: Boolean(
|
|
663
|
+
deps.settings.managedAuthGithubClientId && deps.settings.managedAuthGithubClientSecret,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
477
667
|
/** Remove provider bearer/session material and enforce browser-auth cache/cookie policy. */
|
|
478
668
|
export async function scrubManagedAuthProviderResponse(
|
|
479
669
|
response: Response,
|
|
480
|
-
options: {
|
|
670
|
+
options: {
|
|
671
|
+
replacementCookies?: readonly string[] | undefined;
|
|
672
|
+
preserveCookieNames?: readonly string[] | undefined;
|
|
673
|
+
} = {},
|
|
481
674
|
): Promise<Response> {
|
|
482
675
|
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
483
676
|
const headers = new Headers(response.headers);
|
|
484
677
|
headers.set("cache-control", "no-store");
|
|
485
678
|
headers.set("pragma", "no-cache");
|
|
486
679
|
if (options.replacementCookies !== undefined) {
|
|
680
|
+
const preserved = setCookieHeaders(response.headers).filter((cookie) =>
|
|
681
|
+
options.preserveCookieNames?.some((name) => cookie.startsWith(`${name}=`)),
|
|
682
|
+
);
|
|
487
683
|
headers.delete("set-cookie");
|
|
684
|
+
for (const cookie of preserved) headers.append("set-cookie", cookie);
|
|
488
685
|
for (const cookie of options.replacementCookies) headers.append("set-cookie", cookie);
|
|
489
686
|
}
|
|
490
687
|
let body: BodyInit | null = response.body;
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import {
|
|
34
34
|
getManagedSession,
|
|
35
35
|
organizationMembershipHttpStatus,
|
|
36
|
+
requireCanonicalLocalAccountAdministrator,
|
|
36
37
|
type ApiRouteDeps,
|
|
37
38
|
} from "@opengeni/core";
|
|
38
39
|
import {
|
|
@@ -100,6 +101,25 @@ async function requireManagedHuman(context: Context, deps: ApiRouteDeps) {
|
|
|
100
101
|
return { session, subjectId: `user:${session.user.id}` };
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
async function requireOrganizationAdministrator(
|
|
105
|
+
context: Context,
|
|
106
|
+
deps: ApiRouteDeps,
|
|
107
|
+
organizationId: string,
|
|
108
|
+
): Promise<{ subjectId: string }> {
|
|
109
|
+
if (deps.settings.productAccessMode === "managed") {
|
|
110
|
+
return await requireManagedHuman(context, deps);
|
|
111
|
+
}
|
|
112
|
+
if (deps.settings.productAccessMode === "local") {
|
|
113
|
+
const { subjectId } = await requireCanonicalLocalAccountAdministrator(
|
|
114
|
+
context,
|
|
115
|
+
deps,
|
|
116
|
+
organizationId,
|
|
117
|
+
);
|
|
118
|
+
return { subjectId };
|
|
119
|
+
}
|
|
120
|
+
throw new HTTPException(401, { message: "organization administrator session required" });
|
|
121
|
+
}
|
|
122
|
+
|
|
103
123
|
async function parseBody<S extends z.ZodType>(context: Context, schema: S): Promise<z.infer<S>> {
|
|
104
124
|
const parsed = schema.safeParse(await context.req.json().catch(() => null));
|
|
105
125
|
if (!parsed.success) {
|
|
@@ -213,12 +233,12 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
|
|
|
213
233
|
});
|
|
214
234
|
|
|
215
235
|
app.get("/v1/organizations/:organizationId/overview", async (context) => {
|
|
216
|
-
const { subjectId } = await requireManagedHuman(context, deps);
|
|
217
236
|
const organizationId = parseId(
|
|
218
237
|
OrganizationId,
|
|
219
238
|
context.req.param("organizationId"),
|
|
220
239
|
"organization id",
|
|
221
240
|
);
|
|
241
|
+
const { subjectId } = await requireOrganizationAdministrator(context, deps, organizationId);
|
|
222
242
|
try {
|
|
223
243
|
return context.json(
|
|
224
244
|
OrganizationAdministrationOverview.parse(
|
|
@@ -234,12 +254,12 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
|
|
|
234
254
|
});
|
|
235
255
|
|
|
236
256
|
app.patch("/v1/organizations/:organizationId", async (context) => {
|
|
237
|
-
const { subjectId } = await requireManagedHuman(context, deps);
|
|
238
257
|
const organizationId = parseId(
|
|
239
258
|
OrganizationId,
|
|
240
259
|
context.req.param("organizationId"),
|
|
241
260
|
"organization id",
|
|
242
261
|
);
|
|
262
|
+
const { subjectId } = await requireOrganizationAdministrator(context, deps, organizationId);
|
|
243
263
|
const payload = await parseBody(context, UpdateOrganizationNameRequest);
|
|
244
264
|
try {
|
|
245
265
|
return context.json(
|
|
@@ -257,12 +277,12 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
|
|
|
257
277
|
});
|
|
258
278
|
|
|
259
279
|
app.patch("/v1/organizations/:organizationId/workspaces/:workspaceId", async (context) => {
|
|
260
|
-
const { subjectId } = await requireManagedHuman(context, deps);
|
|
261
280
|
const organizationId = parseId(
|
|
262
281
|
OrganizationId,
|
|
263
282
|
context.req.param("organizationId"),
|
|
264
283
|
"organization id",
|
|
265
284
|
);
|
|
285
|
+
const { subjectId } = await requireOrganizationAdministrator(context, deps, organizationId);
|
|
266
286
|
const workspaceId = parseId(WorkspaceId, context.req.param("workspaceId"), "workspace id");
|
|
267
287
|
const payload = await parseBody(context, UpdateOrganizationWorkspaceRequest);
|
|
268
288
|
try {
|
|
@@ -284,12 +304,12 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
|
|
|
284
304
|
});
|
|
285
305
|
|
|
286
306
|
app.post("/v1/organizations/:organizationId/workspaces", async (context) => {
|
|
287
|
-
const { subjectId } = await requireManagedHuman(context, deps);
|
|
288
307
|
const organizationId = parseId(
|
|
289
308
|
OrganizationId,
|
|
290
309
|
context.req.param("organizationId"),
|
|
291
310
|
"organization id",
|
|
292
311
|
);
|
|
312
|
+
const { subjectId } = await requireOrganizationAdministrator(context, deps, organizationId);
|
|
293
313
|
const payload = await parseBody(context, CreateOrganizationWorkspaceRequest);
|
|
294
314
|
try {
|
|
295
315
|
return context.json(
|
|
@@ -311,12 +331,12 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
|
|
|
311
331
|
app.patch(
|
|
312
332
|
"/v1/organizations/:organizationId/workspaces/:workspaceId/settings",
|
|
313
333
|
async (context) => {
|
|
314
|
-
const { subjectId } = await requireManagedHuman(context, deps);
|
|
315
334
|
const organizationId = parseId(
|
|
316
335
|
OrganizationId,
|
|
317
336
|
context.req.param("organizationId"),
|
|
318
337
|
"organization id",
|
|
319
338
|
);
|
|
339
|
+
const { subjectId } = await requireOrganizationAdministrator(context, deps, organizationId);
|
|
320
340
|
const workspaceId = parseId(WorkspaceId, context.req.param("workspaceId"), "workspace id");
|
|
321
341
|
const payload = await parseBody(context, UpdateWorkspaceSettingsRequest);
|
|
322
342
|
try {
|
|
@@ -642,12 +662,12 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
|
|
|
642
662
|
);
|
|
643
663
|
|
|
644
664
|
app.get("/v1/organizations/:organizationId/members", async (context) => {
|
|
645
|
-
const { subjectId } = await requireManagedHuman(context, deps);
|
|
646
665
|
const organizationId = parseId(
|
|
647
666
|
OrganizationId,
|
|
648
667
|
context.req.param("organizationId"),
|
|
649
668
|
"organization id",
|
|
650
669
|
);
|
|
670
|
+
const { subjectId } = await requireOrganizationAdministrator(context, deps, organizationId);
|
|
651
671
|
try {
|
|
652
672
|
return context.json(
|
|
653
673
|
ListOrganizationAdministrationMembersResponse.parse({
|
|
@@ -689,12 +709,12 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
|
|
|
689
709
|
});
|
|
690
710
|
|
|
691
711
|
app.get("/v1/organizations/:organizationId/retention-policy", async (context) => {
|
|
692
|
-
const { subjectId } = await requireManagedHuman(context, deps);
|
|
693
712
|
const organizationId = parseId(
|
|
694
713
|
OrganizationId,
|
|
695
714
|
context.req.param("organizationId"),
|
|
696
715
|
"organization id",
|
|
697
716
|
);
|
|
717
|
+
const { subjectId } = await requireOrganizationAdministrator(context, deps, organizationId);
|
|
698
718
|
try {
|
|
699
719
|
return context.json(
|
|
700
720
|
OrganizationRetentionPolicy.parse(
|
|
@@ -710,12 +730,12 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
|
|
|
710
730
|
});
|
|
711
731
|
|
|
712
732
|
app.patch("/v1/organizations/:organizationId/retention-policy", async (context) => {
|
|
713
|
-
const { subjectId } = await requireManagedHuman(context, deps);
|
|
714
733
|
const organizationId = parseId(
|
|
715
734
|
OrganizationId,
|
|
716
735
|
context.req.param("organizationId"),
|
|
717
736
|
"organization id",
|
|
718
737
|
);
|
|
738
|
+
const { subjectId } = await requireOrganizationAdministrator(context, deps, organizationId);
|
|
719
739
|
const payload = await parseBody(context, UpdateOrganizationRetentionPolicyRequest);
|
|
720
740
|
try {
|
|
721
741
|
return context.json(
|
package/src/routes/packs.ts
CHANGED
|
@@ -83,7 +83,7 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
83
83
|
// Built-in pack ids stay reserved so a registration can never shadow them.
|
|
84
84
|
app.post("/v1/workspaces/:workspaceId/packs", async (c) => {
|
|
85
85
|
const workspaceId = c.req.param("workspaceId");
|
|
86
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
86
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
87
87
|
const manifest = RegisterCapabilityPackRequest.parse(await c.req.json());
|
|
88
88
|
if (isBuiltInCapabilityPack(manifest.id)) {
|
|
89
89
|
throw new HTTPException(409, {
|
|
@@ -100,7 +100,7 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
100
100
|
|
|
101
101
|
app.delete("/v1/workspaces/:workspaceId/packs/:packId", async (c) => {
|
|
102
102
|
const workspaceId = c.req.param("workspaceId");
|
|
103
|
-
await requireAccessGrant(c, deps, workspaceId, "
|
|
103
|
+
await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
104
104
|
const packId = c.req.param("packId");
|
|
105
105
|
if (isBuiltInCapabilityPack(packId)) {
|
|
106
106
|
throw new HTTPException(409, {
|
|
@@ -172,7 +172,7 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
172
172
|
|
|
173
173
|
app.post("/v1/workspaces/:workspaceId/packs/:packId/install", async (c) => {
|
|
174
174
|
const workspaceId = c.req.param("workspaceId");
|
|
175
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
175
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
176
176
|
const pack = await requirePack(db, workspaceId, c.req.param("packId"));
|
|
177
177
|
const payload = InstallPackRequest.parse(await c.req.json());
|
|
178
178
|
const preview = PackInstallationPreview.parse(
|
|
@@ -393,7 +393,7 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
393
393
|
|
|
394
394
|
app.delete("/v1/workspaces/:workspaceId/packs/:packId/installation", async (c) => {
|
|
395
395
|
const workspaceId = c.req.param("workspaceId");
|
|
396
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
396
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
397
397
|
const pack = await requirePack(db, workspaceId, c.req.param("packId"));
|
|
398
398
|
const payload = UninstallPackRequest.parse(await c.req.json());
|
|
399
399
|
const requestDigest = sha256(
|
|
@@ -483,7 +483,7 @@ export function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
483
483
|
|
|
484
484
|
app.post("/v1/workspaces/:workspaceId/packs/:packId/enable", async (c) => {
|
|
485
485
|
const workspaceId = c.req.param("workspaceId");
|
|
486
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
486
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
487
487
|
const pack = await requirePack(db, workspaceId, c.req.param("packId"));
|
|
488
488
|
if (capabilityPackRequiresInstallationPlan(pack)) {
|
|
489
489
|
throw new HTTPException(409, {
|
package/src/routes/plugins.ts
CHANGED
|
@@ -120,7 +120,7 @@ export function registerPluginRoutes(
|
|
|
120
120
|
|
|
121
121
|
app.post("/v1/workspaces/:workspaceId/plugins/install", async (c) => {
|
|
122
122
|
const workspaceId = c.req.param("workspaceId");
|
|
123
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
123
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
124
124
|
const payload = InstallPluginRequest.parse(await c.req.json());
|
|
125
125
|
const resolved = await resolvePluginPackage({
|
|
126
126
|
deps,
|
|
@@ -253,7 +253,7 @@ export function registerPluginRoutes(
|
|
|
253
253
|
|
|
254
254
|
app.delete("/v1/workspaces/:workspaceId/plugins/:pluginKey", async (c) => {
|
|
255
255
|
const workspaceId = c.req.param("workspaceId");
|
|
256
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
256
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
257
257
|
const pluginKey = decodeURIComponent(c.req.param("pluginKey"));
|
|
258
258
|
const payload = UninstallPluginRequest.parse(await c.req.json());
|
|
259
259
|
try {
|
package/src/routes/pr-review.ts
CHANGED
|
@@ -13,12 +13,14 @@ import {
|
|
|
13
13
|
canonicalConfiguredModel,
|
|
14
14
|
defaultPrReviewProviderBaseUrl,
|
|
15
15
|
getCapabilityPack,
|
|
16
|
+
workspaceCustomModelReference,
|
|
16
17
|
prReviewWebhookAuthKind,
|
|
17
18
|
normalizePrReviewProviderBaseUrl,
|
|
18
19
|
prReviewPackConnectorId,
|
|
19
20
|
PR_REVIEW_AUTOMATION_TEMPLATE_ID,
|
|
20
21
|
requireAccessGrant,
|
|
21
22
|
requirePermission,
|
|
23
|
+
resolveWorkspaceCatalogSettings,
|
|
22
24
|
type ApiRouteDeps,
|
|
23
25
|
} from "@opengeni/core";
|
|
24
26
|
import {
|
|
@@ -32,6 +34,8 @@ import {
|
|
|
32
34
|
getPackInstallation,
|
|
33
35
|
listPrReviewAppRegistrations,
|
|
34
36
|
listPrReviewRepositoryBindings,
|
|
37
|
+
lockActiveWorkspaceGatewayCustomModelForAdmission,
|
|
38
|
+
lockActiveWorkspaceOpenRouterCustomModelForAdmission,
|
|
35
39
|
nestedPostgresSqlState,
|
|
36
40
|
recordAuditEvent,
|
|
37
41
|
updatePrReviewAppRegistration,
|
|
@@ -385,10 +389,24 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
385
389
|
throw error;
|
|
386
390
|
}
|
|
387
391
|
}
|
|
392
|
+
const catalogSettings = (
|
|
393
|
+
await resolveWorkspaceCatalogSettings(db, deps.settings, {
|
|
394
|
+
accountId: grant.accountId,
|
|
395
|
+
workspaceId,
|
|
396
|
+
})
|
|
397
|
+
).settings;
|
|
388
398
|
const model = payload.model
|
|
389
|
-
? (canonicalConfiguredModel(
|
|
399
|
+
? (canonicalConfiguredModel(catalogSettings, payload.model) ?? null)
|
|
390
400
|
: null;
|
|
391
|
-
if (model) await assertWorkspaceModelPolicyAllows(db,
|
|
401
|
+
if (model) await assertWorkspaceModelPolicyAllows(db, catalogSettings, workspaceId, model);
|
|
402
|
+
const beforeCreateCommit = model
|
|
403
|
+
? prReviewCustomModelCommitGuard({
|
|
404
|
+
settings: catalogSettings,
|
|
405
|
+
accountId: grant.accountId,
|
|
406
|
+
workspaceId,
|
|
407
|
+
modelId: model,
|
|
408
|
+
})
|
|
409
|
+
: undefined;
|
|
392
410
|
const template = getCapabilityPack(OPENGENI_PR_REVIEW_PACK_ID)?.automationTemplates?.find(
|
|
393
411
|
(candidate) => candidate.id === PR_REVIEW_AUTOMATION_TEMPLATE_ID,
|
|
394
412
|
);
|
|
@@ -417,6 +435,7 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
417
435
|
eventTypes: template.eventTypes,
|
|
418
436
|
configuration: template.configuration,
|
|
419
437
|
sessionTemplate: template.sessionTemplate,
|
|
438
|
+
...(beforeCreateCommit ? { beforeCreateCommit } : {}),
|
|
420
439
|
}),
|
|
421
440
|
"This repository is already bound to the selected PR Review registration",
|
|
422
441
|
);
|
|
@@ -441,13 +460,40 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
441
460
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
442
461
|
await requirePrReviewPackActive(db, workspaceId);
|
|
443
462
|
const payload = UpdatePrReviewRepositoryBindingRequest.parse(await c.req.json());
|
|
463
|
+
const catalogSettings = (
|
|
464
|
+
await resolveWorkspaceCatalogSettings(db, deps.settings, {
|
|
465
|
+
accountId: grant.accountId,
|
|
466
|
+
workspaceId,
|
|
467
|
+
})
|
|
468
|
+
).settings;
|
|
444
469
|
const model =
|
|
445
470
|
payload.model === undefined
|
|
446
471
|
? undefined
|
|
447
472
|
: payload.model === null
|
|
448
473
|
? null
|
|
449
|
-
: (canonicalConfiguredModel(
|
|
450
|
-
if (model) await assertWorkspaceModelPolicyAllows(db,
|
|
474
|
+
: (canonicalConfiguredModel(catalogSettings, payload.model) ?? null);
|
|
475
|
+
if (model) await assertWorkspaceModelPolicyAllows(db, catalogSettings, workspaceId, model);
|
|
476
|
+
const beforeUpdateCommit = async (
|
|
477
|
+
tx: ApiRouteDeps["db"],
|
|
478
|
+
context: {
|
|
479
|
+
currentModel: string | null;
|
|
480
|
+
currentStatus: "active" | "disabled";
|
|
481
|
+
nextModel: string | null;
|
|
482
|
+
nextStatus: "active" | "disabled";
|
|
483
|
+
},
|
|
484
|
+
): Promise<void> => {
|
|
485
|
+
const materialExecutionChange =
|
|
486
|
+
payload.model !== undefined ||
|
|
487
|
+
payload.additionalInstructions !== undefined ||
|
|
488
|
+
(context.currentStatus === "disabled" && context.nextStatus === "active");
|
|
489
|
+
if (!materialExecutionChange || !context.nextModel) return;
|
|
490
|
+
await prReviewCustomModelCommitGuard({
|
|
491
|
+
settings: catalogSettings,
|
|
492
|
+
accountId: grant.accountId,
|
|
493
|
+
workspaceId,
|
|
494
|
+
modelId: context.nextModel,
|
|
495
|
+
})?.(tx);
|
|
496
|
+
};
|
|
451
497
|
const binding = await updatePrReviewRepositoryBinding(db, {
|
|
452
498
|
accountId: grant.accountId,
|
|
453
499
|
workspaceId,
|
|
@@ -458,6 +504,7 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
458
504
|
? { additionalInstructions: payload.additionalInstructions }
|
|
459
505
|
: {}),
|
|
460
506
|
...(payload.status !== undefined ? { status: payload.status } : {}),
|
|
507
|
+
beforeUpdateCommit,
|
|
461
508
|
});
|
|
462
509
|
if (!binding)
|
|
463
510
|
throw new HTTPException(404, {
|
|
@@ -501,6 +548,35 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
501
548
|
});
|
|
502
549
|
}
|
|
503
550
|
|
|
551
|
+
function prReviewCustomModelCommitGuard(input: {
|
|
552
|
+
settings: ApiRouteDeps["settings"];
|
|
553
|
+
accountId: string;
|
|
554
|
+
workspaceId: string;
|
|
555
|
+
modelId: string;
|
|
556
|
+
}): ((tx: ApiRouteDeps["db"]) => Promise<void>) | undefined {
|
|
557
|
+
const reference = workspaceCustomModelReference(input.settings, input.modelId);
|
|
558
|
+
if (!reference) return undefined;
|
|
559
|
+
return async (tx): Promise<void> => {
|
|
560
|
+
const active =
|
|
561
|
+
reference.providerKind === "openrouter"
|
|
562
|
+
? await lockActiveWorkspaceOpenRouterCustomModelForAdmission(tx, {
|
|
563
|
+
accountId: input.accountId,
|
|
564
|
+
workspaceId: input.workspaceId,
|
|
565
|
+
upstreamModelId: reference.upstreamModelId,
|
|
566
|
+
})
|
|
567
|
+
: await lockActiveWorkspaceGatewayCustomModelForAdmission(tx, {
|
|
568
|
+
accountId: input.accountId,
|
|
569
|
+
workspaceId: input.workspaceId,
|
|
570
|
+
upstreamModelId: reference.upstreamModelId,
|
|
571
|
+
});
|
|
572
|
+
if (!active) {
|
|
573
|
+
throw new HTTPException(422, {
|
|
574
|
+
message: `model is not available: ${input.modelId}`,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
504
580
|
function assertPrReviewSandboxBackend(deps: ApiRouteDeps): void {
|
|
505
581
|
if (deps.settings.sandboxBackend === "selfhosted") {
|
|
506
582
|
throw new HTTPException(409, {
|