@memberjunction/server 5.50.0 → 6.1.0-edge.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/dist/agentSessions/SessionManager.d.ts +22 -1
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +18 -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 +71 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +383 -3
- 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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- 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/realtimeWidget/widgetGuestElevation.d.ts +22 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +36 -0
- package/dist/realtimeWidget/widgetGuestElevation.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 +22 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +90 -30
- 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/ReportResolver.d.ts.map +1 -1
- package/dist/resolvers/ReportResolver.js +11 -3
- package/dist/resolvers/ReportResolver.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/VersionHistoryResolver.d.ts.map +1 -1
- package/dist/resolvers/VersionHistoryResolver.js +8 -12
- package/dist/resolvers/VersionHistoryResolver.js.map +1 -1
- package/dist/services/TaskOrchestrator.d.ts.map +1 -1
- package/dist/services/TaskOrchestrator.js +17 -15
- package/dist/services/TaskOrchestrator.js.map +1 -1
- package/package.json +100 -100
- package/src/__tests__/RealtimeBridgeResolver.test.ts +6 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +420 -0
- package/src/__tests__/SessionManager.test.ts +106 -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/__tests__/widgetGuestElevation.test.ts +70 -2
- package/src/agentSessions/SessionManager.ts +32 -3
- 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 +276 -3
- 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 +4 -2
- package/src/multiTenancy/index.ts +92 -5
- package/src/realtimeWidget/widgetGuestElevation.ts +41 -0
- package/src/resolvers/ActionResolver.ts +15 -7
- package/src/resolvers/MCPResolver.ts +13 -15
- package/src/resolvers/RealtimeClientSessionResolver.ts +99 -29
- package/src/resolvers/RemoteBrowserActionResolver.ts +19 -25
- package/src/resolvers/ReportResolver.ts +11 -3
- 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/VersionHistoryResolver.ts +10 -13
- package/src/resolvers/__tests__/ReportResolver.test.ts +279 -0
- package/src/services/TaskOrchestrator.ts +17 -17
|
@@ -15,7 +15,6 @@ import { AppContext, UserPayload } from '../types.js';
|
|
|
15
15
|
import { LogError, LogStatus, UserInfo } from '@memberjunction/core';
|
|
16
16
|
import { TestEngine } from '@memberjunction/testing-engine';
|
|
17
17
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
18
|
-
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
|
|
19
18
|
import { startLivenessPulse } from '../generic/FireAndForgetHeartbeat.js';
|
|
20
19
|
import { TestRunVariables, TestLogMessage, TestRunResult as EngineTestRunResult } from '@memberjunction/testing-engine-base';
|
|
21
20
|
|
|
@@ -257,7 +256,7 @@ export class RunTestResolver extends ResolverBase {
|
|
|
257
256
|
): void {
|
|
258
257
|
// Keep-alive pulse so a long-running test never trips the client idle timeout.
|
|
259
258
|
// The client captures the testRunId from progress events for reconciliation.
|
|
260
|
-
const pulse = startLivenessPulse({ pubSub, sessionId: userPayload.sessionId, resolver: 'RunTestResolver' });
|
|
259
|
+
const pulse = startLivenessPulse({ pubSub, sessionId: userPayload.sessionId, ownerUserId: userPayload.userRecord.ID, resolver: 'RunTestResolver' });
|
|
261
260
|
|
|
262
261
|
this.executeTest(testId, verbose, environment, tags, variables, pubSub, userPayload, user)
|
|
263
262
|
.catch((error: unknown) => {
|
|
@@ -353,7 +352,7 @@ export class RunTestResolver extends ResolverBase {
|
|
|
353
352
|
// (Suite reconciliation isn't wired: suite progress carries the per-test run id, not the
|
|
354
353
|
// Test Suite Run id, so the client has no handle to reconcile against — the pulse is the
|
|
355
354
|
// protection here.)
|
|
356
|
-
const pulse = startLivenessPulse({ pubSub, sessionId: userPayload.sessionId, resolver: 'RunTestResolver' });
|
|
355
|
+
const pulse = startLivenessPulse({ pubSub, sessionId: userPayload.sessionId, ownerUserId: userPayload.userRecord.ID, resolver: 'RunTestResolver' });
|
|
357
356
|
|
|
358
357
|
this.executeSuite(
|
|
359
358
|
suiteId, verbose, environment, parallel, tags, variables,
|
|
@@ -494,51 +493,42 @@ export class RunTestResolver extends ResolverBase {
|
|
|
494
493
|
// ===== PubSub Publishing =====
|
|
495
494
|
|
|
496
495
|
private publishProgress(pubSub: PubSubEngine, data: TestExecutionStreamMessage, userPayload: UserPayload) {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}),
|
|
504
|
-
sessionId: userPayload.sessionId,
|
|
505
|
-
});
|
|
496
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
497
|
+
resolver: 'RunTestResolver',
|
|
498
|
+
type: 'TestExecutionProgress',
|
|
499
|
+
status: 'ok',
|
|
500
|
+
data,
|
|
501
|
+
}), userPayload);
|
|
506
502
|
}
|
|
507
503
|
|
|
508
504
|
private publishComplete(pubSub: PubSubEngine, userPayload: UserPayload, result: EngineTestRunResult) {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}),
|
|
522
|
-
sessionId: userPayload.sessionId,
|
|
523
|
-
});
|
|
505
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
506
|
+
resolver: 'RunTestResolver',
|
|
507
|
+
type: 'TestExecutionComplete',
|
|
508
|
+
status: 'ok',
|
|
509
|
+
data: {
|
|
510
|
+
sessionId: userPayload.sessionId,
|
|
511
|
+
testRunId: result.testRunId,
|
|
512
|
+
type: 'complete',
|
|
513
|
+
result: JSON.stringify(result),
|
|
514
|
+
timestamp: new Date()
|
|
515
|
+
},
|
|
516
|
+
}), userPayload);
|
|
524
517
|
}
|
|
525
518
|
|
|
526
519
|
private publishError(pubSub: PubSubEngine, userPayload: UserPayload, testId: string, errorMsg: string) {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}),
|
|
540
|
-
sessionId: userPayload.sessionId,
|
|
541
|
-
});
|
|
520
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
521
|
+
resolver: 'RunTestResolver',
|
|
522
|
+
type: 'TestExecutionError',
|
|
523
|
+
status: 'error',
|
|
524
|
+
data: {
|
|
525
|
+
sessionId: userPayload.sessionId,
|
|
526
|
+
testRunId: testId,
|
|
527
|
+
type: 'error',
|
|
528
|
+
errorMessage: errorMsg,
|
|
529
|
+
timestamp: new Date()
|
|
530
|
+
},
|
|
531
|
+
}), userPayload);
|
|
542
532
|
}
|
|
543
533
|
|
|
544
534
|
/**
|
|
@@ -551,24 +541,21 @@ export class RunTestResolver extends ResolverBase {
|
|
|
551
541
|
testId: string,
|
|
552
542
|
testRunResult: TestRunResult
|
|
553
543
|
) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
}),
|
|
570
|
-
sessionId: userPayload.sessionId,
|
|
571
|
-
});
|
|
544
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
545
|
+
resolver: 'RunTestResolver',
|
|
546
|
+
type: 'FireAndForgetComplete',
|
|
547
|
+
status: 'ok',
|
|
548
|
+
data: {
|
|
549
|
+
sessionId: userPayload.sessionId,
|
|
550
|
+
testId,
|
|
551
|
+
type: 'complete',
|
|
552
|
+
success: testRunResult.success,
|
|
553
|
+
errorMessage: testRunResult.errorMessage,
|
|
554
|
+
executionTimeMs: testRunResult.executionTimeMs,
|
|
555
|
+
result: testRunResult.result,
|
|
556
|
+
timestamp: new Date()
|
|
557
|
+
},
|
|
558
|
+
}), userPayload);
|
|
572
559
|
}
|
|
573
560
|
|
|
574
561
|
/**
|
|
@@ -580,23 +567,20 @@ export class RunTestResolver extends ResolverBase {
|
|
|
580
567
|
testId: string,
|
|
581
568
|
errorMessage: string
|
|
582
569
|
) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}),
|
|
598
|
-
sessionId: userPayload.sessionId,
|
|
599
|
-
});
|
|
570
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
571
|
+
resolver: 'RunTestResolver',
|
|
572
|
+
type: 'FireAndForgetComplete',
|
|
573
|
+
status: 'ok',
|
|
574
|
+
data: {
|
|
575
|
+
sessionId: userPayload.sessionId,
|
|
576
|
+
testId,
|
|
577
|
+
type: 'complete',
|
|
578
|
+
success: false,
|
|
579
|
+
errorMessage,
|
|
580
|
+
result: JSON.stringify({ success: false, errorMessage }),
|
|
581
|
+
timestamp: new Date()
|
|
582
|
+
},
|
|
583
|
+
}), userPayload);
|
|
600
584
|
}
|
|
601
585
|
|
|
602
586
|
/**
|
|
@@ -609,24 +593,21 @@ export class RunTestResolver extends ResolverBase {
|
|
|
609
593
|
suiteId: string,
|
|
610
594
|
suiteRunResult: TestSuiteRunResult
|
|
611
595
|
) {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}),
|
|
628
|
-
sessionId: userPayload.sessionId,
|
|
629
|
-
});
|
|
596
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
597
|
+
resolver: 'RunTestResolver',
|
|
598
|
+
type: 'FireAndForgetSuiteComplete',
|
|
599
|
+
status: 'ok',
|
|
600
|
+
data: {
|
|
601
|
+
sessionId: userPayload.sessionId,
|
|
602
|
+
suiteId,
|
|
603
|
+
type: 'complete',
|
|
604
|
+
success: suiteRunResult.success,
|
|
605
|
+
errorMessage: suiteRunResult.errorMessage,
|
|
606
|
+
executionTimeMs: suiteRunResult.executionTimeMs,
|
|
607
|
+
result: suiteRunResult.result,
|
|
608
|
+
timestamp: new Date()
|
|
609
|
+
},
|
|
610
|
+
}), userPayload);
|
|
630
611
|
}
|
|
631
612
|
|
|
632
613
|
/**
|
|
@@ -638,22 +619,19 @@ export class RunTestResolver extends ResolverBase {
|
|
|
638
619
|
suiteId: string,
|
|
639
620
|
errorMessage: string
|
|
640
621
|
) {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}),
|
|
656
|
-
sessionId: userPayload.sessionId,
|
|
657
|
-
});
|
|
622
|
+
this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
|
|
623
|
+
resolver: 'RunTestResolver',
|
|
624
|
+
type: 'FireAndForgetSuiteComplete',
|
|
625
|
+
status: 'ok',
|
|
626
|
+
data: {
|
|
627
|
+
sessionId: userPayload.sessionId,
|
|
628
|
+
suiteId,
|
|
629
|
+
type: 'complete',
|
|
630
|
+
success: false,
|
|
631
|
+
errorMessage,
|
|
632
|
+
result: JSON.stringify({ success: false, errorMessage }),
|
|
633
|
+
timestamp: new Date()
|
|
634
|
+
},
|
|
635
|
+
}), userPayload);
|
|
658
636
|
}
|
|
659
637
|
}
|
|
@@ -3,7 +3,7 @@ import { GraphQLJSON } from 'graphql-type-json';
|
|
|
3
3
|
import { AppContext } from '../types.js';
|
|
4
4
|
import { LogError, LogStatus, Metadata, RunView, UserInfo } from '@memberjunction/core';
|
|
5
5
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
6
|
-
import { SearchEngine, SearchResult as SearchEngineResult, SearchResultItem as SearchEngineResultItem, SearchProviderInfo, SearchContext,
|
|
6
|
+
import { SearchEngine, SearchResult as SearchEngineResult, SearchResultItem as SearchEngineResultItem, SearchProviderInfo, SearchContext, GetSearchScopePermissionResolver } from '@memberjunction/search-engine';
|
|
7
7
|
import { SearchEngineBase, MJAIAgentEntity } from '@memberjunction/core-entities';
|
|
8
8
|
import { UUIDsEqual } from '@memberjunction/global';
|
|
9
9
|
|
|
@@ -284,7 +284,7 @@ export class SearchKnowledgeResolver extends ResolverBase {
|
|
|
284
284
|
agentID: string | undefined,
|
|
285
285
|
): Promise<string | undefined> {
|
|
286
286
|
const agent = await this.loadAgent(agentID, user);
|
|
287
|
-
const resolver =
|
|
287
|
+
const resolver = GetSearchScopePermissionResolver();
|
|
288
288
|
for (const scopeID of scopeIDs) {
|
|
289
289
|
const verdict = await resolver.ResolveEffectivePermission({
|
|
290
290
|
User: user,
|
|
@@ -351,7 +351,7 @@ export class SearchKnowledgeResolver extends ResolverBase {
|
|
|
351
351
|
|
|
352
352
|
const ownedOrUnowned = scopes.filter(s => !s.OwnerUserID || UUIDsEqual(s.OwnerUserID, userID));
|
|
353
353
|
const agent = await this.loadAgent(agentID, currentUser);
|
|
354
|
-
const resolver =
|
|
354
|
+
const resolver = GetSearchScopePermissionResolver();
|
|
355
355
|
|
|
356
356
|
const visible: SearchScopeInfo[] = [];
|
|
357
357
|
for (const s of ownedOrUnowned) {
|
|
@@ -28,7 +28,7 @@ import { ResolverBase } from '../generic/ResolverBase.js';
|
|
|
28
28
|
import {
|
|
29
29
|
SearchEngine,
|
|
30
30
|
SearchContext,
|
|
31
|
-
|
|
31
|
+
GetSearchScopePermissionResolver,
|
|
32
32
|
SearchStreamEvent,
|
|
33
33
|
} from '@memberjunction/search-engine';
|
|
34
34
|
import { MJAIAgentEntity } from '@memberjunction/core-entities';
|
|
@@ -299,7 +299,7 @@ export class SearchKnowledgeStreamResolver extends ResolverBase {
|
|
|
299
299
|
agentID: string | undefined,
|
|
300
300
|
): Promise<string | undefined> {
|
|
301
301
|
const agent = await this.loadAgent(agentID, user);
|
|
302
|
-
const resolver =
|
|
302
|
+
const resolver = GetSearchScopePermissionResolver();
|
|
303
303
|
for (const scopeID of scopeIDs) {
|
|
304
304
|
const verdict = await resolver.ResolveEffectivePermission({
|
|
305
305
|
User: user,
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { Resolver, Mutation, Arg, Ctx, ObjectType, Field, Int, InputType, PubSub, PubSubEngine } from 'type-graphql';
|
|
2
|
-
import { AppContext } from '../types.js';
|
|
2
|
+
import { AppContext, UserPayload } from '../types.js';
|
|
3
3
|
import { LogError, LogStatus, CompositeKey, KeyValuePair } from '@memberjunction/core';
|
|
4
4
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
5
5
|
import { VersionHistoryEngine } from '@memberjunction/version-history';
|
|
6
6
|
import { CreateLabelParams, CreateLabelProgressUpdate, VersionLabelScope } from '@memberjunction/version-history';
|
|
7
7
|
import { KeyValuePairInput } from '../generic/KeyValuePairInput.js';
|
|
8
|
-
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
|
|
9
8
|
|
|
10
9
|
// =========================================================================
|
|
11
10
|
// GraphQL types
|
|
@@ -115,7 +114,7 @@ export class VersionHistoryResolver extends ResolverBase {
|
|
|
115
114
|
|
|
116
115
|
// Build progress callback that publishes to PubSub
|
|
117
116
|
const resolvedSessionId = sessionId ?? context.userPayload.sessionId;
|
|
118
|
-
const onProgress = this.buildProgressCallback(pubSub, resolvedSessionId);
|
|
117
|
+
const onProgress = this.buildProgressCallback(pubSub, resolvedSessionId, context.userPayload);
|
|
119
118
|
|
|
120
119
|
const params: CreateLabelParams = {
|
|
121
120
|
Name: input.Name,
|
|
@@ -160,18 +159,16 @@ export class VersionHistoryResolver extends ResolverBase {
|
|
|
160
159
|
*/
|
|
161
160
|
private buildProgressCallback(
|
|
162
161
|
pubSub: PubSubEngine,
|
|
163
|
-
sessionId: string
|
|
162
|
+
sessionId: string,
|
|
163
|
+
userPayload: UserPayload
|
|
164
164
|
): (progress: CreateLabelProgressUpdate) => void {
|
|
165
165
|
return (progress: CreateLabelProgressUpdate) => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}),
|
|
173
|
-
sessionId,
|
|
174
|
-
});
|
|
166
|
+
this.PublishStatusUpdate(pubSub, sessionId, JSON.stringify({
|
|
167
|
+
resolver: 'VersionHistoryResolver',
|
|
168
|
+
type: 'CreateLabelProgress',
|
|
169
|
+
status: 'ok',
|
|
170
|
+
data: progress,
|
|
171
|
+
}), userPayload);
|
|
175
172
|
};
|
|
176
173
|
}
|
|
177
174
|
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ReportResolverExtended.CreateReportFromConversationDetailID.
|
|
3
|
+
*
|
|
4
|
+
* Regression coverage for a SQL injection fix: ConversationDetailID (a plain GraphQL
|
|
5
|
+
* String arg) used to be interpolated directly into the WHERE clause. It is now bound
|
|
6
|
+
* via mssql's parameterized `request.input(...)`, so a hostile value must never appear
|
|
7
|
+
* spliced into the executed SQL text, and query structure must stay identical regardless
|
|
8
|
+
* of what the value contains.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
11
|
+
|
|
12
|
+
// ─── Hoisted mocks ────────────────────────────────────────────────────────
|
|
13
|
+
const { mockUserCacheUsers, mssqlState } = vi.hoisted(() => ({
|
|
14
|
+
mockUserCacheUsers: [] as Array<{ Email: string; ID: string }>,
|
|
15
|
+
mssqlState: {
|
|
16
|
+
inputCalls: [] as Array<{ name: string; type: unknown; value: unknown }>,
|
|
17
|
+
queryCalls: [] as string[],
|
|
18
|
+
poolArgs: [] as unknown[],
|
|
19
|
+
recordset: [] as Array<Record<string, unknown>>,
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Stub external deps before imports (mirrors resolverBase.rls.test.ts)
|
|
24
|
+
vi.mock('@memberjunction/sqlserver-dataprovider', () => ({
|
|
25
|
+
SQLServerDataProvider: class {},
|
|
26
|
+
UserCache: {
|
|
27
|
+
get Users() { return mockUserCacheUsers; },
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock('cloudevents', () => ({
|
|
32
|
+
CloudEvent: class {},
|
|
33
|
+
httpTransport: () => () => undefined,
|
|
34
|
+
emitterFor: () => () => undefined,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
vi.mock('type-graphql', () => ({
|
|
38
|
+
Resolver: () => () => undefined,
|
|
39
|
+
Mutation: () => () => undefined,
|
|
40
|
+
Query: () => () => undefined,
|
|
41
|
+
Subscription: () => () => undefined,
|
|
42
|
+
Ctx: () => () => undefined,
|
|
43
|
+
Arg: () => () => undefined,
|
|
44
|
+
PubSub: () => () => undefined,
|
|
45
|
+
Root: () => () => undefined,
|
|
46
|
+
ObjectType: () => () => undefined,
|
|
47
|
+
InputType: () => () => undefined,
|
|
48
|
+
Field: () => () => undefined,
|
|
49
|
+
FieldResolver: () => () => undefined,
|
|
50
|
+
Int: () => undefined,
|
|
51
|
+
Float: () => undefined,
|
|
52
|
+
registerEnumType: () => undefined,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
vi.mock('graphql', () => ({
|
|
56
|
+
GraphQLError: class extends Error {
|
|
57
|
+
constructor(msg: string) { super(msg); }
|
|
58
|
+
},
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
vi.mock('mssql', () => {
|
|
62
|
+
const UniqueIdentifier = { __marker: 'UniqueIdentifier' };
|
|
63
|
+
class Request {
|
|
64
|
+
constructor(pool: unknown) {
|
|
65
|
+
mssqlState.poolArgs.push(pool);
|
|
66
|
+
}
|
|
67
|
+
input(name: string, type: unknown, value: unknown) {
|
|
68
|
+
mssqlState.inputCalls.push({ name, type, value });
|
|
69
|
+
}
|
|
70
|
+
async query(sql: string) {
|
|
71
|
+
mssqlState.queryCalls.push(sql);
|
|
72
|
+
return { recordset: mssqlState.recordset };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { default: { Request, UniqueIdentifier }, Request, UniqueIdentifier };
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
vi.mock('@memberjunction/data-context', () => {
|
|
79
|
+
class DataContext {
|
|
80
|
+
LoadMetadata = vi.fn(async () => true);
|
|
81
|
+
}
|
|
82
|
+
(DataContext as unknown as { Clone: unknown }).Clone = vi.fn(async () => ({ ID: 'dctx-clone-1' }));
|
|
83
|
+
return { DataContext };
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
vi.mock('@memberjunction/api-keys', () => ({
|
|
87
|
+
GetAPIKeyEngine: vi.fn(),
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
vi.mock('@memberjunction/encryption', () => ({
|
|
91
|
+
EncryptionEngine: { Instance: {} },
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
vi.mock('@memberjunction/graphql-dataprovider', () => ({
|
|
95
|
+
FieldMapper: class { static Instance = { MapFieldsFromCodeNamesToDBNames: vi.fn() }; },
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
vi.mock('../../generic/PubSubManager.js', () => ({
|
|
99
|
+
PubSubManager: class { static Instance = { publish: vi.fn() }; },
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
vi.mock('../../generic/PushStatusResolver.js', () => ({
|
|
103
|
+
PUSH_STATUS_UPDATES_TOPIC: 'test-push-topic',
|
|
104
|
+
PushStatusNotification: class {},
|
|
105
|
+
PushStatusResolver: class {},
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
vi.mock('../../generic/CacheInvalidationResolver.js', () => ({
|
|
109
|
+
CACHE_INVALIDATION_TOPIC: 'test-cache-topic',
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
vi.mock('../../generic/RunViewResolver.js', () => ({
|
|
113
|
+
RunViewByIDInput: class {},
|
|
114
|
+
RunViewByNameInput: class {},
|
|
115
|
+
RunDynamicViewInput: class {},
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
vi.mock('../../generic/DeleteOptionsInput.js', () => ({
|
|
119
|
+
DeleteOptionsInput: class {},
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
vi.mock('../../types.js', () => ({
|
|
123
|
+
RunViewGenericParams: class {},
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
vi.mock('@memberjunction/core', async () => {
|
|
127
|
+
const actual = await vi.importActual<typeof import('@memberjunction/core')>('@memberjunction/core');
|
|
128
|
+
return {
|
|
129
|
+
...actual,
|
|
130
|
+
LogError: vi.fn(),
|
|
131
|
+
LogStatus: vi.fn(),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
vi.mock('@memberjunction/core-entities', () => ({}));
|
|
136
|
+
|
|
137
|
+
// ─── Import after mocks ──────────────────────────────────────────────────
|
|
138
|
+
import { ReportResolverExtended } from '../ReportResolver';
|
|
139
|
+
|
|
140
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
function makeReportEntity() {
|
|
143
|
+
return {
|
|
144
|
+
ID: 'report-1',
|
|
145
|
+
Name: '',
|
|
146
|
+
Description: '',
|
|
147
|
+
ConversationID: '',
|
|
148
|
+
ConversationDetailID: '',
|
|
149
|
+
DataContextID: '',
|
|
150
|
+
Configuration: '',
|
|
151
|
+
SharingScope: '',
|
|
152
|
+
UserID: '',
|
|
153
|
+
NewRecord: vi.fn(),
|
|
154
|
+
Save: vi.fn(async () => true),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function makeContext(
|
|
159
|
+
getEntityObject: () => ReturnType<typeof makeReportEntity>,
|
|
160
|
+
userPayload: Record<string, unknown> = { email: 'test@example.com' } // no apiKeyHash -> scope check no-ops
|
|
161
|
+
) {
|
|
162
|
+
const getEntityObjectSpy = vi.fn(async (_name: string, _contextUser: unknown) => getEntityObject());
|
|
163
|
+
const md = {
|
|
164
|
+
Entities: [
|
|
165
|
+
{ Name: 'MJ: Conversation Details', SchemaName: 'dbo', BaseView: 'vwConversationDetails' },
|
|
166
|
+
{ Name: 'MJ: Conversations', SchemaName: 'dbo', BaseView: 'vwConversations' },
|
|
167
|
+
],
|
|
168
|
+
GetEntityObject: getEntityObjectSpy,
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
context: {
|
|
172
|
+
dataSource: { __fakePool: true },
|
|
173
|
+
userPayload,
|
|
174
|
+
providers: [{ type: 'Read-Write', provider: md }],
|
|
175
|
+
} as unknown as Parameters<ReportResolverExtended['CreateReportFromConversationDetailID']>[1],
|
|
176
|
+
getEntityObjectSpy,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
describe('ReportResolverExtended.CreateReportFromConversationDetailID', () => {
|
|
181
|
+
let resolver: ReportResolverExtended;
|
|
182
|
+
|
|
183
|
+
beforeEach(() => {
|
|
184
|
+
resolver = new ReportResolverExtended();
|
|
185
|
+
mssqlState.inputCalls.length = 0;
|
|
186
|
+
mssqlState.queryCalls.length = 0;
|
|
187
|
+
mssqlState.poolArgs.length = 0;
|
|
188
|
+
mssqlState.recordset.length = 0;
|
|
189
|
+
mssqlState.recordset.push({
|
|
190
|
+
Message: JSON.stringify({ title: 'Test Report', userExplanation: 'exp' }),
|
|
191
|
+
ConversationID: 'conv-1',
|
|
192
|
+
DataContextID: 'dctx-1',
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
mockUserCacheUsers.length = 0;
|
|
196
|
+
mockUserCacheUsers.push({ Email: 'test@example.com', ID: 'user-1' });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('binds ConversationDetailID as a query parameter rather than splicing it into the SQL text', async () => {
|
|
200
|
+
const maliciousID = "1'; DROP TABLE MJ_Reports; --";
|
|
201
|
+
const { context } = makeContext(makeReportEntity);
|
|
202
|
+
|
|
203
|
+
const result = await resolver.CreateReportFromConversationDetailID(maliciousID, context);
|
|
204
|
+
|
|
205
|
+
expect(result.Success).toBe(true);
|
|
206
|
+
|
|
207
|
+
// The query text must use a bound parameter, never the raw value.
|
|
208
|
+
expect(mssqlState.queryCalls).toHaveLength(1);
|
|
209
|
+
expect(mssqlState.queryCalls[0]).toContain('@ConversationDetailID');
|
|
210
|
+
expect(mssqlState.queryCalls[0]).not.toContain(maliciousID);
|
|
211
|
+
expect(mssqlState.queryCalls[0]).not.toContain('DROP TABLE');
|
|
212
|
+
|
|
213
|
+
// The value must be bound through request.input, not string concatenation.
|
|
214
|
+
expect(mssqlState.inputCalls).toHaveLength(1);
|
|
215
|
+
expect(mssqlState.inputCalls[0].name).toBe('ConversationDetailID');
|
|
216
|
+
expect(mssqlState.inputCalls[0].value).toBe(maliciousID);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('does not alter query structure for values containing quotes, --, or OR 1=1', async () => {
|
|
220
|
+
const hostileID = "abc' OR '1'='1' --";
|
|
221
|
+
const { context } = makeContext(makeReportEntity);
|
|
222
|
+
|
|
223
|
+
await resolver.CreateReportFromConversationDetailID(hostileID, context);
|
|
224
|
+
|
|
225
|
+
expect(mssqlState.queryCalls[0]).toMatch(/WHERE\s+cd\.ID=@ConversationDetailID\s*$/);
|
|
226
|
+
expect(mssqlState.inputCalls[0].value).toBe(hostileID);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('still succeeds end to end for a normal GUID-shaped value', async () => {
|
|
230
|
+
const normalID = '12345678-1234-1234-1234-123456789012';
|
|
231
|
+
const { context } = makeContext(makeReportEntity);
|
|
232
|
+
|
|
233
|
+
const result = await resolver.CreateReportFromConversationDetailID(normalID, context);
|
|
234
|
+
|
|
235
|
+
expect(result.Success).toBe(true);
|
|
236
|
+
expect(result.ReportName).toBe('Test Report');
|
|
237
|
+
expect(mssqlState.inputCalls[0].value).toBe(normalID);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// §5.6 row 11 — the operating user must come from the stamped payload user
|
|
241
|
+
// (GetUserFromPayload), never a fresh UserCache lookup by email. A fresh lookup would
|
|
242
|
+
// silently drop any per-request API-key row-filter binding (APIKeyRowFilters /
|
|
243
|
+
// APIKeyActingContext) already stamped onto userPayload.userRecord, so a filtered key's
|
|
244
|
+
// report-creation work would run unfiltered.
|
|
245
|
+
describe('operating user resolution (§5.6 row 11)', () => {
|
|
246
|
+
it('uses userPayload.userRecord (with its stamped bindings) when present, not a fresh UserCache lookup', async () => {
|
|
247
|
+
const stampedUser = {
|
|
248
|
+
ID: 'user-1',
|
|
249
|
+
Email: 'test@example.com',
|
|
250
|
+
APIKeyRowFilters: [{ EntityID: 'e1', PermissionType: 'Create', FilterID: 'f1' }],
|
|
251
|
+
};
|
|
252
|
+
// A DIFFERENT UserCache entry for the same email — if the resolver fell back to a
|
|
253
|
+
// UserCache lookup, it would get THIS unstamped object instead.
|
|
254
|
+
mockUserCacheUsers.length = 0;
|
|
255
|
+
mockUserCacheUsers.push({ Email: 'test@example.com', ID: 'user-1' });
|
|
256
|
+
|
|
257
|
+
const { context, getEntityObjectSpy } = makeContext(makeReportEntity, {
|
|
258
|
+
email: 'test@example.com',
|
|
259
|
+
userRecord: stampedUser,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const result = await resolver.CreateReportFromConversationDetailID('12345678-1234-1234-1234-123456789012', context);
|
|
263
|
+
|
|
264
|
+
expect(result.Success).toBe(true);
|
|
265
|
+
// GetEntityObject's contextUser must be the SAME stamped object reference — proving
|
|
266
|
+
// the row-filter binding flows through to the entity work, not a fresh, unstamped one.
|
|
267
|
+
expect(getEntityObjectSpy.mock.calls[0][1]).toBe(stampedUser);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('falls back to a UserCache lookup by email when no payload user is present', async () => {
|
|
271
|
+
const { context, getEntityObjectSpy } = makeContext(makeReportEntity, { email: 'test@example.com' });
|
|
272
|
+
|
|
273
|
+
const result = await resolver.CreateReportFromConversationDetailID('12345678-1234-1234-1234-123456789012', context);
|
|
274
|
+
|
|
275
|
+
expect(result.Success).toBe(true);
|
|
276
|
+
expect((getEntityObjectSpy.mock.calls[0][1] as { ID: string }).ID).toBe('user-1');
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
});
|