@opengeni/db 0.28.1 → 0.31.1
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/attached-browser-devices.d.ts +42 -0
- package/dist/attempt-tool-catalogs.d.ts +16 -0
- package/dist/browser-auth.d.ts +91 -0
- package/dist/browser-identities.d.ts +128 -0
- package/dist/browser-sessions.d.ts +272 -0
- package/dist/browser-state-artifacts.d.ts +17 -0
- package/dist/capability-components.d.ts +37 -0
- package/dist/capability-integrations.d.ts +192 -0
- package/dist/chunk-F5FT7BEZ.js +3535 -0
- package/dist/chunk-F5FT7BEZ.js.map +1 -0
- package/dist/chunk-ISI2TIUG.js +639 -0
- package/dist/chunk-ISI2TIUG.js.map +1 -0
- package/dist/chunk-MWDVXQOL.js +11639 -0
- package/dist/chunk-MWDVXQOL.js.map +1 -0
- package/dist/chunk-NBE6CHRI.js +955 -0
- package/dist/chunk-NBE6CHRI.js.map +1 -0
- package/dist/{chunk-DBS5HPEW.js → chunk-YHZXQ7HP.js} +760 -11
- package/dist/chunk-YHZXQ7HP.js.map +1 -0
- package/dist/codemode-operations.d.ts +117 -0
- package/dist/codex-token-resolver.d.ts +2 -2
- package/dist/company-profile-schema.d.ts +798 -0
- package/dist/company-profile.d.ts +268 -0
- package/dist/computer-sessions.d.ts +109 -0
- package/dist/connection-token-resolver.d.ts +9 -4
- package/dist/database.d.ts +52 -3
- package/dist/editable-artifact-durable-export.d.ts +153 -0
- package/dist/editable-artifact-durable-export.js +788 -0
- package/dist/editable-artifact-durable-export.js.map +1 -0
- package/dist/editable-artifact-materialization.d.ts +123 -0
- package/dist/editable-artifacts-schema.d.ts +4152 -0
- package/dist/editable-artifacts.d.ts +610 -0
- package/dist/editable-artifacts.js +26 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/environment-crypto.d.ts +8 -4
- package/dist/generated-images.d.ts +139 -0
- package/dist/index.d.ts +958 -126
- package/dist/index.js +31204 -12172
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +51 -4
- package/dist/integration-bindings.d.ts +70 -0
- package/dist/integration-features.d.ts +99 -0
- package/dist/interaction-revisions.d.ts +3 -0
- package/dist/interaction-schema.d.ts +5372 -0
- package/dist/knowledge-source-sync-schema.d.ts +1679 -0
- package/dist/knowledge-source-sync.d.ts +274 -0
- package/dist/lossless-columns.d.ts +58 -0
- package/dist/lossless-json.d.ts +39 -0
- package/dist/memory-domain.d.ts +3 -6
- package/dist/memory-governance.d.ts +2 -2
- package/dist/memory-slack-delivery.d.ts +101 -0
- package/dist/new-session-drafts.d.ts +2 -2
- package/dist/pack-components.d.ts +76 -0
- package/dist/pack-installations.d.ts +86 -0
- package/dist/persistence-errors.d.ts +7 -14
- package/dist/plugin-packages.d.ts +126 -0
- package/dist/preference-registry.d.ts +1 -1
- package/dist/provision-roles.d.ts +9 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +18 -6
- package/dist/schema.d.ts +19122 -8731
- package/dist/schema.js +173 -1
- package/dist/scoped-knowledge.d.ts +97 -1
- package/dist/session-control.d.ts +4 -4
- package/dist/session-queue-commands.d.ts +19 -11
- package/dist/session-realtime-context.d.ts +2 -2
- package/dist/session-realtime-ledger.d.ts +4 -4
- package/dist/session-realtime-mirror.d.ts +2 -2
- package/dist/session-realtime-state.d.ts +1 -1
- package/dist/session-realtime-terminal.d.ts +8 -1
- package/dist/session-realtime.d.ts +7 -7
- package/dist/session-tool-call-settlement.d.ts +1 -1
- package/dist/slack-user-link-access.d.ts +54 -0
- package/dist/transcription-recordings-schema.d.ts +44 -6
- package/dist/transcription-recordings.d.ts +3 -3
- package/dist/turn-initiator.d.ts +2 -2
- package/dist/video-generation.d.ts +216 -0
- package/dist/video-generation.js +66 -0
- package/dist/video-generation.js.map +1 -0
- package/dist/workspace-artifacts.d.ts +1 -1
- package/dist/workspace-instruction-policies.d.ts +1 -1
- package/dist/workspace-learning-policy-schema.d.ts +932 -0
- package/dist/workspace-learning-policy.d.ts +161 -0
- package/drizzle/0065_enrollment_credential_generation.sql +10 -0
- package/drizzle/0140_retained_screenshot_artifacts.sql +192 -0
- package/drizzle/0176_lossless_canonical_json.sql +975 -0
- package/drizzle/0177_session_events_workspace_turn_type_index.sql +5 -0
- package/drizzle/0178_permissioned_secret_reads.sql +14 -0
- package/drizzle/0179_slack_private_shortcut_delivery_gate.sql +85 -0
- package/drizzle/0180_retained_screenshot_lifecycle_fences.sql +273 -0
- package/drizzle/0181_connected_machine_removal.sql +52 -0
- package/drizzle/0182_connected_machine_remove_session_default.sql +77 -0
- package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
- package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
- package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
- package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
- package/drizzle/0187_generated_image_artifacts.sql +173 -0
- package/drizzle/0188_image_generation_retention_failure.sql +25 -0
- package/drizzle/0189_retained_session_image_formats.sql +13 -0
- package/drizzle/0190_timeline_annotations.sql +31 -0
- package/drizzle/0191_editable_artifact_engine.sql +3853 -0
- package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
- package/drizzle/0193_editable_artifact_authorization.sql +190 -0
- package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
- package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
- package/drizzle/0196_rig_provider_images.sql +152 -0
- package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
- package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
- package/drizzle/0199_workspace_learning_policy.sql +980 -0
- package/drizzle/0201_company_profile_authority.sql +849 -0
- package/drizzle/0202_document_index_checkpoints.sql +85 -0
- package/drizzle/0203_durable_video_generation.sql +353 -0
- package/drizzle/0204_video_generation_funding.sql +44 -0
- package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
- package/drizzle/0206_browser_sessions.sql +646 -0
- package/drizzle/0207_browser_identities.sql +379 -0
- package/drizzle/0208_attached_browser_devices.sql +165 -0
- package/drizzle/0209_computer_sessions.sql +575 -0
- package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
- package/drizzle/0211_editable_artifact_session_links.sql +58 -0
- package/drizzle/0212_slack_installation_bindings.sql +348 -0
- package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
- package/drizzle/0214_session_activity_commit_gate.sql +262 -0
- package/drizzle/0215_capabilities_platform.sql +980 -0
- package/drizzle/0216_pack_component_ownership.sql +235 -0
- package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
- package/package.json +18 -5
- package/src/attached-browser-devices.ts +371 -0
- package/src/attempt-tool-catalogs.ts +131 -0
- package/src/browser-auth.ts +1896 -0
- package/src/browser-identities.ts +1168 -0
- package/src/browser-sessions.ts +2005 -0
- package/src/browser-state-artifacts.ts +59 -0
- package/src/capability-components.ts +166 -0
- package/src/capability-integrations.ts +1663 -0
- package/src/codemode-operations.ts +674 -0
- package/src/company-profile-schema.ts +143 -0
- package/src/company-profile.ts +968 -0
- package/src/computer-sessions.ts +1130 -0
- package/src/connection-token-resolver.ts +270 -25
- package/src/database.ts +434 -24
- package/src/editable-artifact-durable-export.ts +1201 -0
- package/src/editable-artifact-materialization.ts +893 -0
- package/src/editable-artifacts-schema.ts +879 -0
- package/src/editable-artifacts.ts +5404 -0
- package/src/environment-crypto.ts +22 -9
- package/src/generated-images.ts +740 -0
- package/src/index.ts +10117 -2730
- package/src/insights.ts +160 -13
- package/src/integration-bindings.ts +455 -0
- package/src/integration-features.ts +859 -0
- package/src/interaction-revisions.ts +40 -0
- package/src/interaction-schema.ts +1303 -0
- package/src/knowledge-source-sync-schema.ts +192 -0
- package/src/knowledge-source-sync.ts +1922 -0
- package/src/lossless-columns.ts +35 -0
- package/src/lossless-json.ts +322 -0
- package/src/memory-domain.ts +5 -44
- package/src/memory-slack-delivery.ts +801 -0
- package/src/pack-components.ts +954 -0
- package/src/pack-installations.ts +929 -0
- package/src/persistence-errors.ts +26 -34
- package/src/plugin-packages.ts +1186 -0
- package/src/provision-roles.ts +302 -1
- package/src/runtime-posture-cli.ts +9 -0
- package/src/runtime-posture.ts +514 -5
- package/src/schema.ts +5177 -2957
- package/src/scoped-knowledge-schema.ts +1 -1
- package/src/scoped-knowledge.ts +655 -3
- package/src/session-control.ts +133 -81
- package/src/session-queue-commands.ts +511 -247
- package/src/session-realtime-context.ts +20 -7
- package/src/session-realtime-ledger.ts +32 -10
- package/src/session-realtime-mirror.ts +4 -2
- package/src/session-realtime-terminal.ts +89 -21
- package/src/session-realtime.ts +10 -9
- package/src/session-tool-call-settlement.ts +38 -15
- package/src/slack-user-link-access.ts +702 -0
- package/src/transcription-recordings-schema.ts +5 -2
- package/src/transcription-recordings.ts +70 -40
- package/src/video-generation.ts +1452 -0
- package/src/workspace-learning-policy-schema.ts +139 -0
- package/src/workspace-learning-policy.ts +595 -0
- package/dist/chunk-DBS5HPEW.js.map +0 -1
- package/dist/chunk-HT5T62CC.js +0 -6803
- package/dist/chunk-HT5T62CC.js.map +0 -1
- package/dist/event-payload-sanitizer.d.ts +0 -32
- package/src/event-payload-sanitizer.ts +0 -377
|
@@ -0,0 +1,1663 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { stableJson, type McpServerConnectionRef } from "@opengeni/contracts";
|
|
4
|
+
import { and, asc, eq, inArray, ne, or, sql } from "drizzle-orm";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
assertCapabilityComponentVersionCanChange,
|
|
8
|
+
effectiveCapabilityOwnerSql,
|
|
9
|
+
lockCapabilityComponentIdentity,
|
|
10
|
+
} from "./capability-components";
|
|
11
|
+
import {
|
|
12
|
+
setSubjectRlsContext,
|
|
13
|
+
withRlsContext,
|
|
14
|
+
withWorkspaceRls,
|
|
15
|
+
withWorkspaceSubjectRls,
|
|
16
|
+
type Database,
|
|
17
|
+
} from "./database";
|
|
18
|
+
import {
|
|
19
|
+
ensureIntegrationFeatureFacet,
|
|
20
|
+
integrationBindingKey,
|
|
21
|
+
integrationDefinitionHasBindingOwner,
|
|
22
|
+
integrationRuntimeKey,
|
|
23
|
+
listIntegrationFeatureBindingOwners,
|
|
24
|
+
removeIntegrationFeatureBindingOwner,
|
|
25
|
+
upsertIntegrationFeatureBinding,
|
|
26
|
+
type IntegrationFeatureBindingOwner,
|
|
27
|
+
} from "./integration-bindings";
|
|
28
|
+
import * as schema from "./schema";
|
|
29
|
+
|
|
30
|
+
export type ApiIntegrationProtocol = "openapi" | "graphql";
|
|
31
|
+
export type ApiIntegrationToolSafety = "read" | "write" | "destructive";
|
|
32
|
+
export type ApiIntegrationApprovalMode = "never" | "ask";
|
|
33
|
+
|
|
34
|
+
export type ApiIntegrationFeatureDefinition = {
|
|
35
|
+
readonly featureKey: string;
|
|
36
|
+
readonly kind: "knowledge_source" | "inbound_trigger" | "delivery_destination" | "identity_link";
|
|
37
|
+
readonly configSchema: Readonly<Record<string, unknown>>;
|
|
38
|
+
readonly capabilities: Readonly<Record<string, unknown>>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ApiIntegrationToolDefinition = {
|
|
42
|
+
readonly id: string;
|
|
43
|
+
readonly operationKey: string;
|
|
44
|
+
readonly name: string;
|
|
45
|
+
readonly description: string;
|
|
46
|
+
readonly inputSchema: Readonly<Record<string, unknown>>;
|
|
47
|
+
readonly outputSchema?: Readonly<Record<string, unknown>>;
|
|
48
|
+
readonly safety: ApiIntegrationToolSafety;
|
|
49
|
+
readonly approvalMode: ApiIntegrationApprovalMode;
|
|
50
|
+
readonly deprecated: boolean;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type StoredApiIntegrationRevisionBase = {
|
|
54
|
+
readonly id: string;
|
|
55
|
+
readonly integrationId: string;
|
|
56
|
+
readonly contentSha256: string;
|
|
57
|
+
readonly source: {
|
|
58
|
+
readonly url?: string;
|
|
59
|
+
readonly provider?: string;
|
|
60
|
+
readonly fetchedAt?: string;
|
|
61
|
+
};
|
|
62
|
+
readonly title: string;
|
|
63
|
+
readonly description?: string;
|
|
64
|
+
readonly version?: string;
|
|
65
|
+
readonly tools: readonly ApiIntegrationToolDefinition[];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type StoredOpenApiOperationBinding = {
|
|
69
|
+
readonly method: "get" | "put" | "post" | "delete" | "patch" | "head" | "options" | "trace";
|
|
70
|
+
readonly pathTemplate: string;
|
|
71
|
+
readonly serverUrl: string;
|
|
72
|
+
readonly parameters: readonly {
|
|
73
|
+
readonly name: string;
|
|
74
|
+
readonly location: "path" | "query" | "header" | "cookie";
|
|
75
|
+
readonly required: boolean;
|
|
76
|
+
readonly schema: Readonly<Record<string, unknown>>;
|
|
77
|
+
readonly description?: string;
|
|
78
|
+
}[];
|
|
79
|
+
readonly requestBody?: {
|
|
80
|
+
readonly required: boolean;
|
|
81
|
+
readonly contentTypes: readonly string[];
|
|
82
|
+
readonly schemas: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
83
|
+
};
|
|
84
|
+
readonly requiredScopeAlternatives?: readonly (readonly string[])[];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type StoredGraphqlOperationBinding = {
|
|
88
|
+
readonly kind: "query" | "mutation";
|
|
89
|
+
readonly fieldName: string;
|
|
90
|
+
readonly operationName: string;
|
|
91
|
+
readonly variableDefinitions: readonly string[];
|
|
92
|
+
readonly variableNames: readonly string[];
|
|
93
|
+
readonly defaultSelection?: string;
|
|
94
|
+
readonly selectionAllowed: boolean;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** Protocol-normalized immutable revision persisted in integration_spec_revisions.spec. */
|
|
98
|
+
export type StoredApiIntegrationRevision =
|
|
99
|
+
| (StoredApiIntegrationRevisionBase & {
|
|
100
|
+
readonly protocol: "openapi";
|
|
101
|
+
readonly bindings: Readonly<Record<string, StoredOpenApiOperationBinding>>;
|
|
102
|
+
})
|
|
103
|
+
| (StoredApiIntegrationRevisionBase & {
|
|
104
|
+
readonly protocol: "graphql";
|
|
105
|
+
readonly bindings: Readonly<Record<string, StoredGraphqlOperationBinding>>;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export type InstallApiIntegrationInput = {
|
|
109
|
+
accountId: string;
|
|
110
|
+
workspaceId: string;
|
|
111
|
+
subjectId: string;
|
|
112
|
+
capabilityId: string;
|
|
113
|
+
pluginKey: string;
|
|
114
|
+
serverId: string;
|
|
115
|
+
name: string;
|
|
116
|
+
description?: string | null;
|
|
117
|
+
category?: string;
|
|
118
|
+
tags?: string[];
|
|
119
|
+
presetId?: string | null;
|
|
120
|
+
provider?: string;
|
|
121
|
+
providerDomain: string;
|
|
122
|
+
protocol: ApiIntegrationProtocol;
|
|
123
|
+
baseUrl: string;
|
|
124
|
+
sourceUrl?: string | null;
|
|
125
|
+
authScheme?: Record<string, unknown>;
|
|
126
|
+
connectionId?: string | null;
|
|
127
|
+
instanceKey?: string;
|
|
128
|
+
displayName?: string;
|
|
129
|
+
expectedInstanceVersion?: number;
|
|
130
|
+
requiredScopes?: string[];
|
|
131
|
+
ownership?: "workspace" | "subject" | "either";
|
|
132
|
+
allowedTools?: string[];
|
|
133
|
+
featureDefinitions?: readonly ApiIntegrationFeatureDefinition[];
|
|
134
|
+
revision: StoredApiIntegrationRevision;
|
|
135
|
+
owner?: ApiIntegrationOwner;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export type InstalledApiIntegration = {
|
|
139
|
+
capabilityId: string;
|
|
140
|
+
pluginId: string;
|
|
141
|
+
pluginVersionId: string;
|
|
142
|
+
integrationFacetId: string;
|
|
143
|
+
apiFacetId: string;
|
|
144
|
+
pluginInstallationId: string;
|
|
145
|
+
integrationFacetInstallationId: string;
|
|
146
|
+
apiFacetInstallationId: string;
|
|
147
|
+
installationVersion: number;
|
|
148
|
+
instanceId: string;
|
|
149
|
+
instanceKey: string;
|
|
150
|
+
displayName: string;
|
|
151
|
+
instanceVersion: number;
|
|
152
|
+
revisionId: string;
|
|
153
|
+
serverId: string;
|
|
154
|
+
status: "installed";
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export type ApiIntegrationRuntime = {
|
|
158
|
+
capabilityId: string;
|
|
159
|
+
pluginKey: string;
|
|
160
|
+
pluginInstallationId: string;
|
|
161
|
+
installationVersion: number;
|
|
162
|
+
instanceId: string;
|
|
163
|
+
instanceKey: string;
|
|
164
|
+
displayName: string;
|
|
165
|
+
instanceVersion: number;
|
|
166
|
+
serverId: string;
|
|
167
|
+
name: string;
|
|
168
|
+
description: string | null;
|
|
169
|
+
protocol: ApiIntegrationProtocol;
|
|
170
|
+
presetId: string | null;
|
|
171
|
+
baseUrl: string;
|
|
172
|
+
sourceUrl: string | null;
|
|
173
|
+
providerDomain: string;
|
|
174
|
+
authScheme: Record<string, unknown>;
|
|
175
|
+
connectionRef: McpServerConnectionRef | null;
|
|
176
|
+
allowedTools: string[];
|
|
177
|
+
requireApproval: true | string[];
|
|
178
|
+
revision: StoredApiIntegrationRevision;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export type ApiIntegrationOwner = {
|
|
182
|
+
kind: "direct" | "plugin" | "pack" | "migration";
|
|
183
|
+
id: string;
|
|
184
|
+
removable: boolean;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export type ApiIntegrationUninstallPreview = {
|
|
188
|
+
capabilityId: string;
|
|
189
|
+
instanceKey: string;
|
|
190
|
+
displayName: string | null;
|
|
191
|
+
installed: boolean;
|
|
192
|
+
installationVersion: number | null;
|
|
193
|
+
instanceVersion: number | null;
|
|
194
|
+
directOwner: ApiIntegrationOwner | null;
|
|
195
|
+
remainingOwners: ApiIntegrationOwner[];
|
|
196
|
+
removesRuntimeIntegration: boolean;
|
|
197
|
+
removesDefinition: boolean;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export type UninstallApiIntegrationResult = {
|
|
201
|
+
capabilityId: string;
|
|
202
|
+
instanceKey: string;
|
|
203
|
+
status: "not_installed" | "uninstalled" | "retained_by_other_owners";
|
|
204
|
+
remainingOwners: ApiIntegrationOwner[];
|
|
205
|
+
definitionStatus: "retained" | "disabled";
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export class ApiIntegrationInstallationVersionConflictError extends Error {
|
|
209
|
+
readonly name = "ApiIntegrationInstallationVersionConflictError";
|
|
210
|
+
|
|
211
|
+
constructor(
|
|
212
|
+
readonly capabilityId: string,
|
|
213
|
+
readonly expectedVersion: number,
|
|
214
|
+
readonly actualVersion: number,
|
|
215
|
+
) {
|
|
216
|
+
super(
|
|
217
|
+
`API Integration ${capabilityId} changed: expected version ${expectedVersion}, current version ${actualVersion}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function installApiIntegration(
|
|
223
|
+
db: Database,
|
|
224
|
+
input: InstallApiIntegrationInput,
|
|
225
|
+
): Promise<InstalledApiIntegration> {
|
|
226
|
+
assertInstallInput(input);
|
|
227
|
+
return await withRlsContext(
|
|
228
|
+
db,
|
|
229
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
230
|
+
async (scopedDb) => {
|
|
231
|
+
await setSubjectRlsContext(scopedDb, input.subjectId);
|
|
232
|
+
return await scopedDb.transaction(async (tx) => {
|
|
233
|
+
const now = new Date();
|
|
234
|
+
await lockCapabilityComponentIdentity(
|
|
235
|
+
tx as unknown as Database,
|
|
236
|
+
input.workspaceId,
|
|
237
|
+
input.pluginKey,
|
|
238
|
+
);
|
|
239
|
+
const connection = input.connectionId
|
|
240
|
+
? await loadInstallConnection(tx as unknown as Database, input)
|
|
241
|
+
: null;
|
|
242
|
+
let [plugin] = await tx
|
|
243
|
+
.select()
|
|
244
|
+
.from(schema.capabilityPlugins)
|
|
245
|
+
.where(
|
|
246
|
+
and(
|
|
247
|
+
eq(schema.capabilityPlugins.workspaceId, input.workspaceId),
|
|
248
|
+
eq(schema.capabilityPlugins.pluginKey, input.pluginKey),
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
.for("update")
|
|
252
|
+
.limit(1);
|
|
253
|
+
if (!plugin) {
|
|
254
|
+
[plugin] = await tx
|
|
255
|
+
.insert(schema.capabilityPlugins)
|
|
256
|
+
.values({
|
|
257
|
+
pluginKey: input.pluginKey,
|
|
258
|
+
accountId: input.accountId,
|
|
259
|
+
workspaceId: input.workspaceId,
|
|
260
|
+
name: input.name,
|
|
261
|
+
description: input.description ?? null,
|
|
262
|
+
category: input.category ?? "integrations",
|
|
263
|
+
tags: normalizedStrings(input.tags ?? ["integration", input.protocol], 64),
|
|
264
|
+
provenance: "workspace",
|
|
265
|
+
})
|
|
266
|
+
.returning();
|
|
267
|
+
} else if (plugin.accountId !== input.accountId) {
|
|
268
|
+
throw new Error("API Integration plugin tenant mismatch");
|
|
269
|
+
} else if (
|
|
270
|
+
plugin.name !== input.name ||
|
|
271
|
+
plugin.description !== (input.description ?? null) ||
|
|
272
|
+
plugin.category !== (input.category ?? "integrations")
|
|
273
|
+
) {
|
|
274
|
+
[plugin] = await tx
|
|
275
|
+
.update(schema.capabilityPlugins)
|
|
276
|
+
.set({
|
|
277
|
+
name: input.name,
|
|
278
|
+
description: input.description ?? null,
|
|
279
|
+
category: input.category ?? "integrations",
|
|
280
|
+
tags: normalizedStrings(input.tags ?? ["integration", input.protocol], 64),
|
|
281
|
+
updatedAt: now,
|
|
282
|
+
})
|
|
283
|
+
.where(eq(schema.capabilityPlugins.id, plugin.id))
|
|
284
|
+
.returning();
|
|
285
|
+
}
|
|
286
|
+
if (!plugin) throw new Error("Failed to create API Integration plugin");
|
|
287
|
+
|
|
288
|
+
const manifest = {
|
|
289
|
+
schemaVersion: 1,
|
|
290
|
+
kind: "integration",
|
|
291
|
+
capabilityId: input.capabilityId,
|
|
292
|
+
serverId: input.serverId,
|
|
293
|
+
protocol: input.protocol,
|
|
294
|
+
presetId: input.presetId ?? null,
|
|
295
|
+
provider: input.provider ?? null,
|
|
296
|
+
providerDomain: input.providerDomain,
|
|
297
|
+
baseUrl: input.baseUrl,
|
|
298
|
+
sourceUrl: input.sourceUrl ?? null,
|
|
299
|
+
revisionId: input.revision.id,
|
|
300
|
+
contentSha256: input.revision.contentSha256,
|
|
301
|
+
toolCount: input.revision.tools.length,
|
|
302
|
+
};
|
|
303
|
+
const manifestDigest = sha256(stableJson(manifest));
|
|
304
|
+
let [pluginVersion] = await tx
|
|
305
|
+
.select()
|
|
306
|
+
.from(schema.capabilityPluginVersions)
|
|
307
|
+
.where(
|
|
308
|
+
and(
|
|
309
|
+
eq(schema.capabilityPluginVersions.pluginId, plugin.id),
|
|
310
|
+
eq(schema.capabilityPluginVersions.version, input.revision.id),
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
.for("update")
|
|
314
|
+
.limit(1);
|
|
315
|
+
if (pluginVersion && pluginVersion.manifestDigest !== manifestDigest) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`API Integration revision ${input.revision.id} conflicts with stored content`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
if (!pluginVersion) {
|
|
321
|
+
[pluginVersion] = await tx
|
|
322
|
+
.insert(schema.capabilityPluginVersions)
|
|
323
|
+
.values({
|
|
324
|
+
pluginId: plugin.id,
|
|
325
|
+
version: input.revision.id,
|
|
326
|
+
manifestDigest,
|
|
327
|
+
manifest,
|
|
328
|
+
status: "published",
|
|
329
|
+
})
|
|
330
|
+
.returning();
|
|
331
|
+
}
|
|
332
|
+
if (!pluginVersion) throw new Error("Failed to create API Integration version");
|
|
333
|
+
|
|
334
|
+
const integrationFacet = await ensureFacet(tx as unknown as Database, {
|
|
335
|
+
pluginVersionId: pluginVersion.id,
|
|
336
|
+
facetKey: "integration",
|
|
337
|
+
kind: "integration",
|
|
338
|
+
});
|
|
339
|
+
const apiFacet = await ensureFacet(tx as unknown as Database, {
|
|
340
|
+
pluginVersionId: pluginVersion.id,
|
|
341
|
+
facetKey: "api",
|
|
342
|
+
kind: "api",
|
|
343
|
+
});
|
|
344
|
+
await tx
|
|
345
|
+
.insert(schema.capabilityIntegrationFacets)
|
|
346
|
+
.values({
|
|
347
|
+
facetId: integrationFacet.id,
|
|
348
|
+
providerDomain: input.providerDomain,
|
|
349
|
+
connectionKinds: connection ? [connection.kind] : [],
|
|
350
|
+
ownership: input.ownership ?? (connection?.subjectId ? "subject" : "workspace"),
|
|
351
|
+
requiredScopes: normalizedStrings(input.requiredScopes ?? [], 256),
|
|
352
|
+
resourceSelection: "none",
|
|
353
|
+
})
|
|
354
|
+
.onConflictDoNothing();
|
|
355
|
+
await tx
|
|
356
|
+
.insert(schema.capabilityApiFacets)
|
|
357
|
+
.values({
|
|
358
|
+
facetId: apiFacet.id,
|
|
359
|
+
protocol: input.protocol,
|
|
360
|
+
baseUrl: input.baseUrl,
|
|
361
|
+
specSourceUrl: input.sourceUrl ?? null,
|
|
362
|
+
authScheme: input.authScheme ?? {},
|
|
363
|
+
integrationFacetId: integrationFacet.id,
|
|
364
|
+
})
|
|
365
|
+
.onConflictDoNothing();
|
|
366
|
+
await tx
|
|
367
|
+
.insert(schema.integrationSpecRevisions)
|
|
368
|
+
.values({
|
|
369
|
+
apiFacetId: apiFacet.id,
|
|
370
|
+
revision: 1,
|
|
371
|
+
protocol: input.protocol,
|
|
372
|
+
sourceUrl: input.sourceUrl ?? null,
|
|
373
|
+
specDigest: input.revision.contentSha256,
|
|
374
|
+
spec: input.revision as unknown as Record<string, unknown>,
|
|
375
|
+
status: "active",
|
|
376
|
+
})
|
|
377
|
+
.onConflictDoNothing();
|
|
378
|
+
await tx
|
|
379
|
+
.insert(schema.integrationTools)
|
|
380
|
+
.values(
|
|
381
|
+
input.revision.tools.map((tool) => ({
|
|
382
|
+
facetId: apiFacet.id,
|
|
383
|
+
toolKey: tool.id,
|
|
384
|
+
name: tool.name,
|
|
385
|
+
description: tool.description || null,
|
|
386
|
+
inputSchema: tool.inputSchema,
|
|
387
|
+
outputSchema: tool.outputSchema ?? null,
|
|
388
|
+
effect: tool.safety,
|
|
389
|
+
active: !tool.deprecated,
|
|
390
|
+
})),
|
|
391
|
+
)
|
|
392
|
+
.onConflictDoNothing();
|
|
393
|
+
|
|
394
|
+
const owner: IntegrationFeatureBindingOwner = input.owner ?? {
|
|
395
|
+
kind: "direct",
|
|
396
|
+
id: input.capabilityId,
|
|
397
|
+
removable: true,
|
|
398
|
+
};
|
|
399
|
+
const instanceKey = integrationBindingKey(input.connectionId, input.instanceKey);
|
|
400
|
+
const runtimeServerId = integrationRuntimeKey(input.serverId, instanceKey);
|
|
401
|
+
const displayName =
|
|
402
|
+
input.displayName?.trim() ||
|
|
403
|
+
(instanceKey === "default" ? input.name : `${input.name} — connected account`);
|
|
404
|
+
const toolsFeature = await ensureIntegrationFeatureFacet(tx as unknown as Database, {
|
|
405
|
+
integrationFacetId: integrationFacet.id,
|
|
406
|
+
featureKey: "tools",
|
|
407
|
+
kind: "tools",
|
|
408
|
+
configSchema: {
|
|
409
|
+
type: "object",
|
|
410
|
+
properties: {
|
|
411
|
+
allowedTools: { type: "array", items: { type: "string" } },
|
|
412
|
+
requireApproval: { type: "array", items: { type: "string" } },
|
|
413
|
+
},
|
|
414
|
+
additionalProperties: false,
|
|
415
|
+
},
|
|
416
|
+
capabilities: { protocol: input.protocol, runtime: "mcp" },
|
|
417
|
+
});
|
|
418
|
+
const featureFacets = new Map<string, typeof schema.integrationFeatureFacets.$inferSelect>([
|
|
419
|
+
[toolsFeature.featureKey, toolsFeature],
|
|
420
|
+
]);
|
|
421
|
+
for (const feature of input.featureDefinitions ?? []) {
|
|
422
|
+
const facet = await ensureIntegrationFeatureFacet(tx as unknown as Database, {
|
|
423
|
+
integrationFacetId: integrationFacet.id,
|
|
424
|
+
featureKey: feature.featureKey,
|
|
425
|
+
kind: feature.kind,
|
|
426
|
+
configSchema: { ...feature.configSchema },
|
|
427
|
+
capabilities: { ...feature.capabilities },
|
|
428
|
+
});
|
|
429
|
+
featureFacets.set(facet.featureKey, facet);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
let [pluginInstallation] = await tx
|
|
433
|
+
.select()
|
|
434
|
+
.from(schema.capabilityPluginInstallations)
|
|
435
|
+
.where(
|
|
436
|
+
and(
|
|
437
|
+
eq(schema.capabilityPluginInstallations.workspaceId, input.workspaceId),
|
|
438
|
+
eq(schema.capabilityPluginInstallations.pluginId, plugin.id),
|
|
439
|
+
),
|
|
440
|
+
)
|
|
441
|
+
.for("update")
|
|
442
|
+
.limit(1);
|
|
443
|
+
const existingPluginInstallation = pluginInstallation;
|
|
444
|
+
const oldFacetInstallations = pluginInstallation
|
|
445
|
+
? await installedFacetRows(tx as unknown as Database, pluginInstallation.id)
|
|
446
|
+
: [];
|
|
447
|
+
let pluginInstallationGenerationAdvanced = false;
|
|
448
|
+
if (pluginInstallation && pluginInstallation.pluginVersionId !== pluginVersion.id) {
|
|
449
|
+
await assertCapabilityComponentVersionCanChange(tx as unknown as Database, {
|
|
450
|
+
workspaceId: input.workspaceId,
|
|
451
|
+
pluginKey: input.pluginKey,
|
|
452
|
+
pluginInstallationId: pluginInstallation.id,
|
|
453
|
+
owner: {
|
|
454
|
+
kind: owner.kind,
|
|
455
|
+
id: owner.id,
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
if (!pluginInstallation) {
|
|
460
|
+
[pluginInstallation] = await tx
|
|
461
|
+
.insert(schema.capabilityPluginInstallations)
|
|
462
|
+
.values({
|
|
463
|
+
accountId: input.accountId,
|
|
464
|
+
workspaceId: input.workspaceId,
|
|
465
|
+
pluginId: plugin.id,
|
|
466
|
+
pluginVersionId: pluginVersion.id,
|
|
467
|
+
status: "active",
|
|
468
|
+
installedBySubjectId: input.subjectId,
|
|
469
|
+
})
|
|
470
|
+
.returning();
|
|
471
|
+
} else if (
|
|
472
|
+
pluginInstallation.pluginVersionId !== pluginVersion.id ||
|
|
473
|
+
pluginInstallation.status !== "active"
|
|
474
|
+
) {
|
|
475
|
+
[pluginInstallation] = await tx
|
|
476
|
+
.update(schema.capabilityPluginInstallations)
|
|
477
|
+
.set({
|
|
478
|
+
pluginVersionId: pluginVersion.id,
|
|
479
|
+
status: "active",
|
|
480
|
+
version: pluginInstallation.version + 1,
|
|
481
|
+
installedBySubjectId: input.subjectId,
|
|
482
|
+
installedAt: now,
|
|
483
|
+
updatedAt: now,
|
|
484
|
+
})
|
|
485
|
+
.where(eq(schema.capabilityPluginInstallations.id, pluginInstallation.id))
|
|
486
|
+
.returning();
|
|
487
|
+
pluginInstallationGenerationAdvanced = true;
|
|
488
|
+
}
|
|
489
|
+
if (!pluginInstallation) throw new Error("Failed to create API Integration installation");
|
|
490
|
+
|
|
491
|
+
const integrationFacetInstallationResult = await ensureFacetInstallation(
|
|
492
|
+
tx as unknown as Database,
|
|
493
|
+
{
|
|
494
|
+
accountId: input.accountId,
|
|
495
|
+
workspaceId: input.workspaceId,
|
|
496
|
+
pluginInstallationId: pluginInstallation.id,
|
|
497
|
+
facetId: integrationFacet.id,
|
|
498
|
+
connectionId: null,
|
|
499
|
+
config: {},
|
|
500
|
+
},
|
|
501
|
+
);
|
|
502
|
+
const integrationFacetInstallation = integrationFacetInstallationResult.row;
|
|
503
|
+
const selectedTools = selectedToolIds(input);
|
|
504
|
+
const apiFacetInstallationResult = await ensureFacetInstallation(
|
|
505
|
+
tx as unknown as Database,
|
|
506
|
+
{
|
|
507
|
+
accountId: input.accountId,
|
|
508
|
+
workspaceId: input.workspaceId,
|
|
509
|
+
pluginInstallationId: pluginInstallation.id,
|
|
510
|
+
facetId: apiFacet.id,
|
|
511
|
+
connectionId: null,
|
|
512
|
+
config: {
|
|
513
|
+
baseServerId: input.serverId,
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
);
|
|
517
|
+
const apiFacetInstallation = apiFacetInstallationResult.row;
|
|
518
|
+
if (pluginInstallationGenerationAdvanced) {
|
|
519
|
+
await migrateApiIntegrationFacetInstallations(tx as unknown as Database, {
|
|
520
|
+
workspaceId: input.workspaceId,
|
|
521
|
+
oldFacetInstallations,
|
|
522
|
+
integrationFacetInstallationId: integrationFacetInstallation.id,
|
|
523
|
+
apiFacetInstallationId: apiFacetInstallation.id,
|
|
524
|
+
featureFacets,
|
|
525
|
+
excludedRuntimeKey: runtimeServerId,
|
|
526
|
+
revision: input.revision,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
const approvalRequiredTools = input.revision.tools
|
|
530
|
+
.filter((tool) => selectedTools.includes(tool.id) && tool.approvalMode === "ask")
|
|
531
|
+
.map((tool) => tool.id);
|
|
532
|
+
const bindingResult = await upsertIntegrationFeatureBinding(tx as unknown as Database, {
|
|
533
|
+
accountId: input.accountId,
|
|
534
|
+
workspaceId: input.workspaceId,
|
|
535
|
+
integrationFacetInstallationId: integrationFacetInstallation.id,
|
|
536
|
+
featureFacetId: toolsFeature.id,
|
|
537
|
+
bindingKey: instanceKey,
|
|
538
|
+
displayName,
|
|
539
|
+
runtimeKey: runtimeServerId,
|
|
540
|
+
connectionId: input.connectionId ?? null,
|
|
541
|
+
config: {
|
|
542
|
+
baseServerId: input.serverId,
|
|
543
|
+
allowedTools: selectedTools,
|
|
544
|
+
requireApproval: approvalRequiredTools,
|
|
545
|
+
connectionKind: connection?.kind ?? null,
|
|
546
|
+
subjectScope: connection?.subjectId ? "subject" : connection ? "workspace" : "none",
|
|
547
|
+
},
|
|
548
|
+
createdBySubjectId: input.subjectId,
|
|
549
|
+
owner,
|
|
550
|
+
...(input.expectedInstanceVersion !== undefined
|
|
551
|
+
? { expectedVersion: input.expectedInstanceVersion }
|
|
552
|
+
: {}),
|
|
553
|
+
});
|
|
554
|
+
if (
|
|
555
|
+
existingPluginInstallation &&
|
|
556
|
+
!pluginInstallationGenerationAdvanced &&
|
|
557
|
+
(integrationFacetInstallationResult.changed ||
|
|
558
|
+
apiFacetInstallationResult.changed ||
|
|
559
|
+
bindingResult.changed)
|
|
560
|
+
) {
|
|
561
|
+
[pluginInstallation] = await tx
|
|
562
|
+
.update(schema.capabilityPluginInstallations)
|
|
563
|
+
.set({
|
|
564
|
+
version: pluginInstallation.version + 1,
|
|
565
|
+
updatedAt: now,
|
|
566
|
+
})
|
|
567
|
+
.where(eq(schema.capabilityPluginInstallations.id, pluginInstallation.id))
|
|
568
|
+
.returning();
|
|
569
|
+
if (!pluginInstallation) {
|
|
570
|
+
throw new Error("Failed to advance API Integration installation version");
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
for (const facetInstallation of [integrationFacetInstallation, apiFacetInstallation]) {
|
|
574
|
+
await tx
|
|
575
|
+
.insert(schema.capabilityComponentOwners)
|
|
576
|
+
.values({
|
|
577
|
+
accountId: input.accountId,
|
|
578
|
+
workspaceId: input.workspaceId,
|
|
579
|
+
facetInstallationId: facetInstallation.id,
|
|
580
|
+
ownerKind: owner.kind,
|
|
581
|
+
ownerId: owner.id,
|
|
582
|
+
removable: owner.removable,
|
|
583
|
+
})
|
|
584
|
+
.onConflictDoNothing();
|
|
585
|
+
}
|
|
586
|
+
if (pluginInstallationGenerationAdvanced && oldFacetInstallations.length > 0) {
|
|
587
|
+
await tx.delete(schema.capabilityFacetInstallations).where(
|
|
588
|
+
inArray(
|
|
589
|
+
schema.capabilityFacetInstallations.id,
|
|
590
|
+
oldFacetInstallations.map((row) => row.id),
|
|
591
|
+
),
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const compatibilityMetadata = {
|
|
596
|
+
platformVersion: 2,
|
|
597
|
+
protocol: input.protocol,
|
|
598
|
+
provider: input.provider ?? null,
|
|
599
|
+
providerDomain: input.providerDomain,
|
|
600
|
+
pluginId: plugin.id,
|
|
601
|
+
pluginVersionId: pluginVersion.id,
|
|
602
|
+
integrationFacetId: integrationFacet.id,
|
|
603
|
+
apiFacetId: apiFacet.id,
|
|
604
|
+
revisionId: input.revision.id,
|
|
605
|
+
contentSha256: input.revision.contentSha256,
|
|
606
|
+
baseServerId: input.serverId,
|
|
607
|
+
multiInstance: true,
|
|
608
|
+
provenance: "workspace_import",
|
|
609
|
+
};
|
|
610
|
+
await tx
|
|
611
|
+
.insert(schema.capabilityCatalogItems)
|
|
612
|
+
.values({
|
|
613
|
+
id: input.capabilityId,
|
|
614
|
+
accountId: input.accountId,
|
|
615
|
+
workspaceId: input.workspaceId,
|
|
616
|
+
kind: "api",
|
|
617
|
+
source: "manual",
|
|
618
|
+
name: input.name,
|
|
619
|
+
description: input.description ?? null,
|
|
620
|
+
category: input.category ?? "integrations",
|
|
621
|
+
tags: normalizedStrings(input.tags ?? ["integration", input.protocol], 64),
|
|
622
|
+
homepageUrl: input.sourceUrl ?? input.baseUrl,
|
|
623
|
+
endpointUrl: input.baseUrl,
|
|
624
|
+
authModel: objectValue(input.authScheme ?? {}).kind
|
|
625
|
+
? String(objectValue(input.authScheme ?? {}).kind)
|
|
626
|
+
: null,
|
|
627
|
+
providerDomain: input.providerDomain,
|
|
628
|
+
surfaceType: "api",
|
|
629
|
+
authKind: connection ? connection.kind : "none",
|
|
630
|
+
provenance: "workspace_import",
|
|
631
|
+
tier: "community",
|
|
632
|
+
metadata: compatibilityMetadata,
|
|
633
|
+
updatedAt: now,
|
|
634
|
+
})
|
|
635
|
+
.onConflictDoUpdate({
|
|
636
|
+
target: [schema.capabilityCatalogItems.workspaceId, schema.capabilityCatalogItems.id],
|
|
637
|
+
set: {
|
|
638
|
+
name: input.name,
|
|
639
|
+
description: input.description ?? null,
|
|
640
|
+
tags: normalizedStrings(input.tags ?? ["integration", input.protocol], 64),
|
|
641
|
+
homepageUrl: input.sourceUrl ?? input.baseUrl,
|
|
642
|
+
endpointUrl: input.baseUrl,
|
|
643
|
+
authModel: objectValue(input.authScheme ?? {}).kind
|
|
644
|
+
? String(objectValue(input.authScheme ?? {}).kind)
|
|
645
|
+
: null,
|
|
646
|
+
providerDomain: input.providerDomain,
|
|
647
|
+
surfaceType: "api",
|
|
648
|
+
authKind: connection ? connection.kind : "none",
|
|
649
|
+
metadata: compatibilityMetadata,
|
|
650
|
+
updatedAt: now,
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
await tx
|
|
654
|
+
.insert(schema.capabilityInstallations)
|
|
655
|
+
.values({
|
|
656
|
+
accountId: input.accountId,
|
|
657
|
+
workspaceId: input.workspaceId,
|
|
658
|
+
capabilityId: input.capabilityId,
|
|
659
|
+
kind: "api",
|
|
660
|
+
status: "active",
|
|
661
|
+
config: { baseServerId: input.serverId, multiInstance: true },
|
|
662
|
+
metadata: {
|
|
663
|
+
...compatibilityMetadata,
|
|
664
|
+
pluginInstallationId: pluginInstallation.id,
|
|
665
|
+
integrationFacetInstallationId: integrationFacetInstallation.id,
|
|
666
|
+
apiFacetInstallationId: apiFacetInstallation.id,
|
|
667
|
+
},
|
|
668
|
+
enabledAt: now,
|
|
669
|
+
updatedAt: now,
|
|
670
|
+
})
|
|
671
|
+
.onConflictDoUpdate({
|
|
672
|
+
target: [
|
|
673
|
+
schema.capabilityInstallations.workspaceId,
|
|
674
|
+
schema.capabilityInstallations.capabilityId,
|
|
675
|
+
],
|
|
676
|
+
set: {
|
|
677
|
+
status: "active",
|
|
678
|
+
config: { baseServerId: input.serverId, multiInstance: true },
|
|
679
|
+
metadata: {
|
|
680
|
+
...compatibilityMetadata,
|
|
681
|
+
pluginInstallationId: pluginInstallation.id,
|
|
682
|
+
integrationFacetInstallationId: integrationFacetInstallation.id,
|
|
683
|
+
apiFacetInstallationId: apiFacetInstallation.id,
|
|
684
|
+
},
|
|
685
|
+
enabledAt: now,
|
|
686
|
+
updatedAt: now,
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
capabilityId: input.capabilityId,
|
|
692
|
+
pluginId: plugin.id,
|
|
693
|
+
pluginVersionId: pluginVersion.id,
|
|
694
|
+
integrationFacetId: integrationFacet.id,
|
|
695
|
+
apiFacetId: apiFacet.id,
|
|
696
|
+
pluginInstallationId: pluginInstallation.id,
|
|
697
|
+
integrationFacetInstallationId: integrationFacetInstallation.id,
|
|
698
|
+
apiFacetInstallationId: apiFacetInstallation.id,
|
|
699
|
+
installationVersion: pluginInstallation.version,
|
|
700
|
+
instanceId: bindingResult.row.id,
|
|
701
|
+
instanceKey: bindingResult.row.bindingKey,
|
|
702
|
+
displayName: bindingResult.row.displayName,
|
|
703
|
+
instanceVersion: bindingResult.row.version,
|
|
704
|
+
revisionId: input.revision.id,
|
|
705
|
+
serverId: runtimeServerId,
|
|
706
|
+
status: "installed",
|
|
707
|
+
};
|
|
708
|
+
});
|
|
709
|
+
},
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export async function listInstalledApiIntegrations(
|
|
714
|
+
db: Database,
|
|
715
|
+
workspaceId: string,
|
|
716
|
+
subjectId?: string,
|
|
717
|
+
): Promise<ApiIntegrationRuntime[]> {
|
|
718
|
+
return subjectId
|
|
719
|
+
? await withWorkspaceSubjectRls(
|
|
720
|
+
db,
|
|
721
|
+
workspaceId,
|
|
722
|
+
subjectId,
|
|
723
|
+
async (scopedDb) =>
|
|
724
|
+
await listInstalledApiIntegrationsInRlsContext(scopedDb, workspaceId, subjectId),
|
|
725
|
+
)
|
|
726
|
+
: await withWorkspaceRls(
|
|
727
|
+
db,
|
|
728
|
+
workspaceId,
|
|
729
|
+
async (scopedDb) => await listInstalledApiIntegrationsInRlsContext(scopedDb, workspaceId),
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Read installed API Integrations from a transaction that already carries the
|
|
735
|
+
* workspace RLS context. This is intentionally separate from the public
|
|
736
|
+
* wrapper so compound Plugin/Pack lifecycle transactions never open a nested
|
|
737
|
+
* transaction or lose their advisory locks.
|
|
738
|
+
*/
|
|
739
|
+
export async function listInstalledApiIntegrationsInRlsContext(
|
|
740
|
+
scopedDb: Database,
|
|
741
|
+
workspaceId: string,
|
|
742
|
+
subjectId?: string,
|
|
743
|
+
): Promise<ApiIntegrationRuntime[]> {
|
|
744
|
+
const rows = await scopedDb
|
|
745
|
+
.select({
|
|
746
|
+
pluginKey: schema.capabilityPlugins.pluginKey,
|
|
747
|
+
pluginName: schema.capabilityPlugins.name,
|
|
748
|
+
pluginDescription: schema.capabilityPlugins.description,
|
|
749
|
+
manifest: schema.capabilityPluginVersions.manifest,
|
|
750
|
+
pluginInstallationId: schema.capabilityPluginInstallations.id,
|
|
751
|
+
installationVersion: schema.capabilityPluginInstallations.version,
|
|
752
|
+
apiFacetId: schema.capabilityApiFacets.facetId,
|
|
753
|
+
protocol: schema.capabilityApiFacets.protocol,
|
|
754
|
+
baseUrl: schema.capabilityApiFacets.baseUrl,
|
|
755
|
+
sourceUrl: schema.capabilityApiFacets.specSourceUrl,
|
|
756
|
+
authScheme: schema.capabilityApiFacets.authScheme,
|
|
757
|
+
providerDomain: schema.capabilityIntegrationFacets.providerDomain,
|
|
758
|
+
requiredScopes: schema.capabilityIntegrationFacets.requiredScopes,
|
|
759
|
+
instanceId: schema.integrationFeatureBindings.id,
|
|
760
|
+
instanceKey: schema.integrationFeatureBindings.bindingKey,
|
|
761
|
+
displayName: schema.integrationFeatureBindings.displayName,
|
|
762
|
+
instanceVersion: schema.integrationFeatureBindings.version,
|
|
763
|
+
runtimeKey: schema.integrationFeatureBindings.runtimeKey,
|
|
764
|
+
bindingConfig: schema.integrationFeatureBindings.config,
|
|
765
|
+
connectionId: schema.integrationFeatureBindings.connectionId,
|
|
766
|
+
revision: schema.integrationSpecRevisions.spec,
|
|
767
|
+
})
|
|
768
|
+
.from(schema.integrationFeatureBindings)
|
|
769
|
+
.innerJoin(
|
|
770
|
+
schema.integrationFeatureFacets,
|
|
771
|
+
eq(schema.integrationFeatureFacets.id, schema.integrationFeatureBindings.featureFacetId),
|
|
772
|
+
)
|
|
773
|
+
.innerJoin(
|
|
774
|
+
schema.capabilityIntegrationFacets,
|
|
775
|
+
eq(
|
|
776
|
+
schema.capabilityIntegrationFacets.facetId,
|
|
777
|
+
schema.integrationFeatureFacets.integrationFacetId,
|
|
778
|
+
),
|
|
779
|
+
)
|
|
780
|
+
.innerJoin(
|
|
781
|
+
schema.capabilityFacetInstallations,
|
|
782
|
+
eq(
|
|
783
|
+
schema.capabilityFacetInstallations.id,
|
|
784
|
+
schema.integrationFeatureBindings.integrationFacetInstallationId,
|
|
785
|
+
),
|
|
786
|
+
)
|
|
787
|
+
.innerJoin(
|
|
788
|
+
schema.capabilityPluginInstallations,
|
|
789
|
+
eq(
|
|
790
|
+
schema.capabilityPluginInstallations.id,
|
|
791
|
+
schema.capabilityFacetInstallations.pluginInstallationId,
|
|
792
|
+
),
|
|
793
|
+
)
|
|
794
|
+
.innerJoin(
|
|
795
|
+
schema.capabilityPlugins,
|
|
796
|
+
eq(schema.capabilityPlugins.id, schema.capabilityPluginInstallations.pluginId),
|
|
797
|
+
)
|
|
798
|
+
.innerJoin(
|
|
799
|
+
schema.capabilityPluginVersions,
|
|
800
|
+
eq(schema.capabilityPluginVersions.id, schema.capabilityPluginInstallations.pluginVersionId),
|
|
801
|
+
)
|
|
802
|
+
.innerJoin(
|
|
803
|
+
schema.capabilityApiFacets,
|
|
804
|
+
eq(schema.capabilityApiFacets.integrationFacetId, schema.capabilityIntegrationFacets.facetId),
|
|
805
|
+
)
|
|
806
|
+
.innerJoin(
|
|
807
|
+
schema.integrationSpecRevisions,
|
|
808
|
+
and(
|
|
809
|
+
eq(schema.integrationSpecRevisions.apiFacetId, schema.capabilityApiFacets.facetId),
|
|
810
|
+
eq(schema.integrationSpecRevisions.status, "active"),
|
|
811
|
+
),
|
|
812
|
+
)
|
|
813
|
+
.where(
|
|
814
|
+
and(
|
|
815
|
+
eq(schema.capabilityPluginInstallations.workspaceId, workspaceId),
|
|
816
|
+
eq(schema.capabilityPluginInstallations.status, "active"),
|
|
817
|
+
eq(schema.capabilityFacetInstallations.status, "active"),
|
|
818
|
+
eq(schema.integrationFeatureFacets.kind, "tools"),
|
|
819
|
+
eq(schema.integrationFeatureBindings.status, "active"),
|
|
820
|
+
sql`${schema.integrationFeatureBindings.runtimeKey} is not null`,
|
|
821
|
+
sql`exists (
|
|
822
|
+
select 1 from ${schema.capabilityComponentOwners} owner
|
|
823
|
+
where owner.facet_installation_id = ${schema.capabilityFacetInstallations.id}
|
|
824
|
+
and ${effectiveCapabilityOwnerSql(sql`owner.owner_kind`, sql`owner.owner_id`)}
|
|
825
|
+
)`,
|
|
826
|
+
sql`exists (
|
|
827
|
+
select 1 from ${schema.integrationFeatureBindingOwners} owner
|
|
828
|
+
where owner.binding_id = ${schema.integrationFeatureBindings.id}
|
|
829
|
+
and ${effectiveCapabilityOwnerSql(sql`owner.owner_kind`, sql`owner.owner_id`)}
|
|
830
|
+
)`,
|
|
831
|
+
sql`exists (
|
|
832
|
+
select 1 from ${schema.capabilityFacetInstallations} api_installation
|
|
833
|
+
where api_installation.plugin_installation_id = ${schema.capabilityPluginInstallations.id}
|
|
834
|
+
and api_installation.facet_id = ${schema.capabilityApiFacets.facetId}
|
|
835
|
+
and api_installation.status = 'active'
|
|
836
|
+
)`,
|
|
837
|
+
subjectId
|
|
838
|
+
? sql`(
|
|
839
|
+
${schema.integrationFeatureBindings.connectionId} is null
|
|
840
|
+
or exists (
|
|
841
|
+
select 1 from ${schema.connections} connection
|
|
842
|
+
where connection.id = ${schema.integrationFeatureBindings.connectionId}
|
|
843
|
+
and (connection.subject_id is null or connection.subject_id = ${subjectId})
|
|
844
|
+
)
|
|
845
|
+
)`
|
|
846
|
+
: sql`(
|
|
847
|
+
${schema.integrationFeatureBindings.connectionId} is null
|
|
848
|
+
or exists (
|
|
849
|
+
select 1 from ${schema.connections} connection
|
|
850
|
+
where connection.id = ${schema.integrationFeatureBindings.connectionId}
|
|
851
|
+
and connection.subject_id is null
|
|
852
|
+
)
|
|
853
|
+
)`,
|
|
854
|
+
),
|
|
855
|
+
)
|
|
856
|
+
.orderBy(
|
|
857
|
+
asc(schema.capabilityPlugins.name),
|
|
858
|
+
asc(schema.integrationFeatureBindings.displayName),
|
|
859
|
+
asc(schema.integrationFeatureBindings.bindingKey),
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
return rows.flatMap((row): ApiIntegrationRuntime[] => {
|
|
863
|
+
const revision = storedRevision(row.revision);
|
|
864
|
+
const config = objectValue(row.bindingConfig);
|
|
865
|
+
const manifest = objectValue(row.manifest);
|
|
866
|
+
const capabilityId = stringValue(manifest.capabilityId);
|
|
867
|
+
const presetId = stringValue(manifest.presetId) ?? null;
|
|
868
|
+
const serverId = row.runtimeKey;
|
|
869
|
+
if (!capabilityId || !serverId || row.protocol !== revision.protocol) {
|
|
870
|
+
throw new Error(`Installed API Integration ${row.pluginKey} has invalid immutable metadata`);
|
|
871
|
+
}
|
|
872
|
+
const allowedTools = stringArray(config.allowedTools) ?? revision.tools.map((tool) => tool.id);
|
|
873
|
+
const requireApproval =
|
|
874
|
+
stringArray(config.requireApproval) ??
|
|
875
|
+
revision.tools.filter((tool) => tool.approvalMode === "ask").map((tool) => tool.id);
|
|
876
|
+
const connectionKind = stringValue(config.connectionKind);
|
|
877
|
+
const subjectScope = stringValue(config.subjectScope);
|
|
878
|
+
const connectionRef: McpServerConnectionRef | null = row.connectionId
|
|
879
|
+
? {
|
|
880
|
+
connectionId: row.connectionId,
|
|
881
|
+
providerDomain: row.providerDomain,
|
|
882
|
+
...(stringValue(manifest.provider) ? { provider: stringValue(manifest.provider)! } : {}),
|
|
883
|
+
...(connectionKind === "oauth2" ||
|
|
884
|
+
connectionKind === "api_key" ||
|
|
885
|
+
connectionKind === "app_install" ||
|
|
886
|
+
connectionKind === "delegated"
|
|
887
|
+
? { kind: connectionKind }
|
|
888
|
+
: {}),
|
|
889
|
+
...(row.requiredScopes.length > 0 ? { scopes: [...row.requiredScopes] } : {}),
|
|
890
|
+
subjectScope: subjectScope === "subject" ? "subject" : "workspace",
|
|
891
|
+
}
|
|
892
|
+
: null;
|
|
893
|
+
return [
|
|
894
|
+
{
|
|
895
|
+
capabilityId,
|
|
896
|
+
pluginKey: row.pluginKey,
|
|
897
|
+
pluginInstallationId: row.pluginInstallationId,
|
|
898
|
+
installationVersion: row.installationVersion,
|
|
899
|
+
instanceId: row.instanceId,
|
|
900
|
+
instanceKey: row.instanceKey,
|
|
901
|
+
displayName: row.displayName,
|
|
902
|
+
instanceVersion: row.instanceVersion,
|
|
903
|
+
serverId,
|
|
904
|
+
name: row.displayName,
|
|
905
|
+
description: row.pluginDescription,
|
|
906
|
+
protocol: revision.protocol,
|
|
907
|
+
presetId,
|
|
908
|
+
baseUrl: row.baseUrl,
|
|
909
|
+
sourceUrl: row.sourceUrl,
|
|
910
|
+
providerDomain: row.providerDomain,
|
|
911
|
+
authScheme: objectValue(row.authScheme),
|
|
912
|
+
connectionRef,
|
|
913
|
+
allowedTools,
|
|
914
|
+
requireApproval,
|
|
915
|
+
revision,
|
|
916
|
+
},
|
|
917
|
+
];
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
export async function getApiIntegrationUninstallPreview(
|
|
922
|
+
db: Database,
|
|
923
|
+
workspaceId: string,
|
|
924
|
+
subjectId: string,
|
|
925
|
+
capabilityId: string,
|
|
926
|
+
instanceKey: string,
|
|
927
|
+
): Promise<ApiIntegrationUninstallPreview> {
|
|
928
|
+
return await withWorkspaceSubjectRls(db, workspaceId, subjectId, async (scopedDb) => {
|
|
929
|
+
const context = await integrationInstanceContext(
|
|
930
|
+
scopedDb,
|
|
931
|
+
workspaceId,
|
|
932
|
+
subjectId,
|
|
933
|
+
capabilityId,
|
|
934
|
+
instanceKey,
|
|
935
|
+
);
|
|
936
|
+
if (!context) {
|
|
937
|
+
return {
|
|
938
|
+
capabilityId,
|
|
939
|
+
instanceKey,
|
|
940
|
+
displayName: null,
|
|
941
|
+
installed: false,
|
|
942
|
+
installationVersion: null,
|
|
943
|
+
instanceVersion: null,
|
|
944
|
+
directOwner: null,
|
|
945
|
+
remainingOwners: [],
|
|
946
|
+
removesRuntimeIntegration: false,
|
|
947
|
+
removesDefinition: false,
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
const owners = await listIntegrationFeatureBindingOwners(scopedDb, context.instanceId);
|
|
951
|
+
const directOwner = owners.find(
|
|
952
|
+
(owner) => owner.kind === "direct" && owner.id === capabilityId,
|
|
953
|
+
);
|
|
954
|
+
const remainingOwners = owners.filter(
|
|
955
|
+
(owner) => !(owner.kind === "direct" && owner.id === capabilityId),
|
|
956
|
+
);
|
|
957
|
+
const definitionOwners = await integrationOwners(scopedDb, context.pluginInstallationId);
|
|
958
|
+
const remainingDefinitionOwners = definitionOwners.filter(
|
|
959
|
+
(owner) => !(owner.kind === "direct" && owner.id === capabilityId),
|
|
960
|
+
);
|
|
961
|
+
const hasOtherDirectInstance = await integrationDefinitionHasOtherBindingOwner(scopedDb, {
|
|
962
|
+
workspaceId,
|
|
963
|
+
pluginInstallationId: context.pluginInstallationId,
|
|
964
|
+
excludedBindingId: context.instanceId,
|
|
965
|
+
owner: { kind: "direct", id: capabilityId },
|
|
966
|
+
});
|
|
967
|
+
return {
|
|
968
|
+
capabilityId,
|
|
969
|
+
instanceKey,
|
|
970
|
+
displayName: context.displayName,
|
|
971
|
+
installed: true,
|
|
972
|
+
installationVersion: context.installationVersion,
|
|
973
|
+
instanceVersion: context.instanceVersion,
|
|
974
|
+
directOwner: directOwner ?? null,
|
|
975
|
+
remainingOwners,
|
|
976
|
+
removesRuntimeIntegration: remainingOwners.length === 0,
|
|
977
|
+
removesDefinition:
|
|
978
|
+
remainingOwners.length === 0 &&
|
|
979
|
+
!hasOtherDirectInstance &&
|
|
980
|
+
remainingDefinitionOwners.length === 0,
|
|
981
|
+
};
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
export async function uninstallApiIntegration(
|
|
986
|
+
db: Database,
|
|
987
|
+
input: {
|
|
988
|
+
accountId: string;
|
|
989
|
+
workspaceId: string;
|
|
990
|
+
subjectId: string;
|
|
991
|
+
capabilityId: string;
|
|
992
|
+
instanceKey: string;
|
|
993
|
+
expectedInstallationVersion: number;
|
|
994
|
+
expectedInstanceVersion: number;
|
|
995
|
+
},
|
|
996
|
+
): Promise<UninstallApiIntegrationResult> {
|
|
997
|
+
return await withRlsContext(
|
|
998
|
+
db,
|
|
999
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
1000
|
+
async (scopedDb) => {
|
|
1001
|
+
await setSubjectRlsContext(scopedDb, input.subjectId);
|
|
1002
|
+
return await scopedDb.transaction(async (tx) => {
|
|
1003
|
+
const context = await integrationInstanceContext(
|
|
1004
|
+
tx as unknown as Database,
|
|
1005
|
+
input.workspaceId,
|
|
1006
|
+
input.subjectId,
|
|
1007
|
+
input.capabilityId,
|
|
1008
|
+
input.instanceKey,
|
|
1009
|
+
true,
|
|
1010
|
+
);
|
|
1011
|
+
if (!context) {
|
|
1012
|
+
return {
|
|
1013
|
+
capabilityId: input.capabilityId,
|
|
1014
|
+
instanceKey: input.instanceKey,
|
|
1015
|
+
status: "not_installed",
|
|
1016
|
+
remainingOwners: [],
|
|
1017
|
+
definitionStatus: "retained",
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
if (context.installationVersion !== input.expectedInstallationVersion) {
|
|
1021
|
+
throw new ApiIntegrationInstallationVersionConflictError(
|
|
1022
|
+
input.capabilityId,
|
|
1023
|
+
input.expectedInstallationVersion,
|
|
1024
|
+
context.installationVersion,
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
const bindingOwners = await listIntegrationFeatureBindingOwners(
|
|
1028
|
+
tx as unknown as Database,
|
|
1029
|
+
context.instanceId,
|
|
1030
|
+
);
|
|
1031
|
+
if (
|
|
1032
|
+
!bindingOwners.some((owner) => owner.kind === "direct" && owner.id === input.capabilityId)
|
|
1033
|
+
) {
|
|
1034
|
+
return {
|
|
1035
|
+
capabilityId: input.capabilityId,
|
|
1036
|
+
instanceKey: input.instanceKey,
|
|
1037
|
+
status: "not_installed",
|
|
1038
|
+
remainingOwners: bindingOwners,
|
|
1039
|
+
definitionStatus: "retained",
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
const removed = await removeIntegrationFeatureBindingOwner(tx as unknown as Database, {
|
|
1043
|
+
workspaceId: input.workspaceId,
|
|
1044
|
+
bindingId: context.instanceId,
|
|
1045
|
+
owner: { kind: "direct", id: input.capabilityId },
|
|
1046
|
+
expectedVersion: input.expectedInstanceVersion,
|
|
1047
|
+
});
|
|
1048
|
+
const hasAnotherDirectInstance = await integrationDefinitionHasBindingOwner(
|
|
1049
|
+
tx as unknown as Database,
|
|
1050
|
+
{
|
|
1051
|
+
workspaceId: input.workspaceId,
|
|
1052
|
+
pluginInstallationId: context.pluginInstallationId,
|
|
1053
|
+
owner: { kind: "direct", id: input.capabilityId },
|
|
1054
|
+
},
|
|
1055
|
+
);
|
|
1056
|
+
if (!hasAnotherDirectInstance) {
|
|
1057
|
+
await tx.delete(schema.capabilityComponentOwners).where(
|
|
1058
|
+
and(
|
|
1059
|
+
eq(schema.capabilityComponentOwners.workspaceId, input.workspaceId),
|
|
1060
|
+
eq(schema.capabilityComponentOwners.ownerKind, "direct"),
|
|
1061
|
+
eq(schema.capabilityComponentOwners.ownerId, input.capabilityId),
|
|
1062
|
+
sql`${schema.capabilityComponentOwners.facetInstallationId} in (
|
|
1063
|
+
select id from ${schema.capabilityFacetInstallations}
|
|
1064
|
+
where plugin_installation_id = ${context.pluginInstallationId}
|
|
1065
|
+
)`,
|
|
1066
|
+
),
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
const remainingDefinitionOwners = await integrationOwners(
|
|
1070
|
+
tx as unknown as Database,
|
|
1071
|
+
context.pluginInstallationId,
|
|
1072
|
+
);
|
|
1073
|
+
const now = new Date();
|
|
1074
|
+
if (remainingDefinitionOwners.length > 0 || hasAnotherDirectInstance) {
|
|
1075
|
+
await tx
|
|
1076
|
+
.update(schema.capabilityPluginInstallations)
|
|
1077
|
+
.set({ version: context.installationVersion + 1, updatedAt: now })
|
|
1078
|
+
.where(eq(schema.capabilityPluginInstallations.id, context.pluginInstallationId));
|
|
1079
|
+
return {
|
|
1080
|
+
capabilityId: input.capabilityId,
|
|
1081
|
+
instanceKey: input.instanceKey,
|
|
1082
|
+
status: removed.remainingOwners.length > 0 ? "retained_by_other_owners" : "uninstalled",
|
|
1083
|
+
remainingOwners: removed.remainingOwners,
|
|
1084
|
+
definitionStatus: "retained",
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
await tx
|
|
1088
|
+
.delete(schema.capabilityFacetInstallations)
|
|
1089
|
+
.where(
|
|
1090
|
+
eq(
|
|
1091
|
+
schema.capabilityFacetInstallations.pluginInstallationId,
|
|
1092
|
+
context.pluginInstallationId,
|
|
1093
|
+
),
|
|
1094
|
+
);
|
|
1095
|
+
await tx
|
|
1096
|
+
.update(schema.capabilityPluginInstallations)
|
|
1097
|
+
.set({
|
|
1098
|
+
status: "disabled",
|
|
1099
|
+
version: context.installationVersion + 1,
|
|
1100
|
+
updatedAt: now,
|
|
1101
|
+
})
|
|
1102
|
+
.where(eq(schema.capabilityPluginInstallations.id, context.pluginInstallationId));
|
|
1103
|
+
await tx
|
|
1104
|
+
.update(schema.capabilityInstallations)
|
|
1105
|
+
.set({ status: "disabled", updatedAt: now })
|
|
1106
|
+
.where(
|
|
1107
|
+
and(
|
|
1108
|
+
eq(schema.capabilityInstallations.workspaceId, input.workspaceId),
|
|
1109
|
+
eq(schema.capabilityInstallations.capabilityId, input.capabilityId),
|
|
1110
|
+
),
|
|
1111
|
+
);
|
|
1112
|
+
return {
|
|
1113
|
+
capabilityId: input.capabilityId,
|
|
1114
|
+
instanceKey: input.instanceKey,
|
|
1115
|
+
status: removed.remainingOwners.length > 0 ? "retained_by_other_owners" : "uninstalled",
|
|
1116
|
+
remainingOwners: removed.remainingOwners,
|
|
1117
|
+
definitionStatus: "disabled",
|
|
1118
|
+
};
|
|
1119
|
+
});
|
|
1120
|
+
},
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
type InstalledFacetRow = {
|
|
1125
|
+
id: string;
|
|
1126
|
+
kind: string;
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
async function installedFacetRows(
|
|
1130
|
+
db: Database,
|
|
1131
|
+
pluginInstallationId: string,
|
|
1132
|
+
): Promise<InstalledFacetRow[]> {
|
|
1133
|
+
return await db
|
|
1134
|
+
.select({
|
|
1135
|
+
id: schema.capabilityFacetInstallations.id,
|
|
1136
|
+
kind: schema.capabilityFacets.kind,
|
|
1137
|
+
})
|
|
1138
|
+
.from(schema.capabilityFacetInstallations)
|
|
1139
|
+
.innerJoin(
|
|
1140
|
+
schema.capabilityFacets,
|
|
1141
|
+
eq(schema.capabilityFacets.id, schema.capabilityFacetInstallations.facetId),
|
|
1142
|
+
)
|
|
1143
|
+
.where(eq(schema.capabilityFacetInstallations.pluginInstallationId, pluginInstallationId));
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
async function migrateApiIntegrationFacetInstallations(
|
|
1147
|
+
db: Database,
|
|
1148
|
+
input: {
|
|
1149
|
+
workspaceId: string;
|
|
1150
|
+
oldFacetInstallations: InstalledFacetRow[];
|
|
1151
|
+
integrationFacetInstallationId: string;
|
|
1152
|
+
apiFacetInstallationId: string;
|
|
1153
|
+
featureFacets: ReadonlyMap<string, typeof schema.integrationFeatureFacets.$inferSelect>;
|
|
1154
|
+
excludedRuntimeKey: string;
|
|
1155
|
+
revision: StoredApiIntegrationRevision;
|
|
1156
|
+
},
|
|
1157
|
+
): Promise<void> {
|
|
1158
|
+
if (input.oldFacetInstallations.length === 0) return;
|
|
1159
|
+
const oldIntegration = input.oldFacetInstallations.find((row) => row.kind === "integration");
|
|
1160
|
+
const targetByKind = new Map<string, string>([
|
|
1161
|
+
["integration", input.integrationFacetInstallationId],
|
|
1162
|
+
["api", input.apiFacetInstallationId],
|
|
1163
|
+
]);
|
|
1164
|
+
const oldIds = input.oldFacetInstallations.map((row) => row.id);
|
|
1165
|
+
const owners = await db
|
|
1166
|
+
.select({
|
|
1167
|
+
facetInstallationId: schema.capabilityComponentOwners.facetInstallationId,
|
|
1168
|
+
accountId: schema.capabilityComponentOwners.accountId,
|
|
1169
|
+
workspaceId: schema.capabilityComponentOwners.workspaceId,
|
|
1170
|
+
ownerKind: schema.capabilityComponentOwners.ownerKind,
|
|
1171
|
+
ownerId: schema.capabilityComponentOwners.ownerId,
|
|
1172
|
+
removable: schema.capabilityComponentOwners.removable,
|
|
1173
|
+
})
|
|
1174
|
+
.from(schema.capabilityComponentOwners)
|
|
1175
|
+
.where(inArray(schema.capabilityComponentOwners.facetInstallationId, oldIds));
|
|
1176
|
+
const kindByOldId = new Map(input.oldFacetInstallations.map((row) => [row.id, row.kind]));
|
|
1177
|
+
for (const owner of owners) {
|
|
1178
|
+
const targetId = targetByKind.get(kindByOldId.get(owner.facetInstallationId) ?? "");
|
|
1179
|
+
if (!targetId) continue;
|
|
1180
|
+
await db
|
|
1181
|
+
.insert(schema.capabilityComponentOwners)
|
|
1182
|
+
.values({
|
|
1183
|
+
accountId: owner.accountId,
|
|
1184
|
+
workspaceId: owner.workspaceId,
|
|
1185
|
+
facetInstallationId: targetId,
|
|
1186
|
+
ownerKind: owner.ownerKind,
|
|
1187
|
+
ownerId: owner.ownerId,
|
|
1188
|
+
removable: owner.removable,
|
|
1189
|
+
})
|
|
1190
|
+
.onConflictDoNothing();
|
|
1191
|
+
}
|
|
1192
|
+
if (!oldIntegration) return;
|
|
1193
|
+
const bindings = await db
|
|
1194
|
+
.select({
|
|
1195
|
+
binding: schema.integrationFeatureBindings,
|
|
1196
|
+
featureKey: schema.integrationFeatureFacets.featureKey,
|
|
1197
|
+
featureKind: schema.integrationFeatureFacets.kind,
|
|
1198
|
+
})
|
|
1199
|
+
.from(schema.integrationFeatureBindings)
|
|
1200
|
+
.innerJoin(
|
|
1201
|
+
schema.integrationFeatureFacets,
|
|
1202
|
+
eq(schema.integrationFeatureFacets.id, schema.integrationFeatureBindings.featureFacetId),
|
|
1203
|
+
)
|
|
1204
|
+
.where(
|
|
1205
|
+
and(
|
|
1206
|
+
eq(schema.integrationFeatureBindings.workspaceId, input.workspaceId),
|
|
1207
|
+
eq(schema.integrationFeatureBindings.integrationFacetInstallationId, oldIntegration.id),
|
|
1208
|
+
or(
|
|
1209
|
+
sql`${schema.integrationFeatureBindings.runtimeKey} is null`,
|
|
1210
|
+
ne(schema.integrationFeatureBindings.runtimeKey, input.excludedRuntimeKey),
|
|
1211
|
+
),
|
|
1212
|
+
),
|
|
1213
|
+
)
|
|
1214
|
+
.for("update");
|
|
1215
|
+
const available = new Set(input.revision.tools.map((tool) => tool.id));
|
|
1216
|
+
for (const row of bindings) {
|
|
1217
|
+
const binding = row.binding;
|
|
1218
|
+
const targetFeature = input.featureFacets.get(row.featureKey);
|
|
1219
|
+
if (!targetFeature || targetFeature.kind !== row.featureKind) {
|
|
1220
|
+
throw new Error(`API Integration update would remove configured feature ${row.featureKey}`);
|
|
1221
|
+
}
|
|
1222
|
+
const config = objectValue(binding.config);
|
|
1223
|
+
const toolsConfig = row.featureKind === "tools";
|
|
1224
|
+
const selected = toolsConfig
|
|
1225
|
+
? (stringArray(config.allowedTools) ?? []).filter((tool) => available.has(tool))
|
|
1226
|
+
: [];
|
|
1227
|
+
const requireApproval = toolsConfig
|
|
1228
|
+
? input.revision.tools
|
|
1229
|
+
.filter((tool) => selected.includes(tool.id) && tool.approvalMode === "ask")
|
|
1230
|
+
.map((tool) => tool.id)
|
|
1231
|
+
: [];
|
|
1232
|
+
await db
|
|
1233
|
+
.update(schema.integrationFeatureBindings)
|
|
1234
|
+
.set({
|
|
1235
|
+
integrationFacetInstallationId: input.integrationFacetInstallationId,
|
|
1236
|
+
featureFacetId: targetFeature.id,
|
|
1237
|
+
config: toolsConfig ? { ...config, allowedTools: selected, requireApproval } : config,
|
|
1238
|
+
status: toolsConfig && selected.length === 0 ? "needs_attention" : binding.status,
|
|
1239
|
+
lastErrorCode:
|
|
1240
|
+
toolsConfig && selected.length === 0 ? "selected_tools_removed" : binding.lastErrorCode,
|
|
1241
|
+
version: binding.version + 1,
|
|
1242
|
+
updatedAt: new Date(),
|
|
1243
|
+
})
|
|
1244
|
+
.where(eq(schema.integrationFeatureBindings.id, binding.id));
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
async function loadInstallConnection(
|
|
1249
|
+
db: Database,
|
|
1250
|
+
input: InstallApiIntegrationInput,
|
|
1251
|
+
): Promise<typeof schema.connections.$inferSelect> {
|
|
1252
|
+
const [connection] = await db
|
|
1253
|
+
.select()
|
|
1254
|
+
.from(schema.connections)
|
|
1255
|
+
.where(
|
|
1256
|
+
and(
|
|
1257
|
+
eq(schema.connections.workspaceId, input.workspaceId),
|
|
1258
|
+
eq(schema.connections.id, input.connectionId!),
|
|
1259
|
+
),
|
|
1260
|
+
)
|
|
1261
|
+
.limit(1);
|
|
1262
|
+
if (!connection || connection.accountId !== input.accountId) {
|
|
1263
|
+
throw new Error("API Integration connection was not found in this workspace");
|
|
1264
|
+
}
|
|
1265
|
+
if (connection.subjectId && connection.subjectId !== input.subjectId) {
|
|
1266
|
+
throw new Error("API Integration personal connection belongs to another subject");
|
|
1267
|
+
}
|
|
1268
|
+
if (connection.status !== "active") {
|
|
1269
|
+
throw new Error("API Integration connection is not active");
|
|
1270
|
+
}
|
|
1271
|
+
if (connection.providerDomain.toLowerCase() !== input.providerDomain.toLowerCase()) {
|
|
1272
|
+
throw new Error("API Integration connection provider does not match the destination");
|
|
1273
|
+
}
|
|
1274
|
+
assertConnectionKindMatchesAuth(input.authScheme, connection.kind);
|
|
1275
|
+
const missing = normalizedStrings(input.requiredScopes ?? [], 256).filter(
|
|
1276
|
+
(scope) => !connection.grantedScopes.includes(scope),
|
|
1277
|
+
);
|
|
1278
|
+
if (missing.length > 0) {
|
|
1279
|
+
throw new Error("API Integration connection is missing required scopes");
|
|
1280
|
+
}
|
|
1281
|
+
return connection;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function assertConnectionKindMatchesAuth(
|
|
1285
|
+
authScheme: Record<string, unknown> | undefined,
|
|
1286
|
+
connectionKind: string,
|
|
1287
|
+
): void {
|
|
1288
|
+
const auth = objectValue(authScheme);
|
|
1289
|
+
const authKind = stringValue(auth.kind);
|
|
1290
|
+
if (authKind === undefined || authKind === "none") return;
|
|
1291
|
+
if (authKind === "oauth2") {
|
|
1292
|
+
if (connectionKind !== "oauth2") {
|
|
1293
|
+
throw new Error("API Integration requires an OAuth Connection");
|
|
1294
|
+
}
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
if (authKind === "api_key" || authKind === "http") {
|
|
1298
|
+
if (connectionKind !== "api_key") {
|
|
1299
|
+
throw new Error("API Integration requires a credential Connection, not OAuth");
|
|
1300
|
+
}
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
throw new Error("API Integration auth scheme is unsupported");
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
async function ensureFacet(
|
|
1307
|
+
db: Database,
|
|
1308
|
+
input: { pluginVersionId: string; facetKey: string; kind: "integration" | "api" },
|
|
1309
|
+
): Promise<typeof schema.capabilityFacets.$inferSelect> {
|
|
1310
|
+
const [existing] = await db
|
|
1311
|
+
.select()
|
|
1312
|
+
.from(schema.capabilityFacets)
|
|
1313
|
+
.where(
|
|
1314
|
+
and(
|
|
1315
|
+
eq(schema.capabilityFacets.pluginVersionId, input.pluginVersionId),
|
|
1316
|
+
eq(schema.capabilityFacets.facetKey, input.facetKey),
|
|
1317
|
+
),
|
|
1318
|
+
)
|
|
1319
|
+
.limit(1);
|
|
1320
|
+
if (existing) {
|
|
1321
|
+
if (existing.kind !== input.kind)
|
|
1322
|
+
throw new Error(`Capability facet ${input.facetKey} changed kind`);
|
|
1323
|
+
return existing;
|
|
1324
|
+
}
|
|
1325
|
+
const [created] = await db
|
|
1326
|
+
.insert(schema.capabilityFacets)
|
|
1327
|
+
.values({
|
|
1328
|
+
pluginVersionId: input.pluginVersionId,
|
|
1329
|
+
facetKey: input.facetKey,
|
|
1330
|
+
kind: input.kind,
|
|
1331
|
+
activationMode: "workspace_managed",
|
|
1332
|
+
required: true,
|
|
1333
|
+
})
|
|
1334
|
+
.returning();
|
|
1335
|
+
if (!created) throw new Error(`Failed to create capability facet ${input.facetKey}`);
|
|
1336
|
+
return created;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
async function ensureFacetInstallation(
|
|
1340
|
+
db: Database,
|
|
1341
|
+
input: {
|
|
1342
|
+
accountId: string;
|
|
1343
|
+
workspaceId: string;
|
|
1344
|
+
pluginInstallationId: string;
|
|
1345
|
+
facetId: string;
|
|
1346
|
+
connectionId: string | null;
|
|
1347
|
+
config: Record<string, unknown>;
|
|
1348
|
+
},
|
|
1349
|
+
): Promise<{ row: typeof schema.capabilityFacetInstallations.$inferSelect; changed: boolean }> {
|
|
1350
|
+
const [existing] = await db
|
|
1351
|
+
.select()
|
|
1352
|
+
.from(schema.capabilityFacetInstallations)
|
|
1353
|
+
.where(
|
|
1354
|
+
and(
|
|
1355
|
+
eq(schema.capabilityFacetInstallations.pluginInstallationId, input.pluginInstallationId),
|
|
1356
|
+
eq(schema.capabilityFacetInstallations.facetId, input.facetId),
|
|
1357
|
+
),
|
|
1358
|
+
)
|
|
1359
|
+
.limit(1);
|
|
1360
|
+
if (!existing) {
|
|
1361
|
+
const [created] = await db
|
|
1362
|
+
.insert(schema.capabilityFacetInstallations)
|
|
1363
|
+
.values({
|
|
1364
|
+
accountId: input.accountId,
|
|
1365
|
+
workspaceId: input.workspaceId,
|
|
1366
|
+
pluginInstallationId: input.pluginInstallationId,
|
|
1367
|
+
facetId: input.facetId,
|
|
1368
|
+
connectionId: input.connectionId,
|
|
1369
|
+
status: "active",
|
|
1370
|
+
config: input.config,
|
|
1371
|
+
})
|
|
1372
|
+
.returning();
|
|
1373
|
+
if (!created) throw new Error("Failed to create capability facet installation");
|
|
1374
|
+
return { row: created, changed: true };
|
|
1375
|
+
}
|
|
1376
|
+
if (
|
|
1377
|
+
existing.connectionId === input.connectionId &&
|
|
1378
|
+
stableJson(existing.config) === stableJson(input.config) &&
|
|
1379
|
+
existing.status === "active"
|
|
1380
|
+
) {
|
|
1381
|
+
return { row: existing, changed: false };
|
|
1382
|
+
}
|
|
1383
|
+
const [updated] = await db
|
|
1384
|
+
.update(schema.capabilityFacetInstallations)
|
|
1385
|
+
.set({
|
|
1386
|
+
connectionId: input.connectionId,
|
|
1387
|
+
status: "active",
|
|
1388
|
+
config: input.config,
|
|
1389
|
+
version: existing.version + 1,
|
|
1390
|
+
attentionCode: null,
|
|
1391
|
+
updatedAt: new Date(),
|
|
1392
|
+
})
|
|
1393
|
+
.where(eq(schema.capabilityFacetInstallations.id, existing.id))
|
|
1394
|
+
.returning();
|
|
1395
|
+
if (!updated) throw new Error("Failed to update capability facet installation");
|
|
1396
|
+
return { row: updated, changed: true };
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
type IntegrationInstanceContext = {
|
|
1400
|
+
instanceId: string;
|
|
1401
|
+
instanceVersion: number;
|
|
1402
|
+
displayName: string;
|
|
1403
|
+
pluginInstallationId: string;
|
|
1404
|
+
installationVersion: number;
|
|
1405
|
+
};
|
|
1406
|
+
|
|
1407
|
+
async function integrationInstanceContext(
|
|
1408
|
+
db: Database,
|
|
1409
|
+
workspaceId: string,
|
|
1410
|
+
subjectId: string,
|
|
1411
|
+
capabilityId: string,
|
|
1412
|
+
instanceKey: string,
|
|
1413
|
+
lock = false,
|
|
1414
|
+
): Promise<IntegrationInstanceContext | null> {
|
|
1415
|
+
let query = db
|
|
1416
|
+
.select({
|
|
1417
|
+
instanceId: schema.integrationFeatureBindings.id,
|
|
1418
|
+
instanceVersion: schema.integrationFeatureBindings.version,
|
|
1419
|
+
displayName: schema.integrationFeatureBindings.displayName,
|
|
1420
|
+
pluginInstallationId: schema.capabilityPluginInstallations.id,
|
|
1421
|
+
installationVersion: schema.capabilityPluginInstallations.version,
|
|
1422
|
+
})
|
|
1423
|
+
.from(schema.integrationFeatureBindings)
|
|
1424
|
+
.innerJoin(
|
|
1425
|
+
schema.integrationFeatureFacets,
|
|
1426
|
+
eq(schema.integrationFeatureFacets.id, schema.integrationFeatureBindings.featureFacetId),
|
|
1427
|
+
)
|
|
1428
|
+
.innerJoin(
|
|
1429
|
+
schema.capabilityFacetInstallations,
|
|
1430
|
+
eq(
|
|
1431
|
+
schema.capabilityFacetInstallations.id,
|
|
1432
|
+
schema.integrationFeatureBindings.integrationFacetInstallationId,
|
|
1433
|
+
),
|
|
1434
|
+
)
|
|
1435
|
+
.innerJoin(
|
|
1436
|
+
schema.capabilityPluginInstallations,
|
|
1437
|
+
eq(
|
|
1438
|
+
schema.capabilityPluginInstallations.id,
|
|
1439
|
+
schema.capabilityFacetInstallations.pluginInstallationId,
|
|
1440
|
+
),
|
|
1441
|
+
)
|
|
1442
|
+
.innerJoin(
|
|
1443
|
+
schema.capabilityPluginVersions,
|
|
1444
|
+
eq(schema.capabilityPluginVersions.id, schema.capabilityPluginInstallations.pluginVersionId),
|
|
1445
|
+
)
|
|
1446
|
+
.where(
|
|
1447
|
+
and(
|
|
1448
|
+
eq(schema.integrationFeatureBindings.workspaceId, workspaceId),
|
|
1449
|
+
eq(schema.integrationFeatureBindings.bindingKey, instanceKey),
|
|
1450
|
+
eq(schema.integrationFeatureFacets.kind, "tools"),
|
|
1451
|
+
sql`${schema.capabilityPluginVersions.manifest} ->> 'capabilityId' = ${capabilityId}`,
|
|
1452
|
+
sql`(
|
|
1453
|
+
${schema.integrationFeatureBindings.connectionId} is null
|
|
1454
|
+
or exists (
|
|
1455
|
+
select 1 from ${schema.connections} connection
|
|
1456
|
+
where connection.id = ${schema.integrationFeatureBindings.connectionId}
|
|
1457
|
+
and (connection.subject_id is null or connection.subject_id = ${subjectId})
|
|
1458
|
+
)
|
|
1459
|
+
)`,
|
|
1460
|
+
),
|
|
1461
|
+
)
|
|
1462
|
+
.limit(2);
|
|
1463
|
+
if (lock) query = query.for("update") as typeof query;
|
|
1464
|
+
const rows = await query;
|
|
1465
|
+
if (rows.length === 0) return null;
|
|
1466
|
+
if (rows.length !== 1) throw new Error(`API Integration ${capabilityId} instance is ambiguous`);
|
|
1467
|
+
return rows[0]!;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
async function integrationDefinitionHasOtherBindingOwner(
|
|
1471
|
+
db: Database,
|
|
1472
|
+
input: {
|
|
1473
|
+
workspaceId: string;
|
|
1474
|
+
pluginInstallationId: string;
|
|
1475
|
+
excludedBindingId: string;
|
|
1476
|
+
owner: Pick<IntegrationFeatureBindingOwner, "kind" | "id">;
|
|
1477
|
+
},
|
|
1478
|
+
): Promise<boolean> {
|
|
1479
|
+
const [row] = await db
|
|
1480
|
+
.select({ id: schema.integrationFeatureBindingOwners.id })
|
|
1481
|
+
.from(schema.integrationFeatureBindingOwners)
|
|
1482
|
+
.innerJoin(
|
|
1483
|
+
schema.integrationFeatureBindings,
|
|
1484
|
+
eq(schema.integrationFeatureBindings.id, schema.integrationFeatureBindingOwners.bindingId),
|
|
1485
|
+
)
|
|
1486
|
+
.innerJoin(
|
|
1487
|
+
schema.capabilityFacetInstallations,
|
|
1488
|
+
eq(
|
|
1489
|
+
schema.capabilityFacetInstallations.id,
|
|
1490
|
+
schema.integrationFeatureBindings.integrationFacetInstallationId,
|
|
1491
|
+
),
|
|
1492
|
+
)
|
|
1493
|
+
.where(
|
|
1494
|
+
and(
|
|
1495
|
+
eq(schema.integrationFeatureBindingOwners.workspaceId, input.workspaceId),
|
|
1496
|
+
eq(schema.integrationFeatureBindingOwners.ownerKind, input.owner.kind),
|
|
1497
|
+
eq(schema.integrationFeatureBindingOwners.ownerId, input.owner.id),
|
|
1498
|
+
ne(schema.integrationFeatureBindingOwners.bindingId, input.excludedBindingId),
|
|
1499
|
+
eq(schema.capabilityFacetInstallations.pluginInstallationId, input.pluginInstallationId),
|
|
1500
|
+
or(
|
|
1501
|
+
eq(schema.integrationFeatureBindings.status, "active"),
|
|
1502
|
+
eq(schema.integrationFeatureBindings.status, "needs_attention"),
|
|
1503
|
+
eq(schema.integrationFeatureBindings.status, "paused"),
|
|
1504
|
+
),
|
|
1505
|
+
),
|
|
1506
|
+
)
|
|
1507
|
+
.limit(1);
|
|
1508
|
+
return Boolean(row);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
async function integrationOwners(
|
|
1512
|
+
db: Database,
|
|
1513
|
+
pluginInstallationId: string,
|
|
1514
|
+
): Promise<ApiIntegrationOwner[]> {
|
|
1515
|
+
const rows = await db
|
|
1516
|
+
.select({
|
|
1517
|
+
kind: schema.capabilityComponentOwners.ownerKind,
|
|
1518
|
+
id: schema.capabilityComponentOwners.ownerId,
|
|
1519
|
+
removable: schema.capabilityComponentOwners.removable,
|
|
1520
|
+
})
|
|
1521
|
+
.from(schema.capabilityComponentOwners)
|
|
1522
|
+
.innerJoin(
|
|
1523
|
+
schema.capabilityFacetInstallations,
|
|
1524
|
+
eq(
|
|
1525
|
+
schema.capabilityFacetInstallations.id,
|
|
1526
|
+
schema.capabilityComponentOwners.facetInstallationId,
|
|
1527
|
+
),
|
|
1528
|
+
)
|
|
1529
|
+
.where(
|
|
1530
|
+
and(
|
|
1531
|
+
eq(schema.capabilityFacetInstallations.pluginInstallationId, pluginInstallationId),
|
|
1532
|
+
effectiveCapabilityOwnerSql(
|
|
1533
|
+
schema.capabilityComponentOwners.ownerKind,
|
|
1534
|
+
schema.capabilityComponentOwners.ownerId,
|
|
1535
|
+
),
|
|
1536
|
+
),
|
|
1537
|
+
)
|
|
1538
|
+
.orderBy(
|
|
1539
|
+
asc(schema.capabilityComponentOwners.ownerKind),
|
|
1540
|
+
asc(schema.capabilityComponentOwners.ownerId),
|
|
1541
|
+
);
|
|
1542
|
+
const owners = new Map<string, ApiIntegrationOwner>();
|
|
1543
|
+
for (const row of rows) {
|
|
1544
|
+
if (
|
|
1545
|
+
row.kind !== "direct" &&
|
|
1546
|
+
row.kind !== "plugin" &&
|
|
1547
|
+
row.kind !== "pack" &&
|
|
1548
|
+
row.kind !== "migration"
|
|
1549
|
+
) {
|
|
1550
|
+
throw new Error(`Unknown API Integration owner kind: ${row.kind}`);
|
|
1551
|
+
}
|
|
1552
|
+
const key = `${row.kind}\0${row.id}`;
|
|
1553
|
+
const existing = owners.get(key);
|
|
1554
|
+
owners.set(key, {
|
|
1555
|
+
kind: row.kind,
|
|
1556
|
+
id: row.id,
|
|
1557
|
+
removable: (existing?.removable ?? true) && row.removable,
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
return [...owners.values()];
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function assertInstallInput(input: InstallApiIntegrationInput): void {
|
|
1564
|
+
if (input.revision.protocol !== input.protocol)
|
|
1565
|
+
throw new Error("API Integration protocol mismatch");
|
|
1566
|
+
if (
|
|
1567
|
+
input.revision.contentSha256.length !== 64 ||
|
|
1568
|
+
!/^[0-9a-f]+$/.test(input.revision.contentSha256)
|
|
1569
|
+
) {
|
|
1570
|
+
throw new Error("API Integration revision digest is invalid");
|
|
1571
|
+
}
|
|
1572
|
+
if (input.revision.tools.length === 0 || input.revision.tools.length > 2_000) {
|
|
1573
|
+
throw new Error("API Integration revision must expose 1-2000 tools");
|
|
1574
|
+
}
|
|
1575
|
+
if (new Set(input.revision.tools.map((tool) => tool.id)).size !== input.revision.tools.length) {
|
|
1576
|
+
throw new Error("API Integration revision has duplicate tool ids");
|
|
1577
|
+
}
|
|
1578
|
+
const featureDefinitions = input.featureDefinitions ?? [];
|
|
1579
|
+
if (featureDefinitions.length > 128) {
|
|
1580
|
+
throw new Error("API Integration exposes too many feature definitions");
|
|
1581
|
+
}
|
|
1582
|
+
if (
|
|
1583
|
+
new Set(featureDefinitions.map((feature) => feature.featureKey)).size !==
|
|
1584
|
+
featureDefinitions.length
|
|
1585
|
+
) {
|
|
1586
|
+
throw new Error("API Integration has duplicate feature keys");
|
|
1587
|
+
}
|
|
1588
|
+
const base = new URL(input.baseUrl);
|
|
1589
|
+
if (base.protocol !== "https:" || base.username || base.password || base.hash) {
|
|
1590
|
+
throw new Error("API Integration baseUrl must be a credential-free HTTPS URL");
|
|
1591
|
+
}
|
|
1592
|
+
if (
|
|
1593
|
+
base.hostname !== input.providerDomain &&
|
|
1594
|
+
!base.hostname.endsWith(`.${input.providerDomain}`)
|
|
1595
|
+
) {
|
|
1596
|
+
throw new Error("API Integration providerDomain does not match baseUrl");
|
|
1597
|
+
}
|
|
1598
|
+
const authKind = objectValue(input.authScheme).kind;
|
|
1599
|
+
if (authKind !== undefined && authKind !== "none" && !input.connectionId) {
|
|
1600
|
+
throw new Error("Authenticated API Integrations require a Connection");
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function selectedToolIds(input: InstallApiIntegrationInput): string[] {
|
|
1605
|
+
const available = new Set(input.revision.tools.map((tool) => tool.id));
|
|
1606
|
+
const selected =
|
|
1607
|
+
input.allowedTools === undefined
|
|
1608
|
+
? [...available]
|
|
1609
|
+
: normalizedStrings(input.allowedTools, 2_000);
|
|
1610
|
+
if (selected.some((tool) => !available.has(tool))) {
|
|
1611
|
+
throw new Error("API Integration selected an unknown tool");
|
|
1612
|
+
}
|
|
1613
|
+
return selected;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function storedRevision(value: Record<string, unknown>): StoredApiIntegrationRevision {
|
|
1617
|
+
const protocol = value.protocol;
|
|
1618
|
+
const tools = value.tools;
|
|
1619
|
+
const bindings = value.bindings;
|
|
1620
|
+
if (
|
|
1621
|
+
(protocol !== "openapi" && protocol !== "graphql") ||
|
|
1622
|
+
typeof value.id !== "string" ||
|
|
1623
|
+
typeof value.integrationId !== "string" ||
|
|
1624
|
+
typeof value.contentSha256 !== "string" ||
|
|
1625
|
+
typeof value.title !== "string" ||
|
|
1626
|
+
!Array.isArray(tools) ||
|
|
1627
|
+
!bindings ||
|
|
1628
|
+
typeof bindings !== "object" ||
|
|
1629
|
+
Array.isArray(bindings)
|
|
1630
|
+
) {
|
|
1631
|
+
throw new Error("Stored API Integration revision is invalid");
|
|
1632
|
+
}
|
|
1633
|
+
return value as StoredApiIntegrationRevision;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
function normalizedStrings(values: string[], limit: number): string[] {
|
|
1637
|
+
const normalized = [...new Set(values.map((value) => value.trim()).filter(Boolean))].sort();
|
|
1638
|
+
if (normalized.length > limit) throw new Error(`String selection exceeds ${limit} entries`);
|
|
1639
|
+
return normalized;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
function stringArray(value: unknown): string[] | undefined {
|
|
1643
|
+
return Array.isArray(value)
|
|
1644
|
+
? normalizedStrings(
|
|
1645
|
+
value.filter((entry): entry is string => typeof entry === "string"),
|
|
1646
|
+
2_000,
|
|
1647
|
+
)
|
|
1648
|
+
: undefined;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
function objectValue(value: unknown): Record<string, unknown> {
|
|
1652
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
1653
|
+
? (value as Record<string, unknown>)
|
|
1654
|
+
: {};
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function stringValue(value: unknown): string | undefined {
|
|
1658
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function sha256(value: string): string {
|
|
1662
|
+
return createHash("sha256").update(value).digest("hex");
|
|
1663
|
+
}
|