@opengeni/api-router 0.26.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.
Files changed (87) hide show
  1. package/dist/app.d.ts +1 -0
  2. package/dist/app.js +5 -3
  3. package/dist/auth/canonical-human-session-admission.d.ts +27 -0
  4. package/dist/browser-auth-broker.d.ts +20 -0
  5. package/dist/browser-controller-authority.d.ts +19 -1
  6. package/dist/browser-network-route.d.ts +8 -0
  7. package/dist/{chunk-JIKNR5YL.js → chunk-2PQMSRCB.js} +15518 -6863
  8. package/dist/chunk-2PQMSRCB.js.map +1 -0
  9. package/dist/controller-data-plane.d.ts +12 -0
  10. package/dist/http/auth.d.ts +1 -1
  11. package/dist/http/sse.d.ts +18 -1
  12. package/dist/index.js +1 -1
  13. package/dist/integrations/api-integrations.d.ts +2 -2
  14. package/dist/integrations/fiken.d.ts +233 -0
  15. package/dist/integrations/google-drive.d.ts +6 -6
  16. package/dist/integrations/provider-oauth.d.ts +0 -2
  17. package/dist/integrations/slack-bot.d.ts +171 -1
  18. package/dist/integrations/slack-interactions.d.ts +17 -5
  19. package/dist/interaction-holder-heartbeat.d.ts +17 -0
  20. package/dist/interaction-metrics.d.ts +11 -0
  21. package/dist/mcp/server.d.ts +3 -2
  22. package/dist/model-catalog.d.ts +1 -0
  23. package/dist/routes/canonical-human-identities.d.ts +3 -0
  24. package/dist/routes/channels.d.ts +3 -0
  25. package/dist/routes/integration-facets.d.ts +3 -0
  26. package/dist/routes/interaction-resources.d.ts +5 -0
  27. package/dist/routes/organization-memberships.d.ts +3 -0
  28. package/dist/routes/sessions.d.ts +2 -1
  29. package/dist/routes/slack-task-policy.d.ts +3 -0
  30. package/dist/routes/supergrok.d.ts +3 -0
  31. package/dist/sandbox/channel-a.d.ts +8 -1
  32. package/dist/sandbox/machines.d.ts +2 -2
  33. package/dist/sandbox/viewer.d.ts +3 -1
  34. package/dist/slack-reaction-files.d.ts +27 -0
  35. package/dist/transcription/providers/xai-subscription.d.ts +8 -0
  36. package/dist/xai-realtime.d.ts +26 -0
  37. package/dist/xai-subscription-auth.d.ts +23 -0
  38. package/package.json +17 -16
  39. package/src/app.ts +352 -49
  40. package/src/auth/canonical-human-session-admission.ts +116 -0
  41. package/src/auth/managed-auth.ts +101 -0
  42. package/src/browser-auth-broker.ts +155 -0
  43. package/src/browser-controller-authority.ts +50 -1
  44. package/src/browser-network-route.ts +34 -0
  45. package/src/controller-data-plane.ts +35 -0
  46. package/src/http/auth.ts +27 -9
  47. package/src/http/sse.ts +229 -5
  48. package/src/integrations/api-integrations.ts +57 -42
  49. package/src/integrations/fiken.ts +1230 -0
  50. package/src/integrations/google-drive.ts +31 -26
  51. package/src/integrations/provider-oauth.ts +70 -91
  52. package/src/integrations/slack-bot.ts +638 -9
  53. package/src/integrations/slack-interactions.ts +1583 -73
  54. package/src/interaction-holder-heartbeat.ts +95 -0
  55. package/src/interaction-metrics.ts +159 -0
  56. package/src/mcp/documents.ts +115 -6
  57. package/src/mcp/server.ts +865 -242
  58. package/src/model-catalog.ts +19 -6
  59. package/src/routes/api-integrations.ts +32 -29
  60. package/src/routes/browser-identities.ts +24 -0
  61. package/src/routes/browser-sessions.ts +2122 -183
  62. package/src/routes/canonical-human-identities.ts +156 -0
  63. package/src/routes/channels.ts +90 -0
  64. package/src/routes/computer-sessions.ts +441 -86
  65. package/src/routes/connections.ts +141 -4
  66. package/src/routes/{integration-features.ts → integration-facets.ts} +63 -63
  67. package/src/routes/interaction-resources.ts +595 -0
  68. package/src/routes/organization-memberships.ts +53 -0
  69. package/src/routes/plugins.ts +2 -1
  70. package/src/routes/sessions.ts +419 -43
  71. package/src/routes/skills.ts +92 -10
  72. package/src/routes/slack-task-policy.ts +115 -0
  73. package/src/routes/supergrok.ts +717 -0
  74. package/src/routes/transcription-recordings.ts +9 -2
  75. package/src/routes/transcriptions.ts +2 -1
  76. package/src/routes/video-generation.ts +34 -4
  77. package/src/routes/workspaces.ts +22 -4
  78. package/src/sandbox/channel-a.ts +29 -2
  79. package/src/sandbox/machines.ts +5 -5
  80. package/src/sandbox/viewer.ts +10 -3
  81. package/src/slack-reaction-files.ts +181 -0
  82. package/src/transcription/providers/xai-subscription.ts +110 -0
  83. package/src/transcription/service.ts +17 -2
  84. package/src/xai-realtime.ts +216 -0
  85. package/src/xai-subscription-auth.ts +132 -0
  86. package/dist/chunk-JIKNR5YL.js.map +0 -1
  87. package/dist/routes/integration-features.d.ts +0 -3
