@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.
Files changed (96) hide show
  1. package/dist/admission-trace.d.ts +8 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
  4. package/dist/auth/managed-auth-session-adapter.d.ts +2 -0
  5. package/dist/auth/managed-auth.d.ts +1 -0
  6. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  7. package/dist/{chunk-AJ62T5QN.js → chunk-E7TZFJ65.js} +22300 -21113
  8. package/dist/{chunk-AJ62T5QN.js.map → chunk-E7TZFJ65.js.map} +1 -1
  9. package/dist/connection-ownership.d.ts +1 -2
  10. package/dist/fatal-process-boundary.d.ts +1 -1
  11. package/dist/index.js +24 -6
  12. package/dist/index.js.map +1 -1
  13. package/dist/integrations/atlassian.d.ts +8 -3
  14. package/dist/integrations/fiken.d.ts +3 -1
  15. package/dist/integrations/github-lens-connect.d.ts +5 -65
  16. package/dist/integrations/google-drive.d.ts +2 -0
  17. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  18. package/dist/integrations/oauth-client.d.ts +119 -11
  19. package/dist/integrations/oauth-profiles.d.ts +8 -13
  20. package/dist/integrations/slack-interactions.d.ts +1 -1
  21. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  22. package/dist/mcp/first-party-tool-permissions.d.ts +4 -0
  23. package/dist/mcp-oauth.d.ts +22 -1
  24. package/dist/routes/api-integrations.d.ts +1 -0
  25. package/dist/routes/codex.d.ts +28 -0
  26. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  27. package/dist/routes/organization-integration-policy.d.ts +3 -0
  28. package/dist/routes/sessions.d.ts +24 -2
  29. package/dist/scheduled-task-deletion.d.ts +3 -4
  30. package/dist/workspace-tool-gateway.d.ts +3 -1
  31. package/package.json +15 -15
  32. package/src/admission-trace.ts +14 -0
  33. package/src/app.ts +141 -94
  34. package/src/auth/managed-auth-attempt-context.ts +9 -0
  35. package/src/auth/managed-auth-session-adapter.ts +11 -0
  36. package/src/auth/managed-auth.ts +39 -2
  37. package/src/auth/managed-sign-in-notifications.ts +81 -0
  38. package/src/connection-ownership.ts +2 -9
  39. package/src/editable-artifact-production.ts +6 -0
  40. package/src/fatal-process-boundary.ts +17 -2
  41. package/src/index.ts +3 -4
  42. package/src/integrations/atlassian.ts +184 -76
  43. package/src/integrations/fiken.ts +57 -35
  44. package/src/integrations/github-app-connect.ts +51 -15
  45. package/src/integrations/github-lens-connect.ts +7 -28
  46. package/src/integrations/google-drive.ts +296 -125
  47. package/src/integrations/integration-commit-authority.ts +69 -0
  48. package/src/integrations/oauth-client.ts +231 -132
  49. package/src/integrations/oauth-profiles.ts +10 -45
  50. package/src/integrations/personal-github.ts +55 -31
  51. package/src/integrations/provider-oauth.ts +57 -82
  52. package/src/integrations/social-oauth.ts +83 -56
  53. package/src/mcp/capability-account-readiness.ts +17 -0
  54. package/src/mcp/editable-artifacts.ts +7 -2
  55. package/src/mcp/first-party-tool-permissions.ts +1 -0
  56. package/src/mcp/knowledge-entries.ts +25 -8
  57. package/src/mcp/server.ts +135 -43
  58. package/src/mcp-oauth.ts +336 -34
  59. package/src/routes/api-integrations.ts +116 -4
  60. package/src/routes/automations.ts +5 -13
  61. package/src/routes/billing.ts +52 -0
  62. package/src/routes/canonical-human-identities.ts +61 -0
  63. package/src/routes/capabilities.ts +51 -1
  64. package/src/routes/codex.ts +4 -2
  65. package/src/routes/connect.ts +90 -31
  66. package/src/routes/connections.ts +487 -307
  67. package/src/routes/files.ts +21 -12
  68. package/src/routes/github.ts +48 -19
  69. package/src/routes/insights.ts +17 -7
  70. package/src/routes/integration-facets.ts +66 -25
  71. package/src/routes/knowledge.ts +48 -1
  72. package/src/routes/managed-sign-in-methods.ts +433 -0
  73. package/src/routes/organization-integration-policy.ts +113 -0
  74. package/src/routes/organization-memberships.ts +22 -2
  75. package/src/routes/plugins.ts +42 -4
  76. package/src/routes/pr-review-github.ts +67 -57
  77. package/src/routes/pr-review.ts +74 -71
  78. package/src/routes/rigs.ts +32 -13
  79. package/src/routes/scheduled-tasks.ts +6 -6
  80. package/src/routes/sessions.ts +233 -11
  81. package/src/routes/skill-content.ts +19 -3
  82. package/src/routes/social.ts +24 -13
  83. package/src/sandbox/channel-a.ts +7 -1
  84. package/src/sandbox/rematerialize.ts +2 -1
  85. package/src/scheduled-task-deletion.ts +13 -3
  86. package/src/workspace-tool-gateway.ts +31 -31
  87. package/dist/connection-authority-owner.d.ts +0 -59
  88. package/dist/routes/connection-authorities.d.ts +0 -3
  89. package/dist/routes/host-mcp-bindings.d.ts +0 -5
  90. package/dist/routes/host-mcp-resolvers.d.ts +0 -3
  91. package/dist/routes/packs.d.ts +0 -3
  92. package/src/connection-authority-owner.ts +0 -61
  93. package/src/routes/connection-authorities.ts +0 -139
  94. package/src/routes/host-mcp-bindings.ts +0 -122
  95. package/src/routes/host-mcp-resolvers.ts +0 -41
  96. package/src/routes/packs.ts +0 -784
@@ -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
- // a stale deployment image after the worker had resolved a newer Pack/Rig or
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 { deleteScheduledTaskLifecycle, type ApiRouteDeps } from "@opengeni/core";
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
- input: { workspaceId: string; taskId: string; subjectId: string },
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
- hostContext?: { authority: McpGatewayCredentialAuthority; reauthorize: () => Promise<void> },
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 nativeResolveConnection = buildConnectionTokenResolver(routeDeps.db, gatewaySettings);
260
- const hostPort = routeDeps.connectionCredentials?.mcpGatewayCredentials;
261
- const hostResolveConnection =
262
- hostContext && hostPort
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,3 +0,0 @@
1
- import { type ApiRouteDeps } from "@opengeni/core";
2
- import type { Hono } from "hono";
3
- export declare function registerConnectionAuthorityRoutes(app: Hono, deps: ApiRouteDeps): void;
@@ -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;
@@ -1,3 +0,0 @@
1
- import type { Hono } from "hono";
2
- import { type ApiRouteDeps } from "@opengeni/core";
3
- export declare function registerHostMcpResolverRoutes(app: Hono, deps: ApiRouteDeps): void;
@@ -1,3 +0,0 @@
1
- import type { Hono } from "hono";
2
- import type { ApiRouteDeps } from "@opengeni/core";
3
- export declare function registerPackRoutes(app: Hono, deps: ApiRouteDeps): void;
@@ -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
- }