@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
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
- await temporal.schedule
171
- .getHandle(temporalScheduleId)
172
- .delete()
173
- .catch(() => undefined);
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: "manual",
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
- await temporal.workflow.start("rigVerificationWorkflow", {
217
- taskQueue: settings.temporalTaskQueue,
218
- workflowId: workflowId ?? `rig-verification-${targetId}-${crypto.randomUUID()}`,
219
- workflowIdReusePolicy: "ALLOW_DUPLICATE",
220
- args: [
221
- {
222
- workspaceId,
223
- ...(changeId ? { changeId } : {}),
224
- ...(versionId ? { versionId } : {}),
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
- const server = Bun.serve({
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: app.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.status === "paused",
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
+ }