@opengeni/core 0.28.0 → 1.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/README.md +1 -1
- package/dist/access/external-actor-authority.d.ts +47 -0
- package/dist/access/index.d.ts +182 -3
- package/dist/access/service-initiator.d.ts +3 -0
- package/dist/application/api-integration-servers.d.ts +20 -0
- package/dist/application/archived-session-imports.d.ts +6 -0
- package/dist/application/composer-submit.d.ts +24 -0
- package/dist/application/connect-authority.d.ts +13 -0
- package/dist/application/connect-operation.d.ts +31 -0
- package/dist/application/external-continuation.d.ts +15 -0
- package/dist/application/external-identity-lifecycle.d.ts +20 -0
- package/dist/application/external-link-work-admission.d.ts +12 -0
- package/dist/application/external-workspace-members.d.ts +34 -0
- package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
- package/dist/application/new-session-drafts.d.ts +39 -2
- package/dist/application/sandbox-recovery.d.ts +58 -0
- package/dist/application/session-commands.d.ts +26 -13
- package/dist/application/session-mcp-credential-rotation.d.ts +29 -0
- package/dist/application/session-tenancy.d.ts +19 -0
- package/dist/application/user-resource-grants.d.ts +20 -0
- package/dist/billing/limits.d.ts +11 -0
- package/dist/canonical-human-identities.d.ts +7 -2
- package/dist/canonical-human-identities.js +17 -4
- package/dist/canonical-human-identities.js.map +1 -1
- package/dist/{chunk-6WKPWE5S.js → chunk-2MKMRGSY.js} +108 -24
- package/dist/chunk-2MKMRGSY.js.map +1 -0
- package/dist/{chunk-NYPMQ7NO.js → chunk-GVO3N37G.js} +205 -37
- package/dist/chunk-GVO3N37G.js.map +1 -0
- package/dist/{chunk-JJU6I5XD.js → chunk-LCQXNR62.js} +36 -10
- package/dist/chunk-LCQXNR62.js.map +1 -0
- package/dist/chunk-MM6N7CTO.js +387 -0
- package/dist/chunk-MM6N7CTO.js.map +1 -0
- package/dist/chunk-QO5GVFFO.js +235 -0
- package/dist/chunk-QO5GVFFO.js.map +1 -0
- package/dist/codex-model-availability.d.ts +14 -0
- package/dist/default-session-model.d.ts +139 -0
- package/dist/dependencies.d.ts +87 -4
- package/dist/developer-setup-credential-ceiling.d.ts +10 -0
- package/dist/domain/agent-config-resolution.d.ts +49 -0
- package/dist/domain/automations.d.ts +58 -0
- package/dist/domain/capabilities.d.ts +22 -4
- package/dist/domain/company-profile-agent-admin.d.ts +25 -0
- package/dist/domain/connection-account-selection-error.d.ts +8 -0
- package/dist/domain/connector-tool-permissions.d.ts +36 -0
- package/dist/domain/direct-model-provider.d.ts +3 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
- package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
- package/dist/domain/editable-artifacts/ports.d.ts +10 -0
- package/dist/domain/editable-artifacts/service.d.ts +4 -3
- package/dist/domain/environments.d.ts +5 -9
- package/dist/domain/external-creation-attribution.d.ts +6 -0
- package/dist/domain/file-owner.d.ts +8 -0
- package/dist/domain/github-action-policies.d.ts +36 -0
- package/dist/domain/github-repository-bindings.d.ts +117 -0
- package/dist/domain/github-skill-source.d.ts +5 -0
- package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
- package/dist/domain/insights-timing.d.ts +16 -0
- package/dist/domain/insights-usage.d.ts +27 -0
- package/dist/domain/insights.d.ts +28 -1
- package/dist/domain/knowledge-files.d.ts +20 -0
- package/dist/domain/knowledge-messages.d.ts +39 -0
- package/dist/domain/knowledge-messages.test.d.ts +1 -0
- package/dist/domain/knowledge-preparation.d.ts +10 -0
- package/dist/domain/knowledge-preparation.test.d.ts +1 -0
- package/dist/domain/knowledge-search.d.ts +15 -0
- package/dist/domain/knowledge-search.test.d.ts +1 -0
- package/dist/domain/knowledge.d.ts +9 -0
- package/dist/domain/mcp-account-bindings.d.ts +19 -0
- package/dist/domain/mcp-account-routes.d.ts +21 -0
- package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
- package/dist/domain/organization-integration-catalog.d.ts +15 -0
- package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
- package/dist/domain/personal-connection-delegations.d.ts +87 -4
- package/dist/domain/pr-review.d.ts +249 -0
- package/dist/domain/resources.d.ts +24 -3
- package/dist/domain/scheduled-task-access.d.ts +247 -0
- package/dist/domain/scheduled-tasks.d.ts +103 -6
- package/dist/domain/session-connection-accounts.d.ts +16 -0
- package/dist/domain/session-file-access.d.ts +4 -0
- package/dist/domain/session-native-connection-replacement.d.ts +14 -0
- package/dist/domain/session-start-timing.d.ts +7 -0
- package/dist/domain/session-tool-policy.d.ts +93 -4
- package/dist/domain/sessions.d.ts +275 -28
- package/dist/domain/skill-imports.d.ts +1 -0
- package/dist/domain/skill-search.d.ts +30 -0
- package/dist/domain/skills.d.ts +117 -0
- package/dist/domain/slack-bot.d.ts +46 -1
- package/dist/domain/workspace-members.d.ts +8 -1
- package/dist/editable-artifact-live/application.d.ts +12 -3
- package/dist/editable-artifact-live/ports.d.ts +0 -1
- package/dist/editable-artifact-live/server.d.ts +5 -2
- package/dist/editable-artifact-live/ticket.d.ts +2 -2
- package/dist/editable-artifact-live/types.d.ts +14 -2
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +10 -4
- package/dist/index.d.ts +49 -3
- package/dist/index.js +18826 -6847
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-sets.d.ts +131 -0
- package/dist/managed-auth-session-sets.js +51 -0
- package/dist/managed-auth-session-sets.js.map +1 -0
- package/dist/managed-session.d.ts +98 -11
- package/dist/model-catalog.d.ts +117 -0
- package/dist/product-usage-metrics.d.ts +30 -0
- package/dist/rigs/index.d.ts +22 -5
- package/dist/rigs/provider-images.d.ts +1 -1
- package/dist/sandbox/fleet.d.ts +59 -10
- package/dist/sandbox/routing.d.ts +22 -2
- package/dist/sandbox/runtime-settings.d.ts +56 -0
- package/dist/session-authorization.d.ts +38 -1
- package/dist/site-session-origin.d.ts +9 -0
- package/dist/transcription.d.ts +12 -1
- package/dist/turn-surface.d.ts +18 -0
- package/dist/user-presence.d.ts +32 -0
- package/package.json +18 -11
- package/src/access/external-actor-authority.ts +94 -0
- package/src/access/index.ts +1386 -44
- package/src/access/service-initiator.ts +50 -0
- package/src/application/api-integration-servers.ts +215 -0
- package/src/application/archived-session-imports.ts +161 -0
- package/src/application/composer-submit.ts +77 -0
- package/src/application/connect-authority.ts +77 -0
- package/src/application/connect-operation.ts +80 -0
- package/src/application/external-continuation.ts +129 -0
- package/src/application/external-identity-lifecycle.ts +48 -0
- package/src/application/external-link-work-admission.ts +90 -0
- package/src/application/external-workspace-members.ts +232 -0
- package/src/application/modal-native-worker-host-transport.ts +263 -0
- package/src/application/new-session-drafts.ts +222 -29
- package/src/application/sandbox-recovery.ts +78 -0
- package/src/application/session-commands.ts +430 -73
- package/src/application/session-mcp-credential-rotation.ts +217 -0
- package/src/application/session-tenancy.ts +445 -0
- package/src/application/user-resource-grants.ts +154 -0
- package/src/billing/limits.ts +103 -24
- package/src/canonical-human-identities.ts +22 -3
- package/src/codex-model-availability.ts +136 -0
- package/src/default-session-model.ts +656 -0
- package/src/dependencies.ts +97 -5
- package/src/developer-setup-credential-ceiling.ts +60 -0
- package/src/domain/agent-config-resolution.ts +148 -0
- package/src/domain/automations.ts +270 -0
- package/src/domain/capabilities.ts +244 -151
- package/src/domain/company-profile-agent-admin.ts +56 -0
- package/src/domain/connection-account-selection-error.ts +19 -0
- package/src/domain/connector-tool-permissions.ts +371 -0
- package/src/domain/direct-model-provider.ts +62 -0
- package/src/domain/editable-artifacts/agent-application.ts +61 -15
- package/src/domain/editable-artifacts/durable-export.ts +100 -5
- package/src/domain/editable-artifacts/in-memory.ts +6 -0
- package/src/domain/editable-artifacts/ports.ts +11 -0
- package/src/domain/editable-artifacts/service.ts +32 -1
- package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
- package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
- package/src/domain/environments.ts +25 -45
- package/src/domain/external-creation-attribution.ts +22 -0
- package/src/domain/file-owner.ts +67 -0
- package/src/domain/github-action-policies.ts +169 -0
- package/src/domain/github-repository-bindings.ts +295 -0
- package/src/domain/github-skill-source.ts +233 -0
- package/src/domain/governed-learning-slack-publication.ts +157 -0
- package/src/domain/insights-timing.ts +95 -0
- package/src/domain/insights-usage.ts +67 -0
- package/src/domain/insights.ts +270 -169
- package/src/domain/knowledge-files.ts +46 -0
- package/src/domain/knowledge-messages.test.ts +91 -0
- package/src/domain/knowledge-messages.ts +106 -0
- package/src/domain/knowledge-preparation.test.ts +276 -0
- package/src/domain/knowledge-preparation.ts +147 -0
- package/src/domain/knowledge-search.test.ts +561 -0
- package/src/domain/knowledge-search.ts +256 -0
- package/src/domain/knowledge.ts +133 -0
- package/src/domain/mcp-account-bindings.ts +171 -0
- package/src/domain/mcp-account-routes.ts +121 -0
- package/src/domain/native-mcp-connection-admission.ts +14 -0
- package/src/domain/organization-integration-catalog.ts +88 -0
- package/src/domain/organization-membership-lifecycle.ts +10 -0
- package/src/domain/personal-connection-delegations.ts +758 -115
- package/src/domain/pr-review.ts +687 -0
- package/src/domain/resources.ts +182 -25
- package/src/domain/scheduled-task-access.ts +1052 -0
- package/src/domain/scheduled-tasks.ts +1952 -215
- package/src/domain/session-connection-accounts.ts +67 -0
- package/src/domain/session-file-access.ts +47 -0
- package/src/domain/session-native-connection-replacement.ts +64 -0
- package/src/domain/session-start-timing.ts +55 -0
- package/src/domain/session-tool-policy.ts +443 -19
- package/src/domain/sessions.ts +3417 -619
- package/src/domain/skill-imports.ts +137 -17
- package/src/domain/skill-search.ts +221 -0
- package/src/domain/skills.ts +104 -0
- package/src/domain/slack-bot.ts +128 -3
- package/src/domain/workspace-members.ts +34 -2
- package/src/editable-artifact-live/application.ts +29 -4
- package/src/editable-artifact-live/ports.ts +0 -1
- package/src/editable-artifact-live/server.ts +167 -13
- package/src/editable-artifact-live/ticket.ts +87 -6
- package/src/editable-artifact-live/types.ts +16 -2
- package/src/editable-artifact-live/wire.ts +0 -10
- package/src/index.ts +69 -4
- package/src/managed-auth-session-sets.ts +371 -0
- package/src/managed-session.ts +520 -2
- package/src/model-catalog.ts +873 -0
- package/src/product-usage-metrics.ts +101 -0
- package/src/rigs/index.ts +179 -63
- package/src/rigs/provider-images.ts +2 -3
- package/src/sandbox/fleet.ts +471 -59
- package/src/sandbox/routing.ts +160 -33
- package/src/sandbox/runtime-settings.ts +281 -0
- package/src/session-authorization.ts +262 -78
- package/src/site-session-origin.ts +22 -0
- package/src/transcription.ts +13 -1
- package/src/turn-surface.ts +45 -0
- package/src/user-presence.ts +134 -0
- package/dist/chunk-6WKPWE5S.js.map +0 -1
- package/dist/chunk-IBOEYG6N.js +0 -34
- package/dist/chunk-IBOEYG6N.js.map +0 -1
- package/dist/chunk-JJU6I5XD.js.map +0 -1
- package/dist/chunk-NYPMQ7NO.js.map +0 -1
- package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
- package/dist/domain/packs.d.ts +0 -55
- package/src/domain/durable-learning-slack-publication.ts +0 -202
- package/src/domain/packs.ts +0 -556
|
@@ -2,27 +2,101 @@ import type { McpServerConfig, Settings } from "@opengeni/config";
|
|
|
2
2
|
import type {
|
|
3
3
|
AccessGrant,
|
|
4
4
|
ConnectionMetadata,
|
|
5
|
+
McpConnectionAccountBinding,
|
|
6
|
+
McpConnectionAccountSelection,
|
|
5
7
|
McpPersonalConnectionDelegation,
|
|
8
|
+
McpServerConnectionRef,
|
|
9
|
+
ResourceRef,
|
|
6
10
|
SessionTurn,
|
|
7
11
|
SocialConnection,
|
|
8
12
|
ToolRef,
|
|
9
13
|
} from "@opengeni/contracts";
|
|
10
14
|
import {
|
|
15
|
+
GOOGLE_DRIVE_PROVIDER_DOMAIN,
|
|
16
|
+
GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
|
|
17
|
+
GoogleDriveConnectionMetadata,
|
|
18
|
+
googleDriveScopesAllowCapability,
|
|
19
|
+
} from "@opengeni/contracts/google-drive";
|
|
20
|
+
import {
|
|
21
|
+
isPersonalGitHubConnection,
|
|
22
|
+
PERSONAL_GITHUB_CONNECTION_SURFACE_ID,
|
|
23
|
+
PersonalGitHubConnectionMetadata,
|
|
24
|
+
} from "@opengeni/contracts/personal-github";
|
|
25
|
+
import {
|
|
26
|
+
getPersonalGitHubRepositorySelectionState,
|
|
11
27
|
getSessionTurnPersonalConnectionDelegations,
|
|
28
|
+
getSessionTurnMcpAccountBindings,
|
|
12
29
|
getConnectionMetadata,
|
|
13
30
|
getSocialConnection,
|
|
14
|
-
getWorkspaceGrant,
|
|
15
31
|
listConnectionsMetadata,
|
|
16
32
|
listSocialConnections,
|
|
33
|
+
listOwnedConnectionAccounts,
|
|
34
|
+
namedSubjectHasLiveWorkspaceAuthority,
|
|
17
35
|
type Database,
|
|
18
36
|
type ResolveConnectionCredentialInput,
|
|
19
37
|
type ResolveConnectionCredentialResult,
|
|
20
38
|
} from "@opengeni/db";
|
|
39
|
+
import { personalGitHubRepositoryResources } from "./resources";
|
|
40
|
+
import { ConnectionAccountSelectionError } from "./connection-account-selection-error";
|
|
41
|
+
export { ConnectionAccountSelectionError } from "./connection-account-selection-error";
|
|
42
|
+
import {
|
|
43
|
+
mcpAccountBindingsFromVisibleConnections,
|
|
44
|
+
personalDelegationsForAccountBindings,
|
|
45
|
+
} from "./mcp-account-bindings";
|
|
21
46
|
|
|
22
47
|
export type PersonalConnectionDelegationSource =
|
|
23
|
-
| {
|
|
48
|
+
| {
|
|
49
|
+
kind: "subject";
|
|
50
|
+
subjectId: string;
|
|
51
|
+
/**
|
|
52
|
+
* Organization of the authorizing grant. The owner-only
|
|
53
|
+
* personal-workspace pointer lives on an organization membership, so
|
|
54
|
+
* the account is part of the question, not an optimization.
|
|
55
|
+
*/
|
|
56
|
+
accountId: string;
|
|
57
|
+
}
|
|
24
58
|
| { kind: "turn"; sessionId: string; turnId: string }
|
|
25
59
|
| { kind: "none" };
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Is `subjectId` still authorized in `workspaceId`?
|
|
63
|
+
*
|
|
64
|
+
* Personal-connection authority has always been "the owner must still belong
|
|
65
|
+
* to this workspace", but the old check was `getWorkspaceGrant` - a bare
|
|
66
|
+
* `workspace_memberships` join. A managed human's personal workspace
|
|
67
|
+
* deliberately has no row in that table (migration 0219 raises on one), so the
|
|
68
|
+
* join answered `false` for the one person who always belongs, and every
|
|
69
|
+
* personal connection silently disappeared inside the owner's own private
|
|
70
|
+
* workspace.
|
|
71
|
+
*
|
|
72
|
+
* `namedSubjectHasLiveWorkspaceAuthority` models both halves - the membership
|
|
73
|
+
* row and the organization membership's `personal_workspace_id` pointer - but
|
|
74
|
+
* it is an **oracle, not an authorization**: it answers for whatever subject it
|
|
75
|
+
* is handed and cannot establish who the caller is (see its doc comment in
|
|
76
|
+
* `@opengeni/db`). Every use is safe only because *this* module has already
|
|
77
|
+
* established entitlement to ask, locally:
|
|
78
|
+
*
|
|
79
|
+
* - at freeze time the subject is `source.subjectId`, and
|
|
80
|
+
* `personalConnectionDelegationSourceForGrant` admits only a grant whose
|
|
81
|
+
* `subjectId` came from a real authenticated principal. Agent-attempt,
|
|
82
|
+
* service, service-initiated, and **delegated/bearer** grants never reach
|
|
83
|
+
* this branch: they resolve to `{kind:"none"}`, or - for a worker-signed
|
|
84
|
+
* agent attempt - to `{kind:"turn"}`, which probes only a frozen
|
|
85
|
+
* `ownerSubjectId` already persisted for the grant's own workspace. Either
|
|
86
|
+
* way an unvalidated signed-token subject is never the probed subject; and
|
|
87
|
+
* - on the `*ForGrant` paths the subject is the frozen `ownerSubjectId` of a
|
|
88
|
+
* delegation already persisted for the workspace the caller is already
|
|
89
|
+
* authorized in.
|
|
90
|
+
*
|
|
91
|
+
* Do not pass any other subject through this helper. A subject that was merely
|
|
92
|
+
* looked up, guessed, or supplied by a caller is not entitled.
|
|
93
|
+
*/
|
|
94
|
+
async function ownerStillBelongsToWorkspace(
|
|
95
|
+
db: Database,
|
|
96
|
+
input: { accountId: string; workspaceId: string; subjectId: string },
|
|
97
|
+
): Promise<boolean> {
|
|
98
|
+
return await namedSubjectHasLiveWorkspaceAuthority(db, input);
|
|
99
|
+
}
|
|
26
100
|
export type AuthorizedSocialConnection = { connection: SocialConnection; subjectId: string | null };
|
|
27
101
|
export type AuthorizedAtlassianConnection = {
|
|
28
102
|
connection: ConnectionMetadata;
|
|
@@ -47,17 +121,100 @@ export function personalConnectionDelegationSourceForGrant(
|
|
|
47
121
|
): PersonalConnectionDelegationSource {
|
|
48
122
|
const callerSessionId = grant.metadata?.["sessionId"];
|
|
49
123
|
const callerTurnId = grant.metadata?.["turnId"];
|
|
124
|
+
// LOAD-BEARING ORDERING. This turn branch sits ABOVE the delegated filter
|
|
125
|
+
// below, so a grant carrying both `sessionId` and `turnId` never reaches that
|
|
126
|
+
// filter. That is safe only because `DelegatedAccessTokenPayload`'s
|
|
127
|
+
// `superRefine` forbids `principalKind: "human_session"` from carrying any
|
|
128
|
+
// attempt claim (`turnId`/`attemptId`/`executionGeneration`), so a delegated
|
|
129
|
+
// human-session bearer is structurally unable to take this branch. If that
|
|
130
|
+
// refinement is ever relaxed, a delegated bearer could route around the
|
|
131
|
+
// delegated filter by presenting a turn reference - move the filter above
|
|
132
|
+
// this branch at the same time. Pinned by
|
|
133
|
+
// `packages/contracts/test/delegated-access-token-attempt-claims.test.ts`.
|
|
50
134
|
if (typeof callerSessionId === "string" && typeof callerTurnId === "string") {
|
|
51
135
|
return { kind: "turn", sessionId: callerSessionId, turnId: callerTurnId };
|
|
52
136
|
}
|
|
53
137
|
if (
|
|
54
138
|
grant.principalKind === "agent_attempt" ||
|
|
55
139
|
grant.principalKind === "service" ||
|
|
56
|
-
grant.serviceInitiator
|
|
140
|
+
grant.serviceInitiator ||
|
|
141
|
+
// A delegated bearer's grant is built entirely from signed token payload
|
|
142
|
+
// (`delegatedAccessContext`); no database row binds its `subjectId` or
|
|
143
|
+
// `workspaceId` to anything. It is therefore never an entitled subject for
|
|
144
|
+
// the owner-only personal-workspace pointer, which CLAUDE.md reserves for
|
|
145
|
+
// the canonical managed-cookie session: "Bearer/delegated principals, API
|
|
146
|
+
// keys, and account or organization administrators receive no
|
|
147
|
+
// personal-workspace access through that exception." Before the pointer
|
|
148
|
+
// existed this was moot - `getWorkspaceGrant` returned null for a personal
|
|
149
|
+
// workspace regardless - so the filter belongs with the pointer.
|
|
150
|
+
//
|
|
151
|
+
// Why testing THIS field is sound while inspecting the others is not: a
|
|
152
|
+
// host signs `subjectId`, `principalKind`, and `serviceInitiator` into the
|
|
153
|
+
// token itself, so those are host-chosen and a hostile host picks whatever
|
|
154
|
+
// it likes. `metadata.delegated` is different - it is stamped by
|
|
155
|
+
// `delegatedAccessContext` in `@opengeni/core` AFTER the token signature is
|
|
156
|
+
// verified, and is not carried in the payload at all, so a host cannot
|
|
157
|
+
// clear it. Do not "harden" this by switching to `principalKind`. The
|
|
158
|
+
// strictly better shape is a positive assertion of how the request
|
|
159
|
+
// authenticated (a canonical managed-cookie stamp) rather than any
|
|
160
|
+
// inspection of the grant; that convergence is tracked separately.
|
|
161
|
+
grant.metadata?.delegated === true
|
|
57
162
|
) {
|
|
58
163
|
return { kind: "none" };
|
|
59
164
|
}
|
|
60
|
-
return { kind: "subject", subjectId: grant.subjectId };
|
|
165
|
+
return { kind: "subject", subjectId: grant.subjectId, accountId: grant.accountId };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Account-picker inventory: shared workspace accounts and this sender's own
|
|
169
|
+
* personal accounts. Visibility is never an execution grant or a credential
|
|
170
|
+
* response; every selected connection is validated again at admission/use. */
|
|
171
|
+
export async function listOwnConnectionAccountsForGrant(
|
|
172
|
+
db: Database,
|
|
173
|
+
grant: AccessGrant,
|
|
174
|
+
options: { includeInactive?: boolean } = {},
|
|
175
|
+
): Promise<ConnectionMetadata[]> {
|
|
176
|
+
const source = personalConnectionDelegationSourceForGrant(grant);
|
|
177
|
+
const workspace = (await listConnectionsMetadata(db, grant.workspaceId, null)).filter(
|
|
178
|
+
(connection) =>
|
|
179
|
+
connection.subjectId === null &&
|
|
180
|
+
connection.workspaceId === grant.workspaceId &&
|
|
181
|
+
connection.accountId === grant.accountId &&
|
|
182
|
+
(options.includeInactive === true || connection.status === "active"),
|
|
183
|
+
);
|
|
184
|
+
if (source.kind !== "subject") return workspace;
|
|
185
|
+
const personal = await listOwnConnectionMetadata(db, {
|
|
186
|
+
...source,
|
|
187
|
+
workspaceId: grant.workspaceId,
|
|
188
|
+
includeInactive: options.includeInactive === true,
|
|
189
|
+
});
|
|
190
|
+
return [...workspace, ...personal];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function listOwnConnectionMetadata(
|
|
194
|
+
db: Database,
|
|
195
|
+
input: { accountId: string; workspaceId: string; subjectId: string; includeInactive?: boolean },
|
|
196
|
+
): Promise<ConnectionMetadata[]> {
|
|
197
|
+
const accounts = await listOwnedConnectionAccounts(db, input);
|
|
198
|
+
const connections: ConnectionMetadata[] = [];
|
|
199
|
+
// The caller may supply a transaction handle (for example, credential
|
|
200
|
+
// rotation). Each metadata read opens a nested RLS savepoint on that same
|
|
201
|
+
// connection, so these reads must not overlap.
|
|
202
|
+
for (const account of accounts) {
|
|
203
|
+
const connection = await getConnectionMetadata(
|
|
204
|
+
db,
|
|
205
|
+
account.originWorkspaceId,
|
|
206
|
+
account.connectionId,
|
|
207
|
+
input.subjectId,
|
|
208
|
+
);
|
|
209
|
+
if (
|
|
210
|
+
connection?.accountId === input.accountId &&
|
|
211
|
+
connection.subjectId === input.subjectId &&
|
|
212
|
+
connection.authorityId != null &&
|
|
213
|
+
(input.includeInactive === true || connection.status === "active")
|
|
214
|
+
)
|
|
215
|
+
connections.push(connection);
|
|
216
|
+
}
|
|
217
|
+
return connections;
|
|
61
218
|
}
|
|
62
219
|
|
|
63
220
|
export async function authorizedSocialConnectionsForGrant(input: {
|
|
@@ -96,7 +253,13 @@ export async function authorizedSocialConnectionsForGrant(input: {
|
|
|
96
253
|
).filter((item) => item.connectionType === "social");
|
|
97
254
|
const personal: AuthorizedSocialConnection[] = [];
|
|
98
255
|
for (const delegation of delegations) {
|
|
99
|
-
if (
|
|
256
|
+
if (
|
|
257
|
+
!(await ownerStillBelongsToWorkspace(input.db, {
|
|
258
|
+
accountId: input.grant.accountId,
|
|
259
|
+
workspaceId: input.grant.workspaceId,
|
|
260
|
+
subjectId: delegation.ownerSubjectId,
|
|
261
|
+
}))
|
|
262
|
+
)
|
|
100
263
|
continue;
|
|
101
264
|
const connection = await getSocialConnection(
|
|
102
265
|
input.db,
|
|
@@ -112,68 +275,12 @@ export async function authorizedSocialConnectionsForGrant(input: {
|
|
|
112
275
|
return [...workspace.map((connection) => ({ connection, subjectId: null })), ...personal];
|
|
113
276
|
}
|
|
114
277
|
|
|
115
|
-
|
|
278
|
+
/** Historical native Atlassian grants no longer authorize API execution. */
|
|
279
|
+
export async function authorizedAtlassianConnectionsForGrant(_input: {
|
|
116
280
|
db: Database;
|
|
117
281
|
grant: AccessGrant;
|
|
118
282
|
}): Promise<AuthorizedAtlassianConnection[]> {
|
|
119
|
-
|
|
120
|
-
(connection) =>
|
|
121
|
-
connection.subjectId === null &&
|
|
122
|
-
connection.status === "active" &&
|
|
123
|
-
sameProviderDomain(connection.providerDomain, "api.atlassian.com"),
|
|
124
|
-
);
|
|
125
|
-
const source = personalConnectionDelegationSourceForGrant(input.grant);
|
|
126
|
-
if (source.kind === "none") {
|
|
127
|
-
return workspace.map((connection) => ({ connection, subjectId: null }));
|
|
128
|
-
}
|
|
129
|
-
if (source.kind === "subject") {
|
|
130
|
-
const visible = await listConnectionsMetadata(
|
|
131
|
-
input.db,
|
|
132
|
-
input.grant.workspaceId,
|
|
133
|
-
source.subjectId,
|
|
134
|
-
);
|
|
135
|
-
return visible
|
|
136
|
-
.filter(
|
|
137
|
-
(connection) =>
|
|
138
|
-
connection.status === "active" &&
|
|
139
|
-
sameProviderDomain(connection.providerDomain, "api.atlassian.com"),
|
|
140
|
-
)
|
|
141
|
-
.map((connection) => ({
|
|
142
|
-
connection,
|
|
143
|
-
subjectId: connection.subjectId === null ? null : source.subjectId,
|
|
144
|
-
}));
|
|
145
|
-
}
|
|
146
|
-
const delegations = (
|
|
147
|
-
await getSessionTurnPersonalConnectionDelegations(
|
|
148
|
-
input.db,
|
|
149
|
-
input.grant.workspaceId,
|
|
150
|
-
source.sessionId,
|
|
151
|
-
source.turnId,
|
|
152
|
-
)
|
|
153
|
-
).filter((item) => item.connectionType === "atlassian");
|
|
154
|
-
const personal: AuthorizedAtlassianConnection[] = [];
|
|
155
|
-
for (const delegation of delegations) {
|
|
156
|
-
if (!(await getWorkspaceGrant(input.db, delegation.ownerSubjectId, input.grant.workspaceId))) {
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
const connection = await getConnectionMetadata(
|
|
160
|
-
input.db,
|
|
161
|
-
input.grant.workspaceId,
|
|
162
|
-
delegation.connectionId,
|
|
163
|
-
delegation.ownerSubjectId,
|
|
164
|
-
);
|
|
165
|
-
if (
|
|
166
|
-
!connection ||
|
|
167
|
-
connection.subjectId !== delegation.ownerSubjectId ||
|
|
168
|
-
connection.status !== "active" ||
|
|
169
|
-
!sameProviderDomain(connection.providerDomain, delegation.providerDomain) ||
|
|
170
|
-
!sameProviderDomain(connection.providerDomain, "api.atlassian.com")
|
|
171
|
-
) {
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
personal.push({ connection, subjectId: delegation.ownerSubjectId });
|
|
175
|
-
}
|
|
176
|
-
return [...workspace.map((connection) => ({ connection, subjectId: null })), ...personal];
|
|
283
|
+
return [];
|
|
177
284
|
}
|
|
178
285
|
|
|
179
286
|
export function selectedPersonalConnectionServers(
|
|
@@ -182,77 +289,329 @@ export function selectedPersonalConnectionServers(
|
|
|
182
289
|
): McpServerConfig[] {
|
|
183
290
|
const selected = new Set(tools.map((tool) => tool.id));
|
|
184
291
|
return settings.mcpServers.filter(
|
|
185
|
-
(server) => selected.has(server.id) && server.connectionRef
|
|
292
|
+
(server) => selected.has(server.id) && isNativeSubjectConnectionRef(server.connectionRef),
|
|
186
293
|
);
|
|
187
294
|
}
|
|
188
295
|
|
|
189
|
-
function
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const updated = Date.parse(right.updatedAt) - Date.parse(left.updatedAt);
|
|
194
|
-
if (updated !== 0) return updated;
|
|
195
|
-
const created = Date.parse(right.createdAt) - Date.parse(left.createdAt);
|
|
196
|
-
if (created !== 0) return created;
|
|
197
|
-
return right.id.localeCompare(left.id);
|
|
198
|
-
});
|
|
296
|
+
function isNativeSubjectConnectionRef(
|
|
297
|
+
ref: McpServerConnectionRef | null | undefined,
|
|
298
|
+
): ref is McpServerConnectionRef & { subjectScope: "subject" } {
|
|
299
|
+
return ref?.subjectScope === "subject" && ref.authoritySource !== "host";
|
|
199
300
|
}
|
|
200
301
|
|
|
201
302
|
function sameProviderDomain(left: string, right: string): boolean {
|
|
202
303
|
return left.toLowerCase() === right.toLowerCase();
|
|
203
304
|
}
|
|
204
305
|
|
|
306
|
+
function canonicalConnectionResource(value: string): string {
|
|
307
|
+
try {
|
|
308
|
+
const url = new URL(value);
|
|
309
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return value.trim();
|
|
310
|
+
url.hash = "";
|
|
311
|
+
url.pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
312
|
+
return url.toString();
|
|
313
|
+
} catch {
|
|
314
|
+
return value.trim();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
205
318
|
export function personalConnectionDelegationsFromVisibleConnections(input: {
|
|
206
319
|
servers: McpServerConfig[];
|
|
207
320
|
subjectId: string;
|
|
208
321
|
connections: ConnectionMetadata[];
|
|
322
|
+
authoritySelections?: McpConnectionAccountSelection[];
|
|
209
323
|
}): McpPersonalConnectionDelegation[] {
|
|
210
324
|
const delegations: McpPersonalConnectionDelegation[] = [];
|
|
211
|
-
const connections =
|
|
325
|
+
const connections = input.connections;
|
|
326
|
+
const selections = new Map(
|
|
327
|
+
(input.authoritySelections ?? []).map((selection) => [selection.serverId, selection]),
|
|
328
|
+
);
|
|
212
329
|
for (const server of input.servers) {
|
|
213
330
|
const ref = server.connectionRef;
|
|
214
|
-
if (!ref
|
|
215
|
-
const
|
|
331
|
+
if (!isNativeSubjectConnectionRef(ref)) continue;
|
|
332
|
+
const selection = selections.get(server.id);
|
|
333
|
+
const eligible = connections.filter(
|
|
216
334
|
(candidate) =>
|
|
217
335
|
candidate.subjectId === input.subjectId &&
|
|
218
336
|
candidate.status === "active" &&
|
|
337
|
+
candidate.authorityId !== undefined &&
|
|
338
|
+
candidate.authorityId !== null &&
|
|
219
339
|
sameProviderDomain(candidate.providerDomain, ref.providerDomain) &&
|
|
220
340
|
(!ref.kind || candidate.kind === ref.kind) &&
|
|
341
|
+
(!ref.resource ||
|
|
342
|
+
(typeof candidate.metadata.resource === "string" &&
|
|
343
|
+
canonicalConnectionResource(candidate.metadata.resource) ===
|
|
344
|
+
canonicalConnectionResource(ref.resource))) &&
|
|
221
345
|
(!ref.connectionId || candidate.id === ref.connectionId),
|
|
222
346
|
);
|
|
347
|
+
if (!selection && eligible.length > 1) {
|
|
348
|
+
throw new ConnectionAccountSelectionError(
|
|
349
|
+
`Choose an account for ${server.id}: multiple connected accounts match.`,
|
|
350
|
+
{
|
|
351
|
+
version: 1,
|
|
352
|
+
reason: "ambiguous_account",
|
|
353
|
+
accounts: [{ serverId: server.id, connectionId: null, reason: "selection_unavailable" }],
|
|
354
|
+
},
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
const connection = selection
|
|
358
|
+
? eligible.find((candidate) => candidate.id === selection.connectionId)
|
|
359
|
+
: eligible[0];
|
|
223
360
|
if (!connection) continue;
|
|
361
|
+
selections.delete(server.id);
|
|
224
362
|
delegations.push({
|
|
225
363
|
serverId: server.id,
|
|
226
364
|
connectionId: connection.id,
|
|
365
|
+
originWorkspaceId: connection.workspaceId,
|
|
227
366
|
ownerSubjectId: input.subjectId,
|
|
228
367
|
providerDomain: connection.providerDomain,
|
|
229
368
|
kind: connection.kind,
|
|
230
369
|
});
|
|
231
370
|
}
|
|
371
|
+
if (selections.size > 0) {
|
|
372
|
+
throw new ConnectionAccountSelectionError(
|
|
373
|
+
`Selected account is unavailable for: ${[...selections.keys()].join(", ")}`,
|
|
374
|
+
{
|
|
375
|
+
version: 1,
|
|
376
|
+
reason: "selected_account_unavailable",
|
|
377
|
+
accounts: [...selections.values()].map((selection) => ({
|
|
378
|
+
...selection,
|
|
379
|
+
reason: "selection_unavailable",
|
|
380
|
+
})),
|
|
381
|
+
},
|
|
382
|
+
);
|
|
383
|
+
}
|
|
232
384
|
return delegations;
|
|
233
385
|
}
|
|
234
386
|
|
|
235
387
|
export function personalConnectionDelegationsFromParent(input: {
|
|
236
388
|
servers: McpServerConfig[];
|
|
237
389
|
parentDelegations: McpPersonalConnectionDelegation[];
|
|
390
|
+
personalGitHubResources?: ResourceRef[];
|
|
391
|
+
targetSessionId?: string;
|
|
238
392
|
}): McpPersonalConnectionDelegation[] {
|
|
239
393
|
const mcp = input.servers.flatMap((server) => {
|
|
240
394
|
const ref = server.connectionRef;
|
|
241
|
-
if (!ref
|
|
395
|
+
if (!isNativeSubjectConnectionRef(ref)) return [];
|
|
242
396
|
const delegation = input.parentDelegations.find(
|
|
243
397
|
(candidate) =>
|
|
398
|
+
candidate.connectionType !== "atlassian" &&
|
|
244
399
|
candidate.serverId === server.id &&
|
|
245
400
|
sameProviderDomain(candidate.providerDomain, ref.providerDomain) &&
|
|
246
401
|
(!ref.kind || !candidate.kind || candidate.kind === ref.kind),
|
|
247
402
|
);
|
|
248
403
|
return delegation ? [{ ...delegation }] : [];
|
|
249
404
|
});
|
|
250
|
-
|
|
405
|
+
const projected = [
|
|
251
406
|
...mcp,
|
|
252
407
|
...input.parentDelegations
|
|
253
|
-
.filter(
|
|
408
|
+
.filter(
|
|
409
|
+
(item) =>
|
|
410
|
+
item.connectionType === "social" ||
|
|
411
|
+
item.connectionType === "github_personal" ||
|
|
412
|
+
item.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
|
|
413
|
+
)
|
|
254
414
|
.map((item) => ({ ...item })),
|
|
255
415
|
];
|
|
416
|
+
const requestedGitHub = personalGitHubRepositoryResources(input.personalGitHubResources ?? []);
|
|
417
|
+
const inherited = projected.flatMap((delegation) => {
|
|
418
|
+
if (delegation.connectionType !== "github_personal") return [delegation];
|
|
419
|
+
if (requestedGitHub.length === 0) return [];
|
|
420
|
+
const snapshot = delegation.personalGitHubRepositorySelection;
|
|
421
|
+
if (!snapshot) return [];
|
|
422
|
+
const repositories = requestedGitHub.map((resource) => {
|
|
423
|
+
const parent = snapshot.repositories.find(
|
|
424
|
+
(candidate) =>
|
|
425
|
+
candidate.repositoryId === resource.repositoryId &&
|
|
426
|
+
candidate.canonicalUrl === resource.uri &&
|
|
427
|
+
candidate.ref === resource.ref,
|
|
428
|
+
);
|
|
429
|
+
if (
|
|
430
|
+
!parent ||
|
|
431
|
+
resource.credentialBindingId !== snapshot.credentialBindingId ||
|
|
432
|
+
(resource.access === "write" && parent.access !== "write")
|
|
433
|
+
) {
|
|
434
|
+
throw new Error("agent-created personal GitHub repository exceeds parent authority");
|
|
435
|
+
}
|
|
436
|
+
return { ...parent, access: resource.access };
|
|
437
|
+
});
|
|
438
|
+
return [
|
|
439
|
+
{
|
|
440
|
+
...delegation,
|
|
441
|
+
personalGitHubRepositorySelection: { ...snapshot, repositories },
|
|
442
|
+
},
|
|
443
|
+
];
|
|
444
|
+
});
|
|
445
|
+
if (
|
|
446
|
+
requestedGitHub.length > 0 &&
|
|
447
|
+
!inherited.some((delegation) => delegation.connectionType === "github_personal")
|
|
448
|
+
) {
|
|
449
|
+
throw new Error("agent-created personal GitHub repository authority is unavailable");
|
|
450
|
+
}
|
|
451
|
+
return inherited;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Freezes Google Drive publishing only when one exact subject-owned connection
|
|
456
|
+
* is eligible. Multiple writable Google accounts are intentionally ambiguous:
|
|
457
|
+
* callers must narrow the connection before a later turn can advertise or use
|
|
458
|
+
* the private publication tool.
|
|
459
|
+
*/
|
|
460
|
+
export function googleDrivePublicationDelegationFromVisibleConnections(input: {
|
|
461
|
+
subjectId: string;
|
|
462
|
+
connections: ConnectionMetadata[];
|
|
463
|
+
authoritySelection?: McpConnectionAccountSelection;
|
|
464
|
+
}): McpPersonalConnectionDelegation | null {
|
|
465
|
+
const selection = input.authoritySelection;
|
|
466
|
+
const eligible = input.connections.filter((connection) => {
|
|
467
|
+
if (
|
|
468
|
+
connection.subjectId !== input.subjectId ||
|
|
469
|
+
connection.status !== "active" ||
|
|
470
|
+
!connection.authorityId ||
|
|
471
|
+
connection.kind !== "oauth2" ||
|
|
472
|
+
!sameProviderDomain(connection.providerDomain, GOOGLE_DRIVE_PROVIDER_DOMAIN) ||
|
|
473
|
+
!googleDriveScopesAllowCapability(connection.grantedScopes, "publish_file")
|
|
474
|
+
) {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
const metadata = GoogleDriveConnectionMetadata.safeParse(connection.metadata);
|
|
478
|
+
return Boolean(
|
|
479
|
+
metadata.success &&
|
|
480
|
+
metadata.data.outputDestination &&
|
|
481
|
+
metadata.data.lifecycle?.state !== "paused" &&
|
|
482
|
+
(!metadata.data.lifecycle || metadata.data.lifecycle.state === "active"),
|
|
483
|
+
);
|
|
484
|
+
});
|
|
485
|
+
const selectedEligible = selection
|
|
486
|
+
? eligible.filter((connection) => connection.id === selection.connectionId)
|
|
487
|
+
: eligible;
|
|
488
|
+
if (selection && selectedEligible.length !== 1) {
|
|
489
|
+
throw new Error("Google Drive publication authority selection is unavailable");
|
|
490
|
+
}
|
|
491
|
+
if (!selection && selectedEligible.length !== 1) return null;
|
|
492
|
+
const connection = selectedEligible[0]!;
|
|
493
|
+
if (selection && selection.serverId !== GOOGLE_DRIVE_PUBLICATION_SERVER_ID) {
|
|
494
|
+
throw new Error("Google Drive publication account selection is invalid");
|
|
495
|
+
}
|
|
496
|
+
// Freeze the exact output destination on the accepted delegation: a later
|
|
497
|
+
// connection-settings change must never redirect an already-accepted turn's
|
|
498
|
+
// publication. Eligibility above already proved the metadata parses and the
|
|
499
|
+
// destination exists.
|
|
500
|
+
const frozenMetadata = GoogleDriveConnectionMetadata.parse(connection.metadata);
|
|
501
|
+
if (!frozenMetadata.outputDestination) {
|
|
502
|
+
throw new Error("Google Drive publication destination disappeared during acceptance");
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
serverId: GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
|
|
506
|
+
connectionId: connection.id,
|
|
507
|
+
originWorkspaceId: connection.workspaceId,
|
|
508
|
+
ownerSubjectId: input.subjectId,
|
|
509
|
+
providerDomain: connection.providerDomain,
|
|
510
|
+
kind: connection.kind,
|
|
511
|
+
outputDestination: frozenMetadata.outputDestination,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Native Atlassian credentials remain stored but are not new turn authority. */
|
|
516
|
+
export function personalAtlassianDelegationsFromVisibleConnections(_input: {
|
|
517
|
+
subjectId: string;
|
|
518
|
+
connections: ConnectionMetadata[];
|
|
519
|
+
}): McpPersonalConnectionDelegation[] {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function personalGitHubDelegationFromVisibleConnections(input: {
|
|
524
|
+
db: Database;
|
|
525
|
+
accountId: string;
|
|
526
|
+
subjectId: string;
|
|
527
|
+
connections: ConnectionMetadata[];
|
|
528
|
+
resources: ResourceRef[];
|
|
529
|
+
authoritySelection?: McpConnectionAccountSelection;
|
|
530
|
+
}): Promise<McpPersonalConnectionDelegation | null> {
|
|
531
|
+
const resources = personalGitHubRepositoryResources(input.resources);
|
|
532
|
+
if (resources.length === 0) {
|
|
533
|
+
if (input.authoritySelection) {
|
|
534
|
+
throw new Error("personal GitHub connection authority requires selected repositories");
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
const bindingIds = new Set(resources.map((resource) => resource.credentialBindingId));
|
|
539
|
+
if (bindingIds.size !== 1) {
|
|
540
|
+
throw new Error("accepted work may use only one personal GitHub account");
|
|
541
|
+
}
|
|
542
|
+
const authoritySelection = input.authoritySelection;
|
|
543
|
+
const credentialBindingId = [...bindingIds][0]!;
|
|
544
|
+
const connection = input.connections.find(
|
|
545
|
+
(candidate) =>
|
|
546
|
+
(!authoritySelection || candidate.id === authoritySelection.connectionId) &&
|
|
547
|
+
candidate.subjectId === input.subjectId &&
|
|
548
|
+
candidate.status === "active" &&
|
|
549
|
+
isPersonalGitHubConnection(candidate) &&
|
|
550
|
+
PersonalGitHubConnectionMetadata.safeParse(candidate.metadata).data?.credentialBindingId ===
|
|
551
|
+
credentialBindingId,
|
|
552
|
+
);
|
|
553
|
+
if (
|
|
554
|
+
!connection ||
|
|
555
|
+
!connection.authorityId ||
|
|
556
|
+
connection.grantedScopes.length !== 1 ||
|
|
557
|
+
connection.grantedScopes[0] !== "repo"
|
|
558
|
+
) {
|
|
559
|
+
throw new Error("personal GitHub connection authority selection is unavailable");
|
|
560
|
+
}
|
|
561
|
+
const metadata = PersonalGitHubConnectionMetadata.parse(connection.metadata);
|
|
562
|
+
if (metadata.credentialBindingId !== credentialBindingId) {
|
|
563
|
+
throw new Error("personal GitHub credential binding does not match the selected connection");
|
|
564
|
+
}
|
|
565
|
+
const selection = await getPersonalGitHubRepositorySelectionState(input.db, {
|
|
566
|
+
accountId: input.accountId,
|
|
567
|
+
originWorkspaceId: connection.workspaceId,
|
|
568
|
+
subjectId: input.subjectId,
|
|
569
|
+
connectionId: connection.id,
|
|
570
|
+
});
|
|
571
|
+
if (
|
|
572
|
+
!selection ||
|
|
573
|
+
selection.selectionGeneration <= 0 ||
|
|
574
|
+
selection.credentialBindingId !== credentialBindingId ||
|
|
575
|
+
selection.providerPrincipalId !== metadata.providerPrincipalId
|
|
576
|
+
) {
|
|
577
|
+
throw new Error("personal GitHub repository selection changed or is unavailable");
|
|
578
|
+
}
|
|
579
|
+
const selectedById = new Map(
|
|
580
|
+
selection.repositories.map((repository) => [repository.repositoryId, repository]),
|
|
581
|
+
);
|
|
582
|
+
const repositories = resources.map((resource) => {
|
|
583
|
+
const selected = selectedById.get(resource.repositoryId);
|
|
584
|
+
if (
|
|
585
|
+
!selected ||
|
|
586
|
+
selected.canonicalUrl !== resource.uri ||
|
|
587
|
+
(resource.access === "write" && selected.selectedAccess !== "write")
|
|
588
|
+
) {
|
|
589
|
+
throw new Error("personal GitHub repository resource is outside the selected authority");
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
repositoryId: selected.repositoryId,
|
|
593
|
+
fullName: selected.fullName,
|
|
594
|
+
canonicalUrl: selected.canonicalUrl,
|
|
595
|
+
ref: resource.ref,
|
|
596
|
+
access: resource.access,
|
|
597
|
+
selectionGeneration: selected.selectionGeneration,
|
|
598
|
+
};
|
|
599
|
+
});
|
|
600
|
+
return {
|
|
601
|
+
serverId: PERSONAL_GITHUB_CONNECTION_SURFACE_ID,
|
|
602
|
+
connectionId: connection.id,
|
|
603
|
+
originWorkspaceId: connection.workspaceId,
|
|
604
|
+
ownerSubjectId: input.subjectId,
|
|
605
|
+
providerDomain: "github.com",
|
|
606
|
+
kind: "oauth2",
|
|
607
|
+
connectionType: "github_personal",
|
|
608
|
+
personalGitHubRepositorySelection: {
|
|
609
|
+
credentialBindingId,
|
|
610
|
+
connectionAuthorityGeneration: selection.connectionAuthorityGeneration,
|
|
611
|
+
selectionGeneration: selection.selectionGeneration,
|
|
612
|
+
repositories,
|
|
613
|
+
},
|
|
614
|
+
};
|
|
256
615
|
}
|
|
257
616
|
|
|
258
617
|
export function personalConnectionDelegationsEqual(
|
|
@@ -265,10 +624,14 @@ export function personalConnectionDelegationsEqual(
|
|
|
265
624
|
const other = byServer.get(delegation.serverId);
|
|
266
625
|
return (
|
|
267
626
|
other?.connectionId === delegation.connectionId &&
|
|
627
|
+
other.canonicalServerId === delegation.canonicalServerId &&
|
|
628
|
+
other.originWorkspaceId === delegation.originWorkspaceId &&
|
|
268
629
|
other.ownerSubjectId === delegation.ownerSubjectId &&
|
|
269
630
|
sameProviderDomain(other.providerDomain, delegation.providerDomain) &&
|
|
270
631
|
other.kind === delegation.kind &&
|
|
271
|
-
other.connectionType === delegation.connectionType
|
|
632
|
+
other.connectionType === delegation.connectionType &&
|
|
633
|
+
JSON.stringify(other.personalGitHubRepositorySelection ?? null) ===
|
|
634
|
+
JSON.stringify(delegation.personalGitHubRepositorySelection ?? null)
|
|
272
635
|
);
|
|
273
636
|
});
|
|
274
637
|
}
|
|
@@ -278,7 +641,7 @@ export function personalConnectionDelegationForServer(
|
|
|
278
641
|
server: Pick<McpServerConfig, "id" | "connectionRef">,
|
|
279
642
|
): McpPersonalConnectionDelegation | null {
|
|
280
643
|
const ref = server.connectionRef;
|
|
281
|
-
if (!ref
|
|
644
|
+
if (!isNativeSubjectConnectionRef(ref)) return null;
|
|
282
645
|
return (
|
|
283
646
|
delegations.find(
|
|
284
647
|
(delegation) =>
|
|
@@ -318,15 +681,35 @@ export function withFrozenPersonalConnectionDelegations(input: {
|
|
|
318
681
|
resolveCredential: ConnectionCredentialResolver;
|
|
319
682
|
settings: Pick<Settings, "mcpServers">;
|
|
320
683
|
personalConnectionDelegations: McpPersonalConnectionDelegation[];
|
|
684
|
+
/**
|
|
685
|
+
* Does the frozen delegation owner still hold workspace authority? Supply
|
|
686
|
+
* the canonical `namedSubjectHasLiveWorkspaceAuthority` resolver, never a bare
|
|
687
|
+
* `workspace_memberships` lookup: a managed human's personal workspace has
|
|
688
|
+
* no membership row, so a bare lookup revokes the owner's own connections.
|
|
689
|
+
*/
|
|
321
690
|
ownerHasWorkspaceMembership: (subjectId: string) => Promise<boolean>;
|
|
322
691
|
}): ConnectionCredentialResolver {
|
|
323
692
|
return async (request) => {
|
|
324
693
|
let effectiveRequest = request;
|
|
325
|
-
|
|
694
|
+
const nativeSubjectAuthority = isNativeSubjectConnectionRef(request.connectionRef);
|
|
695
|
+
if (nativeSubjectAuthority) {
|
|
326
696
|
const config = input.settings.mcpServers.find((server) => server.id === request.serverId);
|
|
697
|
+
const publicationDelegations =
|
|
698
|
+
request.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID &&
|
|
699
|
+
sameProviderDomain(request.connectionRef.providerDomain, GOOGLE_DRIVE_PROVIDER_DOMAIN)
|
|
700
|
+
? input.personalConnectionDelegations.filter(
|
|
701
|
+
(candidate) =>
|
|
702
|
+
candidate.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID &&
|
|
703
|
+
sameProviderDomain(candidate.providerDomain, GOOGLE_DRIVE_PROVIDER_DOMAIN) &&
|
|
704
|
+
candidate.kind === "oauth2" &&
|
|
705
|
+
request.connectionRef.kind === "oauth2",
|
|
706
|
+
)
|
|
707
|
+
: [];
|
|
327
708
|
const delegation = config
|
|
328
709
|
? personalConnectionDelegationForServer(input.personalConnectionDelegations, config)
|
|
329
|
-
:
|
|
710
|
+
: publicationDelegations.length === 1
|
|
711
|
+
? publicationDelegations[0]!
|
|
712
|
+
: null;
|
|
330
713
|
if (!delegation || !(await input.ownerHasWorkspaceMembership(delegation.ownerSubjectId))) {
|
|
331
714
|
return personalAuthorityUnavailable(request);
|
|
332
715
|
}
|
|
@@ -342,26 +725,198 @@ export function withFrozenPersonalConnectionDelegations(input: {
|
|
|
342
725
|
};
|
|
343
726
|
}
|
|
344
727
|
const result = await input.resolveCredential(effectiveRequest);
|
|
345
|
-
if (result.status === "ok" ||
|
|
728
|
+
if (result.status === "ok" || !nativeSubjectAuthority) {
|
|
346
729
|
return result;
|
|
347
730
|
}
|
|
348
731
|
return personalAuthorityUnavailable(request);
|
|
349
732
|
};
|
|
350
733
|
}
|
|
351
734
|
|
|
352
|
-
|
|
735
|
+
/** Freeze all native account routes without conflating workspace ownership
|
|
736
|
+
* with the exact causal human required for personal connections. */
|
|
737
|
+
export async function freezeConnectionAccounts(
|
|
738
|
+
input: Parameters<typeof freezePersonalConnectionDelegations>[0] & { accountId: string },
|
|
739
|
+
): Promise<{
|
|
740
|
+
mcpAccountBindings: McpConnectionAccountBinding[] | null;
|
|
741
|
+
personalConnectionDelegations: McpPersonalConnectionDelegation[];
|
|
742
|
+
}> {
|
|
743
|
+
const selectedIds = new Set(input.tools.map((tool) => tool.id));
|
|
744
|
+
const servers = input.settings.mcpServers.filter(
|
|
745
|
+
(server) =>
|
|
746
|
+
selectedIds.has(server.id) &&
|
|
747
|
+
server.connectionRef &&
|
|
748
|
+
server.connectionRef.authoritySource !== "host",
|
|
749
|
+
);
|
|
750
|
+
const serverIds = new Set(servers.map((server) => server.id));
|
|
751
|
+
let mcpAccountBindings: McpConnectionAccountBinding[];
|
|
752
|
+
if (input.source.kind === "turn") {
|
|
753
|
+
if (input.authoritySelections?.length) {
|
|
754
|
+
throw new ConnectionAccountSelectionError(
|
|
755
|
+
"Agent-created work inherits the exact parent accounts",
|
|
756
|
+
{ version: 1, reason: "parent_accounts_required", accounts: [] },
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
const inherited = await getSessionTurnMcpAccountBindings(
|
|
760
|
+
input.db,
|
|
761
|
+
input.workspaceId,
|
|
762
|
+
input.source.sessionId,
|
|
763
|
+
input.source.turnId,
|
|
764
|
+
);
|
|
765
|
+
if (inherited === null) {
|
|
766
|
+
return {
|
|
767
|
+
mcpAccountBindings: null,
|
|
768
|
+
personalConnectionDelegations: await freezePersonalConnectionDelegations(input),
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
mcpAccountBindings = inherited.filter((binding) => serverIds.has(binding.canonicalServerId));
|
|
772
|
+
} else {
|
|
773
|
+
mcpAccountBindings = mcpAccountBindingsFromVisibleConnections({
|
|
774
|
+
accountId: input.accountId,
|
|
775
|
+
workspaceId: input.workspaceId,
|
|
776
|
+
subjectId: input.source.kind === "subject" ? input.source.subjectId : null,
|
|
777
|
+
servers,
|
|
778
|
+
selectionsFrozen: input.authoritySelectionsFrozen === true,
|
|
779
|
+
connections: await visibleMcpAccountConnections(input.db, {
|
|
780
|
+
accountId: input.accountId,
|
|
781
|
+
workspaceId: input.workspaceId,
|
|
782
|
+
source: input.source,
|
|
783
|
+
}),
|
|
784
|
+
...(input.authoritySelections
|
|
785
|
+
? {
|
|
786
|
+
selections: input.authoritySelections.filter((selection) =>
|
|
787
|
+
serverIds.has(selection.serverId),
|
|
788
|
+
),
|
|
789
|
+
}
|
|
790
|
+
: {}),
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
// Dedicated first-party publication/repository/social surfaces keep their
|
|
794
|
+
// existing selection and authority contracts; they are not generic MCP routes.
|
|
795
|
+
const special = await freezePersonalConnectionDelegations({
|
|
796
|
+
...input,
|
|
797
|
+
settings: {
|
|
798
|
+
...input.settings,
|
|
799
|
+
mcpServers: input.settings.mcpServers.filter((server) => !serverIds.has(server.id)),
|
|
800
|
+
},
|
|
801
|
+
...(input.authoritySelections
|
|
802
|
+
? {
|
|
803
|
+
authoritySelections: input.authoritySelections.filter(
|
|
804
|
+
(selection) => !serverIds.has(selection.serverId),
|
|
805
|
+
),
|
|
806
|
+
}
|
|
807
|
+
: {}),
|
|
808
|
+
});
|
|
809
|
+
return {
|
|
810
|
+
mcpAccountBindings,
|
|
811
|
+
personalConnectionDelegations: [
|
|
812
|
+
...personalDelegationsForAccountBindings(mcpAccountBindings),
|
|
813
|
+
...special,
|
|
814
|
+
],
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** Shared workspace accounts plus the entitled owner's own active accounts. */
|
|
819
|
+
export async function visibleMcpAccountConnections(
|
|
820
|
+
db: Database,
|
|
821
|
+
input: {
|
|
822
|
+
accountId: string;
|
|
823
|
+
workspaceId: string;
|
|
824
|
+
source: Exclude<PersonalConnectionDelegationSource, { kind: "turn" }>;
|
|
825
|
+
},
|
|
826
|
+
): Promise<ConnectionMetadata[]> {
|
|
827
|
+
const personal =
|
|
828
|
+
input.source.kind === "subject" &&
|
|
829
|
+
(await ownerStillBelongsToWorkspace(db, {
|
|
830
|
+
accountId: input.accountId,
|
|
831
|
+
workspaceId: input.workspaceId,
|
|
832
|
+
subjectId: input.source.subjectId,
|
|
833
|
+
}))
|
|
834
|
+
? await listOwnConnectionMetadata(db, {
|
|
835
|
+
accountId: input.accountId,
|
|
836
|
+
workspaceId: input.workspaceId,
|
|
837
|
+
subjectId: input.source.subjectId,
|
|
838
|
+
})
|
|
839
|
+
: [];
|
|
840
|
+
const workspace = await listConnectionsMetadata(db, input.workspaceId, null);
|
|
841
|
+
return [...workspace.filter((connection) => connection.subjectId === null), ...personal];
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Every account a fresh, selection-free freeze would bind for the generic MCP
|
|
846
|
+
* routes among `tools`: exactly what `freezeConnectionAccounts` binds when no
|
|
847
|
+
* account is chosen. The same entitlement contract applies: `source` is a
|
|
848
|
+
* grant-derived or frozen-owner subject, never a looked-up one. Read-only; it
|
|
849
|
+
* never persists a delegation.
|
|
850
|
+
*/
|
|
851
|
+
export async function availableMcpAccountBindings(input: {
|
|
353
852
|
db: Database;
|
|
853
|
+
accountId: string;
|
|
354
854
|
workspaceId: string;
|
|
355
855
|
settings: Pick<Settings, "mcpServers">;
|
|
356
856
|
tools: ToolRef[];
|
|
857
|
+
source: Exclude<PersonalConnectionDelegationSource, { kind: "turn" }>;
|
|
858
|
+
/** Result of `visibleMcpAccountConnections` for the same source, when already read. */
|
|
859
|
+
connections?: ConnectionMetadata[];
|
|
860
|
+
}): Promise<McpConnectionAccountBinding[]> {
|
|
861
|
+
const selectedIds = new Set(input.tools.map((tool) => tool.id));
|
|
862
|
+
const servers = input.settings.mcpServers.filter(
|
|
863
|
+
(server) =>
|
|
864
|
+
selectedIds.has(server.id) &&
|
|
865
|
+
server.connectionRef &&
|
|
866
|
+
server.connectionRef.authoritySource !== "host",
|
|
867
|
+
);
|
|
868
|
+
if (servers.length === 0) return [];
|
|
869
|
+
return mcpAccountBindingsFromVisibleConnections({
|
|
870
|
+
accountId: input.accountId,
|
|
871
|
+
workspaceId: input.workspaceId,
|
|
872
|
+
subjectId: input.source.kind === "subject" ? input.source.subjectId : null,
|
|
873
|
+
servers,
|
|
874
|
+
connections: input.connections ?? (await visibleMcpAccountConnections(input.db, input)),
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
export async function freezePersonalConnectionDelegations(input: {
|
|
879
|
+
db: Database;
|
|
880
|
+
workspaceId: string;
|
|
881
|
+
settings: Pick<Settings, "mcpServers"> & Partial<Pick<Settings, "githubPersonalOauthEnabled">>;
|
|
882
|
+
tools: ToolRef[];
|
|
883
|
+
resources?: ResourceRef[];
|
|
357
884
|
source: PersonalConnectionDelegationSource;
|
|
885
|
+
authoritySelections?: McpConnectionAccountSelection[];
|
|
886
|
+
/** Internal durable schedule snapshot, not a default account selector. */
|
|
887
|
+
authoritySelectionsFrozen?: boolean;
|
|
888
|
+
/** Exact first-party export tool + permission gate, not broad opengeni attachment. */
|
|
889
|
+
googleDrivePublicationEnabled?: boolean;
|
|
890
|
+
/** Exact first-party Atlassian tool + permission gate. */
|
|
891
|
+
atlassianEnabled?: boolean;
|
|
892
|
+
/** Exact target for successor-mode projection (`session` never crosses it). */
|
|
893
|
+
targetSessionId?: string;
|
|
358
894
|
}): Promise<McpPersonalConnectionDelegation[]> {
|
|
359
895
|
const servers = selectedPersonalConnectionServers(input.settings, input.tools);
|
|
360
896
|
const includeFirstPartyConnections = input.tools.some((tool) => tool.id === "opengeni");
|
|
361
|
-
|
|
897
|
+
const personalGitHubResources = personalGitHubRepositoryResources(input.resources ?? []);
|
|
898
|
+
if (personalGitHubResources.length > 0 && input.settings.githubPersonalOauthEnabled !== true) {
|
|
899
|
+
throw new Error("personal GitHub repository authority is not enabled");
|
|
900
|
+
}
|
|
901
|
+
if (personalGitHubResources.length > 0 && input.source.kind === "none") {
|
|
902
|
+
throw new Error("personal GitHub repository authority requires an authenticated causal human");
|
|
903
|
+
}
|
|
904
|
+
if (
|
|
905
|
+
servers.length === 0 &&
|
|
906
|
+
!includeFirstPartyConnections &&
|
|
907
|
+
personalGitHubResources.length === 0
|
|
908
|
+
) {
|
|
909
|
+
return [];
|
|
910
|
+
}
|
|
911
|
+
if (input.source.kind === "none") {
|
|
362
912
|
return [];
|
|
363
913
|
}
|
|
364
914
|
if (input.source.kind === "turn") {
|
|
915
|
+
if ((input.authoritySelections?.length ?? 0) > 0) {
|
|
916
|
+
throw new Error(
|
|
917
|
+
"agent-created work inherits connection authority from its exact parent turn",
|
|
918
|
+
);
|
|
919
|
+
}
|
|
365
920
|
const inherited = personalConnectionDelegationsFromParent({
|
|
366
921
|
servers,
|
|
367
922
|
parentDelegations: await getSessionTurnPersonalConnectionDelegations(
|
|
@@ -370,22 +925,99 @@ export async function freezePersonalConnectionDelegations(input: {
|
|
|
370
925
|
input.source.sessionId,
|
|
371
926
|
input.source.turnId,
|
|
372
927
|
),
|
|
928
|
+
personalGitHubResources,
|
|
929
|
+
...(input.targetSessionId ? { targetSessionId: input.targetSessionId } : {}),
|
|
930
|
+
});
|
|
931
|
+
return inherited.filter((item) => {
|
|
932
|
+
if (item.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID) {
|
|
933
|
+
return input.googleDrivePublicationEnabled === true;
|
|
934
|
+
}
|
|
935
|
+
if (item.connectionType === "atlassian") {
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
if (item.connectionType === "social") {
|
|
939
|
+
return includeFirstPartyConnections;
|
|
940
|
+
}
|
|
941
|
+
return true;
|
|
373
942
|
});
|
|
374
|
-
return includeFirstPartyConnections
|
|
375
|
-
? inherited
|
|
376
|
-
: inherited.filter(
|
|
377
|
-
(item) => item.connectionType !== "social" && item.connectionType !== "atlassian",
|
|
378
|
-
);
|
|
379
943
|
}
|
|
380
|
-
const
|
|
381
|
-
|
|
944
|
+
const ownerSubjectId = input.source.subjectId;
|
|
945
|
+
const membership = await ownerStillBelongsToWorkspace(input.db, {
|
|
946
|
+
accountId: input.source.accountId,
|
|
947
|
+
workspaceId: input.workspaceId,
|
|
948
|
+
subjectId: ownerSubjectId,
|
|
949
|
+
});
|
|
950
|
+
if (!membership) {
|
|
951
|
+
if ((input.authoritySelections?.length ?? 0) > 0) {
|
|
952
|
+
throw new ConnectionAccountSelectionError(
|
|
953
|
+
"Selected connection account requires live owner workspace access",
|
|
954
|
+
{ version: 1, reason: "owner_access_unavailable", accounts: [] },
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
if (personalGitHubResources.length > 0) {
|
|
958
|
+
throw new Error("personal GitHub repository authority requires live causal user authority");
|
|
959
|
+
}
|
|
960
|
+
return [];
|
|
961
|
+
}
|
|
962
|
+
const visibleConnections = await listOwnConnectionMetadata(input.db, {
|
|
963
|
+
accountId: input.source.accountId,
|
|
964
|
+
workspaceId: input.workspaceId,
|
|
965
|
+
subjectId: ownerSubjectId,
|
|
966
|
+
});
|
|
967
|
+
const selectedMcpServerIds = new Set(servers.map((server) => server.id));
|
|
968
|
+
const supportedSelectionIds = new Set(selectedMcpServerIds);
|
|
969
|
+
if (input.googleDrivePublicationEnabled) {
|
|
970
|
+
supportedSelectionIds.add(GOOGLE_DRIVE_PUBLICATION_SERVER_ID);
|
|
971
|
+
}
|
|
972
|
+
if (personalGitHubResources.length > 0) {
|
|
973
|
+
supportedSelectionIds.add(PERSONAL_GITHUB_CONNECTION_SURFACE_ID);
|
|
974
|
+
}
|
|
975
|
+
const unsupportedSelections = (input.authoritySelections ?? []).filter(
|
|
976
|
+
(selection) => !supportedSelectionIds.has(selection.serverId),
|
|
977
|
+
);
|
|
978
|
+
if (unsupportedSelections.length > 0) {
|
|
979
|
+
throw new ConnectionAccountSelectionError(
|
|
980
|
+
`connection authority selection did not match a selected MCP server: ${unsupportedSelections
|
|
981
|
+
.map((selection) => selection.serverId)
|
|
982
|
+
.join(", ")}`,
|
|
983
|
+
{
|
|
984
|
+
version: 1,
|
|
985
|
+
reason: "selected_account_unavailable",
|
|
986
|
+
accounts: unsupportedSelections.map((selection) => ({
|
|
987
|
+
...selection,
|
|
988
|
+
reason: "connector_unavailable",
|
|
989
|
+
})),
|
|
990
|
+
},
|
|
991
|
+
);
|
|
992
|
+
}
|
|
382
993
|
const mcp = personalConnectionDelegationsFromVisibleConnections({
|
|
383
994
|
servers,
|
|
384
|
-
subjectId:
|
|
385
|
-
connections:
|
|
995
|
+
subjectId: ownerSubjectId,
|
|
996
|
+
connections: visibleConnections,
|
|
997
|
+
authoritySelections: (input.authoritySelections ?? []).filter((selection) =>
|
|
998
|
+
selectedMcpServerIds.has(selection.serverId),
|
|
999
|
+
),
|
|
386
1000
|
});
|
|
387
|
-
|
|
388
|
-
|
|
1001
|
+
const personalGitHubAuthoritySelection = (input.authoritySelections ?? []).find(
|
|
1002
|
+
(selection) => selection.serverId === PERSONAL_GITHUB_CONNECTION_SURFACE_ID,
|
|
1003
|
+
);
|
|
1004
|
+
const personalGitHub = await personalGitHubDelegationFromVisibleConnections({
|
|
1005
|
+
db: input.db,
|
|
1006
|
+
accountId: input.source.accountId,
|
|
1007
|
+
subjectId: ownerSubjectId,
|
|
1008
|
+
connections: input.authoritySelectionsFrozen
|
|
1009
|
+
? visibleConnections.filter(
|
|
1010
|
+
(connection) => connection.id === personalGitHubAuthoritySelection?.connectionId,
|
|
1011
|
+
)
|
|
1012
|
+
: visibleConnections,
|
|
1013
|
+
resources: input.resources ?? [],
|
|
1014
|
+
...(personalGitHubAuthoritySelection
|
|
1015
|
+
? { authoritySelection: personalGitHubAuthoritySelection }
|
|
1016
|
+
: {}),
|
|
1017
|
+
});
|
|
1018
|
+
if (!includeFirstPartyConnections) {
|
|
1019
|
+
return [...mcp, ...(personalGitHub ? [personalGitHub] : [])];
|
|
1020
|
+
}
|
|
389
1021
|
const visible = await listSocialConnections(input.db, input.workspaceId, 500, ownerSubjectId);
|
|
390
1022
|
const latest = new Map<"x" | "reddit", (typeof visible)[number]>();
|
|
391
1023
|
for (const connection of visible) {
|
|
@@ -399,16 +1031,34 @@ export async function freezePersonalConnectionDelegations(input: {
|
|
|
399
1031
|
if (!prior || connection.updatedAt > prior.updatedAt)
|
|
400
1032
|
latest.set(connection.provider, connection);
|
|
401
1033
|
}
|
|
402
|
-
const personalAtlassian =
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
1034
|
+
const personalAtlassian = input.atlassianEnabled
|
|
1035
|
+
? personalAtlassianDelegationsFromVisibleConnections({
|
|
1036
|
+
subjectId: ownerSubjectId,
|
|
1037
|
+
connections: visibleConnections,
|
|
1038
|
+
})
|
|
1039
|
+
: [];
|
|
1040
|
+
const googleDriveAuthoritySelection = input.googleDrivePublicationEnabled
|
|
1041
|
+
? (input.authoritySelections ?? []).find(
|
|
1042
|
+
(selection) => selection.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
|
|
1043
|
+
)
|
|
1044
|
+
: undefined;
|
|
1045
|
+
const googleDrivePublication = input.googleDrivePublicationEnabled
|
|
1046
|
+
? googleDrivePublicationDelegationFromVisibleConnections({
|
|
1047
|
+
subjectId: ownerSubjectId,
|
|
1048
|
+
connections: input.authoritySelectionsFrozen
|
|
1049
|
+
? visibleConnections.filter(
|
|
1050
|
+
(connection) => connection.id === googleDriveAuthoritySelection?.connectionId,
|
|
1051
|
+
)
|
|
1052
|
+
: visibleConnections,
|
|
1053
|
+
...(googleDriveAuthoritySelection
|
|
1054
|
+
? { authoritySelection: googleDriveAuthoritySelection }
|
|
1055
|
+
: {}),
|
|
1056
|
+
})
|
|
1057
|
+
: null;
|
|
410
1058
|
return [
|
|
411
1059
|
...mcp,
|
|
1060
|
+
...(personalGitHub ? [personalGitHub] : []),
|
|
1061
|
+
...(googleDrivePublication ? [googleDrivePublication] : []),
|
|
412
1062
|
...[...latest.values()].map((connection) => ({
|
|
413
1063
|
serverId: `social:${connection.provider}`,
|
|
414
1064
|
connectionId: connection.id,
|
|
@@ -417,13 +1067,6 @@ export async function freezePersonalConnectionDelegations(input: {
|
|
|
417
1067
|
kind: "oauth2" as const,
|
|
418
1068
|
connectionType: "social" as const,
|
|
419
1069
|
})),
|
|
420
|
-
...personalAtlassian
|
|
421
|
-
serverId: `atlassian:${connection.id}`,
|
|
422
|
-
connectionId: connection.id,
|
|
423
|
-
ownerSubjectId,
|
|
424
|
-
providerDomain: connection.providerDomain,
|
|
425
|
-
kind: connection.kind,
|
|
426
|
-
connectionType: "atlassian" as const,
|
|
427
|
-
})),
|
|
1070
|
+
...personalAtlassian,
|
|
428
1071
|
];
|
|
429
1072
|
}
|