@memberjunction/server 5.37.0 → 5.39.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 (121) hide show
  1. package/README.md +79 -12
  2. package/dist/agents/skip-sdk.d.ts.map +1 -1
  3. package/dist/agents/skip-sdk.js +16 -12
  4. package/dist/agents/skip-sdk.js.map +1 -1
  5. package/dist/apolloServer/index.d.ts +0 -8
  6. package/dist/apolloServer/index.d.ts.map +1 -1
  7. package/dist/apolloServer/index.js +61 -0
  8. package/dist/apolloServer/index.js.map +1 -1
  9. package/dist/config.d.ts +68 -0
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +24 -0
  12. package/dist/config.js.map +1 -1
  13. package/dist/context.d.ts.map +1 -1
  14. package/dist/context.js +2 -21
  15. package/dist/context.js.map +1 -1
  16. package/dist/generated/generated.d.ts +172 -5
  17. package/dist/generated/generated.d.ts.map +1 -1
  18. package/dist/generated/generated.js +867 -14
  19. package/dist/generated/generated.js.map +1 -1
  20. package/dist/generic/FireAndForgetHeartbeat.d.ts +51 -0
  21. package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -0
  22. package/dist/generic/FireAndForgetHeartbeat.js +44 -0
  23. package/dist/generic/FireAndForgetHeartbeat.js.map +1 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +177 -16
  27. package/dist/index.js.map +1 -1
  28. package/dist/logging/NoLog.d.ts +50 -0
  29. package/dist/logging/NoLog.d.ts.map +1 -0
  30. package/dist/logging/NoLog.js +80 -0
  31. package/dist/logging/NoLog.js.map +1 -0
  32. package/dist/logging/bootAudit.d.ts +43 -0
  33. package/dist/logging/bootAudit.d.ts.map +1 -0
  34. package/dist/logging/bootAudit.js +83 -0
  35. package/dist/logging/bootAudit.js.map +1 -0
  36. package/dist/logging/boundaryLogPayload.d.ts +18 -0
  37. package/dist/logging/boundaryLogPayload.d.ts.map +1 -0
  38. package/dist/logging/boundaryLogPayload.js +18 -0
  39. package/dist/logging/boundaryLogPayload.js.map +1 -0
  40. package/dist/logging/secretRedactor.d.ts +23 -0
  41. package/dist/logging/secretRedactor.d.ts.map +1 -0
  42. package/dist/logging/secretRedactor.js +53 -0
  43. package/dist/logging/secretRedactor.js.map +1 -0
  44. package/dist/logging/shortenForLog.d.ts +8 -0
  45. package/dist/logging/shortenForLog.d.ts.map +1 -0
  46. package/dist/logging/shortenForLog.js +21 -0
  47. package/dist/logging/shortenForLog.js.map +1 -0
  48. package/dist/logging/variablesLoggingMiddleware.d.ts +22 -0
  49. package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -0
  50. package/dist/logging/variablesLoggingMiddleware.js +127 -0
  51. package/dist/logging/variablesLoggingMiddleware.js.map +1 -0
  52. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  53. package/dist/resolvers/GetDataResolver.js +8 -4
  54. package/dist/resolvers/GetDataResolver.js.map +1 -1
  55. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +259 -2
  56. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  57. package/dist/resolvers/IntegrationDiscoveryResolver.js +1276 -117
  58. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  59. package/dist/resolvers/IntegrationProgressResolver.d.ts +90 -0
  60. package/dist/resolvers/IntegrationProgressResolver.d.ts.map +1 -0
  61. package/dist/resolvers/IntegrationProgressResolver.js +196 -0
  62. package/dist/resolvers/IntegrationProgressResolver.js.map +1 -0
  63. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  64. package/dist/resolvers/MCPResolver.js +19 -14
  65. package/dist/resolvers/MCPResolver.js.map +1 -1
  66. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  67. package/dist/resolvers/QueryResolver.js +5 -4
  68. package/dist/resolvers/QueryResolver.js.map +1 -1
  69. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  70. package/dist/resolvers/QuerySystemUserResolver.js +6 -18
  71. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  72. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  73. package/dist/resolvers/RunAIAgentResolver.js +32 -64
  74. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  75. package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
  76. package/dist/resolvers/RunTestResolver.js +12 -2
  77. package/dist/resolvers/RunTestResolver.js.map +1 -1
  78. package/dist/resolvers/SearchEntitiesResolver.d.ts +46 -0
  79. package/dist/resolvers/SearchEntitiesResolver.d.ts.map +1 -0
  80. package/dist/resolvers/SearchEntitiesResolver.js +216 -0
  81. package/dist/resolvers/SearchEntitiesResolver.js.map +1 -0
  82. package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
  83. package/dist/resolvers/VectorizeEntityResolver.js +14 -1
  84. package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
  85. package/dist/services/ScheduledJobsService.d.ts.map +1 -1
  86. package/dist/services/ScheduledJobsService.js +14 -2
  87. package/dist/services/ScheduledJobsService.js.map +1 -1
  88. package/package.json +75 -74
  89. package/src/__tests__/NoLog.test.ts +76 -0
  90. package/src/__tests__/bootAudit.test.ts +188 -0
  91. package/src/__tests__/boundaryLogPayload.test.ts +31 -0
  92. package/src/__tests__/getDataTokenRedaction.test.ts +84 -0
  93. package/src/__tests__/resolverBase.rls.test.ts +224 -0
  94. package/src/__tests__/secretRedactor.test.ts +163 -0
  95. package/src/__tests__/subscriptionRedaction.test.ts +217 -0
  96. package/src/agents/skip-sdk.ts +16 -13
  97. package/src/apolloServer/index.ts +58 -0
  98. package/src/config.ts +27 -0
  99. package/src/context.ts +2 -19
  100. package/src/generated/generated.ts +627 -15
  101. package/src/generic/FireAndForgetHeartbeat.ts +85 -0
  102. package/src/generic/__tests__/FireAndForgetHeartbeat.test.ts +99 -0
  103. package/src/index.ts +199 -19
  104. package/src/logging/NoLog.ts +88 -0
  105. package/src/logging/bootAudit.ts +117 -0
  106. package/src/logging/boundaryLogPayload.ts +19 -0
  107. package/src/logging/secretRedactor.ts +82 -0
  108. package/src/logging/shortenForLog.ts +17 -0
  109. package/src/logging/variablesLoggingMiddleware.ts +191 -0
  110. package/src/resolvers/GetDataResolver.ts +9 -5
  111. package/src/resolvers/IntegrationDiscoveryResolver.ts +1111 -120
  112. package/src/resolvers/IntegrationProgressResolver.ts +220 -0
  113. package/src/resolvers/MCPResolver.ts +18 -14
  114. package/src/resolvers/QueryResolver.ts +7 -6
  115. package/src/resolvers/QuerySystemUserResolver.ts +11 -25
  116. package/src/resolvers/RunAIAgentResolver.ts +34 -71
  117. package/src/resolvers/RunTestResolver.ts +14 -2
  118. package/src/resolvers/SearchEntitiesResolver.ts +173 -0
  119. package/src/resolvers/VectorizeEntityResolver.ts +14 -1
  120. package/src/resolvers/__tests__/IntegrationProgressResolver.test.ts +170 -0
  121. 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
