@memberjunction/server 6.2.0-edge.7 → 6.2.0-edge.8

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 (86) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts.map +1 -1
  3. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js +2 -1
  4. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js.map +1 -1
  5. package/dist/context.d.ts.map +1 -1
  6. package/dist/context.js +6 -8
  7. package/dist/context.js.map +1 -1
  8. package/dist/generated/graphql-schemas/__mj.d.ts +839 -0
  9. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  10. package/dist/generated/graphql-schemas/__mj.js +14652 -10253
  11. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  12. package/dist/generic/ResolverBase.d.ts.map +1 -1
  13. package/dist/generic/ResolverBase.js +8 -0
  14. package/dist/generic/ResolverBase.js.map +1 -1
  15. package/dist/globalCors.d.ts.map +1 -1
  16. package/dist/globalCors.js +1 -0
  17. package/dist/globalCors.js.map +1 -1
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +44 -8
  21. package/dist/index.js.map +1 -1
  22. package/dist/logging/StartupLogger.d.ts +50 -0
  23. package/dist/logging/StartupLogger.d.ts.map +1 -1
  24. package/dist/logging/StartupLogger.js +15 -22
  25. package/dist/logging/StartupLogger.js.map +1 -1
  26. package/dist/middleware/BaseServerMiddleware.d.ts +21 -3
  27. package/dist/middleware/BaseServerMiddleware.d.ts.map +1 -1
  28. package/dist/middleware/BaseServerMiddleware.js +4 -2
  29. package/dist/middleware/BaseServerMiddleware.js.map +1 -1
  30. package/dist/resolvers/AgentRunStatusPublisher.d.ts +10 -0
  31. package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -1
  32. package/dist/resolvers/AgentRunStatusPublisher.js +18 -2
  33. package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -1
  34. package/dist/resolvers/AutotagPipelineResolver.d.ts +1 -1
  35. package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
  36. package/dist/resolvers/AutotagPipelineResolver.js +59 -15
  37. package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
  38. package/dist/resolvers/InfoResolver.js +1 -1
  39. package/dist/resolvers/InfoResolver.js.map +1 -1
  40. package/dist/resolvers/QuerySystemUserResolver.d.ts +2 -1
  41. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  42. package/dist/resolvers/QuerySystemUserResolver.js +12 -6
  43. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  44. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  45. package/dist/resolvers/SqlLoggingConfigResolver.js +7 -8
  46. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  47. package/dist/resolvers/TagGovernanceResolver.d.ts +12 -0
  48. package/dist/resolvers/TagGovernanceResolver.d.ts.map +1 -1
  49. package/dist/resolvers/TagGovernanceResolver.js +84 -0
  50. package/dist/resolvers/TagGovernanceResolver.js.map +1 -1
  51. package/dist/resolvers/TelemetryResolver.d.ts +13 -8
  52. package/dist/resolvers/TelemetryResolver.d.ts.map +1 -1
  53. package/dist/resolvers/TelemetryResolver.js +40 -9
  54. package/dist/resolvers/TelemetryResolver.js.map +1 -1
  55. package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -1
  56. package/dist/resolvers/meetings/meetingMutations.js +34 -4
  57. package/dist/resolvers/meetings/meetingMutations.js.map +1 -1
  58. package/dist/util.d.ts.map +1 -1
  59. package/dist/util.js +55 -2
  60. package/dist/util.js.map +1 -1
  61. package/package.json +107 -100
  62. package/src/__tests__/ResolverBase.clientOldValues.test.ts +4 -0
  63. package/src/__tests__/ResolverBase.diagnosticSenderRegistrations.test.ts +26 -0
  64. package/src/__tests__/RunAIAgentResolver.streaming.test.ts +28 -4
  65. package/src/__tests__/TagGovernanceAndAutotagResolvers.test.ts +261 -0
  66. package/src/__tests__/TagGovernanceResolver.test.ts +60 -2
  67. package/src/__tests__/globalCors.test.ts +12 -0
  68. package/src/__tests__/review-capture-valid-token-log.test.ts +98 -0
  69. package/src/__tests__/runStreams.fieldSecurity.test.ts +5 -5
  70. package/src/broadcast/BaseCacheInvalidationBroadcastProvider.ts +2 -1
  71. package/src/context.ts +7 -9
  72. package/src/generated/graphql-schemas/__mj.ts +9449 -6383
  73. package/src/generic/ResolverBase.ts +8 -0
  74. package/src/globalCors.ts +1 -0
  75. package/src/index.ts +45 -9
  76. package/src/logging/StartupLogger.ts +24 -0
  77. package/src/middleware/BaseServerMiddleware.ts +20 -3
  78. package/src/resolvers/AgentRunStatusPublisher.ts +27 -2
  79. package/src/resolvers/AutotagPipelineResolver.ts +57 -13
  80. package/src/resolvers/InfoResolver.ts +1 -1
  81. package/src/resolvers/QuerySystemUserResolver.ts +19 -7
  82. package/src/resolvers/SqlLoggingConfigResolver.ts +7 -9
  83. package/src/resolvers/TagGovernanceResolver.ts +65 -0
  84. package/src/resolvers/TelemetryResolver.ts +44 -9
  85. package/src/resolvers/meetings/meetingMutations.ts +37 -7
  86. package/src/util.ts +55 -3
