@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.
- package/dist/app.d.ts +2 -2
- package/dist/app.js +1 -1
- package/dist/browser-controller-authority.d.ts +43 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-T4T2PGU4.js → chunk-JIKNR5YL.js} +26181 -14408
- package/dist/chunk-JIKNR5YL.js.map +1 -0
- package/dist/codemode.d.ts +23 -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/cors.d.ts +1 -0
- 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/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 +19 -0
- package/dist/integrations/slack-bot.d.ts +4 -0
- package/dist/integrations/slack-interactions.d.ts +8 -2
- package/dist/integrations/social-api.d.ts +2 -1
- 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 +14 -3
- package/dist/memory-slack-delivery.d.ts +9 -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/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-features.d.ts +3 -0
- package/dist/routes/memory-slack-publications.d.ts +6 -0
- package/dist/routes/plugins.d.ts +8 -0
- package/dist/routes/sessions.d.ts +17 -2
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/video-generation.d.ts +3 -0
- package/dist/sandbox/channel-a.d.ts +55 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -1
- package/dist/sandbox/viewer.d.ts +4 -2
- package/dist/temporal-schedule-cleanup.d.ts +26 -0
- package/package.json +19 -14
- package/src/app.ts +274 -37
- package/src/browser-controller-authority.ts +137 -0
- package/src/browser-state-authority.ts +236 -0
- package/src/codemode.ts +186 -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 +2 -0
- package/src/http/cors.ts +3 -0
- package/src/index.ts +119 -20
- package/src/integrations/api-integrations.ts +350 -0
- package/src/integrations/atlassian.ts +1621 -0
- package/src/integrations/github-skill-source.ts +142 -0
- package/src/integrations/google-drive.ts +1000 -64
- package/src/integrations/oauth-client.ts +123 -33
- package/src/integrations/provider-oauth.ts +777 -0
- package/src/integrations/slack-bot.ts +31 -2
- package/src/integrations/slack-interactions.ts +487 -27
- package/src/integrations/social-api.ts +11 -0
- package/src/mcp/documents.ts +32 -1
- 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 +923 -145
- package/src/memory-slack-delivery.ts +209 -0
- package/src/routes/api-integrations.ts +407 -0
- package/src/routes/browser-identities.ts +136 -0
- package/src/routes/browser-sessions.ts +2543 -0
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1247 -0
- package/src/routes/connections.ts +284 -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-features.ts +258 -0
- package/src/routes/memory-slack-publications.ts +216 -0
- package/src/routes/packs.ts +437 -7
- package/src/routes/plugins.ts +751 -0
- package/src/routes/rigs.ts +77 -20
- package/src/routes/scheduled-tasks.ts +67 -40
- package/src/routes/sessions.ts +421 -245
- package/src/routes/skills.ts +174 -0
- package/src/routes/video-generation.ts +132 -0
- package/src/routes/workspaces.ts +46 -24
- package/src/sandbox/channel-a.ts +723 -116
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +29 -9
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/dist/chunk-T4T2PGU4.js.map +0 -1
- package/dist/mcp/toolspace.d.ts +0 -71
- package/src/mcp/toolspace.ts +0 -1190
package/src/index.ts
CHANGED
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
} from "@opengeni/db";
|
|
22
22
|
import { createNatsEventBus, type ResponderConnection } from "@opengeni/events";
|
|
23
23
|
import { createObservability, logStartupDependencyRetry } from "@opengeni/observability";
|
|
24
|
+
import { createObjectStorage } from "@opengeni/storage";
|
|
25
|
+
import { isArtifactRuntimeConfigured } from "@opengeni/artifact-tool/runtime/development";
|
|
24
26
|
import { SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID } from "@opengeni/core";
|
|
25
27
|
import {
|
|
26
28
|
Connection,
|
|
@@ -35,6 +37,17 @@ import { observabilityEventLogger } from "./observability";
|
|
|
35
37
|
import { startAuthCalloutResponder } from "./sandbox/auth-callout";
|
|
36
38
|
import { startHelloIngestion, startMetricsIngestion } from "./sandbox/metrics-ingestion";
|
|
37
39
|
import { startSlackInteractionPump } from "./integrations/slack-interactions";
|
|
40
|
+
import { startMemorySlackPublicationPump } from "./memory-slack-delivery";
|
|
41
|
+
import { startTemporalScheduleCleanupPump } from "./temporal-schedule-cleanup";
|
|
42
|
+
import {
|
|
43
|
+
EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES,
|
|
44
|
+
EditableArtifactWebSocketTransport,
|
|
45
|
+
type EditableArtifactWebSocketConnection,
|
|
46
|
+
} from "./editable-artifact-websocket";
|
|
47
|
+
import {
|
|
48
|
+
createStandaloneEditableArtifactApplication,
|
|
49
|
+
type StandaloneEditableArtifactApplication,
|
|
50
|
+
} from "./editable-artifact-production";
|
|
38
51
|
|
|
39
52
|
/**
|
|
40
53
|
* A REJECT_DUPLICATE start collides on the deterministic workflowId when the
|
|
@@ -156,6 +169,14 @@ export async function createTemporalWorkflowClient(
|
|
|
156
169
|
},
|
|
157
170
|
syncScheduledTask: async ({ task }) => {
|
|
158
171
|
const schedule = temporal.schedule.getHandle(task.temporalScheduleId);
|
|
172
|
+
if (task.schedule.type === "manual") {
|
|
173
|
+
try {
|
|
174
|
+
await schedule.delete();
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (!(error instanceof ScheduleNotFoundError)) throw error;
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
159
180
|
const options = temporalScheduleOptions(task, settings.temporalTaskQueue);
|
|
160
181
|
try {
|
|
161
182
|
await schedule.update(() => temporalScheduleUpdateOptions(options));
|
|
@@ -167,16 +188,21 @@ export async function createTemporalWorkflowClient(
|
|
|
167
188
|
}
|
|
168
189
|
},
|
|
169
190
|
deleteScheduledTaskSchedule: async ({ temporalScheduleId }) => {
|
|
170
|
-
|
|
171
|
-
.
|
|
172
|
-
|
|
173
|
-
|
|
191
|
+
try {
|
|
192
|
+
await temporal.withDeadline(Date.now() + 5_000, async () => {
|
|
193
|
+
await temporal.schedule.getHandle(temporalScheduleId).delete();
|
|
194
|
+
});
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (error instanceof ScheduleNotFoundError) return;
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
174
199
|
},
|
|
175
200
|
triggerScheduledTask: async ({
|
|
176
201
|
task,
|
|
177
202
|
agentRunUsageIdempotencyKey,
|
|
178
203
|
triggerWorkflowId,
|
|
179
204
|
initiator,
|
|
205
|
+
triggerType = "manual",
|
|
180
206
|
}) => {
|
|
181
207
|
// Deterministic workflowId (derived from the trigger token by the
|
|
182
208
|
// caller) + REJECT_DUPLICATE makes a retried manual trigger idempotent:
|
|
@@ -193,7 +219,7 @@ export async function createTemporalWorkflowClient(
|
|
|
193
219
|
accountId: task.accountId,
|
|
194
220
|
workspaceId: task.workspaceId,
|
|
195
221
|
taskId: task.id,
|
|
196
|
-
triggerType
|
|
222
|
+
triggerType,
|
|
197
223
|
agentRunUsageIdempotencyKey,
|
|
198
224
|
initiator,
|
|
199
225
|
},
|
|
@@ -213,18 +239,25 @@ export async function createTemporalWorkflowClient(
|
|
|
213
239
|
if (!targetId) {
|
|
214
240
|
throw new Error("rig verification requires changeId or versionId");
|
|
215
241
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
242
|
+
try {
|
|
243
|
+
await temporal.workflow.start("rigVerificationWorkflow", {
|
|
244
|
+
taskQueue: settings.temporalTaskQueue,
|
|
245
|
+
workflowId: workflowId ?? `rig-verification-${targetId}-${crypto.randomUUID()}`,
|
|
246
|
+
workflowIdReusePolicy: "REJECT_DUPLICATE",
|
|
247
|
+
args: [
|
|
248
|
+
{
|
|
249
|
+
workspaceId,
|
|
250
|
+
...(changeId ? { changeId } : {}),
|
|
251
|
+
...(versionId ? { versionId } : {}),
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
});
|
|
255
|
+
} catch (error) {
|
|
256
|
+
// A lost start acknowledgement is indistinguishable from a retry. The
|
|
257
|
+
// caller-owned workflow id makes both cases the same successful start.
|
|
258
|
+
if (isWorkflowAlreadyStarted(error)) return;
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
228
261
|
},
|
|
229
262
|
check: async () => {
|
|
230
263
|
await connection.workflowService.getSystemInfo({});
|
|
@@ -312,26 +345,77 @@ export async function startApi() {
|
|
|
312
345
|
await dbClient.close();
|
|
313
346
|
throw new Error("OpenGeni API startup dependencies were not initialized");
|
|
314
347
|
}
|
|
348
|
+
const objectStorage = createObjectStorage(settings);
|
|
349
|
+
let editableArtifactComposition: StandaloneEditableArtifactApplication | undefined;
|
|
350
|
+
if (objectStorage && isArtifactRuntimeConfigured()) {
|
|
351
|
+
try {
|
|
352
|
+
editableArtifactComposition = await createStandaloneEditableArtifactApplication({
|
|
353
|
+
db: dbClient.db,
|
|
354
|
+
bus,
|
|
355
|
+
objectStorage,
|
|
356
|
+
});
|
|
357
|
+
} catch (error) {
|
|
358
|
+
await Promise.allSettled([bus.close(), workflowClient.close(), dbClient.close()]);
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
315
362
|
const { app, routeDeps } = createAppComposition({
|
|
316
363
|
settings,
|
|
317
364
|
db: dbClient.db,
|
|
318
365
|
bus,
|
|
319
366
|
workflowClient: workflowClient.client,
|
|
320
367
|
documentIndexer: workflowClient.documentIndexer,
|
|
368
|
+
objectStorage,
|
|
369
|
+
...(editableArtifactComposition
|
|
370
|
+
? {
|
|
371
|
+
editableArtifacts: editableArtifactComposition.application,
|
|
372
|
+
editableArtifactExports: editableArtifactComposition.durableExports,
|
|
373
|
+
editableArtifactAgent: editableArtifactComposition.agent,
|
|
374
|
+
editableArtifactOfficeImports: editableArtifactComposition.officeImports,
|
|
375
|
+
}
|
|
376
|
+
: {}),
|
|
321
377
|
observability,
|
|
322
378
|
readinessChecks: {
|
|
323
379
|
db: runtimeDatabaseReadyCheck(dbClient.db, databasePosture),
|
|
324
380
|
},
|
|
325
381
|
});
|
|
326
|
-
|
|
382
|
+
if (!routeDeps.editableArtifacts) {
|
|
383
|
+
observability.warn("editable artifact engine is not composed", {
|
|
384
|
+
subsystem: "editable_artifacts",
|
|
385
|
+
behavior: "http_and_websocket_fail_closed",
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
const artifactWebSockets = new EditableArtifactWebSocketTransport(routeDeps.editableArtifacts);
|
|
389
|
+
const server = Bun.serve<EditableArtifactWebSocketConnection>({
|
|
327
390
|
hostname: settings.apiHost,
|
|
328
391
|
port: settings.apiPort,
|
|
329
392
|
idleTimeout: 255,
|
|
330
|
-
fetch:
|
|
393
|
+
fetch: (request, bunServer) => {
|
|
394
|
+
if (artifactWebSockets.handles(request)) {
|
|
395
|
+
return artifactWebSockets.upgrade(request, bunServer);
|
|
396
|
+
}
|
|
397
|
+
return app.fetch(request);
|
|
398
|
+
},
|
|
399
|
+
websocket: {
|
|
400
|
+
maxPayloadLength: EDITABLE_ARTIFACT_LIVE_WEBSOCKET_MAX_MESSAGE_BYTES,
|
|
401
|
+
backpressureLimit: 16 * 1024 * 1024,
|
|
402
|
+
closeOnBackpressureLimit: true,
|
|
403
|
+
open: (socket) => artifactWebSockets.websocket.open(socket),
|
|
404
|
+
message: (socket, message) => artifactWebSockets.websocket.message(socket, message),
|
|
405
|
+
close: (socket) => artifactWebSockets.websocket.close(socket),
|
|
406
|
+
},
|
|
331
407
|
});
|
|
332
408
|
const stopSlackInteractionPump = settings.slackSigningSecret
|
|
333
409
|
? startSlackInteractionPump(routeDeps)
|
|
334
410
|
: undefined;
|
|
411
|
+
const stopMemorySlackPublicationPump = startMemorySlackPublicationPump(routeDeps);
|
|
412
|
+
const stopTemporalScheduleCleanupPump = startTemporalScheduleCleanupPump({
|
|
413
|
+
db: dbClient.db,
|
|
414
|
+
deleteSchedule: async (temporalScheduleId) => {
|
|
415
|
+
await workflowClient.client.deleteScheduledTaskSchedule({ temporalScheduleId });
|
|
416
|
+
},
|
|
417
|
+
observability,
|
|
418
|
+
});
|
|
335
419
|
// M10 — start the metrics-ingestion consumer (agent heartbeats → DB last-sample
|
|
336
420
|
// + downsampled series), gated on the selfhosted flag. A no-op when disabled.
|
|
337
421
|
let stopMetricsIngestion: (() => void) | undefined;
|
|
@@ -391,9 +475,12 @@ export async function startApi() {
|
|
|
391
475
|
close: async () => {
|
|
392
476
|
server.stop(true);
|
|
393
477
|
stopSlackInteractionPump?.();
|
|
478
|
+
await stopMemorySlackPublicationPump();
|
|
394
479
|
stopMetricsIngestion?.();
|
|
395
480
|
stopHelloIngestion?.();
|
|
481
|
+
await stopTemporalScheduleCleanupPump();
|
|
396
482
|
await Promise.allSettled([
|
|
483
|
+
Promise.resolve(editableArtifactComposition?.close()),
|
|
397
484
|
authCalloutResponder?.close(),
|
|
398
485
|
bus.close(),
|
|
399
486
|
workflowClient.close(),
|
|
@@ -422,6 +509,9 @@ export function shouldCreateScheduleAfterUpdateError(error: unknown): boolean {
|
|
|
422
509
|
}
|
|
423
510
|
|
|
424
511
|
export function temporalScheduleSpec(schedule: ScheduledTaskScheduleSpec): ScheduleSpec {
|
|
512
|
+
if (schedule.type === "manual") {
|
|
513
|
+
throw new Error("manual scheduled tasks do not have a Temporal Schedule spec");
|
|
514
|
+
}
|
|
425
515
|
if (schedule.type === "interval") {
|
|
426
516
|
return {
|
|
427
517
|
intervals: [temporalIntervalSpec(schedule)],
|
|
@@ -508,7 +598,7 @@ function temporalScheduleOptions(task: ScheduledTask, taskQueue: string): Schedu
|
|
|
508
598
|
pauseOnFailure: false,
|
|
509
599
|
},
|
|
510
600
|
state: {
|
|
511
|
-
paused: task
|
|
601
|
+
paused: scheduledTaskEffectivelyPaused(task),
|
|
512
602
|
...(task.schedule.type === "once" ? { remainingActions: 1 } : {}),
|
|
513
603
|
},
|
|
514
604
|
memo: {
|
|
@@ -520,6 +610,15 @@ function temporalScheduleOptions(task: ScheduledTask, taskQueue: string): Schedu
|
|
|
520
610
|
};
|
|
521
611
|
}
|
|
522
612
|
|
|
613
|
+
function scheduledTaskEffectivelyPaused(task: ScheduledTask): boolean {
|
|
614
|
+
if (task.status === "paused") return true;
|
|
615
|
+
if (task.action.kind !== "knowledge_source_sync") return false;
|
|
616
|
+
const control = task.metadata.knowledgeSourceSync;
|
|
617
|
+
if (!control || typeof control !== "object") return false;
|
|
618
|
+
const record = control as Record<string, unknown>;
|
|
619
|
+
return record.sourceEnabled === false || record.connectionPaused === true;
|
|
620
|
+
}
|
|
621
|
+
|
|
523
622
|
function temporalScheduleUpdateOptions(options: ScheduleOptions): ScheduleUpdateOptions {
|
|
524
623
|
return {
|
|
525
624
|
spec: options.spec,
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
compileGraphqlRevision,
|
|
5
|
+
compileOpenApiRevision,
|
|
6
|
+
discoverOpenApiAuth,
|
|
7
|
+
fetchGraphqlIntrospection,
|
|
8
|
+
fetchIntegrationSourceDocument,
|
|
9
|
+
filterOpenApiDocumentForPreset,
|
|
10
|
+
googleDiscoveryToOpenApi,
|
|
11
|
+
parseOpenApiDocument,
|
|
12
|
+
providerPresetById,
|
|
13
|
+
type IntegrationCredentialResolver,
|
|
14
|
+
type IntegrationInvocationAuthority,
|
|
15
|
+
type IntegrationTransport,
|
|
16
|
+
type OpenApiAuthDiscovery,
|
|
17
|
+
} from "@opengeni/capabilities";
|
|
18
|
+
import {
|
|
19
|
+
ApiIntegrationPreview,
|
|
20
|
+
type ApiIntegrationAuthPreview,
|
|
21
|
+
type ApiIntegrationSource,
|
|
22
|
+
} from "@opengeni/contracts";
|
|
23
|
+
import type { StoredApiIntegrationRevision } from "@opengeni/db";
|
|
24
|
+
|
|
25
|
+
const MAX_PROVIDER_PRESET_SOURCE_BYTES = 64 * 1024 * 1024;
|
|
26
|
+
|
|
27
|
+
export type ApiIntegrationConnectionDescriptor = {
|
|
28
|
+
id: string;
|
|
29
|
+
kind: "oauth2" | "api_key" | "app_install" | "delegated";
|
|
30
|
+
providerDomain: string;
|
|
31
|
+
scopes: string[];
|
|
32
|
+
ownership: "workspace" | "personal";
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type ResolvedApiIntegrationPreview = {
|
|
36
|
+
preview: ApiIntegrationPreview;
|
|
37
|
+
revision: StoredApiIntegrationRevision;
|
|
38
|
+
provider: string | null;
|
|
39
|
+
requiredScopes: string[];
|
|
40
|
+
authScheme: Record<string, unknown>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export async function resolveApiIntegrationPreview(input: {
|
|
44
|
+
source: ApiIntegrationSource;
|
|
45
|
+
connection: ApiIntegrationConnectionDescriptor | null;
|
|
46
|
+
transport: IntegrationTransport;
|
|
47
|
+
credentialResolver?: IntegrationCredentialResolver;
|
|
48
|
+
authority: IntegrationInvocationAuthority;
|
|
49
|
+
}): Promise<ResolvedApiIntegrationPreview> {
|
|
50
|
+
if (input.source.kind === "preset") {
|
|
51
|
+
return await resolvePreset(input);
|
|
52
|
+
}
|
|
53
|
+
if (input.source.kind === "graphql") {
|
|
54
|
+
return await resolveGraphql(input, input.source.endpoint, input.source.name);
|
|
55
|
+
}
|
|
56
|
+
if (input.source.kind === "openapi") {
|
|
57
|
+
return await resolveOpenApi(input, input.source.url, input.source.baseUrl);
|
|
58
|
+
}
|
|
59
|
+
let openApiFailure: unknown;
|
|
60
|
+
try {
|
|
61
|
+
return await resolveOpenApi(input, input.source.url, input.source.baseUrl);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
openApiFailure = error;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return await resolveGraphql(input, input.source.url);
|
|
67
|
+
} catch (graphqlFailure) {
|
|
68
|
+
const detectionFailure = new Error(
|
|
69
|
+
"The URL is neither a supported OpenAPI document nor a GraphQL endpoint",
|
|
70
|
+
{ cause: graphqlFailure },
|
|
71
|
+
);
|
|
72
|
+
Object.defineProperty(detectionFailure, "openApiFailure", {
|
|
73
|
+
value: openApiFailure,
|
|
74
|
+
enumerable: false,
|
|
75
|
+
});
|
|
76
|
+
throw detectionFailure;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function resolvePreset(
|
|
81
|
+
input: Parameters<typeof resolveApiIntegrationPreview>[0],
|
|
82
|
+
): Promise<ResolvedApiIntegrationPreview> {
|
|
83
|
+
if (input.source.kind !== "preset") throw new Error("Expected a provider preset source");
|
|
84
|
+
const preset = providerPresetById(input.source.presetId);
|
|
85
|
+
if (!preset) throw new Error(`Unknown Integration preset: ${input.source.presetId}`);
|
|
86
|
+
const bytes = await fetchIntegrationSourceDocument(
|
|
87
|
+
input.transport,
|
|
88
|
+
preset.sourceUrl,
|
|
89
|
+
MAX_PROVIDER_PRESET_SOURCE_BYTES,
|
|
90
|
+
);
|
|
91
|
+
let document: Record<string, unknown>;
|
|
92
|
+
if (preset.sourceFormat === "google-discovery") {
|
|
93
|
+
document = googleDiscoveryToOpenApi(JSON.parse(new TextDecoder().decode(bytes)));
|
|
94
|
+
} else {
|
|
95
|
+
document = filterOpenApiDocumentForPreset(parseOpenApiDocument(bytes), preset);
|
|
96
|
+
}
|
|
97
|
+
const identity = integrationIdentity("openapi", preset.id, preset.sourceUrl);
|
|
98
|
+
const revision = compileOpenApiRevision(document, {
|
|
99
|
+
integrationId: identity.integrationId,
|
|
100
|
+
sourceUrl: preset.sourceUrl,
|
|
101
|
+
...(preset.baseUrl ? { baseUrl: preset.baseUrl } : {}),
|
|
102
|
+
provider: preset.family,
|
|
103
|
+
});
|
|
104
|
+
const baseUrl = firstOpenApiServerUrl(revision);
|
|
105
|
+
const providerDomain = new URL(baseUrl).hostname.toLowerCase();
|
|
106
|
+
const auth: ApiIntegrationAuthPreview = {
|
|
107
|
+
kind: "oauth2",
|
|
108
|
+
providerDomain,
|
|
109
|
+
scopes: [...preset.oauth.scopes],
|
|
110
|
+
};
|
|
111
|
+
return resolvedPreview({
|
|
112
|
+
source: input.source,
|
|
113
|
+
presetId: preset.id,
|
|
114
|
+
identity,
|
|
115
|
+
revision,
|
|
116
|
+
provider: preset.family,
|
|
117
|
+
providerDomain,
|
|
118
|
+
baseUrl,
|
|
119
|
+
sourceUrl: preset.sourceUrl,
|
|
120
|
+
name: preset.name,
|
|
121
|
+
description: preset.summary,
|
|
122
|
+
auth,
|
|
123
|
+
connection: input.connection,
|
|
124
|
+
requiredScopes: [...preset.oauth.scopes],
|
|
125
|
+
authScheme: { kind: "oauth2", placement: preset.oauth.tokenPlacement },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function resolveOpenApi(
|
|
130
|
+
input: Parameters<typeof resolveApiIntegrationPreview>[0],
|
|
131
|
+
sourceUrl: string,
|
|
132
|
+
baseUrl?: string,
|
|
133
|
+
): Promise<ResolvedApiIntegrationPreview> {
|
|
134
|
+
const document = parseOpenApiDocument(
|
|
135
|
+
await fetchIntegrationSourceDocument(input.transport, sourceUrl),
|
|
136
|
+
);
|
|
137
|
+
const identity = integrationIdentity("openapi", new URL(sourceUrl).hostname, sourceUrl);
|
|
138
|
+
const revision = compileOpenApiRevision(document, {
|
|
139
|
+
integrationId: identity.integrationId,
|
|
140
|
+
sourceUrl,
|
|
141
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
142
|
+
});
|
|
143
|
+
const resolvedBaseUrl = firstOpenApiServerUrl(revision);
|
|
144
|
+
const providerDomain = new URL(resolvedBaseUrl).hostname.toLowerCase();
|
|
145
|
+
const discovered = discoverOpenApiAuth(document);
|
|
146
|
+
const auth = authPreview(discovered, providerDomain, input.connection);
|
|
147
|
+
return resolvedPreview({
|
|
148
|
+
source: input.source,
|
|
149
|
+
presetId: null,
|
|
150
|
+
identity,
|
|
151
|
+
revision,
|
|
152
|
+
provider: null,
|
|
153
|
+
providerDomain,
|
|
154
|
+
baseUrl: resolvedBaseUrl,
|
|
155
|
+
sourceUrl,
|
|
156
|
+
name: revision.title,
|
|
157
|
+
description: revision.description ?? null,
|
|
158
|
+
auth,
|
|
159
|
+
connection: input.connection,
|
|
160
|
+
requiredScopes: auth.kind === "oauth2" ? auth.scopes : [],
|
|
161
|
+
authScheme: discovered,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function resolveGraphql(
|
|
166
|
+
input: Parameters<typeof resolveApiIntegrationPreview>[0],
|
|
167
|
+
endpoint: string,
|
|
168
|
+
name?: string,
|
|
169
|
+
): Promise<ResolvedApiIntegrationPreview> {
|
|
170
|
+
const providerDomain = new URL(endpoint).hostname.toLowerCase();
|
|
171
|
+
const identity = integrationIdentity("graphql", providerDomain, endpoint);
|
|
172
|
+
const introspection = await fetchGraphqlIntrospection({
|
|
173
|
+
endpoint,
|
|
174
|
+
transport: input.transport,
|
|
175
|
+
authority: input.authority,
|
|
176
|
+
...(input.credentialResolver ? { credentialResolver: input.credentialResolver } : {}),
|
|
177
|
+
});
|
|
178
|
+
const revision = compileGraphqlRevision(introspection, {
|
|
179
|
+
integrationId: identity.integrationId,
|
|
180
|
+
endpoint,
|
|
181
|
+
sourceUrl: endpoint,
|
|
182
|
+
...(name ? { name } : {}),
|
|
183
|
+
});
|
|
184
|
+
const auth = input.connection
|
|
185
|
+
? connectionAuthPreview(input.connection, providerDomain)
|
|
186
|
+
: ({ kind: "none" } as const);
|
|
187
|
+
return resolvedPreview({
|
|
188
|
+
source: input.source,
|
|
189
|
+
presetId: null,
|
|
190
|
+
identity,
|
|
191
|
+
revision,
|
|
192
|
+
provider: null,
|
|
193
|
+
providerDomain,
|
|
194
|
+
baseUrl: endpoint,
|
|
195
|
+
sourceUrl: endpoint,
|
|
196
|
+
name: revision.title,
|
|
197
|
+
description: revision.description ?? null,
|
|
198
|
+
auth,
|
|
199
|
+
connection: input.connection,
|
|
200
|
+
requiredScopes: input.connection?.scopes ?? [],
|
|
201
|
+
authScheme: auth,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function resolvedPreview(input: {
|
|
206
|
+
source: ApiIntegrationSource;
|
|
207
|
+
presetId: string | null;
|
|
208
|
+
identity: ReturnType<typeof integrationIdentity>;
|
|
209
|
+
revision: StoredApiIntegrationRevision;
|
|
210
|
+
provider: string | null;
|
|
211
|
+
providerDomain: string;
|
|
212
|
+
baseUrl: string;
|
|
213
|
+
sourceUrl: string;
|
|
214
|
+
name: string;
|
|
215
|
+
description: string | null;
|
|
216
|
+
auth: ApiIntegrationAuthPreview;
|
|
217
|
+
connection: ApiIntegrationConnectionDescriptor | null;
|
|
218
|
+
requiredScopes: string[];
|
|
219
|
+
authScheme: Record<string, unknown>;
|
|
220
|
+
}): ResolvedApiIntegrationPreview {
|
|
221
|
+
const deprecated = input.revision.tools.filter((tool) => tool.deprecated).length;
|
|
222
|
+
const warnings = [
|
|
223
|
+
...(deprecated > 0
|
|
224
|
+
? [`${deprecated} deprecated operation${deprecated === 1 ? "" : "s"} will not be enabled.`]
|
|
225
|
+
: []),
|
|
226
|
+
...(input.revision.tools.length > 500
|
|
227
|
+
? ["This Integration has many tools; schemas stay behind lazy tool discovery."]
|
|
228
|
+
: []),
|
|
229
|
+
];
|
|
230
|
+
return {
|
|
231
|
+
preview: ApiIntegrationPreview.parse({
|
|
232
|
+
source: input.source,
|
|
233
|
+
presetId: input.presetId,
|
|
234
|
+
protocol: input.revision.protocol,
|
|
235
|
+
integrationId: input.identity.integrationId,
|
|
236
|
+
capabilityId: input.identity.capabilityId,
|
|
237
|
+
pluginKey: input.identity.pluginKey,
|
|
238
|
+
serverId: input.identity.serverId,
|
|
239
|
+
name: input.name,
|
|
240
|
+
description: input.description,
|
|
241
|
+
provider: input.provider,
|
|
242
|
+
providerDomain: input.providerDomain,
|
|
243
|
+
baseUrl: input.baseUrl,
|
|
244
|
+
sourceUrl: input.sourceUrl,
|
|
245
|
+
revisionId: input.revision.id,
|
|
246
|
+
contentSha256: input.revision.contentSha256,
|
|
247
|
+
auth: input.auth,
|
|
248
|
+
connectionId: input.connection?.id ?? null,
|
|
249
|
+
connectionOwnership: input.connection?.ownership ?? null,
|
|
250
|
+
tools: input.revision.tools.map((tool) => ({
|
|
251
|
+
id: tool.id,
|
|
252
|
+
operationKey: tool.operationKey,
|
|
253
|
+
name: tool.name,
|
|
254
|
+
description: tool.description,
|
|
255
|
+
safety: tool.safety,
|
|
256
|
+
approvalMode: tool.approvalMode,
|
|
257
|
+
deprecated: tool.deprecated,
|
|
258
|
+
})),
|
|
259
|
+
warnings,
|
|
260
|
+
}),
|
|
261
|
+
revision: input.revision,
|
|
262
|
+
provider: input.provider,
|
|
263
|
+
requiredScopes: [...new Set(input.requiredScopes)].sort(),
|
|
264
|
+
authScheme: input.authScheme,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function integrationIdentity(protocol: "openapi" | "graphql", label: string, locator: string) {
|
|
269
|
+
const hash = createHash("sha256").update(`${protocol}\0${locator}`).digest("hex").slice(0, 12);
|
|
270
|
+
const slug =
|
|
271
|
+
label
|
|
272
|
+
.toLowerCase()
|
|
273
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
274
|
+
.replace(/^-+|-+$/g, "")
|
|
275
|
+
.slice(0, 48) || "integration";
|
|
276
|
+
return {
|
|
277
|
+
integrationId: `${slug}-${hash}`,
|
|
278
|
+
pluginKey: `integration/${protocol}/${slug}-${hash}`,
|
|
279
|
+
capabilityId: `api:${protocol}:${slug}-${hash}`,
|
|
280
|
+
serverId: `api_${protocol}_${slug.replaceAll("-", "_").slice(0, 48)}_${hash}`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function firstOpenApiServerUrl(revision: {
|
|
285
|
+
bindings: Readonly<Record<string, { serverUrl: string }>>;
|
|
286
|
+
}): string {
|
|
287
|
+
const first = Object.values(revision.bindings)[0]?.serverUrl;
|
|
288
|
+
if (!first) throw new Error("OpenAPI revision has no executable server URL");
|
|
289
|
+
return first;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function authPreview(
|
|
293
|
+
discovered: OpenApiAuthDiscovery,
|
|
294
|
+
providerDomain: string,
|
|
295
|
+
connection: ApiIntegrationConnectionDescriptor | null,
|
|
296
|
+
): ApiIntegrationAuthPreview {
|
|
297
|
+
if (connection) {
|
|
298
|
+
assertConnectionMatchesAuth(connection, providerDomain, discovered.kind);
|
|
299
|
+
}
|
|
300
|
+
if (discovered.kind === "none") return discovered;
|
|
301
|
+
if (discovered.kind === "oauth2") {
|
|
302
|
+
return { kind: "oauth2", providerDomain, scopes: discovered.scopes };
|
|
303
|
+
}
|
|
304
|
+
if (discovered.kind === "api_key") {
|
|
305
|
+
return {
|
|
306
|
+
kind: "api_key",
|
|
307
|
+
providerDomain,
|
|
308
|
+
carrier: discovered.carrier,
|
|
309
|
+
name: discovered.name,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
return { kind: "http", providerDomain, scheme: discovered.scheme };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function connectionAuthPreview(
|
|
316
|
+
connection: ApiIntegrationConnectionDescriptor,
|
|
317
|
+
providerDomain: string,
|
|
318
|
+
): ApiIntegrationAuthPreview {
|
|
319
|
+
assertConnectionMatchesAuth(connection, providerDomain);
|
|
320
|
+
if (connection.kind === "oauth2") {
|
|
321
|
+
return { kind: "oauth2", providerDomain, scopes: [...connection.scopes] };
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
kind: "api_key",
|
|
325
|
+
providerDomain,
|
|
326
|
+
carrier: "header",
|
|
327
|
+
name: "Authorization",
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function assertConnectionMatchesAuth(
|
|
332
|
+
connection: ApiIntegrationConnectionDescriptor,
|
|
333
|
+
providerDomain: string,
|
|
334
|
+
discoveredKind?: OpenApiAuthDiscovery["kind"],
|
|
335
|
+
): void {
|
|
336
|
+
if (connection.providerDomain.toLowerCase() !== providerDomain) {
|
|
337
|
+
throw new Error("Selected Connection does not match the Integration provider");
|
|
338
|
+
}
|
|
339
|
+
if (discoveredKind === "oauth2" && connection.kind !== "oauth2") {
|
|
340
|
+
throw new Error("This Integration requires an OAuth Connection");
|
|
341
|
+
}
|
|
342
|
+
if (
|
|
343
|
+
discoveredKind !== undefined &&
|
|
344
|
+
discoveredKind !== "none" &&
|
|
345
|
+
discoveredKind !== "oauth2" &&
|
|
346
|
+
connection.kind === "oauth2"
|
|
347
|
+
) {
|
|
348
|
+
throw new Error("This Integration requires a credential Connection, not OAuth");
|
|
349
|
+
}
|
|
350
|
+
}
|