@opengeni/api-router 0.30.1 → 2.1.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-2PQMSRCB.js → chunk-QKDFBBUE.js} +29110 -18137
- package/dist/chunk-QKDFBBUE.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-office-import.d.ts +7 -4
- 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 +364 -86
- 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 +19 -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 +149 -9
- package/dist/integrations/slack-interactions.d.ts +10 -4
- 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 +17 -3
- 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/files.d.ts +4 -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 +160 -27
- 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 +77 -9
- 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 +416 -101
- 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 +337 -57
- package/src/integrations/slack-interactions.ts +1274 -214
- package/src/integrations/slack-routing.ts +324 -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 +22 -2
- 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 +388 -98
- 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 +145 -34
- package/src/routes/documents.ts +221 -3
- 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 +152 -27
- 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 +777 -40
- 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-2PQMSRCB.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" });
|
|
@@ -557,6 +591,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
557
591
|
...(c.req.query("code") ? { code: c.req.query("code") } : {}),
|
|
558
592
|
...(c.req.query("state") ? { state: c.req.query("state") } : {}),
|
|
559
593
|
...(c.req.query("error") ? { error: c.req.query("error") } : {}),
|
|
594
|
+
...(c.req.query("picked_file_ids") ? { pickedFileIds: c.req.query("picked_file_ids") } : {}),
|
|
560
595
|
requestUrl: c.req.url,
|
|
561
596
|
};
|
|
562
597
|
const result = await completeGoogleDriveOAuthCallback(deps, input);
|
|
@@ -662,6 +697,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
662
697
|
assertNotReservedSlackBotMetadata(payload.metadata);
|
|
663
698
|
assertNotReservedFikenMetadata(payload.metadata);
|
|
664
699
|
assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
|
|
700
|
+
assertNotReservedPersonalGitHubMetadata(payload.metadata);
|
|
665
701
|
const existing = await getConnectionMetadata(
|
|
666
702
|
db,
|
|
667
703
|
workspaceId,
|
|
@@ -748,6 +784,11 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
748
784
|
message: "use the dedicated Atlassian reconnect or source-selection flow",
|
|
749
785
|
});
|
|
750
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
|
+
}
|
|
751
792
|
if (existing) {
|
|
752
793
|
const providerDomain = canonicalProviderDomain(
|
|
753
794
|
payload.providerDomain ?? existing.providerDomain,
|
|
@@ -756,6 +797,11 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
756
797
|
assertNotDirectPersonalSlackOAuth(providerDomain, kind);
|
|
757
798
|
assertNotDirectGoogleDriveOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
|
|
758
799
|
assertNotDirectAtlassianOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
|
|
800
|
+
assertNotDirectPersonalGitHubOAuth(
|
|
801
|
+
providerDomain,
|
|
802
|
+
kind,
|
|
803
|
+
payload.metadata ?? existing.metadata,
|
|
804
|
+
);
|
|
759
805
|
}
|
|
760
806
|
// Status is not a free-form field: revocation goes through DELETE, and the
|
|
761
807
|
// broker owns needs_reauth/error. Reactivating a connection is only
|
|
@@ -812,6 +858,17 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
812
858
|
if (!existing) {
|
|
813
859
|
throw new HTTPException(404, { message: "connection not found" });
|
|
814
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
|
+
}
|
|
815
872
|
const isGoogleDrive =
|
|
816
873
|
existing.subjectId === grant.subjectId &&
|
|
817
874
|
existing.providerDomain === GOOGLE_DRIVE_PROVIDER_DOMAIN &&
|
|
@@ -842,7 +899,17 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
842
899
|
atlassianDisconnect.error.issues[0]?.message ?? "invalid Atlassian disconnect request",
|
|
843
900
|
});
|
|
844
901
|
}
|
|
845
|
-
|
|
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) {
|
|
846
913
|
return c.json(ConnectionResponse.parse({ connection: existing }));
|
|
847
914
|
}
|
|
848
915
|
const connection = isGoogleDrive
|
|
@@ -859,24 +926,31 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
859
926
|
connection: existing,
|
|
860
927
|
payload: atlassianDisconnect!.data,
|
|
861
928
|
})
|
|
862
|
-
:
|
|
863
|
-
? await
|
|
864
|
-
accountId: grant.accountId,
|
|
929
|
+
: isPersonalGitHub
|
|
930
|
+
? await disconnectPersonalGitHub(deps, {
|
|
865
931
|
workspaceId,
|
|
866
932
|
subjectId: grant.subjectId,
|
|
867
933
|
connectionId,
|
|
868
|
-
|
|
869
|
-
credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
870
|
-
credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
|
|
871
|
-
slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
|
|
934
|
+
payload: personalGitHubDisconnect!.data,
|
|
872
935
|
})
|
|
873
|
-
:
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
+
);
|
|
880
954
|
if (!connection) {
|
|
881
955
|
throw new HTTPException(409, { message: "connection changed during disconnect; try again" });
|
|
882
956
|
}
|
|
@@ -886,7 +960,8 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
886
960
|
app.post("/v1/workspaces/:workspaceId/connections/oauth/start", async (c) => {
|
|
887
961
|
assertIntegrationsEnabled();
|
|
888
962
|
const workspaceId = c.req.param("workspaceId");
|
|
889
|
-
const
|
|
963
|
+
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
|
|
964
|
+
const grant = access.grant;
|
|
890
965
|
const parsed = OAuthStartRequest.safeParse(await c.req.json());
|
|
891
966
|
if (!parsed.success) {
|
|
892
967
|
throw new HTTPException(400, {
|
|
@@ -900,6 +975,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
900
975
|
accountId: grant.accountId,
|
|
901
976
|
workspaceId,
|
|
902
977
|
subjectId: grant.subjectId,
|
|
978
|
+
personalOwnershipAllowed: isPersonalConnectionOwnerPrincipal(access),
|
|
903
979
|
requestUrl: c.req.url,
|
|
904
980
|
payload,
|
|
905
981
|
},
|
|
@@ -910,7 +986,8 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
910
986
|
app.post("/v1/workspaces/:workspaceId/integrations/oauth/start", async (c) => {
|
|
911
987
|
assertIntegrationsEnabled();
|
|
912
988
|
const workspaceId = c.req.param("workspaceId");
|
|
913
|
-
const
|
|
989
|
+
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
|
|
990
|
+
const grant = access.grant;
|
|
914
991
|
const parsed = ApiIntegrationOAuthStartRequest.safeParse(await c.req.json());
|
|
915
992
|
if (!parsed.success) {
|
|
916
993
|
throw new HTTPException(400, {
|
|
@@ -923,6 +1000,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
923
1000
|
accountId: grant.accountId,
|
|
924
1001
|
workspaceId,
|
|
925
1002
|
subjectId: grant.subjectId,
|
|
1003
|
+
personalOwnershipAllowed: isPersonalConnectionOwnerPrincipal(access),
|
|
926
1004
|
requestUrl: c.req.url,
|
|
927
1005
|
payload: parsed.data,
|
|
928
1006
|
}),
|
|
@@ -932,15 +1010,22 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
932
1010
|
|
|
933
1011
|
app.get("/v1/integrations/oauth/callback", async (c) => {
|
|
934
1012
|
assertIntegrationsEnabled();
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
{
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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);
|
|
944
1029
|
});
|
|
945
1030
|
|
|
946
1031
|
app.get("/v1/integrations/provider-oauth/callback", async (c) => {
|
|
@@ -1073,10 +1158,11 @@ function requireOpenGeniSlackOAuthSettings(settings: ApiRouteDeps["settings"]):
|
|
|
1073
1158
|
} {
|
|
1074
1159
|
const clientId = settings.slackClientId?.trim();
|
|
1075
1160
|
const clientSecret = settings.slackClientSecret?.trim();
|
|
1076
|
-
|
|
1161
|
+
const signingSecret = settings.slackSigningSecret?.trim();
|
|
1162
|
+
if (!clientId || !clientSecret || !signingSecret) {
|
|
1077
1163
|
throw new HTTPException(503, {
|
|
1078
1164
|
message:
|
|
1079
|
-
"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",
|
|
1080
1166
|
});
|
|
1081
1167
|
}
|
|
1082
1168
|
return { clientId, clientSecret };
|
|
@@ -1256,6 +1342,31 @@ function assertNotDirectAtlassianOAuth(
|
|
|
1256
1342
|
}
|
|
1257
1343
|
}
|
|
1258
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
|
+
|
|
1259
1370
|
function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | undefined): void {
|
|
1260
1371
|
if (hasReservedOpenGeniSlackBotMetadata(metadata)) {
|
|
1261
1372
|
throw new HTTPException(422, {
|