@opengeni/db 0.4.0 → 0.6.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/LICENSE +190 -0
- package/dist/{chunk-T2U4H4Z2.js → chunk-ZIUCA2IO.js} +150 -5
- package/dist/chunk-ZIUCA2IO.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1161 -21
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.d.ts +286 -5
- package/dist/{schema-DuRsrmzD.d.ts → schema-Dsz6UHNv.d.ts} +2391 -854
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +11 -1
- package/drizzle/0038_enrollment_desktop_unavailable_reason.sql +18 -0
- package/drizzle/0038_observability_counts.sql +32 -0
- package/drizzle/0039_connections.sql +62 -0
- package/drizzle/0039_session_mcp_require_approval.sql +7 -0
- package/drizzle/0040_schema_agnostic_opengeni_app_grants.sql +36 -0
- package/drizzle/0041_knowledge_layer.sql +73 -0
- package/drizzle/0042_integration_oauth_state.sql +56 -0
- package/drizzle/0043_integrations_catalog_imports.sql +104 -0
- package/drizzle/0043_toolspace_call_budget.sql +14 -0
- package/package.json +5 -10
- package/src/connection-token-resolver.ts +481 -0
- package/src/event-payload-sanitizer.ts +23 -0
- package/src/index.ts +1086 -23
- package/src/schema.ts +150 -2
- package/dist/chunk-T2U4H4Z2.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -9,9 +9,17 @@ import type {
|
|
|
9
9
|
CapabilityKind,
|
|
10
10
|
CapabilityPack,
|
|
11
11
|
CapabilitySource,
|
|
12
|
+
ConnectionKind,
|
|
13
|
+
ConnectionMetadata,
|
|
14
|
+
ConnectionStatus,
|
|
15
|
+
McpServerConnectionRef,
|
|
12
16
|
FileAsset,
|
|
13
17
|
FileStatus,
|
|
14
18
|
FileUploadStatus,
|
|
19
|
+
KnowledgeMemory,
|
|
20
|
+
KnowledgeMemoryKind,
|
|
21
|
+
KnowledgeMemoryStatus,
|
|
22
|
+
KnowledgeSourceRef,
|
|
15
23
|
ManagedAccount,
|
|
16
24
|
Permission,
|
|
17
25
|
PackInstallation,
|
|
@@ -58,7 +66,7 @@ import { isCodexBilledModel } from "@opengeni/codex";
|
|
|
58
66
|
// Re-exported so consumers get the whole codex-billed detection surface (the pure
|
|
59
67
|
// prefix test + the credential-aware predicates below) from a single import.
|
|
60
68
|
export { isCodexBilledModel } from "@opengeni/codex";
|
|
61
|
-
import { and, asc, desc, eq, gt, gte, inArray, lt, ne, sql, type SQL } from "drizzle-orm";
|
|
69
|
+
import { and, asc, desc, eq, gt, gte, inArray, isNull, lt, ne, or, sql, type SQL } from "drizzle-orm";
|
|
62
70
|
import type { PgDatabase } from "drizzle-orm/pg-core";
|
|
63
71
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
64
72
|
import postgres from "postgres";
|
|
@@ -347,6 +355,8 @@ export const allWorkspacePermissions: Permission[] = [
|
|
|
347
355
|
"github:manage",
|
|
348
356
|
"github:use",
|
|
349
357
|
"api_keys:manage",
|
|
358
|
+
"connections:read",
|
|
359
|
+
"connections:write",
|
|
350
360
|
"environments:manage",
|
|
351
361
|
"environments:use",
|
|
352
362
|
"mcp_servers:attach",
|
|
@@ -1132,6 +1142,38 @@ export type RegisterWorkspacePackInput = {
|
|
|
1132
1142
|
pack: CapabilityPack;
|
|
1133
1143
|
};
|
|
1134
1144
|
|
|
1145
|
+
export type CreateKnowledgeMemoryInput = {
|
|
1146
|
+
accountId: string;
|
|
1147
|
+
workspaceId: string;
|
|
1148
|
+
status?: KnowledgeMemoryStatus | undefined;
|
|
1149
|
+
kind?: KnowledgeMemoryKind | undefined;
|
|
1150
|
+
scope?: string | undefined;
|
|
1151
|
+
text: string;
|
|
1152
|
+
sourceRefs?: KnowledgeSourceRef[] | undefined;
|
|
1153
|
+
confidence?: number | undefined;
|
|
1154
|
+
metadata?: Record<string, unknown> | undefined;
|
|
1155
|
+
createdBySessionId?: string | null | undefined;
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
export type UpdateKnowledgeMemoryInput = {
|
|
1159
|
+
status?: KnowledgeMemoryStatus | undefined;
|
|
1160
|
+
kind?: KnowledgeMemoryKind | undefined;
|
|
1161
|
+
scope?: string | undefined;
|
|
1162
|
+
text?: string | undefined;
|
|
1163
|
+
sourceRefs?: KnowledgeSourceRef[] | undefined;
|
|
1164
|
+
confidence?: number | undefined;
|
|
1165
|
+
metadata?: Record<string, unknown> | undefined;
|
|
1166
|
+
reviewedBy?: string | null | undefined;
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
export type ListKnowledgeMemoryOptions = {
|
|
1170
|
+
query?: string | undefined;
|
|
1171
|
+
status?: KnowledgeMemoryStatus | undefined;
|
|
1172
|
+
kind?: KnowledgeMemoryKind | undefined;
|
|
1173
|
+
scope?: string | undefined;
|
|
1174
|
+
limit?: number | undefined;
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1135
1177
|
export type CreateSocialConnectionInput = {
|
|
1136
1178
|
accountId: string;
|
|
1137
1179
|
workspaceId: string;
|
|
@@ -1159,6 +1201,95 @@ export type CreateSocialPostInput = {
|
|
|
1159
1201
|
raw?: Record<string, unknown>;
|
|
1160
1202
|
};
|
|
1161
1203
|
|
|
1204
|
+
export type CreateConnectionInput = {
|
|
1205
|
+
accountId: string;
|
|
1206
|
+
workspaceId: string;
|
|
1207
|
+
subjectId?: string | null;
|
|
1208
|
+
providerDomain: string;
|
|
1209
|
+
kind: ConnectionKind;
|
|
1210
|
+
status?: ConnectionStatus;
|
|
1211
|
+
credentialEncrypted: string;
|
|
1212
|
+
grantedScopes?: string[];
|
|
1213
|
+
expiresAt?: Date | null;
|
|
1214
|
+
metadata?: Record<string, unknown>;
|
|
1215
|
+
createdBySubjectId?: string | null;
|
|
1216
|
+
updatedBySubjectId?: string | null;
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
export type UpdateConnectionInput = {
|
|
1220
|
+
workspaceId: string;
|
|
1221
|
+
connectionId: string;
|
|
1222
|
+
visibleToSubjectId?: string | null;
|
|
1223
|
+
expectedVersion?: number | undefined;
|
|
1224
|
+
subjectId?: string | null;
|
|
1225
|
+
providerDomain?: string;
|
|
1226
|
+
kind?: ConnectionKind;
|
|
1227
|
+
status?: ConnectionStatus;
|
|
1228
|
+
credentialEncrypted?: string;
|
|
1229
|
+
grantedScopes?: string[];
|
|
1230
|
+
expiresAt?: Date | null;
|
|
1231
|
+
metadata?: Record<string, unknown>;
|
|
1232
|
+
updatedBySubjectId?: string | null;
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
export type ConnectionCredentialForBroker = {
|
|
1236
|
+
id: string;
|
|
1237
|
+
accountId: string;
|
|
1238
|
+
workspaceId: string;
|
|
1239
|
+
subjectId: string | null;
|
|
1240
|
+
providerDomain: string;
|
|
1241
|
+
kind: ConnectionKind;
|
|
1242
|
+
status: ConnectionStatus;
|
|
1243
|
+
credential: Record<string, unknown>;
|
|
1244
|
+
grantedScopes: string[];
|
|
1245
|
+
expiresAt: Date | null;
|
|
1246
|
+
lastRefreshAt: Date | null;
|
|
1247
|
+
version: number;
|
|
1248
|
+
metadata: Record<string, unknown>;
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
export type IntegrationOAuthClientForUse = {
|
|
1252
|
+
id: string;
|
|
1253
|
+
issuer: string;
|
|
1254
|
+
authorizationServer: string;
|
|
1255
|
+
clientId: string;
|
|
1256
|
+
clientSecret: string | null;
|
|
1257
|
+
tokenEndpointAuthMethod: string;
|
|
1258
|
+
metadata: Record<string, unknown>;
|
|
1259
|
+
createdAt: Date;
|
|
1260
|
+
updatedAt: Date;
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
export type StoredIntegrationOAuthClient = {
|
|
1264
|
+
id: string;
|
|
1265
|
+
issuer: string;
|
|
1266
|
+
authorizationServer: string;
|
|
1267
|
+
clientId: string;
|
|
1268
|
+
clientSecretEncrypted: string | null;
|
|
1269
|
+
tokenEndpointAuthMethod: string;
|
|
1270
|
+
metadata: Record<string, unknown>;
|
|
1271
|
+
createdAt: Date;
|
|
1272
|
+
updatedAt: Date;
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
export type StoreIntegrationOAuthClientInput = {
|
|
1276
|
+
issuer: string;
|
|
1277
|
+
authorizationServer: string;
|
|
1278
|
+
clientId: string;
|
|
1279
|
+
clientSecretEncrypted?: string | null;
|
|
1280
|
+
tokenEndpointAuthMethod?: string;
|
|
1281
|
+
metadata?: Record<string, unknown>;
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
export type ConsumeOAuthStateNonceInput = {
|
|
1285
|
+
accountId: string;
|
|
1286
|
+
workspaceId: string;
|
|
1287
|
+
subjectId: string;
|
|
1288
|
+
nonce: string;
|
|
1289
|
+
expiresAt: Date;
|
|
1290
|
+
now: Date;
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1162
1293
|
export type CreateCapabilityCatalogItemInput = {
|
|
1163
1294
|
accountId: string;
|
|
1164
1295
|
workspaceId: string;
|
|
@@ -1176,6 +1307,69 @@ export type CreateCapabilityCatalogItemInput = {
|
|
|
1176
1307
|
metadata?: Record<string, unknown>;
|
|
1177
1308
|
};
|
|
1178
1309
|
|
|
1310
|
+
export type ImportBatch = {
|
|
1311
|
+
id: string;
|
|
1312
|
+
source: string;
|
|
1313
|
+
snapshotDate: string;
|
|
1314
|
+
snapshotRef: string | null;
|
|
1315
|
+
attributionNote: string;
|
|
1316
|
+
importedCount: number;
|
|
1317
|
+
skippedCount: number;
|
|
1318
|
+
quarantinedCount: number;
|
|
1319
|
+
logoFailureCount: number;
|
|
1320
|
+
staleCount: number;
|
|
1321
|
+
details: Record<string, unknown>;
|
|
1322
|
+
createdAt: string;
|
|
1323
|
+
updatedAt: string;
|
|
1324
|
+
};
|
|
1325
|
+
|
|
1326
|
+
export type CreateImportBatchInput = {
|
|
1327
|
+
source: string;
|
|
1328
|
+
snapshotDate: Date;
|
|
1329
|
+
snapshotRef?: string | null;
|
|
1330
|
+
attributionNote: string;
|
|
1331
|
+
importedCount?: number;
|
|
1332
|
+
skippedCount?: number;
|
|
1333
|
+
quarantinedCount?: number;
|
|
1334
|
+
logoFailureCount?: number;
|
|
1335
|
+
staleCount?: number;
|
|
1336
|
+
details?: Record<string, unknown>;
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
export type UpdateImportBatchCountsInput = {
|
|
1340
|
+
importedCount: number;
|
|
1341
|
+
skippedCount: number;
|
|
1342
|
+
quarantinedCount: number;
|
|
1343
|
+
logoFailureCount: number;
|
|
1344
|
+
staleCount: number;
|
|
1345
|
+
details?: Record<string, unknown>;
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
export type RegistryCapabilityCatalogItemInput = {
|
|
1349
|
+
id: string;
|
|
1350
|
+
providerDomain: string;
|
|
1351
|
+
name: string;
|
|
1352
|
+
description?: string | null;
|
|
1353
|
+
mcpUrl: string;
|
|
1354
|
+
transport: string;
|
|
1355
|
+
authKind: "oauth2" | "api_key" | "none" | "unknown";
|
|
1356
|
+
credentialFacts: Array<Record<string, unknown>>;
|
|
1357
|
+
tier: "verified" | "community";
|
|
1358
|
+
provenance: string;
|
|
1359
|
+
logoAssetPath?: string | null;
|
|
1360
|
+
importBatchId: string;
|
|
1361
|
+
scopesHint?: string[];
|
|
1362
|
+
homepageUrl?: string | null;
|
|
1363
|
+
tags?: string[];
|
|
1364
|
+
metadata?: Record<string, unknown>;
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
export type RegistryCatalogSurfaceKey = {
|
|
1368
|
+
id: string;
|
|
1369
|
+
providerDomain: string;
|
|
1370
|
+
mcpUrl: string;
|
|
1371
|
+
};
|
|
1372
|
+
|
|
1179
1373
|
export type EnableCapabilityInstallationInput = {
|
|
1180
1374
|
accountId: string;
|
|
1181
1375
|
workspaceId: string;
|
|
@@ -1200,6 +1394,7 @@ export type EnabledMcpCapabilityServer = {
|
|
|
1200
1394
|
* capability API surface.
|
|
1201
1395
|
*/
|
|
1202
1396
|
headersEncrypted?: Record<string, string>;
|
|
1397
|
+
connectionRef?: McpServerConnectionRef;
|
|
1203
1398
|
};
|
|
1204
1399
|
|
|
1205
1400
|
export type CreateSessionMcpServerInput = {
|
|
@@ -1209,6 +1404,7 @@ export type CreateSessionMcpServerInput = {
|
|
|
1209
1404
|
allowedTools?: string[] | null;
|
|
1210
1405
|
timeoutMs?: number | null;
|
|
1211
1406
|
cacheToolsList?: boolean | null;
|
|
1407
|
+
requireApproval?: boolean | string[] | null;
|
|
1212
1408
|
headersEncrypted?: Record<string, string>;
|
|
1213
1409
|
};
|
|
1214
1410
|
|
|
@@ -1226,6 +1422,7 @@ export type SessionMcpServerForRun = SessionMcpServerMetadata & {
|
|
|
1226
1422
|
allowedTools?: string[];
|
|
1227
1423
|
timeoutMs?: number;
|
|
1228
1424
|
cacheToolsList?: boolean;
|
|
1425
|
+
requireApproval?: boolean | string[];
|
|
1229
1426
|
headers: Record<string, string>;
|
|
1230
1427
|
};
|
|
1231
1428
|
|
|
@@ -1485,6 +1682,152 @@ export async function deleteWorkspacePack(db: Database, workspaceId: string, pac
|
|
|
1485
1682
|
});
|
|
1486
1683
|
}
|
|
1487
1684
|
|
|
1685
|
+
const registryCapabilitySource = "registry" as CapabilitySource;
|
|
1686
|
+
|
|
1687
|
+
export async function createImportBatch(db: Database, input: CreateImportBatchInput): Promise<ImportBatch> {
|
|
1688
|
+
const [row] = await db.insert(schema.importBatches).values({
|
|
1689
|
+
source: input.source,
|
|
1690
|
+
snapshotDate: input.snapshotDate,
|
|
1691
|
+
snapshotRef: input.snapshotRef ?? null,
|
|
1692
|
+
attributionNote: input.attributionNote,
|
|
1693
|
+
importedCount: input.importedCount ?? 0,
|
|
1694
|
+
skippedCount: input.skippedCount ?? 0,
|
|
1695
|
+
quarantinedCount: input.quarantinedCount ?? 0,
|
|
1696
|
+
logoFailureCount: input.logoFailureCount ?? 0,
|
|
1697
|
+
staleCount: input.staleCount ?? 0,
|
|
1698
|
+
details: input.details ?? {},
|
|
1699
|
+
}).returning();
|
|
1700
|
+
if (!row) {
|
|
1701
|
+
throw new Error("Failed to create import batch");
|
|
1702
|
+
}
|
|
1703
|
+
return mapImportBatch(row);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
export async function updateImportBatchCounts(db: Database, id: string, input: UpdateImportBatchCountsInput): Promise<ImportBatch> {
|
|
1707
|
+
const [row] = await db.update(schema.importBatches).set({
|
|
1708
|
+
importedCount: input.importedCount,
|
|
1709
|
+
skippedCount: input.skippedCount,
|
|
1710
|
+
quarantinedCount: input.quarantinedCount,
|
|
1711
|
+
logoFailureCount: input.logoFailureCount,
|
|
1712
|
+
staleCount: input.staleCount,
|
|
1713
|
+
...(input.details ? { details: input.details } : {}),
|
|
1714
|
+
updatedAt: new Date(),
|
|
1715
|
+
}).where(eq(schema.importBatches.id, id)).returning();
|
|
1716
|
+
if (!row) {
|
|
1717
|
+
throw new Error(`Import batch not found: ${id}`);
|
|
1718
|
+
}
|
|
1719
|
+
return mapImportBatch(row);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
export async function upsertRegistryCapabilityCatalogItem(db: Database, input: RegistryCapabilityCatalogItemInput): Promise<CapabilityCatalogItem> {
|
|
1723
|
+
const now = new Date();
|
|
1724
|
+
const metadata = {
|
|
1725
|
+
registry: "integrations.sh",
|
|
1726
|
+
providerDomain: input.providerDomain,
|
|
1727
|
+
scopesHint: input.scopesHint ?? [],
|
|
1728
|
+
...input.metadata,
|
|
1729
|
+
};
|
|
1730
|
+
const values = {
|
|
1731
|
+
id: input.id,
|
|
1732
|
+
accountId: null,
|
|
1733
|
+
workspaceId: null,
|
|
1734
|
+
kind: "mcp" as Exclude<CapabilityKind, "pack">,
|
|
1735
|
+
source: registryCapabilitySource,
|
|
1736
|
+
name: input.name,
|
|
1737
|
+
description: input.description ?? null,
|
|
1738
|
+
category: "integrations",
|
|
1739
|
+
tags: input.tags ?? ["mcp", "integration", input.tier],
|
|
1740
|
+
homepageUrl: input.homepageUrl ?? `https://${input.providerDomain}`,
|
|
1741
|
+
endpointUrl: input.mcpUrl,
|
|
1742
|
+
installUrl: input.homepageUrl ?? `https://${input.providerDomain}`,
|
|
1743
|
+
authModel: input.authKind === "none" ? null : "credential_ref",
|
|
1744
|
+
providerDomain: input.providerDomain,
|
|
1745
|
+
surfaceType: "mcp",
|
|
1746
|
+
transport: input.transport,
|
|
1747
|
+
mcpUrl: input.mcpUrl,
|
|
1748
|
+
authKind: input.authKind,
|
|
1749
|
+
credentialFacts: input.credentialFacts,
|
|
1750
|
+
tier: input.tier,
|
|
1751
|
+
provenance: input.provenance,
|
|
1752
|
+
logoAssetPath: input.logoAssetPath ?? null,
|
|
1753
|
+
importBatchId: input.importBatchId,
|
|
1754
|
+
stale: false,
|
|
1755
|
+
staleAt: null,
|
|
1756
|
+
metadata,
|
|
1757
|
+
updatedAt: now,
|
|
1758
|
+
};
|
|
1759
|
+
const updateValues = {
|
|
1760
|
+
id: values.id,
|
|
1761
|
+
kind: values.kind,
|
|
1762
|
+
name: values.name,
|
|
1763
|
+
description: values.description,
|
|
1764
|
+
category: values.category,
|
|
1765
|
+
tags: values.tags,
|
|
1766
|
+
homepageUrl: values.homepageUrl,
|
|
1767
|
+
endpointUrl: values.endpointUrl,
|
|
1768
|
+
installUrl: values.installUrl,
|
|
1769
|
+
authModel: values.authModel,
|
|
1770
|
+
surfaceType: values.surfaceType,
|
|
1771
|
+
transport: values.transport,
|
|
1772
|
+
authKind: values.authKind,
|
|
1773
|
+
credentialFacts: values.credentialFacts,
|
|
1774
|
+
tier: values.tier,
|
|
1775
|
+
provenance: values.provenance,
|
|
1776
|
+
logoAssetPath: sql`coalesce(excluded.logo_asset_path, ${schema.capabilityCatalogItems.logoAssetPath})`,
|
|
1777
|
+
importBatchId: values.importBatchId,
|
|
1778
|
+
stale: false,
|
|
1779
|
+
staleAt: null,
|
|
1780
|
+
metadata: values.metadata,
|
|
1781
|
+
updatedAt: values.updatedAt,
|
|
1782
|
+
};
|
|
1783
|
+
const [row] = await db.insert(schema.capabilityCatalogItems).values(values)
|
|
1784
|
+
.onConflictDoUpdate({
|
|
1785
|
+
target: [
|
|
1786
|
+
schema.capabilityCatalogItems.source,
|
|
1787
|
+
schema.capabilityCatalogItems.providerDomain,
|
|
1788
|
+
schema.capabilityCatalogItems.mcpUrl,
|
|
1789
|
+
],
|
|
1790
|
+
set: updateValues,
|
|
1791
|
+
})
|
|
1792
|
+
.returning();
|
|
1793
|
+
if (!row) {
|
|
1794
|
+
throw new Error("Failed to upsert registry capability catalog item");
|
|
1795
|
+
}
|
|
1796
|
+
return mapCapabilityCatalogItem(row);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
export async function listRegistryCatalogSurfaceKeys(db: Database): Promise<RegistryCatalogSurfaceKey[]> {
|
|
1800
|
+
const rows = await db.select({
|
|
1801
|
+
id: schema.capabilityCatalogItems.id,
|
|
1802
|
+
providerDomain: schema.capabilityCatalogItems.providerDomain,
|
|
1803
|
+
mcpUrl: schema.capabilityCatalogItems.mcpUrl,
|
|
1804
|
+
}).from(schema.capabilityCatalogItems)
|
|
1805
|
+
.where(eq(schema.capabilityCatalogItems.source, registryCapabilitySource));
|
|
1806
|
+
return rows.flatMap((row) => row.providerDomain && row.mcpUrl
|
|
1807
|
+
? [{ id: row.id, providerDomain: row.providerDomain, mcpUrl: row.mcpUrl }]
|
|
1808
|
+
: []);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
export async function markStaleRegistryCatalogItems(db: Database, activeKeys: Iterable<{ providerDomain: string; mcpUrl: string }>, importBatchId: string): Promise<number> {
|
|
1812
|
+
const active = new Set([...activeKeys].map((key) => `${key.providerDomain}\n${key.mcpUrl}`));
|
|
1813
|
+
const existing = await listRegistryCatalogSurfaceKeys(db);
|
|
1814
|
+
const stale = existing.filter((row) => !active.has(`${row.providerDomain}\n${row.mcpUrl}`));
|
|
1815
|
+
if (stale.length === 0) {
|
|
1816
|
+
return 0;
|
|
1817
|
+
}
|
|
1818
|
+
const now = new Date();
|
|
1819
|
+
const updated = await db.update(schema.capabilityCatalogItems).set({
|
|
1820
|
+
stale: true,
|
|
1821
|
+
staleAt: now,
|
|
1822
|
+
importBatchId,
|
|
1823
|
+
updatedAt: now,
|
|
1824
|
+
}).where(and(
|
|
1825
|
+
eq(schema.capabilityCatalogItems.source, registryCapabilitySource),
|
|
1826
|
+
inArray(schema.capabilityCatalogItems.id, stale.map((row) => row.id)),
|
|
1827
|
+
)).returning({ id: schema.capabilityCatalogItems.id });
|
|
1828
|
+
return updated.length;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1488
1831
|
export async function upsertCapabilityCatalogItem(db: Database, input: CreateCapabilityCatalogItemInput): Promise<CapabilityCatalogItem> {
|
|
1489
1832
|
return await withRlsContext(db, { accountId: input.accountId, workspaceId: input.workspaceId }, async (scopedDb) => {
|
|
1490
1833
|
const now = new Date();
|
|
@@ -1502,6 +1845,18 @@ export async function upsertCapabilityCatalogItem(db: Database, input: CreateCap
|
|
|
1502
1845
|
endpointUrl: input.endpointUrl ?? null,
|
|
1503
1846
|
installUrl: input.installUrl ?? null,
|
|
1504
1847
|
authModel: input.authModel ?? null,
|
|
1848
|
+
providerDomain: null,
|
|
1849
|
+
surfaceType: null,
|
|
1850
|
+
transport: null,
|
|
1851
|
+
mcpUrl: null,
|
|
1852
|
+
authKind: null,
|
|
1853
|
+
credentialFacts: [],
|
|
1854
|
+
tier: null,
|
|
1855
|
+
provenance: null,
|
|
1856
|
+
logoAssetPath: null,
|
|
1857
|
+
importBatchId: null,
|
|
1858
|
+
stale: false,
|
|
1859
|
+
staleAt: null,
|
|
1505
1860
|
metadata: input.metadata ?? {},
|
|
1506
1861
|
updatedAt: now,
|
|
1507
1862
|
};
|
|
@@ -1516,6 +1871,18 @@ export async function upsertCapabilityCatalogItem(db: Database, input: CreateCap
|
|
|
1516
1871
|
endpointUrl: values.endpointUrl,
|
|
1517
1872
|
installUrl: values.installUrl,
|
|
1518
1873
|
authModel: values.authModel,
|
|
1874
|
+
providerDomain: values.providerDomain,
|
|
1875
|
+
surfaceType: values.surfaceType,
|
|
1876
|
+
transport: values.transport,
|
|
1877
|
+
mcpUrl: values.mcpUrl,
|
|
1878
|
+
authKind: values.authKind,
|
|
1879
|
+
credentialFacts: values.credentialFacts,
|
|
1880
|
+
tier: values.tier,
|
|
1881
|
+
provenance: values.provenance,
|
|
1882
|
+
logoAssetPath: values.logoAssetPath,
|
|
1883
|
+
importBatchId: values.importBatchId,
|
|
1884
|
+
stale: values.stale,
|
|
1885
|
+
staleAt: values.staleAt,
|
|
1519
1886
|
metadata: values.metadata,
|
|
1520
1887
|
updatedAt: values.updatedAt,
|
|
1521
1888
|
};
|
|
@@ -1535,7 +1902,16 @@ export async function upsertCapabilityCatalogItem(db: Database, input: CreateCap
|
|
|
1535
1902
|
export async function listCapabilityCatalogItems(db: Database, workspaceId: string): Promise<CapabilityCatalogItem[]> {
|
|
1536
1903
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
1537
1904
|
const rows = await scopedDb.select().from(schema.capabilityCatalogItems)
|
|
1538
|
-
.where(
|
|
1905
|
+
.where(or(
|
|
1906
|
+
eq(schema.capabilityCatalogItems.workspaceId, workspaceId),
|
|
1907
|
+
and(
|
|
1908
|
+
isNull(schema.capabilityCatalogItems.workspaceId),
|
|
1909
|
+
or(
|
|
1910
|
+
ne(schema.capabilityCatalogItems.source, registryCapabilitySource),
|
|
1911
|
+
eq(schema.capabilityCatalogItems.stale, false),
|
|
1912
|
+
),
|
|
1913
|
+
),
|
|
1914
|
+
))
|
|
1539
1915
|
.orderBy(asc(schema.capabilityCatalogItems.kind), asc(schema.capabilityCatalogItems.name));
|
|
1540
1916
|
return rows.map(mapCapabilityCatalogItem);
|
|
1541
1917
|
});
|
|
@@ -1544,7 +1920,11 @@ export async function listCapabilityCatalogItems(db: Database, workspaceId: stri
|
|
|
1544
1920
|
export async function getCapabilityCatalogItem(db: Database, workspaceId: string, capabilityId: string): Promise<CapabilityCatalogItem | null> {
|
|
1545
1921
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
1546
1922
|
const [row] = await scopedDb.select().from(schema.capabilityCatalogItems)
|
|
1547
|
-
.where(and(
|
|
1923
|
+
.where(and(
|
|
1924
|
+
eq(schema.capabilityCatalogItems.id, capabilityId),
|
|
1925
|
+
or(eq(schema.capabilityCatalogItems.workspaceId, workspaceId), isNull(schema.capabilityCatalogItems.workspaceId)),
|
|
1926
|
+
))
|
|
1927
|
+
.orderBy(asc(sql`(${schema.capabilityCatalogItems.workspaceId} is null)`))
|
|
1548
1928
|
.limit(1);
|
|
1549
1929
|
return row ? mapCapabilityCatalogItem(row) : null;
|
|
1550
1930
|
});
|
|
@@ -1625,7 +2005,10 @@ export async function listEnabledMcpCapabilityServers(db: Database, workspaceId:
|
|
|
1625
2005
|
installation: schema.capabilityInstallations,
|
|
1626
2006
|
}).from(schema.capabilityInstallations)
|
|
1627
2007
|
.innerJoin(schema.capabilityCatalogItems, and(
|
|
1628
|
-
|
|
2008
|
+
or(
|
|
2009
|
+
eq(schema.capabilityInstallations.workspaceId, schema.capabilityCatalogItems.workspaceId),
|
|
2010
|
+
isNull(schema.capabilityCatalogItems.workspaceId),
|
|
2011
|
+
),
|
|
1629
2012
|
eq(schema.capabilityInstallations.capabilityId, schema.capabilityCatalogItems.id),
|
|
1630
2013
|
))
|
|
1631
2014
|
.where(and(
|
|
@@ -1635,14 +2018,28 @@ export async function listEnabledMcpCapabilityServers(db: Database, workspaceId:
|
|
|
1635
2018
|
))
|
|
1636
2019
|
.orderBy(asc(schema.capabilityCatalogItems.name)));
|
|
1637
2020
|
|
|
1638
|
-
|
|
2021
|
+
// A workspace-scoped catalog row and a global registry row can share the
|
|
2022
|
+
// same capability id; the join then matches one installation twice. Keep
|
|
2023
|
+
// one row per installation, preferring the workspace-scoped catalog row
|
|
2024
|
+
// (same precedence as getCapabilityCatalogItem).
|
|
2025
|
+
const preferredByInstallation = new Map<string, (typeof rows)[number]>();
|
|
2026
|
+
for (const row of rows) {
|
|
2027
|
+
const existing = preferredByInstallation.get(row.installation.id);
|
|
2028
|
+
if (!existing || (existing.item.workspaceId === null && row.item.workspaceId !== null)) {
|
|
2029
|
+
preferredByInstallation.set(row.installation.id, row);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
return [...preferredByInstallation.values()].flatMap(({ item, installation }) => {
|
|
1639
2034
|
if (!item.endpointUrl || !mcpConnectivityOk(installation.metadata)) {
|
|
1640
2035
|
return [];
|
|
1641
2036
|
}
|
|
1642
2037
|
const headersEncrypted = encryptedHeadersConfig(installation.config.headersEncrypted);
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
//
|
|
2038
|
+
const connectionRef = connectionRefConfig(installation.config.connectionRef);
|
|
2039
|
+
if (item.authModel && !headersEncrypted && !connectionRef) {
|
|
2040
|
+
// Credential-gated MCPs are runnable only when either legacy static
|
|
2041
|
+
// credential headers or the connections broker ref were stored at enable
|
|
2042
|
+
// time.
|
|
1646
2043
|
return [];
|
|
1647
2044
|
}
|
|
1648
2045
|
const metadata = item.metadata;
|
|
@@ -1659,6 +2056,7 @@ export async function listEnabledMcpCapabilityServers(db: Database, workspaceId:
|
|
|
1659
2056
|
...(timeoutMs ? { timeoutMs } : {}),
|
|
1660
2057
|
...(cacheToolsList !== undefined ? { cacheToolsList } : {}),
|
|
1661
2058
|
...(headersEncrypted ? { headersEncrypted } : {}),
|
|
2059
|
+
...(connectionRef ? { connectionRef } : {}),
|
|
1662
2060
|
}];
|
|
1663
2061
|
});
|
|
1664
2062
|
}
|
|
@@ -1715,6 +2113,441 @@ export function mcpServerIdForCapability(capabilityId: string, metadata: Record<
|
|
|
1715
2113
|
return `cap-${body}-${shortHash(capabilityId)}`;
|
|
1716
2114
|
}
|
|
1717
2115
|
|
|
2116
|
+
const connectionMetadataColumns = {
|
|
2117
|
+
id: schema.connections.id,
|
|
2118
|
+
accountId: schema.connections.accountId,
|
|
2119
|
+
workspaceId: schema.connections.workspaceId,
|
|
2120
|
+
subjectId: schema.connections.subjectId,
|
|
2121
|
+
providerDomain: schema.connections.providerDomain,
|
|
2122
|
+
kind: schema.connections.kind,
|
|
2123
|
+
status: schema.connections.status,
|
|
2124
|
+
grantedScopes: schema.connections.grantedScopes,
|
|
2125
|
+
expiresAt: schema.connections.expiresAt,
|
|
2126
|
+
lastRefreshAt: schema.connections.lastRefreshAt,
|
|
2127
|
+
lastUsedAt: schema.connections.lastUsedAt,
|
|
2128
|
+
lastError: schema.connections.lastError,
|
|
2129
|
+
version: schema.connections.version,
|
|
2130
|
+
metadata: schema.connections.metadata,
|
|
2131
|
+
createdBySubjectId: schema.connections.createdBySubjectId,
|
|
2132
|
+
updatedBySubjectId: schema.connections.updatedBySubjectId,
|
|
2133
|
+
createdAt: schema.connections.createdAt,
|
|
2134
|
+
updatedAt: schema.connections.updatedAt,
|
|
2135
|
+
};
|
|
2136
|
+
|
|
2137
|
+
function connectionSubjectVisibility(subjectId?: string | null): SQL {
|
|
2138
|
+
return subjectId
|
|
2139
|
+
? or(isNull(schema.connections.subjectId), eq(schema.connections.subjectId, subjectId))!
|
|
2140
|
+
: isNull(schema.connections.subjectId);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
export async function createConnection(db: Database, input: CreateConnectionInput): Promise<ConnectionMetadata> {
|
|
2144
|
+
return await withRlsContext(db, { accountId: input.accountId, workspaceId: input.workspaceId }, async (scopedDb) => {
|
|
2145
|
+
const [row] = await scopedDb.insert(schema.connections).values({
|
|
2146
|
+
accountId: input.accountId,
|
|
2147
|
+
workspaceId: input.workspaceId,
|
|
2148
|
+
subjectId: input.subjectId ?? null,
|
|
2149
|
+
providerDomain: input.providerDomain,
|
|
2150
|
+
kind: input.kind,
|
|
2151
|
+
status: input.status ?? "active",
|
|
2152
|
+
credentialEncrypted: input.credentialEncrypted,
|
|
2153
|
+
grantedScopes: input.grantedScopes ?? [],
|
|
2154
|
+
expiresAt: input.expiresAt ?? null,
|
|
2155
|
+
metadata: input.metadata ?? {},
|
|
2156
|
+
createdBySubjectId: input.createdBySubjectId ?? null,
|
|
2157
|
+
updatedBySubjectId: input.updatedBySubjectId ?? input.createdBySubjectId ?? null,
|
|
2158
|
+
}).returning(connectionMetadataColumns);
|
|
2159
|
+
if (!row) {
|
|
2160
|
+
throw new Error("Failed to create connection");
|
|
2161
|
+
}
|
|
2162
|
+
return mapConnectionMetadata(row);
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
export async function listConnectionsMetadata(db: Database, workspaceId: string, subjectId?: string | null): Promise<ConnectionMetadata[]> {
|
|
2167
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
2168
|
+
const rows = await scopedDb.select(connectionMetadataColumns).from(schema.connections)
|
|
2169
|
+
.where(and(eq(schema.connections.workspaceId, workspaceId), connectionSubjectVisibility(subjectId)))
|
|
2170
|
+
.orderBy(desc(schema.connections.createdAt));
|
|
2171
|
+
return rows.map(mapConnectionMetadata);
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
export async function getConnectionMetadata(db: Database, workspaceId: string, connectionId: string, subjectId?: string | null): Promise<ConnectionMetadata | null> {
|
|
2176
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
2177
|
+
const [row] = await scopedDb.select(connectionMetadataColumns).from(schema.connections)
|
|
2178
|
+
.where(and(
|
|
2179
|
+
eq(schema.connections.workspaceId, workspaceId),
|
|
2180
|
+
eq(schema.connections.id, connectionId),
|
|
2181
|
+
connectionSubjectVisibility(subjectId),
|
|
2182
|
+
))
|
|
2183
|
+
.limit(1);
|
|
2184
|
+
return row ? mapConnectionMetadata(row) : null;
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
export async function updateConnection(db: Database, input: UpdateConnectionInput): Promise<ConnectionMetadata | null> {
|
|
2189
|
+
return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
|
|
2190
|
+
const set = {
|
|
2191
|
+
updatedAt: new Date(),
|
|
2192
|
+
...(input.providerDomain !== undefined ? { providerDomain: input.providerDomain } : {}),
|
|
2193
|
+
...(input.subjectId !== undefined ? { subjectId: input.subjectId } : {}),
|
|
2194
|
+
...(input.kind !== undefined ? { kind: input.kind } : {}),
|
|
2195
|
+
...(input.status !== undefined ? { status: input.status } : {}),
|
|
2196
|
+
...(input.credentialEncrypted !== undefined
|
|
2197
|
+
? {
|
|
2198
|
+
credentialEncrypted: input.credentialEncrypted,
|
|
2199
|
+
version: sql`${schema.connections.version} + 1`,
|
|
2200
|
+
lastError: null,
|
|
2201
|
+
}
|
|
2202
|
+
: {}),
|
|
2203
|
+
...(input.grantedScopes !== undefined ? { grantedScopes: input.grantedScopes } : {}),
|
|
2204
|
+
...(input.expiresAt !== undefined ? { expiresAt: input.expiresAt } : {}),
|
|
2205
|
+
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
|
2206
|
+
...(input.updatedBySubjectId !== undefined ? { updatedBySubjectId: input.updatedBySubjectId } : {}),
|
|
2207
|
+
};
|
|
2208
|
+
const [row] = await scopedDb.update(schema.connections).set(set)
|
|
2209
|
+
.where(and(
|
|
2210
|
+
eq(schema.connections.workspaceId, input.workspaceId),
|
|
2211
|
+
eq(schema.connections.id, input.connectionId),
|
|
2212
|
+
connectionSubjectVisibility(input.visibleToSubjectId),
|
|
2213
|
+
...(input.expectedVersion !== undefined ? [eq(schema.connections.version, input.expectedVersion)] : []),
|
|
2214
|
+
))
|
|
2215
|
+
.returning(connectionMetadataColumns);
|
|
2216
|
+
return row ? mapConnectionMetadata(row) : null;
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
export async function revokeConnection(db: Database, workspaceId: string, connectionId: string, updatedBySubjectId?: string | null): Promise<ConnectionMetadata | null> {
|
|
2221
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
2222
|
+
const [row] = await scopedDb.update(schema.connections).set({
|
|
2223
|
+
status: "revoked",
|
|
2224
|
+
// The version bump invalidates any in-flight refresh's (id, version) CAS,
|
|
2225
|
+
// so a racing refresh cannot commit and flip the row back to active.
|
|
2226
|
+
version: sql`${schema.connections.version} + 1`,
|
|
2227
|
+
updatedBySubjectId: updatedBySubjectId ?? null,
|
|
2228
|
+
updatedAt: new Date(),
|
|
2229
|
+
})
|
|
2230
|
+
.where(and(
|
|
2231
|
+
eq(schema.connections.workspaceId, workspaceId),
|
|
2232
|
+
eq(schema.connections.id, connectionId),
|
|
2233
|
+
// Same visibility rule as get/update: shared rows plus the caller's own
|
|
2234
|
+
// subject rows. Cross-subject revocation (admin janitorial) arrives with
|
|
2235
|
+
// the subject-connections UX in I5, deliberately not before.
|
|
2236
|
+
connectionSubjectVisibility(updatedBySubjectId),
|
|
2237
|
+
))
|
|
2238
|
+
.returning(connectionMetadataColumns);
|
|
2239
|
+
return row ? mapConnectionMetadata(row) : null;
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
export async function loadConnectionCredentialForBroker(db: Database, settings: Settings, input: {
|
|
2244
|
+
workspaceId: string;
|
|
2245
|
+
connectionId?: string;
|
|
2246
|
+
providerDomain: string;
|
|
2247
|
+
kind?: ConnectionKind;
|
|
2248
|
+
subjectId?: string | null;
|
|
2249
|
+
allowSubjectOwned?: boolean;
|
|
2250
|
+
}): Promise<ConnectionCredentialForBroker | null> {
|
|
2251
|
+
const key = environmentsEncryptionKeyBytes(settings);
|
|
2252
|
+
if (!key) {
|
|
2253
|
+
throw new Error("connection credential present but OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured");
|
|
2254
|
+
}
|
|
2255
|
+
const subjectPredicate = input.allowSubjectOwned
|
|
2256
|
+
? connectionSubjectVisibility(input.subjectId)
|
|
2257
|
+
: isNull(schema.connections.subjectId);
|
|
2258
|
+
const conditions: SQL[] = [
|
|
2259
|
+
eq(schema.connections.workspaceId, input.workspaceId),
|
|
2260
|
+
subjectPredicate,
|
|
2261
|
+
];
|
|
2262
|
+
if (input.connectionId) {
|
|
2263
|
+
conditions.push(eq(schema.connections.id, input.connectionId));
|
|
2264
|
+
} else {
|
|
2265
|
+
conditions.push(eq(schema.connections.providerDomain, input.providerDomain));
|
|
2266
|
+
if (input.kind) {
|
|
2267
|
+
conditions.push(eq(schema.connections.kind, input.kind));
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
|
|
2271
|
+
// Prefer active rows: a revoke bumps updatedAt, so recency alone would let a
|
|
2272
|
+
// freshly revoked connection shadow an active replacement for the provider.
|
|
2273
|
+
const [row] = await scopedDb.select().from(schema.connections)
|
|
2274
|
+
.where(and(...conditions))
|
|
2275
|
+
.orderBy(desc(sql`(${schema.connections.status} = 'active')`), desc(schema.connections.updatedAt))
|
|
2276
|
+
.limit(1);
|
|
2277
|
+
if (!row) {
|
|
2278
|
+
return null;
|
|
2279
|
+
}
|
|
2280
|
+
let credential: unknown;
|
|
2281
|
+
try {
|
|
2282
|
+
credential = JSON.parse(decryptEnvironmentValue(key, row.credentialEncrypted));
|
|
2283
|
+
} catch (error) {
|
|
2284
|
+
throw new Error(`connection credential could not be decrypted for ${row.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2285
|
+
}
|
|
2286
|
+
if (!credential || typeof credential !== "object" || Array.isArray(credential)) {
|
|
2287
|
+
throw new Error(`connection credential bundle for ${row.id} is not a JSON object`);
|
|
2288
|
+
}
|
|
2289
|
+
return {
|
|
2290
|
+
id: row.id,
|
|
2291
|
+
accountId: row.accountId,
|
|
2292
|
+
workspaceId: row.workspaceId,
|
|
2293
|
+
subjectId: row.subjectId,
|
|
2294
|
+
providerDomain: row.providerDomain,
|
|
2295
|
+
kind: row.kind as ConnectionKind,
|
|
2296
|
+
status: row.status as ConnectionStatus,
|
|
2297
|
+
credential: credential as Record<string, unknown>,
|
|
2298
|
+
grantedScopes: row.grantedScopes,
|
|
2299
|
+
expiresAt: row.expiresAt,
|
|
2300
|
+
lastRefreshAt: row.lastRefreshAt,
|
|
2301
|
+
version: row.version,
|
|
2302
|
+
metadata: row.metadata,
|
|
2303
|
+
};
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
export async function recordConnectionTokenRefresh(db: Database, input: {
|
|
2308
|
+
id: string;
|
|
2309
|
+
version: number;
|
|
2310
|
+
workspaceId: string;
|
|
2311
|
+
credentialEncrypted: string;
|
|
2312
|
+
expiresAt: Date | null;
|
|
2313
|
+
grantedScopes?: string[];
|
|
2314
|
+
lastRefreshAt: Date;
|
|
2315
|
+
}): Promise<boolean> {
|
|
2316
|
+
return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
|
|
2317
|
+
const set = {
|
|
2318
|
+
credentialEncrypted: input.credentialEncrypted,
|
|
2319
|
+
expiresAt: input.expiresAt,
|
|
2320
|
+
lastRefreshAt: input.lastRefreshAt,
|
|
2321
|
+
status: "active",
|
|
2322
|
+
lastError: null,
|
|
2323
|
+
version: sql`${schema.connections.version} + 1`,
|
|
2324
|
+
updatedAt: new Date(),
|
|
2325
|
+
...(input.grantedScopes !== undefined ? { grantedScopes: input.grantedScopes } : {}),
|
|
2326
|
+
};
|
|
2327
|
+
const updated = await scopedDb.update(schema.connections).set(set)
|
|
2328
|
+
.where(and(
|
|
2329
|
+
eq(schema.connections.id, input.id),
|
|
2330
|
+
eq(schema.connections.workspaceId, input.workspaceId),
|
|
2331
|
+
eq(schema.connections.version, input.version),
|
|
2332
|
+
// A refresh may only ever renew a live credential; revoked/errored rows
|
|
2333
|
+
// stay dead even if a status change somewhere forgot to bump version.
|
|
2334
|
+
eq(schema.connections.status, "active"),
|
|
2335
|
+
))
|
|
2336
|
+
.returning({ id: schema.connections.id });
|
|
2337
|
+
return updated.length > 0;
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
export async function setConnectionStatus(db: Database, workspaceId: string, status: ConnectionStatus, lastError: string | null, guard: { id: string; version: number }): Promise<boolean> {
|
|
2342
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
2343
|
+
const updated = await scopedDb.update(schema.connections).set({
|
|
2344
|
+
status,
|
|
2345
|
+
lastError,
|
|
2346
|
+
version: sql`${schema.connections.version} + 1`,
|
|
2347
|
+
updatedAt: new Date(),
|
|
2348
|
+
}).where(and(
|
|
2349
|
+
eq(schema.connections.workspaceId, workspaceId),
|
|
2350
|
+
eq(schema.connections.id, guard.id),
|
|
2351
|
+
eq(schema.connections.version, guard.version),
|
|
2352
|
+
)).returning({ id: schema.connections.id });
|
|
2353
|
+
return updated.length > 0;
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
export async function recordConnectionUsed(db: Database, workspaceId: string, connectionId: string): Promise<void> {
|
|
2358
|
+
await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
2359
|
+
await scopedDb.update(schema.connections).set({
|
|
2360
|
+
lastUsedAt: new Date(),
|
|
2361
|
+
updatedAt: new Date(),
|
|
2362
|
+
}).where(and(eq(schema.connections.workspaceId, workspaceId), eq(schema.connections.id, connectionId)));
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
export async function loadIntegrationOAuthClient(
|
|
2367
|
+
db: Database,
|
|
2368
|
+
settings: Settings,
|
|
2369
|
+
issuer: string,
|
|
2370
|
+
): Promise<IntegrationOAuthClientForUse | null> {
|
|
2371
|
+
const [row] = await db.select().from(schema.integrationOauthClients)
|
|
2372
|
+
.where(eq(schema.integrationOauthClients.issuer, issuer))
|
|
2373
|
+
.limit(1);
|
|
2374
|
+
if (!row) {
|
|
2375
|
+
return null;
|
|
2376
|
+
}
|
|
2377
|
+
let clientSecret: string | null = null;
|
|
2378
|
+
if (row.clientSecretEncrypted) {
|
|
2379
|
+
const key = environmentsEncryptionKeyBytes(settings);
|
|
2380
|
+
if (!key) {
|
|
2381
|
+
throw new Error("OAuth client secret present but OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured");
|
|
2382
|
+
}
|
|
2383
|
+
clientSecret = decryptEnvironmentValue(key, row.clientSecretEncrypted);
|
|
2384
|
+
}
|
|
2385
|
+
return {
|
|
2386
|
+
id: row.id,
|
|
2387
|
+
issuer: row.issuer,
|
|
2388
|
+
authorizationServer: row.authorizationServer,
|
|
2389
|
+
clientId: row.clientId,
|
|
2390
|
+
clientSecret,
|
|
2391
|
+
tokenEndpointAuthMethod: row.tokenEndpointAuthMethod,
|
|
2392
|
+
metadata: row.metadata,
|
|
2393
|
+
createdAt: row.createdAt,
|
|
2394
|
+
updatedAt: row.updatedAt,
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
export async function storeIntegrationOAuthClient(
|
|
2399
|
+
db: Database,
|
|
2400
|
+
input: StoreIntegrationOAuthClientInput,
|
|
2401
|
+
): Promise<StoredIntegrationOAuthClient> {
|
|
2402
|
+
const [inserted] = await db.insert(schema.integrationOauthClients).values({
|
|
2403
|
+
issuer: input.issuer,
|
|
2404
|
+
authorizationServer: input.authorizationServer,
|
|
2405
|
+
clientId: input.clientId,
|
|
2406
|
+
clientSecretEncrypted: input.clientSecretEncrypted ?? null,
|
|
2407
|
+
tokenEndpointAuthMethod: input.tokenEndpointAuthMethod ?? "none",
|
|
2408
|
+
metadata: input.metadata ?? {},
|
|
2409
|
+
}).onConflictDoNothing({
|
|
2410
|
+
target: schema.integrationOauthClients.issuer,
|
|
2411
|
+
}).returning();
|
|
2412
|
+
if (inserted) {
|
|
2413
|
+
return mapStoredIntegrationOAuthClient(inserted);
|
|
2414
|
+
}
|
|
2415
|
+
const [winner] = await db.select().from(schema.integrationOauthClients)
|
|
2416
|
+
.where(eq(schema.integrationOauthClients.issuer, input.issuer))
|
|
2417
|
+
.limit(1);
|
|
2418
|
+
if (!winner) {
|
|
2419
|
+
throw new Error(`OAuth client registration conflict winner not found for issuer ${input.issuer}`);
|
|
2420
|
+
}
|
|
2421
|
+
return mapStoredIntegrationOAuthClient(winner);
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
function mapStoredIntegrationOAuthClient(row: typeof schema.integrationOauthClients.$inferSelect): StoredIntegrationOAuthClient {
|
|
2425
|
+
return {
|
|
2426
|
+
id: row.id,
|
|
2427
|
+
issuer: row.issuer,
|
|
2428
|
+
authorizationServer: row.authorizationServer,
|
|
2429
|
+
clientId: row.clientId,
|
|
2430
|
+
clientSecretEncrypted: row.clientSecretEncrypted,
|
|
2431
|
+
tokenEndpointAuthMethod: row.tokenEndpointAuthMethod,
|
|
2432
|
+
metadata: row.metadata,
|
|
2433
|
+
createdAt: row.createdAt,
|
|
2434
|
+
updatedAt: row.updatedAt,
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
export async function consumeIntegrationOAuthStateNonce(
|
|
2439
|
+
db: Database,
|
|
2440
|
+
input: ConsumeOAuthStateNonceInput,
|
|
2441
|
+
): Promise<boolean> {
|
|
2442
|
+
return await withRlsContext(db, { accountId: input.accountId, workspaceId: input.workspaceId }, async (scopedDb) => {
|
|
2443
|
+
await scopedDb.delete(schema.integrationOauthStateNonces)
|
|
2444
|
+
.where(and(
|
|
2445
|
+
eq(schema.integrationOauthStateNonces.workspaceId, input.workspaceId),
|
|
2446
|
+
lt(schema.integrationOauthStateNonces.expiresAt, input.now),
|
|
2447
|
+
));
|
|
2448
|
+
const inserted = await scopedDb.insert(schema.integrationOauthStateNonces).values({
|
|
2449
|
+
accountId: input.accountId,
|
|
2450
|
+
workspaceId: input.workspaceId,
|
|
2451
|
+
subjectId: input.subjectId,
|
|
2452
|
+
nonce: input.nonce,
|
|
2453
|
+
expiresAt: input.expiresAt,
|
|
2454
|
+
usedAt: input.now,
|
|
2455
|
+
}).onConflictDoNothing({ target: schema.integrationOauthStateNonces.nonce })
|
|
2456
|
+
.returning({ nonce: schema.integrationOauthStateNonces.nonce });
|
|
2457
|
+
return inserted.length > 0;
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
export async function createKnowledgeMemory(db: Database, input: CreateKnowledgeMemoryInput): Promise<KnowledgeMemory> {
|
|
2462
|
+
const text = requireDbString(input.text, "knowledge memory text");
|
|
2463
|
+
const scope = cleanDbString(input.scope) ?? "workspace";
|
|
2464
|
+
return await withRlsContext(db, { accountId: input.accountId, workspaceId: input.workspaceId }, async (scopedDb) => {
|
|
2465
|
+
const [row] = await scopedDb.insert(schema.knowledgeMemories).values({
|
|
2466
|
+
accountId: input.accountId,
|
|
2467
|
+
workspaceId: input.workspaceId,
|
|
2468
|
+
status: input.status ?? "proposed",
|
|
2469
|
+
kind: input.kind ?? "semantic",
|
|
2470
|
+
scope,
|
|
2471
|
+
text,
|
|
2472
|
+
sourceRefs: input.sourceRefs ?? [],
|
|
2473
|
+
confidence: confidenceToStorage(input.confidence ?? 0.5),
|
|
2474
|
+
metadata: input.metadata ?? {},
|
|
2475
|
+
createdBySessionId: input.createdBySessionId ?? null,
|
|
2476
|
+
}).returning();
|
|
2477
|
+
if (!row) {
|
|
2478
|
+
throw new Error("Failed to create knowledge memory");
|
|
2479
|
+
}
|
|
2480
|
+
return mapKnowledgeMemory(row);
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
export async function updateKnowledgeMemory(db: Database, workspaceId: string, memoryId: string, input: UpdateKnowledgeMemoryInput): Promise<KnowledgeMemory> {
|
|
2485
|
+
const reviewStatus = input.status === "approved" || input.status === "rejected";
|
|
2486
|
+
const scope = input.scope !== undefined ? requireDbString(input.scope, "knowledge memory scope") : undefined;
|
|
2487
|
+
const text = input.text !== undefined ? requireDbString(input.text, "knowledge memory text") : undefined;
|
|
2488
|
+
const reviewedBy = input.reviewedBy === null
|
|
2489
|
+
? null
|
|
2490
|
+
: input.reviewedBy !== undefined
|
|
2491
|
+
? requireDbString(input.reviewedBy, "knowledge memory reviewer")
|
|
2492
|
+
: undefined;
|
|
2493
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
2494
|
+
const [row] = await scopedDb.update(schema.knowledgeMemories).set({
|
|
2495
|
+
...(input.status !== undefined ? { status: input.status } : {}),
|
|
2496
|
+
...(input.kind !== undefined ? { kind: input.kind } : {}),
|
|
2497
|
+
...(scope !== undefined ? { scope } : {}),
|
|
2498
|
+
...(text !== undefined ? { text } : {}),
|
|
2499
|
+
...(input.sourceRefs !== undefined ? { sourceRefs: input.sourceRefs } : {}),
|
|
2500
|
+
...(input.confidence !== undefined ? { confidence: confidenceToStorage(input.confidence) } : {}),
|
|
2501
|
+
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
|
2502
|
+
// Re-proposing clears review metadata; an explicit reviewedBy in the same
|
|
2503
|
+
// update still wins via the later spread.
|
|
2504
|
+
...(input.status === "proposed" ? { reviewedBy: null, reviewedAt: null } : {}),
|
|
2505
|
+
...(reviewedBy !== undefined ? { reviewedBy } : {}),
|
|
2506
|
+
...(reviewStatus ? { reviewedAt: new Date() } : {}),
|
|
2507
|
+
updatedAt: new Date(),
|
|
2508
|
+
}).where(and(eq(schema.knowledgeMemories.workspaceId, workspaceId), eq(schema.knowledgeMemories.id, memoryId))).returning();
|
|
2509
|
+
if (!row) {
|
|
2510
|
+
throw new Error(`Knowledge memory not found: ${memoryId}`);
|
|
2511
|
+
}
|
|
2512
|
+
return mapKnowledgeMemory(row);
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
export async function getKnowledgeMemory(db: Database, workspaceId: string, memoryId: string): Promise<KnowledgeMemory | null> {
|
|
2517
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
2518
|
+
const [row] = await scopedDb.select().from(schema.knowledgeMemories)
|
|
2519
|
+
.where(and(eq(schema.knowledgeMemories.workspaceId, workspaceId), eq(schema.knowledgeMemories.id, memoryId)))
|
|
2520
|
+
.limit(1);
|
|
2521
|
+
return row ? mapKnowledgeMemory(row) : null;
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
export async function listKnowledgeMemories(db: Database, workspaceId: string, options: ListKnowledgeMemoryOptions = {}): Promise<KnowledgeMemory[]> {
|
|
2526
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
2527
|
+
const conditions: SQL[] = [eq(schema.knowledgeMemories.workspaceId, workspaceId)];
|
|
2528
|
+
if (options.status) {
|
|
2529
|
+
conditions.push(eq(schema.knowledgeMemories.status, options.status));
|
|
2530
|
+
}
|
|
2531
|
+
if (options.kind) {
|
|
2532
|
+
conditions.push(eq(schema.knowledgeMemories.kind, options.kind));
|
|
2533
|
+
}
|
|
2534
|
+
const scope = cleanDbString(options.scope);
|
|
2535
|
+
if (scope) {
|
|
2536
|
+
conditions.push(eq(schema.knowledgeMemories.scope, scope));
|
|
2537
|
+
}
|
|
2538
|
+
const query = cleanDbString(options.query);
|
|
2539
|
+
if (query) {
|
|
2540
|
+
conditions.push(sql`to_tsvector('simple', ${schema.knowledgeMemories.text}) @@ plainto_tsquery('simple', ${query})`);
|
|
2541
|
+
}
|
|
2542
|
+
const limit = Math.min(Math.max(options.limit ?? 20, 1), 100);
|
|
2543
|
+
const rows = await scopedDb.select().from(schema.knowledgeMemories)
|
|
2544
|
+
.where(and(...conditions))
|
|
2545
|
+
.orderBy(desc(schema.knowledgeMemories.updatedAt))
|
|
2546
|
+
.limit(limit);
|
|
2547
|
+
return rows.map(mapKnowledgeMemory);
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
|
|
1718
2551
|
export async function createSocialConnection(db: Database, input: CreateSocialConnectionInput): Promise<SocialConnection> {
|
|
1719
2552
|
return await withRlsContext(db, { accountId: input.accountId, workspaceId: input.workspaceId }, async (scopedDb) => {
|
|
1720
2553
|
const [row] = await scopedDb.insert(schema.socialConnections).values({
|
|
@@ -3166,6 +3999,7 @@ async function insertSessionMcpServers(db: Database, input: {
|
|
|
3166
3999
|
allowedTools: server.allowedTools ?? null,
|
|
3167
4000
|
timeoutMs: server.timeoutMs ?? null,
|
|
3168
4001
|
cacheToolsList: server.cacheToolsList ?? false,
|
|
4002
|
+
requireApproval: server.requireApproval ?? null,
|
|
3169
4003
|
headersEncrypted: server.headersEncrypted ?? {},
|
|
3170
4004
|
}))).returning();
|
|
3171
4005
|
return rows.map(mapSessionMcpServerMetadata);
|
|
@@ -3257,6 +4091,7 @@ export async function listSessionMcpServersForRun(
|
|
|
3257
4091
|
...(row.allowedTools ? { allowedTools: row.allowedTools } : {}),
|
|
3258
4092
|
...(row.timeoutMs ? { timeoutMs: row.timeoutMs } : {}),
|
|
3259
4093
|
...(row.cacheToolsList ? { cacheToolsList: row.cacheToolsList } : {}),
|
|
4094
|
+
...(row.requireApproval != null ? { requireApproval: row.requireApproval } : {}),
|
|
3260
4095
|
headers,
|
|
3261
4096
|
};
|
|
3262
4097
|
});
|
|
@@ -3542,6 +4377,41 @@ export async function listSessionEvents(
|
|
|
3542
4377
|
});
|
|
3543
4378
|
}
|
|
3544
4379
|
|
|
4380
|
+
export type ToolspaceCallReservation =
|
|
4381
|
+
| { reserved: true; count: number }
|
|
4382
|
+
| { reserved: false };
|
|
4383
|
+
|
|
4384
|
+
/**
|
|
4385
|
+
* Atomically reserve one toolspace call against a turn's per-turn budget.
|
|
4386
|
+
*
|
|
4387
|
+
* A single conditional UPDATE increments `toolspace_call_count` only while it is
|
|
4388
|
+
* below `limit` and returns the post-increment value. Concurrent reservations
|
|
4389
|
+
* for the same turn serialize on the row lock, so exactly `limit` of N
|
|
4390
|
+
* simultaneous callers observe `reserved: true` — closing the read-then-append
|
|
4391
|
+
* TOCTOU the event-count approach had. `reserved: false` means the turn is at or
|
|
4392
|
+
* over budget (or the turn row no longer exists).
|
|
4393
|
+
*/
|
|
4394
|
+
export async function reserveToolspaceCallForTurn(
|
|
4395
|
+
db: Database,
|
|
4396
|
+
workspaceId: string,
|
|
4397
|
+
sessionId: string,
|
|
4398
|
+
turnId: string,
|
|
4399
|
+
limit: number,
|
|
4400
|
+
): Promise<ToolspaceCallReservation> {
|
|
4401
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
4402
|
+
const [row] = await scopedDb.update(schema.sessionTurns)
|
|
4403
|
+
.set({ toolspaceCallCount: sql`${schema.sessionTurns.toolspaceCallCount} + 1` })
|
|
4404
|
+
.where(and(
|
|
4405
|
+
eq(schema.sessionTurns.workspaceId, workspaceId),
|
|
4406
|
+
eq(schema.sessionTurns.sessionId, sessionId),
|
|
4407
|
+
eq(schema.sessionTurns.id, turnId),
|
|
4408
|
+
sql`${schema.sessionTurns.toolspaceCallCount} < ${limit}`,
|
|
4409
|
+
))
|
|
4410
|
+
.returning({ count: schema.sessionTurns.toolspaceCallCount });
|
|
4411
|
+
return row ? { reserved: true, count: Number(row.count) } : { reserved: false };
|
|
4412
|
+
});
|
|
4413
|
+
}
|
|
4414
|
+
|
|
3545
4415
|
function normalizeEventSequence(value: number | undefined, fallback: number): number {
|
|
3546
4416
|
if (value === undefined || !Number.isFinite(value)) {
|
|
3547
4417
|
return fallback;
|
|
@@ -5113,6 +5983,32 @@ export async function listMeterableWarmLeases(db: Database): Promise<MeterableWa
|
|
|
5113
5983
|
}));
|
|
5114
5984
|
}
|
|
5115
5985
|
|
|
5986
|
+
export async function countQueuedTurns(db: Database): Promise<number> {
|
|
5987
|
+
const rows = await rawRows<{ count: number | string }>(db, sql`
|
|
5988
|
+
select opengeni_private.count_queued_turns() as count
|
|
5989
|
+
`);
|
|
5990
|
+
return Number(rows[0]?.count ?? 0);
|
|
5991
|
+
}
|
|
5992
|
+
|
|
5993
|
+
export async function countSandboxLeasesByLiveness(db: Database): Promise<Record<SandboxLeaseLiveness, number>> {
|
|
5994
|
+
const counts: Record<SandboxLeaseLiveness, number> = {
|
|
5995
|
+
cold: 0,
|
|
5996
|
+
warming: 0,
|
|
5997
|
+
warm: 0,
|
|
5998
|
+
draining: 0,
|
|
5999
|
+
};
|
|
6000
|
+
const rows = await rawRows<{ liveness: SandboxLeaseLiveness; count: number | string }>(db, sql`
|
|
6001
|
+
select liveness, count
|
|
6002
|
+
from opengeni_private.count_sandbox_leases_by_liveness()
|
|
6003
|
+
`);
|
|
6004
|
+
for (const row of rows) {
|
|
6005
|
+
if (row.liveness in counts) {
|
|
6006
|
+
counts[row.liveness] = Number(row.count);
|
|
6007
|
+
}
|
|
6008
|
+
}
|
|
6009
|
+
return counts;
|
|
6010
|
+
}
|
|
6011
|
+
|
|
5116
6012
|
// Cross-workspace live Modal lease read for the provider-side orphan sweep. The
|
|
5117
6013
|
// SECURITY DEFINER function is the sanctioned RLS bypass; see migration 0036.
|
|
5118
6014
|
export async function listLiveModalSandboxLeaseAttributions(db: Database): Promise<LiveModalSandboxLeaseAttribution[]> {
|
|
@@ -5384,6 +6280,9 @@ export type EnrollmentRecord = {
|
|
|
5384
6280
|
pubkey: string;
|
|
5385
6281
|
exposure: EnrollmentExposure;
|
|
5386
6282
|
hasDisplay: boolean;
|
|
6283
|
+
/** Set when a display exists but capture is not permitted (macOS Screen Recording
|
|
6284
|
+
* not granted); null when capture is permitted or the machine is headless. */
|
|
6285
|
+
desktopUnavailableReason: string | null;
|
|
5387
6286
|
allowScreenControl: boolean;
|
|
5388
6287
|
status: EnrollmentStatus;
|
|
5389
6288
|
os: EnrollmentOs;
|
|
@@ -5402,6 +6301,7 @@ function mapEnrollment(row: typeof schema.enrollments.$inferSelect): EnrollmentR
|
|
|
5402
6301
|
pubkey: row.pubkey,
|
|
5403
6302
|
exposure: row.exposure as EnrollmentExposure,
|
|
5404
6303
|
hasDisplay: row.hasDisplay,
|
|
6304
|
+
desktopUnavailableReason: row.desktopUnavailableReason ?? null,
|
|
5405
6305
|
allowScreenControl: row.allowScreenControl,
|
|
5406
6306
|
status: row.status as EnrollmentStatus,
|
|
5407
6307
|
os: row.os as EnrollmentOs,
|
|
@@ -5547,23 +6447,40 @@ export async function touchEnrollmentLastSeen(db: Database, input: {
|
|
|
5547
6447
|
// at enroll time from the enroll-offer snapshot, this tracks REALITY across the
|
|
5548
6448
|
// machine's life — a Mac that later grants Screen Recording, or a Linux box whose
|
|
5549
6449
|
// Xvfb starts after enrollment, flips false→true on its next Hello (and a display
|
|
5550
|
-
// that goes away flips true→false).
|
|
5551
|
-
//
|
|
5552
|
-
//
|
|
5553
|
-
//
|
|
5554
|
-
//
|
|
5555
|
-
|
|
5556
|
-
|
|
6450
|
+
// that goes away flips true→false).
|
|
6451
|
+
//
|
|
6452
|
+
// `desktopUnavailableReason` rides alongside: a machine can have a display it
|
|
6453
|
+
// cannot CAPTURE (macOS Screen Recording / TCC not granted). In that case
|
|
6454
|
+
// has_display is false BUT the reason is a human, actionable string, so the
|
|
6455
|
+
// Machines dashboard can show "display: capture not granted" instead of a bare
|
|
6456
|
+
// "headless". null means capture is permitted OR the machine is genuinely headless.
|
|
6457
|
+
//
|
|
6458
|
+
// CHANGE-GUARDED at the SQL layer: the write fires only when EITHER field differs
|
|
6459
|
+
// from what the row already holds (`hasDisplay` via `ne`, the nullable reason via
|
|
6460
|
+
// `IS DISTINCT FROM`), so a steady-state Hello updates zero rows and never churns.
|
|
6461
|
+
// Returns whether a row was actually changed. Best-effort — the caller swallows
|
|
6462
|
+
// failures so a display refresh never breaks the agent's connect.
|
|
6463
|
+
export async function setEnrollmentDisplayState(db: Database, input: {
|
|
6464
|
+
accountId: string; workspaceId: string; enrollmentId: string;
|
|
6465
|
+
hasDisplay: boolean; desktopUnavailableReason: string | null;
|
|
5557
6466
|
}): Promise<{ updated: boolean }> {
|
|
5558
6467
|
return await withRlsContext(db, { accountId: input.accountId, workspaceId: input.workspaceId }, async (scopedDb) => {
|
|
5559
6468
|
const rows = await scopedDb.update(schema.enrollments)
|
|
5560
|
-
.set({
|
|
6469
|
+
.set({
|
|
6470
|
+
hasDisplay: input.hasDisplay,
|
|
6471
|
+
desktopUnavailableReason: input.desktopUnavailableReason,
|
|
6472
|
+
updatedAt: new Date(),
|
|
6473
|
+
})
|
|
5561
6474
|
.where(and(
|
|
5562
6475
|
eq(schema.enrollments.workspaceId, input.workspaceId),
|
|
5563
6476
|
eq(schema.enrollments.id, input.enrollmentId),
|
|
5564
|
-
// Only write on a CHANGE — an unchanged display must
|
|
5565
|
-
// every reconnect Hello.
|
|
5566
|
-
|
|
6477
|
+
// Only write on a CHANGE to EITHER field — an unchanged display state must
|
|
6478
|
+
// not churn a write on every reconnect Hello. `IS DISTINCT FROM` is the
|
|
6479
|
+
// null-safe inequality (a plain `ne` skips NULL rows).
|
|
6480
|
+
or(
|
|
6481
|
+
ne(schema.enrollments.hasDisplay, input.hasDisplay),
|
|
6482
|
+
sql`${schema.enrollments.desktopUnavailableReason} IS DISTINCT FROM ${input.desktopUnavailableReason}`,
|
|
6483
|
+
),
|
|
5567
6484
|
))
|
|
5568
6485
|
.returning({ id: schema.enrollments.id });
|
|
5569
6486
|
return { updated: rows.length > 0 };
|
|
@@ -7915,12 +8832,30 @@ function mapWorkspacePack(row: typeof schema.workspacePacks.$inferSelect): Works
|
|
|
7915
8832
|
};
|
|
7916
8833
|
}
|
|
7917
8834
|
|
|
8835
|
+
function mapImportBatch(row: typeof schema.importBatches.$inferSelect): ImportBatch {
|
|
8836
|
+
return {
|
|
8837
|
+
id: row.id,
|
|
8838
|
+
source: row.source,
|
|
8839
|
+
snapshotDate: row.snapshotDate.toISOString(),
|
|
8840
|
+
snapshotRef: row.snapshotRef,
|
|
8841
|
+
attributionNote: row.attributionNote,
|
|
8842
|
+
importedCount: row.importedCount,
|
|
8843
|
+
skippedCount: row.skippedCount,
|
|
8844
|
+
quarantinedCount: row.quarantinedCount,
|
|
8845
|
+
logoFailureCount: row.logoFailureCount,
|
|
8846
|
+
staleCount: row.staleCount,
|
|
8847
|
+
details: row.details,
|
|
8848
|
+
createdAt: row.createdAt.toISOString(),
|
|
8849
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
8850
|
+
};
|
|
8851
|
+
}
|
|
8852
|
+
|
|
7918
8853
|
function mapCapabilityCatalogItem(row: typeof schema.capabilityCatalogItems.$inferSelect): CapabilityCatalogItem {
|
|
7919
8854
|
const runtime = row.kind === "mcp" && row.endpointUrl
|
|
7920
8855
|
? {
|
|
7921
8856
|
available: true,
|
|
7922
8857
|
mcpServerId: mcpServerIdForCapability(row.id, row.metadata),
|
|
7923
|
-
transport: "streamable-http",
|
|
8858
|
+
transport: row.transport ?? "streamable-http",
|
|
7924
8859
|
notes: row.authModel
|
|
7925
8860
|
? "Requires credential headers supplied in the enable request."
|
|
7926
8861
|
: null,
|
|
@@ -7933,8 +8868,8 @@ function mapCapabilityCatalogItem(row: typeof schema.capabilityCatalogItems.$inf
|
|
|
7933
8868
|
};
|
|
7934
8869
|
return {
|
|
7935
8870
|
id: row.id,
|
|
7936
|
-
accountId: row.accountId,
|
|
7937
|
-
workspaceId: row.workspaceId,
|
|
8871
|
+
...(row.accountId ? { accountId: row.accountId } : {}),
|
|
8872
|
+
...(row.workspaceId ? { workspaceId: row.workspaceId } : {}),
|
|
7938
8873
|
kind: row.kind as CapabilityKind,
|
|
7939
8874
|
source: row.source as CapabilitySource,
|
|
7940
8875
|
name: row.name,
|
|
@@ -7945,6 +8880,18 @@ function mapCapabilityCatalogItem(row: typeof schema.capabilityCatalogItems.$inf
|
|
|
7945
8880
|
endpointUrl: row.endpointUrl,
|
|
7946
8881
|
installUrl: row.installUrl,
|
|
7947
8882
|
authModel: row.authModel,
|
|
8883
|
+
providerDomain: row.providerDomain,
|
|
8884
|
+
surfaceType: row.surfaceType,
|
|
8885
|
+
transport: row.transport,
|
|
8886
|
+
mcpUrl: row.mcpUrl,
|
|
8887
|
+
authKind: row.authKind as CapabilityCatalogItem["authKind"],
|
|
8888
|
+
credentialFacts: row.credentialFacts,
|
|
8889
|
+
tier: row.tier as CapabilityCatalogItem["tier"],
|
|
8890
|
+
provenance: row.provenance,
|
|
8891
|
+
logoAssetPath: row.logoAssetPath,
|
|
8892
|
+
importBatchId: row.importBatchId,
|
|
8893
|
+
stale: row.stale,
|
|
8894
|
+
staleAt: row.staleAt?.toISOString() ?? null,
|
|
7948
8895
|
tools: [],
|
|
7949
8896
|
runtime,
|
|
7950
8897
|
enabled: false,
|
|
@@ -8009,6 +8956,67 @@ function redactInstallationConfig(config: Record<string, unknown>): Record<strin
|
|
|
8009
8956
|
return { ...rest, headerNames: Object.keys(headersEncrypted).sort() };
|
|
8010
8957
|
}
|
|
8011
8958
|
|
|
8959
|
+
function mapConnectionMetadata(row: {
|
|
8960
|
+
id: string;
|
|
8961
|
+
accountId: string;
|
|
8962
|
+
workspaceId: string;
|
|
8963
|
+
subjectId: string | null;
|
|
8964
|
+
providerDomain: string;
|
|
8965
|
+
kind: string;
|
|
8966
|
+
status: string;
|
|
8967
|
+
grantedScopes: string[];
|
|
8968
|
+
expiresAt: Date | null;
|
|
8969
|
+
lastRefreshAt: Date | null;
|
|
8970
|
+
lastUsedAt: Date | null;
|
|
8971
|
+
lastError: string | null;
|
|
8972
|
+
version: number;
|
|
8973
|
+
metadata: Record<string, unknown>;
|
|
8974
|
+
createdBySubjectId: string | null;
|
|
8975
|
+
updatedBySubjectId: string | null;
|
|
8976
|
+
createdAt: Date;
|
|
8977
|
+
updatedAt: Date;
|
|
8978
|
+
}): ConnectionMetadata {
|
|
8979
|
+
return {
|
|
8980
|
+
id: row.id,
|
|
8981
|
+
accountId: row.accountId,
|
|
8982
|
+
workspaceId: row.workspaceId,
|
|
8983
|
+
subjectId: row.subjectId,
|
|
8984
|
+
providerDomain: row.providerDomain,
|
|
8985
|
+
kind: row.kind as ConnectionKind,
|
|
8986
|
+
status: row.status as ConnectionStatus,
|
|
8987
|
+
grantedScopes: row.grantedScopes,
|
|
8988
|
+
expiresAt: row.expiresAt?.toISOString() ?? null,
|
|
8989
|
+
lastRefreshAt: row.lastRefreshAt?.toISOString() ?? null,
|
|
8990
|
+
lastUsedAt: row.lastUsedAt?.toISOString() ?? null,
|
|
8991
|
+
lastError: row.lastError,
|
|
8992
|
+
version: row.version,
|
|
8993
|
+
metadata: row.metadata,
|
|
8994
|
+
createdBySubjectId: row.createdBySubjectId,
|
|
8995
|
+
updatedBySubjectId: row.updatedBySubjectId,
|
|
8996
|
+
createdAt: row.createdAt.toISOString(),
|
|
8997
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
8998
|
+
};
|
|
8999
|
+
}
|
|
9000
|
+
|
|
9001
|
+
function mapKnowledgeMemory(row: typeof schema.knowledgeMemories.$inferSelect): KnowledgeMemory {
|
|
9002
|
+
return {
|
|
9003
|
+
id: row.id,
|
|
9004
|
+
workspaceId: row.workspaceId,
|
|
9005
|
+
status: row.status as KnowledgeMemoryStatus,
|
|
9006
|
+
kind: row.kind as KnowledgeMemoryKind,
|
|
9007
|
+
scope: row.scope,
|
|
9008
|
+
text: row.text,
|
|
9009
|
+
sourceRefs: Array.isArray(row.sourceRefs) ? row.sourceRefs as KnowledgeSourceRef[] : [],
|
|
9010
|
+
confidence: confidenceFromStorage(row.confidence),
|
|
9011
|
+
metadata: row.metadata,
|
|
9012
|
+
createdBySessionId: row.createdBySessionId,
|
|
9013
|
+
reviewedBy: row.reviewedBy,
|
|
9014
|
+
reviewedAt: row.reviewedAt ? row.reviewedAt.toISOString() : null,
|
|
9015
|
+
createdAt: row.createdAt.toISOString(),
|
|
9016
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
9017
|
+
};
|
|
9018
|
+
}
|
|
9019
|
+
|
|
8012
9020
|
function mapSocialConnection(row: typeof schema.socialConnections.$inferSelect): SocialConnection {
|
|
8013
9021
|
return {
|
|
8014
9022
|
id: row.id,
|
|
@@ -8102,6 +9110,30 @@ function stringArrayConfig(value: unknown): string[] | undefined {
|
|
|
8102
9110
|
return values.length > 0 ? [...new Set(values.map((item) => item.trim()))] : undefined;
|
|
8103
9111
|
}
|
|
8104
9112
|
|
|
9113
|
+
function cleanDbString(value: string | undefined | null): string | undefined {
|
|
9114
|
+
const trimmed = value?.trim();
|
|
9115
|
+
return trimmed ? trimmed : undefined;
|
|
9116
|
+
}
|
|
9117
|
+
|
|
9118
|
+
function requireDbString(value: string, field: string): string {
|
|
9119
|
+
const trimmed = cleanDbString(value);
|
|
9120
|
+
if (!trimmed) {
|
|
9121
|
+
throw new Error(`${field} is required`);
|
|
9122
|
+
}
|
|
9123
|
+
return trimmed;
|
|
9124
|
+
}
|
|
9125
|
+
|
|
9126
|
+
function confidenceToStorage(value: number): number {
|
|
9127
|
+
if (!Number.isFinite(value)) {
|
|
9128
|
+
return 50;
|
|
9129
|
+
}
|
|
9130
|
+
return Math.round(Math.min(Math.max(value, 0), 1) * 100);
|
|
9131
|
+
}
|
|
9132
|
+
|
|
9133
|
+
function confidenceFromStorage(value: number): number {
|
|
9134
|
+
return Number((Math.min(Math.max(value, 0), 100) / 100).toFixed(2));
|
|
9135
|
+
}
|
|
9136
|
+
|
|
8105
9137
|
function positiveIntegerConfig(value: unknown): number | undefined {
|
|
8106
9138
|
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
8107
9139
|
return value;
|
|
@@ -8127,7 +9159,37 @@ function encryptedHeadersConfig(value: unknown): Record<string, string> | undefi
|
|
|
8127
9159
|
|
|
8128
9160
|
function mcpConnectivityOk(metadata: Record<string, unknown>): boolean {
|
|
8129
9161
|
const value = metadata.mcpConnectivity;
|
|
8130
|
-
return !!value && typeof value === "object" && "status" in value && value.status === "ok";
|
|
9162
|
+
return !!value && typeof value === "object" && "status" in value && (value.status === "ok" || value.status === "auth_deferred");
|
|
9163
|
+
}
|
|
9164
|
+
|
|
9165
|
+
function connectionRefConfig(value: unknown): McpServerConnectionRef | undefined {
|
|
9166
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
9167
|
+
return undefined;
|
|
9168
|
+
}
|
|
9169
|
+
const record = value as Record<string, unknown>;
|
|
9170
|
+
if (typeof record.providerDomain !== "string" || record.providerDomain.length === 0) {
|
|
9171
|
+
return undefined;
|
|
9172
|
+
}
|
|
9173
|
+
const ref: McpServerConnectionRef = { providerDomain: record.providerDomain };
|
|
9174
|
+
if (typeof record.connectionId === "string" && record.connectionId.length > 0) {
|
|
9175
|
+
ref.connectionId = record.connectionId;
|
|
9176
|
+
}
|
|
9177
|
+
if (typeof record.kind === "string" && ["oauth2", "api_key", "app_install", "delegated"].includes(record.kind)) {
|
|
9178
|
+
ref.kind = record.kind as ConnectionKind;
|
|
9179
|
+
}
|
|
9180
|
+
if (Array.isArray(record.scopes)) {
|
|
9181
|
+
const scopes = record.scopes.filter((scope): scope is string => typeof scope === "string" && scope.length > 0);
|
|
9182
|
+
if (scopes.length > 0) {
|
|
9183
|
+
ref.scopes = scopes;
|
|
9184
|
+
}
|
|
9185
|
+
}
|
|
9186
|
+
if (typeof record.resource === "string" && record.resource.length > 0) {
|
|
9187
|
+
ref.resource = record.resource;
|
|
9188
|
+
}
|
|
9189
|
+
if (record.subjectScope === "workspace" || record.subjectScope === "subject") {
|
|
9190
|
+
ref.subjectScope = record.subjectScope;
|
|
9191
|
+
}
|
|
9192
|
+
return ref;
|
|
8131
9193
|
}
|
|
8132
9194
|
|
|
8133
9195
|
function shortHash(value: string): string {
|
|
@@ -8145,3 +9207,4 @@ function shortHash(value: string): string {
|
|
|
8145
9207
|
// recordCodexAccountUsage) is already initialized when its default-deps bag
|
|
8146
9208
|
// evaluates under the index↔resolver module cycle.
|
|
8147
9209
|
export * from "./codex-token-resolver";
|
|
9210
|
+
export * from "./connection-token-resolver";
|