@opengeni/api-router 0.23.1 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +3 -2
- package/dist/app.js +5 -3
- package/dist/auth/canonical-human-session-admission.d.ts +27 -0
- package/dist/browser-auth-broker.d.ts +20 -0
- package/dist/browser-controller-authority.d.ts +61 -0
- package/dist/browser-network-route.d.ts +8 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-T4T2PGU4.js → chunk-2PQMSRCB.js} +37891 -17463
- package/dist/chunk-2PQMSRCB.js.map +1 -0
- package/dist/codemode.d.ts +23 -0
- package/dist/controller-data-plane.d.ts +12 -0
- package/dist/editable-artifact-live-hints.d.ts +11 -0
- package/dist/editable-artifact-native-kernel.d.ts +37 -0
- package/dist/editable-artifact-office-import.d.ts +22 -0
- package/dist/editable-artifact-production.d.ts +29 -0
- package/dist/editable-artifact-websocket.d.ts +49 -0
- package/dist/editable-artifact-workspace-files.d.ts +23 -0
- package/dist/github-browser-flow.d.ts +6 -0
- package/dist/http/auth.d.ts +1 -1
- package/dist/http/cors.d.ts +1 -0
- package/dist/http/sse.d.ts +18 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1796 -22
- package/dist/index.js.map +1 -1
- package/dist/integrations/api-integrations.d.ts +24 -0
- package/dist/integrations/atlassian.d.ts +176 -0
- package/dist/integrations/fiken.d.ts +233 -0
- package/dist/integrations/github-skill-source.d.ts +5 -0
- package/dist/integrations/google-drive.d.ts +85 -0
- package/dist/integrations/oauth-client.d.ts +22 -1
- package/dist/integrations/provider-oauth.d.ts +17 -0
- package/dist/integrations/slack-bot.d.ts +175 -1
- package/dist/integrations/slack-interactions.d.ts +24 -6
- package/dist/integrations/social-api.d.ts +2 -1
- package/dist/interaction-holder-heartbeat.d.ts +17 -0
- package/dist/interaction-metrics.d.ts +11 -0
- package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
- package/dist/mcp/editable-artifacts.d.ts +13 -0
- package/dist/mcp/scheduled-task-view.d.ts +168 -0
- package/dist/mcp/server.d.ts +16 -4
- package/dist/memory-slack-delivery.d.ts +9 -0
- package/dist/model-catalog.d.ts +1 -0
- package/dist/routes/api-integrations.d.ts +8 -0
- package/dist/routes/browser-identities.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +6 -0
- package/dist/routes/canonical-human-identities.d.ts +3 -0
- package/dist/routes/channels.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +3 -0
- package/dist/routes/computer-sessions.d.ts +6 -0
- package/dist/routes/editable-artifacts.d.ts +44 -0
- package/dist/routes/integration-facets.d.ts +3 -0
- package/dist/routes/interaction-resources.d.ts +5 -0
- package/dist/routes/memory-slack-publications.d.ts +6 -0
- package/dist/routes/organization-memberships.d.ts +3 -0
- package/dist/routes/plugins.d.ts +8 -0
- package/dist/routes/sessions.d.ts +18 -2
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/slack-task-policy.d.ts +3 -0
- package/dist/routes/supergrok.d.ts +3 -0
- package/dist/routes/video-generation.d.ts +3 -0
- package/dist/sandbox/channel-a.d.ts +62 -2
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -1
- package/dist/sandbox/viewer.d.ts +7 -3
- package/dist/slack-reaction-files.d.ts +27 -0
- package/dist/temporal-schedule-cleanup.d.ts +26 -0
- package/dist/transcription/providers/xai-subscription.d.ts +8 -0
- package/dist/xai-realtime.d.ts +26 -0
- package/dist/xai-subscription-auth.d.ts +23 -0
- package/package.json +20 -14
- package/src/app.ts +604 -64
- package/src/auth/canonical-human-session-admission.ts +116 -0
- package/src/auth/managed-auth.ts +101 -0
- package/src/browser-auth-broker.ts +155 -0
- package/src/browser-controller-authority.ts +186 -0
- package/src/browser-network-route.ts +34 -0
- package/src/browser-state-authority.ts +236 -0
- package/src/codemode.ts +186 -0
- package/src/controller-data-plane.ts +35 -0
- package/src/editable-artifact-live-hints.ts +64 -0
- package/src/editable-artifact-native-kernel.ts +659 -0
- package/src/editable-artifact-office-import.ts +230 -0
- package/src/editable-artifact-production.ts +419 -0
- package/src/editable-artifact-websocket.ts +311 -0
- package/src/editable-artifact-workspace-files.ts +186 -0
- package/src/github-browser-flow.ts +35 -6
- package/src/http/auth.ts +29 -9
- package/src/http/cors.ts +3 -0
- package/src/http/sse.ts +229 -5
- package/src/index.ts +119 -20
- package/src/integrations/api-integrations.ts +365 -0
- package/src/integrations/atlassian.ts +1621 -0
- package/src/integrations/fiken.ts +1230 -0
- package/src/integrations/github-skill-source.ts +142 -0
- package/src/integrations/google-drive.ts +1005 -64
- package/src/integrations/oauth-client.ts +123 -33
- package/src/integrations/provider-oauth.ts +756 -0
- package/src/integrations/slack-bot.ts +669 -11
- package/src/integrations/slack-interactions.ts +2063 -93
- package/src/integrations/social-api.ts +11 -0
- package/src/interaction-holder-heartbeat.ts +95 -0
- package/src/interaction-metrics.ts +159 -0
- package/src/mcp/documents.ts +146 -6
- package/src/mcp/editable-artifact-query-schema.ts +236 -0
- package/src/mcp/editable-artifacts.ts +448 -0
- package/src/mcp/scheduled-task-view.ts +34 -0
- package/src/mcp/server.ts +1756 -355
- package/src/memory-slack-delivery.ts +209 -0
- package/src/model-catalog.ts +19 -6
- package/src/routes/api-integrations.ts +410 -0
- package/src/routes/browser-identities.ts +160 -0
- package/src/routes/browser-sessions.ts +4482 -0
- package/src/routes/canonical-human-identities.ts +156 -0
- package/src/routes/channels.ts +90 -0
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1602 -0
- package/src/routes/connections.ts +421 -99
- package/src/routes/documents.ts +22 -3
- package/src/routes/editable-artifacts.ts +1159 -0
- package/src/routes/files.ts +106 -4
- package/src/routes/github.ts +18 -2
- package/src/routes/install.ts +7 -1
- package/src/routes/integration-facets.ts +258 -0
- package/src/routes/interaction-resources.ts +595 -0
- package/src/routes/memory-slack-publications.ts +216 -0
- package/src/routes/organization-memberships.ts +53 -0
- package/src/routes/packs.ts +437 -7
- package/src/routes/plugins.ts +752 -0
- package/src/routes/rigs.ts +77 -20
- package/src/routes/scheduled-tasks.ts +67 -40
- package/src/routes/sessions.ts +830 -278
- package/src/routes/skills.ts +256 -0
- package/src/routes/slack-task-policy.ts +115 -0
- package/src/routes/supergrok.ts +717 -0
- package/src/routes/transcription-recordings.ts +9 -2
- package/src/routes/transcriptions.ts +2 -1
- package/src/routes/video-generation.ts +162 -0
- package/src/routes/workspaces.ts +68 -28
- package/src/sandbox/channel-a.ts +750 -116
- package/src/sandbox/machines.ts +5 -5
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +39 -12
- package/src/slack-reaction-files.ts +181 -0
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/src/transcription/providers/xai-subscription.ts +110 -0
- package/src/transcription/service.ts +17 -2
- package/src/xai-realtime.ts +216 -0
- package/src/xai-subscription-auth.ts +132 -0
- package/dist/chunk-T4T2PGU4.js.map +0 -1
- package/dist/mcp/toolspace.d.ts +0 -71
- package/src/mcp/toolspace.ts +0 -1190
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createPinnedIntegrationTransport,
|
|
5
|
+
fetchIntegrationSourceDocument,
|
|
6
|
+
type IntegrationCredentialResolver,
|
|
7
|
+
} from "@opengeni/capabilities";
|
|
8
|
+
import type { McpServerConfig } from "@opengeni/config";
|
|
9
|
+
import {
|
|
10
|
+
InstallPluginRequest,
|
|
11
|
+
InstalledPlugin,
|
|
12
|
+
ListInstalledPluginsResponse,
|
|
13
|
+
PluginManifest,
|
|
14
|
+
PluginPreview,
|
|
15
|
+
PluginUninstallPreview,
|
|
16
|
+
PreviewPluginRequest,
|
|
17
|
+
UninstallPluginRequest,
|
|
18
|
+
UninstallPluginResult,
|
|
19
|
+
stableJson,
|
|
20
|
+
type PluginComponentPreview,
|
|
21
|
+
} from "@opengeni/contracts";
|
|
22
|
+
import {
|
|
23
|
+
portableSkillCapabilityId,
|
|
24
|
+
portableSkillPluginKey,
|
|
25
|
+
requireAccessGrant,
|
|
26
|
+
resolveSkillImport,
|
|
27
|
+
type ApiRouteDeps,
|
|
28
|
+
type GitHubSkillSourceClient,
|
|
29
|
+
} from "@opengeni/core";
|
|
30
|
+
import {
|
|
31
|
+
buildConnectionTokenResolver,
|
|
32
|
+
CapabilityComponentVersionConflictError,
|
|
33
|
+
checkpointPluginPackageOperation,
|
|
34
|
+
deferPluginPackageOperation,
|
|
35
|
+
finalizePluginPackageInstall,
|
|
36
|
+
getConnectionMetadata,
|
|
37
|
+
getInstalledPluginPackage,
|
|
38
|
+
getPluginPackageUninstallPreview,
|
|
39
|
+
integrationBindingKey,
|
|
40
|
+
installApiIntegration,
|
|
41
|
+
installPluginMcpReference,
|
|
42
|
+
installPortableSkill,
|
|
43
|
+
listInstalledPluginPackages,
|
|
44
|
+
PluginInstallationVersionConflictError,
|
|
45
|
+
PluginInstallationVersionRequiredError,
|
|
46
|
+
PluginOperationIdempotencyError,
|
|
47
|
+
preparePluginPackageInstall,
|
|
48
|
+
uninstallPluginPackage,
|
|
49
|
+
type ConnectionMetadataWithVerification,
|
|
50
|
+
} from "@opengeni/db";
|
|
51
|
+
import type { FetchLike } from "@opengeni/network";
|
|
52
|
+
import type { Hono } from "hono";
|
|
53
|
+
import { HTTPException } from "hono/http-exception";
|
|
54
|
+
|
|
55
|
+
import {
|
|
56
|
+
resolveApiIntegrationPreview,
|
|
57
|
+
type ApiIntegrationConnectionDescriptor,
|
|
58
|
+
type ResolvedApiIntegrationPreview,
|
|
59
|
+
} from "../integrations/api-integrations";
|
|
60
|
+
import { createGitHubSkillSourceClient } from "../integrations/github-skill-source";
|
|
61
|
+
|
|
62
|
+
const MAX_PLUGIN_MANIFEST_BYTES = 1024 * 1024;
|
|
63
|
+
|
|
64
|
+
export type PluginRouteOverrides = Readonly<{
|
|
65
|
+
fetchImpl?: FetchLike;
|
|
66
|
+
github?: GitHubSkillSourceClient;
|
|
67
|
+
}>;
|
|
68
|
+
|
|
69
|
+
type ResolvedPluginComponent = {
|
|
70
|
+
preview: PluginComponentPreview;
|
|
71
|
+
install(ownerPluginInstallationId: string): Promise<{
|
|
72
|
+
facetInstallationIds: string[];
|
|
73
|
+
bindingIds?: string[];
|
|
74
|
+
}>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type ResolvedPluginPackage = {
|
|
78
|
+
preview: PluginPreview;
|
|
79
|
+
storedManifest: Record<string, unknown>;
|
|
80
|
+
components: ResolvedPluginComponent[];
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export function registerPluginRoutes(
|
|
84
|
+
app: Hono,
|
|
85
|
+
deps: ApiRouteDeps,
|
|
86
|
+
overrides: PluginRouteOverrides = {},
|
|
87
|
+
): void {
|
|
88
|
+
const github = overrides.github ?? createGitHubSkillSourceClient(deps.settings);
|
|
89
|
+
const transport = createPinnedIntegrationTransport({
|
|
90
|
+
network: deps.settings,
|
|
91
|
+
...(overrides.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {}),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
app.get("/v1/workspaces/:workspaceId/plugins", async (c) => {
|
|
95
|
+
const workspaceId = c.req.param("workspaceId");
|
|
96
|
+
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
97
|
+
return c.json(
|
|
98
|
+
ListInstalledPluginsResponse.parse({
|
|
99
|
+
plugins: await listInstalledPluginPackages(deps.db, workspaceId),
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
app.post("/v1/workspaces/:workspaceId/plugins/preview", async (c) => {
|
|
105
|
+
const workspaceId = c.req.param("workspaceId");
|
|
106
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
107
|
+
const payload = PreviewPluginRequest.parse(await c.req.json());
|
|
108
|
+
const resolved = await resolvePluginPackage({
|
|
109
|
+
deps,
|
|
110
|
+
github,
|
|
111
|
+
transport,
|
|
112
|
+
accountId: grant.accountId,
|
|
113
|
+
workspaceId,
|
|
114
|
+
subjectId: grant.subjectId,
|
|
115
|
+
url: payload.url,
|
|
116
|
+
bindings: payload.bindings,
|
|
117
|
+
});
|
|
118
|
+
return c.json(PluginPreview.parse(resolved.preview));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
app.post("/v1/workspaces/:workspaceId/plugins/install", async (c) => {
|
|
122
|
+
const workspaceId = c.req.param("workspaceId");
|
|
123
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
124
|
+
const payload = InstallPluginRequest.parse(await c.req.json());
|
|
125
|
+
const resolved = await resolvePluginPackage({
|
|
126
|
+
deps,
|
|
127
|
+
github,
|
|
128
|
+
transport,
|
|
129
|
+
accountId: grant.accountId,
|
|
130
|
+
workspaceId,
|
|
131
|
+
subjectId: grant.subjectId,
|
|
132
|
+
url: payload.url,
|
|
133
|
+
bindings: payload.bindings,
|
|
134
|
+
});
|
|
135
|
+
if (resolved.preview.manifestDigest !== payload.expectedManifestDigest) {
|
|
136
|
+
throw new HTTPException(409, {
|
|
137
|
+
message: "The Plugin manifest changed after preview. Review the new components.",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const expected = new Map(
|
|
141
|
+
payload.expectedComponents.map((component) => [component.key, component.digest]),
|
|
142
|
+
);
|
|
143
|
+
if (
|
|
144
|
+
expected.size !== resolved.components.length ||
|
|
145
|
+
resolved.components.some(
|
|
146
|
+
(component) => expected.get(component.preview.key) !== component.preview.digest,
|
|
147
|
+
)
|
|
148
|
+
) {
|
|
149
|
+
throw new HTTPException(409, {
|
|
150
|
+
message: "A Plugin component changed after preview. Review the updated bill of materials.",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const requestDigest = sha256(
|
|
154
|
+
stableJson({
|
|
155
|
+
url: payload.url,
|
|
156
|
+
expectedManifestDigest: payload.expectedManifestDigest,
|
|
157
|
+
expectedComponents: [...expected.entries()].sort(),
|
|
158
|
+
bindings: payload.bindings,
|
|
159
|
+
expectedInstallationVersion: payload.expectedInstallationVersion ?? null,
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
let prepared;
|
|
163
|
+
try {
|
|
164
|
+
prepared = await preparePluginPackageInstall(deps.db, {
|
|
165
|
+
accountId: grant.accountId,
|
|
166
|
+
workspaceId,
|
|
167
|
+
subjectId: grant.subjectId,
|
|
168
|
+
pluginKey: resolved.preview.manifest.pluginKey,
|
|
169
|
+
version: resolved.preview.manifest.version,
|
|
170
|
+
name: resolved.preview.manifest.name,
|
|
171
|
+
description: resolved.preview.manifest.description,
|
|
172
|
+
category: resolved.preview.manifest.category,
|
|
173
|
+
tags: resolved.preview.manifest.tags,
|
|
174
|
+
manifestDigest: resolved.preview.manifestDigest,
|
|
175
|
+
manifest: resolved.storedManifest,
|
|
176
|
+
idempotencyKey: payload.idempotencyKey,
|
|
177
|
+
requestDigest,
|
|
178
|
+
...(payload.expectedInstallationVersion !== undefined
|
|
179
|
+
? { expectedInstallationVersion: payload.expectedInstallationVersion }
|
|
180
|
+
: {}),
|
|
181
|
+
});
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw pluginMutationHttpError(error);
|
|
184
|
+
}
|
|
185
|
+
if (prepared.replayResult?.status === "installed") {
|
|
186
|
+
return c.json(InstalledPlugin.parse(prepared.replayResult), 200);
|
|
187
|
+
}
|
|
188
|
+
const retainedFacetInstallationIds: string[] = [];
|
|
189
|
+
const retainedBindingIds: string[] = [];
|
|
190
|
+
const completedKeys: string[] = [];
|
|
191
|
+
let activeComponentKey = "none";
|
|
192
|
+
try {
|
|
193
|
+
for (const component of resolved.components) {
|
|
194
|
+
activeComponentKey = component.preview.key;
|
|
195
|
+
const installed = await component.install(prepared.pluginInstallationId);
|
|
196
|
+
retainedFacetInstallationIds.push(...installed.facetInstallationIds);
|
|
197
|
+
retainedBindingIds.push(...(installed.bindingIds ?? []));
|
|
198
|
+
completedKeys.push(component.preview.key);
|
|
199
|
+
await checkpointPluginPackageOperation(deps.db, {
|
|
200
|
+
workspaceId,
|
|
201
|
+
operationId: prepared.operationId,
|
|
202
|
+
phase: `component:${component.preview.key}`,
|
|
203
|
+
completedKeys,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const result = InstalledPlugin.parse({
|
|
207
|
+
pluginKey: prepared.pluginKey,
|
|
208
|
+
version: prepared.version,
|
|
209
|
+
pluginId: prepared.pluginId,
|
|
210
|
+
pluginVersionId: prepared.pluginVersionId,
|
|
211
|
+
pluginInstallationId: prepared.pluginInstallationId,
|
|
212
|
+
installationVersion: prepared.installationVersion,
|
|
213
|
+
componentCount: resolved.components.length,
|
|
214
|
+
status: "installed",
|
|
215
|
+
});
|
|
216
|
+
await finalizePluginPackageInstall(deps.db, {
|
|
217
|
+
accountId: grant.accountId,
|
|
218
|
+
workspaceId,
|
|
219
|
+
subjectId: grant.subjectId,
|
|
220
|
+
operationId: prepared.operationId,
|
|
221
|
+
pluginInstallationId: prepared.pluginInstallationId,
|
|
222
|
+
retainedFacetInstallationIds,
|
|
223
|
+
retainedBindingIds,
|
|
224
|
+
result,
|
|
225
|
+
});
|
|
226
|
+
return c.json(result, resolved.preview.installed ? 200 : 201);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
await deferPluginPackageOperation(deps.db, {
|
|
229
|
+
workspaceId,
|
|
230
|
+
operationId: prepared.operationId,
|
|
231
|
+
phase: `component_failed:${activeComponentKey}`,
|
|
232
|
+
errorCode: pluginFailureCode(error),
|
|
233
|
+
}).catch(() => undefined);
|
|
234
|
+
if (error instanceof HTTPException) throw error;
|
|
235
|
+
if (error instanceof CapabilityComponentVersionConflictError) {
|
|
236
|
+
throw new HTTPException(409, {
|
|
237
|
+
message: `Plugin component ${activeComponentKey} is pinned to another version by an installed owner. Update or remove that owner before retrying with the same idempotency key.`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
throw new HTTPException(422, {
|
|
241
|
+
message: `Plugin component ${activeComponentKey} could not be installed. No Plugin-owned component is active until installation completes; retry with the same idempotency key.`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
app.get("/v1/workspaces/:workspaceId/plugins/:pluginKey/uninstall-preview", async (c) => {
|
|
247
|
+
const workspaceId = c.req.param("workspaceId");
|
|
248
|
+
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
249
|
+
const pluginKey = decodeURIComponent(c.req.param("pluginKey"));
|
|
250
|
+
const preview = await getPluginPackageUninstallPreview(deps.db, workspaceId, pluginKey);
|
|
251
|
+
return c.json(PluginUninstallPreview.parse({ pluginKey, ...preview }));
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
app.delete("/v1/workspaces/:workspaceId/plugins/:pluginKey", async (c) => {
|
|
255
|
+
const workspaceId = c.req.param("workspaceId");
|
|
256
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
257
|
+
const pluginKey = decodeURIComponent(c.req.param("pluginKey"));
|
|
258
|
+
const payload = UninstallPluginRequest.parse(await c.req.json());
|
|
259
|
+
try {
|
|
260
|
+
return c.json(
|
|
261
|
+
UninstallPluginResult.parse({
|
|
262
|
+
pluginKey,
|
|
263
|
+
...(await uninstallPluginPackage(deps.db, {
|
|
264
|
+
accountId: grant.accountId,
|
|
265
|
+
workspaceId,
|
|
266
|
+
subjectId: grant.subjectId,
|
|
267
|
+
pluginKey,
|
|
268
|
+
expectedInstallationVersion: payload.expectedInstallationVersion,
|
|
269
|
+
idempotencyKey: payload.idempotencyKey,
|
|
270
|
+
})),
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
throw pluginMutationHttpError(error);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function resolvePluginPackage(input: {
|
|
280
|
+
deps: ApiRouteDeps;
|
|
281
|
+
github: GitHubSkillSourceClient;
|
|
282
|
+
transport: ReturnType<typeof createPinnedIntegrationTransport>;
|
|
283
|
+
accountId: string;
|
|
284
|
+
workspaceId: string;
|
|
285
|
+
subjectId: string;
|
|
286
|
+
url: string;
|
|
287
|
+
bindings: Record<
|
|
288
|
+
string,
|
|
289
|
+
{
|
|
290
|
+
connectionId?: string | undefined;
|
|
291
|
+
instanceKey?: string | undefined;
|
|
292
|
+
displayName?: string | undefined;
|
|
293
|
+
}
|
|
294
|
+
>;
|
|
295
|
+
}): Promise<ResolvedPluginPackage> {
|
|
296
|
+
let manifest;
|
|
297
|
+
try {
|
|
298
|
+
const bytes = await fetchIntegrationSourceDocument(
|
|
299
|
+
input.transport,
|
|
300
|
+
input.url,
|
|
301
|
+
MAX_PLUGIN_MANIFEST_BYTES,
|
|
302
|
+
);
|
|
303
|
+
manifest = PluginManifest.parse(
|
|
304
|
+
JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)),
|
|
305
|
+
);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (error instanceof HTTPException) throw error;
|
|
308
|
+
throw new HTTPException(422, {
|
|
309
|
+
message: error instanceof Error ? error.message : "Plugin manifest is invalid",
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
const manifestDigest = sha256(stableJson(manifest));
|
|
313
|
+
const components: ResolvedPluginComponent[] = [];
|
|
314
|
+
for (const component of manifest.components) {
|
|
315
|
+
if (component.kind === "skill") {
|
|
316
|
+
const resolved = await resolveSkillImport(component.url, input.github);
|
|
317
|
+
const capabilityId = portableSkillCapabilityId(resolved.preview);
|
|
318
|
+
const digest = sha256(
|
|
319
|
+
stableJson({
|
|
320
|
+
capabilityId,
|
|
321
|
+
sourceCommit: resolved.preview.sourceCommit,
|
|
322
|
+
sourcePath: resolved.preview.sourcePath,
|
|
323
|
+
contentSha256: resolved.preview.contentSha256,
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
components.push({
|
|
327
|
+
preview: {
|
|
328
|
+
key: component.key,
|
|
329
|
+
kind: "skill",
|
|
330
|
+
name: resolved.preview.name,
|
|
331
|
+
capabilityId,
|
|
332
|
+
digest,
|
|
333
|
+
connectionRequired: false,
|
|
334
|
+
connectionId: null,
|
|
335
|
+
instanceKey: null,
|
|
336
|
+
displayName: null,
|
|
337
|
+
facts: {
|
|
338
|
+
sourceCommit: resolved.preview.sourceCommit,
|
|
339
|
+
sourcePath: resolved.preview.sourcePath,
|
|
340
|
+
fileCount: resolved.preview.files.length,
|
|
341
|
+
totalBytes: resolved.preview.totalBytes,
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
install: async (ownerPluginInstallationId) => {
|
|
345
|
+
const summaries = new Map(
|
|
346
|
+
resolved.preview.files.map((file) => [file.path, file] as const),
|
|
347
|
+
);
|
|
348
|
+
const installed = await installPortableSkill(input.deps.db, {
|
|
349
|
+
accountId: input.accountId,
|
|
350
|
+
workspaceId: input.workspaceId,
|
|
351
|
+
subjectId: input.subjectId,
|
|
352
|
+
capabilityId,
|
|
353
|
+
pluginKey: portableSkillPluginKey(resolved.preview),
|
|
354
|
+
source: resolved.preview.source,
|
|
355
|
+
sourceUrl: resolved.preview.sourceUrl,
|
|
356
|
+
repositoryUrl: resolved.preview.repositoryUrl,
|
|
357
|
+
sourceCommit: resolved.preview.sourceCommit,
|
|
358
|
+
sourcePath: resolved.preview.sourcePath,
|
|
359
|
+
name: resolved.preview.name,
|
|
360
|
+
description: resolved.preview.description,
|
|
361
|
+
contentSha256: resolved.preview.contentSha256,
|
|
362
|
+
totalBytes: resolved.preview.totalBytes,
|
|
363
|
+
files: resolved.files.map((file) => {
|
|
364
|
+
const summary = summaries.get(file.path)!;
|
|
365
|
+
return { ...file, byteSize: summary.byteSize, contentSha256: summary.contentSha256 };
|
|
366
|
+
}),
|
|
367
|
+
owner: { kind: "plugin", id: ownerPluginInstallationId, removable: true },
|
|
368
|
+
});
|
|
369
|
+
return { facetInstallationIds: [installed.facetInstallationId] };
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (component.kind === "integration") {
|
|
375
|
+
const requestedBinding = input.bindings[component.key];
|
|
376
|
+
const connection = await optionalConnection(
|
|
377
|
+
input.deps,
|
|
378
|
+
input.workspaceId,
|
|
379
|
+
input.subjectId,
|
|
380
|
+
requestedBinding?.connectionId,
|
|
381
|
+
);
|
|
382
|
+
const resolved = await resolveApiIntegrationPreview({
|
|
383
|
+
source: component.source,
|
|
384
|
+
connection: connectionDescriptor(connection),
|
|
385
|
+
transport: input.transport,
|
|
386
|
+
authority: {
|
|
387
|
+
accountId: input.accountId,
|
|
388
|
+
workspaceId: input.workspaceId,
|
|
389
|
+
initiatingSubjectId: input.subjectId,
|
|
390
|
+
...(connection ? { connectionRef: connection.id } : {}),
|
|
391
|
+
},
|
|
392
|
+
...(connection
|
|
393
|
+
? {
|
|
394
|
+
credentialResolver: pluginCredentialResolver(
|
|
395
|
+
input.deps,
|
|
396
|
+
input.workspaceId,
|
|
397
|
+
input.subjectId,
|
|
398
|
+
connection,
|
|
399
|
+
),
|
|
400
|
+
}
|
|
401
|
+
: {}),
|
|
402
|
+
});
|
|
403
|
+
const digest = integrationDigest(resolved);
|
|
404
|
+
const instanceKey = integrationBindingKey(connection?.id, requestedBinding?.instanceKey);
|
|
405
|
+
const displayName =
|
|
406
|
+
requestedBinding?.displayName?.trim() ||
|
|
407
|
+
(instanceKey === "default"
|
|
408
|
+
? resolved.preview.name
|
|
409
|
+
: `${resolved.preview.name} — connected account`);
|
|
410
|
+
components.push({
|
|
411
|
+
preview: {
|
|
412
|
+
key: component.key,
|
|
413
|
+
kind: "integration",
|
|
414
|
+
name: resolved.preview.name,
|
|
415
|
+
capabilityId: resolved.preview.capabilityId,
|
|
416
|
+
digest,
|
|
417
|
+
connectionRequired: resolved.preview.auth.kind !== "none",
|
|
418
|
+
connectionId: connection?.id ?? null,
|
|
419
|
+
instanceKey,
|
|
420
|
+
displayName,
|
|
421
|
+
facts: {
|
|
422
|
+
protocol: resolved.preview.protocol,
|
|
423
|
+
providerDomain: resolved.preview.providerDomain,
|
|
424
|
+
revisionId: resolved.preview.revisionId,
|
|
425
|
+
contentSha256: resolved.preview.contentSha256,
|
|
426
|
+
toolCount: resolved.preview.tools.length,
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
install: async (ownerPluginInstallationId) => {
|
|
430
|
+
if (resolved.preview.auth.kind !== "none" && !connection) {
|
|
431
|
+
throw new HTTPException(422, {
|
|
432
|
+
message: `Plugin component ${component.key} requires a Connection binding`,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
const installed = await installApiIntegration(input.deps.db, {
|
|
436
|
+
accountId: input.accountId,
|
|
437
|
+
workspaceId: input.workspaceId,
|
|
438
|
+
subjectId: input.subjectId,
|
|
439
|
+
capabilityId: resolved.preview.capabilityId,
|
|
440
|
+
pluginKey: resolved.preview.pluginKey,
|
|
441
|
+
serverId: resolved.preview.serverId,
|
|
442
|
+
name: resolved.preview.name,
|
|
443
|
+
description: resolved.preview.description,
|
|
444
|
+
category: "integrations",
|
|
445
|
+
tags: [resolved.preview.protocol, resolved.preview.provider ?? "custom", "plugin"],
|
|
446
|
+
definitionId: resolved.preview.definitionId,
|
|
447
|
+
definitionProvenance: resolved.preview.definitionProvenance,
|
|
448
|
+
...(resolved.provider ? { provider: resolved.provider } : {}),
|
|
449
|
+
providerDomain: resolved.preview.providerDomain,
|
|
450
|
+
protocol: resolved.preview.protocol,
|
|
451
|
+
baseUrl: resolved.preview.baseUrl,
|
|
452
|
+
sourceUrl: resolved.preview.sourceUrl,
|
|
453
|
+
authScheme: resolved.authScheme,
|
|
454
|
+
...(connection ? { connectionId: connection.id } : {}),
|
|
455
|
+
instanceKey,
|
|
456
|
+
displayName,
|
|
457
|
+
requiredScopes: resolved.requiredScopes,
|
|
458
|
+
ownership: connection?.subjectId ? "subject" : "workspace",
|
|
459
|
+
revision: resolved.revision,
|
|
460
|
+
owner: { kind: "plugin", id: ownerPluginInstallationId, removable: true },
|
|
461
|
+
});
|
|
462
|
+
return {
|
|
463
|
+
facetInstallationIds: [
|
|
464
|
+
installed.integrationFacetInstallationId,
|
|
465
|
+
installed.apiFacetInstallationId,
|
|
466
|
+
],
|
|
467
|
+
bindingIds: [installed.instanceId],
|
|
468
|
+
};
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const server = configuredMcpServer(input.deps.settings.mcpServers, component.serverId);
|
|
474
|
+
const digest = sha256(stableJson(safeMcpFacts(server)));
|
|
475
|
+
components.push({
|
|
476
|
+
preview: {
|
|
477
|
+
key: component.key,
|
|
478
|
+
kind: "mcp",
|
|
479
|
+
name: server.name ?? server.id,
|
|
480
|
+
capabilityId: `mcp:configured:${server.id}`,
|
|
481
|
+
digest,
|
|
482
|
+
connectionRequired: Boolean(server.connectionRef),
|
|
483
|
+
connectionId: server.connectionRef?.connectionId ?? null,
|
|
484
|
+
instanceKey: null,
|
|
485
|
+
displayName: null,
|
|
486
|
+
facts: {
|
|
487
|
+
serverId: server.id,
|
|
488
|
+
allowedToolCount: server.allowedTools?.length ?? 0,
|
|
489
|
+
approvalRequired:
|
|
490
|
+
server.requireApproval !== false && server.requireApproval !== undefined,
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
install: async (ownerPluginInstallationId) => {
|
|
494
|
+
const installed = await installPluginMcpReference(input.deps.db, {
|
|
495
|
+
accountId: input.accountId,
|
|
496
|
+
workspaceId: input.workspaceId,
|
|
497
|
+
subjectId: input.subjectId,
|
|
498
|
+
ownerPluginInstallationId,
|
|
499
|
+
serverId: server.id,
|
|
500
|
+
name: server.name ?? server.id,
|
|
501
|
+
url: server.url,
|
|
502
|
+
allowedTools: server.allowedTools ?? [],
|
|
503
|
+
...(server.timeoutMs ? { timeoutMs: server.timeoutMs } : {}),
|
|
504
|
+
cacheToolsList: server.cacheToolsList,
|
|
505
|
+
...(server.requireApproval !== undefined
|
|
506
|
+
? { requireApproval: server.requireApproval }
|
|
507
|
+
: {}),
|
|
508
|
+
...(server.connectionRef
|
|
509
|
+
? { connectionRef: server.connectionRef as unknown as Record<string, unknown> }
|
|
510
|
+
: {}),
|
|
511
|
+
authKind:
|
|
512
|
+
server.connectionRef?.kind === "oauth2" || server.connectionRef?.kind === "api_key"
|
|
513
|
+
? server.connectionRef.kind
|
|
514
|
+
: server.connectionRef
|
|
515
|
+
? "unknown"
|
|
516
|
+
: "none",
|
|
517
|
+
digest,
|
|
518
|
+
});
|
|
519
|
+
return { facetInstallationIds: [installed.facetInstallationId] };
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
const installed = await getInstalledPluginPackage(
|
|
524
|
+
input.deps.db,
|
|
525
|
+
input.workspaceId,
|
|
526
|
+
manifest.pluginKey,
|
|
527
|
+
);
|
|
528
|
+
const bom = components.map((component) => ({
|
|
529
|
+
key: component.preview.key,
|
|
530
|
+
kind: component.preview.kind,
|
|
531
|
+
capabilityId: component.preview.capabilityId,
|
|
532
|
+
digest: component.preview.digest,
|
|
533
|
+
}));
|
|
534
|
+
const previousBom = pluginBom(installed?.manifest);
|
|
535
|
+
const diff = pluginDiff(installed?.version ?? null, manifest.version, previousBom, bom);
|
|
536
|
+
return {
|
|
537
|
+
preview: PluginPreview.parse({
|
|
538
|
+
sourceUrl: input.url,
|
|
539
|
+
manifest,
|
|
540
|
+
manifestDigest,
|
|
541
|
+
installed: Boolean(installed && installed.status !== "disabled"),
|
|
542
|
+
installationVersion: installed?.installationVersion ?? null,
|
|
543
|
+
components: components.map((component) => component.preview),
|
|
544
|
+
diff,
|
|
545
|
+
}),
|
|
546
|
+
storedManifest: { ...manifest, sourceUrl: input.url, manifestDigest, bom },
|
|
547
|
+
components,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function optionalConnection(
|
|
552
|
+
deps: ApiRouteDeps,
|
|
553
|
+
workspaceId: string,
|
|
554
|
+
subjectId: string,
|
|
555
|
+
connectionId: string | undefined,
|
|
556
|
+
): Promise<ConnectionMetadataWithVerification | null> {
|
|
557
|
+
if (!connectionId) return null;
|
|
558
|
+
const connection = await getConnectionMetadata(deps.db, workspaceId, connectionId, subjectId);
|
|
559
|
+
if (!connection) throw new HTTPException(404, { message: "Plugin Connection was not found" });
|
|
560
|
+
if (connection.status !== "active") {
|
|
561
|
+
throw new HTTPException(422, { message: "Plugin Connection is not active" });
|
|
562
|
+
}
|
|
563
|
+
return connection;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function connectionDescriptor(
|
|
567
|
+
connection: ConnectionMetadataWithVerification | null,
|
|
568
|
+
): ApiIntegrationConnectionDescriptor | null {
|
|
569
|
+
return connection
|
|
570
|
+
? {
|
|
571
|
+
id: connection.id,
|
|
572
|
+
kind: connection.kind,
|
|
573
|
+
providerDomain: connection.providerDomain,
|
|
574
|
+
scopes: [...connection.grantedScopes],
|
|
575
|
+
ownership: connection.subjectId ? "personal" : "workspace",
|
|
576
|
+
}
|
|
577
|
+
: null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function pluginCredentialResolver(
|
|
581
|
+
deps: ApiRouteDeps,
|
|
582
|
+
workspaceId: string,
|
|
583
|
+
subjectId: string,
|
|
584
|
+
connection: ConnectionMetadataWithVerification,
|
|
585
|
+
): IntegrationCredentialResolver {
|
|
586
|
+
const resolve = buildConnectionTokenResolver(deps.db, deps.settings);
|
|
587
|
+
return {
|
|
588
|
+
resolve: async (request) => {
|
|
589
|
+
const result = await resolve({
|
|
590
|
+
workspaceId,
|
|
591
|
+
...(connection.subjectId ? { subjectId } : {}),
|
|
592
|
+
serverId: `plugin_preview_${connection.id}`,
|
|
593
|
+
toolName: request.operationKey,
|
|
594
|
+
connectionRef: {
|
|
595
|
+
connectionId: connection.id,
|
|
596
|
+
providerDomain: connection.providerDomain,
|
|
597
|
+
kind: connection.kind,
|
|
598
|
+
...(connection.grantedScopes.length > 0 ? { scopes: connection.grantedScopes } : {}),
|
|
599
|
+
subjectScope: connection.subjectId ? "subject" : "workspace",
|
|
600
|
+
},
|
|
601
|
+
destinationUrl: request.destinationUrl,
|
|
602
|
+
credentialTarget: "http_api",
|
|
603
|
+
forceRefresh: request.forceRefresh === true,
|
|
604
|
+
});
|
|
605
|
+
if (result.status === "auth_needed") return null;
|
|
606
|
+
const destination = new URL(request.destinationUrl);
|
|
607
|
+
return {
|
|
608
|
+
audience: { origin: destination.origin, pathPrefix: "/" },
|
|
609
|
+
placements:
|
|
610
|
+
result.placements ??
|
|
611
|
+
Object.entries(result.headers).map(([name, value]) => ({
|
|
612
|
+
carrier: "header" as const,
|
|
613
|
+
name,
|
|
614
|
+
value,
|
|
615
|
+
})),
|
|
616
|
+
};
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function configuredMcpServer(servers: McpServerConfig[], id: string): McpServerConfig {
|
|
622
|
+
const server = servers.find((candidate) => candidate.id === id);
|
|
623
|
+
if (!server) {
|
|
624
|
+
throw new HTTPException(422, {
|
|
625
|
+
message: `Plugin references MCP server ${id}, which is not configured by this deployment`,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
if (server.headers && Object.keys(server.headers).length > 0) {
|
|
629
|
+
throw new HTTPException(422, {
|
|
630
|
+
message: `Plugin MCP reference ${id} cannot copy deployment header credentials`,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
if (server.connectionRef?.subjectScope === "subject") {
|
|
634
|
+
throw new HTTPException(422, {
|
|
635
|
+
message: `Plugin MCP reference ${id} cannot auto-activate a Personal Connection`,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
return server;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function safeMcpFacts(server: McpServerConfig): Record<string, unknown> {
|
|
642
|
+
return {
|
|
643
|
+
serverId: server.id,
|
|
644
|
+
name: server.name ?? null,
|
|
645
|
+
url: server.url,
|
|
646
|
+
allowedTools: server.allowedTools ?? [],
|
|
647
|
+
timeoutMs: server.timeoutMs ?? null,
|
|
648
|
+
cacheToolsList: server.cacheToolsList,
|
|
649
|
+
requireApproval: server.requireApproval ?? false,
|
|
650
|
+
connectionRef: server.connectionRef ?? null,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function integrationDigest(resolved: ResolvedApiIntegrationPreview): string {
|
|
655
|
+
return sha256(
|
|
656
|
+
stableJson({
|
|
657
|
+
capabilityId: resolved.preview.capabilityId,
|
|
658
|
+
revisionId: resolved.preview.revisionId,
|
|
659
|
+
contentSha256: resolved.preview.contentSha256,
|
|
660
|
+
requiredScopes: resolved.requiredScopes,
|
|
661
|
+
}),
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function pluginBom(value: unknown): Array<{
|
|
666
|
+
key: string;
|
|
667
|
+
kind: "skill" | "integration" | "mcp";
|
|
668
|
+
capabilityId: string;
|
|
669
|
+
digest: string;
|
|
670
|
+
}> {
|
|
671
|
+
const record =
|
|
672
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
673
|
+
? (value as Record<string, unknown>)
|
|
674
|
+
: {};
|
|
675
|
+
if (!Array.isArray(record.bom)) return [];
|
|
676
|
+
return record.bom.flatMap((entry) => {
|
|
677
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
|
|
678
|
+
const item = entry as Record<string, unknown>;
|
|
679
|
+
if (
|
|
680
|
+
typeof item.key !== "string" ||
|
|
681
|
+
(item.kind !== "skill" && item.kind !== "integration" && item.kind !== "mcp") ||
|
|
682
|
+
typeof item.capabilityId !== "string" ||
|
|
683
|
+
typeof item.digest !== "string"
|
|
684
|
+
) {
|
|
685
|
+
return [];
|
|
686
|
+
}
|
|
687
|
+
return [
|
|
688
|
+
{ key: item.key, kind: item.kind, capabilityId: item.capabilityId, digest: item.digest },
|
|
689
|
+
];
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function pluginDiff(
|
|
694
|
+
fromVersion: string | null,
|
|
695
|
+
toVersion: string,
|
|
696
|
+
previous: ReturnType<typeof pluginBom>,
|
|
697
|
+
next: ReturnType<typeof pluginBom>,
|
|
698
|
+
) {
|
|
699
|
+
const previousByKey = new Map(previous.map((component) => [component.key, component]));
|
|
700
|
+
const nextByKey = new Map(next.map((component) => [component.key, component]));
|
|
701
|
+
return {
|
|
702
|
+
fromVersion,
|
|
703
|
+
toVersion,
|
|
704
|
+
added: [...nextByKey.keys()].filter((key) => !previousByKey.has(key)).sort(),
|
|
705
|
+
removed: [...previousByKey.keys()].filter((key) => !nextByKey.has(key)).sort(),
|
|
706
|
+
changed: [...nextByKey.entries()]
|
|
707
|
+
.filter(([key, component]) => {
|
|
708
|
+
const before = previousByKey.get(key);
|
|
709
|
+
return Boolean(
|
|
710
|
+
before && (before.kind !== component.kind || before.digest !== component.digest),
|
|
711
|
+
);
|
|
712
|
+
})
|
|
713
|
+
.map(([key]) => key)
|
|
714
|
+
.sort(),
|
|
715
|
+
unchanged: [...nextByKey.entries()]
|
|
716
|
+
.filter(([key, component]) => {
|
|
717
|
+
const before = previousByKey.get(key);
|
|
718
|
+
return Boolean(
|
|
719
|
+
before && before.kind === component.kind && before.digest === component.digest,
|
|
720
|
+
);
|
|
721
|
+
})
|
|
722
|
+
.map(([key]) => key)
|
|
723
|
+
.sort(),
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function pluginMutationHttpError(error: unknown): HTTPException {
|
|
728
|
+
if (error instanceof PluginOperationIdempotencyError) {
|
|
729
|
+
return new HTTPException(409, { message: "Plugin idempotency key was already used" });
|
|
730
|
+
}
|
|
731
|
+
if (error instanceof PluginInstallationVersionConflictError) {
|
|
732
|
+
return new HTTPException(409, { message: "Plugin installation changed after preview" });
|
|
733
|
+
}
|
|
734
|
+
if (error instanceof PluginInstallationVersionRequiredError) {
|
|
735
|
+
return new HTTPException(400, {
|
|
736
|
+
message: "Updating a Plugin requires the previewed installation version",
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
if (error instanceof HTTPException) return error;
|
|
740
|
+
return new HTTPException(422, {
|
|
741
|
+
message: error instanceof Error ? error.message : "Plugin mutation failed",
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function pluginFailureCode(error: unknown): string {
|
|
746
|
+
if (error instanceof HTTPException) return `http_${error.status}`;
|
|
747
|
+
return "component_install_failed";
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function sha256(value: string): string {
|
|
751
|
+
return createHash("sha256").update(value).digest("hex");
|
|
752
|
+
}
|