@memberjunction/server 5.38.0 → 5.40.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 (180) hide show
  1. package/README.md +67 -0
  2. package/dist/apolloServer/index.d.ts +0 -8
  3. package/dist/apolloServer/index.d.ts.map +1 -1
  4. package/dist/apolloServer/index.js +61 -0
  5. package/dist/apolloServer/index.js.map +1 -1
  6. package/dist/auth/index.js +1 -1
  7. package/dist/auth/index.js.map +1 -1
  8. package/dist/auth/magicLink/MagicLinkKeys.d.ts +58 -0
  9. package/dist/auth/magicLink/MagicLinkKeys.d.ts.map +1 -0
  10. package/dist/auth/magicLink/MagicLinkKeys.js +99 -0
  11. package/dist/auth/magicLink/MagicLinkKeys.js.map +1 -0
  12. package/dist/auth/magicLink/MagicLinkRouter.d.ts +33 -0
  13. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -0
  14. package/dist/auth/magicLink/MagicLinkRouter.js +184 -0
  15. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -0
  16. package/dist/auth/magicLink/MagicLinkService.d.ts +123 -0
  17. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -0
  18. package/dist/auth/magicLink/MagicLinkService.js +605 -0
  19. package/dist/auth/magicLink/MagicLinkService.js.map +1 -0
  20. package/dist/auth/magicLink/index.d.ts +6 -0
  21. package/dist/auth/magicLink/index.d.ts.map +1 -0
  22. package/dist/auth/magicLink/index.js +6 -0
  23. package/dist/auth/magicLink/index.js.map +1 -0
  24. package/dist/auth/magicLink/magicLinkCore.d.ts +82 -0
  25. package/dist/auth/magicLink/magicLinkCore.d.ts.map +1 -0
  26. package/dist/auth/magicLink/magicLinkCore.js +164 -0
  27. package/dist/auth/magicLink/magicLinkCore.js.map +1 -0
  28. package/dist/auth/magicLink/redeemLanding.d.ts +22 -0
  29. package/dist/auth/magicLink/redeemLanding.d.ts.map +1 -0
  30. package/dist/auth/magicLink/redeemLanding.js +61 -0
  31. package/dist/auth/magicLink/redeemLanding.js.map +1 -0
  32. package/dist/auth/magicLink/types.d.ts +131 -0
  33. package/dist/auth/magicLink/types.d.ts.map +1 -0
  34. package/dist/auth/magicLink/types.js +6 -0
  35. package/dist/auth/magicLink/types.js.map +1 -0
  36. package/dist/config.d.ts +479 -0
  37. package/dist/config.d.ts.map +1 -1
  38. package/dist/config.js +84 -0
  39. package/dist/config.js.map +1 -1
  40. package/dist/context.d.ts.map +1 -1
  41. package/dist/context.js +223 -19
  42. package/dist/context.js.map +1 -1
  43. package/dist/generated/generated.d.ts +952 -4
  44. package/dist/generated/generated.d.ts.map +1 -1
  45. package/dist/generated/generated.js +25663 -20321
  46. package/dist/generated/generated.js.map +1 -1
  47. package/dist/generic/FireAndForgetHeartbeat.d.ts +51 -0
  48. package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -0
  49. package/dist/generic/FireAndForgetHeartbeat.js +44 -0
  50. package/dist/generic/FireAndForgetHeartbeat.js.map +1 -0
  51. package/dist/generic/ResolverBase.d.ts.map +1 -1
  52. package/dist/generic/ResolverBase.js +35 -7
  53. package/dist/generic/ResolverBase.js.map +1 -1
  54. package/dist/index.d.ts +5 -0
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +145 -2
  57. package/dist/index.js.map +1 -1
  58. package/dist/logging/NoLog.d.ts +50 -0
  59. package/dist/logging/NoLog.d.ts.map +1 -0
  60. package/dist/logging/NoLog.js +80 -0
  61. package/dist/logging/NoLog.js.map +1 -0
  62. package/dist/logging/bootAudit.d.ts +43 -0
  63. package/dist/logging/bootAudit.d.ts.map +1 -0
  64. package/dist/logging/bootAudit.js +83 -0
  65. package/dist/logging/bootAudit.js.map +1 -0
  66. package/dist/logging/boundaryLogPayload.d.ts +18 -0
  67. package/dist/logging/boundaryLogPayload.d.ts.map +1 -0
  68. package/dist/logging/boundaryLogPayload.js +18 -0
  69. package/dist/logging/boundaryLogPayload.js.map +1 -0
  70. package/dist/logging/secretRedactor.d.ts +23 -0
  71. package/dist/logging/secretRedactor.d.ts.map +1 -0
  72. package/dist/logging/secretRedactor.js +53 -0
  73. package/dist/logging/secretRedactor.js.map +1 -0
  74. package/dist/logging/shortenForLog.d.ts +8 -0
  75. package/dist/logging/shortenForLog.d.ts.map +1 -0
  76. package/dist/logging/shortenForLog.js +21 -0
  77. package/dist/logging/shortenForLog.js.map +1 -0
  78. package/dist/logging/variablesLoggingMiddleware.d.ts +22 -0
  79. package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -0
  80. package/dist/logging/variablesLoggingMiddleware.js +127 -0
  81. package/dist/logging/variablesLoggingMiddleware.js.map +1 -0
  82. package/dist/resolvers/CurrentUserContextResolver.d.ts +9 -3
  83. package/dist/resolvers/CurrentUserContextResolver.d.ts.map +1 -1
  84. package/dist/resolvers/CurrentUserContextResolver.js +19 -5
  85. package/dist/resolvers/CurrentUserContextResolver.js.map +1 -1
  86. package/dist/resolvers/EntityResolver.d.ts.map +1 -1
  87. package/dist/resolvers/EntityResolver.js +13 -2
  88. package/dist/resolvers/EntityResolver.js.map +1 -1
  89. package/dist/resolvers/GenerateSeedTaxonomyResolver.d.ts +28 -0
  90. package/dist/resolvers/GenerateSeedTaxonomyResolver.d.ts.map +1 -0
  91. package/dist/resolvers/GenerateSeedTaxonomyResolver.js +100 -0
  92. package/dist/resolvers/GenerateSeedTaxonomyResolver.js.map +1 -0
  93. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  94. package/dist/resolvers/GetDataResolver.js +8 -4
  95. package/dist/resolvers/GetDataResolver.js.map +1 -1
  96. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +259 -2
  97. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  98. package/dist/resolvers/IntegrationDiscoveryResolver.js +1276 -117
  99. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  100. package/dist/resolvers/IntegrationProgressResolver.d.ts +90 -0
  101. package/dist/resolvers/IntegrationProgressResolver.d.ts.map +1 -0
  102. package/dist/resolvers/IntegrationProgressResolver.js +196 -0
  103. package/dist/resolvers/IntegrationProgressResolver.js.map +1 -0
  104. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  105. package/dist/resolvers/MCPResolver.js +19 -14
  106. package/dist/resolvers/MCPResolver.js.map +1 -1
  107. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  108. package/dist/resolvers/RunAIAgentResolver.js +26 -5
  109. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  110. package/dist/resolvers/RunClusterAnalysisResolver.d.ts +74 -0
  111. package/dist/resolvers/RunClusterAnalysisResolver.d.ts.map +1 -0
  112. package/dist/resolvers/RunClusterAnalysisResolver.js +243 -0
  113. package/dist/resolvers/RunClusterAnalysisResolver.js.map +1 -0
  114. package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
  115. package/dist/resolvers/RunTestResolver.js +12 -2
  116. package/dist/resolvers/RunTestResolver.js.map +1 -1
  117. package/dist/resolvers/SearchEntitiesResolver.d.ts +46 -0
  118. package/dist/resolvers/SearchEntitiesResolver.d.ts.map +1 -0
  119. package/dist/resolvers/SearchEntitiesResolver.js +216 -0
  120. package/dist/resolvers/SearchEntitiesResolver.js.map +1 -0
  121. package/dist/resolvers/UserResolver.d.ts +16 -2
  122. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  123. package/dist/resolvers/UserResolver.js +45 -2
  124. package/dist/resolvers/UserResolver.js.map +1 -1
  125. package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
  126. package/dist/resolvers/VectorizeEntityResolver.js +14 -1
  127. package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
  128. package/dist/rest/SignatureWebhookHandler.d.ts +19 -0
  129. package/dist/rest/SignatureWebhookHandler.d.ts.map +1 -0
  130. package/dist/rest/SignatureWebhookHandler.js +86 -0
  131. package/dist/rest/SignatureWebhookHandler.js.map +1 -0
  132. package/dist/services/ScheduledJobsService.d.ts.map +1 -1
  133. package/dist/services/ScheduledJobsService.js +14 -2
  134. package/dist/services/ScheduledJobsService.js.map +1 -1
  135. package/package.json +79 -74
  136. package/src/__tests__/NoLog.test.ts +76 -0
  137. package/src/__tests__/bootAudit.test.ts +188 -0
  138. package/src/__tests__/boundaryLogPayload.test.ts +31 -0
  139. package/src/__tests__/getDataTokenRedaction.test.ts +84 -0
  140. package/src/__tests__/magicLink.test.ts +387 -0
  141. package/src/__tests__/secretRedactor.test.ts +163 -0
  142. package/src/__tests__/subscriptionRedaction.test.ts +217 -0
  143. package/src/apolloServer/index.ts +58 -0
  144. package/src/auth/index.ts +2 -2
  145. package/src/auth/magicLink/MagicLinkKeys.ts +122 -0
  146. package/src/auth/magicLink/MagicLinkRouter.ts +209 -0
  147. package/src/auth/magicLink/MagicLinkService.ts +724 -0
  148. package/src/auth/magicLink/index.ts +17 -0
  149. package/src/auth/magicLink/magicLinkCore.ts +216 -0
  150. package/src/auth/magicLink/redeemLanding.ts +62 -0
  151. package/src/auth/magicLink/types.ts +137 -0
  152. package/src/config.ts +89 -0
  153. package/src/context.ts +249 -17
  154. package/src/generated/generated.ts +12528 -8866
  155. package/src/generic/FireAndForgetHeartbeat.ts +85 -0
  156. package/src/generic/ResolverBase.ts +35 -7
  157. package/src/generic/__tests__/FireAndForgetHeartbeat.test.ts +99 -0
  158. package/src/index.ts +165 -2
  159. package/src/logging/NoLog.ts +88 -0
  160. package/src/logging/bootAudit.ts +117 -0
  161. package/src/logging/boundaryLogPayload.ts +19 -0
  162. package/src/logging/secretRedactor.ts +82 -0
  163. package/src/logging/shortenForLog.ts +17 -0
  164. package/src/logging/variablesLoggingMiddleware.ts +191 -0
  165. package/src/resolvers/CurrentUserContextResolver.ts +21 -5
  166. package/src/resolvers/EntityResolver.ts +17 -5
  167. package/src/resolvers/GenerateSeedTaxonomyResolver.ts +90 -0
  168. package/src/resolvers/GetDataResolver.ts +9 -5
  169. package/src/resolvers/IntegrationDiscoveryResolver.ts +1111 -120
  170. package/src/resolvers/IntegrationProgressResolver.ts +220 -0
  171. package/src/resolvers/MCPResolver.ts +18 -14
  172. package/src/resolvers/RunAIAgentResolver.ts +28 -5
  173. package/src/resolvers/RunClusterAnalysisResolver.ts +249 -0
  174. package/src/resolvers/RunTestResolver.ts +14 -2
  175. package/src/resolvers/SearchEntitiesResolver.ts +173 -0
  176. package/src/resolvers/UserResolver.ts +38 -2
  177. package/src/resolvers/VectorizeEntityResolver.ts +14 -1
  178. package/src/resolvers/__tests__/IntegrationProgressResolver.test.ts +170 -0
  179. package/src/rest/SignatureWebhookHandler.ts +103 -0
  180. package/src/services/ScheduledJobsService.ts +15 -2