+ }
@@ -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';
@@ -26,8 +26,12 @@ import sql from 'mssql';
26
26
  import { WebSocketServer } from 'ws';
27
27
  import buildApolloServer from './apolloServer/index.js';
28
28
  import { configInfo, dbDatabase, dbHost, dbPort, dbUsername, graphqlPort, graphqlRootPath, mj_core_schema, websiteRunFromPackage, RESTApiOptions } from './config.js';
29
+ import { default as jwt } from 'jsonwebtoken';
29
30
  import { contextFunction, createUnifiedAuthMiddleware, getUserPayload } from './context.js';
31
+ import { UserPayload } from './types.js';
30
32
  import { requireSystemUserDirective, publicDirective } from './directives/index.js';
33
+ import { variablesLoggingMiddleware } from './logging/variablesLoggingMiddleware.js';
34
+ import { auditResolversForUndecoratedArgs } from './logging/bootAudit.js';
31
35
  import createMSSQLConfig from './orm.js';
32
36
  import { setupRESTEndpoints } from './rest/setupRESTEndpoints.js';
33
37
  import { createOAuthCallbackHandler } from './rest/OAuthCallbackHandler.js';
@@ -43,7 +47,9 @@ import { GetAPIKeyEngine } from '@memberjunction/api-keys';
43
47
  import { RedisLocalStorageProvider } from '@memberjunction/redis-provider';
44
48
  import { GenericDatabaseProvider } from '@memberjunction/generic-database-provider';
45
49
  import { PubSubManager } from './generic/PubSubManager.js';
