@memberjunction/server 5.49.0 → 5.51.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.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  3. package/dist/agentSessions/SessionManager.js +6 -1
  4. package/dist/agentSessions/SessionManager.js.map +1 -1
  5. package/dist/auth/newUsers.d.ts.map +1 -1
  6. package/dist/auth/newUsers.js +6 -4
  7. package/dist/auth/newUsers.js.map +1 -1
  8. package/dist/generated/generated.d.ts +259 -14
  9. package/dist/generated/generated.d.ts.map +1 -1
  10. package/dist/generated/generated.js +1380 -73
  11. package/dist/generated/generated.js.map +1 -1
  12. package/dist/realtimeWidget/WidgetSessionService.d.ts +12 -1
  13. package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -1
  14. package/dist/realtimeWidget/WidgetSessionService.js +23 -8
  15. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
  16. package/dist/realtimeWidget/widgetGuestElevation.d.ts +22 -0
  17. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  18. package/dist/realtimeWidget/widgetGuestElevation.js +36 -0
  19. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  20. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  21. package/dist/resolvers/IntegrationDiscoveryResolver.js +9 -4
  22. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  23. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +21 -0
  24. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  25. package/dist/resolvers/RealtimeClientSessionResolver.js +75 -16
  26. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  27. package/package.json +91 -89
  28. package/src/__tests__/RealtimeClientSessionResolver.test.ts +420 -0
  29. package/src/__tests__/SessionManager.test.ts +62 -0
  30. package/src/__tests__/WidgetSessionService.test.ts +96 -0
  31. package/src/__tests__/widgetGuestElevation.test.ts +70 -2
  32. package/src/agentSessions/SessionManager.ts +6 -1
  33. package/src/auth/newUsers.ts +6 -5
  34. package/src/generated/generated.ts +967 -55
  35. package/src/realtimeWidget/WidgetSessionService.ts +24 -8
  36. package/src/realtimeWidget/widgetGuestElevation.ts +41 -0
  37. package/src/resolvers/IntegrationDiscoveryResolver.ts +9 -4
  38. package/src/resolvers/RealtimeClientSessionResolver.ts +84 -16
@@ -1,6 +1,22 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import type { DatabaseProviderBase, UserInfo } from '@memberjunction/core';
3
- import { resolveWidgetGuestRunContext, elevateUserPayload } from '../realtimeWidget/widgetGuestElevation.js';
3
+
4
+ // Controls what UserCache.Instance.GetSystemUser() serves — undefined simulates an unpopulated
5
+ // cache (no system user available), the fail-closed path.
6
+ const getSystemUserMock = vi.fn<[], UserInfo | undefined>();
7
+ vi.mock('@memberjunction/sqlserver-dataprovider', () => ({
8
+ UserCache: {
9
+ get Instance() {
10
+ return { GetSystemUser: getSystemUserMock };
11
+ },
12
+ },
13
+ }));
14
+
15
+ import {
16
+ resolveWidgetGuestRunContext,
17
+ elevateUserPayload,
18
+ ResolveScopedAnonymousRunUser,
19
+ } from '../realtimeWidget/widgetGuestElevation.js';
4
20
  import type { UserPayload } from '../types.js';
5
21
 
6
22
  /** A provider stub — the guard paths under test return before ever touching the provider. */
@@ -31,6 +47,58 @@ describe('widgetGuestElevation — resolveWidgetGuestRunContext (guard paths, no
31
47
  });
32
48
  });
33
49
 
