@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,246 @@
|
|
|
1
|
+
import type { PrReviewProvider } from "@opengeni/contracts";
|
|
2
|
+
import {
|
|
3
|
+
pinnedFetch,
|
|
4
|
+
readResponseJsonBounded,
|
|
5
|
+
readResponseTextBounded,
|
|
6
|
+
type FetchLike,
|
|
7
|
+
type OutboundNetworkSettings,
|
|
8
|
+
} from "@opengeni/network";
|
|
9
|
+
|
|
10
|
+
const providerVerificationTimeoutMs = 15_000;
|
|
11
|
+
const providerVerificationResponseMaxBytes = 256 * 1024;
|
|
12
|
+
const providerVerificationErrorMaxBytes = 8 * 1024;
|
|
13
|
+
|
|
14
|
+
export class PrReviewProviderRepositoryError extends Error {
|
|
15
|
+
constructor(
|
|
16
|
+
readonly reason: "denied" | "identity_mismatch" | "unavailable",
|
|
17
|
+
message: string,
|
|
18
|
+
) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "PrReviewProviderRepositoryError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type VerifiedPrReviewProviderRepository = {
|
|
25
|
+
repositoryUri: string;
|
|
26
|
+
repositoryFullName: string;
|
|
27
|
+
providerRepositoryId: string;
|
|
28
|
+
projectId: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export async function verifyPrReviewProviderRepository(input: {
|
|
32
|
+
provider: Exclude<PrReviewProvider, "github">;
|
|
33
|
+
providerBaseUrl: string;
|
|
34
|
+
providerRepositoryId: string;
|
|
35
|
+
projectId?: string;
|
|
36
|
+
token: string;
|
|
37
|
+
username: string | null;
|
|
38
|
+
settings: OutboundNetworkSettings;
|
|
39
|
+
fetchImpl?: FetchLike;
|
|
40
|
+
}): Promise<VerifiedPrReviewProviderRepository> {
|
|
41
|
+
return input.provider === "gitlab"
|
|
42
|
+
? await verifyGitLabRepository(input)
|
|
43
|
+
: await verifyAzureDevOpsRepository(input);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function verifyGitLabRepository(
|
|
47
|
+
input: Parameters<typeof verifyPrReviewProviderRepository>[0],
|
|
48
|
+
): Promise<VerifiedPrReviewProviderRepository> {
|
|
49
|
+
if (!/^\d+$/.test(input.providerRepositoryId)) {
|
|
50
|
+
throw new PrReviewProviderRepositoryError(
|
|
51
|
+
"identity_mismatch",
|
|
52
|
+
"GitLab repository bindings require the numeric project ID",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const url = providerApiUrl(
|
|
56
|
+
input.providerBaseUrl,
|
|
57
|
+
`api/v4/projects/${encodeURIComponent(input.providerRepositoryId)}`,
|
|
58
|
+
);
|
|
59
|
+
const payload = await providerJson(input, url, {
|
|
60
|
+
authorization: `Bearer ${input.token}`,
|
|
61
|
+
});
|
|
62
|
+
const id = scalarId(payload.id);
|
|
63
|
+
const fullName = nonEmptyText(payload.path_with_namespace);
|
|
64
|
+
const repositoryUri = credentialFreeHttps(payload.http_url_to_repo, input.providerBaseUrl);
|
|
65
|
+
if (id !== input.providerRepositoryId || !fullName || !repositoryUri) {
|
|
66
|
+
throw new PrReviewProviderRepositoryError(
|
|
67
|
+
"identity_mismatch",
|
|
68
|
+
"GitLab returned a different or incomplete project identity",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
repositoryUri,
|
|
73
|
+
repositoryFullName: fullName,
|
|
74
|
+
providerRepositoryId: id,
|
|
75
|
+
projectId: id,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function verifyAzureDevOpsRepository(
|
|
80
|
+
input: Parameters<typeof verifyPrReviewProviderRepository>[0],
|
|
81
|
+
): Promise<VerifiedPrReviewProviderRepository> {
|
|
82
|
+
if (!input.projectId) {
|
|
83
|
+
throw new PrReviewProviderRepositoryError(
|
|
84
|
+
"identity_mismatch",
|
|
85
|
+
"Azure DevOps repository bindings require the exact project ID",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const url = providerApiUrl(
|
|
89
|
+
input.providerBaseUrl,
|
|
90
|
+
`${encodeURIComponent(input.projectId)}/_apis/git/repositories/${encodeURIComponent(input.providerRepositoryId)}`,
|
|
91
|
+
);
|
|
92
|
+
url.searchParams.set("api-version", "7.1");
|
|
93
|
+
const authorization = Buffer.from(`${input.username ?? "pr-review"}:${input.token}`).toString(
|
|
94
|
+
"base64",
|
|
95
|
+
);
|
|
96
|
+
const payload = await providerJson(input, url, {
|
|
97
|
+
authorization: `Basic ${authorization}`,
|
|
98
|
+
});
|
|
99
|
+
const project = record(payload.project);
|
|
100
|
+
const id = scalarId(payload.id);
|
|
101
|
+
const projectId = scalarId(project?.id);
|
|
102
|
+
const repositoryName = nonEmptyText(payload.name);
|
|
103
|
+
const projectName = nonEmptyText(project?.name);
|
|
104
|
+
const repositoryUri = credentialFreeHttps(payload.remoteUrl, input.providerBaseUrl);
|
|
105
|
+
if (
|
|
106
|
+
!id ||
|
|
107
|
+
id.toLowerCase() !== input.providerRepositoryId.toLowerCase() ||
|
|
108
|
+
!projectId ||
|
|
109
|
+
projectId.toLowerCase() !== input.projectId.toLowerCase() ||
|
|
110
|
+
!repositoryName ||
|
|
111
|
+
!repositoryUri
|
|
112
|
+
) {
|
|
113
|
+
throw new PrReviewProviderRepositoryError(
|
|
114
|
+
"identity_mismatch",
|
|
115
|
+
"Azure DevOps returned a different or incomplete repository identity",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
repositoryUri,
|
|
120
|
+
repositoryFullName: projectName ? `${projectName}/${repositoryName}` : repositoryName,
|
|
121
|
+
providerRepositoryId: id,
|
|
122
|
+
projectId,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function providerJson(
|
|
127
|
+
input: Parameters<typeof verifyPrReviewProviderRepository>[0],
|
|
128
|
+
url: URL,
|
|
129
|
+
headers: Record<string, string>,
|
|
130
|
+
): Promise<Record<string, unknown>> {
|
|
131
|
+
const signal = AbortSignal.timeout(providerVerificationTimeoutMs);
|
|
132
|
+
let response: Response;
|
|
133
|
+
try {
|
|
134
|
+
const requestInit = {
|
|
135
|
+
headers: { accept: "application/json", ...headers },
|
|
136
|
+
signal,
|
|
137
|
+
} satisfies RequestInit;
|
|
138
|
+
response = input.fetchImpl
|
|
139
|
+
? await input.fetchImpl(url, requestInit)
|
|
140
|
+
: await pinnedFetch(url, requestInit, input.settings, {
|
|
141
|
+
label: `${input.provider} PR Review repository verification`,
|
|
142
|
+
requireHttpsOutsideLocalTest: true,
|
|
143
|
+
});
|
|
144
|
+
} catch {
|
|
145
|
+
throw new PrReviewProviderRepositoryError(
|
|
146
|
+
"unavailable",
|
|
147
|
+
`${input.provider} repository verification is unavailable`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (response.status === 401 || response.status === 403 || response.status === 404) {
|
|
151
|
+
await readResponseTextBounded(
|
|
152
|
+
response,
|
|
153
|
+
providerVerificationErrorMaxBytes,
|
|
154
|
+
`${input.provider} repository verification error`,
|
|
155
|
+
{ signal },
|
|
156
|
+
).catch(() => undefined);
|
|
157
|
+
throw new PrReviewProviderRepositoryError(
|
|
158
|
+
"denied",
|
|
159
|
+
`${input.provider} credential cannot access the requested repository`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
await readResponseTextBounded(
|
|
164
|
+
response,
|
|
165
|
+
providerVerificationErrorMaxBytes,
|
|
166
|
+
`${input.provider} repository verification error`,
|
|
167
|
+
{ signal },
|
|
168
|
+
).catch(() => undefined);
|
|
169
|
+
throw new PrReviewProviderRepositoryError(
|
|
170
|
+
"unavailable",
|
|
171
|
+
`${input.provider} repository verification failed`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
let payload: unknown;
|
|
175
|
+
try {
|
|
176
|
+
payload = await readResponseJsonBounded(
|
|
177
|
+
response,
|
|
178
|
+
providerVerificationResponseMaxBytes,
|
|
179
|
+
`${input.provider} repository metadata`,
|
|
180
|
+
{ signal },
|
|
181
|
+
);
|
|
182
|
+
} catch {
|
|
183
|
+
throw new PrReviewProviderRepositoryError(
|
|
184
|
+
"unavailable",
|
|
185
|
+
`${input.provider} returned invalid repository metadata`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const parsed = record(payload);
|
|
189
|
+
if (!parsed) {
|
|
190
|
+
throw new PrReviewProviderRepositoryError(
|
|
191
|
+
"unavailable",
|
|
192
|
+
`${input.provider} returned invalid repository metadata`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return parsed;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function providerApiUrl(base: string, suffix: string): URL {
|
|
199
|
+
const url = new URL(base);
|
|
200
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/${suffix}`;
|
|
201
|
+
url.search = "";
|
|
202
|
+
url.hash = "";
|
|
203
|
+
return url;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function credentialFreeHttps(value: unknown, providerBaseUrl: string): string | null {
|
|
207
|
+
if (typeof value !== "string") return null;
|
|
208
|
+
let candidate: URL;
|
|
209
|
+
const provider = new URL(providerBaseUrl);
|
|
210
|
+
try {
|
|
211
|
+
candidate = new URL(value);
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
const providerPath = provider.pathname.replace(/\/+$/, "");
|
|
216
|
+
if (
|
|
217
|
+
candidate.protocol !== "https:" ||
|
|
218
|
+
candidate.username ||
|
|
219
|
+
candidate.password ||
|
|
220
|
+
candidate.href.length > 2048 ||
|
|
221
|
+
candidate.host.toLowerCase() !== provider.host.toLowerCase() ||
|
|
222
|
+
(providerPath !== "" &&
|
|
223
|
+
providerPath !== "/" &&
|
|
224
|
+
candidate.pathname !== providerPath &&
|
|
225
|
+
!candidate.pathname.startsWith(`${providerPath}/`))
|
|
226
|
+
) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
return candidate.href;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function record(value: unknown): Record<string, unknown> | null {
|
|
233
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
234
|
+
? (value as Record<string, unknown>)
|
|
235
|
+
: null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function scalarId(value: unknown): string | null {
|
|
239
|
+
if (typeof value === "string" && value.length > 0 && value.length <= 512) return value;
|
|
240
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return String(value);
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function nonEmptyText(value: unknown): string | null {
|
|
245
|
+
return typeof value === "string" && value.length > 0 && value.length <= 1024 ? value : null;
|
|
246
|
+
}
|
|
@@ -37,11 +37,23 @@ import {
|
|
|
37
37
|
} from "@opengeni/network";
|
|
38
38
|
import { HTTPException } from "hono/http-exception";
|
|
39
39
|
|
|
40
|
+
import {
|
|
41
|
+
assertConnectionOwnershipAllowedForPrincipal,
|
|
42
|
+
personalOwnerStateAccepted,
|
|
43
|
+
personalOwnerVerifiedInState,
|
|
44
|
+
PERSONAL_OWNER_VERIFIED_STATE_CLAIM,
|
|
45
|
+
} from "../connection-ownership";
|
|
40
46
|
import {
|
|
41
47
|
integrationBaseUrl,
|
|
42
48
|
oauthStateTtlMs,
|
|
43
49
|
requireIntegrationsStateSecret,
|
|
44
50
|
} from "./oauth-client";
|
|
51
|
+
import {
|
|
52
|
+
assertOwnershipAllowed,
|
|
53
|
+
builtInOAuthProfileFor,
|
|
54
|
+
DEFAULT_OAUTH_PROFILE,
|
|
55
|
+
type OAuthProviderProfile,
|
|
56
|
+
} from "./oauth-profiles";
|
|
45
57
|
|
|
46
58
|
const PROVIDER_OAUTH_CALLBACK_PATH = "/v1/integrations/provider-oauth/callback";
|
|
47
59
|
const PROVIDER_OAUTH_TIMEOUT_MS = 15_000;
|
|
@@ -60,6 +72,7 @@ type ProviderOAuthState = {
|
|
|
60
72
|
workspaceId: string;
|
|
61
73
|
subjectId: string;
|
|
62
74
|
ownership: ConnectionOwnership;
|
|
75
|
+
personalOwnerVerified: boolean;
|
|
63
76
|
definitionId: string;
|
|
64
77
|
definitionFingerprint: string;
|
|
65
78
|
providerDomain: string;
|
|
@@ -117,6 +130,12 @@ export async function startApiIntegrationProviderOAuth(
|
|
|
117
130
|
accountId: string;
|
|
118
131
|
workspaceId: string;
|
|
119
132
|
subjectId: string;
|
|
133
|
+
/**
|
|
134
|
+
* False for every principal that cannot own a personal Connection (API
|
|
135
|
+
* keys, the configured key, services, agent attempts). Resolved by the
|
|
136
|
+
* route from the live authenticated principal, never inferred here.
|
|
137
|
+
*/
|
|
138
|
+
personalOwnershipAllowed: boolean;
|
|
120
139
|
requestUrl: string;
|
|
121
140
|
payload: ApiIntegrationOAuthStartRequest;
|
|
122
141
|
},
|
|
@@ -155,7 +174,33 @@ export async function startApiIntegrationProviderOAuth(
|
|
|
155
174
|
message: "The selected Connection ownership does not match this OAuth request",
|
|
156
175
|
});
|
|
157
176
|
}
|
|
158
|
-
|
|
177
|
+
// This flow used to resolve an omitted ownership to `personal`, inverting the
|
|
178
|
+
// documented workspace-owned default. Resolving it to `workspace` instead
|
|
179
|
+
// would have been the opposite defect: an executed probe on
|
|
180
|
+
// `microsoft-outlook-mail` confirmed it flips a newly connected mailbox from
|
|
181
|
+
// subject-scoped to workspace-shared for API/SDK callers, which is a real
|
|
182
|
+
// narrow -> broad widening. So an ambiguous omission is refused outright and
|
|
183
|
+
// the caller must choose. A profile that allows exactly one ownership is not
|
|
184
|
+
// ambiguous (Gmail and hosted Slack MCP are personal-only), and a reconnect
|
|
185
|
+
// takes the existing row's ownership.
|
|
186
|
+
const profile = providerOAuthProfile(definition, providerDomain);
|
|
187
|
+
const ownership =
|
|
188
|
+
existingOwnership ?? input.payload.ownership ?? soleAllowedOwnership(profile) ?? null;
|
|
189
|
+
if (ownership === null) {
|
|
190
|
+
throw new HTTPException(422, {
|
|
191
|
+
message:
|
|
192
|
+
'ownership is required: choose "workspace" to share this Connection with the workspace, ' +
|
|
193
|
+
'or "personal" to connect only for yourself',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
// Inert by construction today, and deliberately kept: no curated Definition
|
|
197
|
+
// resolves to a personal-only profile (all five are Google Workspace and
|
|
198
|
+
// Microsoft Graph APIs, so `providerOAuthProfile` returns the default), which
|
|
199
|
+
// is why disabling this line reddens nothing. It is the fence that keeps a
|
|
200
|
+
// future Definition targeting slack.com or Gmail from minting an ownership
|
|
201
|
+
// its profile forbids - read it as pre-wiring, not as an untested gap.
|
|
202
|
+
assertOwnershipAllowed(profile, ownership);
|
|
203
|
+
assertConnectionOwnershipAllowedForPrincipal(ownership, input.personalOwnershipAllowed);
|
|
159
204
|
const authorizeScopes = uniqueStrings([
|
|
160
205
|
...(existing?.grantedScopes ?? []),
|
|
161
206
|
...definition.authentication.scopes,
|
|
@@ -164,7 +209,7 @@ export async function startApiIntegrationProviderOAuth(
|
|
|
164
209
|
const verifier = randomBytes(48).toString("base64url");
|
|
165
210
|
const key = requireEnvironmentEncryption(deps.settings);
|
|
166
211
|
const baseUrl = integrationBaseUrl(deps.settings.publicBaseUrl, input.requestUrl);
|
|
167
|
-
const redirectUri = `${baseUrl}${
|
|
212
|
+
const redirectUri = `${baseUrl}${providerOAuthCallbackPath(definition)}`;
|
|
168
213
|
const returnPath = safeReturnPath(
|
|
169
214
|
input.payload.returnPath ?? `/workspaces/${input.workspaceId}/capabilities`,
|
|
170
215
|
);
|
|
@@ -173,6 +218,9 @@ export async function startApiIntegrationProviderOAuth(
|
|
|
173
218
|
workspaceId: input.workspaceId,
|
|
174
219
|
subjectId: input.subjectId,
|
|
175
220
|
ownership,
|
|
221
|
+
// Signed record that a live principal was checked; the callback has no
|
|
222
|
+
// principal of its own and enforces exactly this decision.
|
|
223
|
+
[PERSONAL_OWNER_VERIFIED_STATE_CLAIM]: input.personalOwnershipAllowed,
|
|
176
224
|
definitionId: definition.id,
|
|
177
225
|
definitionFingerprint: providerDefinitionFingerprint(definition),
|
|
178
226
|
providerDomain,
|
|
@@ -227,6 +275,7 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
227
275
|
let state: ProviderOAuthState | null = null;
|
|
228
276
|
try {
|
|
229
277
|
state = readProviderOAuthState(input.state, deps.settings);
|
|
278
|
+
requireProviderOAuthOwner(state);
|
|
230
279
|
await requireProviderOAuthGrant(deps, state);
|
|
231
280
|
const consumed = await consumeIntegrationOAuthStateNonce(deps.db, {
|
|
232
281
|
accountId: state.accountId,
|
|
@@ -258,7 +307,7 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
258
307
|
}
|
|
259
308
|
const key = requireEnvironmentEncryption(deps.settings);
|
|
260
309
|
const verifier = decryptEnvironmentValue(key, state.encryptedPkceVerifier);
|
|
261
|
-
const redirectUri = `${apiBaseUrl}${
|
|
310
|
+
const redirectUri = `${apiBaseUrl}${providerOAuthCallbackPath(definition)}`;
|
|
262
311
|
const token = await exchangeProviderAuthorizationCode(deps, definition, client, {
|
|
263
312
|
code: input.code,
|
|
264
313
|
verifier,
|
|
@@ -387,6 +436,28 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
387
436
|
}
|
|
388
437
|
}
|
|
389
438
|
|
|
439
|
+
/**
|
|
440
|
+
* The ownership fences that apply to a curated Definition, read from the same
|
|
441
|
+
* profile table the MCP OAuth start uses. No curated Definition targets a
|
|
442
|
+
* personal-only provider today (they are Google Workspace and Microsoft Graph
|
|
443
|
+
* APIs), so this resolves to the default profile and its workspace default;
|
|
444
|
+
* matching by provider domain keeps the Slack/Gmail fences in force if one ever
|
|
445
|
+
* does, instead of letting this flow mint an ownership their profile forbids.
|
|
446
|
+
*/
|
|
447
|
+
/** The profile's ownership when it allows exactly one, else null (ambiguous). */
|
|
448
|
+
function soleAllowedOwnership(profile: OAuthProviderProfile): ConnectionOwnership | null {
|
|
449
|
+
return profile.allowedOwnership.length === 1 ? profile.allowedOwnership[0]! : null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function providerOAuthProfile(
|
|
453
|
+
definition: IntegrationDefinition,
|
|
454
|
+
providerDomain: string,
|
|
455
|
+
): OAuthProviderProfile {
|
|
456
|
+
return (
|
|
457
|
+
builtInOAuthProfileFor({ mcpUrl: definition.baseUrl, providerDomain }) ?? DEFAULT_OAUTH_PROFILE
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
390
461
|
function requiredDefinition(id: string): IntegrationDefinition {
|
|
391
462
|
const definition = integrationDefinitionById(id);
|
|
392
463
|
if (!definition) throw new HTTPException(404, { message: "Unknown Integration definition" });
|
|
@@ -434,6 +505,28 @@ function providerClientForDefinition(
|
|
|
434
505
|
};
|
|
435
506
|
}
|
|
436
507
|
|
|
508
|
+
export function isApiIntegrationProviderOAuthState(
|
|
509
|
+
value: string | undefined,
|
|
510
|
+
settings: Settings,
|
|
511
|
+
): boolean {
|
|
512
|
+
try {
|
|
513
|
+
readProviderOAuthState(value, settings);
|
|
514
|
+
return true;
|
|
515
|
+
} catch {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function providerOAuthCallbackPath(definition: IntegrationDefinition): string {
|
|
521
|
+
// Google definitions share the established MCP OAuth callback. The
|
|
522
|
+
// route distinguishes signed provider state from MCP state, while the
|
|
523
|
+
// separate google-drive callback remains available for the legacy Drive
|
|
524
|
+
// installation flow.
|
|
525
|
+
return definition.provider.id === "google"
|
|
526
|
+
? "/v1/integrations/oauth/callback"
|
|
527
|
+
: PROVIDER_OAUTH_CALLBACK_PATH;
|
|
528
|
+
}
|
|
529
|
+
|
|
437
530
|
function providerClientKeys(definition: IntegrationDefinition): string[] {
|
|
438
531
|
const authorization = new URL(definition.authentication.authorizationUrl);
|
|
439
532
|
const token = new URL(definition.authentication.tokenUrl);
|
|
@@ -503,6 +596,7 @@ function readProviderOAuthState(raw: string | undefined, settings: Settings): Pr
|
|
|
503
596
|
workspaceId: requiredStateString(payload.workspaceId),
|
|
504
597
|
subjectId: requiredStateString(payload.subjectId),
|
|
505
598
|
ownership,
|
|
599
|
+
personalOwnerVerified: personalOwnerVerifiedInState(payload),
|
|
506
600
|
definitionId: requiredStateString(payload.definitionId),
|
|
507
601
|
definitionFingerprint: requiredStateString(payload.definitionFingerprint),
|
|
508
602
|
providerDomain: requiredStateString(payload.providerDomain),
|
|
@@ -657,6 +751,18 @@ async function requireProviderOAuthGrant(
|
|
|
657
751
|
}
|
|
658
752
|
}
|
|
659
753
|
|
|
754
|
+
/**
|
|
755
|
+
* A state minted before the start-side principal fence existed can still be in
|
|
756
|
+
* flight for one `oauthStateTtlMs` window, and it carries no
|
|
757
|
+
* `personalOwnerVerified` claim. Such a state must not land a personal
|
|
758
|
+
* Connection.
|
|
759
|
+
*/
|
|
760
|
+
function requireProviderOAuthOwner(state: ProviderOAuthState): void {
|
|
761
|
+
if (!personalOwnerStateAccepted(state)) {
|
|
762
|
+
throw new ProviderOAuthCallbackError("connection_conflict");
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
660
766
|
function requireProviderOAuthConnection(
|
|
661
767
|
connection: NonNullable<Awaited<ReturnType<typeof getConnectionMetadata>>>,
|
|
662
768
|
input: {
|
|
@@ -686,8 +792,18 @@ function providerScopesInclude(
|
|
|
686
792
|
fallbackScopes: string[],
|
|
687
793
|
): boolean {
|
|
688
794
|
const effective = returnedScopes.length > 0 ? returnedScopes : fallbackScopes;
|
|
689
|
-
const normalize = (value: string) =>
|
|
690
|
-
definition.provider.id === "microsoft"
|
|
795
|
+
const normalize = (value: string) => {
|
|
796
|
+
if (definition.provider.id === "microsoft") return value.toLowerCase();
|
|
797
|
+
if (definition.provider.id === "google") {
|
|
798
|
+
if (value === "email" || value === "https://www.googleapis.com/auth/userinfo.email") {
|
|
799
|
+
return "email";
|
|
800
|
+
}
|
|
801
|
+
if (value === "profile" || value === "https://www.googleapis.com/auth/userinfo.profile") {
|
|
802
|
+
return "profile";
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return value;
|
|
806
|
+
};
|
|
691
807
|
const granted = new Set(effective.map(normalize));
|
|
692
808
|
return definition.authentication.scopes.every((scope) => granted.has(normalize(scope)));
|
|
693
809
|
}
|