46
- import { ClientToolRequestManager } from '@memberjunction/ai-agents';
50
+ import { IntegrationProgressEmitter } from '@memberjunction/integration-progress-artifacts';
51
+ import { PublishIntegrationProgress } from './resolvers/IntegrationProgressResolver.js';
52
+ import { ClientToolRequestManager, AgentRunWatchdog } from '@memberjunction/ai-agents';
47
53
  import { CACHE_INVALIDATION_TOPIC } from './generic/CacheInvalidationResolver.js';
48
54
  import { ConnectorFactory, IntegrationEngine, IntegrationSyncOptions } from '@memberjunction/integration-engine';
49
55
  import { CronExpressionHelper } from '@memberjunction/scheduling-engine';
@@ -81,6 +87,7 @@ export { configInfo, DEFAULT_SERVER_CONFIG } from './config.js';
81
87
  export { ServerExtensionLoader, BaseServerExtension } from '@memberjunction/server-extensions-core';
82
88
  export type { ServerExtensionConfig, ExtensionInitResult, ExtensionHealthResult } from '@memberjunction/server-extensions-core';
83
89
  export * from './directives/index.js';
90
+ export { NoLog, hasNoLogParameter, getNoLogFields } from './logging/NoLog.js';
84
91
  export * from './entitySubclasses/MJEntityPermissionEntityServer.server.js';
85
92
  export * from './types.js';
86
93
  export {
@@ -105,6 +112,7 @@ export * from './resolvers/SearchKnowledgeStreamResolver.js';
105
112
  export * from './resolvers/AvailableSearchProvidersResolver.js';
106
113
  export * from './resolvers/FetchEntityVectorsResolver.js';
107
114
  export * from './resolvers/PipelineProgressResolver.js';
115
+ export * from './resolvers/IntegrationProgressResolver.js';
108
116
  export * from './resolvers/ClientToolRequestResolver.js';
109
117
  export * from './resolvers/AutotagPipelineResolver.js';
110
118
  export * from './resolvers/TagGovernanceResolver.js';
@@ -148,6 +156,7 @@ export * from './resolvers/FileResolver.js';
148
156
  export * from './resolvers/InfoResolver.js';
149
157
  export * from './resolvers/PotentialDuplicateRecordResolver.js';
150
158
  export * from './resolvers/RunTestResolver.js';
159
+ export * from './resolvers/SearchEntitiesResolver.js';
151
160
  export * from './resolvers/UserFavoriteResolver.js';
152
161
  export * from './resolvers/UserResolver.js';
153
162
  export * from './resolvers/UserViewResolver.js';
@@ -292,6 +301,86 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
292
301
  return origExecuteSQLWithPool.call(this, pool, query, parameters, contextUser);
293
302
  };
294
303
 