@@ -0,0 +1,85 @@
1
+ import { PubSubEngine } from 'type-graphql';
2
+ import { LogError } from '@memberjunction/core';
3
+ import { PUSH_STATUS_UPDATES_TOPIC } from './PushStatusResolver.js';
4
+
5
+ /** Default cadence for fire-and-forget liveness pulses (5 minutes). */
6
+ export const DEFAULT_PULSE_INTERVAL_MS = 5 * 60 * 1000;
7
+
8
+ /** The `type` discriminator carried by liveness pulse messages. */
9
+ export const HEARTBEAT_MESSAGE_TYPE = 'Heartbeat';
10
+
11
+ /**
12
+ * Lightweight status snapshot included in an enriched pulse so the client can
13
+ * correlate the pulse with a run record and surface a coarse "still running"
14
+ * signal. Read from the in-memory run entity — never a DB query.
15
+ */
16
+ export interface PulseStatus {
17
+ /** Primary key of the persisted run record (AIAgentRun, TestRun, TestSuiteRun). */
18
+ runId?: string;
19
+ /** Current run status, e.g. 'Running'. */
20
+ status?: string;
21
+ /** Optional human-readable current step for UI display. */
22
+ currentStep?: string;
23
+ }
24
+
25
+ /** Handle returned by {@link startLivenessPulse}; call `stop()` when the work settles. */
26
+ export interface LivenessPulseHandle {
27
+ stop(): void;
28
+ }
29
+
30
+ export interface LivenessPulseOptions {
31
+ /** PubSub engine used to publish on the shared push-status topic. */
32
+ pubSub: PubSubEngine;
33
+ /** Session the client is subscribed on (used by the subscription filter). */
34
+ sessionId: string;
35
+ /** Resolver label echoed in the message envelope (e.g. 'RunAIAgentResolver'). */
36
+ resolver: string;
37
+ /** Pulse cadence in ms. Defaults to {@link DEFAULT_PULSE_INTERVAL_MS}. */
38
+ intervalMs?: number;
39
+ /**
40
+ * Optional cheap status reader invoked on each tick. Should read from an
41
+ * in-memory ref, not the database. Errors are swallowed so a transient read
42
+ * never kills the pulse loop.
43
+ */
44
+ readStatus?: () => PulseStatus | undefined;
45
+ }
46
+
47
+ /**
48
+ * Publishes a periodic liveness pulse on {@link PUSH_STATUS_UPDATES_TOPIC} while a
49
+ * fire-and-forget background operation runs. Each pulse resets the client's idle
50
+ * timer, so a long-but-active operation never spuriously times out. The pulse
51
+ * stops when the caller invokes `stop()` (typically in the background promise's
52
+ * `finally` block).
53
+ *
54
+ * The envelope matches the resolvers' existing push-status messages
55
+ * (`{ message: JSON.stringify({ resolver, type, status, data }), sessionId }`),
56
+ * so the client receives it through the same subscription with no special parsing.
57
+ */
58
+ export function startLivenessPulse(options: LivenessPulseOptions): LivenessPulseHandle {
59
+ const { pubSub, sessionId, resolver, readStatus } = options;
60
+ const intervalMs = options.intervalMs ?? DEFAULT_PULSE_INTERVAL_MS;
61
+
62
+ const timer = setInterval(() => {
63
+ let data: PulseStatus | undefined;
64
+ try {
65
+ data = readStatus?.();
66
+ } catch (e) {
67
+ // A status read failure must not break the liveness loop.
68
+ LogError(`[LivenessPulse:${resolver}] readStatus failed: ${(e as Error).message}`);
69
+ }
70
+
71
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
72
+ message: JSON.stringify({
73
+ resolver,
74
+ type: HEARTBEAT_MESSAGE_TYPE,
75
+ status: 'ok',
76
+ data: data ?? {},
77
+ }),
78
+ sessionId,
79
+ });
80
+ }, intervalMs);
81
+
82
+ return {
83
+ stop: () => clearInterval(timer),
84
+ };
85
+ }
@@ -587,7 +587,16 @@ export class ResolverBase {
587
587
 
588
588
  // first check permissions, the logged in user must have read permissions on the entity to run the view
589
589
  if (entityInfo) {
590
- const userInfo = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload.email.toLowerCase().trim()); // get the user record from MD so we have ROLES attached, don't use the one from payload directly
590
+ // Prefer the authenticated session's payload user WHEN it carries roles that is the
591
+ // authoritative per-session identity (and, for magic-link sessions, carries claims-based
592
+ // synthesized roles that are deliberately NOT persisted to the shared UserCache). For
593
+ // normal users the payload user IS the UserCache user, so this is a no-op; we only diverge
594
+ // for per-session synthesized identities. Fall back to the cache lookup when the payload
595
+ // user has no roles attached (older/edge auth paths).
596
+ const payloadUser = this.GetUserFromPayload(userPayload);
597
+ const userInfo = (payloadUser && payloadUser.UserRoles && payloadUser.UserRoles.length > 0)
598
+ ? payloadUser
599
+ : UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload.email.toLowerCase().trim());
591
600
  if (!userInfo) {
592
601
  throw new Error(`User ${userPayload.email} not found in metadata`);
593
602
  }
