@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
@@ -3,8 +3,11 @@ import { AppContext } from '../types.js';
3
3
  import {
4
4
  TelemetryManager,
5
5
  TelemetryCategory,
6
- TelemetryInsightSeverity
6
+ TelemetryInsightSeverity,
7
+ AuthorizationEvaluator,
7
8
  } from '@memberjunction/core';
9
+ import { IsScopeLimitedPrincipal } from '../auth/scopeLimitedPrincipal.js';
10
+ import { GetReadOnlyProvider } from '../util.js';
8
11
 
9
12
  // ============================================================================
10
13
  // GraphQL Types for TelemetryEvent
@@ -318,12 +321,38 @@ export class TelemetryInsightFilterInput {
318
321
 
319
322
  @Resolver()
320
323
  export class TelemetryResolver {
324
+ /**
325
+ * Enforces that the caller is an authenticated, non-scope-limited user who holds either
326
+ * Owner privileges or the `'Diagnostics: Run Sessions'` authorization.
327
+ */
328
+ private requireDiagnosticsAccess(context: AppContext): void {
329
+ const user = context?.userPayload?.userRecord;
330
+ if (!user || IsScopeLimitedPrincipal(user)) {
331
+ throw new Error('Access denied: server telemetry requires an authenticated non-scope-limited user.');
332
+ }
333
+ if (user.Type?.trim().toLowerCase() === 'owner') {
334
+ return;
335
+ }
336
+ const authorizations = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true })?.Authorizations ?? [];
337
+ const diagAuth = authorizations.find(
338
+ (a) => a.Name?.trim().toLowerCase() === 'diagnostics: run sessions'
339
+ );
340
+ if (diagAuth) {
341
+ const evaluator = new AuthorizationEvaluator();
342
+ if (evaluator.UserCanExecuteWithAncestors(diagAuth, user, authorizations)) {
343
+ return;
344
+ }
345
+ }
346
+ throw new Error('Access denied: server telemetry requires Owner privileges or the "Diagnostics: Run Sessions" authorization.');
347
+ }
348
+
321
349
  /**
322
350
  * Get telemetry settings
323
- * Accessible to any authenticated user - telemetry data is not sensitive
351
+ * Requires Owner privileges or 'Diagnostics: Run Sessions' authorization
324
352
  */
325
353
  @Query(() => TelemetrySettingsGQL)
