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