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