@@ -709,7 +718,12 @@ export class ResolverBase {
709
718
  await this.CheckAPIKeyScopeAuthorization('view:run', viewInfo.Entity, userPayload);
710
719
 
711
720
  const md = provider
712
- const user = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
721
+ // Prefer the authenticated session's payload user it is the authoritative per-request
722
+ // identity and (for magic-link sessions) carries the per-session resource scope / synthesized
723
+ // roles that drive RLS. The cached lookup is a fallback for paths where the payload user
724
+ // isn't populated. For normal users the two are the same instance, so this is a no-op.
725
+ const user = this.GetUserFromPayload(userPayload)
726
+ ?? UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
713
727
  if (!user) throw new Error(`User ${userPayload?.email} not found in metadata`);
714
728
 
715
729
  const entityInfo = md.Entities.find((e) => e.Name === viewInfo.Entity);
@@ -823,8 +837,14 @@ export class ResolverBase {
823
837
  // Fix #1: Get user info only once for all queries
824
838
  let contextUser: UserInfo | null = null;
825
839
  if (params[0]?.userPayload?.email) {
826
- const userEmail = params[0].userPayload.email.toLowerCase().trim();
827
- const user = UserCache.Users.find(u => u.Email.toLowerCase().trim() === userEmail);
840
+ const userPayload = params[0].userPayload;
841
+ const userEmail = userPayload.email.toLowerCase().trim();
842
+ // Prefer the authenticated session's payload user — it is the authoritative per-request
843
+ // identity and (for magic-link sessions) carries the per-session resource scope / synthesized
844
+ // roles that drive RLS. The cached lookup is a fallback for paths where the payload user
845
+ // isn't populated. For normal users the two are the same instance, so this is a no-op.
846
+ const user = this.GetUserFromPayload(userPayload)
847
+ ?? UserCache.Users.find(u => u.Email.toLowerCase().trim() === userEmail);
828
848
  if (!user) {
829
849
  throw new Error(`User ${userEmail} not found in metadata`);
830
850
  }
@@ -921,7 +941,9 @@ export class ResolverBase {
921
941
  if (!entityInfo) throw new Error(`Entity ${entityName} not found in metadata`);
922
942
 
923
943
  if (entityInfo.AuditRecordAccess) {
924
- const userInfo = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
944
+ // Prefer the per-session payload user (the actual actor for this request) over the cache lookup.
945
+ const userInfo = this.GetUserFromPayload(userPayload)
946
+ ?? UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
925
947
  const auditLogTypeName = 'Record Accessed';
926
948
  const auditLogType = md.AuditLogTypes.find((a) => a.Name.trim().toLowerCase() === auditLogTypeName.trim().toLowerCase());
927
949
 
@@ -939,7 +961,11 @@ export class ResolverBase {
939
961
  const md = provider;
940
962
  const entityInfo = md.Entities.find((e) => e.Name.trim().toLowerCase() === entityName.trim().toLowerCase());
941
963
  if (!entityInfo) throw new Error(`Entity ${entityName} not found in metadata`);
942
- const user = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
964
+ // Prefer the authenticated session's payload user it is the authoritative per-request identity
965
+ // and (for magic-link sessions) carries the per-session resource scope / synthesized roles that
966
+ // the RLS WHERE clause (e.g. {{ScopeResourceID}}) depends on. Cache lookup is the fallback.
967
+ const user = this.GetUserFromPayload(userPayload)
968
+ ?? UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
943
969
  if (!user) throw new Error(`User ${userPayload?.email} not found in metadata`);
944
970
 
945
971
  return entityInfo.GetUserRowLevelSecurityWhereClause(user, type, returnPrefix);
@@ -957,7 +983,9 @@ export class ResolverBase {
957
983
  ): Promise<any> {
958
984
  try {
959
985
  const md = provider;
960
- const userInfo = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
986
+ // Prefer the per-session payload user (the actual actor for this request) over the cache lookup.
987
+ const userInfo = this.GetUserFromPayload(userPayload)
988
+ ?? UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
961
989
  const authorization = authorizationName
962
990
  ? md.Authorizations.find((a) => a.Name.trim().toLowerCase() === authorizationName.trim().toLowerCase())
963
991
  : null;
@@ -0,0 +1,99 @@
1
+ import 'reflect-metadata';
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
+ import { PubSubEngine } from 'type-graphql';
4
+ import {
5
+ startLivenessPulse,
6
+ DEFAULT_PULSE_INTERVAL_MS,
7
+ HEARTBEAT_MESSAGE_TYPE,
8
+ } from '../FireAndForgetHeartbeat';
9
+ import { PUSH_STATUS_UPDATES_TOPIC } from '../PushStatusResolver';
10
+
11
+ /** Minimal PubSubEngine stub capturing publish calls. */
12
+ function makePubSub() {
13
+ const publish = vi.fn().mockResolvedValue(undefined);
14
+ return { publish } as unknown as PubSubEngine & { publish: ReturnType<typeof vi.fn> };
15
+ }
16
+
17
+ function parseLastMessage(pubSub: { publish: ReturnType<typeof vi.fn> }) {
18
+ const [topic, payload] = pubSub.publish.mock.calls[pubSub.publish.mock.calls.length - 1];
19
+ return { topic, payload, parsed: JSON.parse(payload.message) };
20
+ }
21
+
22
+ describe('startLivenessPulse', () => {
23
+ beforeEach(() => {
24
+ vi.useFakeTimers();
25
+ });
26
+
27
+ afterEach(() => {
28
+ vi.useRealTimers();
29
+ vi.restoreAllMocks();
30
+ });
31
+
32
+ it('does not publish before the first interval elapses', () => {
33
+ const pubSub = makePubSub();
34
+ startLivenessPulse({ pubSub, sessionId: 's1', resolver: 'TestResolver' });
35
+ vi.advanceTimersByTime(DEFAULT_PULSE_INTERVAL_MS - 1);
36
+ expect(pubSub.publish).not.toHaveBeenCalled();
37
+ });
38
+
39
+ it('publishes a heartbeat on the push-status topic each interval', () => {
40
+ const pubSub = makePubSub();
41
+ startLivenessPulse({ pubSub, sessionId: 's1', resolver: 'TestResolver', intervalMs: 1000 });
42
+
43
+ vi.advanceTimersByTime(3000);
44
+
45
+ expect(pubSub.publish).toHaveBeenCalledTimes(3);
46
+ const { topic, payload, parsed } = parseLastMessage(pubSub);
47
+ expect(topic).toBe(PUSH_STATUS_UPDATES_TOPIC);
48
+ expect(payload.sessionId).toBe('s1');
49
+ expect(parsed.resolver).toBe('TestResolver');
50
+ expect(parsed.type).toBe(HEARTBEAT_MESSAGE_TYPE);
51
+ });
52
+
53
+ it('includes the enriched status snapshot from readStatus', () => {
54
+ const pubSub = makePubSub();
55
+ startLivenessPulse({
56
+ pubSub,
57
+ sessionId: 's1',
58
+ resolver: 'RunAIAgentResolver',
59
+ intervalMs: 1000,
60
+ readStatus: () => ({ runId: 'run-123', status: 'Running', currentStep: 'Prompt' }),
61
+ });
62
+
63
+ vi.advanceTimersByTime(1000);
64
+
65
+ const { parsed } = parseLastMessage(pubSub);
66
+ expect(parsed.data).toEqual({ runId: 'run-123', status: 'Running', currentStep: 'Prompt' });
67
+ });
68
+
69
+ it('publishes an empty data object when readStatus throws (loop survives)', () => {
70
+ const pubSub = makePubSub();
71
+ startLivenessPulse({
72
+ pubSub,
73
+ sessionId: 's1',
74
+ resolver: 'RunAIAgentResolver',
75
+ intervalMs: 1000,
76
+ readStatus: () => {
77
+ throw new Error('transient read failure');
78
+ },
79
+ });
80
+
81
+ vi.advanceTimersByTime(2000);
82
+
83
+ expect(pubSub.publish).toHaveBeenCalledTimes(2);
84
+ const { parsed } = parseLastMessage(pubSub);
85
+ expect(parsed.data).toEqual({});
86
+ });
87
+
88
+ it('stops publishing after stop() is called', () => {
89
+ const pubSub = makePubSub();
90
+ const handle = startLivenessPulse({ pubSub, sessionId: 's1', resolver: 'TestResolver', intervalMs: 1000 });
91
+
92
+ vi.advanceTimersByTime(2000);
93
+ expect(pubSub.publish).toHaveBeenCalledTimes(2);
94
+
95
+ handle.stop();
96
+ vi.advanceTimersByTime(5000);
97
+ expect(pubSub.publish).toHaveBeenCalledTimes(2);
98
+ });
99
+ });
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ dotenv.config({ quiet: true });
4
4
 
5
5
  import { expressMiddleware } from '@as-integrations/express5';
6
6
  import { mergeSchemas } from '@graphql-tools/schema';
7
- import { Metadata, DatabasePlatform, SetProvider, StartupManager as StartupManagerImport, BaseEntity, BaseEntityEvent, RunView } from '@memberjunction/core';
7
+ import { Metadata, DatabasePlatform, SetProvider, StartupManager as StartupManagerImport, BaseEntity, BaseEntityEvent, RunView, DatabaseProviderBase } from '@memberjunction/core';
8
8
  import { resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provider';
9
9
  import { MJGlobal, MJEventType, UUIDsEqual, ShutdownRegistry } from '@memberjunction/global';
10
10
  import { setupSQLServerClient, SQLServerDataProvider, SQLServerProviderConfigData, UserCache } from '@memberjunction/sqlserver-dataprovider';
@@ -30,9 +30,13 @@ import { default as jwt } from 'jsonwebtoken';
30
30
  import { contextFunction, createUnifiedAuthMiddleware, getUserPayload } from './context.js';
31
31
  import { UserPayload } from './types.js';
32
32
  import { requireSystemUserDirective, publicDirective } from './directives/index.js';
33
+ import { variablesLoggingMiddleware } from './logging/variablesLoggingMiddleware.js';
34
+ import { auditResolversForUndecoratedArgs } from './logging/bootAudit.js';
33
35
  import createMSSQLConfig from './orm.js';
34
36
  import { setupRESTEndpoints } from './rest/setupRESTEndpoints.js';
35
37
  import { createOAuthCallbackHandler } from './rest/OAuthCallbackHandler.js';
38
+ import { createSignatureWebhookHandler } from './rest/SignatureWebhookHandler.js';
39
+ import { createMagicLinkHandler, registerMagicLinkAuthProvider, MAGIC_LINK_MOUNT_PATH } from './auth/magicLink/index.js';
36
40
 
37
41
  import { resolve } from 'node:path';
38
42
  import { DataSourceInfo, raiseEvent } from './types.js';
@@ -45,7 +49,9 @@ import { GetAPIKeyEngine } from '@memberjunction/api-keys';
45
49
  import { RedisLocalStorageProvider } from '@memberjunction/redis-provider';
46
50
  import { GenericDatabaseProvider } from '@memberjunction/generic-database-provider';
47
51
  import { PubSubManager } from './generic/PubSubManager.js';
48
- import { ClientToolRequestManager } from '@memberjunction/ai-agents';
52
+ import { IntegrationProgressEmitter } from '@memberjunction/integration-progress-artifacts';
53
+ import { PublishIntegrationProgress } from './resolvers/IntegrationProgressResolver.js';
54
+ import { ClientToolRequestManager, AgentRunWatchdog } from '@memberjunction/ai-agents';
49
55
  import { CACHE_INVALIDATION_TOPIC } from './generic/CacheInvalidationResolver.js';
50
56
  import { ConnectorFactory, IntegrationEngine, IntegrationSyncOptions } from '@memberjunction/integration-engine';
51
57
  import { CronExpressionHelper } from '@memberjunction/scheduling-engine';
@@ -83,6 +89,7 @@ export { configInfo, DEFAULT_SERVER_CONFIG } from './config.js';
83
89
  export { ServerExtensionLoader, BaseServerExtension } from '@memberjunction/server-extensions-core';
84
90
  export type { ServerExtensionConfig, ExtensionInitResult, ExtensionHealthResult } from '@memberjunction/server-extensions-core';
85
91
  export * from './directives/index.js';
92
+ export { NoLog, hasNoLogParameter, getNoLogFields } from './logging/NoLog.js';
86
93
  export * from './entitySubclasses/MJEntityPermissionEntityServer.server.js';
87
94
  export * from './types.js';
88
95
  export {
@@ -106,7 +113,10 @@ export * from './resolvers/SearchKnowledgeResolver.js';
106
113
  export * from './resolvers/SearchKnowledgeStreamResolver.js';
107
114
  export * from './resolvers/AvailableSearchProvidersResolver.js';
108
115
  export * from './resolvers/FetchEntityVectorsResolver.js';
116
+ export * from './resolvers/RunClusterAnalysisResolver.js';
117
+ export * from './resolvers/GenerateSeedTaxonomyResolver.js';
109
118
  export * from './resolvers/PipelineProgressResolver.js';
119
+ export * from './resolvers/IntegrationProgressResolver.js';
110
120
  export * from './resolvers/ClientToolRequestResolver.js';
111
121
  export * from './resolvers/AutotagPipelineResolver.js';
112
122
  export * from './resolvers/TagGovernanceResolver.js';
@@ -150,6 +160,7 @@ export * from './resolvers/FileResolver.js';
150
160
  export * from './resolvers/InfoResolver.js';
151
161
  export * from './resolvers/PotentialDuplicateRecordResolver.js';
152
162
  export * from './resolvers/RunTestResolver.js';
163
+ export * from './resolvers/SearchEntitiesResolver.js';
153
164
  export * from './resolvers/UserFavoriteResolver.js';
154
165
  export * from './resolvers/UserResolver.js';
155
166
  export * from './resolvers/UserViewResolver.js';
@@ -294,6 +305,86 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
294
305
  return origExecuteSQLWithPool.call(this, pool, query, parameters, contextUser);
295
306
  };
296
307
 
308
+ // Set up CodeGen-credentialed provider + in-process CodeGen runner for RSU (PostgreSQL).
309
+ // Without this, RuntimeSchemaManager has no injected runner and falls back to spawning a
310
+ // child-process CodeGen. On Postgres that child path is slow, its output is buffered (so
311
+ // progress is unobservable), and a non-responding advancedGen AI pass hangs it indefinitely
312
+ // — which leaves association/junction CRUD functions un-regenerated and blocks the sync.
313
+ // Running in-process keeps CodeGen inside the MJAPI event loop where it is observable and
314
+ // bounded by advanced_generation's per-call timeout + circuit breaker. Mirrors the SQL
315
+ // Server path below (the runner is required for BOTH platforms).
316
+ const pgCodegenUser = process.env.CODEGEN_DB_USERNAME;
317
+ const pgCodegenPass = process.env.CODEGEN_DB_PASSWORD;
318
+ if (pgCodegenUser && pgCodegenPass) {
319
+ try {
320
+ const codegenPgPool = new pg.default.Pool({
321
+ host: pgHost,
322
+ port: pgPort,
323
+ user: pgCodegenUser,
324
+ password: pgCodegenPass,
325
+ database: pgDatabase,
326
+ max: 10,
327
+ });
328
+ const codegenTestClient = await codegenPgPool.connect();
329
+ await codegenTestClient.query('SELECT 1');
330
+ codegenTestClient.release();
331
+
332
+ const { RuntimeSchemaManager } = await import('@memberjunction/schema-engine');
333
+ const codegenPgConfigData = new PostgreSQLProviderConfigData(
334
+ { Host: pgHost, Port: pgPort, Database: pgDatabase, User: pgCodegenUser, Password: pgCodegenPass },
335
+ mj_core_schema,
336
+ cacheRefreshInterval / 1000, // ms → seconds
337
+ );
338
+ const codegenPgProvider = new PostgreSQLDataProvider();
339
+ await codegenPgProvider.Config(codegenPgConfigData); // separate pool (per-instance manager); does NOT touch the global API provider
340
+ RuntimeSchemaManager.Instance.SetDDLProvider(codegenPgProvider);
341
+ console.log('RSU DDL provider initialized with CodeGen credentials (PostgreSQL).');
342
+
343
+ // Set up in-process CodeGen runner for RSU
344
+ try {
345
+ const { RunCodeGenBase } = await import('@memberjunction/codegen-lib');
346
+ const { PostgreSQLCodeGenConnection } = await import('@memberjunction/codegen-lib/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.js');
347
+
348
+ const codegenConnection = new PostgreSQLCodeGenConnection(codegenPgPool);
349
+ const codegenCurrentUser = UserCache.Instance.Users.find(u => u.Type?.trim().toLowerCase() === 'owner') ?? UserCache.Instance.Users[0];
350
+
351
+ const codegenDataSource = {
352
+ provider: codegenPgProvider,
353
+ connection: codegenConnection,
354
+ currentUser: codegenCurrentUser,
355
+ connectionInfo: `${pgHost}:${pgPort}/${pgDatabase} (CodeGen)`,
356
+ };
357
+
358
+ const runObject = MJGlobal.Instance.ClassFactory.CreateInstance(RunCodeGenBase) as InstanceType<typeof RunCodeGenBase>;
359
+
360
+ const rsuWorkDir = process.env.RSU_WORK_DIR || process.cwd();
361
+ RuntimeSchemaManager.Instance.SetCodeGenRunner({
362
+ RunInProcess: (skipDB) => runObject.RunInProcess(codegenDataSource, skipDB, rsuWorkDir),
363
+ });
364
+ console.log('RSU in-process CodeGen runner initialized (PostgreSQL).');
365
+
366
+ // Inject CodeGen output paths for targeted git staging
367
+ const { initializeConfig } = await import('@memberjunction/codegen-lib');
368
+ const cgConfig = initializeConfig(rsuWorkDir);
369
+ const outputPaths = (cgConfig.output ?? []).map((o: { directory: string }) => o.directory);
370
+ RuntimeSchemaManager.Instance.SetCodeGenOutputPaths(outputPaths);
371
+ console.log(`RSU CodeGen output paths: ${outputPaths.length} directories configured.`);
372
+
373
+ // Point RSU's soft PK/FK writer at the SAME file CodeGen reads (mj.config.cjs
374
+ // `additionalSchemaInfo`), or RSU writes soft PKs to its own default path while
375
+ // CodeGen reads a different one and skips integration tables with "No primary key found".
376
+ if (cgConfig.additionalSchemaInfo) {
377
+ RuntimeSchemaManager.Instance.SetAdditionalSchemaInfoPath(cgConfig.additionalSchemaInfo);
378
+ console.log(`RSU additionalSchemaInfo path: ${cgConfig.additionalSchemaInfo}`);
379
+ }
380
+ } catch (codegenErr) {
381
+ console.warn(`RSU in-process CodeGen runner setup failed (will fall back to child process): ${(codegenErr as Error).message}`);
382
+ }
383
+ } catch (err) {
384
+ console.warn(`RSU DDL provider setup failed (PostgreSQL; RSU will fall back to default provider): ${(err as Error).message}`);
385
+ }
386
+ }
387
+
297
388
  const md = new Metadata(); // global-provider-ok: bootstrap
298
389
  console.log(`Data Source has been initialized. ${md?.Entities ? md.Entities.length : 0} entities loaded.`);
299
390
  } else {
@@ -348,6 +439,13 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
348
439
  ...createMSSQLConfig(),
349
440
  user: codegenUser,
350
441
  password: codegenPass,
442
+ // CodeGen's metadata management ("manage entity fields", schema refresh) runs
443
+ // long-running queries across ALL entities. The default API requestTimeout (30s)
444
+ // is far too short at scale — e.g. a large integration Create-Tables pushing the
445
+ // schema to 400+ entities times out mid-refresh and leaves entity-field metadata
446
+ // only partially applied. This pool is used ONLY for CodeGen/DDL (never to serve
447
+ // API requests), so a generous timeout is safe. Mirrors MJCLI's baseline connection.
448
+ requestTimeout: 600000,
351
449
  });
352
450
  codegenPool.on('error', (err) => {
353
451
  console.error('[ConnectionPool] CodeGen pool connection error:', err.message);
@@ -390,6 +488,15 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
390
488
  const outputPaths = (codegenConfig.output ?? []).map((o: { directory: string }) => o.directory);
391
489
  RuntimeSchemaManager.Instance.SetCodeGenOutputPaths(outputPaths);
392
490
  console.log(`RSU CodeGen output paths: ${outputPaths.length} directories configured.`);
491
+
492
+ // Point RSU's soft PK/FK writer at the SAME file CodeGen reads (mj.config.cjs
493
+ // `additionalSchemaInfo`). Without this, RSU writes soft PKs to its own default
494
+ // path while CodeGen reads a different one and skips every integration table
495
+ // with "No primary key found".
496
+ if (codegenConfig.additionalSchemaInfo) {
497
+ RuntimeSchemaManager.Instance.SetAdditionalSchemaInfoPath(codegenConfig.additionalSchemaInfo);
498
+ console.log(`RSU additionalSchemaInfo path: ${codegenConfig.additionalSchemaInfo}`);
499
+ }
393
500
  } catch (codegenErr) {
394
501
  console.warn(`RSU in-process CodeGen runner setup failed (will fall back to child process): ${(codegenErr as Error).message}`);
395
502
  }
@@ -620,6 +727,24 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
620
727
  (topic: string, payload: Record<string, unknown>) => PubSubManager.Instance.Publish(topic, payload)
621
728
  );
622
729
 
730
+ // §11: fan every integration progress event onto the live GraphQL subscription topic. The emitter
731
+ // (in @memberjunction/integration-progress-artifacts) stays server-agnostic; we inject the publish
732
+ // here so integration runs/refreshes/syncs are subscribable in addition to the durable JSONL
733
+ // artifact + the pollable IntegrationTailRunEvents query.
734
+ IntegrationProgressEmitter.SetPublishHook((manifest, event) =>
735
+ PublishIntegrationProgress({
736
+ RunID: manifest.runID,
737
+ Kind: manifest.runKind,
738
+ CompanyIntegrationID: manifest.companyIntegrationID,
739
+ EventType: event.eventType,
740
+ Seq: event.seq,
741
+ Message: event.message,
742
+ Stage: event.stage,
743
+ Level: event.level,
744
+ Data: event.data,
745
+ })
746
+ );
747
+
623
748
  // Global listener: broadcast CACHE_INVALIDATION to all browser clients whenever
624
749
  // ANY BaseEntity save/delete occurs on this server — regardless of whether it
625
750
  // originated from a GraphQL mutation or internal server-side code (agents, actions,
@@ -652,10 +777,15 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
652
777
  scalarsMap: [{ type: Date, scalar: GraphQLTimestamp }],
653
778
  emitSchemaFile: websiteRunFromPackage !== 1,
654
779
  pubSub,
780
+ globalMiddlewares: [variablesLoggingMiddleware],
655
781
  }),
656
782
  ],
657
783
  typeDefs: [requireSystemUserDirective.typeDefs, publicDirective.typeDefs],
658
784
  });
785
+
786
+ // Verbose-mode-only diagnostic: name custom-resolver args that aren't metadata-bound
787
+ // and aren't @NoLog-marked. No-op in default config (logVariables=false).
788
+ auditResolversForUndecoratedArgs();
659
789
  schema = requireSystemUserDirective.transformer(schema);
660
790
  schema = publicDirective.transformer(schema);
661
791
 
@@ -799,6 +929,24 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
799
929
  console.log('[OAuth] Callback route registered at /oauth/callback');
800
930
  }
801
931
 
932
+ // ─── eSignature webhook (unauthenticated, registered BEFORE auth) ─────
933
+ // Called by external signature providers (DocuSign Connect, etc.) without an MJ bearer token.
934
+ // The provider DRIVER verifies the payload signature/HMAC; MJ auth does not apply here.
935
+ app.use('/esignature', cors<cors.CorsRequest>(), createSignatureWebhookHandler());
936
+ console.log('[eSignature] Webhook route registered at /esignature/webhook/:driverKey');
937
+
938
+ // ─── Magic-link routes (MJ-issued, app-scoped external access) ───────────
939
+ // Public router (JWKS + redeem) mounts BEFORE the auth middleware; the
940
+ // authenticated invite-creation router mounts AFTER it (see below).
941
+ let magicLinkAuthenticatedRouter: ReturnType<typeof createMagicLinkHandler>['authenticatedRouter'] | undefined;
942
+ if (configInfo.magicLink?.enabled) {
943
+ const { publicRouter, authenticatedRouter } = createMagicLinkHandler(oauthPublicUrl, configInfo.magicLink);
944
+ magicLinkAuthenticatedRouter = authenticatedRouter;
945
+ registerMagicLinkAuthProvider(oauthPublicUrl, configInfo.magicLink);
946
+ app.use(MAGIC_LINK_MOUNT_PATH, cors<cors.CorsRequest>(), publicRouter);
947
+ console.log(`[MagicLink] Public routes registered at ${MAGIC_LINK_MOUNT_PATH}/redeem and ${MAGIC_LINK_MOUNT_PATH}/jwks.json`);
948
+ }
949
+
802
950
  // ─── Global CORS (before auth so 401 responses include CORS headers) ─────
803
951
  // Without this, the browser blocks 401 responses from the auth middleware
804
952
  // because they lack Access-Control-Allow-Origin headers, preventing the
@@ -840,6 +988,12 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
840
988
  console.log('[OAuth] Authenticated routes registered at /oauth/status, /oauth/initiate, and /oauth/exchange');
841
989
  }