50
+ describe('widgetGuestElevation — ResolveScopedAnonymousRunUser (issue #3371)', () => {
51
+ const systemUser = { ID: 'system-1', Email: 'system@system.org' } as UserInfo;
52
+
53
+ /** Builds a UserInfo carrying the given per-session guest flags. */
54
+ function userWith(flags: Partial<UserInfo>): UserInfo {
55
+ return { ID: 'anon-1', Email: 'anonymous@magic-link.local', ...flags } as UserInfo;
56
+ }
57
+
58
+ beforeEach(() => {
59
+ getSystemUserMock.mockReset();
60
+ getSystemUserMock.mockReturnValue(systemUser);
61
+ });
62
+
63
+ it('returns the caller unchanged for a normal authenticated user', () => {
64
+ const named = userWith({ IsMagicLinkAnonymous: false, MagicLinkScope: { ResourceID: 'res-1' } });
65
+ expect(ResolveScopedAnonymousRunUser(named)).toBe(named);
66
+ expect(getSystemUserMock).not.toHaveBeenCalled();
67
+ });
68
+
69
+ it('returns the caller unchanged for an anonymous session with no resource scope', () => {
70
+ const unscoped = userWith({ IsMagicLinkAnonymous: true, MagicLinkScope: undefined });
71
+ expect(ResolveScopedAnonymousRunUser(unscoped)).toBe(unscoped);
72
+ expect(getSystemUserMock).not.toHaveBeenCalled();
73
+ });
74
+
75
+ it('returns the caller unchanged for a scoped anonymous session with an empty ResourceID', () => {
76
+ const emptyScope = userWith({ IsMagicLinkAnonymous: true, MagicLinkScope: { ResourceID: '' } });
77
+ expect(ResolveScopedAnonymousRunUser(emptyScope)).toBe(emptyScope);
78
+ });
79
+
80
+ it('returns the caller unchanged for a PUBLIC WEB-WIDGET guest (widget guests keep their own path)', () => {
81
+ const widgetGuest = userWith({
82
+ IsMagicLinkAnonymous: true,
83
+ MagicLinkScope: { ResourceID: 'res-1' },
84
+ WidgetGuestContext: { WidgetID: 'widget-1' },
85
+ });
86
+ expect(ResolveScopedAnonymousRunUser(widgetGuest)).toBe(widgetGuest);
87
+ expect(getSystemUserMock).not.toHaveBeenCalled();
88
+ });
89
+
90
+ it('FAILS CLOSED to the caller when no system user is available', () => {
91
+ getSystemUserMock.mockReturnValue(undefined);
92
+ const scopedAnon = userWith({ IsMagicLinkAnonymous: true, MagicLinkScope: { ResourceID: 'res-1' } });
93
+ expect(ResolveScopedAnonymousRunUser(scopedAnon)).toBe(scopedAnon);
94
+ });
95
+
96
+ it('returns the SYSTEM user for a scoped anonymous (non-widget) magic-link session', () => {
97
+ const scopedAnon = userWith({ IsMagicLinkAnonymous: true, MagicLinkScope: { ResourceID: 'res-1' } });
98
+ expect(ResolveScopedAnonymousRunUser(scopedAnon)).toBe(systemUser);
99
+ });
100
+ });
101
+
34
102
  describe('widgetGuestElevation — elevateUserPayload', () => {
35
103
  it('swaps in the elevated principal while preserving the guest sessionId for PubSub routing', () => {
36
104
  const guestPayload = payloadWith({ IsMagicLinkAnonymous: true });
@@ -8,6 +8,7 @@ import { AIAgentPermissionHelper } from '@memberjunction/ai-engine-base';
8
8
  import { RealtimeClientSessionService, RealtimeChannelServerHost } from '@memberjunction/ai-agents';
9
9
  import { GetHostInstanceID } from './HostInstance.js';
10
10
  import { writeReturningVisitorRecap } from './ReturningVisitorRecap.js';
11
+ import { ResolveScopedAnonymousRunUser } from '../realtimeWidget/widgetGuestElevation.js';
11
12
 
12
13
  /** Entity names — centralised so the `MJ:`-prefix convention is applied in exactly one place. */
13
14
  const SESSION_ENTITY = 'MJ: AI Agent Sessions';
@@ -230,10 +231,14 @@ export class SessionManager {
230
231
  return;
231
232
  }
232
233
  try {
234
+ // SCOPED-ANONYMOUS ELEVATION (issue #3371): the runs were CREATED under the system user
235
+ // for a scoped anonymous session, so an owner-initiated (or error) close must finalize
236
+ // under it too — the caller's role holds no grants on the AI run entities. Janitor and
237
+ // shutdown sweeps already close as the system user and pass through unchanged.
233
238
  await new RealtimeClientSessionService().FinalizeCoAgentRun(
234
239
  config.coAgentRunID ?? null,
235
240
  config.promptRunID ?? null,
236
- contextUser,
241
+ ResolveScopedAnonymousRunUser(contextUser),
237
242
  provider,
238
243
  true,
239
244
  config.coAgentRunStepID ?? null,
@@ -2,7 +2,7 @@ import { ApplicationInfo, DatabaseProviderBase, EntitySaveOptions, LogError, Log
2
2
  import { RegisterClass } from "@memberjunction/global";
3
3
  import { UserCache } from "@memberjunction/sqlserver-dataprovider";
4
4
  import { configInfo } from "../config.js";
5
- import { MJUserEntity, MJUserRoleEntity, MJUserApplicationEntity, MJUserApplicationEntityEntity, MJApplicationEntityType, MJApplicationEntityEntityType } from "@memberjunction/core-entities";
5
+ import { MJUserEntity, MJUserRoleEntity, MJUserApplicationEntity, MJUserApplicationEntityEntity, MJApplicationEntityType, MJApplicationEntityEntityType, UserInfoEngine } from "@memberjunction/core-entities";
6
6
 
7
7
  export class NewUserBase {
8
8
  public async createNewUser(firstName: string, lastName: string, email: string, linkedRecordType: string = 'None', linkedEntityId?: string, linkedEntityRecordId?: string): Promise<MJUserEntity | null> {
@@ -90,10 +90,11 @@ export class NewUserBase {
90
90
  }
91
91
  } else {
92
92
  LogStatus(`No UserApplications configured, using DefaultForNewUser applications for new user ${user.Name}`);
93
- applicationsToCreate = md.Applications
94
- .filter(a => a.DefaultForNewUser)
95
- .sort((a, b) => (a.DefaultSequence ?? 100) - (b.DefaultSequence ?? 100));
96
- LogStatus(`Found ${applicationsToCreate.length} applications with DefaultForNewUser=true`);
93
+ // Bug F2: use the shared selector, which filters to Active apps flagged
94
+ // DefaultForNewUser (in DefaultSequence order). This path previously omitted the
95
+ // Status = 'Active' check, so an inactive default app could be provisioned here.
96
+ applicationsToCreate = UserInfoEngine.GetDefaultApplicationsForNewUser(md);
97
+ LogStatus(`Found ${applicationsToCreate.length} active applications with DefaultForNewUser=true`);
97
98
  }
98
99
 
99
100
  for (const [appIndex, application] of applicationsToCreate.entries()) {