304
+ // Set up CodeGen-credentialed provider + in-process CodeGen runner for RSU (PostgreSQL).
305
+ // Without this, RuntimeSchemaManager has no injected runner and falls back to spawning a
306
+ // child-process CodeGen. On Postgres that child path is slow, its output is buffered (so
307
+ // progress is unobservable), and a non-responding advancedGen AI pass hangs it indefinitely
308
+ // — which leaves association/junction CRUD functions un-regenerated and blocks the sync.
309
+ // Running in-process keeps CodeGen inside the MJAPI event loop where it is observable and
310
+ // bounded by advanced_generation's per-call timeout + circuit breaker. Mirrors the SQL
311
+ // Server path below (the runner is required for BOTH platforms).
312
+ const pgCodegenUser = process.env.CODEGEN_DB_USERNAME;
313
+ const pgCodegenPass = process.env.CODEGEN_DB_PASSWORD;
314
+ if (pgCodegenUser && pgCodegenPass) {
315
+ try {
316
+ const codegenPgPool = new pg.default.Pool({
317
+ host: pgHost,
318
+ port: pgPort,
319
+ user: pgCodegenUser,
320
+ password: pgCodegenPass,
321
+ database: pgDatabase,
322
+ max: 10,
323
+ });
324
+ const codegenTestClient = await codegenPgPool.connect();
325
+ await codegenTestClient.query('SELECT 1');
326
+ codegenTestClient.release();
327
+
328
+ const { RuntimeSchemaManager } = await import('@memberjunction/schema-engine');
329
+ const codegenPgConfigData = new PostgreSQLProviderConfigData(
330
+ { Host: pgHost, Port: pgPort, Database: pgDatabase, User: pgCodegenUser, Password: pgCodegenPass },
331
+ mj_core_schema,
332
+ cacheRefreshInterval / 1000, // ms → seconds
333
+ );
334
+ const codegenPgProvider = new PostgreSQLDataProvider();
335
+ await codegenPgProvider.Config(codegenPgConfigData); // separate pool (per-instance manager); does NOT touch the global API provider
336
+ RuntimeSchemaManager.Instance.SetDDLProvider(codegenPgProvider);
337
+ console.log('RSU DDL provider initialized with CodeGen credentials (PostgreSQL).');
338
+
339
+ // Set up in-process CodeGen runner for RSU
340
+ try {
341
+ const { RunCodeGenBase } = await import('@memberjunction/codegen-lib');
342
+ const { PostgreSQLCodeGenConnection } = await import('@memberjunction/codegen-lib/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.js');
343
+
344
+ const codegenConnection = new PostgreSQLCodeGenConnection(codegenPgPool);
345
+ const codegenCurrentUser = UserCache.Instance.Users.find(u => u.Type?.trim().toLowerCase() === 'owner') ?? UserCache.Instance.Users[0];
346
+
347
+ const codegenDataSource = {
348
+ provider: codegenPgProvider,
349
+ connection: codegenConnection,
350
+ currentUser: codegenCurrentUser,
351
+ connectionInfo: `${pgHost}:${pgPort}/${pgDatabase} (CodeGen)`,
352
+ };
353
+
354
+ const runObject = MJGlobal.Instance.ClassFactory.CreateInstance(RunCodeGenBase) as InstanceType<typeof RunCodeGenBase>;
355
+
356
+ const rsuWorkDir = process.env.RSU_WORK_DIR || process.cwd();
357
+ RuntimeSchemaManager.Instance.SetCodeGenRunner({
358
+ RunInProcess: (skipDB) => runObject.RunInProcess(codegenDataSource, skipDB, rsuWorkDir),
359
+ });
360
+ console.log('RSU in-process CodeGen runner initialized (PostgreSQL).');
361
+
362
+ // Inject CodeGen output paths for targeted git staging
363
+ const { initializeConfig } = await import('@memberjunction/codegen-lib');
364
+ const cgConfig = initializeConfig(rsuWorkDir);
365
+ const outputPaths = (cgConfig.output ?? []).map((o: { directory: string }) => o.directory);
366
+ RuntimeSchemaManager.Instance.SetCodeGenOutputPaths(outputPaths);
367
+ console.log(`RSU CodeGen output paths: ${outputPaths.length} directories configured.`);
368
+
369
+ // Point RSU's soft PK/FK writer at the SAME file CodeGen reads (mj.config.cjs
370
+ // `additionalSchemaInfo`), or RSU writes soft PKs to its own default path while
371
+ // CodeGen reads a different one and skips integration tables with "No primary key found".
372
+ if (cgConfig.additionalSchemaInfo) {
373
+ RuntimeSchemaManager.Instance.SetAdditionalSchemaInfoPath(cgConfig.additionalSchemaInfo);
374
+ console.log(`RSU additionalSchemaInfo path: ${cgConfig.additionalSchemaInfo}`);
375
+ }
376
+ } catch (codegenErr) {
377
+ console.warn(`RSU in-process CodeGen runner setup failed (will fall back to child process): ${(codegenErr as Error).message}`);
378
+ }
379
+ } catch (err) {
380
+ console.warn(`RSU DDL provider setup failed (PostgreSQL; RSU will fall back to default provider): ${(err as Error).message}`);
381
+ }
382
+ }
383
+
295
384
  const md = new Metadata(); // global-provider-ok: bootstrap
296
385
  console.log(`Data Source has been initialized. ${md?.Entities ? md.Entities.length : 0} entities loaded.`);
297
386
  } else {
@@ -346,6 +435,13 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
346
435
  ...createMSSQLConfig(),
347
436
  user: codegenUser,
348
437
  password: codegenPass,
438
+ // CodeGen's metadata management ("manage entity fields", schema refresh) runs
439
+ // long-running queries across ALL entities. The default API requestTimeout (30s)
440
+ // is far too short at scale — e.g. a large integration Create-Tables pushing the
441
+ // schema to 400+ entities times out mid-refresh and leaves entity-field metadata
442
+ // only partially applied. This pool is used ONLY for CodeGen/DDL (never to serve
443
+ // API requests), so a generous timeout is safe. Mirrors MJCLI's baseline connection.
444
+ requestTimeout: 600000,
349
445
  });
