@opengeni/api-router 0.30.3 → 2.2.0-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.
Files changed (165) hide show
  1. package/dist/api-websocket.d.ts +18 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth.d.ts +6 -0
  4. package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
  5. package/dist/chunk-3TP54PPX.js.map +1 -0
  6. package/dist/codemode.d.ts +8 -1
  7. package/dist/company-brain-okf.d.ts +24 -0
  8. package/dist/connected-machine-computer-access.d.ts +11 -0
  9. package/dist/connection-authority-owner.d.ts +59 -0
  10. package/dist/connection-ownership.d.ts +47 -0
  11. package/dist/controller-data-plane.d.ts +16 -0
  12. package/dist/editable-artifact-websocket.d.ts +5 -15
  13. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  14. package/dist/http/api-error.d.ts +10 -0
  15. package/dist/http/cors.d.ts +5 -0
  16. package/dist/http/interaction-control-error.d.ts +13 -0
  17. package/dist/http/sse.d.ts +2 -3
  18. package/dist/index.d.ts +5 -2
  19. package/dist/index.js +326 -82
  20. package/dist/index.js.map +1 -1
  21. package/dist/integrations/atlassian.d.ts +17 -0
  22. package/dist/integrations/fiken.d.ts +7 -2
  23. package/dist/integrations/google-drive.d.ts +18 -0
  24. package/dist/integrations/oauth-client.d.ts +22 -3
  25. package/dist/integrations/oauth-profiles.d.ts +195 -0
  26. package/dist/integrations/personal-github-repositories.d.ts +45 -0
  27. package/dist/integrations/personal-github.d.ts +40 -0
  28. package/dist/integrations/pr-review-provider.d.ts +22 -0
  29. package/dist/integrations/provider-oauth.d.ts +8 -0
  30. package/dist/integrations/slack-app-home.d.ts +24 -0
  31. package/dist/integrations/slack-bot.d.ts +157 -9
  32. package/dist/integrations/slack-interactions.d.ts +39 -5
  33. package/dist/integrations/slack-routing.d.ts +120 -0
  34. package/dist/integrations/social-oauth.d.ts +5 -0
  35. package/dist/interaction-frame-proxy.d.ts +68 -0
  36. package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
  37. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  38. package/dist/mcp/documents.d.ts +1 -0
  39. package/dist/mcp/remember.d.ts +28 -0
  40. package/dist/mcp/request-abort.d.ts +19 -0
  41. package/dist/mcp/scheduled-task-view.d.ts +4 -4
  42. package/dist/mcp/server.d.ts +15 -2
  43. package/dist/mcp/session-view.d.ts +4 -0
  44. package/dist/mcp/session-wait.d.ts +137 -0
  45. package/dist/routes/automations.d.ts +4 -0
  46. package/dist/routes/billing.d.ts +5 -0
  47. package/dist/routes/browser-sessions.d.ts +4 -1
  48. package/dist/routes/company-brain.d.ts +3 -0
  49. package/dist/routes/company-profile.d.ts +2 -1
  50. package/dist/routes/connection-authorities.d.ts +3 -0
  51. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  52. package/dist/routes/personal-github.d.ts +3 -0
  53. package/dist/routes/pr-review.d.ts +3 -0
  54. package/dist/routes/sessions.d.ts +1 -0
  55. package/dist/routes/user-resource-authorities.d.ts +3 -0
  56. package/dist/routes/workspace-capture.d.ts +10 -2
  57. package/dist/routes/workspace-learning.d.ts +3 -0
  58. package/dist/routes/workspaces.d.ts +19 -0
  59. package/dist/sandbox/channel-a.d.ts +11 -1
  60. package/dist/sandbox/connection-authority.d.ts +3 -0
  61. package/dist/sandbox/enrollment.d.ts +2 -0
  62. package/dist/sandbox/machines.d.ts +2 -2
  63. package/dist/sandbox/metrics-ingestion.d.ts +10 -20
  64. package/dist/sandbox/viewer.d.ts +24 -1
  65. package/dist/sandbox-file-artifacts.d.ts +11 -0
  66. package/dist/scheduled-task-deletion.d.ts +14 -0
  67. package/dist/slack-reaction-files.d.ts +1 -1
  68. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  69. package/package.json +20 -18
  70. package/src/api-websocket.ts +23 -0
  71. package/src/app.ts +161 -23
  72. package/src/auth/managed-auth.ts +25 -3
  73. package/src/codemode.ts +53 -23
  74. package/src/codex-realtime.ts +8 -2
  75. package/src/company-brain-okf.ts +340 -0
  76. package/src/connected-machine-computer-access.ts +33 -0
  77. package/src/connection-authority-owner.ts +61 -0
  78. package/src/connection-ownership.ts +180 -0
  79. package/src/controller-data-plane.ts +47 -0
  80. package/src/editable-artifact-native-kernel.ts +26 -15
  81. package/src/editable-artifact-office-import.ts +28 -3
  82. package/src/editable-artifact-production.ts +13 -7
  83. package/src/editable-artifact-websocket.ts +11 -18
  84. package/src/editable-artifact-workspace-files.ts +31 -6
  85. package/src/http/api-error.ts +28 -0
  86. package/src/http/auth.ts +4 -0
  87. package/src/http/cors.ts +35 -0
  88. package/src/http/interaction-control-error.ts +164 -0
  89. package/src/http/sse.ts +112 -66
  90. package/src/index.ts +46 -6
  91. package/src/integrations/atlassian.ts +146 -35
  92. package/src/integrations/fiken.ts +102 -22
  93. package/src/integrations/google-drive.ts +301 -92
  94. package/src/integrations/oauth-client.ts +272 -143
  95. package/src/integrations/oauth-profiles.ts +477 -0
  96. package/src/integrations/personal-github-repositories.ts +445 -0
  97. package/src/integrations/personal-github.ts +705 -0
  98. package/src/integrations/pr-review-provider.ts +246 -0
  99. package/src/integrations/provider-oauth.ts +121 -5
  100. package/src/integrations/slack-app-home.ts +300 -0
  101. package/src/integrations/slack-bot.ts +342 -57
  102. package/src/integrations/slack-interactions.ts +1898 -259
  103. package/src/integrations/slack-routing.ts +337 -0
  104. package/src/integrations/social-oauth.ts +25 -0
  105. package/src/interaction-frame-proxy.ts +409 -0
  106. package/src/mcp/company-brain-governed-writes.ts +262 -0
  107. package/src/mcp/company-profile-agent-admin.ts +205 -0
  108. package/src/mcp/documents.ts +37 -6
  109. package/src/mcp/files.ts +16 -1
  110. package/src/mcp/remember.ts +181 -0
  111. package/src/mcp/request-abort.ts +44 -0
  112. package/src/mcp/scheduled-task-view.ts +1 -0
  113. package/src/mcp/server.ts +719 -369
  114. package/src/mcp/session-view.ts +54 -0
  115. package/src/mcp/session-wait.ts +554 -0
  116. package/src/model-catalog.ts +20 -12
  117. package/src/routes/api-integrations.ts +4 -0
  118. package/src/routes/api-keys.ts +1 -0
  119. package/src/routes/automations.ts +534 -0
  120. package/src/routes/billing.ts +57 -1
  121. package/src/routes/browser-sessions.ts +329 -79
  122. package/src/routes/channels.ts +17 -1
  123. package/src/routes/codex.ts +50 -0
  124. package/src/routes/company-brain.ts +367 -0
  125. package/src/routes/company-profile.ts +1 -1
  126. package/src/routes/computer-sessions.ts +320 -93
  127. package/src/routes/connection-authorities.ts +139 -0
  128. package/src/routes/connections.ts +144 -34
  129. package/src/routes/documents.ts +353 -4
  130. package/src/routes/editable-artifacts.ts +11 -3
  131. package/src/routes/enrollments.ts +116 -26
  132. package/src/routes/environments.ts +127 -55
  133. package/src/routes/files.ts +72 -11
  134. package/src/routes/install.ts +82 -28
  135. package/src/routes/integration-facets.ts +29 -0
  136. package/src/routes/interaction-resources.ts +20 -2
  137. package/src/routes/machines.ts +275 -13
  138. package/src/routes/organization-memberships.ts +717 -28
  139. package/src/routes/packs.ts +4 -3
  140. package/src/routes/personal-github-git-broker.ts +785 -0
  141. package/src/routes/personal-github.ts +319 -0
  142. package/src/routes/pr-review.ts +531 -0
  143. package/src/routes/rigs.ts +104 -39
  144. package/src/routes/scheduled-tasks.ts +19 -21
  145. package/src/routes/sessions.ts +775 -38
  146. package/src/routes/social.ts +14 -2
  147. package/src/routes/supergrok.ts +23 -4
  148. package/src/routes/transcription-recordings.ts +8 -2
  149. package/src/routes/user-resource-authorities.ts +138 -0
  150. package/src/routes/workspace-artifacts.ts +4 -1
  151. package/src/routes/workspace-capture.ts +21 -0
  152. package/src/routes/workspace-learning.ts +228 -0
  153. package/src/routes/workspaces.ts +59 -6
  154. package/src/sandbox/auth-callout.ts +38 -12
  155. package/src/sandbox/channel-a.ts +242 -19
  156. package/src/sandbox/connection-authority.ts +3 -0
  157. package/src/sandbox/enrollment.ts +15 -3
  158. package/src/sandbox/machines.ts +186 -64
  159. package/src/sandbox/metrics-ingestion.ts +220 -58
  160. package/src/sandbox/viewer.ts +243 -19
  161. package/src/sandbox-file-artifacts.ts +329 -0
  162. package/src/scheduled-task-deletion.ts +127 -0
  163. package/src/slack-reaction-files.ts +16 -5
  164. package/src/temporal-schedule-cleanup.ts +13 -0
  165. package/dist/chunk-WCXHI3FF.js.map +0 -1
