@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.
- package/dist/api-websocket.d.ts +18 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +6 -0
- package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
- package/dist/chunk-3TP54PPX.js.map +1 -0
- package/dist/codemode.d.ts +8 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-authority-owner.d.ts +59 -0
- package/dist/connection-ownership.d.ts +47 -0
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/editable-artifact-websocket.d.ts +5 -15
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/http/api-error.d.ts +10 -0
- package/dist/http/cors.d.ts +5 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/sse.d.ts +2 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +326 -82
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +17 -0
- package/dist/integrations/fiken.d.ts +7 -2
- package/dist/integrations/google-drive.d.ts +18 -0
- package/dist/integrations/oauth-client.d.ts +22 -3
- package/dist/integrations/oauth-profiles.d.ts +195 -0
- package/dist/integrations/personal-github-repositories.d.ts +45 -0
- package/dist/integrations/personal-github.d.ts +40 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +8 -0
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +157 -9
- package/dist/integrations/slack-interactions.d.ts +39 -5
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/social-oauth.d.ts +5 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/documents.d.ts +1 -0
- package/dist/mcp/remember.d.ts +28 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/mcp/server.d.ts +15 -2
- package/dist/mcp/session-view.d.ts +4 -0
- package/dist/mcp/session-wait.d.ts +137 -0
- package/dist/routes/automations.d.ts +4 -0
- package/dist/routes/billing.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +4 -1
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/connection-authorities.d.ts +3 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/sessions.d.ts +1 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +19 -0
- package/dist/sandbox/channel-a.d.ts +11 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +2 -0
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +10 -20
- package/dist/sandbox/viewer.d.ts +24 -1
- package/dist/sandbox-file-artifacts.d.ts +11 -0
- package/dist/scheduled-task-deletion.d.ts +14 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/package.json +20 -18
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +161 -23
- package/src/auth/managed-auth.ts +25 -3
- package/src/codemode.ts +53 -23
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-authority-owner.ts +61 -0
- package/src/connection-ownership.ts +180 -0
- package/src/controller-data-plane.ts +47 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +28 -3
- package/src/editable-artifact-production.ts +13 -7
- package/src/editable-artifact-websocket.ts +11 -18
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/http/api-error.ts +28 -0
- package/src/http/auth.ts +4 -0
- package/src/http/cors.ts +35 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/sse.ts +112 -66
- package/src/index.ts +46 -6
- package/src/integrations/atlassian.ts +146 -35
- package/src/integrations/fiken.ts +102 -22
- package/src/integrations/google-drive.ts +301 -92
- package/src/integrations/oauth-client.ts +272 -143
- package/src/integrations/oauth-profiles.ts +477 -0
- package/src/integrations/personal-github-repositories.ts +445 -0
- package/src/integrations/personal-github.ts +705 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +121 -5
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +342 -57
- package/src/integrations/slack-interactions.ts +1898 -259
- package/src/integrations/slack-routing.ts +337 -0
- package/src/integrations/social-oauth.ts +25 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/mcp/company-brain-governed-writes.ts +262 -0
- package/src/mcp/company-profile-agent-admin.ts +205 -0
- package/src/mcp/documents.ts +37 -6
- package/src/mcp/files.ts +16 -1
- package/src/mcp/remember.ts +181 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-view.ts +1 -0
- package/src/mcp/server.ts +719 -369
- package/src/mcp/session-view.ts +54 -0
- package/src/mcp/session-wait.ts +554 -0
- package/src/model-catalog.ts +20 -12
- package/src/routes/api-integrations.ts +4 -0
- package/src/routes/api-keys.ts +1 -0
- package/src/routes/automations.ts +534 -0
- package/src/routes/billing.ts +57 -1
- package/src/routes/browser-sessions.ts +329 -79
- package/src/routes/channels.ts +17 -1
- package/src/routes/codex.ts +50 -0
- package/src/routes/company-brain.ts +367 -0
- package/src/routes/company-profile.ts +1 -1
- package/src/routes/computer-sessions.ts +320 -93
- package/src/routes/connection-authorities.ts +139 -0
- package/src/routes/connections.ts +144 -34
- package/src/routes/documents.ts +353 -4
- package/src/routes/editable-artifacts.ts +11 -3
- package/src/routes/enrollments.ts +116 -26
- package/src/routes/environments.ts +127 -55
- package/src/routes/files.ts +72 -11
- package/src/routes/install.ts +82 -28
- package/src/routes/integration-facets.ts +29 -0
- package/src/routes/interaction-resources.ts +20 -2
- package/src/routes/machines.ts +275 -13
- package/src/routes/organization-memberships.ts +717 -28
- package/src/routes/packs.ts +4 -3
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +319 -0
- package/src/routes/pr-review.ts +531 -0
- package/src/routes/rigs.ts +104 -39
- package/src/routes/scheduled-tasks.ts +19 -21
- package/src/routes/sessions.ts +775 -38
- package/src/routes/social.ts +14 -2
- package/src/routes/supergrok.ts +23 -4
- package/src/routes/transcription-recordings.ts +8 -2
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/workspace-artifacts.ts +4 -1
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-learning.ts +228 -0
- package/src/routes/workspaces.ts +59 -6
- package/src/sandbox/auth-callout.ts +38 -12
- package/src/sandbox/channel-a.ts +242 -19
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +15 -3
- package/src/sandbox/machines.ts +186 -64
- package/src/sandbox/metrics-ingestion.ts +220 -58
- package/src/sandbox/viewer.ts +243 -19
- package/src/sandbox-file-artifacts.ts +329 -0
- package/src/scheduled-task-deletion.ts +127 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- 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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
864
|
-
? await
|
|
865
|
-
accountId: grant.accountId,
|
|
929
|
+
: isPersonalGitHub
|
|
930
|
+
? await disconnectPersonalGitHub(deps, {
|
|
866
931
|
workspaceId,
|
|
867
932
|
subjectId: grant.subjectId,
|
|
868
933
|
connectionId,
|
|
869
|
-
|
|
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
|
-
:
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
|
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
|
|
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
|
|
937
|
-
|
|
938
|
-
{
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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
|
-
|
|
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
|
|
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, {
|