@opengeni/api-router 0.23.1 → 0.26.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.
Files changed (107) hide show
  1. package/dist/app.d.ts +2 -2
  2. package/dist/app.js +1 -1
  3. package/dist/browser-controller-authority.d.ts +43 -0
  4. package/dist/browser-state-authority.d.ts +27 -0
  5. package/dist/{chunk-T4T2PGU4.js → chunk-JIKNR5YL.js} +26181 -14408
  6. package/dist/chunk-JIKNR5YL.js.map +1 -0
  7. package/dist/codemode.d.ts +23 -0
  8. package/dist/editable-artifact-live-hints.d.ts +11 -0
  9. package/dist/editable-artifact-native-kernel.d.ts +37 -0
  10. package/dist/editable-artifact-office-import.d.ts +22 -0
  11. package/dist/editable-artifact-production.d.ts +29 -0
  12. package/dist/editable-artifact-websocket.d.ts +49 -0
  13. package/dist/editable-artifact-workspace-files.d.ts +23 -0
  14. package/dist/github-browser-flow.d.ts +6 -0
  15. package/dist/http/cors.d.ts +1 -0
  16. package/dist/index.d.ts +3 -2
  17. package/dist/index.js +1796 -22
  18. package/dist/index.js.map +1 -1
  19. package/dist/integrations/api-integrations.d.ts +24 -0
  20. package/dist/integrations/atlassian.d.ts +176 -0
  21. package/dist/integrations/github-skill-source.d.ts +5 -0
  22. package/dist/integrations/google-drive.d.ts +85 -0
  23. package/dist/integrations/oauth-client.d.ts +22 -1
  24. package/dist/integrations/provider-oauth.d.ts +19 -0
  25. package/dist/integrations/slack-bot.d.ts +4 -0
  26. package/dist/integrations/slack-interactions.d.ts +8 -2
  27. package/dist/integrations/social-api.d.ts +2 -1
  28. package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
  29. package/dist/mcp/editable-artifacts.d.ts +13 -0
  30. package/dist/mcp/scheduled-task-view.d.ts +168 -0
  31. package/dist/mcp/server.d.ts +14 -3
  32. package/dist/memory-slack-delivery.d.ts +9 -0
  33. package/dist/routes/api-integrations.d.ts +8 -0
  34. package/dist/routes/browser-identities.d.ts +5 -0
  35. package/dist/routes/browser-sessions.d.ts +6 -0
  36. package/dist/routes/company-profile.d.ts +3 -0
  37. package/dist/routes/computer-sessions.d.ts +6 -0
  38. package/dist/routes/editable-artifacts.d.ts +44 -0
  39. package/dist/routes/integration-features.d.ts +3 -0
  40. package/dist/routes/memory-slack-publications.d.ts +6 -0
  41. package/dist/routes/plugins.d.ts +8 -0
  42. package/dist/routes/sessions.d.ts +17 -2
  43. package/dist/routes/skills.d.ts +6 -0
  44. package/dist/routes/video-generation.d.ts +3 -0
  45. package/dist/sandbox/channel-a.d.ts +55 -2
  46. package/dist/sandbox/metrics-ingestion.d.ts +6 -1
  47. package/dist/sandbox/viewer.d.ts +4 -2
  48. package/dist/temporal-schedule-cleanup.d.ts +26 -0
  49. package/package.json +19 -14
  50. package/src/app.ts +274 -37
  51. package/src/browser-controller-authority.ts +137 -0
  52. package/src/browser-state-authority.ts +236 -0
  53. package/src/codemode.ts +186 -0
  54. package/src/editable-artifact-live-hints.ts +64 -0
  55. package/src/editable-artifact-native-kernel.ts +659 -0
  56. package/src/editable-artifact-office-import.ts +230 -0
  57. package/src/editable-artifact-production.ts +419 -0
  58. package/src/editable-artifact-websocket.ts +311 -0
  59. package/src/editable-artifact-workspace-files.ts +186 -0
  60. package/src/github-browser-flow.ts +35 -6
  61. package/src/http/auth.ts +2 -0
  62. package/src/http/cors.ts +3 -0
  63. package/src/index.ts +119 -20
  64. package/src/integrations/api-integrations.ts +350 -0
  65. package/src/integrations/atlassian.ts +1621 -0
  66. package/src/integrations/github-skill-source.ts +142 -0
  67. package/src/integrations/google-drive.ts +1000 -64
  68. package/src/integrations/oauth-client.ts +123 -33
  69. package/src/integrations/provider-oauth.ts +777 -0
  70. package/src/integrations/slack-bot.ts +31 -2
  71. package/src/integrations/slack-interactions.ts +487 -27
  72. package/src/integrations/social-api.ts +11 -0
  73. package/src/mcp/documents.ts +32 -1
  74. package/src/mcp/editable-artifact-query-schema.ts +236 -0
  75. package/src/mcp/editable-artifacts.ts +448 -0
  76. package/src/mcp/scheduled-task-view.ts +34 -0
  77. package/src/mcp/server.ts +923 -145
  78. package/src/memory-slack-delivery.ts +209 -0
  79. package/src/routes/api-integrations.ts +407 -0
  80. package/src/routes/browser-identities.ts +136 -0
  81. package/src/routes/browser-sessions.ts +2543 -0
  82. package/src/routes/company-profile.ts +255 -0
  83. package/src/routes/computer-sessions.ts +1247 -0
  84. package/src/routes/connections.ts +284 -99
  85. package/src/routes/documents.ts +22 -3
  86. package/src/routes/editable-artifacts.ts +1159 -0
  87. package/src/routes/files.ts +106 -4
  88. package/src/routes/github.ts +18 -2
  89. package/src/routes/install.ts +7 -1
  90. package/src/routes/integration-features.ts +258 -0
  91. package/src/routes/memory-slack-publications.ts +216 -0
  92. package/src/routes/packs.ts +437 -7
  93. package/src/routes/plugins.ts +751 -0
  94. package/src/routes/rigs.ts +77 -20
  95. package/src/routes/scheduled-tasks.ts +67 -40
  96. package/src/routes/sessions.ts +421 -245
  97. package/src/routes/skills.ts +174 -0
  98. package/src/routes/video-generation.ts +132 -0
  99. package/src/routes/workspaces.ts +46 -24
  100. package/src/sandbox/channel-a.ts +723 -116
  101. package/src/sandbox/metrics-ingestion.ts +121 -3
  102. package/src/sandbox/rematerialize.ts +35 -47
  103. package/src/sandbox/viewer.ts +29 -9
  104. package/src/temporal-schedule-cleanup.ts +135 -0
  105. package/dist/chunk-T4T2PGU4.js.map +0 -1
  106. package/dist/mcp/toolspace.d.ts +0 -71
  107. package/src/mcp/toolspace.ts +0 -1190
