@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,717 @@
|
|
|
1
|
+
import {
|
|
2
|
+
configuredModels,
|
|
3
|
+
environmentsEncryptionKeyBytes,
|
|
4
|
+
withXaiSubscriptionCatalogProvider,
|
|
5
|
+
} from "@opengeni/config";
|
|
6
|
+
import {
|
|
7
|
+
WORKSPACE_XAI_PROVIDER_ACCOUNT_AUTHORITY_SNAPSHOT_V1,
|
|
8
|
+
type XaiProviderAccountAuthoritySnapshotV1,
|
|
9
|
+
} from "@opengeni/contracts";
|
|
10
|
+
import {
|
|
11
|
+
disconnectXaiSubscriptionCredentialAndRepick,
|
|
12
|
+
ensureXaiRotationSettings,
|
|
13
|
+
getXaiRotationSettings,
|
|
14
|
+
getXaiSubscriptionAccountAuthoritySnapshot,
|
|
15
|
+
listXaiSubscriptionAccountsMetadata,
|
|
16
|
+
materializeXaiCredentialForRun,
|
|
17
|
+
refreshXaiSubscriptionCredentialSerialized,
|
|
18
|
+
renameXaiSubscriptionAccount,
|
|
19
|
+
resolveXaiProviderAccountAuthoritySnapshotForAcceptance,
|
|
20
|
+
setActiveXaiCredential,
|
|
21
|
+
setInitialActiveXaiCredential,
|
|
22
|
+
updateXaiAllocatorEligibility,
|
|
23
|
+
updateXaiRotationSettings,
|
|
24
|
+
upsertXaiSubscriptionCredential,
|
|
25
|
+
wakeXaiCapacityWaiters,
|
|
26
|
+
type XaiSubscriptionAccountMetadata,
|
|
27
|
+
} from "@opengeni/db";
|
|
28
|
+
import { createSignedState, readSignedState } from "@opengeni/github";
|
|
29
|
+
import { getManagedSession, requireAccessGrant, type ApiRouteDeps } from "@opengeni/core";
|
|
30
|
+
import {
|
|
31
|
+
XAI_CLIENT_VERSION,
|
|
32
|
+
XaiSubscriptionError,
|
|
33
|
+
fetchXaiSubscriptionModels,
|
|
34
|
+
fetchXaiVerifiedIdentity,
|
|
35
|
+
pollXaiDeviceCode,
|
|
36
|
+
refreshXaiToken,
|
|
37
|
+
requestXaiDeviceCode,
|
|
38
|
+
xaiAccessTokenExpiry,
|
|
39
|
+
type XaiFetch,
|
|
40
|
+
type XaiProxyAuthContext,
|
|
41
|
+
} from "@opengeni/xai-subscription";
|
|
42
|
+
import type { Context, Hono } from "hono";
|
|
43
|
+
import { HTTPException } from "hono/http-exception";
|
|
44
|
+
import * as z from "zod/v4";
|
|
45
|
+
import { projectClientModel } from "../model-catalog";
|
|
46
|
+
|
|
47
|
+
type XaiAuthoritySnapshot = XaiProviderAccountAuthoritySnapshotV1;
|
|
48
|
+
type ManagedCookieHuman = { subjectId: string };
|
|
49
|
+
|
|
50
|
+
type SuperGrokConnectState = {
|
|
51
|
+
workspaceId: string;
|
|
52
|
+
scope: "workspace" | "user";
|
|
53
|
+
subjectId: string;
|
|
54
|
+
deviceCode: string;
|
|
55
|
+
intervalSeconds: number;
|
|
56
|
+
expiresAt: number;
|
|
57
|
+
iat: number;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const connectStartBody = z.object({
|
|
61
|
+
scope: z.enum(["workspace", "user"]).default("workspace"),
|
|
62
|
+
});
|
|
63
|
+
const connectPollBody = z.object({ state: z.string().min(1).max(16_384) });
|
|
64
|
+
const allocatorBody = z.object({
|
|
65
|
+
enabled: z.boolean(),
|
|
66
|
+
expectedVersion: z.number().int().positive(),
|
|
67
|
+
});
|
|
68
|
+
const settingsBody = z.object({ rotationEnabled: z.boolean() });
|
|
69
|
+
const renameBody = z.object({ label: z.string().trim().max(200).nullable() });
|
|
70
|
+
|
|
71
|
+
async function managedCookieHuman(
|
|
72
|
+
c: Context,
|
|
73
|
+
deps: ApiRouteDeps,
|
|
74
|
+
): Promise<ManagedCookieHuman | null> {
|
|
75
|
+
if (
|
|
76
|
+
deps.settings.productAccessMode !== "managed" ||
|
|
77
|
+
!deps.managedAuth ||
|
|
78
|
+
!c.req.header("cookie") ||
|
|
79
|
+
c.req.header("authorization")
|
|
80
|
+
) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const session = await getManagedSession(c, deps.managedAuth);
|
|
84
|
+
return session?.user?.id ? { subjectId: `user:${session.user.id}` } : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function requireSameOriginBrowserMutation(c: Context, deps: ApiRouteDeps): void {
|
|
88
|
+
const contentType = c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
89
|
+
if (contentType !== "application/json") {
|
|
90
|
+
throw new HTTPException(403, { message: "JSON browser request required" });
|
|
91
|
+
}
|
|
92
|
+
if (!deps.settings.publicBaseUrl) {
|
|
93
|
+
throw new HTTPException(503, {
|
|
94
|
+
message: "managed browser origin is not configured",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const expectedOrigin = new URL(deps.settings.publicBaseUrl).origin;
|
|
98
|
+
if (c.req.header("origin") !== expectedOrigin) {
|
|
99
|
+
throw new HTTPException(403, {
|
|
100
|
+
message: "same-origin browser request required",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (c.req.header("sec-fetch-site")?.toLowerCase() !== "same-origin") {
|
|
104
|
+
throw new HTTPException(403, {
|
|
105
|
+
message: "same-origin fetch metadata required",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function requireEnabled(deps: ApiRouteDeps): void {
|
|
111
|
+
if (!deps.settings.supergrokSubscriptionEnabled) {
|
|
112
|
+
throw new HTTPException(404, {
|
|
113
|
+
message: "SuperGrok subscriptions are not enabled",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function requirePrivateHuman(
|
|
119
|
+
c: Context,
|
|
120
|
+
deps: ApiRouteDeps,
|
|
121
|
+
workspaceId: string,
|
|
122
|
+
): Promise<{ accountId: string; subjectId: string }> {
|
|
123
|
+
if (c.req.header("authorization")) {
|
|
124
|
+
throw new HTTPException(403, {
|
|
125
|
+
message: "authorization bearer is not allowed for private SuperGrok accounts",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const human = await managedCookieHuman(c, deps);
|
|
129
|
+
if (!human) {
|
|
130
|
+
throw new HTTPException(401, {
|
|
131
|
+
message: "managed browser session required",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
135
|
+
if (grant.subjectId !== human.subjectId) {
|
|
136
|
+
throw new HTTPException(403, {
|
|
137
|
+
message: "managed browser identity mismatch",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return { accountId: grant.accountId, subjectId: grant.subjectId };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function requireScopeMutation(
|
|
144
|
+
c: Context,
|
|
145
|
+
deps: ApiRouteDeps,
|
|
146
|
+
workspaceId: string,
|
|
147
|
+
scope: "workspace" | "user",
|
|
148
|
+
): Promise<{ accountId: string; subjectId: string }> {
|
|
149
|
+
if (scope === "user") {
|
|
150
|
+
requireSameOriginBrowserMutation(c, deps);
|
|
151
|
+
return await requirePrivateHuman(c, deps, workspaceId);
|
|
152
|
+
}
|
|
153
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
154
|
+
return { accountId: grant.accountId, subjectId: grant.subjectId };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function resolveReadAuthority(
|
|
158
|
+
c: Context,
|
|
159
|
+
deps: ApiRouteDeps,
|
|
160
|
+
workspaceId: string,
|
|
161
|
+
): Promise<{
|
|
162
|
+
accountId: string;
|
|
163
|
+
subjectId: string;
|
|
164
|
+
snapshot: XaiAuthoritySnapshot;
|
|
165
|
+
}> {
|
|
166
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
167
|
+
const snapshot = await resolveXaiProviderAccountAuthoritySnapshotForAcceptance(deps.db, {
|
|
168
|
+
workspaceId,
|
|
169
|
+
subjectId: grant.subjectId,
|
|
170
|
+
});
|
|
171
|
+
if (snapshot.scope === "workspace") {
|
|
172
|
+
return { accountId: grant.accountId, subjectId: grant.subjectId, snapshot };
|
|
173
|
+
}
|
|
174
|
+
const human = await requirePrivateHuman(c, deps, workspaceId);
|
|
175
|
+
if (human.subjectId !== grant.subjectId) {
|
|
176
|
+
throw new HTTPException(403, {
|
|
177
|
+
message: "managed browser identity mismatch",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return { accountId: human.accountId, subjectId: human.subjectId, snapshot };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function xaiHttpError(error: unknown, fallback: string): HTTPException {
|
|
184
|
+
if (error instanceof HTTPException) return error;
|
|
185
|
+
if (error instanceof XaiSubscriptionError) {
|
|
186
|
+
const status =
|
|
187
|
+
error.kind === "not_enabled" ? 409 : error.kind === "relogin_required" ? 401 : 502;
|
|
188
|
+
return new HTTPException(status, { message: error.message });
|
|
189
|
+
}
|
|
190
|
+
return new HTTPException(502, { message: fallback });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function accountJson(row: XaiSubscriptionAccountMetadata, activeCredentialId: string | null) {
|
|
194
|
+
return {
|
|
195
|
+
id: row.id,
|
|
196
|
+
scope: row.scope,
|
|
197
|
+
subject: row.providerAccountId ?? row.accountEmail ?? row.id,
|
|
198
|
+
email: row.accountEmail,
|
|
199
|
+
label: row.label,
|
|
200
|
+
status: row.status === "disabled" ? "error" : row.status,
|
|
201
|
+
active: row.id === activeCredentialId,
|
|
202
|
+
expiresAt: row.expiresAt?.toISOString() ?? null,
|
|
203
|
+
lastRefreshAt: row.lastRefreshAt?.toISOString() ?? null,
|
|
204
|
+
lastError: row.lastError,
|
|
205
|
+
allocatorEnabled: row.allocatorEnabled,
|
|
206
|
+
allocatorVersion: row.allocatorVersion,
|
|
207
|
+
allocatorUpdatedAt: row.allocatorUpdatedAt?.toISOString() ?? null,
|
|
208
|
+
exhaustedUntil: row.exhaustedUntil?.toISOString() ?? null,
|
|
209
|
+
quota:
|
|
210
|
+
row.quotaUsedPercent === null && row.quotaResetAt === null && row.quotaCheckedAt === null
|
|
211
|
+
? null
|
|
212
|
+
: {
|
|
213
|
+
usedPercent: row.quotaUsedPercent,
|
|
214
|
+
periodStart: null,
|
|
215
|
+
periodEnd: row.quotaResetAt?.toISOString() ?? null,
|
|
216
|
+
subscriptionTier: row.planType,
|
|
217
|
+
checkedAt: row.quotaCheckedAt?.toISOString() ?? null,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function materializedAuthContext(
|
|
223
|
+
deps: ApiRouteDeps,
|
|
224
|
+
input: {
|
|
225
|
+
accountId: string;
|
|
226
|
+
workspaceId: string;
|
|
227
|
+
subjectId: string;
|
|
228
|
+
credentialId: string;
|
|
229
|
+
authoritySnapshot: XaiAuthoritySnapshot;
|
|
230
|
+
},
|
|
231
|
+
): Promise<{
|
|
232
|
+
context: XaiProxyAuthContext;
|
|
233
|
+
account: XaiSubscriptionAccountMetadata;
|
|
234
|
+
}> {
|
|
235
|
+
const encryptionKey = environmentsEncryptionKeyBytes(deps.settings);
|
|
236
|
+
if (!encryptionKey) {
|
|
237
|
+
throw new HTTPException(500, {
|
|
238
|
+
message: "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured",
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
let credential = await materializeXaiCredentialForRun(deps.db, {
|
|
242
|
+
...input,
|
|
243
|
+
encryptionKey,
|
|
244
|
+
});
|
|
245
|
+
const tokenSnapshot = () => {
|
|
246
|
+
if (!credential.secret.accessToken || !credential.providerAccountId) {
|
|
247
|
+
throw new XaiSubscriptionError(
|
|
248
|
+
"relogin_required",
|
|
249
|
+
"The SuperGrok connection is missing an access token or verified identity",
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
accessToken: credential.secret.accessToken,
|
|
254
|
+
userId: credential.providerAccountId,
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
return {
|
|
258
|
+
account: credential,
|
|
259
|
+
context: {
|
|
260
|
+
clientVersion: XAI_CLIENT_VERSION,
|
|
261
|
+
getToken: async () => tokenSnapshot(),
|
|
262
|
+
refresh: async () => {
|
|
263
|
+
const observedAccessToken = credential.secret.accessToken;
|
|
264
|
+
const observedRefreshToken = credential.secret.refreshToken;
|
|
265
|
+
if (!observedRefreshToken) {
|
|
266
|
+
throw new XaiSubscriptionError(
|
|
267
|
+
"relogin_required",
|
|
268
|
+
"The SuperGrok connection cannot be refreshed",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const result = await refreshXaiSubscriptionCredentialSerialized(deps.db, {
|
|
272
|
+
accountId: input.accountId,
|
|
273
|
+
workspaceId: input.workspaceId,
|
|
274
|
+
subjectId: input.subjectId,
|
|
275
|
+
credentialId: input.credentialId,
|
|
276
|
+
authoritySnapshot: input.authoritySnapshot,
|
|
277
|
+
encryptionKey,
|
|
278
|
+
observedAccessToken,
|
|
279
|
+
observedRefreshToken,
|
|
280
|
+
refresh: async (current) => {
|
|
281
|
+
const refreshToken = current.secret.refreshToken;
|
|
282
|
+
if (!refreshToken) {
|
|
283
|
+
throw new XaiSubscriptionError(
|
|
284
|
+
"relogin_required",
|
|
285
|
+
"The SuperGrok connection cannot be refreshed",
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
const tokens = await refreshXaiToken(refreshToken, {
|
|
289
|
+
fetch: (deps.xaiFetch ?? fetch) as XaiFetch,
|
|
290
|
+
});
|
|
291
|
+
return {
|
|
292
|
+
secret: {
|
|
293
|
+
version: 1,
|
|
294
|
+
accessToken: tokens.accessToken,
|
|
295
|
+
refreshToken: tokens.refreshToken,
|
|
296
|
+
},
|
|
297
|
+
expiresAt:
|
|
298
|
+
xaiAccessTokenExpiry(tokens.accessToken) ??
|
|
299
|
+
new Date(Date.now() + tokens.expiresInSeconds * 1_000),
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
credential = result.credential;
|
|
304
|
+
return tokenSnapshot();
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function authorityForAccountMutation(
|
|
311
|
+
c: Context,
|
|
312
|
+
deps: ApiRouteDeps,
|
|
313
|
+
workspaceId: string,
|
|
314
|
+
credentialId: string,
|
|
315
|
+
): Promise<{
|
|
316
|
+
accountId: string;
|
|
317
|
+
subjectId: string;
|
|
318
|
+
snapshot: XaiAuthoritySnapshot;
|
|
319
|
+
}> {
|
|
320
|
+
const readGrant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
321
|
+
const snapshot = await getXaiSubscriptionAccountAuthoritySnapshot(deps.db, {
|
|
322
|
+
workspaceId,
|
|
323
|
+
subjectId: readGrant.subjectId,
|
|
324
|
+
credentialId,
|
|
325
|
+
});
|
|
326
|
+
if (!snapshot) throw new HTTPException(404, { message: "SuperGrok account not found" });
|
|
327
|
+
const mutation = await requireScopeMutation(c, deps, workspaceId, snapshot.scope);
|
|
328
|
+
return { ...mutation, snapshot };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function registerSuperGrokRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
332
|
+
app.post("/v1/workspaces/:workspaceId/supergrok/connect/start", async (c) => {
|
|
333
|
+
requireEnabled(deps);
|
|
334
|
+
const workspaceId = c.req.param("workspaceId");
|
|
335
|
+
const parsed = connectStartBody.safeParse(await c.req.json().catch(() => ({})));
|
|
336
|
+
if (!parsed.success) throw new HTTPException(400, { message: "invalid SuperGrok scope" });
|
|
337
|
+
const authority = await requireScopeMutation(c, deps, workspaceId, parsed.data.scope);
|
|
338
|
+
try {
|
|
339
|
+
const start = await requestXaiDeviceCode({
|
|
340
|
+
fetch: (deps.xaiFetch ?? fetch) as XaiFetch,
|
|
341
|
+
});
|
|
342
|
+
const expiresAt = Math.floor(Date.now() / 1_000) + start.expiresInSeconds;
|
|
343
|
+
return c.json({
|
|
344
|
+
userCode: start.userCode,
|
|
345
|
+
verificationUri: start.verificationUri,
|
|
346
|
+
verificationUriComplete: start.verificationUriComplete,
|
|
347
|
+
intervalSeconds: start.intervalSeconds,
|
|
348
|
+
expiresInSeconds: start.expiresInSeconds,
|
|
349
|
+
scope: parsed.data.scope,
|
|
350
|
+
state: createSignedState(deps.githubStateSecret, {
|
|
351
|
+
workspaceId,
|
|
352
|
+
scope: parsed.data.scope,
|
|
353
|
+
subjectId: authority.subjectId,
|
|
354
|
+
deviceCode: start.deviceCode,
|
|
355
|
+
intervalSeconds: start.intervalSeconds,
|
|
356
|
+
expiresAt,
|
|
357
|
+
}),
|
|
358
|
+
});
|
|
359
|
+
} catch (error) {
|
|
360
|
+
throw xaiHttpError(error, "failed to start SuperGrok device login");
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
app.post("/v1/workspaces/:workspaceId/supergrok/connect/poll", async (c) => {
|
|
365
|
+
requireEnabled(deps);
|
|
366
|
+
const workspaceId = c.req.param("workspaceId");
|
|
367
|
+
const parsed = connectPollBody.safeParse(await c.req.json().catch(() => null));
|
|
368
|
+
if (!parsed.success) throw new HTTPException(400, { message: "SuperGrok state is required" });
|
|
369
|
+
const state = readSignedState(
|
|
370
|
+
parsed.data.state,
|
|
371
|
+
deps.githubStateSecret,
|
|
372
|
+
) as SuperGrokConnectState | null;
|
|
373
|
+
if (
|
|
374
|
+
!state ||
|
|
375
|
+
state.workspaceId !== workspaceId ||
|
|
376
|
+
!["workspace", "user"].includes(state.scope) ||
|
|
377
|
+
!state.subjectId ||
|
|
378
|
+
!state.deviceCode ||
|
|
379
|
+
!Number.isFinite(state.intervalSeconds) ||
|
|
380
|
+
!Number.isFinite(state.expiresAt)
|
|
381
|
+
) {
|
|
382
|
+
throw new HTTPException(400, {
|
|
383
|
+
message: "SuperGrok connect state is invalid or expired",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
const authority = await requireScopeMutation(c, deps, workspaceId, state.scope);
|
|
387
|
+
if (authority.subjectId !== state.subjectId) {
|
|
388
|
+
throw new HTTPException(403, {
|
|
389
|
+
message: "SuperGrok connect identity changed",
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
if (Math.floor(Date.now() / 1_000) >= state.expiresAt) {
|
|
393
|
+
return c.json({ status: "expired" as const });
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const poll = await pollXaiDeviceCode(
|
|
397
|
+
{
|
|
398
|
+
deviceCode: state.deviceCode,
|
|
399
|
+
intervalSeconds: state.intervalSeconds,
|
|
400
|
+
},
|
|
401
|
+
{ fetch: (deps.xaiFetch ?? fetch) as XaiFetch },
|
|
402
|
+
);
|
|
403
|
+
if (poll.status !== "authorized") return c.json(poll);
|
|
404
|
+
const identity = await fetchXaiVerifiedIdentity(poll.tokens.accessToken, {
|
|
405
|
+
fetch: (deps.xaiFetch ?? fetch) as XaiFetch,
|
|
406
|
+
});
|
|
407
|
+
const encryptionKey = environmentsEncryptionKeyBytes(deps.settings);
|
|
408
|
+
if (!encryptionKey) {
|
|
409
|
+
throw new HTTPException(500, {
|
|
410
|
+
message: "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured",
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
const upserted = await upsertXaiSubscriptionCredential(deps.db, {
|
|
414
|
+
accountId: authority.accountId,
|
|
415
|
+
workspaceId,
|
|
416
|
+
subjectId: authority.subjectId,
|
|
417
|
+
scope: state.scope,
|
|
418
|
+
encryptionKey,
|
|
419
|
+
secret: {
|
|
420
|
+
version: 1,
|
|
421
|
+
accessToken: poll.tokens.accessToken,
|
|
422
|
+
refreshToken: poll.tokens.refreshToken,
|
|
423
|
+
},
|
|
424
|
+
providerAccountId: identity.subject,
|
|
425
|
+
label: identity.name ?? identity.email ?? identity.subject,
|
|
426
|
+
accountEmail: identity.email,
|
|
427
|
+
expiresAt:
|
|
428
|
+
xaiAccessTokenExpiry(poll.tokens.accessToken) ??
|
|
429
|
+
new Date(Date.now() + poll.tokens.expiresInSeconds * 1_000),
|
|
430
|
+
});
|
|
431
|
+
const rotation = await ensureXaiRotationSettings(deps.db, {
|
|
432
|
+
accountId: authority.accountId,
|
|
433
|
+
workspaceId,
|
|
434
|
+
subjectId: authority.subjectId,
|
|
435
|
+
authoritySnapshot: upserted.authoritySnapshot,
|
|
436
|
+
});
|
|
437
|
+
let isActive = rotation.activeCredentialId === upserted.account.id;
|
|
438
|
+
if (!isActive && rotation.activeCredentialId === null) {
|
|
439
|
+
isActive = await setInitialActiveXaiCredential(deps.db, {
|
|
440
|
+
accountId: authority.accountId,
|
|
441
|
+
workspaceId,
|
|
442
|
+
subjectId: authority.subjectId,
|
|
443
|
+
authoritySnapshot: upserted.authoritySnapshot,
|
|
444
|
+
credentialId: upserted.account.id,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
await wakeXaiCapacityWaiters(deps.db, {
|
|
448
|
+
workspaceId,
|
|
449
|
+
subjectId: authority.subjectId,
|
|
450
|
+
authoritySnapshot: upserted.authoritySnapshot,
|
|
451
|
+
reason: "xai_credential_connected",
|
|
452
|
+
});
|
|
453
|
+
return c.json({
|
|
454
|
+
status: "connected" as const,
|
|
455
|
+
accountId: upserted.account.id,
|
|
456
|
+
scope: state.scope,
|
|
457
|
+
isActive,
|
|
458
|
+
email: identity.email,
|
|
459
|
+
});
|
|
460
|
+
} catch (error) {
|
|
461
|
+
throw xaiHttpError(error, "SuperGrok device login failed");
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
app.get("/v1/workspaces/:workspaceId/supergrok/accounts", async (c) => {
|
|
466
|
+
requireEnabled(deps);
|
|
467
|
+
const workspaceId = c.req.param("workspaceId");
|
|
468
|
+
const authority = await resolveReadAuthority(c, deps, workspaceId);
|
|
469
|
+
const [accounts, settings] = await Promise.all([
|
|
470
|
+
listXaiSubscriptionAccountsMetadata(deps.db, {
|
|
471
|
+
workspaceId,
|
|
472
|
+
subjectId: authority.subjectId,
|
|
473
|
+
}),
|
|
474
|
+
getXaiRotationSettings(deps.db, {
|
|
475
|
+
workspaceId,
|
|
476
|
+
subjectId: authority.subjectId,
|
|
477
|
+
authoritySnapshot: authority.snapshot,
|
|
478
|
+
}),
|
|
479
|
+
]);
|
|
480
|
+
const activeCredentialId = settings?.activeCredentialId ?? null;
|
|
481
|
+
return c.json({
|
|
482
|
+
accounts: accounts.map((account) => accountJson(account, activeCredentialId)),
|
|
483
|
+
activeAccountId: activeCredentialId,
|
|
484
|
+
settings: {
|
|
485
|
+
rotationEnabled: settings?.rotationEnabled ?? true,
|
|
486
|
+
rotationStrategy: "sharded" as const,
|
|
487
|
+
activeCredentialId,
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
app.get("/v1/workspaces/:workspaceId/supergrok/status", async (c) => {
|
|
493
|
+
requireEnabled(deps);
|
|
494
|
+
const workspaceId = c.req.param("workspaceId");
|
|
495
|
+
const authority = await resolveReadAuthority(c, deps, workspaceId);
|
|
496
|
+
const [accounts, settings] = await Promise.all([
|
|
497
|
+
listXaiSubscriptionAccountsMetadata(deps.db, {
|
|
498
|
+
workspaceId,
|
|
499
|
+
subjectId: authority.subjectId,
|
|
500
|
+
}),
|
|
501
|
+
getXaiRotationSettings(deps.db, {
|
|
502
|
+
workspaceId,
|
|
503
|
+
subjectId: authority.subjectId,
|
|
504
|
+
authoritySnapshot: authority.snapshot,
|
|
505
|
+
}),
|
|
506
|
+
]);
|
|
507
|
+
const active = accounts.find((account) => account.id === settings?.activeCredentialId) ?? null;
|
|
508
|
+
if (!active || !settings?.activeCredentialId) {
|
|
509
|
+
return c.json({
|
|
510
|
+
connected: accounts.length > 0,
|
|
511
|
+
valid: false,
|
|
512
|
+
accountCount: accounts.length,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
let valid = false;
|
|
516
|
+
try {
|
|
517
|
+
const auth = await materializedAuthContext(deps, {
|
|
518
|
+
accountId: authority.accountId,
|
|
519
|
+
workspaceId,
|
|
520
|
+
subjectId: authority.subjectId,
|
|
521
|
+
credentialId: active.id,
|
|
522
|
+
authoritySnapshot: authority.snapshot,
|
|
523
|
+
});
|
|
524
|
+
await fetchXaiSubscriptionModels({
|
|
525
|
+
context: auth.context,
|
|
526
|
+
...(deps.xaiFetch ? { fetch: deps.xaiFetch } : {}),
|
|
527
|
+
});
|
|
528
|
+
valid = true;
|
|
529
|
+
} catch {
|
|
530
|
+
valid = false;
|
|
531
|
+
}
|
|
532
|
+
const catalog = valid
|
|
533
|
+
? configuredModels(withXaiSubscriptionCatalogProvider(deps.settings))
|
|
534
|
+
.filter(
|
|
535
|
+
(model) =>
|
|
536
|
+
model.credentialSource.kind === "connected_subscription" &&
|
|
537
|
+
model.credentialSource.provider === "xai",
|
|
538
|
+
)
|
|
539
|
+
.map(projectClientModel)
|
|
540
|
+
: [];
|
|
541
|
+
return c.json({
|
|
542
|
+
connected: true,
|
|
543
|
+
valid,
|
|
544
|
+
accountCount: accounts.length,
|
|
545
|
+
models: catalog,
|
|
546
|
+
activeAccount: {
|
|
547
|
+
id: active.id,
|
|
548
|
+
label: active.label,
|
|
549
|
+
subject: active.providerAccountId,
|
|
550
|
+
scope: active.scope,
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
app.post("/v1/workspaces/:workspaceId/supergrok/accounts/:accountId/activate", async (c) => {
|
|
556
|
+
requireEnabled(deps);
|
|
557
|
+
const workspaceId = c.req.param("workspaceId");
|
|
558
|
+
const credentialId = c.req.param("accountId");
|
|
559
|
+
const authority = await authorityForAccountMutation(c, deps, workspaceId, credentialId);
|
|
560
|
+
const activated = await setActiveXaiCredential(deps.db, {
|
|
561
|
+
accountId: authority.accountId,
|
|
562
|
+
workspaceId,
|
|
563
|
+
subjectId: authority.subjectId,
|
|
564
|
+
authoritySnapshot: authority.snapshot,
|
|
565
|
+
credentialId,
|
|
566
|
+
});
|
|
567
|
+
if (!activated)
|
|
568
|
+
throw new HTTPException(409, {
|
|
569
|
+
message: "SuperGrok account requires relogin",
|
|
570
|
+
});
|
|
571
|
+
await wakeXaiCapacityWaiters(deps.db, {
|
|
572
|
+
workspaceId,
|
|
573
|
+
subjectId: authority.subjectId,
|
|
574
|
+
authoritySnapshot: authority.snapshot,
|
|
575
|
+
reason: "xai_active_credential_changed",
|
|
576
|
+
});
|
|
577
|
+
return c.json({ activated: true, accountId: credentialId });
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
app.patch("/v1/workspaces/:workspaceId/supergrok/settings", async (c) => {
|
|
581
|
+
requireEnabled(deps);
|
|
582
|
+
const workspaceId = c.req.param("workspaceId");
|
|
583
|
+
const parsed = settingsBody.safeParse(await c.req.json().catch(() => null));
|
|
584
|
+
if (!parsed.success) throw new HTTPException(400, { message: "rotationEnabled is required" });
|
|
585
|
+
const readAuthority = await resolveReadAuthority(c, deps, workspaceId);
|
|
586
|
+
const authority = await requireScopeMutation(
|
|
587
|
+
c,
|
|
588
|
+
deps,
|
|
589
|
+
workspaceId,
|
|
590
|
+
readAuthority.snapshot.scope,
|
|
591
|
+
);
|
|
592
|
+
const current = await ensureXaiRotationSettings(deps.db, {
|
|
593
|
+
accountId: authority.accountId,
|
|
594
|
+
workspaceId,
|
|
595
|
+
subjectId: authority.subjectId,
|
|
596
|
+
authoritySnapshot: readAuthority.snapshot,
|
|
597
|
+
});
|
|
598
|
+
const updated = await updateXaiRotationSettings(deps.db, {
|
|
599
|
+
workspaceId,
|
|
600
|
+
subjectId: authority.subjectId,
|
|
601
|
+
authoritySnapshot: readAuthority.snapshot,
|
|
602
|
+
expectedVersion: current.version,
|
|
603
|
+
rotationEnabled: parsed.data.rotationEnabled,
|
|
604
|
+
}).catch(() => null);
|
|
605
|
+
if (!updated) throw new HTTPException(409, { message: "SuperGrok settings changed" });
|
|
606
|
+
await wakeXaiCapacityWaiters(deps.db, {
|
|
607
|
+
workspaceId,
|
|
608
|
+
subjectId: authority.subjectId,
|
|
609
|
+
authoritySnapshot: readAuthority.snapshot,
|
|
610
|
+
reason: "xai_rotation_settings_changed",
|
|
611
|
+
});
|
|
612
|
+
return c.json({
|
|
613
|
+
rotationEnabled: updated.rotationEnabled,
|
|
614
|
+
rotationStrategy: "sharded" as const,
|
|
615
|
+
activeCredentialId: updated.activeCredentialId,
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
app.patch("/v1/workspaces/:workspaceId/supergrok/accounts/:accountId/allocator", async (c) => {
|
|
620
|
+
requireEnabled(deps);
|
|
621
|
+
const workspaceId = c.req.param("workspaceId");
|
|
622
|
+
const credentialId = c.req.param("accountId");
|
|
623
|
+
const authority = await authorityForAccountMutation(c, deps, workspaceId, credentialId);
|
|
624
|
+
const parsed = allocatorBody.safeParse(await c.req.json().catch(() => null));
|
|
625
|
+
if (!parsed.success) {
|
|
626
|
+
throw new HTTPException(400, {
|
|
627
|
+
message: "enabled and expectedVersion are required",
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
const result = await updateXaiAllocatorEligibility(deps.db, {
|
|
631
|
+
workspaceId,
|
|
632
|
+
subjectId: authority.subjectId,
|
|
633
|
+
credentialId,
|
|
634
|
+
enabled: parsed.data.enabled,
|
|
635
|
+
expectedVersion: parsed.data.expectedVersion,
|
|
636
|
+
});
|
|
637
|
+
if (result.kind === "not_found") {
|
|
638
|
+
throw new HTTPException(404, {
|
|
639
|
+
message: "SuperGrok account not found",
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
const response = {
|
|
643
|
+
allocatorEnabled: result.allocatorEnabled,
|
|
644
|
+
allocatorVersion: result.allocatorVersion,
|
|
645
|
+
allocatorUpdatedAt: result.allocatorUpdatedAt?.toISOString() ?? null,
|
|
646
|
+
changed: result.kind === "updated",
|
|
647
|
+
};
|
|
648
|
+
if (result.kind === "updated") {
|
|
649
|
+
await wakeXaiCapacityWaiters(deps.db, {
|
|
650
|
+
workspaceId,
|
|
651
|
+
subjectId: authority.subjectId,
|
|
652
|
+
authoritySnapshot: authority.snapshot,
|
|
653
|
+
reason: "xai_allocator_eligibility_changed",
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
return result.kind === "conflict" ? c.json(response, 409) : c.json(response);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
app.patch("/v1/workspaces/:workspaceId/supergrok/accounts/:accountId", async (c) => {
|
|
660
|
+
requireEnabled(deps);
|
|
661
|
+
const workspaceId = c.req.param("workspaceId");
|
|
662
|
+
const credentialId = c.req.param("accountId");
|
|
663
|
+
const authority = await authorityForAccountMutation(c, deps, workspaceId, credentialId);
|
|
664
|
+
const parsed = renameBody.safeParse(await c.req.json().catch(() => null));
|
|
665
|
+
if (!parsed.success) throw new HTTPException(400, { message: "label is invalid" });
|
|
666
|
+
const account = await renameXaiSubscriptionAccount(deps.db, {
|
|
667
|
+
workspaceId,
|
|
668
|
+
subjectId: authority.subjectId,
|
|
669
|
+
credentialId,
|
|
670
|
+
label: parsed.data.label || null,
|
|
671
|
+
});
|
|
672
|
+
if (!account)
|
|
673
|
+
throw new HTTPException(404, {
|
|
674
|
+
message: "SuperGrok account not found",
|
|
675
|
+
});
|
|
676
|
+
const settings = await getXaiRotationSettings(deps.db, {
|
|
677
|
+
workspaceId,
|
|
678
|
+
subjectId: authority.subjectId,
|
|
679
|
+
authoritySnapshot: authority.snapshot,
|
|
680
|
+
});
|
|
681
|
+
return c.json(accountJson(account, settings?.activeCredentialId ?? null));
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
app.delete("/v1/workspaces/:workspaceId/supergrok/accounts/:accountId", async (c) => {
|
|
685
|
+
requireEnabled(deps);
|
|
686
|
+
const workspaceId = c.req.param("workspaceId");
|
|
687
|
+
const credentialId = c.req.param("accountId");
|
|
688
|
+
const authority = await authorityForAccountMutation(c, deps, workspaceId, credentialId);
|
|
689
|
+
if (authority.snapshot.scope === "user") {
|
|
690
|
+
await wakeXaiCapacityWaiters(deps.db, {
|
|
691
|
+
workspaceId,
|
|
692
|
+
subjectId: authority.subjectId,
|
|
693
|
+
authoritySnapshot: authority.snapshot,
|
|
694
|
+
reason: "xai_credential_disconnecting",
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
const result = await disconnectXaiSubscriptionCredentialAndRepick(deps.db, {
|
|
698
|
+
accountId: authority.accountId,
|
|
699
|
+
workspaceId,
|
|
700
|
+
subjectId: authority.subjectId,
|
|
701
|
+
credentialId,
|
|
702
|
+
authoritySnapshot: authority.snapshot,
|
|
703
|
+
});
|
|
704
|
+
if (result.disconnected && authority.snapshot.scope === "workspace") {
|
|
705
|
+
await wakeXaiCapacityWaiters(deps.db, {
|
|
706
|
+
workspaceId,
|
|
707
|
+
subjectId: authority.subjectId,
|
|
708
|
+
authoritySnapshot: WORKSPACE_XAI_PROVIDER_ACCOUNT_AUTHORITY_SNAPSHOT_V1,
|
|
709
|
+
reason: "xai_credential_disconnected",
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
return c.json({
|
|
713
|
+
disconnected: result.disconnected,
|
|
714
|
+
newActiveId: result.newActiveCredentialId,
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
}
|