@memberjunction/server 5.51.0 → 6.1.0-edge.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 +7 -0
- package/dist/agentSessions/SessionManager.d.ts +22 -1
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +13 -3
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/auth/actingContextResolver.d.ts +44 -0
- package/dist/auth/actingContextResolver.d.ts.map +1 -0
- package/dist/auth/actingContextResolver.js +18 -0
- package/dist/auth/actingContextResolver.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/sessionUserClone.d.ts +32 -0
- package/dist/auth/sessionUserClone.d.ts.map +1 -0
- package/dist/auth/sessionUserClone.js +50 -0
- package/dist/auth/sessionUserClone.js.map +1 -0
- package/dist/context.d.ts +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +43 -6
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +234 -642
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +1277 -3755
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts +5 -0
- package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.js +5 -4
- package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
- package/dist/generic/PushStatusResolver.d.ts +58 -0
- package/dist/generic/PushStatusResolver.d.ts.map +1 -1
- package/dist/generic/PushStatusResolver.js +42 -3
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +9 -0
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +31 -13
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -4
- package/dist/index.js.map +1 -1
- package/dist/multiTenancy/index.d.ts +19 -0
- package/dist/multiTenancy/index.d.ts.map +1 -1
- package/dist/multiTenancy/index.js +82 -5
- package/dist/multiTenancy/index.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +12 -5
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +13 -16
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +15 -14
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +19 -26
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +30 -40
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTestResolver.js +90 -112
- package/dist/resolvers/RunTestResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +3 -3
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.js +2 -2
- package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +85 -0
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -0
- package/dist/resolvers/TaskGraphFrameResolver.js +163 -0
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -0
- package/dist/resolvers/VersionHistoryResolver.d.ts.map +1 -1
- package/dist/resolvers/VersionHistoryResolver.js +8 -12
- package/dist/resolvers/VersionHistoryResolver.js.map +1 -1
- package/dist/rest/RESTEndpointHandler.d.ts +0 -4
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
- package/dist/rest/RESTEndpointHandler.js +1 -15
- package/dist/rest/RESTEndpointHandler.js.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.d.ts +33 -0
- package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -0
- package/dist/services/StartTaskGraphDispatcher.js +31 -0
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -0
- package/dist/services/TaskGraphAgentRunner.d.ts +15 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphAgentRunner.js +74 -0
- package/dist/services/TaskGraphAgentRunner.js.map +1 -0
- package/dist/services/TaskGraphContinuationDeliverer.d.ts +70 -0
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -0
- package/dist/services/TaskGraphContinuationDeliverer.js +162 -0
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -0
- package/dist/services/TaskGraphProviderFactory.d.ts +39 -0
- package/dist/services/TaskGraphProviderFactory.d.ts.map +1 -0
- package/dist/services/TaskGraphProviderFactory.js +28 -0
- package/dist/services/TaskGraphProviderFactory.js.map +1 -0
- package/package.json +103 -100
- package/src/__tests__/RealtimeBridgeResolver.test.ts +6 -0
- package/src/__tests__/SessionManager.test.ts +44 -0
- package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +249 -0
- package/src/__tests__/TaskGraphFrameResolver.test.ts +136 -0
- package/src/__tests__/multiTenancy.security.test.ts +254 -22
- package/src/__tests__/multiTenancy.test.ts +22 -0
- package/src/__tests__/pushStatusResolver.filter.test.ts +56 -0
- package/src/__tests__/resolverBase.rls.test.ts +4 -0
- package/src/__tests__/search-knowledge-tags.test.ts +74 -6
- package/src/__tests__/sessionUserClone.test.ts +161 -0
- package/src/agentSessions/SessionManager.ts +26 -2
- package/src/auth/actingContextResolver.ts +56 -0
- package/src/auth/index.ts +1 -0
- package/src/auth/sessionUserClone.ts +50 -0
- package/src/context.ts +48 -6
- package/src/generated/generated.ts +899 -2555
- package/src/generic/FireAndForgetHeartbeat.ts +10 -4
- package/src/generic/PushStatusResolver.ts +79 -4
- package/src/generic/ResolverBase.ts +33 -13
- package/src/index.ts +21 -4
- package/src/multiTenancy/index.ts +92 -5
- package/src/resolvers/ActionResolver.ts +15 -7
- package/src/resolvers/MCPResolver.ts +13 -15
- package/src/resolvers/RealtimeClientSessionResolver.ts +15 -13
- package/src/resolvers/RemoteBrowserActionResolver.ts +19 -25
- package/src/resolvers/RunAIAgentResolver.ts +30 -40
- package/src/resolvers/RunTestResolver.ts +90 -112
- package/src/resolvers/SearchKnowledgeResolver.ts +3 -3
- package/src/resolvers/SearchKnowledgeStreamResolver.ts +2 -2
- package/src/resolvers/TaskGraphFrameResolver.ts +160 -0
- package/src/resolvers/VersionHistoryResolver.ts +10 -13
- package/src/rest/RESTEndpointHandler.ts +1 -15
- package/src/services/StartTaskGraphDispatcher.ts +61 -0
- package/src/services/TaskGraphAgentRunner.ts +81 -0
- package/src/services/TaskGraphContinuationDeliverer.ts +181 -0
- package/src/services/TaskGraphProviderFactory.ts +53 -0
- package/dist/resolvers/ReportResolver.d.ts +0 -24
- package/dist/resolvers/ReportResolver.d.ts.map +0 -1
- package/dist/resolvers/ReportResolver.js +0 -190
- package/dist/resolvers/ReportResolver.js.map +0 -1
- package/dist/resolvers/TaskResolver.d.ts +0 -31
- package/dist/resolvers/TaskResolver.d.ts.map +0 -1
- package/dist/resolvers/TaskResolver.js +0 -157
- package/dist/resolvers/TaskResolver.js.map +0 -1
- package/dist/services/TaskOrchestrator.d.ts +0 -130
- package/dist/services/TaskOrchestrator.d.ts.map +0 -1
- package/dist/services/TaskOrchestrator.js +0 -690
- package/dist/services/TaskOrchestrator.js.map +0 -1
- package/src/resolvers/ReportResolver.ts +0 -157
- package/src/resolvers/TaskResolver.ts +0 -148
- package/src/services/TaskOrchestrator.ts +0 -873
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PubSubEngine } from 'type-graphql';
|
|
2
2
|
import { LogError } from '@memberjunction/core';
|
|
3
|
-
import {
|
|
3
|
+
import { publishStatusUpdate } from './PushStatusResolver.js';
|
|
4
4
|
|
|
5
5
|
/** Default cadence for fire-and-forget liveness pulses (5 minutes). */
|
|
6
6
|
export const DEFAULT_PULSE_INTERVAL_MS = 5 * 60 * 1000;
|
|
@@ -32,6 +32,11 @@ export interface LivenessPulseOptions {
|
|
|
32
32
|
pubSub: PubSubEngine;
|
|
33
33
|
/** Session the client is subscribed on (used by the subscription filter). */
|
|
34
34
|
sessionId: string;
|
|
35
|
+
/**
|
|
36
|
+
* Authenticated user the operation belongs to (B49). Stamped on every pulse so the
|
|
37
|
+
* subscription filter binds delivery to identity, not just the client-chosen sessionId.
|
|
38
|
+
*/
|
|
39
|
+
ownerUserId: string;
|
|
35
40
|
/** Resolver label echoed in the message envelope (e.g. 'RunAIAgentResolver'). */
|
|
36
41
|
resolver: string;
|
|
37
42
|
/** Pulse cadence in ms. Defaults to {@link DEFAULT_PULSE_INTERVAL_MS}. */
|
|
@@ -56,7 +61,7 @@ export interface LivenessPulseOptions {
|
|
|
56
61
|
* so the client receives it through the same subscription with no special parsing.
|
|
57
62
|
*/
|
|
58
63
|
export function startLivenessPulse(options: LivenessPulseOptions): LivenessPulseHandle {
|
|
59
|
-
const { pubSub, sessionId, resolver, readStatus } = options;
|
|
64
|
+
const { pubSub, sessionId, ownerUserId, resolver, readStatus } = options;
|
|
60
65
|
const intervalMs = options.intervalMs ?? DEFAULT_PULSE_INTERVAL_MS;
|
|
61
66
|
|
|
62
67
|
const timer = setInterval(() => {
|
|
@@ -68,14 +73,15 @@ export function startLivenessPulse(options: LivenessPulseOptions): LivenessPulse
|
|
|
68
73
|
LogError(`[LivenessPulse:${resolver}] readStatus failed: ${(e as Error).message}`);
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
pubSub
|
|
76
|
+
publishStatusUpdate(pubSub, {
|
|
77
|
+
sessionId,
|
|
78
|
+
ownerUserId,
|
|
72
79
|
message: JSON.stringify({
|
|
73
80
|
resolver,
|
|
74
81
|
type: HEARTBEAT_MESSAGE_TYPE,
|
|
75
82
|
status: 'ok',
|
|
76
83
|
data: data ?? {},
|
|
77
84
|
}),
|
|
78
|
-
sessionId,
|
|
79
85
|
});
|
|
80
86
|
}, intervalMs);
|
|
81
87
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { Arg, Field, ID, ObjectType, Resolver, ResolverFilterData, Root, Subscription } from 'type-graphql';
|
|
1
|
+
import { Arg, Field, ID, ObjectType, PubSubEngine, Resolver, ResolverFilterData, Root, Subscription } from 'type-graphql';
|
|
2
|
+
import { UUIDsEqual } from '@memberjunction/global';
|
|
3
|
+
import { UserPayload } from '../types.js';
|
|
2
4
|
|
|
3
5
|
export const PUSH_STATUS_UPDATES_TOPIC = 'PUSH_STATUS_UPDATES';
|
|
4
6
|
|
|
@@ -14,27 +16,100 @@ export class PushStatusNotification {
|
|
|
14
16
|
sessionId!: string;
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Internal wire payload for a push-status update. Carries `ownerUserId` — the authenticated user
|
|
21
|
+
* the update belongs to — used ONLY server-side by the subscription filter and deliberately NOT
|
|
22
|
+
* exposed on the client-facing {@link PushStatusNotification} (we never leak user IDs to subscribers).
|
|
23
|
+
*
|
|
24
|
+
* `ownerUserId` is REQUIRED: it is the load-bearing security property (see
|
|
25
|
+
* {@link statusUpdatesFilter}), so making it required means the compiler rejects any publish that
|
|
26
|
+
* omits identity. Always publish through {@link publishStatusUpdate} (or the
|
|
27
|
+
* `ResolverBase.PublishStatusUpdate` ergonomic wrapper) rather than calling `pubSub.publish` on
|
|
28
|
+
* this topic directly, so a new publisher can never forget it.
|
|
29
|
+
*/
|
|
17
30
|
export interface PushStatusNotificationPayload {
|
|
18
31
|
message?: string;
|
|
19
32
|
sessionId: string;
|
|
33
|
+
/** Authenticated user the update belongs to. Server-side filter key; never sent to the client. */
|
|
34
|
+
ownerUserId: string;
|
|
20
35
|
}
|
|
21
36
|
|
|
22
37
|
interface PushStatusNotificationArgs {
|
|
23
38
|
sessionId: string;
|
|
24
39
|
}
|
|
25
40
|
|
|
41
|
+
/** Parameters for {@link publishStatusUpdate} — identity (`ownerUserId`) is required by design. */
|
|
42
|
+
export interface StatusUpdateParams {
|
|
43
|
+
/** Session the target client subscribed on (routes the push to the right browser tab). */
|
|
44
|
+
sessionId: string;
|
|
45
|
+
/** Authenticated user the operation belongs to (the trust anchor the filter enforces). */
|
|
46
|
+
ownerUserId: string;
|
|
47
|
+
/** Message envelope (plain string, or the resolvers' JSON `{resolver,type,status,data}` shape). */
|
|
48
|
+
message?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The single, centralized vehicle for publishing on {@link PUSH_STATUS_UPDATES_TOPIC}. Every
|
|
53
|
+
* publisher — resolver or service — routes through here (resolvers via the
|
|
54
|
+
* `ResolverBase.PublishStatusUpdate` wrapper; non-resolvers by calling this directly), so the
|
|
55
|
+
* payload shape and the required-identity guarantee live in exactly one place. Adding a field to
|
|
56
|
+
* the push is a one-line change here; a new publisher physically cannot omit `ownerUserId`.
|
|
57
|
+
*/
|
|
58
|
+
export function publishStatusUpdate(pubSub: PubSubEngine, params: StatusUpdateParams): void {
|
|
59
|
+
const payload: PushStatusNotificationPayload = {
|
|
60
|
+
sessionId: params.sessionId,
|
|
61
|
+
ownerUserId: params.ownerUserId,
|
|
62
|
+
message: params.message,
|
|
63
|
+
};
|
|
64
|
+
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, payload);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Minimal shape of the subscription's connection context needed by the filter. */
|
|
68
|
+
export interface StatusUpdatesFilterContext {
|
|
69
|
+
userPayload?: UserPayload;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Filter predicate for the `statusUpdates` subscription. Exported so it can be unit-tested in
|
|
74
|
+
* isolation (it is the security-critical gate). A push reaches a subscriber only when BOTH hold:
|
|
75
|
+
* 1. the push's `sessionId` matches the subscriber's requested `sessionId` (tab routing), AND
|
|
76
|
+
* 2. the push's `ownerUserId` matches the subscriber CONNECTION's authenticated identity
|
|
77
|
+
* (`context.userPayload.userRecord.ID`, established once at WS connect).
|
|
78
|
+
*
|
|
79
|
+
* Condition (2) is the fix for the session-hijack class (B49): `sessionId` is a client-chosen
|
|
80
|
+
* correlation value, so a subscriber who lifts another user's `sessionId` would previously receive
|
|
81
|
+
* their pushes. Binding delivery to the connection's server-authenticated identity means knowing a
|
|
82
|
+
* `sessionId` is no longer sufficient. Fails CLOSED — a missing owner or connection identity never
|
|
83
|
+
* matches.
|
|
84
|
+
*/
|
|
85
|
+
export function statusUpdatesFilter(data: {
|
|
86
|
+
payload: PushStatusNotificationPayload;
|
|
87
|
+
args: PushStatusNotificationArgs;
|
|
88
|
+
context: StatusUpdatesFilterContext | undefined;
|
|
89
|
+
}): boolean {
|
|
90
|
+
const { payload, args, context } = data;
|
|
91
|
+
if (payload.sessionId !== args.sessionId) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const connectionUserId: string | undefined = context?.userPayload?.userRecord?.ID;
|
|
95
|
+
if (!connectionUserId || !payload.ownerUserId) {
|
|
96
|
+
return false; // fail closed — no identity on either side means no delivery
|
|
97
|
+
}
|
|
98
|
+
return UUIDsEqual(payload.ownerUserId, connectionUserId);
|
|
99
|
+
}
|
|
100
|
+
|
|
26
101
|
@Resolver()
|
|
27
102
|
export class PushStatusResolver {
|
|
28
103
|
@Subscription(() => PushStatusNotification, {
|
|
29
104
|
topics: PUSH_STATUS_UPDATES_TOPIC,
|
|
30
|
-
filter: (
|
|
31
|
-
|
|
32
|
-
},
|
|
105
|
+
filter: (data: ResolverFilterData<PushStatusNotificationPayload, PushStatusNotificationArgs, StatusUpdatesFilterContext>) =>
|
|
106
|
+
statusUpdatesFilter(data),
|
|
33
107
|
})
|
|
34
108
|
statusUpdates(
|
|
35
109
|
@Root() { message }: PushStatusNotificationPayload,
|
|
36
110
|
@Arg('sessionId', () => String) sessionId: string
|
|
37
111
|
): PushStatusNotification {
|
|
112
|
+
// NOTE: ownerUserId is intentionally NOT returned — it is a server-side filter key only.
|
|
38
113
|
return { message, date: new Date(), sessionId };
|
|
39
114
|
}
|
|
40
115
|
}
|
|
@@ -32,7 +32,7 @@ import { RunDynamicViewInput, RunViewByIDInput, RunViewByNameInput } from './Run
|
|
|
32
32
|
import { DeleteOptionsInput } from './DeleteOptionsInput.js';
|
|
33
33
|
import { MJEvent, MJEventType, MJGlobal, ENCRYPTED_SENTINEL, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
|
|
34
34
|
import { EncryptionEngine } from '@memberjunction/encryption';
|
|
35
|
-
import { PUSH_STATUS_UPDATES_TOPIC } from './PushStatusResolver.js';
|
|
35
|
+
import { PUSH_STATUS_UPDATES_TOPIC, publishStatusUpdate } from './PushStatusResolver.js';
|
|
36
36
|
import { CACHE_INVALIDATION_TOPIC } from './CacheInvalidationResolver.js';
|
|
37
37
|
import { PubSubManager } from './PubSubManager.js';
|
|
38
38
|
import { FieldMapper } from '@memberjunction/graphql-dataprovider';
|
|
@@ -691,7 +691,11 @@ export class ResolverBase {
|
|
|
691
691
|
return;
|
|
692
692
|
}
|
|
693
693
|
|
|
694
|
-
// Check specific scope
|
|
694
|
+
// Check specific scope. The acting context stamped on the session user (set
|
|
695
|
+
// server-side in context.ts, never client-supplied) rides along so filtered
|
|
696
|
+
// rules with {{Acting*}} tokens can validate their required values — without
|
|
697
|
+
// it, a filtered rule fails closed even for a session carrying valid context.
|
|
698
|
+
const sessionUser = this.GetUserFromPayload(userPayload);
|
|
695
699
|
const result = await apiKeyEngine.Authorize(
|
|
696
700
|
userPayload.apiKeyHash,
|
|
697
701
|
'MJAPI',
|
|
@@ -701,6 +705,9 @@ export class ResolverBase {
|
|
|
701
705
|
{
|
|
702
706
|
endpoint: '/graphql',
|
|
703
707
|
method: 'POST'
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
actingContext: sessionUser?.APIKeyActingContext
|
|
704
711
|
}
|
|
705
712
|
);
|
|
706
713
|
|
|
@@ -999,7 +1006,7 @@ export class ResolverBase {
|
|
|
999
1006
|
?? UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
|
|
1000
1007
|
if (!user) throw new Error(`User ${userPayload?.email} not found in metadata`);
|
|
1001
1008
|
|
|
1002
|
-
return entityInfo.
|
|
1009
|
+
return entityInfo.GetEffectiveRowFilterWhereClause(user, type, returnPrefix);
|
|
1003
1010
|
}
|
|
1004
1011
|
|
|
1005
1012
|
protected async createAuditLogRecord(
|
|
@@ -1101,6 +1108,22 @@ export class ResolverBase {
|
|
|
1101
1108
|
});
|
|
1102
1109
|
}
|
|
1103
1110
|
|
|
1111
|
+
/**
|
|
1112
|
+
* Publishes a push-status update to the client on {@link PUSH_STATUS_UPDATES_TOPIC}, stamping the
|
|
1113
|
+
* authenticated owner's user ID from `userPayload` so the subscription filter can bind delivery
|
|
1114
|
+
* to identity (see B49 / `statusUpdatesFilter`). The ergonomic wrapper every resolver should use
|
|
1115
|
+
* instead of calling `pubSub.publish` on the topic directly — it makes omitting identity
|
|
1116
|
+
* impossible. Non-resolver publishers (services, the liveness heartbeat) call the shared
|
|
1117
|
+
* `publishStatusUpdate()` function directly with an explicit `ownerUserId`.
|
|
1118
|
+
*/
|
|
1119
|
+
protected PublishStatusUpdate(pubSub: PubSubEngine, sessionId: string, message: string | undefined, userPayload: UserPayload): void {
|
|
1120
|
+
publishStatusUpdate(pubSub, {
|
|
1121
|
+
sessionId,
|
|
1122
|
+
ownerUserId: userPayload?.userRecord?.ID ?? '',
|
|
1123
|
+
message,
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1104
1127
|
protected ListenForEntityMessages(entityObject: BaseEntity, pubSub: PubSubEngine, userPayload: UserPayload) {
|
|
1105
1128
|
// The unique key is set up for each entity object via it's primary key to ensure that we only have one listener at most for each unique
|
|
1106
1129
|
// entity in the system. This is important because we don't want to have multiple listeners for the same entity as it could
|
|
@@ -1130,16 +1153,13 @@ export class ResolverBase {
|
|
|
1130
1153
|
const baseEntityEvent = event.args as BaseEntityEvent;
|
|
1131
1154
|
// message from our entity object, relay it to the client
|
|
1132
1155
|
LogDebug('ResolverBase.ListenForEntityMessages: About to publish PUSH_STATUS_UPDATES_TOPIC');
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
}),
|
|
1141
|
-
sessionId: userPayload.sessionId,
|
|
1142
|
-
});
|
|
1156
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
1157
|
+
status: 'OK',
|
|
1158
|
+
type: 'EntityObjectStatusMessage',
|
|
1159
|
+
entityName: baseEntityEvent.baseEntity.EntityInfo.Name,
|
|
1160
|
+
primaryKey: baseEntityEvent.baseEntity.PrimaryKey,
|
|
1161
|
+
message: event.args.payload,
|
|
1162
|
+
}), userPayload);
|
|
1143
1163
|
}
|
|
1144
1164
|
}
|
|
1145
1165
|
});
|
package/src/index.ts
CHANGED
|
@@ -66,6 +66,7 @@ import { IntegrationProgressEmitter } from '@memberjunction/integration-progress
|
|
|
66
66
|
import { PublishIntegrationProgress } from './resolvers/IntegrationProgressResolver.js';
|
|
67
67
|
import { ClientToolRequestManager, AgentRunWatchdog } from '@memberjunction/ai-agents';
|
|
68
68
|
import { SessionJanitor } from './agentSessions/index.js';
|
|
69
|
+
import { StartTaskGraphDispatcher } from './services/StartTaskGraphDispatcher.js';
|
|
69
70
|
import { CACHE_INVALIDATION_TOPIC } from './generic/CacheInvalidationResolver.js';
|
|
70
71
|
import { ConnectorFactory, IntegrationEngine, IntegrationSyncOptions } from '@memberjunction/integration-engine';
|
|
71
72
|
import { CronExpressionHelper } from '@memberjunction/scheduling-engine';
|
|
@@ -113,6 +114,8 @@ export {
|
|
|
113
114
|
verifyUserRecord,
|
|
114
115
|
} from './auth/index.js';
|
|
115
116
|
export * from './auth/APIKeyScopeAuth.js';
|
|
117
|
+
export * from './auth/actingContextResolver.js';
|
|
118
|
+
export { CloneUserForSessionContext } from './auth/sessionUserClone.js';
|
|
116
119
|
|
|
117
120
|
export * from './generic/PushStatusResolver.js';
|
|
118
121
|
export * from './generic/PubSubManager.js';
|
|
@@ -134,7 +137,6 @@ export * from './resolvers/IntegrationProgressResolver.js';
|
|
|
134
137
|
export * from './resolvers/ClientToolRequestResolver.js';
|
|
135
138
|
export * from './resolvers/AutotagPipelineResolver.js';
|
|
136
139
|
export * from './resolvers/TagGovernanceResolver.js';
|
|
137
|
-
export * from './resolvers/TaskResolver.js';
|
|
138
140
|
export * from './generic/KeyValuePairInput.js';
|
|
139
141
|
export * from './generic/KeyInputOutputTypes.js';
|
|
140
142
|
export * from './generic/DeleteOptionsInput.js';
|
|
@@ -147,8 +149,12 @@ export * from './resolvers/ComponentRegistryResolver.js';
|
|
|
147
149
|
export * from './resolvers/DatasetResolver.js';
|
|
148
150
|
export * from './resolvers/EntityRecordNameResolver.js';
|
|
149
151
|
export * from './resolvers/MergeRecordsResolver.js';
|
|
150
|
-
export * from './resolvers/ReportResolver.js';
|
|
151
152
|
export * from './resolvers/QueryResolver.js';
|
|
153
|
+
export * from './services/TaskGraphProviderFactory.js';
|
|
154
|
+
export * from './services/TaskGraphAgentRunner.js';
|
|
155
|
+
export * from './services/StartTaskGraphDispatcher.js';
|
|
156
|
+
export * from './services/TaskGraphContinuationDeliverer.js';
|
|
157
|
+
export * from './resolvers/TaskGraphFrameResolver.js';
|
|
152
158
|
export * from './resolvers/TestQuerySQLResolver.js';
|
|
153
159
|
export * from './resolvers/SqlLoggingConfigResolver.js';
|
|
154
160
|
export * from './resolvers/SyncRolesUsersResolver.js';
|
|
@@ -481,7 +487,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
481
487
|
startupLog.LogIf('verbose', 'Read-only Connection Pool has been initialized.');
|
|
482
488
|
}
|
|
483
489
|
|
|
484
|
-
const config = new SQLServerProviderConfigData(pool, mj_core_schema, cacheRefreshInterval);
|
|
490
|
+
const config = new SQLServerProviderConfigData(pool, mj_core_schema, cacheRefreshInterval / 1000); // convert ms to seconds (checkRefreshIntervalSeconds)
|
|
485
491
|
// MJAPI is a long-running server, so entry-point default is 'full' engine pre-warm;
|
|
486
492
|
// MJ_STARTUP_MODE / mj.config.cjs startup.mode can override per the shared precedence chain
|
|
487
493
|
const startupMode = ResolveStartupMode({ configValue: configInfo.startup?.mode, defaultMode: 'full' });
|
|
@@ -516,7 +522,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
516
522
|
await codegenPool.connect();
|
|
517
523
|
|
|
518
524
|
const { RuntimeSchemaManager } = await import('@memberjunction/schema-engine');
|
|
519
|
-
const codegenConfig = new SQLServerProviderConfigData(codegenPool, mj_core_schema, cacheRefreshInterval);
|
|
525
|
+
const codegenConfig = new SQLServerProviderConfigData(codegenPool, mj_core_schema, cacheRefreshInterval / 1000); // convert ms to seconds (checkRefreshIntervalSeconds)
|
|
520
526
|
const codegenProvider = new SQLServerDataProvider();
|
|
521
527
|
await codegenProvider.Config(codegenConfig);
|
|
522
528
|
RuntimeSchemaManager.Instance.SetDDLProvider(codegenProvider);
|
|
@@ -1339,6 +1345,17 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1339
1345
|
.catch(err => console.warn(`[SessionJanitor] Startup failed: ${err}`));
|
|
1340
1346
|
}
|
|
1341
1347
|
|
|
1348
|
+
// Launch the durable task-graph dispatcher: claim Pending tasks, execute them with a fresh
|
|
1349
|
+
// provider each, and reconcile claims orphaned by a crash. Without this nothing ever picks up a
|
|
1350
|
+
// submitted graph — submission would be durable and inert, which is strictly worse than the old
|
|
1351
|
+
// client-driven path it replaced. Gated on SQL Server because the provider factory mints
|
|
1352
|
+
// SQLServerDataProvider; the PG branch lands with PG parity. Self-registers with ShutdownRegistry.
|
|
1353
|
+
const taskGraphPool = dataSources[0]?.dataSource;
|
|
1354
|
+
if (resumeUser && taskGraphPool instanceof sql.ConnectionPool) {
|
|
1355
|
+
StartTaskGraphDispatcher(taskGraphPool, resumeUser)
|
|
1356
|
+
.catch(err => console.warn(`[TaskGraphDispatcher] Startup failed: ${err}`));
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1342
1359
|
// Launch the calendar / scheduled-bridge loop (M2): poll agent calendars for meeting invites and
|
|
1343
1360
|
// start due meeting bridges. Mirrors the SessionJanitor lifecycle (run-once + interval, timer
|
|
1344
1361
|
// unref'd). Gated on Teams meetings being enabled (the provider whose scheduled-join is wired) and
|
|
@@ -12,6 +12,24 @@ import type { PreRunViewHook, PreSaveHook } from '@memberjunction/core';
|
|
|
12
12
|
import { Metadata, type UserInfo, type TenantContext } from '@memberjunction/core';
|
|
13
13
|
import type { MultiTenancyConfig } from '../config.js';
|
|
14
14
|
import type { UserPayload } from '../types.js';
|
|
15
|
+
import { CloneUserForSessionContext } from '../auth/sessionUserClone.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Allowlist for tenant identifiers arriving from a request header: GUIDs and
|
|
19
|
+
* conservative identifiers only. Anything else is rejected AT THE BOUNDARY with
|
|
20
|
+
* a 400 — never silently dropped, because a dropped tenant header degrades to an
|
|
21
|
+
* UNSCOPED session, which is the fail-open case validation exists to prevent.
|
|
22
|
+
* Escaping at predicate construction is defense in depth behind this, not a
|
|
23
|
+
* substitute: `x' OR '1'='1` previously survived to the WHERE clause because the
|
|
24
|
+
* downstream keyword blocklist strips string literals before matching and has no
|
|
25
|
+
* rule for OR.
|
|
26
|
+
*/
|
|
27
|
+
const TENANT_ID_PATTERN = /^[A-Za-z0-9_.\-]{1,128}$/;
|
|
28
|
+
|
|
29
|
+
/** True when the value is acceptable as a tenant identifier. Exported for tests. */
|
|
30
|
+
export function IsValidTenantId(tenantId: string): boolean {
|
|
31
|
+
return TENANT_ID_PATTERN.test(tenantId);
|
|
32
|
+
}
|
|
15
33
|
|
|
16
34
|
/** Custom tenant context extractor signature */
|
|
17
35
|
export type TenantContextExtractor = (
|
|
@@ -31,7 +49,7 @@ export type TenantContextExtractor = (
|
|
|
31
49
|
* has TenantContext set — no deferred pickup via `req['__mj_tenantId']` needed.
|
|
32
50
|
*/
|
|
33
51
|
export function createTenantMiddleware(config: MultiTenancyConfig): RequestHandler {
|
|
34
|
-
return (req,
|
|
52
|
+
return (req, res, next) => {
|
|
35
53
|
const userPayload = (req as { userPayload?: UserPayload }).userPayload;
|
|
36
54
|
if (!userPayload?.userRecord) {
|
|
37
55
|
// No authenticated user — skip tenant resolution
|
|
@@ -40,9 +58,29 @@ export function createTenantMiddleware(config: MultiTenancyConfig): RequestHandl
|
|
|
40
58
|
}
|
|
41
59
|
|
|
42
60
|
if (config.contextSource === 'header') {
|
|
43
|
-
const
|
|
61
|
+
const rawHeader = req.headers[config.tenantHeader.toLowerCase()];
|
|
62
|
+
// A repeated header arrives as string[] — ambiguous which value is authoritative, so
|
|
63
|
+
// reject rather than silently pick one (Array.prototype's coercion to a comma-joined
|
|
64
|
+
// string would otherwise sail past IsValidTenantId's regex unnoticed for some inputs).
|
|
65
|
+
if (Array.isArray(rawHeader)) {
|
|
66
|
+
res.status(400).json({ error: `Malformed ${config.tenantHeader} header: expected a single value.` });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const tenantId = rawHeader;
|
|
44
70
|
if (tenantId) {
|
|
45
|
-
|
|
71
|
+
if (!IsValidTenantId(tenantId)) {
|
|
72
|
+
// Reject the REQUEST, never degrade to an unscoped session: a malformed
|
|
73
|
+
// tenant header that silently attaches nothing produces a session with NO
|
|
74
|
+
// tenant filter at all — strictly worse than failing loudly.
|
|
75
|
+
res.status(400).json({ error: 'Invalid tenant identifier' });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Clone before stamping: userRecord is very often the SHARED UserCache
|
|
79
|
+
// instance, and stamping it in place races concurrent requests for the
|
|
80
|
+
// same user (one request's tenant becomes another's, same-instant).
|
|
81
|
+
const sessionUser = CloneUserForSessionContext(userPayload.userRecord as UserInfo);
|
|
82
|
+
attachTenantContext(sessionUser, tenantId, 'header');
|
|
83
|
+
userPayload.userRecord = sessionUser;
|
|
46
84
|
}
|
|
47
85
|
}
|
|
48
86
|
next();
|
|
@@ -52,12 +90,22 @@ export function createTenantMiddleware(config: MultiTenancyConfig): RequestHandl
|
|
|
52
90
|
/**
|
|
53
91
|
* Attaches TenantContext to a UserInfo object.
|
|
54
92
|
* Called from the GraphQL context function after authentication.
|
|
93
|
+
*
|
|
94
|
+
* The caller is responsible for two invariants (createTenantMiddleware shows both):
|
|
95
|
+
* validate untrusted tenant ids with {@link IsValidTenantId} BEFORE calling, and
|
|
96
|
+
* never pass the shared cached UserInfo — clone it first
|
|
97
|
+
* (CloneUserForSessionContext). This function still validates as defense in depth
|
|
98
|
+
* so a future caller from a different source ('linkedEntity', 'custom') cannot
|
|
99
|
+
* reintroduce the injection path.
|
|
55
100
|
*/
|
|
56
101
|
export function attachTenantContext(
|
|
57
102
|
user: UserInfo,
|
|
58
103
|
tenantId: string,
|
|
59
104
|
source: TenantContext['Source']
|
|
60
105
|
): void {
|
|
106
|
+
if (!IsValidTenantId(tenantId)) {
|
|
107
|
+
throw new Error(`Invalid tenant identifier (must match ${TENANT_ID_PATTERN})`);
|
|
108
|
+
}
|
|
61
109
|
user.TenantContext = { TenantID: tenantId, Source: source };
|
|
62
110
|
}
|
|
63
111
|
|
|
@@ -118,9 +166,29 @@ export function createTenantPreRunViewHook(config: MultiTenancyConfig): PreRunVi
|
|
|
118
166
|
// Check if this entity should be scoped
|
|
119
167
|
if (!isEntityScoped(entityName, config)) return params;
|
|
120
168
|
|
|
121
|
-
// Determine which column holds the tenant ID
|
|
169
|
+
// Determine which column holds the tenant ID. The column is operator config, not
|
|
170
|
+
// user input — but it is interpolated into an identifier position, so it must
|
|
171
|
+
// resolve to a real, non-virtual field on the entity (rejecting typos AND ruling
|
|
172
|
+
// out identifier injection through a compromised config), and it is quoted through
|
|
173
|
+
// the platform-appropriate helper rather than hardcoded T-SQL brackets, which are
|
|
174
|
+
// invalid on PostgreSQL.
|
|
122
175
|
const tenantColumn = config.entityColumnMappings[entityName] ?? config.defaultTenantColumn;
|
|
123
|
-
const
|
|
176
|
+
const md = new Metadata(); // global-provider-ok: same rationale as isEntityScoped above — hooks carry no per-request provider
|
|
177
|
+
const entityInfo = md.EntityByName(entityName);
|
|
178
|
+
const field = entityInfo?.Fields.find(
|
|
179
|
+
f => !f.IsVirtual && f.Name.trim().toLowerCase() === tenantColumn.trim().toLowerCase()
|
|
180
|
+
);
|
|
181
|
+
if (!field) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Multi-tenancy misconfiguration: tenant column '${tenantColumn}' does not resolve to a stored field on entity '${entityName}'`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Escape the value at construction even though the boundary validated it —
|
|
188
|
+
// defense in depth for future callers that attach TenantContext from sources
|
|
189
|
+
// other than the validated header path.
|
|
190
|
+
const safeTenantId = contextUser.TenantContext.TenantID.replace(/'/g, "''");
|
|
191
|
+
const tenantFilter = `${QuoteFilterIdentifier(field.Name)} = '${safeTenantId}'`;
|
|
124
192
|
|
|
125
193
|
// Inject the tenant filter
|
|
126
194
|
if (params.ExtraFilter && typeof params.ExtraFilter === 'string' && params.ExtraFilter.trim().length > 0) {
|
|
@@ -133,6 +201,25 @@ export function createTenantPreRunViewHook(config: MultiTenancyConfig): PreRunVi
|
|
|
133
201
|
};
|
|
134
202
|
}
|
|
135
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Quotes an identifier for use in a tenant predicate via the active provider's
|
|
206
|
+
* QuoteIdentifier (DatabaseProviderBase always exposes one server-side, yielding [x]
|
|
207
|
+
* on SQL Server and "x" on PostgreSQL). Throws — never falls back to hardcoded T-SQL
|
|
208
|
+
* bracket quoting — when no provider is available: a silent fallback would emit
|
|
209
|
+
* invalid SQL on PostgreSQL, and "fails to build a query" is a worse failure mode to
|
|
210
|
+
* debug than "refused to build one." A tenant filter must fail closed, loudly, at the
|
|
211
|
+
* point where it can no longer guarantee platform-correct SQL. Exported for tests.
|
|
212
|
+
*/
|
|
213
|
+
export function QuoteFilterIdentifier(name: string): string {
|
|
214
|
+
const provider = Metadata.Provider as unknown as { QuoteIdentifier?: (id: string) => string }; // global-provider-ok: hook-level code, single server provider
|
|
215
|
+
if (!provider || typeof provider.QuoteIdentifier !== 'function') {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`[MultiTenancy] Cannot quote identifier '${name}': no database provider available to quote it — refusing to fall back to hardcoded bracket syntax, which is invalid on PostgreSQL.`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return provider.QuoteIdentifier(name);
|
|
221
|
+
}
|
|
222
|
+
|
|
136
223
|
/**
|
|
137
224
|
* Creates a PreSaveHook that validates the tenant column on writes.
|
|
138
225
|
*
|
|
@@ -7,7 +7,7 @@ import { Field, InputType, ObjectType } from "type-graphql";
|
|
|
7
7
|
import { KeyValuePairInput } from "../generic/KeyValuePairInput.js";
|
|
8
8
|
import { AppContext, ProviderInfo } from "../types.js";
|
|
9
9
|
import { CopyScalarsAndArrays, UUIDsEqual } from "@memberjunction/global";
|
|
10
|
-
import { GetReadOnlyProvider } from "../util.js";
|
|
10
|
+
import { GetReadOnlyProvider, GetReadWriteProvider } from "../util.js";
|
|
11
11
|
import { ResolverBase } from "../generic/ResolverBase.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -203,8 +203,10 @@ export class ActionResolver extends ResolverBase {
|
|
|
203
203
|
// Parse the parameters
|
|
204
204
|
const params = this.parseActionParameters(input.Params);
|
|
205
205
|
|
|
206
|
-
// Run the action
|
|
207
|
-
|
|
206
|
+
// Run the action — thread the request-scoped provider so provider-bound
|
|
207
|
+
// actions (e.g. the semantic find-* / Search Entity actions, which call
|
|
208
|
+
// params.Provider.SearchEntity) run against the caller's metadata layer.
|
|
209
|
+
const result = await this.executeAction(action, user, params, ctx.providers, input.SkipActionLog);
|
|
208
210
|
|
|
209
211
|
// Return the result
|
|
210
212
|
return this.createActionResult(result);
|
|
@@ -271,17 +273,23 @@ export class ActionResolver extends ResolverBase {
|
|
|
271
273
|
* @private
|
|
272
274
|
*/
|
|
273
275
|
private async executeAction(
|
|
274
|
-
action: any,
|
|
275
|
-
user: UserInfo,
|
|
276
|
-
params: ActionParam[],
|
|
276
|
+
action: any,
|
|
277
|
+
user: UserInfo,
|
|
278
|
+
params: ActionParam[],
|
|
279
|
+
providers: Array<ProviderInfo>,
|
|
277
280
|
skipActionLog?: boolean
|
|
278
281
|
): Promise<ActionResult> {
|
|
282
|
+
// Resolve the request-scoped provider. Prefer read-write (actions may mutate);
|
|
283
|
+
// fall back to read-only, then to the engine's global-Metadata default when no
|
|
284
|
+
// request providers are present (preserves prior behavior).
|
|
285
|
+
const provider = GetReadWriteProvider(providers, { allowFallbackToReadOnly: true }) ?? undefined;
|
|
279
286
|
return await ActionEngineServer.Instance.RunAction({
|
|
280
287
|
Action: action,
|
|
281
288
|
ContextUser: user,
|
|
282
289
|
Params: params,
|
|
283
290
|
SkipActionLog: skipActionLog,
|
|
284
|
-
Filters: []
|
|
291
|
+
Filters: [],
|
|
292
|
+
Provider: provider
|
|
285
293
|
});
|
|
286
294
|
}
|
|
287
295
|
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from '@memberjunction/ai-mcp-client';
|
|
20
20
|
import { AppContext } from '../types.js';
|
|
21
21
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
22
|
-
import {
|
|
22
|
+
import { publishStatusUpdate } from '../generic/PushStatusResolver.js';
|
|
23
23
|
import { GraphQLJSONObject } from 'graphql-type-json';
|
|
24
24
|
import { configInfo } from '../config.js';
|
|
25
25
|
|
|
@@ -583,7 +583,7 @@ export class MCPResolver extends ResolverBase {
|
|
|
583
583
|
await manager.initialize(user, { publicUrl });
|
|
584
584
|
|
|
585
585
|
// Publish initial progress
|
|
586
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'connecting', 'Connecting to MCP server...');
|
|
586
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'connecting', 'Connecting to MCP server...');
|
|
587
587
|
|
|
588
588
|
// Connect if not already connected
|
|
589
589
|
const isConnected = manager.isConnected(ConnectionID);
|
|
@@ -595,18 +595,18 @@ export class MCPResolver extends ResolverBase {
|
|
|
595
595
|
// Check for OAuth authorization required
|
|
596
596
|
if (connectError instanceof OAuthAuthorizationRequiredError) {
|
|
597
597
|
const authError = connectError as OAuthAuthorizationRequiredError;
|
|
598
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'error',
|
|
598
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'error',
|
|
599
599
|
`OAuth authorization required. Please authorize at: ${authError.authorizationUrl}`);
|
|
600
600
|
return this.createOAuthRequiredResult(authError.authorizationUrl, authError.stateParameter);
|
|
601
601
|
}
|
|
602
602
|
if (connectError instanceof OAuthReauthorizationRequiredError) {
|
|
603
603
|
const reAuthError = connectError as OAuthReauthorizationRequiredError;
|
|
604
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'error',
|
|
604
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'error',
|
|
605
605
|
`OAuth re-authorization required: ${reAuthError.reason}`);
|
|
606
606
|
return this.createOAuthReauthorizationResult(reAuthError.reason, reAuthError.authorizationUrl, reAuthError.stateParameter);
|
|
607
607
|
}
|
|
608
608
|
const connectErrorMsg = connectError instanceof Error ? connectError.message : String(connectError);
|
|
609
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'error', `Connection failed: ${connectErrorMsg}`);
|
|
609
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'error', `Connection failed: ${connectErrorMsg}`);
|
|
610
610
|
return this.createErrorResult(`Failed to connect to MCP server: ${connectErrorMsg}`);
|
|
611
611
|
}
|
|
612
612
|
}
|
|
@@ -617,7 +617,7 @@ export class MCPResolver extends ResolverBase {
|
|
|
617
617
|
const connectionName = connectionInfo?.connectionName || 'Unknown Connection';
|
|
618
618
|
|
|
619
619
|
// Publish listing progress
|
|
620
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'listing', 'Discovering tools from MCP server...');
|
|
620
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'listing', 'Discovering tools from MCP server...');
|
|
621
621
|
|
|
622
622
|
// Perform the sync with event listening for granular progress
|
|
623
623
|
LogStatus(`MCPResolver: Starting tool sync for connection ${ConnectionID}`);
|
|
@@ -626,7 +626,7 @@ export class MCPResolver extends ResolverBase {
|
|
|
626
626
|
const eventHandler = (event: { type: string; data?: Record<string, unknown> }) => {
|
|
627
627
|
if (event.type === 'toolsSynced') {
|
|
628
628
|
const data = event.data as { added: number; updated: number; deprecated: number; total: number } | undefined;
|
|
629
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'complete', 'Tool sync complete', {
|
|
629
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'complete', 'Tool sync complete', {
|
|
630
630
|
added: data?.added || 0,
|
|
631
631
|
updated: data?.updated || 0,
|
|
632
632
|
deprecated: data?.deprecated || 0,
|
|
@@ -637,7 +637,7 @@ export class MCPResolver extends ResolverBase {
|
|
|
637
637
|
manager.addEventListener('toolsSynced', eventHandler);
|
|
638
638
|
|
|
639
639
|
// Publish syncing progress
|
|
640
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'syncing', 'Synchronizing tools to database...');
|
|
640
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'syncing', 'Synchronizing tools to database...');
|
|
641
641
|
|
|
642
642
|
// Perform the sync
|
|
643
643
|
const syncResult: MCPSyncToolsResult = await manager.syncTools(ConnectionID, { contextUser: user });
|
|
@@ -646,12 +646,12 @@ export class MCPResolver extends ResolverBase {
|
|
|
646
646
|
manager.removeEventListener('toolsSynced', eventHandler);
|
|
647
647
|
|
|
648
648
|
if (!syncResult.success) {
|
|
649
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'error', `Sync failed: ${syncResult.error}`);
|
|
649
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'error', `Sync failed: ${syncResult.error}`);
|
|
650
650
|
return this.createErrorResult(syncResult.error || 'Tool sync failed');
|
|
651
651
|
}
|
|
652
652
|
|
|
653
653
|
// Publish final completion
|
|
654
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'complete',
|
|
654
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'complete',
|
|
655
655
|
`Sync complete: ${syncResult.added} added, ${syncResult.updated} updated, ${syncResult.deprecated} deprecated`,
|
|
656
656
|
{
|
|
657
657
|
added: syncResult.added,
|
|
@@ -675,7 +675,7 @@ export class MCPResolver extends ResolverBase {
|
|
|
675
675
|
} catch (error) {
|
|
676
676
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
677
677
|
LogError(`MCPResolver: Error syncing tools for ${ConnectionID}: ${errorMsg}`);
|
|
678
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'error', `Error: ${errorMsg}`);
|
|
678
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'error', `Error: ${errorMsg}`);
|
|
679
679
|
return this.createErrorResult(errorMsg);
|
|
680
680
|
}
|
|
681
681
|
}
|
|
@@ -1345,6 +1345,7 @@ export class MCPResolver extends ResolverBase {
|
|
|
1345
1345
|
private publishProgress(
|
|
1346
1346
|
pubSub: PubSubEngine,
|
|
1347
1347
|
sessionId: string,
|
|
1348
|
+
ownerUserId: string,
|
|
1348
1349
|
connectionId: string,
|
|
1349
1350
|
phase: SyncProgressMessage['phase'],
|
|
1350
1351
|
message: string,
|
|
@@ -1360,10 +1361,7 @@ export class MCPResolver extends ResolverBase {
|
|
|
1360
1361
|
result
|
|
1361
1362
|
};
|
|
1362
1363
|
|
|
1363
|
-
pubSub.
|
|
1364
|
-
message: JSON.stringify(progressMessage),
|
|
1365
|
-
sessionId
|
|
1366
|
-
});
|
|
1364
|
+
publishStatusUpdate(pubSub, { sessionId, ownerUserId, message: JSON.stringify(progressMessage) });
|
|
1367
1365
|
}
|
|
1368
1366
|
|
|
1369
1367
|
/**
|