@@ -0,0 +1,261 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ const {
4
+ mockTagEngineConfig,
5
+ mockMergeTags,
6
+ mockRecomputeCoOccurrence,
7
+ mockRunViewFn,
8
+ mockGetEntityObjectFn,
9
+ mockActionConfig,
10
+ mockRunAction,
11
+ mockPublish,
12
+ actionsList,
13
+ } = vi.hoisted(() => ({
14
+ mockTagEngineConfig: vi.fn().mockResolvedValue(undefined),
15
+ mockMergeTags: vi.fn(),
16
+ mockRecomputeCoOccurrence: vi.fn(),
17
+ mockRunViewFn: vi.fn(),
18
+ mockGetEntityObjectFn: vi.fn(),
19
+ mockActionConfig: vi.fn().mockResolvedValue(undefined),
20
+ mockRunAction: vi.fn(),
21
+ mockPublish: vi.fn(),
22
+ actionsList: [{ Name: 'Autotag and Vectorize Content' }] as Array<{ Name: string }>,
23
+ }));
24
+
25
+ vi.mock('type-graphql', () => {
26
+ const noop = () => () => undefined;
27
+ const passthrough = () => (target: unknown) => target;
28
+ return {
29
+ Resolver: passthrough,
30
+ Mutation: noop,
31
+ Query: noop,
32
+ Subscription: noop,
33
+ Arg: noop,
34
+ Args: noop,
35
+ Ctx: noop,
36
+ Root: noop,
37
+ ObjectType: passthrough,
38
+ InputType: passthrough,
39
+ ArgsType: passthrough,
40
+ Field: noop,
41
+ Float: Number,
42
+ Int: Number,
43
+ ID: String,
44
+ };
45
+ });
46
+
47
+ vi.mock('../generic/ResolverBase.js', () => ({
48
+ ResolverBase: class {
49
+ GetUserFromPayload(payload: unknown) {
50
+ if (!payload) return null;
51
+ return { ID: 'user-1', Email: 'u@example.com', Name: 'User' };
52
+ }
53
+ },
54
+ }));
55
+
56
+ vi.mock('@memberjunction/tag-engine', () => ({
57
+ TagEngine: {
58
+ Instance: {
59
+ Config: (...args: unknown[]) => mockTagEngineConfig(...args),
60
+ },
61
+ },
62
+ TagGovernanceEngine: {
63
+ Instance: {
64
+ MergeTags: (...args: unknown[]) => mockMergeTags(...args),
65
+ },
66
+ },
67
+ TagHealthJob: {
68
+ Instance: {
69
+ Run: vi.fn(),
70
+ },
71
+ },
72
+ DEFAULT_TAG_HEALTH_THRESHOLDS: {
73
+ minCoOccurrence: 5,
74
+ minEmbeddingSimilarity: 0.85,
75
+ minNameSimilarity: 0.5,
76
+ maxUsage: 2,
77
+ maxImplicitChildren: 25,
78
+ },
79
+ }));
80
+
81
+ vi.mock('@memberjunction/tag-engine-base', () => ({
82
+ TagCoOccurrenceEngine: {
83
+ Instance: {
84
+ RecomputeCoOccurrence: (...args: unknown[]) => mockRecomputeCoOccurrence(...args),
85
+ },
86
+ },
87
+ }));
88
+
89
+ vi.mock('@memberjunction/actions', () => ({
90
+ ActionEngineServer: {
91
+ Instance: {
92
+ Config: (...args: unknown[]) => mockActionConfig(...args),
93
+ get Actions() {
94
+ return actionsList;
95
+ },
96
+ RunAction: (...args: unknown[]) => mockRunAction(...args),
97
+ },
98
+ },
99
+ }));
100
+
101
+ vi.mock('../generic/PubSubManager.js', () => ({
102
+ PubSubManager: {
103
+ Instance: {
104
+ Publish: (...args: unknown[]) => mockPublish(...args),
105
+ },
106
+ },
107
+ }));
108
+
109
+ vi.mock('../util.js', () => ({
110
+ GetReadWriteProvider: () => ({
111
+ GetEntityObject: (...args: unknown[]) => mockGetEntityObjectFn(...args),
112
+ }),
113
+ }));
114
+
115
+ vi.mock('@memberjunction/core', async (importOriginal) => {
116
+ const actual = await importOriginal<typeof import('@memberjunction/core')>();
117
+ class MockRunView {
118
+ RunView = mockRunViewFn;
119
+ static FromMetadataProvider() {
120
+ return new MockRunView();
121
+ }
122
+ }
123
+ class MockMetadata {
124
+ GetEntityObject = mockGetEntityObjectFn;
125
+ }
126
+ return {
127
+ ...actual,
128
+ RunView: MockRunView,
129
+ Metadata: MockMetadata,
130
+ LogError: vi.fn(),
131
+ LogStatus: vi.fn(),
132
+ };
133
+ });
134
+
135
+ import { TagGovernanceResolver } from '../resolvers/TagGovernanceResolver.js';
136
+ import { AutotagPipelineResolver } from '../resolvers/AutotagPipelineResolver.js';
137
+ import type { AppContext } from '../types.js';
138
+
139
+ const mockUser = { ID: 'user-1', Email: 'u@example.com', Name: 'User' };
140
+
141
+ describe('TagGovernanceResolver', () => {
142
+ let resolver: TagGovernanceResolver;
143
+
144
+ beforeEach(() => {
145
+ vi.clearAllMocks();
146
+ resolver = new TagGovernanceResolver();
147
+ (resolver as unknown as Record<string, unknown>)['GetUserFromPayload'] = vi.fn().mockReturnValue(mockUser);
148
+ });
149
+
150
+ it('RecomputeTagCoOccurrence delegates to TagCoOccurrenceEngine.Instance.RecomputeCoOccurrence', async () => {
151
+ mockRecomputeCoOccurrence.mockResolvedValueOnce({ PairsUpdated: 12, PairsDeleted: 3 });
152
+
153
+ const res = await resolver.RecomputeTagCoOccurrence({} as AppContext);
154
+
155
+ expect(mockRecomputeCoOccurrence).toHaveBeenCalledWith(mockUser);
156
+ expect(res).toEqual({ PairsUpdated: 12, PairsDeleted: 3 });
157
+ });
158
+
159
+ it('MergeTags calls TagEngine.Instance.Config first and returns ItemsMoved and TaggedItemsMoved', async () => {
160
+ mockMergeTags.mockResolvedValueOnce({ ItemsMoved: 4, TaggedItemsMoved: 7 });
161
+
162
+ const res = await resolver.MergeTags(['src-1'], 'surviving-1', {} as AppContext);
163
+
164
+ expect(mockTagEngineConfig).toHaveBeenCalledWith(false, mockUser);
165
+ expect(mockMergeTags).toHaveBeenCalledWith(['src-1'], 'surviving-1', mockUser);
166
+ expect(res).toEqual({
167
+ Success: true,
168
+ ItemsMoved: 4,
169
+ TaggedItemsMoved: 7,
170
+ });
171
+ });
172
+
173
+ it('MergeTags catches engine errors and maps them into ErrorMessage', async () => {
174
+ mockMergeTags.mockRejectedValueOnce(new Error('Failed to load surviving tag'));
175
+
176
+ const res = await resolver.MergeTags(['src-1'], 'bad-id', {} as AppContext);
177
+
178
+ expect(res).toEqual({
179
+ Success: false,
180
+ ItemsMoved: 0,
181
+ TaggedItemsMoved: 0,
182
+ ErrorMessage: 'Failed to load surviving tag',
183
+ });
184
+ });
185
+ });
186
+
187
+ describe('AutotagPipelineResolver', () => {
188
+ let resolver: AutotagPipelineResolver;
189
+
190
+ beforeEach(() => {
191
+ vi.clearAllMocks();
192
+ resolver = new AutotagPipelineResolver();
193
+ (resolver as unknown as Record<string, unknown>)['GetUserFromPayload'] = vi.fn().mockReturnValue(mockUser);
194
+ });
195
+
196
+ it('rejects a client-supplied pipelineRunID that is not a valid UUID', async () => {
197
+ const res = await resolver.RunAutotagPipeline(undefined, undefined, 'not-a-uuid', {} as AppContext);
198
+
199
+ expect(res.Success).toBe(false);
200
+ expect(res.ErrorMessage).toMatch(/Invalid pipelineRunID/);
201
+ expect(mockRunViewFn).not.toHaveBeenCalled();
202
+ });
203
+
204
+ it('rejects a client-supplied pipelineRunID when a Content Process Run with that ID already exists', async () => {
205
+ const existingId = '11111111-2222-3333-4444-555555555555';
206
+ mockRunViewFn.mockResolvedValueOnce({
207
+ Success: true,
208
+ Results: [{ ID: existingId }],
209
+ });
210
+
211
+ const res = await resolver.RunAutotagPipeline(undefined, undefined, existingId, {} as AppContext);
212
+
213
+ expect(res.Success).toBe(false);
214
+ expect(res.ErrorMessage).toContain('already exists');
215
+ });
216
+
217
+ it('passes processed item count (not pct) to publishProgress and clears heartbeatTimer in finally', async () => {
218
+ const clearIntervalSpy = vi.spyOn(global, 'clearInterval');
219
+ mockGetEntityObjectFn.mockResolvedValue({
220
+ NewRecord: vi.fn(),
221
+ Save: vi.fn().mockResolvedValue(true),
222
+ LatestResult: { CompleteMessage: '' },
223
+ });
224
+
225
+ mockRunAction.mockImplementationOnce(async ({ Params }: { Params: Array<{ Name: string; Value: unknown }> }) => {
226
+ const cbParam = Params.find(p => p.Name === '__progressCallback');
227
+ const cb = cbParam?.Value as ((processed: number, total: number, currentItem?: string) => void) | undefined;
228
+ cb?.(3, 10, 'https://example.com/page-3');
229
+ return { Success: true, Message: '' };
230
+ });
231
+
232
+ const runInBg = (resolver as unknown as {
233
+ runPipelineInBackground(
234
+ pipelineRunID: string,
235
+ currentUser: unknown,
236
+ provider: unknown,
237
+ contentSourceIDs?: string[],
238
+ forceReprocess?: boolean,
239
+ ): Promise<void>;
240
+ }).runPipelineInBackground.bind(resolver);
241
+
242
+ await runInBg(
243
+ 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
244
+ mockUser,
245
+ { GetEntityObject: mockGetEntityObjectFn },
246
+ ['22222222-3333-4444-5555-666666666666'],
247
+ false,
248
+ );
249
+
250
+ const autotagEvent = mockPublish.mock.calls
251
+ .map(c => c[1] as Record<string, unknown>)
252
+ .find(n => n['Stage'] === 'autotag' && n['TotalItems'] === 10);
253
+
254
+ expect(autotagEvent).toBeDefined();
255
+ expect(autotagEvent?.['ProcessedItems']).toBe(3);
256
+ expect(autotagEvent?.['PercentComplete']).toBe(30);
257
+ expect(clearIntervalSpy).toHaveBeenCalled();
258
+ clearIntervalSpy.mockRestore();
259
+ });
260
+ });
261
+
@@ -10,12 +10,15 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
10
10
  * how errors are surfaced.
