@opengeni/api-router 3.0.1 → 4.0.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/admission-trace.d.ts +8 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +2 -0
- package/dist/auth/managed-auth.d.ts +1 -0
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/{chunk-AJ62T5QN.js → chunk-E7TZFJ65.js} +22300 -21113
- package/dist/{chunk-AJ62T5QN.js.map → chunk-E7TZFJ65.js.map} +1 -1
- package/dist/connection-ownership.d.ts +1 -2
- package/dist/fatal-process-boundary.d.ts +1 -1
- package/dist/index.js +24 -6
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +8 -3
- package/dist/integrations/fiken.d.ts +3 -1
- package/dist/integrations/github-lens-connect.d.ts +5 -65
- package/dist/integrations/google-drive.d.ts +2 -0
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/oauth-client.d.ts +119 -11
- package/dist/integrations/oauth-profiles.d.ts +8 -13
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/capability-account-readiness.d.ts +7 -0
- package/dist/mcp/first-party-tool-permissions.d.ts +4 -0
- package/dist/mcp-oauth.d.ts +22 -1
- package/dist/routes/api-integrations.d.ts +1 -0
- package/dist/routes/codex.d.ts +28 -0
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/dist/routes/organization-integration-policy.d.ts +3 -0
- package/dist/routes/sessions.d.ts +24 -2
- package/dist/scheduled-task-deletion.d.ts +3 -4
- package/dist/workspace-tool-gateway.d.ts +3 -1
- package/package.json +15 -15
- package/src/admission-trace.ts +14 -0
- package/src/app.ts +141 -94
- package/src/auth/managed-auth-attempt-context.ts +9 -0
- package/src/auth/managed-auth-session-adapter.ts +11 -0
- package/src/auth/managed-auth.ts +39 -2
- package/src/auth/managed-sign-in-notifications.ts +81 -0
- package/src/connection-ownership.ts +2 -9
- package/src/editable-artifact-production.ts +6 -0
- package/src/fatal-process-boundary.ts +17 -2
- package/src/index.ts +3 -4
- package/src/integrations/atlassian.ts +184 -76
- package/src/integrations/fiken.ts +57 -35
- package/src/integrations/github-app-connect.ts +51 -15
- package/src/integrations/github-lens-connect.ts +7 -28
- package/src/integrations/google-drive.ts +296 -125
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/oauth-client.ts +231 -132
- package/src/integrations/oauth-profiles.ts +10 -45
- package/src/integrations/personal-github.ts +55 -31
- package/src/integrations/provider-oauth.ts +57 -82
- package/src/integrations/social-oauth.ts +83 -56
- package/src/mcp/capability-account-readiness.ts +17 -0
- package/src/mcp/editable-artifacts.ts +7 -2
- package/src/mcp/first-party-tool-permissions.ts +1 -0
- package/src/mcp/knowledge-entries.ts +25 -8
- package/src/mcp/server.ts +135 -43
- package/src/mcp-oauth.ts +336 -34
- package/src/routes/api-integrations.ts +116 -4
- package/src/routes/automations.ts +5 -13
- package/src/routes/billing.ts +52 -0
- package/src/routes/canonical-human-identities.ts +61 -0
- package/src/routes/capabilities.ts +51 -1
- package/src/routes/codex.ts +4 -2
- package/src/routes/connect.ts +90 -31
- package/src/routes/connections.ts +487 -307
- package/src/routes/files.ts +21 -12
- package/src/routes/github.ts +48 -19
- package/src/routes/insights.ts +17 -7
- package/src/routes/integration-facets.ts +66 -25
- package/src/routes/knowledge.ts +48 -1
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/organization-integration-policy.ts +113 -0
- package/src/routes/organization-memberships.ts +22 -2
- package/src/routes/plugins.ts +42 -4
- package/src/routes/pr-review-github.ts +67 -57
- package/src/routes/pr-review.ts +74 -71
- package/src/routes/rigs.ts +32 -13
- package/src/routes/scheduled-tasks.ts +6 -6
- package/src/routes/sessions.ts +233 -11
- package/src/routes/skill-content.ts +19 -3
- package/src/routes/social.ts +24 -13
- package/src/sandbox/channel-a.ts +7 -1
- package/src/sandbox/rematerialize.ts +2 -1
- package/src/scheduled-task-deletion.ts +13 -3
- package/src/workspace-tool-gateway.ts +31 -31
- package/dist/connection-authority-owner.d.ts +0 -59
- package/dist/routes/connection-authorities.d.ts +0 -3
- package/dist/routes/host-mcp-bindings.d.ts +0 -5
- package/dist/routes/host-mcp-resolvers.d.ts +0 -3
- package/dist/routes/packs.d.ts +0 -3
- package/src/connection-authority-owner.ts +0 -61
- package/src/routes/connection-authorities.ts +0 -139
- package/src/routes/host-mcp-bindings.ts +0 -122
- package/src/routes/host-mcp-resolvers.ts +0 -41
- package/src/routes/packs.ts +0 -784
package/src/sandbox/channel-a.ts
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
recordTenancyCompatibilityLaneUse,
|
|
52
52
|
sandboxLeaseTelemetryKey,
|
|
53
53
|
sandboxOperationMetricObserver,
|
|
54
|
+
sandboxCaptureWaitMetricObserver,
|
|
54
55
|
type Observability,
|
|
55
56
|
} from "@opengeni/observability";
|
|
56
57
|
import { HTTPException } from "hono/http-exception";
|
|
@@ -531,6 +532,9 @@ async function withChannelAOperation<T>(
|
|
|
531
532
|
const onSandboxOperation = services.observability
|
|
532
533
|
? sandboxOperationMetricObserver(services.observability)
|
|
533
534
|
: undefined;
|
|
535
|
+
const onSandboxCaptureWait = services.observability
|
|
536
|
+
? sandboxCaptureWaitMetricObserver(services.observability)
|
|
537
|
+
: undefined;
|
|
534
538
|
const { accountId, workspaceId, session } = ctx;
|
|
535
539
|
|
|
536
540
|
if (session.sandboxBackend === "none") {
|
|
@@ -753,6 +757,7 @@ async function withChannelAOperation<T>(
|
|
|
753
757
|
settings,
|
|
754
758
|
bus,
|
|
755
759
|
...(onSandboxOperation ? { onSandboxOperation } : {}),
|
|
760
|
+
...(onSandboxCaptureWait ? { onSandboxCaptureWait } : {}),
|
|
756
761
|
...(ctx.waitSignal ? { waitSignal: ctx.waitSignal } : {}),
|
|
757
762
|
},
|
|
758
763
|
{
|
|
@@ -787,7 +792,7 @@ async function withChannelAOperation<T>(
|
|
|
787
792
|
|
|
788
793
|
// One session has one logical runtime across turns and every API-direct
|
|
789
794
|
// surface. Without this, Terminal/Files/Browser/Computer/viewers could rearm
|
|
790
|
-
|
|
795
|
+
|
|
791
796
|
// deployment image for the same durable sandbox group.
|
|
792
797
|
const sandboxRuntime = await resolveSessionSandboxRuntime(db, settings, session);
|
|
793
798
|
|
|
@@ -1060,6 +1065,7 @@ async function withChannelAOperation<T>(
|
|
|
1060
1065
|
settings,
|
|
1061
1066
|
bus,
|
|
1062
1067
|
...(onSandboxOperation ? { onSandboxOperation } : {}),
|
|
1068
|
+
...(onSandboxCaptureWait ? { onSandboxCaptureWait } : {}),
|
|
1063
1069
|
...(ctx.waitSignal ? { waitSignal: ctx.waitSignal } : {}),
|
|
1064
1070
|
},
|
|
1065
1071
|
{
|
|
@@ -162,7 +162,8 @@ export async function establishApiSandboxSpawner(input: {
|
|
|
162
162
|
try {
|
|
163
163
|
if (
|
|
164
164
|
(input.acquiredLease.recovery.archive.status === "available" &&
|
|
165
|
-
input.acquiredLease.archiveComplete
|
|
165
|
+
(input.acquiredLease.archiveComplete ||
|
|
166
|
+
input.acquiredLease.historicalRecoveryAuthorized === true)) ||
|
|
166
167
|
(input.acquiredLease.recovery.archive.status === "none" && hasWorkspaceArchive(archiveSource))
|
|
167
168
|
) {
|
|
168
169
|
const id = crypto.randomUUID();
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { scheduledTaskKnowledgeSource } from "@opengeni/contracts";
|
|
2
|
-
import type { ScheduledTask as ScheduledTaskValue } from "@opengeni/contracts";
|
|
3
|
-
import {
|
|
2
|
+
import type { AccessGrant, ScheduledTask as ScheduledTaskValue } from "@opengeni/contracts";
|
|
3
|
+
import {
|
|
4
|
+
assertScheduledTaskMutationOwner,
|
|
5
|
+
deleteScheduledTaskLifecycle,
|
|
6
|
+
type ApiRouteDeps,
|
|
7
|
+
} from "@opengeni/core";
|
|
4
8
|
import type { TemporalScheduleCleanupClaim } from "@opengeni/db";
|
|
5
9
|
import { HTTPException } from "hono/http-exception";
|
|
6
10
|
import * as z4 from "zod/v4";
|
|
@@ -100,13 +104,19 @@ export async function cleanupScheduledTaskConnectorAuthorization(
|
|
|
100
104
|
/** Shared HTTP/MCP domain operation: authorize, tombstone, persist, then accelerate cleanup. */
|
|
101
105
|
export async function deleteScheduledTaskWithDurableCleanup(
|
|
102
106
|
deps: ApiRouteDeps,
|
|
103
|
-
|
|
107
|
+
request: { grant: AccessGrant; taskId: string },
|
|
104
108
|
): Promise<{ task: ScheduledTaskValue; changed: boolean }> {
|
|
109
|
+
const input = {
|
|
110
|
+
workspaceId: request.grant.workspaceId,
|
|
111
|
+
subjectId: request.grant.subjectId,
|
|
112
|
+
taskId: request.taskId,
|
|
113
|
+
};
|
|
105
114
|
const result = await deleteScheduledTaskLifecycle({
|
|
106
115
|
db: deps.db,
|
|
107
116
|
workspaceId: input.workspaceId,
|
|
108
117
|
taskId: input.taskId,
|
|
109
118
|
subjectId: input.subjectId,
|
|
119
|
+
beforeDeleteCommit: (tx) => assertScheduledTaskMutationOwner(tx, request.grant, request.taskId),
|
|
110
120
|
preflightConnectorAuthorization: async (task) =>
|
|
111
121
|
await preflightConnectorAuthorization(deps, task, input.subjectId),
|
|
112
122
|
cleanupConnectorAuthorization: async (db, task) =>
|
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
ToolGatewayApprovalResponse,
|
|
23
23
|
ToolGatewayDeclarationsResponse,
|
|
24
24
|
type AccessGrant,
|
|
25
|
-
type McpGatewayCredentialAuthority,
|
|
26
25
|
type ToolGatewayCatalog,
|
|
27
26
|
type ToolGatewayIdentity,
|
|
28
27
|
type ToolRef,
|
|
@@ -43,7 +42,6 @@ import {
|
|
|
43
42
|
import {
|
|
44
43
|
buildCodexTokenResolver,
|
|
45
44
|
buildConnectionTokenResolver,
|
|
46
|
-
buildHostGatewayConnectionTokenResolver,
|
|
47
45
|
lockActiveExternalOrganizationKey,
|
|
48
46
|
withAccountRls,
|
|
49
47
|
requireWorkspace,
|
|
@@ -187,16 +185,7 @@ export async function prepareWorkspaceToolGateway(
|
|
|
187
185
|
routeDeps,
|
|
188
186
|
grant,
|
|
189
187
|
undefined,
|
|
190
|
-
reauthorize
|
|
191
|
-
? {
|
|
192
|
-
authority: {
|
|
193
|
-
kind: external ? "external_user" : "organization_service",
|
|
194
|
-
subjectId: scope.subjectId,
|
|
195
|
-
permissions,
|
|
196
|
-
},
|
|
197
|
-
reauthorize,
|
|
198
|
-
}
|
|
199
|
-
: undefined,
|
|
188
|
+
reauthorize,
|
|
200
189
|
);
|
|
201
190
|
try {
|
|
202
191
|
await reauthorize?.();
|
|
@@ -226,11 +215,36 @@ export async function prepareWorkspaceToolGatewayForGrant(
|
|
|
226
215
|
return await prepareWorkspaceToolGatewayForGrantInternal(routeDeps, grant, allowedIdentities);
|
|
227
216
|
}
|
|
228
217
|
|
|
218
|
+
/** Keep live caller authority independent of native connection acquisition and refresh. */
|
|
219
|
+
export function withWorkspaceConnectionAuthorization(
|
|
220
|
+
resolve: ReturnType<typeof buildConnectionTokenResolver>,
|
|
221
|
+
reauthorize?: () => Promise<void>,
|
|
222
|
+
): ReturnType<typeof buildConnectionTokenResolver> {
|
|
223
|
+
if (!reauthorize) return resolve;
|
|
224
|
+
return async (input) => {
|
|
225
|
+
await reauthorize();
|
|
226
|
+
const result = await resolve(input);
|
|
227
|
+
await reauthorize();
|
|
228
|
+
if (result.status !== "ok") return result;
|
|
229
|
+
return {
|
|
230
|
+
...result,
|
|
231
|
+
authorizeProviderRequest: async () => {
|
|
232
|
+
try {
|
|
233
|
+
await reauthorize();
|
|
234
|
+
return result.authorizeProviderRequest ? await result.authorizeProviderRequest() : true;
|
|
235
|
+
} catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
229
243
|
async function prepareWorkspaceToolGatewayForGrantInternal(
|
|
230
244
|
routeDeps: ApiRouteDeps,
|
|
231
245
|
grant: AccessGrant,
|
|
232
246
|
allowedIdentities?: readonly { serverId: string; toolName: string }[],
|
|
233
|
-
|
|
247
|
+
reauthorize?: () => Promise<void>,
|
|
234
248
|
): Promise<PreparedWorkspaceToolGateway> {
|
|
235
249
|
const catalogSourceSettings = routeDeps.catalogSourceSettings ?? routeDeps.settings;
|
|
236
250
|
const resolvedCatalog = await resolveWorkspaceCatalogSettings(
|
|
@@ -256,24 +270,10 @@ async function prepareWorkspaceToolGatewayForGrantInternal(
|
|
|
256
270
|
const gatewaySettings = workspaceToolGatewaySettingsForGrant(settings, grant, allowedIdentities);
|
|
257
271
|
const gatewayServerIds = new Set(gatewaySettings.mcpServers.map((server) => server.id));
|
|
258
272
|
const deps = { ...routeDeps, catalogSourceSettings, settings: gatewaySettings };
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
? buildHostGatewayConnectionTokenResolver(
|
|
264
|
-
hostPort,
|
|
265
|
-
{
|
|
266
|
-
accountId: grant.accountId,
|
|
267
|
-
workspaceId: grant.workspaceId,
|
|
268
|
-
authority: hostContext.authority,
|
|
269
|
-
},
|
|
270
|
-
hostContext.reauthorize,
|
|
271
|
-
)
|
|
272
|
-
: undefined;
|
|
273
|
-
const resolveConnection: typeof nativeResolveConnection = async (input) =>
|
|
274
|
-
input.connectionRef.authoritySource === "host" && hostResolveConnection
|
|
275
|
-
? await hostResolveConnection(input)
|
|
276
|
-
: await nativeResolveConnection(input);
|
|
273
|
+
const resolveConnection = withWorkspaceConnectionAuthorization(
|
|
274
|
+
buildConnectionTokenResolver(routeDeps.db, gatewaySettings),
|
|
275
|
+
reauthorize,
|
|
276
|
+
);
|
|
277
277
|
const resolveCredential = async (
|
|
278
278
|
input: ResolveConnectionCredentialInput,
|
|
279
279
|
): Promise<ResolveConnectionCredentialResult> =>
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import type { AccessGrantAuthorization } from "@opengeni/core";
|
|
2
|
-
import type { UserResourceAuthoritySummary } from "@opengeni/db";
|
|
3
|
-
import { type IssueConnectionUseGrantRequest as IssueConnectionUseGrantRequestValue } from "@opengeni/contracts/connection-authority";
|
|
4
|
-
/**
|
|
5
|
-
* Personal connection authority is available only to the exact authenticated
|
|
6
|
-
* human. API keys, delegated bearers, services, and agent attempts cannot name
|
|
7
|
-
* or substitute a personal owner.
|
|
8
|
-
*/
|
|
9
|
-
export declare function requireConnectionAuthorityOwner(access: AccessGrantAuthorization): string;
|
|
10
|
-
/** Removes generic resource kind and refuses non-connection grant actions. */
|
|
11
|
-
export declare function projectSelfConnectionAuthorities(page: {
|
|
12
|
-
authorities: UserResourceAuthoritySummary[];
|
|
13
|
-
nextCursor: string | null;
|
|
14
|
-
}): {
|
|
15
|
-
scope: "user";
|
|
16
|
-
authorities: {
|
|
17
|
-
authorityId: string;
|
|
18
|
-
resourceId: string;
|
|
19
|
-
originWorkspaceId: string | null;
|
|
20
|
-
generation: number;
|
|
21
|
-
status: "active" | "retained" | "revoked";
|
|
22
|
-
grants: {
|
|
23
|
-
grantId: string;
|
|
24
|
-
targetWorkspaceId: string;
|
|
25
|
-
targetSessionId: string | null;
|
|
26
|
-
mode: "always" | "once" | "session";
|
|
27
|
-
context: "user_private" | "workspace_shared";
|
|
28
|
-
authorityEpoch: number | null;
|
|
29
|
-
generation: number;
|
|
30
|
-
status: "active" | "consumed" | "expired" | "revoked";
|
|
31
|
-
expiresAt: string | null;
|
|
32
|
-
delegation: {
|
|
33
|
-
authorityId: string;
|
|
34
|
-
grantId: string;
|
|
35
|
-
organizationId: string;
|
|
36
|
-
workspaceId: string;
|
|
37
|
-
sessionId: string | null;
|
|
38
|
-
action: string;
|
|
39
|
-
mode: "always" | "once" | "session";
|
|
40
|
-
context: "user_private" | "workspace_shared";
|
|
41
|
-
authorityEpoch: number | null;
|
|
42
|
-
authorityGeneration: number;
|
|
43
|
-
grantGeneration: number;
|
|
44
|
-
resourceVersionId?: string | null | undefined;
|
|
45
|
-
};
|
|
46
|
-
action: "connection.use";
|
|
47
|
-
}[];
|
|
48
|
-
}[];
|
|
49
|
-
nextCursor: string | null;
|
|
50
|
-
};
|
|
51
|
-
/** The API owns the action; callers supply neither action nor owner identity. */
|
|
52
|
-
export declare function connectionUseGrantLifecycleInput(input: unknown): {
|
|
53
|
-
action: "connection.use";
|
|
54
|
-
mode: IssueConnectionUseGrantRequestValue["mode"];
|
|
55
|
-
context: IssueConnectionUseGrantRequestValue["context"];
|
|
56
|
-
sessionId: string | null;
|
|
57
|
-
expectedAuthorityEpoch: number | null;
|
|
58
|
-
workspaceSharedAcknowledged: boolean;
|
|
59
|
-
};
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { Hono } from "hono";
|
|
2
|
-
import { type ApiRouteDeps } from "@opengeni/core";
|
|
3
|
-
/** Registration is not an execution grant. The durable runtime must separately
|
|
4
|
-
* validate the exact binding generation and accepted execution context. */
|
|
5
|
-
export declare function registerHostMcpBindingRoutes(app: Hono, deps: ApiRouteDeps): void;
|
package/dist/routes/packs.d.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import type { AccessGrantAuthorization } from "@opengeni/core";
|
|
2
|
-
import type { UserResourceAuthoritySummary } from "@opengeni/db";
|
|
3
|
-
import {
|
|
4
|
-
IssueConnectionUseGrantRequest,
|
|
5
|
-
ListConnectionAuthoritiesResponse,
|
|
6
|
-
type IssueConnectionUseGrantRequest as IssueConnectionUseGrantRequestValue,
|
|
7
|
-
} from "@opengeni/contracts/connection-authority";
|
|
8
|
-
import { HTTPException } from "hono/http-exception";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Personal connection authority is available only to the exact authenticated
|
|
12
|
-
* human. API keys, delegated bearers, services, and agent attempts cannot name
|
|
13
|
-
* or substitute a personal owner.
|
|
14
|
-
*/
|
|
15
|
-
export function requireConnectionAuthorityOwner(access: AccessGrantAuthorization): string {
|
|
16
|
-
if (
|
|
17
|
-
!access.contextIntegrity ||
|
|
18
|
-
access.authenticatedSubjectId !== access.grant.subjectId ||
|
|
19
|
-
access.grant.principalKind !== "human_session" ||
|
|
20
|
-
access.grant.serviceInitiator ||
|
|
21
|
-
access.grant.serviceInitiatorContext ||
|
|
22
|
-
access.grant.subjectId.startsWith("api_key:")
|
|
23
|
-
) {
|
|
24
|
-
throw new HTTPException(403, { message: "authenticated connection owner required" });
|
|
25
|
-
}
|
|
26
|
-
return access.grant.subjectId;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Removes generic resource kind and refuses non-connection grant actions. */
|
|
30
|
-
export function projectSelfConnectionAuthorities(page: {
|
|
31
|
-
authorities: UserResourceAuthoritySummary[];
|
|
32
|
-
nextCursor: string | null;
|
|
33
|
-
}) {
|
|
34
|
-
return ListConnectionAuthoritiesResponse.parse({
|
|
35
|
-
scope: "user",
|
|
36
|
-
authorities: page.authorities
|
|
37
|
-
.filter((authority) => authority.resourceKind === "connection")
|
|
38
|
-
.map(({ resourceKind: _resourceKind, ...authority }) => authority),
|
|
39
|
-
nextCursor: page.nextCursor,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** The API owns the action; callers supply neither action nor owner identity. */
|
|
44
|
-
export function connectionUseGrantLifecycleInput(input: unknown): {
|
|
45
|
-
action: "connection.use";
|
|
46
|
-
mode: IssueConnectionUseGrantRequestValue["mode"];
|
|
47
|
-
context: IssueConnectionUseGrantRequestValue["context"];
|
|
48
|
-
sessionId: string | null;
|
|
49
|
-
expectedAuthorityEpoch: number | null;
|
|
50
|
-
workspaceSharedAcknowledged: boolean;
|
|
51
|
-
} {
|
|
52
|
-
const request = IssueConnectionUseGrantRequest.parse(input);
|
|
53
|
-
return {
|
|
54
|
-
action: "connection.use",
|
|
55
|
-
mode: request.mode,
|
|
56
|
-
context: request.context,
|
|
57
|
-
sessionId: request.sessionId ?? null,
|
|
58
|
-
expectedAuthorityEpoch: request.expectedAuthorityEpoch ?? null,
|
|
59
|
-
workspaceSharedAcknowledged: request.workspaceSharedAcknowledged,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ConnectionUseGrantMutationResponse,
|
|
3
|
-
ConnectionUseGrantRevocationResponse,
|
|
4
|
-
IssueConnectionUseGrantRequest,
|
|
5
|
-
ListConnectionAuthoritiesQuery,
|
|
6
|
-
RevokeConnectionUseGrantQuery,
|
|
7
|
-
} from "@opengeni/contracts/connection-authority";
|
|
8
|
-
import {
|
|
9
|
-
issueManagedHumanUserResourceGrant,
|
|
10
|
-
listManagedHumanUserResourceAuthorities,
|
|
11
|
-
requireAccessGrantAuthorization,
|
|
12
|
-
revokeManagedHumanUserResourceGrant,
|
|
13
|
-
SessionAuthorizationDeniedError,
|
|
14
|
-
SessionAuthorizationUnavailableError,
|
|
15
|
-
SessionTenancyManagedHumanRequiredError,
|
|
16
|
-
type ApiRouteDeps,
|
|
17
|
-
} from "@opengeni/core";
|
|
18
|
-
import { nestedPostgresSqlState, SessionTenancyNotActivatedError } from "@opengeni/db";
|
|
19
|
-
import type { Context, Hono } from "hono";
|
|
20
|
-
import { HTTPException } from "hono/http-exception";
|
|
21
|
-
import { z } from "zod";
|
|
22
|
-
import {
|
|
23
|
-
connectionUseGrantLifecycleInput,
|
|
24
|
-
projectSelfConnectionAuthorities,
|
|
25
|
-
} from "../connection-authority-owner";
|
|
26
|
-
|
|
27
|
-
async function body<S extends z.ZodType>(context: Context, schema: S): Promise<z.infer<S>> {
|
|
28
|
-
const parsed = schema.safeParse(await context.req.json().catch(() => null));
|
|
29
|
-
if (!parsed.success) throw new HTTPException(422, { message: "invalid connection authority" });
|
|
30
|
-
return parsed.data;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function lifecycleError(error: unknown): never {
|
|
34
|
-
if (
|
|
35
|
-
error instanceof SessionAuthorizationDeniedError ||
|
|
36
|
-
error instanceof SessionTenancyManagedHumanRequiredError ||
|
|
37
|
-
error instanceof SessionTenancyNotActivatedError
|
|
38
|
-
) {
|
|
39
|
-
throw new HTTPException(403, { message: "connection authority denied" });
|
|
40
|
-
}
|
|
41
|
-
if (error instanceof SessionAuthorizationUnavailableError) {
|
|
42
|
-
throw new HTTPException(503, { message: "session authorization unavailable" });
|
|
43
|
-
}
|
|
44
|
-
if (nestedPostgresSqlState(error) === "42501") {
|
|
45
|
-
throw new HTTPException(403, { message: "connection authority denied" });
|
|
46
|
-
}
|
|
47
|
-
if (nestedPostgresSqlState(error) === "22023") {
|
|
48
|
-
throw new HTTPException(422, { message: "invalid connection authority" });
|
|
49
|
-
}
|
|
50
|
-
throw error;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function registerConnectionAuthorityRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
54
|
-
const base = "/v1/workspaces/:workspaceId/connection-authorities";
|
|
55
|
-
|
|
56
|
-
app.get(base, async (context) => {
|
|
57
|
-
const query = ListConnectionAuthoritiesQuery.safeParse({
|
|
58
|
-
scope: context.req.query("scope"),
|
|
59
|
-
cursor: context.req.query("cursor"),
|
|
60
|
-
limit: context.req.query("limit"),
|
|
61
|
-
});
|
|
62
|
-
if (!query.success) throw new HTTPException(422, { message: "explicit scope=user required" });
|
|
63
|
-
const workspaceId = context.req.param("workspaceId");
|
|
64
|
-
const access = await requireAccessGrantAuthorization(
|
|
65
|
-
context,
|
|
66
|
-
deps,
|
|
67
|
-
workspaceId,
|
|
68
|
-
"connections:read",
|
|
69
|
-
);
|
|
70
|
-
try {
|
|
71
|
-
return context.json(
|
|
72
|
-
projectSelfConnectionAuthorities(
|
|
73
|
-
await listManagedHumanUserResourceAuthorities(deps, access, workspaceId, {
|
|
74
|
-
resourceKind: "connection",
|
|
75
|
-
cursor: query.data.cursor,
|
|
76
|
-
limit: query.data.limit,
|
|
77
|
-
}),
|
|
78
|
-
),
|
|
79
|
-
);
|
|
80
|
-
} catch (error) {
|
|
81
|
-
return lifecycleError(error);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
app.post(`${base}/:authorityId/grants`, async (context) => {
|
|
86
|
-
const workspaceId = context.req.param("workspaceId");
|
|
87
|
-
const authorityId = z.string().uuid().safeParse(context.req.param("authorityId"));
|
|
88
|
-
if (!authorityId.success) throw new HTTPException(422, { message: "invalid authority id" });
|
|
89
|
-
const request = await body(context, IssueConnectionUseGrantRequest);
|
|
90
|
-
const access = await requireAccessGrantAuthorization(
|
|
91
|
-
context,
|
|
92
|
-
deps,
|
|
93
|
-
workspaceId,
|
|
94
|
-
"connections:read",
|
|
95
|
-
);
|
|
96
|
-
try {
|
|
97
|
-
const normalized = connectionUseGrantLifecycleInput(request);
|
|
98
|
-
return context.json(
|
|
99
|
-
ConnectionUseGrantMutationResponse.parse({
|
|
100
|
-
scope: "user",
|
|
101
|
-
grant: await issueManagedHumanUserResourceGrant(
|
|
102
|
-
deps,
|
|
103
|
-
access,
|
|
104
|
-
workspaceId,
|
|
105
|
-
authorityId.data,
|
|
106
|
-
{ scope: "user", resourceKind: "connection", ...normalized },
|
|
107
|
-
"http",
|
|
108
|
-
),
|
|
109
|
-
}),
|
|
110
|
-
);
|
|
111
|
-
} catch (error) {
|
|
112
|
-
return lifecycleError(error);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
app.delete(`${base}/grants/:grantId`, async (context) => {
|
|
117
|
-
const query = RevokeConnectionUseGrantQuery.safeParse({ scope: context.req.query("scope") });
|
|
118
|
-
if (!query.success) throw new HTTPException(422, { message: "explicit scope=user required" });
|
|
119
|
-
const grantId = z.string().uuid().safeParse(context.req.param("grantId"));
|
|
120
|
-
if (!grantId.success) throw new HTTPException(422, { message: "invalid grant id" });
|
|
121
|
-
const workspaceId = context.req.param("workspaceId");
|
|
122
|
-
const access = await requireAccessGrantAuthorization(
|
|
123
|
-
context,
|
|
124
|
-
deps,
|
|
125
|
-
workspaceId,
|
|
126
|
-
"workspace:read",
|
|
127
|
-
);
|
|
128
|
-
try {
|
|
129
|
-
return context.json(
|
|
130
|
-
ConnectionUseGrantRevocationResponse.parse({
|
|
131
|
-
scope: "user",
|
|
132
|
-
grant: await revokeManagedHumanUserResourceGrant(deps, access, workspaceId, grantId.data),
|
|
133
|
-
}),
|
|
134
|
-
);
|
|
135
|
-
} catch (error) {
|
|
136
|
-
return lifecycleError(error);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import type { Hono } from "hono";
|
|
2
|
-
import { HTTPException } from "hono/http-exception";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import {
|
|
5
|
-
CreateHostMcpBindingRequest,
|
|
6
|
-
IssueHostMcpDelegationRequest,
|
|
7
|
-
} from "@opengeni/contracts/host-mcp-bindings";
|
|
8
|
-
import {
|
|
9
|
-
createHostMcpBinding,
|
|
10
|
-
getHostMcpBinding,
|
|
11
|
-
revokeHostMcpBinding,
|
|
12
|
-
issueHostMcpDelegation,
|
|
13
|
-
getHostMcpDelegation,
|
|
14
|
-
revokeHostMcpDelegation,
|
|
15
|
-
HostMcpDelegationAuthorityError,
|
|
16
|
-
HostMcpBindingConflictError,
|
|
17
|
-
withWorkspaceSubjectRls,
|
|
18
|
-
} from "@opengeni/db";
|
|
19
|
-
import {
|
|
20
|
-
requireAccessGrantAuthorization,
|
|
21
|
-
prepareHostMcpOwnerAuthorization,
|
|
22
|
-
type ApiRouteDeps,
|
|
23
|
-
} from "@opengeni/core";
|
|
24
|
-
|
|
25
|
-
/** Registration is not an execution grant. The durable runtime must separately
|
|
26
|
-
* validate the exact binding generation and accepted execution context. */
|
|
27
|
-
export function registerHostMcpBindingRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
28
|
-
for (const entity of ["bindings", "delegations"] as const) {
|
|
29
|
-
app.on(
|
|
30
|
-
["GET", "POST"],
|
|
31
|
-
[
|
|
32
|
-
`/v1/workspaces/:workspaceId/host-mcp-${entity}`,
|
|
33
|
-
`/v1/workspaces/:workspaceId/host-mcp-${entity}/:bindingId`,
|
|
34
|
-
`/v1/workspaces/:workspaceId/host-mcp-${entity}/:bindingId/revoke`,
|
|
35
|
-
],
|
|
36
|
-
async (c) => {
|
|
37
|
-
const workspaceId = c.req.param("workspaceId")!;
|
|
38
|
-
const bindingId = c.req.param("bindingId");
|
|
39
|
-
const method = c.req.method;
|
|
40
|
-
const revoke = c.req.path.endsWith("/revoke");
|
|
41
|
-
if (
|
|
42
|
-
!(
|
|
43
|
-
(method === "POST" && !bindingId) ||
|
|
44
|
-
(method === "GET" && bindingId && !revoke) ||
|
|
45
|
-
(method === "POST" && bindingId && revoke)
|
|
46
|
-
)
|
|
47
|
-
)
|
|
48
|
-
throw new HTTPException(404, { message: "not found" });
|
|
49
|
-
const permission = method === "GET" ? "connections:read" : "connections:write";
|
|
50
|
-
const authorization = await requireAccessGrantAuthorization(
|
|
51
|
-
c,
|
|
52
|
-
deps,
|
|
53
|
-
workspaceId,
|
|
54
|
-
permission,
|
|
55
|
-
);
|
|
56
|
-
const authorizeCommit = prepareHostMcpOwnerAuthorization(
|
|
57
|
-
authorization,
|
|
58
|
-
workspaceId,
|
|
59
|
-
permission,
|
|
60
|
-
);
|
|
61
|
-
if (bindingId) z.string().uuid().parse(bindingId);
|
|
62
|
-
const createInput =
|
|
63
|
-
method === "POST" && !bindingId
|
|
64
|
-
? (entity === "bindings"
|
|
65
|
-
? CreateHostMcpBindingRequest
|
|
66
|
-
: IssueHostMcpDelegationRequest
|
|
67
|
-
).parse(await c.req.json())
|
|
68
|
-
: null;
|
|
69
|
-
const revokeInput = revoke
|
|
70
|
-
? z
|
|
71
|
-
.object({ expectedGeneration: z.number().int().positive().safe() })
|
|
72
|
-
.strict()
|
|
73
|
-
.parse(await c.req.json())
|
|
74
|
-
: null;
|
|
75
|
-
const owner = {
|
|
76
|
-
accountId: authorization.grant.accountId,
|
|
77
|
-
workspaceId,
|
|
78
|
-
subjectId: authorization.grant.subjectId,
|
|
79
|
-
};
|
|
80
|
-
try {
|
|
81
|
-
const result = await withWorkspaceSubjectRls(
|
|
82
|
-
deps.db,
|
|
83
|
-
workspaceId,
|
|
84
|
-
owner.subjectId,
|
|
85
|
-
async (tx) => {
|
|
86
|
-
const effectiveOwner = await authorizeCommit(tx);
|
|
87
|
-
if (createInput)
|
|
88
|
-
return entity === "bindings"
|
|
89
|
-
? createHostMcpBinding(tx, effectiveOwner, createInput)
|
|
90
|
-
: issueHostMcpDelegation(tx, effectiveOwner, createInput);
|
|
91
|
-
const existing = await (
|
|
92
|
-
entity === "bindings" ? getHostMcpBinding : getHostMcpDelegation
|
|
93
|
-
)(tx, effectiveOwner, bindingId!);
|
|
94
|
-
if (!existing)
|
|
95
|
-
throw new HTTPException(404, {
|
|
96
|
-
message:
|
|
97
|
-
entity === "bindings" ? "Host binding not found" : "Host delegation not found",
|
|
98
|
-
});
|
|
99
|
-
return revokeInput
|
|
100
|
-
? (entity === "bindings" ? revokeHostMcpBinding : revokeHostMcpDelegation)(
|
|
101
|
-
tx,
|
|
102
|
-
effectiveOwner,
|
|
103
|
-
bindingId!,
|
|
104
|
-
revokeInput.expectedGeneration,
|
|
105
|
-
)
|
|
106
|
-
: existing;
|
|
107
|
-
},
|
|
108
|
-
);
|
|
109
|
-
return c.json(result, createInput ? 201 : 200);
|
|
110
|
-
} catch (error) {
|
|
111
|
-
if (error instanceof HostMcpDelegationAuthorityError)
|
|
112
|
-
throw new HTTPException(403, { message: "Host delegation authority unavailable" });
|
|
113
|
-
if (error instanceof HostMcpBindingConflictError)
|
|
114
|
-
throw new HTTPException(409, {
|
|
115
|
-
message: "Host binding changed; reload before retrying",
|
|
116
|
-
});
|
|
117
|
-
throw error;
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { Hono } from "hono";
|
|
2
|
-
import { hostMcpResolverForRequest, type ApiRouteDeps } from "@opengeni/core";
|
|
3
|
-
|
|
4
|
-
export function registerHostMcpResolverRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
5
|
-
const path = "/v1/organizations/:organizationId/mcp-credential-resolvers/:externalSource";
|
|
6
|
-
app.get(path, async (context) =>
|
|
7
|
-
context.json(
|
|
8
|
-
await hostMcpResolverForRequest(
|
|
9
|
-
context,
|
|
10
|
-
deps,
|
|
11
|
-
context.req.param("organizationId"),
|
|
12
|
-
context.req.param("externalSource"),
|
|
13
|
-
"get",
|
|
14
|
-
),
|
|
15
|
-
),
|
|
16
|
-
);
|
|
17
|
-
app.put(path, async (context) =>
|
|
18
|
-
context.json(
|
|
19
|
-
await hostMcpResolverForRequest(
|
|
20
|
-
context,
|
|
21
|
-
deps,
|
|
22
|
-
context.req.param("organizationId"),
|
|
23
|
-
context.req.param("externalSource"),
|
|
24
|
-
"put",
|
|
25
|
-
await context.req.json().catch(() => null),
|
|
26
|
-
),
|
|
27
|
-
),
|
|
28
|
-
);
|
|
29
|
-
app.post(`${path}/revoke`, async (context) =>
|
|
30
|
-
context.json(
|
|
31
|
-
await hostMcpResolverForRequest(
|
|
32
|
-
context,
|
|
33
|
-
deps,
|
|
34
|
-
context.req.param("organizationId"),
|
|
35
|
-
context.req.param("externalSource"),
|
|
36
|
-
"revoke",
|
|
37
|
-
await context.req.json().catch(() => null),
|
|
38
|
-
),
|
|
39
|
-
),
|
|
40
|
-
);
|
|
41
|
-
}
|