842
990
 
991
+ // ─── Magic-link authenticated route (invite creation) ─────────────────────
992
+ if (magicLinkAuthenticatedRouter) {
993
+ app.use(MAGIC_LINK_MOUNT_PATH, cors<cors.CorsRequest>(), magicLinkAuthenticatedRouter);
994
+ console.log(`[MagicLink] Authenticated route registered at ${MAGIC_LINK_MOUNT_PATH}/create`);
995
+ }
996
+
843
997
  // ─── REST API endpoints (auth already handled by unified middleware) ─────
844
998
  const restApiConfig = {
845
999
  enabled: configInfo.restApiOptions?.enabled ?? false,
@@ -929,6 +1083,15 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
929
1083
  .catch(err => console.warn(`[IntegrationEngine] Orphaned sync resume failed: ${err}`));
930
1084
  }
931
1085
 
1086
+ // Force-fail any agent runs left 'Running' by a process that died (restart/crash/OOM) or whose
1087
+ // terminal-state write never landed. Staleness-based, so it never touches runs another healthy
1088
+ // instance is still heart-beating. The watchdog also self-registers for graceful-shutdown
1089
+ // cancellation (via ShutdownRegistry) once it begins tracking this process's first live run.
1090
+ if (resumeUser && Metadata.Provider instanceof DatabaseProviderBase) { // global-provider-ok: server startup recovery — one-shot orphaned-run sweep at boot
1091
+ AgentRunWatchdog.SweepOrphanedRuns(Metadata.Provider, resumeUser) // global-provider-ok: server startup recovery — one-shot orphaned-run sweep at boot
1092
+ .catch(err => console.warn(`[AgentRunWatchdog] Startup sweep failed: ${err}`));
1093
+ }
1094
+
932
1095
  // Set up graceful shutdown handlers