package/src/http/sse.ts CHANGED
@@ -1,5 +1,14 @@
1
- import type { SessionEvent, WorkspaceControlEvent } from "@opengeni/contracts";
2
- import { listSessionEvents, listWorkspaceControlEvents, type Database } from "@opengeni/db";
1
+ import {
2
+ WorkspaceInteractionRevisionEvent,
3
+ type SessionEvent,
4
+ type WorkspaceControlEvent,
5
+ } from "@opengeni/contracts";
6
+ import {
7
+ getWorkspaceInteractionRevisionState,
8
+ listSessionEvents,
9
+ listWorkspaceControlEvents,
10
+ type Database,
11
+ } from "@opengeni/db";
3
12
  import {
4
13
  formatSessionEventSse,
5
14
  formatWorkspaceControlEventSse,
@@ -13,9 +22,11 @@ const WORKSPACE_CONTROL_REPLAY_PAGE_SIZE = 100;
13
22
  export const SSE_QUEUED_FRAME_MAX_COUNT = 1;
14
23
  export const SSE_WRITE_STALL_TIMEOUT_MS = 30_000;
15
24
  export const SSE_HEARTBEAT_INTERVAL_MS = 15_000;
16
- const activeSseStreams: Record<"session" | "workspace_control", number> = {
25
+ type SseStreamKind = "session" | "workspace_control" | "workspace_interaction";
26
+ const activeSseStreams: Record<SseStreamKind, number> = {
17
27
  session: 0,
18
28
  workspace_control: 0,
29
+ workspace_interaction: 0,
19
30
  };
20
31
 
21
32
  export type SseDeliveryBoundObservation = {
@@ -586,8 +597,196 @@ export async function sseWorkspaceControlStream(
586
597
  });
587
598
  }
588
599
 
600
+ export type WorkspaceInteractionSseOptions = SseDeliveryOptions & {
601
+ pollIntervalMs?: number | undefined;
602
+ };
603
+
604
+ /**
605
+ * One HTTP connection for the two workspace-wide invalidation domains used by
606
+ * every visible OpenGeni surface. Keeping these as separate HTTP/1 streams
607
+ * consumes all six per-origin browser connections with only two windows and
608
+ * starves ordinary mutations/terminal grants. The durable cursors remain
609
+ * independent; this function only multiplexes their already-bounded SSE frames.
610
+ */
611
+ export async function sseWorkspaceLiveStream(
612
+ db: Database,
613
+ bus: EventBus,
614
+ accountId: string,
615
+ workspaceId: string,
616
+ controlAfter: number,
617
+ interactionAfter: number,
618
+ signal: AbortSignal,
619
+ options: WorkspaceInteractionSseOptions = {},
620
+ ): Promise<Response> {
621
+ const upstream = new AbortController();
622
+ const channel = createByteBoundedSseStream({
623
+ maxQueuedBytes: options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES,
624
+ ...(options.stallTimeoutMs === undefined ? {} : { stallTimeoutMs: options.stallTimeoutMs }),
625
+ onObservation: sseObservationReporter("workspace_interaction", options),
626
+ onStop: () => {
627
+ signal.removeEventListener("abort", abort);
628
+ upstream.abort();
629
+ },
630
+ });
631
+ const abort = () => {
632
+ upstream.abort();
633
+ channel.close();
634
+ };
635
+ if (signal.aborted) abort();
636
+ else signal.addEventListener("abort", abort, { once: true });
637
+
638
+ let writeTail = Promise.resolve(true);
639
+ const write = (frame: string): Promise<boolean> => {
640
+ const pending = writeTail.then(() => channel.write(frame));
641
+ writeTail = pending.catch(() => false);
642
+ return pending;
643
+ };
644
+ const pump = async (response: Response): Promise<void> => {
645
+ if (!response.body) throw new TypeError("workspace live upstream omitted its body");
646
+ const reader = response.body.getReader();
647
+ const decoder = new TextDecoder();
648
+ try {
649
+ for (;;) {
650
+ const next = await reader.read();
651
+ if (next.done) {
652
+ const tail = decoder.decode();
653
+ if (tail) await write(tail);
654
+ return;
655
+ }
656
+ const frame = decoder.decode(next.value, { stream: true });
657
+ if (frame && !(await write(frame))) return;
658
+ }
659
+ } finally {
660
+ await reader.cancel().catch(() => undefined);
661
+ reader.releaseLock();
662
+ }
663
+ };
664
+
665
+ void (async () => {
666
+ try {
667
+ const [control, interaction] = await Promise.all([
668
+ sseWorkspaceControlStream(db, bus, workspaceId, controlAfter, upstream.signal, options),
669
+ sseWorkspaceInteractionRevisionStream(
670
+ db,
671
+ accountId,
672
+ workspaceId,
673
+ interactionAfter,
674
+ upstream.signal,
675
+ options,
676
+ ),
677
+ ]);
678
+ const pumps = [pump(control), pump(interaction)];
679
+ let termination: unknown = new TypeError("workspace live upstream ended unexpectedly");
680
+ try {
681
+ await Promise.race(pumps);
682
+ } catch (error) {
683
+ termination = error;
684
+ } finally {
685
+ // Either durable domain ending makes the multiplexed connection stale.
686
+ // Stop its sibling immediately so the SDK reconnects both independent
687
+ // cursors instead of silently losing one domain forever.
688
+ upstream.abort();
689
+ await Promise.allSettled(pumps);
690
+ }
691
+ if (!signal.aborted && !channel.stopped()) channel.fail(termination);
692
+ } catch (error) {
693
+ if (!upstream.signal.aborted && !channel.stopped()) channel.fail(error);
694
+ }
695
+ })();
696
+
697
+ return new Response(channel.stream, {
698
+ headers: {
699
+ "Content-Type": "text/event-stream; charset=utf-8",
700
+ "Cache-Control": "no-cache, no-transform",
701
+ Connection: "keep-alive",
702
+ },
703
+ });
704
+ }
705
+
706
+ /**
707
+ * Latest-wins interaction invalidation stream. The durable truth is one
708
+ * monotonic workspace row, not an ever-growing event log. Each poll reads only
709
+ * that row; reconnect immediately projects the newest revision after `after`.
710
+ */
711
+ export async function sseWorkspaceInteractionRevisionStream(
712
+ db: Database,
713
+ accountId: string,
714
+ workspaceId: string,
715
+ after: number,
716
+ signal: AbortSignal,
717
+ options: WorkspaceInteractionSseOptions = {},
718
+ ): Promise<Response> {
719
+ const heartbeatIntervalMs = resolveHeartbeatInterval(options.heartbeatIntervalMs);
720
+ const pollIntervalMs = resolveInteractionPollInterval(options.pollIntervalMs);
721
+ let lastSent = after;
722
+ let lastWriteAt = Date.now();
723
+ let stopRequested = false;
724
+ let detachAbortListener = () => {};
725
+ let closeMetrics = () => {};
726
+ const channel = createByteBoundedSseStream({
727
+ maxQueuedBytes: options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES,
728
+ ...(options.stallTimeoutMs === undefined ? {} : { stallTimeoutMs: options.stallTimeoutMs }),
729
+ onObservation: sseObservationReporter("workspace_interaction", options),
730
+ onStop: () => {
731
+ stopRequested = true;
732
+ closeMetrics();
733
+ detachAbortListener();
734
+ },
735
+ });
736
+ closeMetrics = observeSseConnection("workspace_interaction", after, options.observability);
737
+
738
+ const write = async (frame: string): Promise<boolean> => {
739
+ const accepted = await channel.write(frame);
740
+ if (accepted) lastWriteAt = Date.now();
741
+ return accepted;
742
+ };
743
+
744
+ void (async () => {
745
+ try {
746
+ if (!(await write(": connected\n\n"))) return;
747
+ for (;;) {
748
+ if (stopRequested || signal.aborted || channel.stopped()) return;
749
+ const state = await getWorkspaceInteractionRevisionState(db, { accountId, workspaceId });
750
+ if (state.revision > lastSent) {
751
+ const event = WorkspaceInteractionRevisionEvent.parse({
752
+ workspaceId,
753
+ sequence: state.revision,
754
+ revision: state.revision,
755
+ type: "workspace.interaction.changed",
756
+ occurredAt: (state.updatedAt ?? new Date()).toISOString(),
757
+ });
758
+ if (!(await write(formatWorkspaceInteractionRevisionSse(event)))) return;
759
+ lastSent = event.revision;
760
+ } else if (Date.now() - lastWriteAt >= heartbeatIntervalMs) {
761
+ if (!(await write(": heartbeat\n\n"))) return;
762
+ }
763
+ await abortableDelay(pollIntervalMs, signal);
764
+ }
765
+ } catch (error) {
766
+ if (!signal.aborted && !channel.stopped()) {
767
+ channel.fail(retryableSseFailure("workspace interaction stream delivery failed", error));
768
+ }
769
+ }
770
+ })();
771
+
772
+ const abort = () => channel.close();
773
+ if (signal.aborted) abort();
774
+ else {
775
+ signal.addEventListener("abort", abort, { once: true });
776
+ detachAbortListener = () => signal.removeEventListener("abort", abort);
777
+ }
778
+
779
+ return new Response(channel.stream, {
780
+ headers: {
781
+ "Content-Type": "text/event-stream; charset=utf-8",
782
+ "Cache-Control": "no-cache, no-transform",
783
+ Connection: "keep-alive",
784
+ },
785
+ });
786
+ }
787
+
589
788
  function observeSseConnection(
590
- stream: "session" | "workspace_control",
789
+ stream: SseStreamKind,
591
790
  after: number,
592
791
  observability: Observability | undefined,
593
792
  ): () => void {
@@ -659,7 +858,7 @@ export type SessionSseDeliveryOptions = SseDeliveryOptions & {
659
858
  };
660
859
 
661
860
  function sseObservationReporter(
662
- stream: "session" | "workspace_control",
861
+ stream: SseStreamKind,
663
862
  options: SseDeliveryOptions,
664
863
  ): (observation: SseDeliveryBoundObservation) => void {
665
864
  return (observation) => {
@@ -692,3 +891,28 @@ function resolveHeartbeatInterval(value: number | undefined): number {
692
891
  }
693
892
  return interval;
694
893
  }
894
+
895
+ function resolveInteractionPollInterval(value: number | undefined): number {
896
+ const interval = value ?? 1_000;
897
+ if (!Number.isSafeInteger(interval) || interval < 100 || interval > 60_000) {
898
+ throw new RangeError("interaction revision poll interval must be between 100 and 60000ms");
899
+ }
900
+ return interval;
901
+ }
902
+
903
+ function formatWorkspaceInteractionRevisionSse(event: WorkspaceInteractionRevisionEvent): string {
904
+ return `id: ${event.sequence}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`;
905
+ }
906
+
907
+ async function abortableDelay(delayMs: number, signal: AbortSignal): Promise<void> {
908
+ if (signal.aborted) return;
909
+ await new Promise<void>((resolve) => {
910
+ const timer = setTimeout(done, delayMs);
911
+ function done() {
912
+ clearTimeout(timer);
913
+ signal.removeEventListener("abort", done);
914
+ resolve();
915
+ }
916
+ signal.addEventListener("abort", done, { once: true });
917
+ });
918
+ }
@@ -6,10 +6,10 @@ import {
6
6
  discoverOpenApiAuth,
7
7
  fetchGraphqlIntrospection,
8
8
  fetchIntegrationSourceDocument,
9
- filterOpenApiDocumentForPreset,
9
+ filterOpenApiDocumentForDefinition,
10
10
  googleDiscoveryToOpenApi,
11
+ integrationDefinitionById,
11
12
  parseOpenApiDocument,
12
- providerPresetById,
13
13
  type IntegrationCredentialResolver,
14
14
  type IntegrationInvocationAuthority,
15
15
  type IntegrationTransport,
@@ -18,11 +18,11 @@ import {
18
18
  import {
19
19
  ApiIntegrationPreview,
20
20
  type ApiIntegrationAuthPreview,
21
- type ApiIntegrationSource,
21
+ type IntegrationSource,
22
22
  } from "@opengeni/contracts";
23
23
  import type { StoredApiIntegrationRevision } from "@opengeni/db";
24
24
 
25
- const MAX_PROVIDER_PRESET_SOURCE_BYTES = 64 * 1024 * 1024;
25
+ const MAX_CURATED_DEFINITION_SOURCE_BYTES = 64 * 1024 * 1024;
26
26
 
27
27
  export type ApiIntegrationConnectionDescriptor = {
28
28
  id: string;
@@ -41,14 +41,14 @@ export type ResolvedApiIntegrationPreview = {
41
41
  };
42
42
 
43
43
  export async function resolveApiIntegrationPreview(input: {
44
- source: ApiIntegrationSource;
44
+ source: IntegrationSource;
45
45
  connection: ApiIntegrationConnectionDescriptor | null;
46
46
  transport: IntegrationTransport;
47
47
  credentialResolver?: IntegrationCredentialResolver;
48
48
  authority: IntegrationInvocationAuthority;
49
49
  }): Promise<ResolvedApiIntegrationPreview> {
50
- if (input.source.kind === "preset") {
51
- return await resolvePreset(input);
50
+ if (input.source.kind === "definition") {
51
+ return await resolveDefinition(input);
52
52
  }
53
53
  if (input.source.kind === "graphql") {
54
54
  return await resolveGraphql(input, input.source.endpoint, input.source.name);
@@ -77,52 +77,56 @@ export async function resolveApiIntegrationPreview(input: {
77
77
  }
78
78
  }
79
79
 
80
- async function resolvePreset(
80
+ async function resolveDefinition(
81
81
  input: Parameters<typeof resolveApiIntegrationPreview>[0],
82
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}`);
83
+ if (input.source.kind !== "definition") throw new Error("Expected a provider definition source");
84
+ const definition = integrationDefinitionById(input.source.definitionId);
85
+ if (!definition) throw new Error(`Unknown Integration definition: ${input.source.definitionId}`);
86
86
  const bytes = await fetchIntegrationSourceDocument(
87
87
  input.transport,
88
- preset.sourceUrl,
89
- MAX_PROVIDER_PRESET_SOURCE_BYTES,
88
+ definition.source.url,
89
+ MAX_CURATED_DEFINITION_SOURCE_BYTES,
90
90
  );
91
91
  let document: Record<string, unknown>;
92
- if (preset.sourceFormat === "google-discovery") {
92
+ if (definition.source.kind === "google_discovery") {
93
93
  document = googleDiscoveryToOpenApi(JSON.parse(new TextDecoder().decode(bytes)));
94
94
  } else {
95
- document = filterOpenApiDocumentForPreset(parseOpenApiDocument(bytes), preset);
95
+ document = filterOpenApiDocumentForDefinition(parseOpenApiDocument(bytes), definition);
96
96
  }
97
- const identity = integrationIdentity("openapi", preset.id, preset.sourceUrl);
97
+ const identity = integrationInstallationIdentity("openapi", definition.id, definition.source.url);
98
98
  const revision = compileOpenApiRevision(document, {
99
- integrationId: identity.integrationId,
100
- sourceUrl: preset.sourceUrl,
101
- ...(preset.baseUrl ? { baseUrl: preset.baseUrl } : {}),
102
- provider: preset.family,
99
+ definitionId: definition.id,
100
+ sourceUrl: definition.source.url,
101
+ baseUrl: definition.baseUrl,
102
+ provider: definition.provider.id,
103
103
  });
104
104
  const baseUrl = firstOpenApiServerUrl(revision);
105
- const providerDomain = new URL(baseUrl).hostname.toLowerCase();
105
+ const providerDomain = definition.provider.domain;
106
106
  const auth: ApiIntegrationAuthPreview = {
107
107
  kind: "oauth2",
108
108
  providerDomain,
109
- scopes: [...preset.oauth.scopes],
109
+ scopes: [...definition.authentication.scopes],
110
110
  };
111
111
  return resolvedPreview({
112
112
  source: input.source,
113
- presetId: preset.id,
113
+ definitionId: definition.id,
114
+ definitionProvenance: "curated",
114
115
  identity,
115
116
  revision,
116
- provider: preset.family,
117
+ provider: definition.provider.id,
117
118
  providerDomain,
118
119
  baseUrl,
119
- sourceUrl: preset.sourceUrl,
120
- name: preset.name,
121
- description: preset.summary,
120
+ sourceUrl: definition.source.url,
121
+ name: definition.name,
122
+ description: definition.summary,
122
123
  auth,
123
124
  connection: input.connection,
124
- requiredScopes: [...preset.oauth.scopes],
125
- authScheme: { kind: "oauth2", placement: preset.oauth.tokenPlacement },
125
+ requiredScopes: [...definition.authentication.scopes],
126
+ authScheme: {
127
+ kind: definition.authentication.kind,
128
+ placement: definition.authentication.tokenPlacement,
129
+ },
126
130
  });
127
131
  }
128
132
 
@@ -134,9 +138,13 @@ async function resolveOpenApi(
134
138
  const document = parseOpenApiDocument(
135
139
  await fetchIntegrationSourceDocument(input.transport, sourceUrl),
136
140
  );
137
- const identity = integrationIdentity("openapi", new URL(sourceUrl).hostname, sourceUrl);
141
+ const identity = integrationInstallationIdentity(
142
+ "openapi",
143
+ new URL(sourceUrl).hostname,
144
+ sourceUrl,
145
+ );
138
146
  const revision = compileOpenApiRevision(document, {
139
- integrationId: identity.integrationId,
147
+ definitionId: identity.sourceDerivedDefinitionId,
140
148
  sourceUrl,
141
149
  ...(baseUrl ? { baseUrl } : {}),
142
150
  });
@@ -146,7 +154,8 @@ async function resolveOpenApi(
146
154
  const auth = authPreview(discovered, providerDomain, input.connection);
147
155
  return resolvedPreview({
148
156
  source: input.source,
149
- presetId: null,
157
+ definitionId: identity.sourceDerivedDefinitionId,
158
+ definitionProvenance: "workspace",
150
159
  identity,
151
160
  revision,
152
161
  provider: null,
@@ -168,7 +177,7 @@ async function resolveGraphql(
168
177
  name?: string,
169
178
  ): Promise<ResolvedApiIntegrationPreview> {
170
179
  const providerDomain = new URL(endpoint).hostname.toLowerCase();
171
- const identity = integrationIdentity("graphql", providerDomain, endpoint);
180
+ const identity = integrationInstallationIdentity("graphql", providerDomain, endpoint);
172
181
  const introspection = await fetchGraphqlIntrospection({
173
182
  endpoint,
174
183
  transport: input.transport,
@@ -176,7 +185,7 @@ async function resolveGraphql(
176
185
  ...(input.credentialResolver ? { credentialResolver: input.credentialResolver } : {}),
177
186
  });
178
187
  const revision = compileGraphqlRevision(introspection, {
179
- integrationId: identity.integrationId,
188
+ definitionId: identity.sourceDerivedDefinitionId,
180
189
  endpoint,
181
190
  sourceUrl: endpoint,
182
191
  ...(name ? { name } : {}),
@@ -186,7 +195,8 @@ async function resolveGraphql(
186
195
  : ({ kind: "none" } as const);
187
196
  return resolvedPreview({
188
197
  source: input.source,
189
- presetId: null,
198
+ definitionId: identity.sourceDerivedDefinitionId,
199
+ definitionProvenance: "workspace",
190
200
  identity,
191
201
  revision,
192
202
  provider: null,
@@ -203,9 +213,10 @@ async function resolveGraphql(
203
213
  }
204
214
 
205
215
  function resolvedPreview(input: {
206
- source: ApiIntegrationSource;
207
- presetId: string | null;
208
- identity: ReturnType<typeof integrationIdentity>;
216
+ source: IntegrationSource;
217
+ definitionId: string;
218
+ definitionProvenance: "curated" | "workspace";
219
+ identity: ReturnType<typeof integrationInstallationIdentity>;
209
220
  revision: StoredApiIntegrationRevision;
210
221
  provider: string | null;
211
222
  providerDomain: string;
@@ -230,9 +241,9 @@ function resolvedPreview(input: {
230
241
  return {
231
242
  preview: ApiIntegrationPreview.parse({
232
243
  source: input.source,
233
- presetId: input.presetId,
244
+ definitionId: input.definitionId,
245
+ definitionProvenance: input.definitionProvenance,
234
246
  protocol: input.revision.protocol,
235
- integrationId: input.identity.integrationId,
236
247
  capabilityId: input.identity.capabilityId,
237
248
  pluginKey: input.identity.pluginKey,
238
249
  serverId: input.identity.serverId,
@@ -265,7 +276,11 @@ function resolvedPreview(input: {
265
276
  };
266
277
  }
267
278
 
268
- function integrationIdentity(protocol: "openapi" | "graphql", label: string, locator: string) {
279
+ function integrationInstallationIdentity(
280
+ protocol: "openapi" | "graphql",
281
+ label: string,
282
+ locator: string,
283
+ ) {
269
284
  const hash = createHash("sha256").update(`${protocol}\0${locator}`).digest("hex").slice(0, 12);
270
285
  const slug =
271
286
  label
@@ -274,7 +289,7 @@ function integrationIdentity(protocol: "openapi" | "graphql", label: string, loc
274
289
  .replace(/^-+|-+$/g, "")
275
290
  .slice(0, 48) || "integration";
276
291
  return {
277
- integrationId: `${slug}-${hash}`,
292
+ sourceDerivedDefinitionId: `${slug}-${hash}`,
278
293
  pluginKey: `integration/${protocol}/${slug}-${hash}`,
279
294
  capabilityId: `api:${protocol}:${slug}-${hash}`,
280
295
  serverId: `api_${protocol}_${slug.replaceAll("-", "_").slice(0, 48)}_${hash}`,