326
- GetServerTelemetrySettings(@Ctx() _context: AppContext): TelemetrySettingsGQL {
354
+ GetServerTelemetrySettings(@Ctx() context: AppContext): TelemetrySettingsGQL {
355
+ this.requireDiagnosticsAccess(context);
327
356
  const tm = TelemetryManager.Instance;
328
357
  const settings = tm.Settings;
329
358
 
@@ -365,7 +394,8 @@ export class TelemetryResolver {
365
394
  * Get telemetry statistics
366
395
  */
367
396
  @Query(() => TelemetryStatsGQL)
368
- GetServerTelemetryStats(@Ctx() _context: AppContext): TelemetryStatsGQL {
397
+ GetServerTelemetryStats(@Ctx() context: AppContext): TelemetryStatsGQL {
398
+ this.requireDiagnosticsAccess(context);
369
399
  const tm = TelemetryManager.Instance;
370
400
  const stats = tm.GetStats();
371
401
 
@@ -391,9 +421,10 @@ export class TelemetryResolver {
391
421
  */
392
422
  @Query(() => [TelemetryEventGQL])
393
423
  GetServerTelemetryEvents(
394
- @Ctx() _context: AppContext,
424
+ @Ctx() context: AppContext,
395
425
  @Arg('filter', () => TelemetryEventFilterInput, { nullable: true }) filter?: TelemetryEventFilterInput
396
426
  ): TelemetryEventGQL[] {
427
+ this.requireDiagnosticsAccess(context);
397
428
  const tm = TelemetryManager.Instance;
398
429
 
399
430
  const events = tm.GetEvents({
@@ -427,9 +458,10 @@ export class TelemetryResolver {
427
458
  */
428
459
  @Query(() => [TelemetryPatternGQL])
429
460
  GetServerTelemetryPatterns(
430
- @Ctx() _context: AppContext,
461
+ @Ctx() context: AppContext,
431
462
  @Arg('filter', () => TelemetryPatternFilterInput, { nullable: true }) filter?: TelemetryPatternFilterInput
432
463
  ): TelemetryPatternGQL[] {
464
+ this.requireDiagnosticsAccess(context);
433
465
  const tm = TelemetryManager.Instance;
434
466
 
435
467
  const patterns = tm.GetPatterns({
@@ -468,9 +500,10 @@ export class TelemetryResolver {
468
500
  */
469
501
  @Query(() => [TelemetryInsightGQL])
470
502
  GetServerTelemetryInsights(
471
- @Ctx() _context: AppContext,
503
+ @Ctx() context: AppContext,
472
504
  @Arg('filter', () => TelemetryInsightFilterInput, { nullable: true }) filter?: TelemetryInsightFilterInput
473
505
  ): TelemetryInsightGQL[] {
506
+ this.requireDiagnosticsAccess(context);
474
507
  const tm = TelemetryManager.Instance;
475
508
 
476
509
  const insights = tm.GetInsights({
@@ -500,9 +533,10 @@ export class TelemetryResolver {
500
533
  */
501
534
  @Query(() => [TelemetryPatternGQL])
502
535
  GetServerTelemetryDuplicates(
503
- @Ctx() _context: AppContext,
536
+ @Ctx() context: AppContext,
504
537
  @Arg('minCount', () => Int, { defaultValue: 2 }) minCount: number
505
538
  ): TelemetryPatternGQL[] {
539
+ this.requireDiagnosticsAccess(context);
506
540
  const tm = TelemetryManager.Instance;
507
541
  const patterns = tm.GetDuplicates(minCount);
508
542
 
@@ -534,7 +568,8 @@ export class TelemetryResolver {
534
568
  * Get currently active (in-progress) events
535
569
  */
536
570
  @Query(() => [TelemetryEventGQL])
537
- GetServerTelemetryActiveEvents(@Ctx() _context: AppContext): TelemetryEventGQL[] {
571
+ GetServerTelemetryActiveEvents(@Ctx() context: AppContext): TelemetryEventGQL[] {
572
+ this.requireDiagnosticsAccess(context);
538
573
  const tm = TelemetryManager.Instance;
539
574
  const events = tm.GetActiveEvents();
540
575
 
@@ -5,7 +5,7 @@ import {
5
5
  RunView,
6
6
  UserInfo,
7
7
  } from '@memberjunction/core';
8
- import { EscapeSQLString, UUIDsEqual } from '@memberjunction/global';
8
+ import { EscapeSQLString, IndexByUUID, UUIDsEqual } from '@memberjunction/global';
9
9
  import type {
10
10
  MJMeetingEntity,
11
11
  MJMeetingParticipantEntity,
@@ -25,6 +25,7 @@ import {
25
25
  UpdateMeetingInput,
26
26
  RSVPMeetingInput,
27
27
  MeetingParticipantType,
28
+ MeetingParticipantInput,
28
29
  } from './meetingTypes.js';
29
30
  import * as defaultHelpers from './meetingHelpers.js';
30
31
 
@@ -34,6 +35,39 @@ function resolveHelpers(overrides?: Partial<MeetingHelpers>): MeetingHelpers {
34
35
  return { ...defaultHelpers, ...overrides };
35
36
  }
36
37
 
38
+ /**
39
+ * Indexes a meeting's participants by UserID, AgentID and lower-cased ExternalEmail, and returns a
40
+ * lookup that finds the participant an update input refers to. When the input matches different
41
+ * participants on different keys, the one listed first in `existing` wins, as a single
42
+ * `existing.find` over all three keys would answer.
43
+ */
44
+ function indexParticipants(
45
+ existing: MeetingParticipantType[],
46
+ ): (input: MeetingParticipantInput) => MeetingParticipantType | undefined {
47
+ const byUserID = IndexByUUID(existing.filter((e) => e.UserID), (e) => e.UserID);
48
+ const byAgentID = IndexByUUID(existing.filter((e) => e.AgentID), (e) => e.AgentID);
49
+ const byEmail = new Map<string, MeetingParticipantType>();
50
+ const position = new Map<MeetingParticipantType, number>();
51
+ existing.forEach((e, i) => {
52
+ position.set(e, i);
53
+ const email = e.ExternalEmail?.toLowerCase();
54
+ if (email && !byEmail.has(email)) byEmail.set(email, e);
55
+ });
56
+
57
+ return (input) => {
58
+ const candidates = [
59
+ input.UserID ? byUserID.Get(input.UserID) : undefined,
60
+ input.AgentID ? byAgentID.Get(input.AgentID) : undefined,
61
+ input.ExternalEmail ? byEmail.get(input.ExternalEmail.toLowerCase()) : undefined,
62
+ ];
63
+ let first: MeetingParticipantType | undefined;
64
+ for (const candidate of candidates) {
65
+ if (candidate && (!first || (position.get(candidate) ?? 0) < (position.get(first) ?? 0))) first = candidate;
66
+ }
67
+ return first;
68
+ };
69
+ }
70
+
37
71
  export async function ExecuteCreateMeeting(
38
72
  provider: IMetadataProvider,
39
73
  user: UserInfo,
@@ -235,15 +269,11 @@ export async function ExecuteUpdateMeeting(
235
269
  // If participants specified in update, sync participants (upsert by UserID/AgentID/ExternalEmail)
236
270
  if (input.Participants !== undefined) {
237
271
  const existing = await h.LoadParticipants(meeting.ID, user, provider);
272
+ const findExisting = indexParticipants(existing);
238
273
 
239
274
  for (const pInput of input.Participants) {
240
275
  const isSelf = pInput.UserID && UUIDsEqual(pInput.UserID, user.ID);
241
- const match = existing.find(
242
- (e) =>
243
- (pInput.UserID && e.UserID && UUIDsEqual(e.UserID, pInput.UserID)) ||
244
- (pInput.AgentID && e.AgentID && UUIDsEqual(e.AgentID, pInput.AgentID)) ||
245
- (pInput.ExternalEmail && e.ExternalEmail && e.ExternalEmail.toLowerCase() === pInput.ExternalEmail.toLowerCase()),
246
- );
276
+ const match = findExisting(pInput);
247
277
 
248
278
  if (match) {
249
279
  if (!isSelf && pInput.Role) {
package/src/util.ts CHANGED
@@ -6,7 +6,8 @@ import { URL } from 'url';
6
6
  import { z } from 'zod';
7
7
  import { DataSourceInfo, ProviderInfo } from './types';
8
8
  import sql from 'mssql';
9
- import { DatabaseProviderBase } from '@memberjunction/core';
9
+ import { DatabaseProviderBase, GetDiagnosticContext, IsDiagnosticCaptureSuppressed } from '@memberjunction/core';
10
+ import { GetGlobalObjectStore } from '@memberjunction/global';
10
11
 
11
12
  const gzip = promisify(gzipCallback);
12
13
 
@@ -249,8 +250,59 @@ export async function sendPostRequest(url: string, payload: any, useCompression:
249
250
  sqlQuery = sqlQuery.replace(/\?/g, () => `@p${paramIndex++}`);
250
251
  }
251
252
  }
252
- const result = await request.query(sqlQuery);
253
- return result.recordset || [];
253
+ const startTime = performance.now();
254
+ const notifyPoolSqlObservers = (rowCount: number, errorMessage?: string): void => {
255
+ if (typeof IsDiagnosticCaptureSuppressed === 'function' && IsDiagnosticCaptureSuppressed()) {
256
+ return;
257
+ }
258
+ const observers = GetGlobalObjectStore()?.['___MJ_SQL_OBSERVERS___'] as
259
+ | Set<{ onSqlExecuted: (obs: Record<string, unknown>) => void }>
260
+ | undefined;
261
+ if (!observers || observers.size === 0) {
262
+ return;
263
+ }
264
+ const ctx = typeof GetDiagnosticContext === 'function' ? GetDiagnosticContext() : undefined;
265
+ const obs: Record<string, unknown> = {
266
+ ts: Date.now(),
267
+ sql: sqlQuery,
268
+ parameterTypes: Array.isArray(parameters)
269
+ ? parameters.map((v, idx) => ({
270
+ name: `p${idx}`,
271
+ sqlType: v === null ? 'null' : typeof v,
272
+ byteLength: typeof v === 'string' ? v.length : 0,
273
+ }))
274
+ : [],
275
+ durationMs: performance.now() - startTime,
276
+ rowCount,
277
+ isMutation: false,
278
+ description: 'ConnectionPool.query',
279
+ ...(errorMessage ? { errorMessage } : {}),
280
+ ...(ctx
281
+ ? {
282
+ diagnosticContext: ctx,
283
+ sessionId: ctx.sessionId ?? ctx.SessionID,
284
+ traceId: ctx.traceId ?? ctx.TraceID,
285
+ spanId: ctx.spanId ?? ctx.SpanID,
286
+ }
287
+ : {}),
288
+ };
289
+ for (const observer of observers) {
290
+ try {
291
+ observer.onSqlExecuted(obs);
292
+ } catch {
293
+ // Observer errors must never affect SQL execution
294
+ }
295
+ }
296
+ };
297
+ try {
298
+ const result = await request.query(sqlQuery);
299
+ const rows = result.recordset || [];
300
+ notifyPoolSqlObservers(rows.length);
301
+ return rows;
302
+ } catch (err) {
303
+ notifyPoolSqlObservers(0, err instanceof Error ? err.message : String(err));
304
+ throw err;
305
+ }
254
306
  };
255
307
  return extendedPool;
256
308
  }