@@ -0,0 +1,209 @@
1
+ import {
2
+ claimMemorySlackPublication,
3
+ completeMemorySlackPublication,
4
+ currentMemorySlackConfigurationMatches,
5
+ deferMemorySlackPublication,
6
+ failMemorySlackPublication,
7
+ type ClaimedMemorySlackPublication,
8
+ } from "@opengeni/db";
9
+ import { sanitizeSlackPublicationText, type ApiRouteDeps } from "@opengeni/core";
10
+ import {
11
+ createOpenGeniSlackBotInteractionClient,
12
+ SlackBotProviderError,
13
+ } from "./integrations/slack-bot";
14
+
15
+ const DELIVERY_LEASE_MS = 30_000;
16
+ const MAX_DELIVERY_ATTEMPTS = 8;
17
+ const MAX_DELIVERY_RETRY_MS = 5 * 60_000;
18
+ const MAX_SLACK_MESSAGE_CHARS = 3_500;
19
+
20
+ export async function drainMemorySlackPublicationsOnce(deps: ApiRouteDeps): Promise<boolean> {
21
+ const claimHolderId = crypto.randomUUID();
22
+ const publication = await claimMemorySlackPublication(deps.db, claimHolderId, DELIVERY_LEASE_MS);
23
+ if (!publication) return false;
24
+
25
+ try {
26
+ if (!(await currentMemorySlackConfigurationMatches(deps.db, publication))) {
27
+ await failMemorySlackPublication(deps.db, {
28
+ publication,
29
+ claimHolderId,
30
+ errorCode: "configuration_changed",
31
+ cancelled: true,
32
+ });
33
+ return true;
34
+ }
35
+ const client = await createOpenGeniSlackBotInteractionClient(deps, {
36
+ accountId: publication.accountId,
37
+ workspaceId: publication.workspaceId,
38
+ connectionId: publication.connectionId,
39
+ subjectId: publication.initiatorSubjectId,
40
+ sessionId: publication.sessionId,
41
+ });
42
+ const result = await client.postMessage({
43
+ operationId: publication.operationId,
44
+ channelId: publication.slackChannelId,
45
+ text: formatMemorySlackPublicationMessage(deps, publication),
46
+ requireActiveNonSharedChannel: true,
47
+ });
48
+ const completed = await completeMemorySlackPublication(deps.db, {
49
+ publication,
50
+ claimHolderId,
51
+ slackChannelId: result.channelId,
52
+ slackMessageTimestamp: result.timestamp,
53
+ });
54
+ if (!completed) throw new Error("Memory Slack publication lost its durable claim");
55
+ } catch (error) {
56
+ const errorCode = deliveryErrorCode(error);
57
+ if (memorySlackDestinationDrift(error)) {
58
+ await failMemorySlackPublication(deps.db, {
59
+ publication,
60
+ claimHolderId,
61
+ errorCode,
62
+ cancelled: true,
63
+ }).catch(() => undefined);
64
+ } else if (publication.attemptCount >= MAX_DELIVERY_ATTEMPTS || permanentDeliveryError(error)) {
65
+ await failMemorySlackPublication(deps.db, {
66
+ publication,
67
+ claimHolderId,
68
+ errorCode,
69
+ }).catch(() => undefined);
70
+ } else {
71
+ await deferMemorySlackPublication(deps.db, {
72
+ publication,
73
+ claimHolderId,
74
+ errorCode,
75
+ retryAt: new Date(Date.now() + deliveryRetryMs(error, publication.attemptCount)),
76
+ }).catch(() => undefined);
77
+ }
78
+ }
79
+ return true;
80
+ }
81
+
82
+ export function startMemorySlackPublicationPump(
83
+ deps: ApiRouteDeps,
84
+ options: { intervalMs?: number; maxPerTick?: number } = {},
85
+ ): () => Promise<void> {
86
+ let stopped = false;
87
+ let running: Promise<void> | undefined;
88
+ const intervalMs = Math.max(250, options.intervalMs ?? 1_000);
89
+ const maxPerTick = Math.max(1, Math.min(50, options.maxPerTick ?? 10));
90
+ const tick = () => {
91
+ if (stopped || running) return;
92
+ running = (async () => {
93
+ for (let index = 0; index < maxPerTick; index += 1) {
94
+ if (!(await drainMemorySlackPublicationsOnce(deps))) break;
95
+ }
96
+ })()
97
+ .catch((error) => {
98
+ deps.observability?.error("Memory Slack publication claim failed", {
99
+ errorClass: error instanceof Error ? error.name : "MemorySlackPublicationPumpError",
100
+ errorCode: deliveryErrorCode(error),
101
+ origin: "api",
102
+ });
103
+ })
104
+ .finally(() => {
105
+ running = undefined;
106
+ });
107
+ };
108
+ const timer = setInterval(tick, intervalMs);
109
+ queueMicrotask(tick);
110
+ return async () => {
111
+ stopped = true;
112
+ clearInterval(timer);
113
+ await running;
114
+ };
115
+ }
116
+
117
+ export function formatMemorySlackPublicationMessage(
118
+ deps: Pick<ApiRouteDeps, "settings">,
119
+ publication: ClaimedMemorySlackPublication,
120
+ ): string {
121
+ const projection = publication.projection;
122
+ const summary = boundedProjectionString(projection["summary"], 512) || "Important change";
123
+ const owner = boundedProjectionString(projection["ownerLabel"], 96);
124
+ const occurredAt = boundedProjectionString(projection["occurredAt"], 64);
125
+ const changeKind = boundedProjectionString(projection["changeKind"], 32);
126
+ const destination = boundedProjectionString(projection["destination"], 64);
127
+ const outcome = boundedProjectionString(projection["outcome"], 64);
128
+ const heading =
129
+ publication.sourceType === "workspace_memory"
130
+ ? `Workspace Memory ${changeKind || "change"}`
131
+ : `Governed learning ${outcome || "change"}`;
132
+ const details = [
133
+ `Importance: ${publication.importance}`,
134
+ ...(destination ? [`Surface: ${destination}`] : []),
135
+ ...(owner ? [`Owner: ${owner}`] : []),
136
+ ...(occurredAt ? [`Recorded: ${occurredAt}`] : []),
137
+ ];
138
+ const path =
139
+ publication.sourceType === "workspace_memory"
140
+ ? `/workspaces/${publication.workspaceId}/memory?memoryId=${encodeURIComponent(publication.sourceId)}`
141
+ : `/workspaces/${publication.workspaceId}/workspace-state`;
142
+ const base = deps.settings.webBaseUrl ?? deps.settings.publicBaseUrl;
143
+ const link = base ? new URL(path, base).toString() : null;
144
+ const text = `*${heading}*\n${summary}\n\n${details.join(" · ")}${link ? `\n${link}` : ""}`;
145
+ return text.length <= MAX_SLACK_MESSAGE_CHARS
146
+ ? text
147
+ : `${text.slice(0, MAX_SLACK_MESSAGE_CHARS - 20)}\n… message truncated`;
148
+ }
149
+
150
+ const PERMANENT_DELIVERY_CODES = new Set([
151
+ "account_inactive",
152
+ "channel_not_found",
153
+ "invalid_auth",
154
+ "is_archived",
155
+ "not_authed",
156
+ "not_in_channel",
157
+ "token_expired",
158
+ "token_revoked",
159
+ ]);
160
+
161
+ const MEMORY_SLACK_DESTINATION_DRIFT_CODES = new Set([
162
+ "channel_not_found",
163
+ "is_archived",
164
+ "not_in_channel",
165
+ "slack_connect_unsupported",
166
+ ]);
167
+
168
+ export function memorySlackDestinationDrift(error: unknown) {
169
+ return (
170
+ error instanceof SlackBotProviderError && MEMORY_SLACK_DESTINATION_DRIFT_CODES.has(error.code)
171
+ );
172
+ }
173
+
174
+ function permanentDeliveryError(error: unknown) {
175
+ if (!(error instanceof SlackBotProviderError)) return false;
176
+ if (PERMANENT_DELIVERY_CODES.has(error.code)) return true;
177
+ const status = /^http_(\d{3})$/.exec(error.code)?.[1];
178
+ return status
179
+ ? Number(status) >= 400 && Number(status) < 500 && status !== "408" && status !== "429"
180
+ : false;
181
+ }
182
+
183
+ function deliveryRetryMs(error: unknown, attemptCount: number) {
184
+ if (error instanceof SlackBotProviderError && error.retryAfterMs) {
185
+ return Math.min(Math.max(error.retryAfterMs, 1_000), MAX_DELIVERY_RETRY_MS);
186
+ }
187
+ return Math.min(1_000 * 2 ** Math.max(0, attemptCount - 1), MAX_DELIVERY_RETRY_MS);
188
+ }
189
+
190
+ function deliveryErrorCode(error: unknown) {
191
+ if (error instanceof SlackBotProviderError) return error.code.slice(0, 128);
192
+ const raw = error instanceof Error ? error.name : "memory_slack_delivery_error";
193
+ return (
194
+ raw
195
+ .toLowerCase()
196
+ .replace(/[^a-z0-9_-]/g, "_")
197
+ .slice(0, 128) || "error"
198
+ );
199
+ }
200
+
201
+ function boundedProjectionString(value: unknown, maxChars: number) {
202
+ return typeof value === "string"
203
+ ? escapeSlackText(sanitizeSlackPublicationText(value).slice(0, maxChars))
204
+ : "";
205
+ }
206
+
207
+ function escapeSlackText(value: string) {
208
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
209
+ }
@@ -0,0 +1,407 @@
1
+ import {
2
+ CORE_PROVIDER_PRESETS,
3
+ createPinnedIntegrationTransport,
4
+ integrationFeaturesForPreset,
5
+ providerDomainForPreset,
6
+ type IntegrationCredentialResolver,
7
+ } from "@opengeni/capabilities";
8
+ import {
9
+ ApiIntegrationPreview,
10
+ ListApiIntegrationPresetsResponse,
11
+ ApiIntegrationUninstallPreview,
12
+ InstallApiIntegrationRequest,
13
+ InstalledApiIntegration,
14
+ ListApiIntegrationsResponse,
15
+ PreviewApiIntegrationRequest,
16
+ UninstallApiIntegrationRequest,
17
+ UninstallApiIntegrationResult,
18
+ } from "@opengeni/contracts";
19
+ import { requireAccessGrant, type ApiRouteDeps } from "@opengeni/core";
20
+ import {
21
+ ApiIntegrationInstallationVersionConflictError,
22
+ buildConnectionTokenResolver,
23
+ getApiIntegrationUninstallPreview,
24
+ getConnectionMetadata,
25
+ installApiIntegration,
26
+ IntegrationFeatureBindingOwnershipConflictError,
27
+ IntegrationFeatureBindingVersionConflictError,
28
+ IntegrationFeatureBindingVersionRequiredError,
29
+ listInstalledApiIntegrations,
30
+ uninstallApiIntegration,
31
+ type ConnectionMetadataWithVerification,
32
+ } from "@opengeni/db";
33
+ import type { FetchLike } from "@opengeni/network";
34
+ import type { Hono } from "hono";
35
+ import { HTTPException } from "hono/http-exception";
36
+
37
+ import {
38
+ resolveApiIntegrationPreview,
39
+ type ApiIntegrationConnectionDescriptor,
40
+ } from "../integrations/api-integrations";
41
+
42
+ export type ApiIntegrationRouteOverrides = Readonly<{ fetchImpl?: FetchLike }>;
43
+
44
+ export function apiIntegrationRequiresConnection(authScheme: Record<string, unknown>): boolean {
45
+ const authKind = authScheme.kind;
46
+ return typeof authKind === "string" && authKind !== "none";
47
+ }
48
+
49
+ export function registerApiIntegrationRoutes(
50
+ app: Hono,
51
+ deps: ApiRouteDeps,
52
+ overrides: ApiIntegrationRouteOverrides = {},
53
+ ): void {
54
+ const transport = createPinnedIntegrationTransport({
55
+ network: deps.settings,
56
+ ...(overrides.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {}),
57
+ });
58
+
59
+ app.get("/v1/workspaces/:workspaceId/integrations/presets", async (c) => {
60
+ const workspaceId = c.req.param("workspaceId");
61
+ await requireAccessGrant(c, deps, workspaceId, "workspace:read");
62
+ return c.json(
63
+ ListApiIntegrationPresetsResponse.parse({
64
+ presets: CORE_PROVIDER_PRESETS.map((preset) => ({
65
+ id: preset.id,
66
+ name: preset.name,
67
+ summary: preset.summary,
68
+ family: preset.family,
69
+ protocol: "openapi",
70
+ providerDomain: providerDomainForPreset(preset),
71
+ scopes: [...preset.oauth.scopes],
72
+ features: preset.features.map((feature) => ({
73
+ featureKey: feature.featureKey,
74
+ kind: feature.kind,
75
+ configSchema: { ...feature.configSchema },
76
+ capabilities: { ...feature.capabilities },
77
+ })),
78
+ })),
79
+ }),
80
+ );
81
+ });
82
+
83
+ app.get("/v1/workspaces/:workspaceId/integrations", async (c) => {
84
+ const workspaceId = c.req.param("workspaceId");
85
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
86
+ const integrations = await listInstalledApiIntegrations(deps.db, workspaceId, grant.subjectId);
87
+ return c.json(
88
+ ListApiIntegrationsResponse.parse({
89
+ integrations: integrations.map((integration) => {
90
+ return {
91
+ capabilityId: integration.capabilityId,
92
+ pluginKey: integration.pluginKey,
93
+ installationVersion: integration.installationVersion,
94
+ instanceId: integration.instanceId,
95
+ instanceKey: integration.instanceKey,
96
+ displayName: integration.displayName,
97
+ instanceVersion: integration.instanceVersion,
98
+ serverId: integration.serverId,
99
+ name: integration.name,
100
+ description: integration.description,
101
+ protocol: integration.protocol,
102
+ presetId: integration.presetId,
103
+ providerDomain: integration.providerDomain,
104
+ baseUrl: integration.baseUrl,
105
+ sourceUrl: integration.sourceUrl,
106
+ connected: integration.connectionRef !== null,
107
+ requiresConnection: apiIntegrationRequiresConnection(integration.authScheme),
108
+ connectionId: integration.connectionRef?.connectionId ?? null,
109
+ ownership:
110
+ integration.connectionRef?.subjectScope === "subject"
111
+ ? "personal"
112
+ : integration.connectionRef
113
+ ? "workspace"
114
+ : "none",
115
+ allowedTools: integration.allowedTools,
116
+ toolCount: integration.allowedTools.length,
117
+ approvalRequiredToolCount:
118
+ integration.requireApproval === true
119
+ ? integration.allowedTools.length
120
+ : integration.requireApproval.length,
121
+ revisionId: integration.revision.id,
122
+ contentSha256: integration.revision.contentSha256,
123
+ };
124
+ }),
125
+ }),
126
+ );
127
+ });
128
+
129
+ app.post("/v1/workspaces/:workspaceId/integrations/preview", async (c) => {
130
+ const workspaceId = c.req.param("workspaceId");
131
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
132
+ const payload = PreviewApiIntegrationRequest.parse(await c.req.json());
133
+ const resolved = await resolveForRoute({
134
+ deps,
135
+ transport,
136
+ workspaceId,
137
+ subjectId: grant.subjectId,
138
+ payload,
139
+ });
140
+ return c.json(ApiIntegrationPreview.parse(resolved.preview));
141
+ });
142
+
143
+ app.post("/v1/workspaces/:workspaceId/integrations/install", async (c) => {
144
+ const workspaceId = c.req.param("workspaceId");
145
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
146
+ const payload = InstallApiIntegrationRequest.parse(await c.req.json());
147
+ const resolved = await resolveForRoute({
148
+ deps,
149
+ transport,
150
+ workspaceId,
151
+ subjectId: grant.subjectId,
152
+ payload,
153
+ });
154
+ if (
155
+ resolved.preview.revisionId !== payload.expectedRevisionId ||
156
+ resolved.preview.contentSha256 !== payload.expectedContentSha256
157
+ ) {
158
+ throw new HTTPException(409, {
159
+ message:
160
+ "The Integration source changed after preview. Review the new tools and permissions before installing.",
161
+ });
162
+ }
163
+ if (resolved.preview.auth.kind !== "none" && !payload.connectionId) {
164
+ throw new HTTPException(422, {
165
+ message: "Connect an account before installing this Integration.",
166
+ });
167
+ }
168
+ if (
169
+ payload.ownership &&
170
+ resolved.preview.connectionOwnership &&
171
+ payload.ownership !== resolved.preview.connectionOwnership
172
+ ) {
173
+ throw new HTTPException(422, {
174
+ message: "The selected Connection ownership does not match this install request.",
175
+ });
176
+ }
177
+ if (payload.ownership === "personal" && !resolved.preview.connectionOwnership) {
178
+ throw new HTTPException(422, {
179
+ message: "Choose a Personal Connection before installing for yourself.",
180
+ });
181
+ }
182
+ try {
183
+ return c.json(
184
+ InstalledApiIntegration.parse(
185
+ await installApiIntegration(deps.db, {
186
+ accountId: grant.accountId,
187
+ workspaceId,
188
+ subjectId: grant.subjectId,
189
+ capabilityId: resolved.preview.capabilityId,
190
+ pluginKey: resolved.preview.pluginKey,
191
+ serverId: resolved.preview.serverId,
192
+ name: resolved.preview.name,
193
+ description: resolved.preview.description,
194
+ category: "integrations",
195
+ tags: [resolved.preview.protocol, resolved.preview.provider ?? "custom"],
196
+ presetId: resolved.preview.presetId,
197
+ ...(resolved.provider ? { provider: resolved.provider } : {}),
198
+ providerDomain: resolved.preview.providerDomain,
199
+ protocol: resolved.preview.protocol,
200
+ baseUrl: resolved.preview.baseUrl,
201
+ sourceUrl: resolved.preview.sourceUrl,
202
+ authScheme: resolved.authScheme,
203
+ ...(payload.connectionId ? { connectionId: payload.connectionId } : {}),
204
+ ...(payload.instanceKey ? { instanceKey: payload.instanceKey } : {}),
205
+ ...(payload.displayName ? { displayName: payload.displayName } : {}),
206
+ ...(payload.expectedInstanceVersion !== undefined
207
+ ? { expectedInstanceVersion: payload.expectedInstanceVersion }
208
+ : {}),
209
+ requiredScopes: resolved.requiredScopes,
210
+ ownership:
211
+ resolved.preview.connectionOwnership === "personal" ? "subject" : "workspace",
212
+ ...(payload.allowedTools ? { allowedTools: payload.allowedTools } : {}),
213
+ featureDefinitions: integrationFeaturesForPreset(resolved.preview.presetId),
214
+ revision: resolved.revision,
215
+ }),
216
+ ),
217
+ payload.expectedInstanceVersion === undefined ? 201 : 200,
218
+ );
219
+ } catch (error) {
220
+ if (
221
+ error instanceof IntegrationFeatureBindingVersionConflictError ||
222
+ error instanceof IntegrationFeatureBindingVersionRequiredError ||
223
+ error instanceof IntegrationFeatureBindingOwnershipConflictError
224
+ ) {
225
+ throw new HTTPException(409, {
226
+ message:
227
+ "The Integration instance changed or is shared by another owner. Refresh its details before updating it.",
228
+ });
229
+ }
230
+ throw error;
231
+ }
232
+ });
233
+
234
+ app.get(
235
+ "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/uninstall-preview",
236
+ async (c) => {
237
+ const workspaceId = c.req.param("workspaceId");
238
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
239
+ return c.json(
240
+ ApiIntegrationUninstallPreview.parse(
241
+ await getApiIntegrationUninstallPreview(
242
+ deps.db,
243
+ workspaceId,
244
+ grant.subjectId,
245
+ decodeURIComponent(c.req.param("capabilityId")),
246
+ decodeURIComponent(c.req.param("instanceKey")),
247
+ ),
248
+ ),
249
+ );
250
+ },
251
+ );
252
+
253
+ app.delete(
254
+ "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey",
255
+ async (c) => {
256
+ const workspaceId = c.req.param("workspaceId");
257
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
258
+ const capabilityId = decodeURIComponent(c.req.param("capabilityId"));
259
+ const instanceKey = decodeURIComponent(c.req.param("instanceKey"));
260
+ const payload = UninstallApiIntegrationRequest.parse(await c.req.json());
261
+ try {
262
+ return c.json(
263
+ UninstallApiIntegrationResult.parse(
264
+ await uninstallApiIntegration(deps.db, {
265
+ accountId: grant.accountId,
266
+ workspaceId,
267
+ subjectId: grant.subjectId,
268
+ capabilityId,
269
+ instanceKey,
270
+ expectedInstallationVersion: payload.expectedInstallationVersion,
271
+ expectedInstanceVersion: payload.expectedInstanceVersion,
272
+ }),
273
+ ),
274
+ );
275
+ } catch (error) {
276
+ if (
277
+ error instanceof ApiIntegrationInstallationVersionConflictError ||
278
+ error instanceof IntegrationFeatureBindingVersionConflictError
279
+ ) {
280
+ throw new HTTPException(409, {
281
+ message:
282
+ "The Integration instance changed after preview. Review uninstall impact again.",
283
+ });
284
+ }
285
+ throw error;
286
+ }
287
+ },
288
+ );
289
+ }
290
+
291
+ async function resolveForRoute(input: {
292
+ deps: ApiRouteDeps;
293
+ transport: ReturnType<typeof createPinnedIntegrationTransport>;
294
+ workspaceId: string;
295
+ subjectId: string;
296
+ payload: PreviewApiIntegrationRequest | InstallApiIntegrationRequest;
297
+ }): ReturnType<typeof resolveApiIntegrationPreview> {
298
+ try {
299
+ const connection = input.payload.connectionId
300
+ ? await requireVisibleConnection(
301
+ input.deps,
302
+ input.workspaceId,
303
+ input.subjectId,
304
+ input.payload.connectionId,
305
+ )
306
+ : null;
307
+ return await resolveApiIntegrationPreview({
308
+ source: input.payload.source,
309
+ connection: connectionDescriptor(connection),
310
+ transport: input.transport,
311
+ authority: {
312
+ accountId: connection?.accountId ?? "preview",
313
+ workspaceId: input.workspaceId,
314
+ initiatingSubjectId: input.subjectId,
315
+ ...(connection ? { connectionRef: connection.id } : {}),
316
+ },
317
+ ...(connection
318
+ ? {
319
+ credentialResolver: previewCredentialResolver(
320
+ input.deps,
321
+ input.workspaceId,
322
+ input.subjectId,
323
+ connection,
324
+ ),
325
+ }
326
+ : {}),
327
+ });
328
+ } catch (error) {
329
+ if (error instanceof HTTPException) throw error;
330
+ throw new HTTPException(422, {
331
+ message:
332
+ error instanceof Error
333
+ ? error.message
334
+ : "The Integration source could not be detected safely",
335
+ });
336
+ }
337
+ }
338
+
339
+ async function requireVisibleConnection(
340
+ deps: ApiRouteDeps,
341
+ workspaceId: string,
342
+ subjectId: string,
343
+ connectionId: string,
344
+ ): Promise<ConnectionMetadataWithVerification> {
345
+ const connection = await getConnectionMetadata(deps.db, workspaceId, connectionId, subjectId);
346
+ if (!connection) throw new HTTPException(404, { message: "connection not found" });
347
+ if (connection.status !== "active") {
348
+ throw new HTTPException(422, { message: "connection is not active" });
349
+ }
350
+ return connection;
351
+ }
352
+
353
+ function connectionDescriptor(
354
+ connection: ConnectionMetadataWithVerification | null,
355
+ ): ApiIntegrationConnectionDescriptor | null {
356
+ return connection
357
+ ? {
358
+ id: connection.id,
359
+ kind: connection.kind,
360
+ providerDomain: connection.providerDomain,
361
+ scopes: [...connection.grantedScopes],
362
+ ownership: connection.subjectId ? "personal" : "workspace",
363
+ }
364
+ : null;
365
+ }
366
+
367
+ function previewCredentialResolver(
368
+ deps: ApiRouteDeps,
369
+ workspaceId: string,
370
+ subjectId: string,
371
+ connection: ConnectionMetadataWithVerification,
372
+ ): IntegrationCredentialResolver {
373
+ const resolve = buildConnectionTokenResolver(deps.db, deps.settings);
374
+ return {
375
+ resolve: async (request) => {
376
+ const result = await resolve({
377
+ workspaceId,
378
+ ...(connection.subjectId ? { subjectId } : {}),
379
+ serverId: `preview_${connection.id}`,
380
+ toolName: request.operationKey,
381
+ connectionRef: {
382
+ connectionId: connection.id,
383
+ providerDomain: connection.providerDomain,
384
+ kind: connection.kind,
385
+ ...(connection.grantedScopes.length > 0 ? { scopes: [...connection.grantedScopes] } : {}),
386
+ subjectScope: connection.subjectId ? "subject" : "workspace",
387
+ },
388
+ destinationUrl: request.destinationUrl,
389
+ credentialTarget: "http_api",
390
+ forceRefresh: request.forceRefresh === true,
391
+ });
392
+ if (result.status === "auth_needed") return null;
393
+ const destination = new URL(request.destinationUrl);
394
+ return {
395
+ audience: { origin: destination.origin, pathPrefix: "/" },
396
+ placements:
397
+ result.placements ??
398
+ Object.entries(result.headers).map(([name, value]) => ({
399
+ carrier: "header" as const,
400
+ name,
401
+ value,
402
+ })),
403
+ ...(result.expiresAt ? { expiresAt: result.expiresAt.toISOString() } : {}),
404
+ };
405
+ },
406
+ };
407
+ }