350
446
  codegenPool.on('error', (err) => {
351
447
  console.error('[ConnectionPool] CodeGen pool connection error:', err.message);
@@ -388,6 +484,15 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
388
484
  const outputPaths = (codegenConfig.output ?? []).map((o: { directory: string }) => o.directory);
389
485
  RuntimeSchemaManager.Instance.SetCodeGenOutputPaths(outputPaths);
390
486
  console.log(`RSU CodeGen output paths: ${outputPaths.length} directories configured.`);
487
+
488
+ // Point RSU's soft PK/FK writer at the SAME file CodeGen reads (mj.config.cjs
489
+ // `additionalSchemaInfo`). Without this, RSU writes soft PKs to its own default
490
+ // path while CodeGen reads a different one and skips every integration table
491
+ // with "No primary key found".
492
+ if (codegenConfig.additionalSchemaInfo) {
493
+ RuntimeSchemaManager.Instance.SetAdditionalSchemaInfoPath(codegenConfig.additionalSchemaInfo);
494
+ console.log(`RSU additionalSchemaInfo path: ${codegenConfig.additionalSchemaInfo}`);
495
+ }
391
496
  } catch (codegenErr) {
392
497
  console.warn(`RSU in-process CodeGen runner setup failed (will fall back to child process): ${(codegenErr as Error).message}`);
393
498
  }
@@ -618,6 +723,24 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
618
723
  (topic: string, payload: Record<string, unknown>) => PubSubManager.Instance.Publish(topic, payload)
619
724
  );
620
725
 
726
+ // §11: fan every integration progress event onto the live GraphQL subscription topic. The emitter
727
+ // (in @memberjunction/integration-progress-artifacts) stays server-agnostic; we inject the publish
728
+ // here so integration runs/refreshes/syncs are subscribable in addition to the durable JSONL
729
+ // artifact + the pollable IntegrationTailRunEvents query.
730
+ IntegrationProgressEmitter.SetPublishHook((manifest, event) =>
731
+ PublishIntegrationProgress({
732
+ RunID: manifest.runID,
733
+ Kind: manifest.runKind,
734
+ CompanyIntegrationID: manifest.companyIntegrationID,
735
+ EventType: event.eventType,
736
+ Seq: event.seq,
737
+ Message: event.message,
738
+ Stage: event.stage,
739
+ Level: event.level,
740
+ Data: event.data,
741
+ })
742
+ );
743
+
621
744
  // Global listener: broadcast CACHE_INVALIDATION to all browser clients whenever
622
745
  // ANY BaseEntity save/delete occurs on this server — regardless of whether it
623
746
  // originated from a GraphQL mutation or internal server-side code (agents, actions,
@@ -650,10 +773,15 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
650
773
  scalarsMap: [{ type: Date, scalar: GraphQLTimestamp }],
651
774
  emitSchemaFile: websiteRunFromPackage !== 1,
652
775
  pubSub,
776
+ globalMiddlewares: [variablesLoggingMiddleware],
653
777
  }),
654
778
  ],
655
779
  typeDefs: [requireSystemUserDirective.typeDefs, publicDirective.typeDefs],
656
780
  });
781
+
782
+ // Verbose-mode-only diagnostic: name custom-resolver args that aren't metadata-bound
783
+ // and aren't @NoLog-marked. No-op in default config (logVariables=false).
784
+ auditResolversForUndecoratedArgs();
657
785
  schema = requireSystemUserDirective.transformer(schema);
658
786
  schema = publicDirective.transformer(schema);
659
787
 
@@ -667,28 +795,71 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
667
795
  const httpServer = createServer(app);
668
796
 
669
797
  const webSocketServer = new WebSocketServer({ server: httpServer, path: graphqlRootPath });