933
1096
  const gracefulShutdown = async (signal: string) => {
934
1097
  console.log(`\n${signal} received, shutting down gracefully...`);
@@ -0,0 +1,88 @@
1
+ import 'reflect-metadata';
2
+
3
+ /**
4
+ * Reflect-metadata key for `@NoLog` marks. A single symbol covers both parameter-level
5
+ * marks (stored against `(target, propertyKey)` pairs) and field-level marks (stored
6
+ * against the input class prototype).
7
+ */
8
+ const NO_LOG_PARAM_KEY = Symbol('mj:NoLog:param');
9
+ const NO_LOG_FIELD_KEY = Symbol('mj:NoLog:field');
10
+
11
+ /**
12
+ * `@NoLog` — marks a resolver argument or input-type field as never-loggable.
13
+ *
14
+ * Two modes, distinguished by decorator arity at runtime:
15
+ *
16
+ * **Parameter** — applied to an `@Arg(...)` parameter on a resolver method:
17
+ * ```ts
18
+ * @Mutation(() => Boolean)
19
+ * async VoiceTestHubSpotCredential(
20
+ * @Arg('accessToken') @NoLog accessToken: string,
21
+ * @Ctx() ctx: AppContext,
22
+ * ): Promise<boolean> { ... }
23
+ * ```
24
+ *
25
+ * **Property** — applied to a `@Field()` on an `@InputType` class:
26
+ * ```ts
27
+ * @InputType()
28
+ * export class GetDataInputType {
29
+ * @Field(() => String) @NoLog Token: string;
30
+ * @Field(() => [String]) Queries: string[];
31
+ * }
32
+ * ```
33
+ *
34
+ * The variables-logging middleware reads these marks at runtime via
35
+ * `hasNoLogParameter` / `getNoLogFields` and replaces the marked value (or whole arg)
36
+ * with `"<redacted>"` before emitting the variables block. Metadata-covered fields
37
+ * (entity columns with `EntityFieldInfo.Encrypt=true`) do not need `@NoLog` — applying
38
+ * it is harmless but redundant. Use `@NoLog` for arguments that the redactor cannot
39
+ * identify via metadata: custom-resolver parameters, MCP tool args, fields on input
40
+ * types that don't map to a known entity.
41
+ */
42
+ export function NoLog(target: object, propertyKey?: string | symbol, parameterIndex?: number): void {
43
+ // Parameter-decorator path: (target, propertyKey, parameterIndex)
44
+ if (typeof parameterIndex === 'number' && propertyKey !== undefined) {
45
+ const existing = (Reflect.getOwnMetadata(NO_LOG_PARAM_KEY, target, propertyKey) as Set<number> | undefined) ?? new Set<number>();
46
+ existing.add(parameterIndex);
47
+ Reflect.defineMetadata(NO_LOG_PARAM_KEY, existing, target, propertyKey);
48
+ return;
49
+ }
50
+
51
+ // Property-decorator path: (target, propertyKey)
52
+ if (propertyKey !== undefined) {
53
+ const existing = (Reflect.getOwnMetadata(NO_LOG_FIELD_KEY, target) as Set<string> | undefined) ?? new Set<string>();
54
+ existing.add(String(propertyKey));
55
+ Reflect.defineMetadata(NO_LOG_FIELD_KEY, existing, target);
56
+ return;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Returns true if `@NoLog` was applied to `methodName`'s argument at `parameterIndex`
62
+ * on the given resolver class prototype. False otherwise.
63
+ *
64
+ * Pass the resolver class itself (e.g. `MyResolver`) — the function reads from the
65
+ * prototype internally to match how type-graphql stores resolver metadata.
66
+ */
67
+ export function hasNoLogParameter(
68
+ resolverClass: Function,
69
+ methodName: string,
70
+ parameterIndex: number,
71
+ ): boolean {
72
+ const target = resolverClass.prototype as object;
73
+ const marks = Reflect.getMetadata(NO_LOG_PARAM_KEY, target, methodName) as Set<number> | undefined;
74
+ return marks !== undefined && marks.has(parameterIndex);
75
+ }
76
+
77
+ /**
78
+ * Returns the set of field names decorated `@NoLog` on the given input class. Empty set
79
+ * if none were marked.
80
+ *
81
+ * Pass the input class itself (e.g. `GetDataInputType`) — the function reads from the
82
+ * prototype internally.
83
+ */
84
+ export function getNoLogFields(inputTypeClass: Function): ReadonlySet<string> {
85
+ const target = inputTypeClass.prototype as object;
86
+ const marks = Reflect.getMetadata(NO_LOG_FIELD_KEY, target) as Set<string> | undefined;
87
+ return marks ?? new Set<string>();
88
+ }