@opengeni/db 0.4.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +1149 -21
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.d.ts +289 -5
- package/dist/{schema-C7Wvjwge.d.ts → schema-BUbuMteO.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/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_credit_balance_observability.sql +22 -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 +1079 -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;
|
|
@@ -5139,6 +6009,22 @@ export async function countSandboxLeasesByLiveness(db: Database): Promise<Record
|
|
|
5139
6009
|
return counts;
|
|
5140
6010
|
}
|
|
5141
6011
|
|
|
6012
|
+
export type CreditBalanceByAccount = {
|
|
6013
|
+
accountId: string;
|
|
6014
|
+
balanceMicros: number;
|
|
6015
|
+
};
|
|
6016
|
+
|
|
6017
|
+
export async function listCreditBalancesByAccount(db: Database): Promise<CreditBalanceByAccount[]> {
|
|
6018
|
+
const rows = await rawRows<{ account_id: string; balance_micros: number | string }>(db, sql`
|
|
6019
|
+
select account_id, balance_micros
|
|
6020
|
+
from opengeni_private.credit_balance_by_account()
|
|
6021
|
+
`);
|
|
6022
|
+
return rows.map((row) => ({
|
|
6023
|
+
accountId: row.account_id,
|
|
6024
|
+
balanceMicros: Number(row.balance_micros),
|
|
6025
|
+
}));
|
|
6026
|
+
}
|
|
6027
|
+
|
|
5142
6028
|
// Cross-workspace live Modal lease read for the provider-side orphan sweep. The
|
|
5143
6029
|
// SECURITY DEFINER function is the sanctioned RLS bypass; see migration 0036.
|
|
5144
6030
|
export async function listLiveModalSandboxLeaseAttributions(db: Database): Promise<LiveModalSandboxLeaseAttribution[]> {
|
|
@@ -5410,6 +6296,9 @@ export type EnrollmentRecord = {
|
|
|
5410
6296
|
pubkey: string;
|
|
5411
6297
|
exposure: EnrollmentExposure;
|
|
5412
6298
|
hasDisplay: boolean;
|
|
6299
|
+
/** Set when a display exists but capture is not permitted (macOS Screen Recording
|
|
6300
|
+
* not granted); null when capture is permitted or the machine is headless. */
|
|
6301
|
+
desktopUnavailableReason: string | null;
|
|
5413
6302
|
allowScreenControl: boolean;
|
|
5414
6303
|
status: EnrollmentStatus;
|
|
5415
6304
|
os: EnrollmentOs;
|
|
@@ -5428,6 +6317,7 @@ function mapEnrollment(row: typeof schema.enrollments.$inferSelect): EnrollmentR
|
|
|
5428
6317
|
pubkey: row.pubkey,
|
|
5429
6318
|
exposure: row.exposure as EnrollmentExposure,
|
|
5430
6319
|
hasDisplay: row.hasDisplay,
|
|
6320
|
+
desktopUnavailableReason: row.desktopUnavailableReason ?? null,
|
|
5431
6321
|
allowScreenControl: row.allowScreenControl,
|
|
5432
6322
|
status: row.status as EnrollmentStatus,
|
|
5433
6323
|
os: row.os as EnrollmentOs,
|
|
@@ -5573,23 +6463,40 @@ export async function touchEnrollmentLastSeen(db: Database, input: {
|
|
|
5573
6463
|
// at enroll time from the enroll-offer snapshot, this tracks REALITY across the
|
|
5574
6464
|
// machine's life — a Mac that later grants Screen Recording, or a Linux box whose
|
|
5575
6465
|
// Xvfb starts after enrollment, flips false→true on its next Hello (and a display
|
|
5576
|
-
// that goes away flips true→false).
|
|
5577
|
-
//
|
|
5578
|
-
//
|
|
5579
|
-
//
|
|
5580
|
-
//
|
|
5581
|
-
|
|
5582
|
-
|
|
6466
|
+
// that goes away flips true→false).
|
|
6467
|
+
//
|
|
6468
|
+
// `desktopUnavailableReason` rides alongside: a machine can have a display it
|
|
6469
|
+
// cannot CAPTURE (macOS Screen Recording / TCC not granted). In that case
|
|
6470
|
+
// has_display is false BUT the reason is a human, actionable string, so the
|
|
6471
|
+
// Machines dashboard can show "display: capture not granted" instead of a bare
|
|
6472
|
+
// "headless". null means capture is permitted OR the machine is genuinely headless.
|
|
6473
|
+
//
|
|
6474
|
+
// CHANGE-GUARDED at the SQL layer: the write fires only when EITHER field differs
|
|
6475
|
+
// from what the row already holds (`hasDisplay` via `ne`, the nullable reason via
|
|
6476
|
+
// `IS DISTINCT FROM`), so a steady-state Hello updates zero rows and never churns.
|
|
6477
|
+
// Returns whether a row was actually changed. Best-effort — the caller swallows
|
|
6478
|
+
// failures so a display refresh never breaks the agent's connect.
|
|
6479
|
+
export async function setEnrollmentDisplayState(db: Database, input: {
|
|
6480
|
+
accountId: string; workspaceId: string; enrollmentId: string;
|
|
6481
|
+
hasDisplay: boolean; desktopUnavailableReason: string | null;
|
|
5583
6482
|
}): Promise<{ updated: boolean }> {
|
|
5584
6483
|
return await withRlsContext(db, { accountId: input.accountId, workspaceId: input.workspaceId }, async (scopedDb) => {
|
|
5585
6484
|
const rows = await scopedDb.update(schema.enrollments)
|
|
5586
|
-
.set({
|
|
6485
|
+
.set({
|
|
6486
|
+
hasDisplay: input.hasDisplay,
|
|
6487
|
+
desktopUnavailableReason: input.desktopUnavailableReason,
|
|
6488
|
+
updatedAt: new Date(),
|
|
6489
|
+
})
|
|
5587
6490
|
.where(and(
|
|
5588
6491
|
eq(schema.enrollments.workspaceId, input.workspaceId),
|
|
5589
6492
|
eq(schema.enrollments.id, input.enrollmentId),
|
|
5590
|
-
// Only write on a CHANGE — an unchanged display must
|
|
5591
|
-
// every reconnect Hello.
|
|
5592
|
-
|
|
6493
|
+
// Only write on a CHANGE to EITHER field — an unchanged display state must
|
|
6494
|
+
// not churn a write on every reconnect Hello. `IS DISTINCT FROM` is the
|
|
6495
|
+
// null-safe inequality (a plain `ne` skips NULL rows).
|
|
6496
|
+
or(
|
|
6497
|
+
ne(schema.enrollments.hasDisplay, input.hasDisplay),
|
|
6498
|
+
sql`${schema.enrollments.desktopUnavailableReason} IS DISTINCT FROM ${input.desktopUnavailableReason}`,
|
|
6499
|
+
),
|
|
5593
6500
|
))
|
|
5594
6501
|
.returning({ id: schema.enrollments.id });
|
|
5595
6502
|
return { updated: rows.length > 0 };
|
|
@@ -7941,12 +8848,30 @@ function mapWorkspacePack(row: typeof schema.workspacePacks.$inferSelect): Works
|
|
|
7941
8848
|
};
|
|
7942
8849
|
}
|
|
7943
8850
|
|
|
8851
|
+
function mapImportBatch(row: typeof schema.importBatches.$inferSelect): ImportBatch {
|
|
8852
|
+
return {
|
|
8853
|
+
id: row.id,
|
|
8854
|
+
source: row.source,
|
|
8855
|
+
snapshotDate: row.snapshotDate.toISOString(),
|
|
8856
|
+
snapshotRef: row.snapshotRef,
|
|
8857
|
+
attributionNote: row.attributionNote,
|
|
8858
|
+
importedCount: row.importedCount,
|
|
8859
|
+
skippedCount: row.skippedCount,
|
|
8860
|
+
quarantinedCount: row.quarantinedCount,
|
|
8861
|
+
logoFailureCount: row.logoFailureCount,
|
|
8862
|
+
staleCount: row.staleCount,
|
|
8863
|
+
details: row.details,
|
|
8864
|
+
createdAt: row.createdAt.toISOString(),
|
|
8865
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
8866
|
+
};
|
|
8867
|
+
}
|
|
8868
|
+
|
|
7944
8869
|
function mapCapabilityCatalogItem(row: typeof schema.capabilityCatalogItems.$inferSelect): CapabilityCatalogItem {
|
|
7945
8870
|
const runtime = row.kind === "mcp" && row.endpointUrl
|
|
7946
8871
|
? {
|
|
7947
8872
|
available: true,
|
|
7948
8873
|
mcpServerId: mcpServerIdForCapability(row.id, row.metadata),
|
|
7949
|
-
transport: "streamable-http",
|
|
8874
|
+
transport: row.transport ?? "streamable-http",
|
|
7950
8875
|
notes: row.authModel
|
|
7951
8876
|
? "Requires credential headers supplied in the enable request."
|
|
7952
8877
|
: null,
|
|
@@ -7959,8 +8884,8 @@ function mapCapabilityCatalogItem(row: typeof schema.capabilityCatalogItems.$inf
|
|
|
7959
8884
|
};
|
|
7960
8885
|
return {
|
|
7961
8886
|
id: row.id,
|
|
7962
|
-
accountId: row.accountId,
|
|
7963
|
-
workspaceId: row.workspaceId,
|
|
8887
|
+
...(row.accountId ? { accountId: row.accountId } : {}),
|
|
8888
|
+
...(row.workspaceId ? { workspaceId: row.workspaceId } : {}),
|
|
7964
8889
|
kind: row.kind as CapabilityKind,
|
|
7965
8890
|
source: row.source as CapabilitySource,
|
|
7966
8891
|
name: row.name,
|
|
@@ -7971,10 +8896,25 @@ function mapCapabilityCatalogItem(row: typeof schema.capabilityCatalogItems.$inf
|
|
|
7971
8896
|
endpointUrl: row.endpointUrl,
|
|
7972
8897
|
installUrl: row.installUrl,
|
|
7973
8898
|
authModel: row.authModel,
|
|
8899
|
+
providerDomain: row.providerDomain,
|
|
8900
|
+
surfaceType: row.surfaceType,
|
|
8901
|
+
transport: row.transport,
|
|
8902
|
+
mcpUrl: row.mcpUrl,
|
|
8903
|
+
authKind: row.authKind as CapabilityCatalogItem["authKind"],
|
|
8904
|
+
credentialFacts: row.credentialFacts,
|
|
8905
|
+
tier: row.tier as CapabilityCatalogItem["tier"],
|
|
8906
|
+
provenance: row.provenance,
|
|
8907
|
+
logoAssetPath: row.logoAssetPath,
|
|
8908
|
+
importBatchId: row.importBatchId,
|
|
8909
|
+
stale: row.stale,
|
|
8910
|
+
staleAt: row.staleAt?.toISOString() ?? null,
|
|
7974
8911
|
tools: [],
|
|
7975
8912
|
runtime,
|
|
7976
8913
|
enabled: false,
|
|
7977
8914
|
enabledReason: null,
|
|
8915
|
+
// Overwritten by applyCapabilityEnablement in @opengeni/core, which knows
|
|
8916
|
+
// the installation; a freshly-read catalog row carries no connection.
|
|
8917
|
+
connectionRef: null,
|
|
7978
8918
|
metadata: row.metadata,
|
|
7979
8919
|
createdAt: row.createdAt.toISOString(),
|
|
7980
8920
|
updatedAt: row.updatedAt.toISOString(),
|
|
@@ -8035,6 +8975,67 @@ function redactInstallationConfig(config: Record<string, unknown>): Record<strin
|
|
|
8035
8975
|
return { ...rest, headerNames: Object.keys(headersEncrypted).sort() };
|
|
8036
8976
|
}
|
|
8037
8977
|
|
|
8978
|
+
function mapConnectionMetadata(row: {
|
|
8979
|
+
id: string;
|
|
8980
|
+
accountId: string;
|
|
8981
|
+
workspaceId: string;
|
|
8982
|
+
subjectId: string | null;
|
|
8983
|
+
providerDomain: string;
|
|
8984
|
+
kind: string;
|
|
8985
|
+
status: string;
|
|
8986
|
+
grantedScopes: string[];
|
|
8987
|
+
expiresAt: Date | null;
|
|
8988
|
+
lastRefreshAt: Date | null;
|
|
8989
|
+
lastUsedAt: Date | null;
|
|
8990
|
+
lastError: string | null;
|
|
8991
|
+
version: number;
|
|
8992
|
+
metadata: Record<string, unknown>;
|
|
8993
|
+
createdBySubjectId: string | null;
|
|
8994
|
+
updatedBySubjectId: string | null;
|
|
8995
|
+
createdAt: Date;
|
|
8996
|
+
updatedAt: Date;
|
|
8997
|
+
}): ConnectionMetadata {
|
|
8998
|
+
return {
|
|
8999
|
+
id: row.id,
|
|
9000
|
+
accountId: row.accountId,
|
|
9001
|
+
workspaceId: row.workspaceId,
|
|
9002
|
+
subjectId: row.subjectId,
|
|
9003
|
+
providerDomain: row.providerDomain,
|
|
9004
|
+
kind: row.kind as ConnectionKind,
|
|
9005
|
+
status: row.status as ConnectionStatus,
|
|
9006
|
+
grantedScopes: row.grantedScopes,
|
|
9007
|
+
expiresAt: row.expiresAt?.toISOString() ?? null,
|
|
9008
|
+
lastRefreshAt: row.lastRefreshAt?.toISOString() ?? null,
|
|
9009
|
+
lastUsedAt: row.lastUsedAt?.toISOString() ?? null,
|
|
9010
|
+
lastError: row.lastError,
|
|
9011
|
+
version: row.version,
|
|
9012
|
+
metadata: row.metadata,
|
|
9013
|
+
createdBySubjectId: row.createdBySubjectId,
|
|
9014
|
+
updatedBySubjectId: row.updatedBySubjectId,
|
|
9015
|
+
createdAt: row.createdAt.toISOString(),
|
|
9016
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
9017
|
+
};
|
|
9018
|
+
}
|
|
9019
|
+
|
|
9020
|
+
function mapKnowledgeMemory(row: typeof schema.knowledgeMemories.$inferSelect): KnowledgeMemory {
|
|
9021
|
+
return {
|
|
9022
|
+
id: row.id,
|
|
9023
|
+
workspaceId: row.workspaceId,
|
|
9024
|
+
status: row.status as KnowledgeMemoryStatus,
|
|
9025
|
+
kind: row.kind as KnowledgeMemoryKind,
|
|
9026
|
+
scope: row.scope,
|
|
9027
|
+
text: row.text,
|
|
9028
|
+
sourceRefs: Array.isArray(row.sourceRefs) ? row.sourceRefs as KnowledgeSourceRef[] : [],
|
|
9029
|
+
confidence: confidenceFromStorage(row.confidence),
|
|
9030
|
+
metadata: row.metadata,
|
|
9031
|
+
createdBySessionId: row.createdBySessionId,
|
|
9032
|
+
reviewedBy: row.reviewedBy,
|
|
9033
|
+
reviewedAt: row.reviewedAt ? row.reviewedAt.toISOString() : null,
|
|
9034
|
+
createdAt: row.createdAt.toISOString(),
|
|
9035
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
9036
|
+
};
|
|
9037
|
+
}
|
|
9038
|
+
|
|
8038
9039
|
function mapSocialConnection(row: typeof schema.socialConnections.$inferSelect): SocialConnection {
|
|
8039
9040
|
return {
|
|
8040
9041
|
id: row.id,
|
|
@@ -8128,6 +9129,30 @@ function stringArrayConfig(value: unknown): string[] | undefined {
|
|
|
8128
9129
|
return values.length > 0 ? [...new Set(values.map((item) => item.trim()))] : undefined;
|
|
8129
9130
|
}
|
|
8130
9131
|
|
|
9132
|
+
function cleanDbString(value: string | undefined | null): string | undefined {
|
|
9133
|
+
const trimmed = value?.trim();
|
|
9134
|
+
return trimmed ? trimmed : undefined;
|
|
9135
|
+
}
|
|
9136
|
+
|
|
9137
|
+
function requireDbString(value: string, field: string): string {
|
|
9138
|
+
const trimmed = cleanDbString(value);
|
|
9139
|
+
if (!trimmed) {
|
|
9140
|
+
throw new Error(`${field} is required`);
|
|
9141
|
+
}
|
|
9142
|
+
return trimmed;
|
|
9143
|
+
}
|
|
9144
|
+
|
|
9145
|
+
function confidenceToStorage(value: number): number {
|
|
9146
|
+
if (!Number.isFinite(value)) {
|
|
9147
|
+
return 50;
|
|
9148
|
+
}
|
|
9149
|
+
return Math.round(Math.min(Math.max(value, 0), 1) * 100);
|
|
9150
|
+
}
|
|
9151
|
+
|
|
9152
|
+
function confidenceFromStorage(value: number): number {
|
|
9153
|
+
return Number((Math.min(Math.max(value, 0), 100) / 100).toFixed(2));
|
|
9154
|
+
}
|
|
9155
|
+
|
|
8131
9156
|
function positiveIntegerConfig(value: unknown): number | undefined {
|
|
8132
9157
|
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
8133
9158
|
return value;
|
|
@@ -8153,7 +9178,37 @@ function encryptedHeadersConfig(value: unknown): Record<string, string> | undefi
|
|
|
8153
9178
|
|
|
8154
9179
|
function mcpConnectivityOk(metadata: Record<string, unknown>): boolean {
|
|
8155
9180
|
const value = metadata.mcpConnectivity;
|
|
8156
|
-
return !!value && typeof value === "object" && "status" in value && value.status === "ok";
|
|
9181
|
+
return !!value && typeof value === "object" && "status" in value && (value.status === "ok" || value.status === "auth_deferred");
|
|
9182
|
+
}
|
|
9183
|
+
|
|
9184
|
+
function connectionRefConfig(value: unknown): McpServerConnectionRef | undefined {
|
|
9185
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
9186
|
+
return undefined;
|
|
9187
|
+
}
|
|
9188
|
+
const record = value as Record<string, unknown>;
|
|
9189
|
+
if (typeof record.providerDomain !== "string" || record.providerDomain.length === 0) {
|
|
9190
|
+
return undefined;
|
|
9191
|
+
}
|
|
9192
|
+
const ref: McpServerConnectionRef = { providerDomain: record.providerDomain };
|
|
9193
|
+
if (typeof record.connectionId === "string" && record.connectionId.length > 0) {
|
|
9194
|
+
ref.connectionId = record.connectionId;
|
|
9195
|
+
}
|
|
9196
|
+
if (typeof record.kind === "string" && ["oauth2", "api_key", "app_install", "delegated"].includes(record.kind)) {
|
|
9197
|
+
ref.kind = record.kind as ConnectionKind;
|
|
9198
|
+
}
|
|
9199
|
+
if (Array.isArray(record.scopes)) {
|
|
9200
|
+
const scopes = record.scopes.filter((scope): scope is string => typeof scope === "string" && scope.length > 0);
|
|
9201
|
+
if (scopes.length > 0) {
|
|
9202
|
+
ref.scopes = scopes;
|
|
9203
|
+
}
|
|
9204
|
+
}
|
|
9205
|
+
if (typeof record.resource === "string" && record.resource.length > 0) {
|
|
9206
|
+
ref.resource = record.resource;
|
|
9207
|
+
}
|
|
9208
|
+
if (record.subjectScope === "workspace" || record.subjectScope === "subject") {
|
|
9209
|
+
ref.subjectScope = record.subjectScope;
|
|
9210
|
+
}
|
|
9211
|
+
return ref;
|
|
8157
9212
|
}
|
|
8158
9213
|
|
|
8159
9214
|
function shortHash(value: string): string {
|
|
@@ -8171,3 +9226,4 @@ function shortHash(value: string): string {
|
|
|
8171
9226
|
// recordCodexAccountUsage) is already initialized when its default-deps bag
|
|
8172
9227
|
// evaluates under the index↔resolver module cycle.
|
|
8173
9228
|
export * from "./codex-token-resolver";
|
|
9229
|
+
export * from "./connection-token-resolver";
|