@memberjunction/server 6.2.0-edge.4 → 6.2.0-edge.5
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/README.md +20 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/agentSessions/SessionJanitor.d.ts +91 -30
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +311 -81
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts +5 -1
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +13 -1
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/apiRoot.d.ts +32 -0
- package/dist/apiRoot.d.ts.map +1 -0
- package/dist/apiRoot.js +67 -0
- package/dist/apiRoot.js.map +1 -0
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.js +25 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
- package/dist/auth/LoginScreenEngine.d.ts +22 -0
- package/dist/auth/LoginScreenEngine.d.ts.map +1 -0
- package/dist/auth/LoginScreenEngine.js +57 -0
- package/dist/auth/LoginScreenEngine.js.map +1 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.js +3 -2
- package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
- package/dist/config.d.ts +136 -22
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -4
- package/dist/config.js.map +1 -1
- package/dist/coreRoutes.d.ts +29 -0
- package/dist/coreRoutes.d.ts.map +1 -0
- package/dist/coreRoutes.js +142 -0
- package/dist/coreRoutes.js.map +1 -0
- package/dist/generated/graphql-schemas/__mj.d.ts +369 -3
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +1750 -7
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +43 -8
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +59 -19
- package/dist/generic/CacheInvalidationResolver.js.map +1 -1
- package/dist/generic/PushStatusResolver.js +1 -1
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +103 -44
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +264 -305
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +12 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/globalCors.d.ts +33 -0
- package/dist/globalCors.d.ts.map +1 -0
- package/dist/globalCors.js +58 -0
- package/dist/globalCors.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +112 -160
- package/dist/index.js.map +1 -1
- package/dist/multiTenancy/index.d.ts.map +1 -1
- package/dist/multiTenancy/index.js +22 -2
- package/dist/multiTenancy/index.js.map +1 -1
- package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -1
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js +2 -1
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -1
- package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -1
- package/dist/realtimeSessions/RealtimeVerifyRouter.js +2 -1
- package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -1
- package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts +100 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts.map +1 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.js +173 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.js.map +1 -0
- package/dist/realtimeSessions/index.d.ts +2 -0
- package/dist/realtimeSessions/index.d.ts.map +1 -1
- package/dist/realtimeSessions/index.js +2 -0
- package/dist/realtimeSessions/index.js.map +1 -1
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts +30 -0
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -0
- package/dist/realtimeSessions/ownedRealtimeSession.js +47 -0
- package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -0
- package/dist/realtimeSessions/sessionAccess.d.ts +6 -0
- package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
- package/dist/realtimeSessions/sessionAccess.js +7 -3
- package/dist/realtimeSessions/sessionAccess.js.map +1 -1
- package/dist/resolvers/ArtifactFileResolver.d.ts +5 -1
- package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -1
- package/dist/resolvers/ArtifactFileResolver.js +9 -7
- package/dist/resolvers/ArtifactFileResolver.js.map +1 -1
- package/dist/resolvers/ConversationRunEventsResolver.d.ts +8 -4
- package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
- package/dist/resolvers/ConversationRunEventsResolver.js +6 -6
- package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts +24 -3
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/DatasetResolver.js +57 -8
- package/dist/resolvers/DatasetResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +3 -2
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts +18 -3
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +100 -35
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +82 -11
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +279 -101
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +117 -38
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +6 -3
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +4 -2
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +2 -0
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +13 -0
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +2 -8
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +10 -26
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +35 -6
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts +12 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.js +15 -2
- package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +2 -3
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +2 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.js +28 -1
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.d.ts +24 -0
- package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.js +65 -13
- package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts +19 -2
- package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.js +102 -24
- package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
- package/dist/resolvers/agent-run-data-guard.d.ts +31 -0
- package/dist/resolvers/agent-run-data-guard.d.ts.map +1 -0
- package/dist/resolvers/agent-run-data-guard.js +72 -0
- package/dist/resolvers/agent-run-data-guard.js.map +1 -0
- package/dist/rest/RealtimeRecordingHandler.d.ts +49 -0
- package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -0
- package/dist/rest/RealtimeRecordingHandler.js +383 -0
- package/dist/rest/RealtimeRecordingHandler.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts +3 -1
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +21 -12
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/rest/setupRESTEndpoints.d.ts +6 -0
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
- package/dist/rest/setupRESTEndpoints.js +6 -0
- package/dist/rest/setupRESTEndpoints.js.map +1 -1
- package/dist/serverExtensionReservedRoots.d.ts +12 -10
- package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
- package/dist/serverExtensionReservedRoots.js +14 -17
- package/dist/serverExtensionReservedRoots.js.map +1 -1
- package/dist/services/MJStorageBlobStore.d.ts +8 -1
- package/dist/services/MJStorageBlobStore.d.ts.map +1 -1
- package/dist/services/MJStorageBlobStore.js +25 -19
- package/dist/services/MJStorageBlobStore.js.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +2 -2
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +5 -3
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -5
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/TaskGraphAgentRunner.d.ts +14 -2
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +157 -9
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts +54 -2
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +598 -15
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/sharedCache.d.ts.map +1 -1
- package/dist/sharedCache.js +3 -0
- package/dist/sharedCache.js.map +1 -1
- package/package.json +99 -98
- package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
- package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
- package/src/__tests__/FetchEntityVectorsResolver.test.ts +163 -15
- package/src/__tests__/FileResolver.storageAccess.test.ts +562 -0
- package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
- package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
- package/src/__tests__/LoginScreenEngine.test.ts +86 -0
- package/src/__tests__/MJStorageBlobStore.storageGate.test.ts +63 -0
- package/src/__tests__/RecordingAssemblyScheduler.test.ts +178 -0
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +133 -2
- package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
- package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +209 -6
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +5 -2
- package/src/__tests__/RunAIAgentResolver.branch.test.ts +1 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +5 -3
- package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +258 -0
- package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
- package/src/__tests__/SearchKnowledgeStreamResolver.test.ts +161 -0
- package/src/__tests__/SessionJanitor.test.ts +365 -10
- package/src/__tests__/SessionManager.test.ts +27 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +25 -0
- package/src/__tests__/TaskGraphAgentRunner.test.ts +266 -0
- package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +356 -22
- package/src/__tests__/TaskGraphFrameResolver.test.ts +22 -0
- package/src/__tests__/TestQuerySQLResolver.test.ts +274 -0
- package/src/__tests__/TransactionGroupResolver.refusals.test.ts +98 -1
- package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
- package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
- package/src/__tests__/agent-run-data-guard.test.ts +130 -0
- package/src/__tests__/apiRootBoot.test.ts +202 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
- package/src/__tests__/conversationRunEvents.test.ts +56 -14
- package/src/__tests__/globalCors.test.ts +217 -0
- package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
- package/src/__tests__/multiTenancy.security.test.ts +51 -0
- package/src/__tests__/multiTenancy.test.ts +1 -1
- package/src/__tests__/ownedRealtimeSession.test.ts +39 -0
- package/src/__tests__/pushStatusResolver.filter.test.ts +7 -0
- package/src/__tests__/realtimeRecordingHandler.test.ts +652 -0
- package/src/__tests__/serverExtensionHostOverlay.test.ts +66 -0
- package/src/__tests__/serverExtensionReservedRoots.test.ts +11 -98
- package/src/__tests__/sharedCache.test.ts +5 -3
- package/src/agentSessions/SessionJanitor.ts +369 -93
- package/src/agentSessions/SessionManager.ts +13 -1
- package/src/apiRoot.ts +78 -0
- package/src/auth/AuthProviderCatalogRouter.ts +26 -0
- package/src/auth/LoginScreenEngine.ts +50 -0
- package/src/auth/index.ts +1 -0
- package/src/auth/magicLink/MagicLinkRouter.ts +3 -2
- package/src/config.ts +46 -5
- package/src/coreRoutes.ts +166 -0
- package/src/generated/graphql-schemas/__mj.ts +1273 -10
- package/src/generic/CacheInvalidationResolver.ts +73 -18
- package/src/generic/PushStatusResolver.ts +1 -1
- package/src/generic/ResolverBase.ts +304 -325
- package/src/generic/RunViewResolver.ts +17 -0
- package/src/globalCors.ts +71 -0
- package/src/index.ts +116 -174
- package/src/multiTenancy/index.ts +24 -2
- package/src/realtimeSessions/RealtimeSessionVerificationService.ts +2 -1
- package/src/realtimeSessions/RealtimeVerifyRouter.ts +2 -1
- package/src/realtimeSessions/RecordingAssemblyScheduler.ts +234 -0
- package/src/realtimeSessions/index.ts +2 -0
- package/src/realtimeSessions/ownedRealtimeSession.ts +55 -0
- package/src/realtimeSessions/sessionAccess.ts +7 -3
- package/src/resolvers/ArtifactFileResolver.ts +9 -8
- package/src/resolvers/ConversationRunEventsResolver.ts +15 -11
- package/src/resolvers/DatasetResolver.ts +70 -10
- package/src/resolvers/EntityRecordNameResolver.ts +3 -2
- package/src/resolvers/FetchEntityVectorsResolver.ts +121 -39
- package/src/resolvers/FileResolver.ts +321 -106
- package/src/resolvers/GetDataContextDataResolver.ts +139 -41
- package/src/resolvers/GetDataResolver.ts +6 -3
- package/src/resolvers/IntegrationDiscoveryResolver.ts +4 -2
- package/src/resolvers/QueryResolver.ts +11 -0
- package/src/resolvers/RealtimeClientSessionResolver.ts +10 -33
- package/src/resolvers/RunAIAgentResolver.ts +39 -4
- package/src/resolvers/SearchKnowledgeStreamResolver.ts +19 -3
- package/src/resolvers/SqlLoggingConfigResolver.ts +2 -4
- package/src/resolvers/TaskGraphFrameResolver.ts +33 -1
- package/src/resolvers/TestQuerySQLResolver.ts +68 -13
- package/src/resolvers/TransactionGroupResolver.ts +131 -27
- package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
- package/src/resolvers/agent-run-data-guard.ts +89 -0
- package/src/rest/RealtimeRecordingHandler.ts +464 -0
- package/src/rest/ViewOperationsHandler.ts +29 -13
- package/src/rest/setupRESTEndpoints.ts +6 -0
- package/src/serverExtensionReservedRoots.ts +14 -17
- package/src/services/MJStorageBlobStore.ts +31 -30
- package/src/services/StartTaskGraphDispatcher.ts +2 -2
- package/src/services/TaskGraphActionRunner.ts +25 -5
- package/src/services/TaskGraphAgentRunner.ts +188 -11
- package/src/services/TaskGraphContinuationDeliverer.ts +704 -14
- package/src/sharedCache.ts +3 -0
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { Arg, Ctx, Field, ObjectType, Query, Resolver } from "type-graphql";
|
|
2
|
+
import type { DatabaseProviderBase, UserInfo } from "@memberjunction/core";
|
|
3
|
+
import { IsValidUUID, UUIDsEqual } from "@memberjunction/global";
|
|
4
|
+
import { DataContext, DataContextItem } from "@memberjunction/data-context";
|
|
5
|
+
import { MJDataContextEntity, MJDataContextItemEntity } from "@memberjunction/core-entities";
|
|
6
|
+
import { RenderPipeline } from "@memberjunction/generic-database-provider";
|
|
2
7
|
import { AppContext } from "../types.js";
|
|
3
|
-
import {
|
|
4
|
-
import { GetReadOnlyDataSource, GetReadOnlyProvider } from "../util.js";
|
|
5
|
-
import { MJDataContextItemEntity } from "@memberjunction/core-entities";
|
|
8
|
+
import { GetReadOnlyProvider } from "../util.js";
|
|
6
9
|
import { ResolverBase } from "../generic/ResolverBase.js";
|
|
10
|
+
import { IsScopeLimitedPrincipal } from "../auth/scopeLimitedPrincipal.js";
|
|
7
11
|
|
|
8
12
|
@ObjectType()
|
|
9
13
|
export class GetDataContextItemDataOutputType {
|
|
@@ -39,6 +43,28 @@ export class GetDataContextDataOutputType {
|
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
|
|
46
|
+
/** The outcome of loading one data context item: its data as JSON, or why it has none. */
|
|
47
|
+
interface ItemOutcome {
|
|
48
|
+
Result: string | null;
|
|
49
|
+
Error: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Which `sql` items of a data context may run: the read-only provider, and each refused item's reason. */
|
|
53
|
+
interface SQLItemGate {
|
|
54
|
+
Provider: DatabaseProviderBase | null;
|
|
55
|
+
Refusals: Map<DataContextItem, string>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Loads the data of data contexts and their items.
|
|
60
|
+
*
|
|
61
|
+
* Items of type `sql` hold raw SQL, which is held to the rules ExecuteAdhocQuery applies to
|
|
62
|
+
* caller-supplied SQL. It runs only for the data context's owner or an administrator (Owner-type
|
|
63
|
+
* user), never for a scope-limited session, only on the read-only provider, and only as a single read
|
|
64
|
+
* statement over entity base views the caller may read in full. A SQL item that may not run is
|
|
65
|
+
* refused on its own, and the other items still load. Items of the other types load through entity
|
|
66
|
+
* permissions for the calling user.
|
|
67
|
+
*/
|
|
42
68
|
@Resolver()
|
|
43
69
|
export class GetDataContextDataResolver extends ResolverBase {
|
|
44
70
|
/**
|
|
@@ -52,30 +78,25 @@ export class GetDataContextDataResolver extends ResolverBase {
|
|
|
52
78
|
) {
|
|
53
79
|
// Check API key scope authorization for data context read
|
|
54
80
|
await this.CheckAPIKeyScopeAuthorization('datacontext:read', DataContextItemID, appCtx.userPayload);
|
|
81
|
+
if (!IsValidUUID(DataContextItemID)) {
|
|
82
|
+
return { Success: false, ErrorMessage: 'DataContextItemID must be a UUID', Result: null };
|
|
83
|
+
}
|
|
55
84
|
|
|
56
85
|
try {
|
|
57
|
-
const
|
|
58
|
-
allowFallbackToReadWrite: true,
|
|
59
|
-
})
|
|
86
|
+
const user: UserInfo = appCtx.userPayload.userRecord;
|
|
60
87
|
const md = GetReadOnlyProvider(appCtx.providers, {allowFallbackToReadWrite: true});
|
|
61
|
-
const dciData = await md.GetEntityObject<MJDataContextItemEntity>("MJ: Data Context Items",
|
|
88
|
+
const dciData = await md.GetEntityObject<MJDataContextItemEntity>("MJ: Data Context Items", user);
|
|
62
89
|
if (await dciData.Load(DataContextItemID)) {
|
|
63
90
|
const dci = DataContext.CreateDataContextItem(); // use class factory to get whatever lowest level sub-class is registered
|
|
64
|
-
await dci.LoadMetadataFromEntityRecord(dciData, md,
|
|
91
|
+
await dci.LoadMetadataFromEntityRecord(dciData, md, user);
|
|
92
|
+
const ownerID = dci.Type === 'sql' ? await this.loadDataContextOwnerID(dciData.DataContextID, md, user) : null;
|
|
93
|
+
const gate = this.authorizeSQLItems([dci], ownerID, appCtx);
|
|
65
94
|
// now the metadata is loaded so we can call the regular load function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
return {
|
|
75
|
-
Success: false,
|
|
76
|
-
ErrorMessage: 'Error loading data context item data',
|
|
77
|
-
Result: null,
|
|
78
|
-
}
|
|
95
|
+
const outcome = await this.loadItem(dci, gate, user, false);
|
|
96
|
+
return {
|
|
97
|
+
Success: outcome.Error === null,
|
|
98
|
+
ErrorMessage: outcome.Error,
|
|
99
|
+
Result: outcome.Result,
|
|
79
100
|
}
|
|
80
101
|
}
|
|
81
102
|
else {
|
|
@@ -89,14 +110,15 @@ export class GetDataContextDataResolver extends ResolverBase {
|
|
|
89
110
|
catch (e) {
|
|
90
111
|
return {
|
|
91
112
|
Success: false,
|
|
92
|
-
ErrorMessage: e,
|
|
113
|
+
ErrorMessage: e instanceof Error ? e.message : String(e),
|
|
93
114
|
Result: null,
|
|
94
115
|
}
|
|
95
116
|
}
|
|
96
117
|
}
|
|
97
118
|
|
|
98
119
|
/**
|
|
99
|
-
* Returns data for a given data context.
|
|
120
|
+
* Returns data for a given data context. `Results` and `ErrorMessages` hold one entry per item, in
|
|
121
|
+
* item order. `Success` is true, and `ErrorMessages` null, only when every item loaded.
|
|
100
122
|
* @param DataContextID
|
|
101
123
|
*/
|
|
102
124
|
@Query(() => GetDataContextDataOutputType)
|
|
@@ -106,38 +128,114 @@ export class GetDataContextDataResolver extends ResolverBase {
|
|
|
106
128
|
) {
|
|
107
129
|
// Check API key scope authorization for data context read
|
|
108
130
|
await this.CheckAPIKeyScopeAuthorization('datacontext:read', DataContextID, appCtx.userPayload);
|
|
131
|
+
// DataContext.LoadMetadata places this ID in the item filter, so it must be a plain UUID.
|
|
132
|
+
if (!IsValidUUID(DataContextID)) {
|
|
133
|
+
return { Success: false, ErrorMessages: ['DataContextID must be a UUID'], Results: null };
|
|
134
|
+
}
|
|
109
135
|
|
|
110
136
|
try {
|
|
111
137
|
// our job here is to load the entire data context, so we do that with the Data Context object
|
|
138
|
+
const user: UserInfo = appCtx.userPayload.userRecord;
|
|
139
|
+
const md = GetReadOnlyProvider(appCtx.providers, { allowFallbackToReadWrite: true });
|
|
112
140
|
const dc = new DataContext();
|
|
113
|
-
|
|
114
|
-
allowFallbackToReadWrite: true,
|
|
115
|
-
})
|
|
116
|
-
const success = await dc.Load(DataContextID, ds, true, false, 0, appCtx.userPayload.userRecord);
|
|
117
|
-
if (success) {
|
|
118
|
-
const retVal = {
|
|
119
|
-
Success: true,
|
|
120
|
-
ErrorMessages: null,
|
|
121
|
-
Results: dc.Items.map((item) => {
|
|
122
|
-
return JSON.stringify(item.Data);
|
|
123
|
-
}),
|
|
124
|
-
}
|
|
125
|
-
return retVal;
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
141
|
+
if (!(await dc.LoadMetadata(DataContextID, user, md))) {
|
|
128
142
|
return {
|
|
129
143
|
Success: false,
|
|
130
144
|
ErrorMessages: ['Error loading data context'],
|
|
131
|
-
Results: null,
|
|
145
|
+
Results: null,
|
|
132
146
|
}
|
|
133
147
|
}
|
|
148
|
+
const gate = this.authorizeSQLItems(dc.Items, dc.MJDataContextEntity.UserID, appCtx);
|
|
149
|
+
const outcomes = await Promise.all(dc.Items.map((item) => this.loadItem(item, gate, user, true)));
|
|
150
|
+
const allLoaded = outcomes.every((outcome) => outcome.Error === null);
|
|
151
|
+
return {
|
|
152
|
+
Success: allLoaded,
|
|
153
|
+
ErrorMessages: allLoaded ? null : outcomes.map((outcome) => outcome.Error),
|
|
154
|
+
Results: outcomes.map((outcome) => outcome.Result),
|
|
155
|
+
}
|
|
134
156
|
}
|
|
135
157
|
catch (e) {
|
|
136
158
|
return {
|
|
137
159
|
Success: false,
|
|
138
|
-
ErrorMessages: [e],
|
|
160
|
+
ErrorMessages: [e instanceof Error ? e.message : String(e)],
|
|
139
161
|
Results: null,
|
|
140
162
|
}
|
|
141
163
|
}
|
|
142
164
|
}
|
|
143
|
-
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Decides which `sql` items among `items` may run. Returns the read-only provider they run on, and
|
|
168
|
+
* the reason for each SQL item that may not run.
|
|
169
|
+
*/
|
|
170
|
+
private authorizeSQLItems(items: DataContextItem[], ownerID: string | null, appCtx: AppContext): SQLItemGate {
|
|
171
|
+
const refusals = new Map<DataContextItem, string>();
|
|
172
|
+
const sqlItems = items.filter((item) => item.Type === 'sql');
|
|
173
|
+
if (sqlItems.length === 0) {
|
|
174
|
+
return { Provider: null, Refusals: refusals };
|
|
175
|
+
}
|
|
176
|
+
const user: UserInfo | undefined = appCtx.userPayload?.userRecord;
|
|
177
|
+
const provider = GetReadOnlyProvider(appCtx.providers, { allowFallbackToReadWrite: false });
|
|
178
|
+
for (const item of sqlItems) {
|
|
179
|
+
const refusal = this.sqlItemRefusal(item, ownerID, user, provider);
|
|
180
|
+
if (refusal !== null) {
|
|
181
|
+
refusals.set(item, refusal);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return { Provider: provider, Refusals: refusals };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Returns why `item` may not run for `user`, or null when it may. */
|
|
188
|
+
private sqlItemRefusal(item: DataContextItem, ownerID: string | null, user: UserInfo | undefined, provider: DatabaseProviderBase | null): string | null {
|
|
189
|
+
if (!user || IsScopeLimitedPrincipal(user)) {
|
|
190
|
+
return 'SQL data context items cannot be run by a scope-limited or unidentified session.';
|
|
191
|
+
}
|
|
192
|
+
if (!this.mayRunDataContextSQL(ownerID, user)) {
|
|
193
|
+
return 'Only the owner of this data context, or an administrator, may run its SQL items.';
|
|
194
|
+
}
|
|
195
|
+
if (!provider) {
|
|
196
|
+
return 'No read-only data source is configured, so SQL data context items cannot run.';
|
|
197
|
+
}
|
|
198
|
+
return this.screenItemSQL(item.SQL ?? '', provider, user);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** True when `user` owns the data context or is an administrator (Owner-type user). */
|
|
202
|
+
private mayRunDataContextSQL(ownerID: string | null, user: UserInfo): boolean {
|
|
203
|
+
const isOwner = !!ownerID && UUIDsEqual(ownerID, user.ID);
|
|
204
|
+
return isOwner || user.Type?.trim().toLowerCase() === 'owner';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Returns why `sqlText` may not run, or null when it is a single read statement whose every table
|
|
209
|
+
* reference is an entity base view `user` may read without row-level or field-level narrowing. It is
|
|
210
|
+
* the check ExecuteAdhocQuery makes on the rendered SQL.
|
|
211
|
+
*/
|
|
212
|
+
private screenItemSQL(sqlText: string, provider: DatabaseProviderBase, user: UserInfo): string | null {
|
|
213
|
+
try {
|
|
214
|
+
const rendered = RenderPipeline.Run(sqlText, { Platform: provider.PlatformKey, ContextUser: user, RequireReadStatement: true });
|
|
215
|
+
this.assertFullQueryUsesReadableEntityViews(rendered.Trace.AfterTemplates, provider, user, 'data context SQL');
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
220
|
+
return reason || 'The SQL of this item did not pass the read-only check.';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Loads one item's data unless `gate` refused it. Returns the data as JSON, or why there is none. */
|
|
225
|
+
private async loadItem(item: DataContextItem, gate: SQLItemGate, user: UserInfo, forceRefresh: boolean): Promise<ItemOutcome> {
|
|
226
|
+
const refusal = gate.Refusals.get(item);
|
|
227
|
+
if (refusal !== undefined) {
|
|
228
|
+
return { Result: null, Error: refusal };
|
|
229
|
+
}
|
|
230
|
+
if (await item.LoadData(gate.Provider, forceRefresh, false, 0, user)) {
|
|
231
|
+
return { Result: JSON.stringify(item.Data), Error: null };
|
|
232
|
+
}
|
|
233
|
+
return { Result: null, Error: item.DataLoadingError || 'Error loading data context item data' };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Returns the owner (`UserID`) of a data context, or null when the caller cannot load it. */
|
|
237
|
+
private async loadDataContextOwnerID(dataContextID: string, provider: DatabaseProviderBase, user: UserInfo): Promise<string | null> {
|
|
238
|
+
const dataContext = await provider.GetEntityObject<MJDataContextEntity>('MJ: Data Contexts', user);
|
|
239
|
+
return (await dataContext.Load(dataContextID)) ? dataContext.UserID : null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -158,14 +158,17 @@ export class GetDataResolver {
|
|
|
158
158
|
// Defense-in-depth: even system-token holders get SELECT-only SQL here.
|
|
159
159
|
// The read-only pool is the primary control, but if it is ever
|
|
160
160
|
// misconfigured to a read-write login this validator still refuses DML/DDL
|
|
161
|
-
// (mirrors AdhocQueryResolver).
|
|
162
|
-
|
|
161
|
+
// (mirrors AdhocQueryResolver). The validator reads exactly the text that
|
|
162
|
+
// runs: it is trimmed here as validate() trims it, and literal \n sequences
|
|
163
|
+
// are not rewritten as ValidateFullQuery would.
|
|
164
|
+
const sqlToRun = resolvedSQL.trim();
|
|
165
|
+
const validation = SQLExpressionValidator.Instance.validate(sqlToRun, { context: 'full_query', Dialect: platform });
|
|
163
166
|
if (!validation.valid) {
|
|
164
167
|
return { result: null, error: validation.error || 'SQL validation failed' };
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
const request = new sql.Request(readOnlyDataSource);
|
|
168
|
-
const result = await request.query(
|
|
171
|
+
const result = await request.query(sqlToRun);
|
|
169
172
|
return { result: result.recordset, error: null };
|
|
170
173
|
} catch (err) {
|
|
171
174
|
// Extract clean SQL error message
|
|
@@ -3,7 +3,7 @@ import { CompositeKey, DatabaseProviderBase, EntityInfo, LocalCacheManager, Meta
|
|
|
3
3
|
import { GetReadOnlyProvider, GetReadWriteProvider } from "../util.js";
|
|
4
4
|
import { NoLog } from "../logging/NoLog.js";
|
|
5
5
|
import { UUIDsEqual } from "@memberjunction/global";
|
|
6
|
-
import { DrainResponseBody } from "@memberjunction/network-utils";
|
|
6
|
+
import { DrainResponseBody, SafeFetch } from "@memberjunction/network-utils";
|
|
7
7
|
import { CronExpressionHelper } from "@memberjunction/scheduling-engine";
|
|
8
8
|
import {
|
|
9
9
|
MJCompanyIntegrationEntity,
|
|
@@ -6467,7 +6467,9 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
6467
6467
|
|
|
6468
6468
|
private async sendWebhook(url: string, payload: Record<string, unknown>): Promise<void> {
|
|
6469
6469
|
try {
|
|
6470
|
-
|
|
6470
|
+
// SECURITY: the webhook URL is caller-supplied, so go through the SSRF guard —
|
|
6471
|
+
// SafeFetch validates the URL (and every redirect hop) resolves to a public address.
|
|
6472
|
+
const response = await SafeFetch(url, {
|
|
6471
6473
|
method: 'POST',
|
|
6472
6474
|
headers: { 'Content-Type': 'application/json' },
|
|
6473
6475
|
body: JSON.stringify(payload)
|
|
@@ -101,6 +101,12 @@ export class RunQueryCacheStatusInput {
|
|
|
101
101
|
|
|
102
102
|
@Field(() => Int, { description: 'The number of rows in cached results' })
|
|
103
103
|
rowCount: number; // case-violation-ok-legacy-back-compat: the property name is the GraphQL schema field name — renaming it breaks every client query
|
|
104
|
+
|
|
105
|
+
@Field(() => String, {
|
|
106
|
+
nullable: true,
|
|
107
|
+
description: "The query definition hash the cached results were written under, as this server returned it. When it differs from the server's, the result is 'stale' with fresh rows. Empty string for a slot with no hash; omit to skip the definition check."
|
|
108
|
+
})
|
|
109
|
+
definitionHash?: string; // case-violation-ok-legacy-back-compat: the property name is the GraphQL schema field name, matching its siblings
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
@InputType()
|
|
@@ -140,6 +146,9 @@ export class RunQueryWithCacheCheckResultOutput {
|
|
|
140
146
|
|
|
141
147
|
@Field(() => String, { nullable: true, description: 'Error message if status is error' })
|
|
142
148
|
errorMessage?: string; // case-violation-ok-legacy-back-compat: the property name is the GraphQL schema field name — renaming it breaks every client query
|
|
149
|
+
|
|
150
|
+
@Field(() => String, { nullable: true, description: "The server's definition hash for the query; store it with the cached results and send it back in cacheStatus.definitionHash" })
|
|
151
|
+
definitionHash?: string; // case-violation-ok-legacy-back-compat: the property name is the GraphQL schema field name, matching its siblings
|
|
143
152
|
}
|
|
144
153
|
|
|
145
154
|
@ObjectType()
|
|
@@ -558,6 +567,7 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
558
567
|
cacheStatus: item.cacheStatus ? {
|
|
559
568
|
maxUpdatedAt: item.cacheStatus.maxUpdatedAt,
|
|
560
569
|
rowCount: item.cacheStatus.rowCount,
|
|
570
|
+
definitionHash: item.cacheStatus.definitionHash ?? undefined,
|
|
561
571
|
} : undefined,
|
|
562
572
|
}));
|
|
563
573
|
|
|
@@ -572,6 +582,7 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
572
582
|
maxUpdatedAt: result.maxUpdatedAt,
|
|
573
583
|
rowCount: result.rowCount,
|
|
574
584
|
errorMessage: result.errorMessage,
|
|
585
|
+
definitionHash: result.definitionHash,
|
|
575
586
|
}));
|
|
576
587
|
|
|
577
588
|
return {
|
|
@@ -87,6 +87,7 @@ import { ResolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser, ResolveRec
|
|
|
87
87
|
import { RunWithTrustedSessionConfigWrites } from '@memberjunction/core-entities-server';
|
|
88
88
|
import { BuildMintConfigStamps, LoadInheritedVerifiedIdentity } from '../realtimeSessions/sessionConfigStamps.js';
|
|
89
89
|
import { ParseConfigObject } from '../realtimeSessions/verificationCore.js';
|
|
90
|
+
import { LoadOwnedRealtimeSession } from '../realtimeSessions/ownedRealtimeSession.js';
|
|
90
91
|
|
|
91
92
|
/**
|
|
92
93
|
* Progress steps worth narrating to the realtime model — mirrors the normal agent-run path's filter
|
|
@@ -415,7 +416,7 @@ export class CancelRealtimeSessionToolResult {
|
|
|
415
416
|
* Result of {@link RealtimeClientSessionResolver.UploadRealtimeRecording} — a structured
|
|
416
417
|
* success/failure envelope. A recording-storage failure must never throw to the browser (the
|
|
417
418
|
* session it belongs to has already ended), so problems come back as `Success: false` with a
|
|
418
|
-
* human-readable reason; only ownership/authn violations (from `
|
|
419
|
+
* human-readable reason; only ownership/authn violations (from `LoadOwnedRealtimeSession`) still throw.
|
|
419
420
|
*/
|
|
420
421
|
@ObjectType()
|
|
421
422
|
export class UploadRealtimeRecordingResult {
|
|
@@ -944,7 +945,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
944
945
|
* the call and uploads the assembled blob (base64) once the session ends.
|
|
945
946
|
*
|
|
946
947
|
* Hard gates (in order):
|
|
947
|
-
* 1. Ownership — `
|
|
948
|
+
* 1. Ownership — `LoadOwnedRealtimeSession` enforces `UserID === contextUser.ID` (throws on violation).
|
|
948
949
|
* 2. Consent — `consent !== true` is a hard refusal: no audio is ever stored without it.
|
|
949
950
|
* 3. Storage configuration — the session's agent must resolve a recording storage account (the
|
|
950
951
|
* agent's `RecordingStorageProviderID`, else `AttachmentStorageProviderID`).
|
|
@@ -973,7 +974,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
973
974
|
@Arg('peaks', () => [Float], { nullable: true }) peaks?: number[],
|
|
974
975
|
): Promise<UploadRealtimeRecordingResult> {
|
|
975
976
|
const { contextUser, provider } = this.requireUserAndProvider(ctx.userPayload, ctx.providers);
|
|
976
|
-
const session = await
|
|
977
|
+
const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
|
|
977
978
|
|
|
978
979
|
// HARD consent gate — no audio is ever stored without an explicit grant.
|
|
979
980
|
if (consent !== true) {
|
|
@@ -1060,7 +1061,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1060
1061
|
): Promise<boolean> {
|
|
1061
1062
|
try {
|
|
1062
1063
|
const { contextUser, provider } = this.requireUserAndProvider(ctx.userPayload, ctx.providers);
|
|
1063
|
-
const session = await
|
|
1064
|
+
const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
|
|
1064
1065
|
// The consolidated recording already landed and its upload deleted the shards; a shard
|
|
1065
1066
|
// arriving now (one still on the wire at hang-up) would be an orphan nothing deletes.
|
|
1066
1067
|
if (session.RecordingFileID) {
|
|
@@ -1183,7 +1184,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1183
1184
|
@Arg('callId', () => String, { nullable: true }) callId?: string,
|
|
1184
1185
|
): Promise<CancelRealtimeSessionToolResult> {
|
|
1185
1186
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
1186
|
-
await
|
|
1187
|
+
await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
|
|
1187
1188
|
try {
|
|
1188
1189
|
const aborted = this.clientSessionService.CancelInFlightDelegations(agentSessionId, callId);
|
|
1189
1190
|
return { AbortedCount: aborted, Success: true };
|
|
@@ -1230,7 +1231,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1230
1231
|
@Arg('usageDetailsJson', () => String, { nullable: true }) usageDetailsJson?: string,
|
|
1231
1232
|
): Promise<boolean> {
|
|
1232
1233
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
1233
|
-
const session = await
|
|
1234
|
+
const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
|
|
1234
1235
|
|
|
1235
1236
|
const inputDelta = this.clampTokenDelta(inputTokens);
|
|
1236
1237
|
const outputDelta = this.clampTokenDelta(outputTokens);
|
|
@@ -1358,7 +1359,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1358
1359
|
@Ctx() { userPayload, providers }: AppContext,
|
|
1359
1360
|
): Promise<boolean> {
|
|
1360
1361
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
1361
|
-
const session = await
|
|
1362
|
+
const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
|
|
1362
1363
|
|
|
1363
1364
|
if (!this.isChannelInSessionScope(session, channelName)) {
|
|
1364
1365
|
LogError(`SaveSessionChannelState: channel '${channelName}' is not in session ${agentSessionId}'s resolved scope — state not saved.`);
|
|
@@ -1441,7 +1442,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1441
1442
|
@Ctx() { userPayload, providers }: AppContext,
|
|
1442
1443
|
): Promise<SaveSessionChannelArtifactResult> {
|
|
1443
1444
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
1444
|
-
const session = await
|
|
1445
|
+
const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
|
|
1445
1446
|
|
|
1446
1447
|
if (!this.isChannelInSessionScope(session, channelName)) {
|
|
1447
1448
|
const message = `SaveSessionChannelArtifact: channel '${channelName}' is not in session ${agentSessionId}'s resolved scope — nothing saved.`;
|
|
@@ -1954,37 +1955,13 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1954
1955
|
contextUser: UserInfo,
|
|
1955
1956
|
provider: IMetadataProvider,
|
|
1956
1957
|
): Promise<MJAIAgentSessionEntity> {
|
|
1957
|
-
const session = await
|
|
1958
|
+
const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
|
|
1958
1959
|
if (session.Status === 'Closed') {
|
|
1959
1960
|
throw new Error(`Realtime session ${agentSessionId} is closed`);
|
|
1960
1961
|
}
|
|
1961
1962
|
return session;
|
|
1962
1963
|
}
|
|
1963
1964
|
|
|
1964
|
-
/**
|
|
1965
|
-
* Loads a session and enforces inbound ownership (`UserID === contextUser.ID`) WITHOUT
|
|
1966
|
-
* rejecting `Closed` sessions — {@link SaveSessionChannelState}'s final on-end flush
|
|
1967
|
-
* legitimately arrives after the session closed. Throws when not found / not owned.
|
|
1968
|
-
*/
|
|
1969
|
-
private async loadOwnedSession(
|
|
1970
|
-
agentSessionId: string,
|
|
1971
|
-
contextUser: UserInfo,
|
|
1972
|
-
provider: IMetadataProvider,
|
|
1973
|
-
): Promise<MJAIAgentSessionEntity> {
|
|
1974
|
-
const session = await provider.GetEntityObject<MJAIAgentSessionEntity>(SESSION_ENTITY, contextUser);
|
|
1975
|
-
const loaded = await session.Load(agentSessionId);
|
|
1976
|
-
if (!loaded) {
|
|
1977
|
-
throw new Error(`Realtime session ${agentSessionId} not found`);
|
|
1978
|
-
}
|
|
1979
|
-
if (!UUIDsEqual(session.UserID, contextUser.ID)) {
|
|
1980
|
-
LogError(
|
|
1981
|
-
`RealtimeClientSessionResolver: ownership check failed — user ${contextUser.ID} attempted to operate on session ${agentSessionId} owned by ${session.UserID}`,
|
|
1982
|
-
);
|
|
1983
|
-
throw new Error('Not authorized: you do not own this realtime session');
|
|
1984
|
-
}
|
|
1985
|
-
return session;
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
1965
|
/**
|
|
1989
1966
|
* Resolves an ACTIVE channel definition (`MJ: AI Agent Channels`) by name from
|
|
1990
1967
|
* {@link AIEngineBase}'s cached `AgentChannels` (provider-scoped engine instance, lazy
|
|
@@ -13,6 +13,7 @@ import { StartLivenessPulse } from '../generic/FireAndForgetHeartbeat.js';
|
|
|
13
13
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
14
14
|
import { GetReadWriteProvider } from '../util.js';
|
|
15
15
|
import { ResolveWidgetGuestRunContext, ElevateUserPayload } from '../realtimeWidget/widgetGuestElevation.js';
|
|
16
|
+
import { IsTrustedAgentRunCaller } from './agent-run-data-guard.js';
|
|
16
17
|
import { SafeJSONParse, UUIDsEqual } from '@memberjunction/global';
|
|
17
18
|
import { GetAttachmentService } from '@memberjunction/aiengine';
|
|
18
19
|
import { NotificationEngine } from '@memberjunction/notifications';
|
|
@@ -252,6 +253,14 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
252
253
|
throw new Error(`AI Agent "${agentEntity.Name}" is not active (Status: ${agentEntity.Status})`);
|
|
253
254
|
}
|
|
254
255
|
|
|
256
|
+
// Enforce agent run permissions for the calling user
|
|
257
|
+
if (currentUser && typeof AIEngine.Instance.CanUserRunAgent === 'function') {
|
|
258
|
+
const canRun = await AIEngine.Instance.CanUserRunAgent(agentEntity.ID, currentUser);
|
|
259
|
+
if (!canRun) {
|
|
260
|
+
throw new Error(`User does not have permission to run AI Agent "${agentEntity.Name}" (${agentEntity.ID})`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
255
264
|
return agentEntity;
|
|
256
265
|
}
|
|
257
266
|
|
|
@@ -294,6 +303,8 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
294
303
|
/** History floor for a conversation run — threaded into ExecuteAgentParams.ConversationHistoryFrom,
|
|
295
304
|
* so the framework's own conversation reads (retrieval tools, artifacts, compaction) honour it too. */
|
|
296
305
|
conversationHistoryFrom?: Date,
|
|
306
|
+
/** ExecuteAgentParams.TrustReservedRunData: true only for a caller IsTrustedAgentRunCaller accepts. */
|
|
307
|
+
trustReservedRunData: boolean = false,
|
|
297
308
|
/** The conversation branch of the run — threaded into ExecuteAgentParams.ConversationBranchID. */
|
|
298
309
|
conversationBranchId?: string | null
|
|
299
310
|
): Promise<AIAgentRunResult> {
|
|
@@ -366,6 +377,8 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
366
377
|
ConversationHistoryFrom: conversationHistoryFrom,
|
|
367
378
|
...(conversationBranchId ? { ConversationBranchID: conversationBranchId } : {}),
|
|
368
379
|
data: parsedData,
|
|
380
|
+
// Whether the run may read the reserved data keys (scope, agent-type params); BaseAgent drops them otherwise.
|
|
381
|
+
TrustReservedRunData: trustReservedRunData,
|
|
369
382
|
context: {
|
|
370
383
|
dataSource: dataSource
|
|
371
384
|
}
|
|
@@ -564,6 +577,9 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
564
577
|
await this.CheckAPIKeyScopeAuthorization('agent:execute', agentId, userPayload);
|
|
565
578
|
|
|
566
579
|
const p = GetReadWriteProvider(providers);
|
|
580
|
+
// Only the system user or an API key holding the agent:trusted scope may set a run's scope and agent-type
|
|
581
|
+
// params through data; for anyone else BaseAgent drops those keys (ExecuteAgentParams.TrustReservedRunData).
|
|
582
|
+
const trustRunData = await IsTrustedAgentRunCaller({ UserPayload: userPayload, IsWidgetGuestRun: false, AgentID: agentId });
|
|
567
583
|
|
|
568
584
|
if (fireAndForget) {
|
|
569
585
|
// Fire-and-forget mode: start execution in background, return immediately.
|
|
@@ -572,7 +588,8 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
572
588
|
p, dataSource, agentId, userPayload, messagesJson, sessionId, pubSub,
|
|
573
589
|
data, payload, lastRunId, autoPopulateLastRunPayload, configurationId,
|
|
574
590
|
conversationDetailId, createArtifacts || false, createNotification || false,
|
|
575
|
-
sourceArtifactId, sourceArtifactVersionId, undefined /*conversationId*/, planMode, requestedSkillIDs, taskGraphDebug
|
|
591
|
+
sourceArtifactId, sourceArtifactVersionId, undefined /*conversationId*/, planMode, requestedSkillIDs, taskGraphDebug,
|
|
592
|
+
undefined /*conversationHistoryFrom*/, trustRunData
|
|
576
593
|
);
|
|
577
594
|
|
|
578
595
|
LogStatus(`🔥 Fire-and-forget: Agent ${agentId} execution started in background for session ${sessionId}`);
|
|
@@ -607,7 +624,9 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
607
624
|
undefined, // runRef
|
|
608
625
|
planMode,
|
|
609
626
|
requestedSkillIDs,
|
|
610
|
-
taskGraphDebug
|
|
627
|
+
taskGraphDebug,
|
|
628
|
+
undefined, // conversationHistoryFrom
|
|
629
|
+
trustRunData
|
|
611
630
|
);
|
|
612
631
|
}
|
|
613
632
|
|
|
@@ -640,6 +659,9 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
640
659
|
@Arg('requestedSkillIDs', () => [String], { nullable: true }) requestedSkillIDs?: string[]
|
|
641
660
|
): Promise<AIAgentRunResult> {
|
|
642
661
|
const p = GetReadWriteProvider(providers);
|
|
662
|
+
// @RequireSystemUser admits only the system user, whom the rule trusts; it is still asked, so every
|
|
663
|
+
// resolver entry that takes a client's data decides the same way.
|
|
664
|
+
const trustRunData = await IsTrustedAgentRunCaller({ UserPayload: userPayload, IsWidgetGuestRun: false, AgentID: agentId });
|
|
643
665
|
return this.executeAIAgent(
|
|
644
666
|
p,
|
|
645
667
|
dataSource,
|
|
@@ -662,7 +684,10 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
662
684
|
undefined, // conversationId (not pre-resolved on this path)
|
|
663
685
|
undefined, // runRef
|
|
664
686
|
planMode,
|
|
665
|
-
requestedSkillIDs
|
|
687
|
+
requestedSkillIDs,
|
|
688
|
+
undefined, // taskGraphDebug
|
|
689
|
+
undefined, // conversationHistoryFrom
|
|
690
|
+
trustRunData
|
|
666
691
|
);
|
|
667
692
|
}
|
|
668
693
|
|
|
@@ -958,6 +983,12 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
958
983
|
const widgetElevation = await ResolveWidgetGuestRunContext(userPayload, p);
|
|
959
984
|
const effectiveAgentId = widgetElevation ? widgetElevation.PinnedAgentId : agentId;
|
|
960
985
|
const effectiveUserPayload = widgetElevation ? ElevateUserPayload(userPayload, widgetElevation.ElevatedUser) : userPayload;
|
|
986
|
+
// Only the system user or an API key holding the agent:trusted scope may set a run's scope and agent-type
|
|
987
|
+
// params through data (ExecuteAgentParams.TrustReservedRunData). Judged on the caller's own payload: a widget
|
|
988
|
+
// guest runs under an elevated server principal but is never trusted.
|
|
989
|
+
const trustRunData = await IsTrustedAgentRunCaller({
|
|
990
|
+
UserPayload: userPayload, IsWidgetGuestRun: widgetElevation !== null, AgentID: agentId,
|
|
991
|
+
});
|
|
961
992
|
|
|
962
993
|
try {
|
|
963
994
|
// Parsed before anything loads: a floor the caller asked for must never be dropped.
|
|
@@ -1021,6 +1052,7 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1021
1052
|
sourceArtifactId, sourceArtifactVersionId, conversationId, planMode, requestedSkillIDs,
|
|
1022
1053
|
undefined, // taskGraphDebug
|
|
1023
1054
|
historyFrom,
|
|
1055
|
+
trustRunData,
|
|
1024
1056
|
branchId
|
|
1025
1057
|
);
|
|
1026
1058
|
|
|
@@ -1058,6 +1090,7 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1058
1090
|
requestedSkillIDs,
|
|
1059
1091
|
undefined, // taskGraphDebug
|
|
1060
1092
|
historyFrom,
|
|
1093
|
+
trustRunData,
|
|
1061
1094
|
branchId
|
|
1062
1095
|
);
|
|
1063
1096
|
} catch (error) {
|
|
@@ -1281,6 +1314,8 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1281
1314
|
taskGraphDebug?: string,
|
|
1282
1315
|
/** History floor — threaded through to ExecuteAgentParams.ConversationHistoryFrom. */
|
|
1283
1316
|
conversationHistoryFrom?: Date,
|
|
1317
|
+
/** ExecuteAgentParams.TrustReservedRunData — see executeAIAgent. */
|
|
1318
|
+
trustReservedRunData: boolean = false,
|
|
1284
1319
|
/** The conversation branch of the run — threaded through to ExecuteAgentParams.ConversationBranchID. */
|
|
1285
1320
|
conversationBranchId?: string | null
|
|
1286
1321
|
): void {
|
|
@@ -1302,7 +1337,7 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1302
1337
|
data, payload, undefined, lastRunId, autoPopulateLastRunPayload,
|
|
1303
1338
|
configurationId, conversationDetailId, createArtifacts, createNotification,
|
|
1304
1339
|
sourceArtifactId, sourceArtifactVersionId, conversationId, runRef, planMode, requestedSkillIDs, taskGraphDebug,
|
|
1305
|
-
conversationHistoryFrom, conversationBranchId
|
|
1340
|
+
conversationHistoryFrom, trustReservedRunData, conversationBranchId
|
|
1306
1341
|
).catch((error: unknown) => {
|
|
1307
1342
|
// Background execution failed unexpectedly (executeAIAgent has its own try-catch,
|
|
1308
1343
|
// so this would only fire for truly unexpected errors).
|
|
@@ -18,8 +18,14 @@
|
|
|
18
18
|
* Permission enforcement is identical to SearchKnowledge: each requested
|
|
19
19
|
* scopeID is run through SearchScopePermissionResolver before the engine
|
|
20
20
|
* is invoked. A denial publishes a single 'error' event and ends.
|
|
21
|
+
*
|
|
22
|
+
* 'provider' events are progress only — ProviderName, DurationMs and
|
|
23
|
+
* ResultCount. They arrive before the engine's permission pass, so they never
|
|
24
|
+
* carry a result: rows the caller's row filters would drop and unverified
|
|
25
|
+
* external-index hits must not reach the client. Results arrive in 'fused'
|
|
26
|
+
* and 'final'.
|
|
21
27
|
*/
|
|
22
|
-
import { Resolver, Mutation, Arg, Ctx, ObjectType, Field, Float, ID, Subscription, Root, PubSub, PubSubEngine } from 'type-graphql';
|
|
28
|
+
import { Resolver, Mutation, Arg, Ctx, ObjectType, Field, Float, Int, ID, Subscription, Root, PubSub, PubSubEngine } from 'type-graphql';
|
|
23
29
|
import { v4 as uuidv4 } from 'uuid';
|
|
24
30
|
import { GraphQLJSON } from 'graphql-type-json';
|
|
25
31
|
import { AppContext } from '../types.js';
|
|
@@ -59,10 +65,17 @@ export class SearchStreamNotification {
|
|
|
59
65
|
@Field(() => Float, { nullable: true })
|
|
60
66
|
DurationMs?: number;
|
|
61
67
|
|
|
62
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Set on `fused`, `reranked` and `final` events. Always empty on a `provider` event: a provider's
|
|
70
|
+
* hits arrive before the permission pass, so the stream reports only their count (`ResultCount`).
|
|
71
|
+
*/
|
|
63
72
|
@Field(() => [SearchKnowledgeResultItem], { nullable: true })
|
|
64
73
|
Results?: SearchKnowledgeResultItem[];
|
|
65
74
|
|
|
75
|
+
/** Set on `provider` events: how many hits the provider returned, capped at `maxResults`. */
|
|
76
|
+
@Field(() => Int, { nullable: true })
|
|
77
|
+
ResultCount?: number;
|
|
78
|
+
|
|
66
79
|
/** Set on `final` events. */
|
|
67
80
|
@Field(() => SearchSourceCounts, { nullable: true })
|
|
68
81
|
SourceCounts?: SearchSourceCounts;
|
|
@@ -253,12 +266,15 @@ export class SearchKnowledgeStreamResolver extends ResolverBase {
|
|
|
253
266
|
|
|
254
267
|
switch (ev.phase) {
|
|
255
268
|
case 'provider':
|
|
269
|
+
// Progress only. `ev.results` is never forwarded, whatever it holds: these hits have
|
|
270
|
+
// not been through the permission pass.
|
|
256
271
|
return {
|
|
257
272
|
StreamID: streamID,
|
|
258
273
|
Phase: 'provider',
|
|
259
274
|
ProviderName: ev.providerName,
|
|
260
275
|
DurationMs: ev.durationMs,
|
|
261
|
-
|
|
276
|
+
ResultCount: ev.resultCount,
|
|
277
|
+
Results: [],
|
|
262
278
|
};
|
|
263
279
|
case 'fused':
|
|
264
280
|
return {
|
|
@@ -670,10 +670,8 @@ export class SqlLoggingConfigResolver extends ResolverBase {
|
|
|
670
670
|
}
|
|
671
671
|
|
|
672
672
|
try {
|
|
673
|
-
//
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
// Read file content
|
|
673
|
+
// Read directly: a missing file surfaces as ENOENT below, so a separate existence check
|
|
674
|
+
// would only open a window for the file to change between the check and the read.
|
|
677
675
|
const content = await fs.readFile(session.filePath, 'utf-8');
|
|
678
676
|
|
|
679
677
|
// Apply line limit if specified
|