11
11
  */
12
12
 
13
- const { promoteCalls, rejectCalls, rebuildCalls, healthCalls, behaviorFlags } = vi.hoisted(() => ({
13
+ const { promoteCalls, rejectCalls, rebuildCalls, healthCalls, mergeCalls, recomputeCalls, configCalls, behaviorFlags } = vi.hoisted(() => ({
14
14
  promoteCalls: [] as Array<{ id: string; strategy: unknown; user: unknown }>,
15
15
  rejectCalls: [] as Array<{ id: string; notes: string | null; user: unknown }>,
16
16
  rebuildCalls: [] as Array<{ user: unknown }>,
17
17
  healthCalls: [] as Array<{ thresholds: unknown; user: unknown }>,
18
- behaviorFlags: { promoteThrows: false, rejectThrows: false, rebuildThrows: false, healthThrows: false },
18
+ mergeCalls: [] as Array<{ sourceTagIDs: string[]; survivingTagID: string; user: unknown }>,
19
+ recomputeCalls: [] as Array<{ user: unknown }>,
20
+ configCalls: [] as Array<{ forceRefresh: boolean; user: unknown }>,
21
+ behaviorFlags: { promoteThrows: false, rejectThrows: false, rebuildThrows: false, healthThrows: false, mergeThrows: false },
19
22
  }));
20
23
 
21
24
  vi.mock('@memberjunction/tag-engine', () => ({
@@ -30,10 +33,18 @@ vi.mock('@memberjunction/tag-engine', () => ({
30
33
  rejectCalls.push({ id, notes, user });
31
34
  if (behaviorFlags.rejectThrows) throw new Error('reject-fail');
32
35
  },
36
+ MergeTags: async (sourceTagIDs: string[], survivingTagID: string, user: unknown) => {
37
+ mergeCalls.push({ sourceTagIDs, survivingTagID, user });
38
+ if (behaviorFlags.mergeThrows) throw new Error('merge-fail');
39
+ return { ItemsMoved: 4, TaggedItemsMoved: 7 };
40
+ },
33
41
  },
34
42
  },
35
43
  TagEngine: {
36
44
  Instance: {
45
+ Config: async (forceRefresh: boolean, user: unknown) => {
46
+ configCalls.push({ forceRefresh, user });
47
+ },
37
48
  RebuildTagEmbeddings: async (user: unknown) => {
38
49
  rebuildCalls.push({ user });
39
50
  if (behaviorFlags.rebuildThrows) throw new Error('rebuild-fail');
@@ -59,6 +70,17 @@ vi.mock('@memberjunction/tag-engine', () => ({
59
70
  },
60
71
  }));
61
72
 
73
+ vi.mock('@memberjunction/tag-engine-base', () => ({
74
+ TagCoOccurrenceEngine: {
75
+ Instance: {
76
+ RecomputeCoOccurrence: async (user: unknown) => {
77
+ recomputeCalls.push({ user });
78
+ return { PairsUpdated: 12, PairsDeleted: 3 };
79
+ },
80
+ },
81
+ },
82
+ }));
83
+
62
84
  // Minimal type-graphql shim — we only need the decorators to be no-ops so the
63
85
  // resolver class can load. Method behavior is what we test.
64
86
  vi.mock('type-graphql', () => ({
@@ -98,10 +120,14 @@ describe('TagGovernanceResolver', () => {
98
120
  rejectCalls.length = 0;
99
121
  rebuildCalls.length = 0;
100
122
  healthCalls.length = 0;
123
+ mergeCalls.length = 0;
124
+ recomputeCalls.length = 0;
125
+ configCalls.length = 0;
101
126
  behaviorFlags.promoteThrows = false;
102
127
  behaviorFlags.rejectThrows = false;
103
128
  behaviorFlags.rebuildThrows = false;
104
129
  behaviorFlags.healthThrows = false;
130
+ behaviorFlags.mergeThrows = false;
105
131
  resolver = new TagGovernanceResolver();
106
132
  });
107
133
 
@@ -252,4 +278,36 @@ describe('TagGovernanceResolver', () => {
252
278
  expect(r.ErrorMessage).toBe('health-fail');
253
279
  });
254
280
  });
281
+
282
+ // ---- MergeTags ---------------------------------------------------------
283
+
284
+ describe('MergeTags', () => {
285
+ it('calls TagEngine.Instance.Config first and delegates to TagGovernanceEngine.Instance.MergeTags', async () => {
286
+ const r = await resolver.MergeTags(['src-1'], 'surviving-1', ctx() as never);
287
+ expect(r.Success).toBe(true);
288
+ expect(r.ItemsMoved).toBe(4);
289
+ expect(r.TaggedItemsMoved).toBe(7);
290
+ expect(configCalls).toHaveLength(1);
291
+ expect(mergeCalls[0]).toMatchObject({ sourceTagIDs: ['src-1'], survivingTagID: 'surviving-1' });
292
+ });
293
+
294
+ it('maps engine errors into ErrorMessage', async () => {
295
+ behaviorFlags.mergeThrows = true;
296
+ const r = await resolver.MergeTags(['src-1'], 'surviving-1', ctx() as never);
297
+ expect(r.Success).toBe(false);
298
+ expect(r.ItemsMoved).toBe(0);
299
+ expect(r.TaggedItemsMoved).toBe(0);
300
+ expect(r.ErrorMessage).toBe('merge-fail');
301
+ });
302
+ });
303
+
304
+ // ---- RecomputeTagCoOccurrence ------------------------------------------
305
+
306
+ describe('RecomputeTagCoOccurrence', () => {
307
+ it('delegates to TagCoOccurrenceEngine.Instance.RecomputeCoOccurrence', async () => {
308
+ const r = await resolver.RecomputeTagCoOccurrence(ctx() as never);
309
+ expect(r).toEqual({ PairsUpdated: 12, PairsDeleted: 3 });
310
+ expect(recomputeCalls).toHaveLength(1);
311
+ });
312
+ });
255
313
  });
@@ -215,3 +215,15 @@ describe('X-Powered-By', () => {
215
215
  expect(src.match(/app\.disable\('x-powered-by'\)/g)?.length ?? 0).toBeGreaterThanOrEqual(2);
216
216
  });
217
217
  });
218
+
219
+ describe('X-MJ-Diagnostic-Trigger', () => {
220
+ it('is exposed to the browser by the global CORS and by the GraphQL CORS', async () => {
221
+ const base = await startApp([]);
222
+ const res = await fetch(`${base}/core`, { headers: { Origin: OTHER_ORIGIN } });
223
+ expect(res.headers.get('access-control-expose-headers')).toContain('X-MJ-Diagnostic-Trigger');
224
+
225
+ const src = readFileSync(new URL('../index.ts', import.meta.url), 'utf8');
226
+ const graphqlCors = src.match(/app\.use\(\s*graphqlRootPath,\s*cors<cors\.CorsRequest>\(\{\s*exposedHeaders: \[([^\]]*)\]/);
227
+ expect(graphqlCors?.[1]).toContain('X-MJ-Diagnostic-Trigger');
228
+ });
229
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Tests the debug log GetUserPayload writes for a freshly verified token. All auth and database
3
+ * dependencies are mocked; no connection is opened.
4
+ */
5
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
6
+
7
+ vi.mock('jsonwebtoken', () => ({
8
+ default: {
9
+ decode: vi.fn(() => ({ iss: 'https://issuer.example.com/', exp: Math.floor(Date.now() / 1000) + 3600, sub: 'idp-subject' })),
10
+ verify: vi.fn((_token: string, _keys: unknown, _opts: unknown, cb: (err: Error | null, decoded: unknown) => void) =>
11
+ cb(null, { payload: { iss: 'https://issuer.example.com/', sub: 'idp-subject' } })
12
+ ),
13
+ },
14
+ }));
15
+
16
+ vi.mock('../auth/index.js', () => ({
17
+ GetSigningKeys: vi.fn(() => vi.fn()),
18
+ GetSystemUser: vi.fn(),
19
+ GetValidationOptions: vi.fn(() => ({ audience: 'mjapi' })),
20
+ ExtractUserInfoFromPayload: vi.fn(() => ({
21
+ fullName: 'Alice Smith',
22
+ firstName: 'Alice',
23
+ lastName: 'Smith',
24
+ email: 'alice@example.com',
25
+ preferredUsername: 'alice@example.com',
26
+ })),
27
+ VerifyUserRecord: vi.fn(async () => ({ ID: 'USER-ID-1', IsActive: true, Email: 'alice@example.com', UserRoles: [] })),
28
+ }));
29
+
30
+ vi.mock('@memberjunction/auth-providers', () => ({
31
+ TokenExpiredError: class extends Error {},
32
+ AuthProviderFactory: { Instance: { getByIssuer: vi.fn(() => ({})) } },
33
+ }));
34
+
35
+ vi.mock('../logging/StartupLogger.js', () => ({
36
+ StartupLogger: { resolveLevelFromConfig: vi.fn(() => 'debug') },
37
+ }));
38
+
39
+ vi.mock('../util.js', () => ({
40
+ GetReadOnlyDataSource: vi.fn(() => undefined),
41
+ GetReadWriteDataSource: vi.fn(() => undefined),
42
+ }));
43
+
44
+ vi.mock('../config.js', () => ({
45
+ configInfo: { databaseSettings: { metadataCacheRefreshInterval: 0 } },
46
+ userEmailMap: {},
47
+ apiKey: 'test-api-key',
48
+ mj_core_schema: '__mj',
49
+ }));
50
+
51
+ vi.mock('@memberjunction/sqlserver-dataprovider', () => ({
52
+ SQLServerDataProvider: class {},
53
+ SQLServerProviderConfigData: class {},
54
+ }));
55
+
56
+ vi.mock('@memberjunction/postgresql-dataprovider', () => ({
57
+ PostgreSQLDataProvider: class {},
58
+ PostgreSQLProviderConfigData: class {},
59
+ }));
60
+
61
+ vi.mock('@memberjunction/api-keys', () => ({
62
+ GetAPIKeyEngine: vi.fn(),
63
+ }));
64
+
65
+ vi.mock('@memberjunction/core', async (importOriginal) => {
66
+ const actual = await importOriginal<typeof import('@memberjunction/core')>();
67
+ return { ...actual, Metadata: { Provider: undefined } };
68
+ });
69
+
70
+ import { GetUserPayload } from '../context.js';
71
+
72
+ describe('GetUserPayload valid-token debug log', () => {
73
+ let logged: string[];
74
+
75
+ beforeEach(() => {
76
+ logged = [];
77
+ vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
78
+ logged.push(args.map(String).join(' '));
79
+ });
80
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
81
+ vi.spyOn(console, 'error').mockImplementation(() => {});
82
+ });
83
+
84
+ afterEach(() => {
85
+ vi.restoreAllMocks();
86
+ });
87
+
88
+ it('logs the user ID, not the name or email', async () => {
89
+ const result = await GetUserPayload('Bearer token-for-log-test', 'session-1', []);
90
+
91
+ expect(result.userRecord?.ID).toBe('USER-ID-1');
92
+ const tokenLines = logged.filter((line) => line.includes('Valid token'));
93
+ expect(tokenLines).toHaveLength(1);
94
+ expect(tokenLines[0]).toContain('USER-ID-1');
95
+ expect(tokenLines[0]).not.toContain('Alice');
96
+ expect(tokenLines[0]).not.toContain('alice@example.com');
97
+ });
98
+ });
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Field-level security on the agent-run and test-run streams.
3
3
  *
4
- * Each of these publishes an entity object's `GetAll()` to the caller's session: the agent run on
5
- * every progress and streaming message and in the final result, the test run on every progress
6
- * message. The objects come from long-running engines and can have been built for another
7
- * identity, so the transport projects them for the recipient. The objects here are FULL WIDTH,
4
+ * Each of these publishes an entity object's values to the caller's session: the agent run on
5
+ * every progress message and in the final result, its summary on every streaming message, and the
6
+ * test run on every progress message. The objects come from long-running engines and can have
7
+ * been built for another identity, so the transport projects them for the recipient. The objects here are FULL WIDTH,
8
8
  * and the recipient may not read `Salary`.
9
9
  */
10
10
  import 'reflect-metadata';
@@ -64,8 +64,8 @@ describe('AgentRunStatusPublisher: the run is projected for the recipient', () =
64
64
  expect(messages()).toHaveLength(2);
65
65
  for (const message of messages()) {
66
66
  expect(message).not.toContain(String(SECRET_SALARY));
67
- expect(message).toContain('"Name":"Ada"');
68
67
  }
68
+ expect(messages()[0]).toContain('"Name":"Ada"');
69
69
  });
70
70
  });
71
71
 
@@ -153,7 +153,8 @@ export const DEFAULT_HIGH_CHURN_SYSTEM_ENTITIES: readonly string[] = [
153
153
  'MJ: AI Prompt Runs',
154
154
  'MJ: AI Agent Runs',
155
155
  'MJ: AI Agent Run Steps',
156
- 'MJ: Query Execution Histories',
156
+ 'MJ: Diagnostic Session Chunks',
157
+ 'MJ: Diagnostic Bundles',
157
158
  ];
158
159
 
159
160
  /**
package/src/context.ts CHANGED
@@ -21,7 +21,7 @@ import { GetReadOnlyDataSource, GetReadWriteDataSource } from './util.js';
21
21
  import { v4 as uuidv4 } from 'uuid';
22
22
  import e from 'express';
23
23
  import type { RequestHandler, Request, Response, NextFunction } from 'express';
24
- import { DatabaseProviderBase, UserInfo, type IMetadataProvider, type MagicLinkScope, type ReturningVisitorContext, type WidgetGuestContext } from '@memberjunction/core';
24
+ import { DatabaseProviderBase, UserInfo, GetDiagnosticContext, type IMetadataProvider, type MagicLinkScope, type ReturningVisitorContext, type WidgetGuestContext } from '@memberjunction/core';
25
25
  import { SQLServerDataProvider, SQLServerProviderConfigData } from '@memberjunction/sqlserver-dataprovider';
26
26
  import { PostgreSQLDataProvider, PostgreSQLProviderConfigData } from '@memberjunction/postgresql-dataprovider';
27
27
  import { Metadata, IsValidTimeZone, TIME_ZONE_HEADER } from '@memberjunction/core';
@@ -49,7 +49,7 @@ function isDebugLogLevel(): boolean {
49
49
  if (isDebugLevelCache === undefined) {
50
50
  isDebugLevelCache = StartupLogger.resolveLevelFromConfig() === 'debug';
51
51
  }
52
- return isDebugLevelCache;
52
+ return isDebugLevelCache || GetDiagnosticContext()?.sessionId != null;
53
53
  }
54
54
 
55
55
  const SESSION_AUDIT_TYPE = 'Session Established';
@@ -371,13 +371,6 @@ const verifyAsync = async (issuer: string, token: string): Promise<jwt.JwtPayloa
371
371
  jwt.verify(token, GetSigningKeys(issuer), verifyOptions, (err, jwt) => {
372
372
  if (jwt && typeof jwt !== 'string' && !err) {
373
373
  const payload = jwt.payload ?? jwt;
374
-
375
- // Per-request token confirmation — debug-only (one of the worst
376
- // "constantly on" offenders on an authenticated server).
377
- if (isDebugLogLevel()) {
378
- const userInfo = ExtractUserInfoFromPayload(payload);
379
- console.log(`Valid token: ${userInfo.fullName || 'Unknown'} (${userInfo.email || userInfo.preferredUsername || 'Unknown'})`);
380
- }
381
374
  resolve(payload);
382
375
  } else {
383
376
  console.warn('Invalid token');
@@ -578,6 +571,11 @@ export const GetUserPayload = async (
578
571
  // for everything else.
579
572
  const sessionUser = BuildMagicLinkSessionUser(userRecord, payload);
580
573
 
574
+ // Debug-only confirmation of a freshly verified token. It names the user by ID only.
575
+ if (!cachedAuth && isDebugLogLevel()) {
576
+ console.log(`Valid token: user ${sessionUser.ID}`);
577
+ }
578
+
581
579
  // Session-established audit — once per issued token (deduped), fire-and-forget so
582
580
  // it never adds latency to the request. The dedup mark is set synchronously here.
583
581
  if (markSessionAuditSeen(sessionAuditKey('sess', payload))) {