@@ -0,0 +1,139 @@
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
+ }
@@ -39,6 +39,12 @@ import {
39
39
  GoogleDriveOAuthStartRequest,
40
40
  GoogleDriveOAuthStartResponse,
41
41
  } from "@opengeni/contracts/google-drive";
42
+ import {
43
+ PERSONAL_GITHUB_PROVIDER_DOMAIN,
44
+ PersonalGitHubDisconnectRequest,
45
+ hasReservedPersonalGitHubMetadata,
46
+ isPersonalGitHubConnection,
47
+ } from "@opengeni/contracts/personal-github";
42
48
  import {
43
49
  fikenConnectionMetadata,
44
50
  hasPermission,
@@ -84,8 +90,10 @@ import {
84
90
  } from "../integrations/google-drive";
85
91
  import {
86
92
  completeApiIntegrationProviderOAuth,
93
+ isApiIntegrationProviderOAuthState,
87
94
  startApiIntegrationProviderOAuth,
88
95
  } from "../integrations/provider-oauth";
96
+ import { disconnectPersonalGitHub } from "../integrations/personal-github";
89
97
  import {
90
98
  browseAtlassianSources,
91
99
  completeAtlassianOAuthCallback,
@@ -99,6 +107,10 @@ import {
99
107
  integrationBaseUrl,
100
108
  startMcpOAuth,
101
109
  } from "../integrations/oauth-client";
110
+ import {
111
+ assertPersonalConnectionOwnerPrincipal,
112
+ isPersonalConnectionOwnerPrincipal,
113
+ } from "../connection-ownership";
102
114
  import { canonicalProviderDomain } from "../integrations/provider-domain";
103
115
  import {
104
116
  exchangeOpenGeniSlackAuthorizationCode,
@@ -164,17 +176,23 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
164
176
 
165
177
  app.post("/v1/workspaces/:workspaceId/connections", async (c) => {
166
178
  const workspaceId = c.req.param("workspaceId");
167
- const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
179
+ const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
180
+ const grant = access.grant;
168
181
  const payload = CreateConnectionRequest.parse(await c.req.json());
169
182
  assertNotReservedSlackBotMetadata(payload.metadata);
170
183
  assertNotReservedFikenMetadata(payload.metadata);
171
184
  assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
185
+ assertNotReservedPersonalGitHubMetadata(payload.metadata);
172
186
  const key = requireEnvironmentEncryption(settings);
173
187
  const subjectId = createConnectionSubjectId(payload, grant.subjectId);
188
+ if (subjectId !== null) {
189
+ assertPersonalConnectionOwnerPrincipal(access);
190
+ }
174
191
  const providerDomain = canonicalProviderDomain(payload.providerDomain);
175
192
  assertNotDirectPersonalSlackOAuth(providerDomain, payload.kind);
176
193
  assertNotDirectGoogleDriveOAuth(providerDomain, payload.kind, payload.metadata);
177
194
  assertNotDirectAtlassianOAuth(providerDomain, payload.kind, payload.metadata);
195
+ assertNotDirectPersonalGitHubOAuth(providerDomain, payload.kind, payload.metadata);
178
196
  const connection = await createConnection(db, {
179
197
  accountId: grant.accountId,
180
198
  workspaceId,
@@ -271,7 +289,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
271
289
  }
272
290
  const slack = requireOpenGeniSlackOAuthSettings(settings);
273
291
  const redirectUri = `${baseUrl}/v1/integrations/slack/callback`;
274
- const token = await exchangeOpenGeniSlackAuthorizationCode(
292
+ const authorization = await exchangeOpenGeniSlackAuthorizationCode(
275
293
  {
276
294
  code,
277
295
  clientId: slack.clientId,
@@ -281,14 +299,22 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
281
299
  deps.slackFetch ?? fetch,
282
300
  );
283
301
  stage = "credential_verification";
284
- const verified = await verifyOpenGeniSlackBotCredential(token, deps.slackFetch ?? fetch);
302
+ const verified = await verifyOpenGeniSlackBotCredential(
303
+ authorization.accessToken,
304
+ {
305
+ appId: authorization.appId,
306
+ displayName: settings.slackBotDisplayName,
307
+ },
308
+ deps.slackFetch ?? fetch,
309
+ new Date(),
310
+ );
285
311
  stage = "permission_recheck";
286
312
  await requireSlackInstallCallbackGrant(db, state);
287
313
  stage = "persistence";
288
314
  const connection = await persistOpenGeniSlackBotConnection({
289
315
  deps,
290
316
  state,
291
- token,
317
+ token: authorization.accessToken,
292
318
  verified,
293
319
  });
294
320
  return c.redirect(
@@ -439,7 +465,11 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
439
465
  app.post("/v1/workspaces/:workspaceId/connections/google-drive/install", async (c) => {
440
466
  assertIntegrationsEnabled();
441
467
  const workspaceId = c.req.param("workspaceId");
442
- const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
468
+ // The Drive connector writes only a personal Connection, so only a managed
469
+ // human may start it.
470
+ const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
471
+ assertPersonalConnectionOwnerPrincipal(access, "Google Drive");
472
+ const grant = access.grant;
443
473
  const parsed = GoogleDriveOAuthStartRequest.safeParse(await c.req.json());
444
474
  if (!parsed.success) {
445
475
  throw new HTTPException(400, { message: "invalid Google Drive install request" });
@@ -460,7 +490,11 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
460
490
  app.post("/v1/workspaces/:workspaceId/connections/atlassian/install", async (c) => {
461
491
  assertIntegrationsEnabled();
462
492
  const workspaceId = c.req.param("workspaceId");
463
- const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
493
+ // The Atlassian connector writes only a personal Connection, so only a
494
+ // managed human may start it.
495
+ const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
496
+ assertPersonalConnectionOwnerPrincipal(access, "Atlassian");
497
+ const grant = access.grant;
464
498
  const parsed = AtlassianOAuthStartRequest.safeParse(await c.req.json());
465
499
  if (!parsed.success) {
466
500
  throw new HTTPException(400, { message: "invalid Atlassian install request" });
@@ -663,6 +697,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
663
697
  assertNotReservedSlackBotMetadata(payload.metadata);
664
698
  assertNotReservedFikenMetadata(payload.metadata);
665
699
  assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
700
+ assertNotReservedPersonalGitHubMetadata(payload.metadata);
666
701
  const existing = await getConnectionMetadata(
667
702
  db,
668
703
  workspaceId,
@@ -749,6 +784,11 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
749
784
  message: "use the dedicated Atlassian reconnect or source-selection flow",
750
785
  });
751
786
  }
787
+ if (existing && isPersonalGitHubConnection(existing)) {
788
+ throw new HTTPException(422, {
789
+ message: "use the dedicated personal GitHub reconnect flow to update this connection",
790
+ });
791
+ }
752
792
  if (existing) {
753
793
  const providerDomain = canonicalProviderDomain(
754
794
  payload.providerDomain ?? existing.providerDomain,
@@ -757,6 +797,11 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
757
797
  assertNotDirectPersonalSlackOAuth(providerDomain, kind);
758
798
  assertNotDirectGoogleDriveOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
759
799
  assertNotDirectAtlassianOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
800
+ assertNotDirectPersonalGitHubOAuth(
801
+ providerDomain,
802
+ kind,
803
+ payload.metadata ?? existing.metadata,
804
+ );
760
805
  }
761
806
  // Status is not a free-form field: revocation goes through DELETE, and the
762
807
  // broker owns needs_reauth/error. Reactivating a connection is only
@@ -813,6 +858,17 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
813
858
  if (!existing) {
814
859
  throw new HTTPException(404, { message: "connection not found" });
815
860
  }
861
+ const isPersonalGitHub =
862
+ existing.subjectId === grant.subjectId && isPersonalGitHubConnection(existing);
863
+ if (isPersonalGitHub) {
864
+ const access = await requireAccessGrantAuthorization(
865
+ c,
866
+ deps,
867
+ workspaceId,
868
+ "connections:write",
869
+ );
870
+ assertPersonalConnectionOwnerPrincipal(access, "My GitHub account");
871
+ }
816
872
  const isGoogleDrive =
817
873
  existing.subjectId === grant.subjectId &&
818
874
  existing.providerDomain === GOOGLE_DRIVE_PROVIDER_DOMAIN &&
@@ -843,7 +899,17 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
843
899
  atlassianDisconnect.error.issues[0]?.message ?? "invalid Atlassian disconnect request",
844
900
  });
845
901
  }
846
- if (existing.status === "revoked" && !isGoogleDrive && !isAtlassian) {
902
+ const personalGitHubDisconnect = isPersonalGitHub
903
+ ? PersonalGitHubDisconnectRequest.safeParse(disconnectPayload)
904
+ : null;
905
+ if (personalGitHubDisconnect && !personalGitHubDisconnect.success) {
906
+ throw new HTTPException(400, {
907
+ message:
908
+ personalGitHubDisconnect.error.issues[0]?.message ??
909
+ "invalid personal GitHub disconnect request",
910
+ });
911
+ }
912
+ if (existing.status === "revoked" && !isGoogleDrive && !isAtlassian && !isPersonalGitHub) {
847
913
  return c.json(ConnectionResponse.parse({ connection: existing }));
848
914
  }
849
915
  const connection = isGoogleDrive
@@ -860,24 +926,31 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
860
926
  connection: existing,
861
927
  payload: atlassianDisconnect!.data,
862
928
  })
863
- : isOpenGeniSlackBotConnection(existing)
864
- ? await revokeConnectionWithSlackBotSuccessAudit(db, {
865
- accountId: grant.accountId,
929
+ : isPersonalGitHub
930
+ ? await disconnectPersonalGitHub(deps, {
866
931
  workspaceId,
867
932
  subjectId: grant.subjectId,
868
933
  connectionId,
869
- expectedVersion: existing.version,
870
- credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
871
- credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
872
- slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
934
+ payload: personalGitHubDisconnect!.data,
873
935
  })
874
- : await revokeConnection(
875
- db,
876
- workspaceId,
877
- connectionId,
878
- grant.subjectId,
879
- existing.version,
880
- );
936
+ : isOpenGeniSlackBotConnection(existing)
937
+ ? await revokeConnectionWithSlackBotSuccessAudit(db, {
938
+ accountId: grant.accountId,
939
+ workspaceId,
940
+ subjectId: grant.subjectId,
941
+ connectionId,
942
+ expectedVersion: existing.version,
943
+ credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
944
+ credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
945
+ slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
946
+ })
947
+ : await revokeConnection(
948
+ db,
949
+ workspaceId,
950
+ connectionId,
951
+ grant.subjectId,
952
+ existing.version,
953
+ );
881
954
  if (!connection) {
882
955
  throw new HTTPException(409, { message: "connection changed during disconnect; try again" });
883
956
  }
@@ -887,7 +960,8 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
887
960
  app.post("/v1/workspaces/:workspaceId/connections/oauth/start", async (c) => {
888
961
  assertIntegrationsEnabled();
889
962
  const workspaceId = c.req.param("workspaceId");
890
- const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
963
+ const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
964
+ const grant = access.grant;
891
965
  const parsed = OAuthStartRequest.safeParse(await c.req.json());
892
966
  if (!parsed.success) {
893
967
  throw new HTTPException(400, {
@@ -901,6 +975,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
901
975
  accountId: grant.accountId,
902
976
  workspaceId,
903
977
  subjectId: grant.subjectId,
978
+ personalOwnershipAllowed: isPersonalConnectionOwnerPrincipal(access),
904
979
  requestUrl: c.req.url,
905
980
  payload,
906
981
  },
@@ -911,7 +986,8 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
911
986
  app.post("/v1/workspaces/:workspaceId/integrations/oauth/start", async (c) => {
912
987
  assertIntegrationsEnabled();
913
988
  const workspaceId = c.req.param("workspaceId");
914
- const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
989
+ const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
990
+ const grant = access.grant;
915
991
  const parsed = ApiIntegrationOAuthStartRequest.safeParse(await c.req.json());
916
992
  if (!parsed.success) {
917
993
  throw new HTTPException(400, {
@@ -924,6 +1000,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
924
1000
  accountId: grant.accountId,
925
1001
  workspaceId,
926
1002
  subjectId: grant.subjectId,
1003
+ personalOwnershipAllowed: isPersonalConnectionOwnerPrincipal(access),
927
1004
  requestUrl: c.req.url,
928
1005
  payload: parsed.data,
929
1006
  }),
@@ -933,15 +1010,22 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
933
1010
 
934
1011
  app.get("/v1/integrations/oauth/callback", async (c) => {
935
1012
  assertIntegrationsEnabled();
936
- const result = await completeMcpOAuthCallback(
937
- { db, settings, observability, oauthCallbackDeadlineMs: deps.oauthCallbackDeadlineMs },
938
- {
939
- code: c.req.query("code"),
940
- state: c.req.query("state"),
941
- requestUrl: c.req.url,
942
- },
943
- );
944
- return c.redirect(result.redirectTo, 302);
1013
+ const input = {
1014
+ ...(c.req.query("code") ? { code: c.req.query("code") } : {}),
1015
+ ...(c.req.query("state") ? { state: c.req.query("state") } : {}),
1016
+ ...(c.req.query("error") ? { error: c.req.query("error") } : {}),
1017
+ requestUrl: c.req.url,
1018
+ };
1019
+ const result = isApiIntegrationProviderOAuthState(input.state, deps.settings)
1020
+ ? await completeApiIntegrationProviderOAuth(deps, input)
1021
+ : await completeMcpOAuthCallback(
1022
+ { db, settings, observability, oauthCallbackDeadlineMs: deps.oauthCallbackDeadlineMs },
1023
+ input,
1024
+ );
1025
+ const redirectTo = settings.webBaseUrl
1026
+ ? new URL(result.redirectTo, settings.webBaseUrl).toString()
1027
+ : result.redirectTo;
1028
+ return c.redirect(redirectTo, 302);
945
1029
  });
946
1030
 
947
1031
  app.get("/v1/integrations/provider-oauth/callback", async (c) => {
@@ -1074,10 +1158,11 @@ function requireOpenGeniSlackOAuthSettings(settings: ApiRouteDeps["settings"]):
1074
1158
  } {
1075
1159
  const clientId = settings.slackClientId?.trim();
1076
1160
  const clientSecret = settings.slackClientSecret?.trim();
1077
- if (!clientId || !clientSecret) {
1161
+ const signingSecret = settings.slackSigningSecret?.trim();
1162
+ if (!clientId || !clientSecret || !signingSecret) {
1078
1163
  throw new HTTPException(503, {
1079
1164
  message:
1080
- "OpenGeni Slack installation requires OPENGENI_SLACK_CLIENT_ID and OPENGENI_SLACK_CLIENT_SECRET",
1165
+ "OpenGeni Slack bot installation requires OPENGENI_SLACK_CLIENT_ID, OPENGENI_SLACK_CLIENT_SECRET, and OPENGENI_SLACK_SIGNING_SECRET",
1081
1166
  });
1082
1167
  }
1083
1168
  return { clientId, clientSecret };
@@ -1257,6 +1342,31 @@ function assertNotDirectAtlassianOAuth(
1257
1342
  }
1258
1343
  }
1259
1344
 
1345
+ function assertNotDirectPersonalGitHubOAuth(
1346
+ providerDomain: string,
1347
+ kind: string,
1348
+ metadata: Record<string, unknown> | undefined,
1349
+ ): void {
1350
+ if (
1351
+ (providerDomain === PERSONAL_GITHUB_PROVIDER_DOMAIN && kind === "oauth2") ||
1352
+ hasReservedPersonalGitHubMetadata(metadata)
1353
+ ) {
1354
+ throw new HTTPException(422, {
1355
+ message: "personal GitHub credentials must use the dedicated OAuth connection flow",
1356
+ });
1357
+ }
1358
+ }
1359
+
1360
+ function assertNotReservedPersonalGitHubMetadata(
1361
+ metadata: Record<string, unknown> | undefined,
1362
+ ): void {
1363
+ if (hasReservedPersonalGitHubMetadata(metadata)) {
1364
+ throw new HTTPException(422, {
1365
+ message: "personal GitHub metadata is reserved for the dedicated OAuth connection flow",
1366
+ });
1367
+ }
1368
+ }
1369
+
1260
1370
  function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | undefined): void {
1261
1371
  if (hasReservedOpenGeniSlackBotMetadata(metadata)) {
1262
1372
  throw new HTTPException(422, {