@opengeni/api-router 3.0.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admission-trace.d.ts +8 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +2 -0
- package/dist/auth/managed-auth.d.ts +1 -0
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/{chunk-AJ62T5QN.js → chunk-E7TZFJ65.js} +22300 -21113
- package/dist/{chunk-AJ62T5QN.js.map → chunk-E7TZFJ65.js.map} +1 -1
- package/dist/connection-ownership.d.ts +1 -2
- package/dist/fatal-process-boundary.d.ts +1 -1
- package/dist/index.js +24 -6
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +8 -3
- package/dist/integrations/fiken.d.ts +3 -1
- package/dist/integrations/github-lens-connect.d.ts +5 -65
- package/dist/integrations/google-drive.d.ts +2 -0
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/oauth-client.d.ts +119 -11
- package/dist/integrations/oauth-profiles.d.ts +8 -13
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/capability-account-readiness.d.ts +7 -0
- package/dist/mcp/first-party-tool-permissions.d.ts +4 -0
- package/dist/mcp-oauth.d.ts +22 -1
- package/dist/routes/api-integrations.d.ts +1 -0
- package/dist/routes/codex.d.ts +28 -0
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/dist/routes/organization-integration-policy.d.ts +3 -0
- package/dist/routes/sessions.d.ts +24 -2
- package/dist/scheduled-task-deletion.d.ts +3 -4
- package/dist/workspace-tool-gateway.d.ts +3 -1
- package/package.json +15 -15
- package/src/admission-trace.ts +14 -0
- package/src/app.ts +141 -94
- package/src/auth/managed-auth-attempt-context.ts +9 -0
- package/src/auth/managed-auth-session-adapter.ts +11 -0
- package/src/auth/managed-auth.ts +39 -2
- package/src/auth/managed-sign-in-notifications.ts +81 -0
- package/src/connection-ownership.ts +2 -9
- package/src/editable-artifact-production.ts +6 -0
- package/src/fatal-process-boundary.ts +17 -2
- package/src/index.ts +3 -4
- package/src/integrations/atlassian.ts +184 -76
- package/src/integrations/fiken.ts +57 -35
- package/src/integrations/github-app-connect.ts +51 -15
- package/src/integrations/github-lens-connect.ts +7 -28
- package/src/integrations/google-drive.ts +296 -125
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/oauth-client.ts +231 -132
- package/src/integrations/oauth-profiles.ts +10 -45
- package/src/integrations/personal-github.ts +55 -31
- package/src/integrations/provider-oauth.ts +57 -82
- package/src/integrations/social-oauth.ts +83 -56
- package/src/mcp/capability-account-readiness.ts +17 -0
- package/src/mcp/editable-artifacts.ts +7 -2
- package/src/mcp/first-party-tool-permissions.ts +1 -0
- package/src/mcp/knowledge-entries.ts +25 -8
- package/src/mcp/server.ts +135 -43
- package/src/mcp-oauth.ts +336 -34
- package/src/routes/api-integrations.ts +116 -4
- package/src/routes/automations.ts +5 -13
- package/src/routes/billing.ts +52 -0
- package/src/routes/canonical-human-identities.ts +61 -0
- package/src/routes/capabilities.ts +51 -1
- package/src/routes/codex.ts +4 -2
- package/src/routes/connect.ts +90 -31
- package/src/routes/connections.ts +487 -307
- package/src/routes/files.ts +21 -12
- package/src/routes/github.ts +48 -19
- package/src/routes/insights.ts +17 -7
- package/src/routes/integration-facets.ts +66 -25
- package/src/routes/knowledge.ts +48 -1
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/organization-integration-policy.ts +113 -0
- package/src/routes/organization-memberships.ts +22 -2
- package/src/routes/plugins.ts +42 -4
- package/src/routes/pr-review-github.ts +67 -57
- package/src/routes/pr-review.ts +74 -71
- package/src/routes/rigs.ts +32 -13
- package/src/routes/scheduled-tasks.ts +6 -6
- package/src/routes/sessions.ts +233 -11
- package/src/routes/skill-content.ts +19 -3
- package/src/routes/social.ts +24 -13
- package/src/sandbox/channel-a.ts +7 -1
- package/src/sandbox/rematerialize.ts +2 -1
- package/src/scheduled-task-deletion.ts +13 -3
- package/src/workspace-tool-gateway.ts +31 -31
- package/dist/connection-authority-owner.d.ts +0 -59
- package/dist/routes/connection-authorities.d.ts +0 -3
- package/dist/routes/host-mcp-bindings.d.ts +0 -5
- package/dist/routes/host-mcp-resolvers.d.ts +0 -3
- package/dist/routes/packs.d.ts +0 -3
- package/src/connection-authority-owner.ts +0 -61
- package/src/routes/connection-authorities.ts +0 -139
- package/src/routes/host-mcp-bindings.ts +0 -122
- package/src/routes/host-mcp-resolvers.ts +0 -41
- package/src/routes/packs.ts +0 -784
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withOrganizationIntegrationAcquisition } from "@opengeni/db/organization-integration-policy";
|
|
2
|
+
import { claimOAuthAcquisition, finishOAuthAcquisition } from "./oauth-client";
|
|
1
3
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
4
|
import { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
|
|
3
5
|
import { requireConnectOwnerAuthority } from "./connect-authority";
|
|
@@ -23,7 +25,6 @@ import {
|
|
|
23
25
|
} from "@opengeni/core";
|
|
24
26
|
import {
|
|
25
27
|
consumeIntegrationOAuthStateNonce,
|
|
26
|
-
claimConnectOperation,
|
|
27
28
|
finishConnectOperation,
|
|
28
29
|
getConnectAttempt,
|
|
29
30
|
type Database,
|
|
@@ -216,6 +217,7 @@ export async function startPersonalGitHubOAuth(
|
|
|
216
217
|
) {
|
|
217
218
|
const oauth = requirePersonalGitHubOAuthSettings(deps.settings);
|
|
218
219
|
const { grant } = input.access;
|
|
220
|
+
await withOrganizationIntegrationAcquisition(deps.db, grant, ["github-personal"], async () => {});
|
|
219
221
|
const existing = input.connectionId
|
|
220
222
|
? await getConnectionMetadata(deps.db, input.workspaceId, input.connectionId, grant.subjectId)
|
|
221
223
|
: null;
|
|
@@ -301,12 +303,18 @@ export async function completePersonalGitHubOAuthCallback(
|
|
|
301
303
|
operationId: `oauth:${state.nonce}`,
|
|
302
304
|
inputDigest: createHash("sha256").update(input.state!).digest("hex"),
|
|
303
305
|
};
|
|
304
|
-
const claim = await
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
306
|
+
const claim = await claimOAuthAcquisition(
|
|
307
|
+
deps.db,
|
|
308
|
+
state,
|
|
309
|
+
{
|
|
310
|
+
...operation,
|
|
311
|
+
expectedRevision: stored.attempt.revision,
|
|
312
|
+
authorize: (tx, _attempt, origin) =>
|
|
313
|
+
requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
|
|
314
|
+
},
|
|
315
|
+
"github-personal",
|
|
316
|
+
Boolean(input.code && !input.error),
|
|
317
|
+
);
|
|
310
318
|
if (claim.status === "replayed") return { redirectTo: exactReturnUrl, exactReturn: true };
|
|
311
319
|
}
|
|
312
320
|
await requirePersonalGitHubCallbackGrant(deps, state);
|
|
@@ -340,6 +348,12 @@ export async function completePersonalGitHubOAuthCallback(
|
|
|
340
348
|
}
|
|
341
349
|
if (input.error) throw new PersonalGitHubCallbackError("provider_denied");
|
|
342
350
|
if (!input.code) throw new PersonalGitHubCallbackError("missing_code");
|
|
351
|
+
await withOrganizationIntegrationAcquisition(
|
|
352
|
+
deps.db,
|
|
353
|
+
state,
|
|
354
|
+
["github-personal"],
|
|
355
|
+
async () => {},
|
|
356
|
+
);
|
|
343
357
|
|
|
344
358
|
const oauth = requirePersonalGitHubOAuthSettings(deps.settings);
|
|
345
359
|
if (
|
|
@@ -474,33 +488,43 @@ export async function completePersonalGitHubOAuthCallback(
|
|
|
474
488
|
});
|
|
475
489
|
};
|
|
476
490
|
if (operation) {
|
|
477
|
-
await
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
491
|
+
await finishOAuthAcquisition(
|
|
492
|
+
deps.db,
|
|
493
|
+
acceptedState,
|
|
494
|
+
{
|
|
495
|
+
...operation,
|
|
496
|
+
authorize: (tx, _attempt, origin) =>
|
|
497
|
+
requireConnectOwnerAuthority(tx, acceptedState, "connections:write", origin),
|
|
498
|
+
commit: async (tx, current) => {
|
|
499
|
+
const connection = await persist(tx);
|
|
500
|
+
if (!connection) throw new PersonalGitHubCallbackError("connection_conflict");
|
|
501
|
+
return {
|
|
502
|
+
...current,
|
|
503
|
+
revision: current.revision + 1,
|
|
504
|
+
state: "complete",
|
|
505
|
+
credentialsCommitted: true,
|
|
506
|
+
nextAction: { type: "none" },
|
|
507
|
+
account: {
|
|
508
|
+
id: connection.id,
|
|
509
|
+
version: connection.version,
|
|
510
|
+
providerId: "github-personal",
|
|
511
|
+
label: identity.login,
|
|
512
|
+
ownership: "personal",
|
|
513
|
+
status: "connected",
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
},
|
|
499
517
|
},
|
|
500
|
-
|
|
518
|
+
"github-personal",
|
|
519
|
+
);
|
|
501
520
|
return { redirectTo: exactReturnUrl!, exactReturn: true };
|
|
502
521
|
}
|
|
503
|
-
const connection = await
|
|
522
|
+
const connection = await withOrganizationIntegrationAcquisition(
|
|
523
|
+
deps.db,
|
|
524
|
+
acceptedState,
|
|
525
|
+
["github-personal"],
|
|
526
|
+
persist,
|
|
527
|
+
);
|
|
504
528
|
if (!connection) throw new PersonalGitHubCallbackError("connection_conflict");
|
|
505
529
|
return {
|
|
506
530
|
redirectTo: personalGitHubCallbackReturnUrl(returnBaseUrl, state.returnPath, "success", {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withOrganizationIntegrationAcquisition } from "@opengeni/db/organization-integration-policy";
|
|
2
|
+
import { claimOAuthAcquisition, finishOAuthAcquisition } from "./oauth-client";
|
|
1
3
|
import { Buffer } from "node:buffer";
|
|
2
4
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
5
|
|
|
@@ -18,7 +20,11 @@ import {
|
|
|
18
20
|
type ApiIntegrationOAuthStartRequest,
|
|
19
21
|
type ConnectionOwnership,
|
|
20
22
|
} from "@opengeni/contracts";
|
|
21
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
integrationKeyForConnectProvider,
|
|
25
|
+
requireEnvironmentEncryption,
|
|
26
|
+
type ApiRouteDeps,
|
|
27
|
+
} from "@opengeni/core";
|
|
22
28
|
import { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
|
|
23
29
|
import {
|
|
24
30
|
consumeIntegrationOAuthStateNonce,
|
|
@@ -28,7 +34,6 @@ import {
|
|
|
28
34
|
loadConnectionCredentialForBroker,
|
|
29
35
|
persistProviderOAuthConnection,
|
|
30
36
|
getConnectAttempt,
|
|
31
|
-
claimConnectOperation,
|
|
32
37
|
finishConnectOperation,
|
|
33
38
|
} from "@opengeni/db";
|
|
34
39
|
import { createSignedState, readSignedState } from "@opengeni/github";
|
|
@@ -52,12 +57,6 @@ import {
|
|
|
52
57
|
oauthStateTtlMs,
|
|
53
58
|
requireIntegrationsStateSecret,
|
|
54
59
|
} from "./oauth-client";
|
|
55
|
-
import {
|
|
56
|
-
assertOwnershipAllowed,
|
|
57
|
-
builtInOAuthProfileFor,
|
|
58
|
-
DEFAULT_OAUTH_PROFILE,
|
|
59
|
-
type OAuthProviderProfile,
|
|
60
|
-
} from "./oauth-profiles";
|
|
61
60
|
|
|
62
61
|
const PROVIDER_OAUTH_CALLBACK_PATH = "/v1/integrations/provider-oauth/callback";
|
|
63
62
|
const PROVIDER_OAUTH_TIMEOUT_MS = 15_000;
|
|
@@ -149,6 +148,7 @@ export async function startApiIntegrationProviderOAuth(
|
|
|
149
148
|
},
|
|
150
149
|
): Promise<OAuthStartResponse> {
|
|
151
150
|
const definition = requiredDefinition(input.payload.definitionId);
|
|
151
|
+
await withOrganizationIntegrationAcquisition(deps.db, input, [definition.id], async () => {});
|
|
152
152
|
const providerDomain = integrationDefinitionProviderDomain(definition);
|
|
153
153
|
const existing = input.payload.connectionId
|
|
154
154
|
? await getConnectionMetadata(
|
|
@@ -182,18 +182,8 @@ export async function startApiIntegrationProviderOAuth(
|
|
|
182
182
|
message: "The selected Connection ownership does not match this OAuth request",
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
// would have been the opposite defect: an executed probe on
|
|
188
|
-
// `microsoft-outlook-mail` confirmed it flips a newly connected mailbox from
|
|
189
|
-
// subject-scoped to workspace-shared for API/SDK callers, which is a real
|
|
190
|
-
// narrow -> broad widening. So an ambiguous omission is refused outright and
|
|
191
|
-
// the caller must choose. A profile that allows exactly one ownership is not
|
|
192
|
-
// ambiguous (Gmail and hosted Slack MCP are personal-only), and a reconnect
|
|
193
|
-
// takes the existing row's ownership.
|
|
194
|
-
const profile = providerOAuthProfile(definition, providerDomain);
|
|
195
|
-
const ownership =
|
|
196
|
-
existingOwnership ?? input.payload.ownership ?? soleAllowedOwnership(profile) ?? null;
|
|
185
|
+
// API callers choose ownership explicitly; reconnects preserve the saved choice.
|
|
186
|
+
const ownership = existingOwnership ?? input.payload.ownership ?? null;
|
|
197
187
|
if (ownership === null) {
|
|
198
188
|
throw new HTTPException(422, {
|
|
199
189
|
message:
|
|
@@ -201,13 +191,6 @@ export async function startApiIntegrationProviderOAuth(
|
|
|
201
191
|
'or "personal" to connect only for yourself',
|
|
202
192
|
});
|
|
203
193
|
}
|
|
204
|
-
// Inert by construction today, and deliberately kept: no curated Definition
|
|
205
|
-
// resolves to a personal-only profile (all five are Google Workspace and
|
|
206
|
-
// Microsoft Graph APIs, so `providerOAuthProfile` returns the default), which
|
|
207
|
-
// is why disabling this line reddens nothing. It is the fence that keeps a
|
|
208
|
-
// future Definition targeting slack.com or Gmail from minting an ownership
|
|
209
|
-
// its profile forbids - read it as pre-wiring, not as an untested gap.
|
|
210
|
-
assertOwnershipAllowed(profile, ownership);
|
|
211
194
|
assertConnectionOwnershipAllowedForPrincipal(ownership, input.personalOwnershipAllowed);
|
|
212
195
|
const authorizeScopes = uniqueStrings([
|
|
213
196
|
...(existing?.grantedScopes ?? []),
|
|
@@ -307,12 +290,18 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
307
290
|
operationId: `oauth:${state.nonce}`,
|
|
308
291
|
inputDigest: createHash("sha256").update(input.state!).digest("hex"),
|
|
309
292
|
};
|
|
310
|
-
const claim = await
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
293
|
+
const claim = await claimOAuthAcquisition(
|
|
294
|
+
deps.db,
|
|
295
|
+
state,
|
|
296
|
+
{
|
|
297
|
+
...connectOperation,
|
|
298
|
+
expectedRevision: stored.attempt.revision,
|
|
299
|
+
authorize: (tx, _attempt, origin) =>
|
|
300
|
+
requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
|
|
301
|
+
},
|
|
302
|
+
integrationKeyForConnectProvider(state.definitionId),
|
|
303
|
+
Boolean(input.code && !input.error),
|
|
304
|
+
);
|
|
316
305
|
if (claim.status === "replayed") return { redirectTo: exactReturnUrl, exactReturn: true };
|
|
317
306
|
}
|
|
318
307
|
requireProviderOAuthOwner(state);
|
|
@@ -350,6 +339,7 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
350
339
|
|
|
351
340
|
const definition = integrationDefinitionById(state.definitionId);
|
|
352
341
|
if (!definition) throw new ProviderOAuthCallbackError("state_invalid");
|
|
342
|
+
await withOrganizationIntegrationAcquisition(deps.db, state, [definition.id], async () => {});
|
|
353
343
|
if (
|
|
354
344
|
state.definitionFingerprint !== providerDefinitionFingerprint(definition) ||
|
|
355
345
|
state.providerDomain !== integrationDefinitionProviderDomain(definition) ||
|
|
@@ -480,32 +470,42 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
480
470
|
const persist = (database: import("@opengeni/db").Database) =>
|
|
481
471
|
persistProviderOAuthConnection(database, persistenceInput);
|
|
482
472
|
if (connectOperation) {
|
|
483
|
-
await
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
473
|
+
await finishOAuthAcquisition(
|
|
474
|
+
deps.db,
|
|
475
|
+
state,
|
|
476
|
+
{
|
|
477
|
+
...connectOperation,
|
|
478
|
+
authorize: (tx, _attempt, origin) =>
|
|
479
|
+
requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
|
|
480
|
+
commit: async (tx, current) => {
|
|
481
|
+
const connection = await persist(tx);
|
|
482
|
+
if (!connection) throw new ProviderOAuthCallbackError("connection_conflict");
|
|
483
|
+
return {
|
|
484
|
+
...current,
|
|
485
|
+
revision: current.revision + 1,
|
|
486
|
+
state: "connected_but_incomplete",
|
|
487
|
+
credentialsCommitted: true,
|
|
488
|
+
nextAction: { type: "none" },
|
|
489
|
+
account: {
|
|
490
|
+
id: connection.id,
|
|
491
|
+
providerId: current.providerId,
|
|
492
|
+
label: identity.displayName ?? identity.email ?? identity.principalId,
|
|
493
|
+
ownership: current.ownership,
|
|
494
|
+
status: "connected",
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
},
|
|
504
498
|
},
|
|
505
|
-
|
|
499
|
+
definition.id,
|
|
500
|
+
);
|
|
506
501
|
return { redirectTo: exactReturnUrl!, exactReturn: true };
|
|
507
502
|
}
|
|
508
|
-
const connection = await
|
|
503
|
+
const connection = await withOrganizationIntegrationAcquisition(
|
|
504
|
+
deps.db,
|
|
505
|
+
state,
|
|
506
|
+
[definition.id],
|
|
507
|
+
persist,
|
|
508
|
+
);
|
|
509
509
|
if (!connection) throw new ProviderOAuthCallbackError("connection_conflict");
|
|
510
510
|
return {
|
|
511
511
|
redirectTo: providerOAuthReturnUrl(returnBaseUrl, state.returnPath, "success", {
|
|
@@ -531,28 +531,6 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
531
531
|
}
|
|
532
532
|
}
|
|
533
533
|
|
|
534
|
-
/**
|
|
535
|
-
* The ownership fences that apply to a curated Definition, read from the same
|
|
536
|
-
* profile table the MCP OAuth start uses. No curated Definition targets a
|
|
537
|
-
* personal-only provider today (they are Google Workspace and Microsoft Graph
|
|
538
|
-
* APIs), so this resolves to the default profile and its workspace default;
|
|
539
|
-
* matching by provider domain keeps the Slack/Gmail fences in force if one ever
|
|
540
|
-
* does, instead of letting this flow mint an ownership their profile forbids.
|
|
541
|
-
*/
|
|
542
|
-
/** The profile's ownership when it allows exactly one, else null (ambiguous). */
|
|
543
|
-
function soleAllowedOwnership(profile: OAuthProviderProfile): ConnectionOwnership | null {
|
|
544
|
-
return profile.allowedOwnership.length === 1 ? profile.allowedOwnership[0]! : null;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function providerOAuthProfile(
|
|
548
|
-
definition: IntegrationDefinition,
|
|
549
|
-
providerDomain: string,
|
|
550
|
-
): OAuthProviderProfile {
|
|
551
|
-
return (
|
|
552
|
-
builtInOAuthProfileFor({ mcpUrl: definition.baseUrl, providerDomain }) ?? DEFAULT_OAUTH_PROFILE
|
|
553
|
-
);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
534
|
function requiredDefinition(id: string): IntegrationDefinition {
|
|
557
535
|
const definition = integrationDefinitionById(id);
|
|
558
536
|
if (!definition) throw new HTTPException(404, { message: "Unknown Integration definition" });
|
|
@@ -564,10 +542,7 @@ export function curatedOAuthReadiness(
|
|
|
564
542
|
settings: Settings,
|
|
565
543
|
definition: IntegrationDefinition,
|
|
566
544
|
): { configured: boolean; ownership: ConnectionOwnership[] } {
|
|
567
|
-
const ownership = [
|
|
568
|
-
...providerOAuthProfile(definition, integrationDefinitionProviderDomain(definition))
|
|
569
|
-
.allowedOwnership,
|
|
570
|
-
];
|
|
545
|
+
const ownership: ConnectionOwnership[] = ["workspace", "personal"];
|
|
571
546
|
try {
|
|
572
547
|
if (!settings.integrationsEnabled) return { configured: false, ownership };
|
|
573
548
|
requireEnvironmentEncryption(settings);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withOrganizationIntegrationAcquisition } from "@opengeni/db/organization-integration-policy";
|
|
2
|
+
import { claimOAuthAcquisition, finishOAuthAcquisition } from "./oauth-client";
|
|
1
3
|
import { parseSocialOauthClientsJson, type Settings } from "@opengeni/config";
|
|
2
4
|
import {
|
|
3
5
|
OAuthStartResponse,
|
|
@@ -15,7 +17,6 @@ import {
|
|
|
15
17
|
loadSocialConnectionCredential,
|
|
16
18
|
updateSocialConnectionCredential,
|
|
17
19
|
upsertSocialOAuthConnection,
|
|
18
|
-
claimConnectOperation,
|
|
19
20
|
finishConnectOperation,
|
|
20
21
|
getConnectAttempt,
|
|
21
22
|
type Database,
|
|
@@ -196,6 +197,7 @@ export async function startSocialOAuth(
|
|
|
196
197
|
): Promise<OAuthStartResponse> {
|
|
197
198
|
const { settings } = deps;
|
|
198
199
|
const provider = SOCIAL_OAUTH_PROVIDERS[context.payload.provider];
|
|
200
|
+
await withOrganizationIntegrationAcquisition(deps.db, context, [provider.id], async () => {});
|
|
199
201
|
const client = socialOAuthClientFor(settings, provider.id);
|
|
200
202
|
const redirectUri = socialOAuthRedirectUri(settings, context.requestUrl);
|
|
201
203
|
const returnPath = safeReturnPath(context.payload.returnPath ?? "/integrations");
|
|
@@ -323,6 +325,7 @@ export async function completeSocialOAuthCallback(
|
|
|
323
325
|
}
|
|
324
326
|
try {
|
|
325
327
|
const provider = SOCIAL_OAUTH_PROVIDERS[state.provider];
|
|
328
|
+
await withOrganizationIntegrationAcquisition(db, state, [provider.id], async () => {});
|
|
326
329
|
const client = socialOAuthClientFor(settings, provider.id);
|
|
327
330
|
const key = requireEnvironmentEncryption(settings);
|
|
328
331
|
const verifier = state.encryptedPkceVerifier
|
|
@@ -344,18 +347,31 @@ export async function completeSocialOAuthCallback(
|
|
|
344
347
|
...(token.scope ? { scope: token.scope } : {}),
|
|
345
348
|
};
|
|
346
349
|
const grantedScopes = token.scope ? token.scope.split(/[\s,]+/).filter(Boolean) : state.scopes;
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
provider
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
350
|
+
const acceptedState = state;
|
|
351
|
+
const connection = await withOrganizationIntegrationAcquisition(
|
|
352
|
+
db,
|
|
353
|
+
acceptedState,
|
|
354
|
+
[provider.id],
|
|
355
|
+
async (tx) => {
|
|
356
|
+
await requireConnectOwnerAuthority(
|
|
357
|
+
tx,
|
|
358
|
+
acceptedState,
|
|
359
|
+
acceptedState.ownership === "workspace" ? "workspace:admin" : "workspace:read",
|
|
360
|
+
);
|
|
361
|
+
return upsertSocialOAuthConnection(tx, {
|
|
362
|
+
accountId: acceptedState.accountId,
|
|
363
|
+
workspaceId: acceptedState.workspaceId,
|
|
364
|
+
subjectId: acceptedState.ownership === "personal" ? acceptedState.subjectId : null,
|
|
365
|
+
provider: provider.id,
|
|
366
|
+
accountHandle: identity.handle,
|
|
367
|
+
accountName: identity.name ?? null,
|
|
368
|
+
externalAccountId: identity.externalAccountId,
|
|
369
|
+
scopes: grantedScopes,
|
|
370
|
+
credentialEncrypted: encryptEnvironmentValue(key, JSON.stringify(bundle)),
|
|
371
|
+
tokenMetadata: publicTokenMetadata(bundle),
|
|
372
|
+
});
|
|
373
|
+
},
|
|
374
|
+
);
|
|
359
375
|
return {
|
|
360
376
|
redirectTo: callbackReturnPath(state.returnPath, "success", {
|
|
361
377
|
connectionId: connection.id,
|
|
@@ -407,10 +423,16 @@ async function completeSocialConnect(
|
|
|
407
423
|
authorize,
|
|
408
424
|
};
|
|
409
425
|
try {
|
|
410
|
-
const claim = await
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
426
|
+
const claim = await claimOAuthAcquisition(
|
|
427
|
+
deps.db,
|
|
428
|
+
state,
|
|
429
|
+
{
|
|
430
|
+
...operation,
|
|
431
|
+
expectedRevision: stored.attempt.revision,
|
|
432
|
+
},
|
|
433
|
+
state.provider,
|
|
434
|
+
Boolean(input.code && !input.error),
|
|
435
|
+
);
|
|
414
436
|
if (claim.status === "replayed") return { redirectTo: stored.returnUrl, exactReturn: true };
|
|
415
437
|
if (!personalOwnerStateAccepted(state))
|
|
416
438
|
throw new HTTPException(403, { message: PERSONAL_CONNECTION_PRINCIPAL_MESSAGE });
|
|
@@ -455,46 +477,51 @@ async function completeSocialConnect(
|
|
|
455
477
|
...(token.expiresAt ? { expiresAt: token.expiresAt } : {}),
|
|
456
478
|
...(token.scope ? { scope: token.scope } : {}),
|
|
457
479
|
};
|
|
458
|
-
await
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
480
|
+
await finishOAuthAcquisition(
|
|
481
|
+
deps.db,
|
|
482
|
+
state,
|
|
483
|
+
{
|
|
484
|
+
...operation,
|
|
485
|
+
commit: async (tx, current) => {
|
|
486
|
+
const connection = await upsertSocialOAuthConnection(tx, {
|
|
487
|
+
accountId: state.accountId,
|
|
488
|
+
workspaceId: state.workspaceId,
|
|
489
|
+
...(current.account
|
|
490
|
+
? {
|
|
491
|
+
expectedConnection: {
|
|
492
|
+
id: current.account.id.slice("social:".length),
|
|
493
|
+
version: current.account.version!,
|
|
494
|
+
},
|
|
495
|
+
}
|
|
496
|
+
: {}),
|
|
497
|
+
subjectId: state.ownership === "personal" ? state.subjectId : null,
|
|
498
|
+
provider: provider.id,
|
|
499
|
+
accountHandle: identity.handle,
|
|
500
|
+
accountName: identity.name ?? null,
|
|
501
|
+
externalAccountId: identity.externalAccountId,
|
|
502
|
+
scopes: token.scope ? token.scope.split(/[\s,]+/).filter(Boolean) : state.scopes,
|
|
503
|
+
credentialEncrypted: encryptEnvironmentValue(key, JSON.stringify(bundle)),
|
|
504
|
+
tokenMetadata: publicTokenMetadata(bundle),
|
|
505
|
+
});
|
|
506
|
+
return {
|
|
507
|
+
...current,
|
|
508
|
+
revision: current.revision + 1,
|
|
509
|
+
state: "complete",
|
|
510
|
+
credentialsCommitted: true,
|
|
511
|
+
nextAction: { type: "none" },
|
|
512
|
+
account: {
|
|
513
|
+
id: `social:${connection.id}`,
|
|
514
|
+
providerId: provider.id,
|
|
515
|
+
version: connection.version!,
|
|
516
|
+
label: identity.handle,
|
|
517
|
+
ownership: state.ownership,
|
|
518
|
+
status: "connected",
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
},
|
|
496
522
|
},
|
|
497
|
-
|
|
523
|
+
state.provider,
|
|
524
|
+
);
|
|
498
525
|
}
|
|
499
526
|
} catch (error) {
|
|
500
527
|
// An uncertain exchange stays occupied: do not replay a consumed OAuth code.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { McpConnectionAccountBinding } from "@opengeni/contracts";
|
|
2
|
+
|
|
3
|
+
/** Canonical readiness comes only from routes present in this exact attempt,
|
|
4
|
+
* joined to this turn's accepted bindings. It never grants account access. */
|
|
5
|
+
export function capabilityAccountReadiness(
|
|
6
|
+
availableRoutes: Iterable<string>,
|
|
7
|
+
bindings: readonly McpConnectionAccountBinding[] | null,
|
|
8
|
+
) {
|
|
9
|
+
const available = new Set(availableRoutes);
|
|
10
|
+
const actualRoutes = new Set(available);
|
|
11
|
+
const accepted = new Set<string>();
|
|
12
|
+
for (const binding of bindings ?? []) {
|
|
13
|
+
accepted.add(binding.canonicalServerId);
|
|
14
|
+
if (actualRoutes.has(binding.serverId)) available.add(binding.canonicalServerId);
|
|
15
|
+
}
|
|
16
|
+
return { available, accepted };
|
|
17
|
+
}
|
|
@@ -38,6 +38,7 @@ const ArtifactCommand = z
|
|
|
38
38
|
const ArtifactMetadata = z
|
|
39
39
|
.object({
|
|
40
40
|
id: ArtifactId,
|
|
41
|
+
artifactReference: z.string(),
|
|
41
42
|
modality: Modality,
|
|
42
43
|
title: z.string(),
|
|
43
44
|
lifecycle: z.enum(["active", "archived"]),
|
|
@@ -203,13 +204,17 @@ export function registerEditableArtifactAgentTools(
|
|
|
203
204
|
{
|
|
204
205
|
title: "Inspect editable artifact",
|
|
205
206
|
description:
|
|
206
|
-
"Query the artifact's current canonical head through the same native kernel used by the browser. Use bounded modality queries; results are current, not a sandbox-file snapshot.",
|
|
207
|
+
"Query the artifact's current canonical head through the same native kernel used by the browser. Use bounded modality queries; results are current, not a sandbox-file snapshot. Native document queries return a server-issued inspectionReceiptId; use a post-edit body inspection receipt for declared report completion. If the document changes during inspection, retry against its current head.",
|
|
207
208
|
inputSchema: {
|
|
208
209
|
artifactId: ArtifactId,
|
|
209
210
|
modality: Modality,
|
|
210
211
|
request: EditableArtifactQueryRequestSchema,
|
|
211
212
|
},
|
|
212
|
-
outputSchema: {
|
|
213
|
+
outputSchema: {
|
|
214
|
+
artifact: ArtifactMetadata,
|
|
215
|
+
projection: z.unknown(),
|
|
216
|
+
inspectionReceiptId: z.string().uuid().optional(),
|
|
217
|
+
},
|
|
213
218
|
annotations: readOnlyAnnotations("Inspect editable artifact"),
|
|
214
219
|
},
|
|
215
220
|
async ({ artifactId, modality, request }, extra) =>
|
|
@@ -21,6 +21,7 @@ export const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
21
21
|
goal_pause: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
22
22
|
goal_resume: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
23
23
|
knowledge_search: { sessionRequired: true, allOf: ["documents:search"] },
|
|
24
|
+
knowledge_prepare_save: { sessionRequired: true, allOf: ["documents:search"] },
|
|
24
25
|
knowledge_get: { sessionRequired: true, allOf: ["documents:search"] },
|
|
25
26
|
knowledge_browse: { sessionRequired: true, allOf: ["documents:search"] },
|
|
26
27
|
knowledge_retain_file: { sessionRequired: true, allOf: ["documents:search", "files:read"] },
|