670
- const serverCleanup = useServer(
798
+
799
+ // Track per-connection expiry timers so we can clean them up on close
800
+ const expiryTimers = new WeakMap<object, ReturnType<typeof setTimeout>>();
801
+
802
+ const serverCleanup = useServer<Record<string, unknown>, { userPayload: UserPayload }>(
671
803
  {
672
804
  schema,
673
- context: async ({ connectionParams }) => {
674
- const userPayload = await getUserPayload(String(connectionParams?.Authorization), undefined, dataSources);
675
- return { userPayload };
676
- },
677
- onError: (ctx, message, errors) => {
678
- // Check if error is token expiration (expected behavior)
679
- const isTokenExpired = errors.some(err =>
680
- err.extensions?.code === 'JWT_EXPIRED' ||
681
- err.message?.includes('token has expired')
682
- );
805
+ // RAII: validate the token once at connection time and cache the result.
806
+ // This prevents re-validating (and re-logging) on every subscription message.
807
+ onConnect: async (ctx) => {
808
+ try {
809
+ const token = String(ctx.connectionParams?.Authorization);
810
+ const userPayload = await getUserPayload(token, undefined, dataSources);
811
+
812
+ // Store validated payload on the connection for use in context()
813
+ ctx.extra.userPayload = userPayload;
814
+
815
+ // Schedule proactive socket close at token expiry so the client
816
+ // reconnects with a fresh token instead of spamming errors.
817
+ const decoded = jwt.decode(token.replace('Bearer ', ''));
818
+ if (decoded && typeof decoded !== 'string' && decoded.exp) {
819
+ const msUntilExpiry = decoded.exp * 1000 - Date.now();
820
+ if (msUntilExpiry > 0) {
821
+ const timer = setTimeout(() => {
822
+ console.log(`WebSocket token expiring — closing connection for ${userPayload.email}`);
823
+ // 4403 = Forbidden — retriable in graphql-ws, signals "get a new token"
824
+ // (4401 is reserved as fatal/"Unauthorized" in the graphql-ws protocol)
825
+ try {
826
+ ctx.extra.socket.close(4403, 'Token expired');
827
+ } catch {
828
+ // Socket may already be closed; benign
829
+ }
830
+ }, msUntilExpiry);
831
+ // Prevent the timer from keeping the process alive during shutdown
832
+ timer.unref();
833
+ expiryTimers.set(ctx, timer);
834
+ }
835
+ }
683
836
 
684
- if (isTokenExpired) {
685
- // Log at warn level - this is expected from long-lived browser sessions
686
- console.warn('WebSocket connection token expired - client should reconnect with refreshed token');
687
- } else {
688
- // Log actual errors at error level
689
- console.error('WebSocket error:', errors);
837
+ return true;
838
+ } catch (error: unknown) {
839
+ // Return false instead of throwing graphql-ws sends 4403 Forbidden
840
+ // (retriable) rather than letting the throw produce 1011 (fatal).
841
+ const msg = error instanceof Error ? error.message : 'Authentication failed';
842
+ console.warn(`WebSocket connection rejected: ${msg}`);
843
+ return false;
844
+ }
845
+ },
846
+ // context() now just reads the already-validated payload — no I/O, no logging
847
+ context: async (ctx) => {
848
+ return { userPayload: ctx.extra.userPayload as UserPayload };
849
+ },
850
+ onClose: (ctx) => {
851
+ // Clear the expiry timer if the connection closes before the token expires
852
+ const timer = expiryTimers.get(ctx);
853
+ if (timer) {
854
+ clearTimeout(timer);
855
+ expiryTimers.delete(ctx);
690
856
  }
691
857
  },
858
+ onError: (_ctx, _message, errors) => {
859
+ // Token expiry errors can't happen anymore (handled in onConnect + timer),
860
+ // so all errors here are genuine and worth logging.
861
+ console.error('WebSocket error:', errors);
862
+ },
692
863
  },
693
864
  webSocketServer
694
865
  );
@@ -884,6 +1055,15 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
884
1055
  .catch(err => console.warn(`[IntegrationEngine] Orphaned sync resume failed: ${err}`));
885
1056
  }
886
1057
 
1058
+ // Force-fail any agent runs left 'Running' by a process that died (restart/crash/OOM) or whose
1059
+ // terminal-state write never landed. Staleness-based, so it never touches runs another healthy
1060
+ // instance is still heart-beating. The watchdog also self-registers for graceful-shutdown
1061
+ // cancellation (via ShutdownRegistry) once it begins tracking this process's first live run.
1062
+ if (resumeUser && Metadata.Provider instanceof DatabaseProviderBase) { // global-provider-ok: server startup recovery — one-shot orphaned-run sweep at boot
1063
+ AgentRunWatchdog.SweepOrphanedRuns(Metadata.Provider, resumeUser) // global-provider-ok: server startup recovery — one-shot orphaned-run sweep at boot
1064
+ .catch(err => console.warn(`[AgentRunWatchdog] Startup sweep failed: ${err}`));
1065
+ }
1066
+
887
1067
  // Set up graceful shutdown handlers
888
1068
  const gracefulShutdown = async (signal: string) => {
889
1069
  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
+ }