@memberjunction/server 5.40.2 → 5.42.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 (129) hide show
  1. package/dist/agentSessions/HostInstance.d.ts +19 -0
  2. package/dist/agentSessions/HostInstance.d.ts.map +1 -0
  3. package/dist/agentSessions/HostInstance.js +48 -0
  4. package/dist/agentSessions/HostInstance.js.map +1 -0
  5. package/dist/agentSessions/SessionJanitor.d.ts +97 -0
  6. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -0
  7. package/dist/agentSessions/SessionJanitor.js +222 -0
  8. package/dist/agentSessions/SessionJanitor.js.map +1 -0
  9. package/dist/agentSessions/SessionManager.d.ts +142 -0
  10. package/dist/agentSessions/SessionManager.d.ts.map +1 -0
  11. package/dist/agentSessions/SessionManager.js +308 -0
  12. package/dist/agentSessions/SessionManager.js.map +1 -0
  13. package/dist/agentSessions/index.d.ts +5 -0
  14. package/dist/agentSessions/index.d.ts.map +1 -0
  15. package/dist/agentSessions/index.js +31 -0
  16. package/dist/agentSessions/index.js.map +1 -0
  17. package/dist/agentSessions/remoteBrowserGoalEngine.d.ts +102 -0
  18. package/dist/agentSessions/remoteBrowserGoalEngine.d.ts.map +1 -0
  19. package/dist/agentSessions/remoteBrowserGoalEngine.js +164 -0
  20. package/dist/agentSessions/remoteBrowserGoalEngine.js.map +1 -0
  21. package/dist/agentSessions/remoteBrowserGoalRegistry.d.ts +66 -0
  22. package/dist/agentSessions/remoteBrowserGoalRegistry.d.ts.map +1 -0
  23. package/dist/agentSessions/remoteBrowserGoalRegistry.js +86 -0
  24. package/dist/agentSessions/remoteBrowserGoalRegistry.js.map +1 -0
  25. package/dist/auth/initializeProviders.d.ts.map +1 -1
  26. package/dist/auth/initializeProviders.js +4 -2
  27. package/dist/auth/initializeProviders.js.map +1 -1
  28. package/dist/config.d.ts +7 -0
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +12 -2
  31. package/dist/config.js.map +1 -1
  32. package/dist/context.d.ts.map +1 -1
  33. package/dist/context.js +41 -7
  34. package/dist/context.js.map +1 -1
  35. package/dist/generated/generated.d.ts +1296 -49
  36. package/dist/generated/generated.d.ts.map +1 -1
  37. package/dist/generated/generated.js +50343 -43421
  38. package/dist/generated/generated.js.map +1 -1
  39. package/dist/generic/ResolverBase.js +1 -1
  40. package/dist/generic/ResolverBase.js.map +1 -1
  41. package/dist/generic/RunViewResolver.js +9 -10
  42. package/dist/generic/RunViewResolver.js.map +1 -1
  43. package/dist/index.d.ts +4 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +127 -54
  46. package/dist/index.js.map +1 -1
  47. package/dist/integration/CustomColumnPromoter.d.ts +108 -0
  48. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -0
  49. package/dist/integration/CustomColumnPromoter.js +508 -0
  50. package/dist/integration/CustomColumnPromoter.js.map +1 -0
  51. package/dist/logging/StartupLogger.d.ts +177 -0
  52. package/dist/logging/StartupLogger.d.ts.map +1 -0
  53. package/dist/logging/StartupLogger.js +354 -0
  54. package/dist/logging/StartupLogger.js.map +1 -0
  55. package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -1
  56. package/dist/logging/variablesLoggingMiddleware.js +21 -2
  57. package/dist/logging/variablesLoggingMiddleware.js.map +1 -1
  58. package/dist/resolvers/AgentSessionResolver.d.ts +42 -0
  59. package/dist/resolvers/AgentSessionResolver.d.ts.map +1 -0
  60. package/dist/resolvers/AgentSessionResolver.js +152 -0
  61. package/dist/resolvers/AgentSessionResolver.js.map +1 -0
  62. package/dist/resolvers/EntityPermissionResolver.d.ts +16 -0
  63. package/dist/resolvers/EntityPermissionResolver.d.ts.map +1 -0
  64. package/dist/resolvers/EntityPermissionResolver.js +95 -0
  65. package/dist/resolvers/EntityPermissionResolver.js.map +1 -0
  66. package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts +27 -0
  67. package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts.map +1 -0
  68. package/dist/resolvers/ExecuteRemoteOperationResolver.js +136 -0
  69. package/dist/resolvers/ExecuteRemoteOperationResolver.js.map +1 -0
  70. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +101 -1
  71. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  72. package/dist/resolvers/IntegrationDiscoveryResolver.js +544 -42
  73. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  74. package/dist/resolvers/RealtimeBridgeResolver.d.ts +121 -0
  75. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -0
  76. package/dist/resolvers/RealtimeBridgeResolver.js +533 -0
  77. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -0
  78. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +688 -0
  79. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -0
  80. package/dist/resolvers/RealtimeClientSessionResolver.js +1774 -0
  81. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -0
  82. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +432 -0
  83. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -0
  84. package/dist/resolvers/RemoteBrowserActionResolver.js +1108 -0
  85. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -0
  86. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  87. package/dist/resolvers/RunAIAgentResolver.js +0 -7
  88. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  89. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
  90. package/dist/rest/setupRESTEndpoints.js +5 -3
  91. package/dist/rest/setupRESTEndpoints.js.map +1 -1
  92. package/dist/services/ScheduledJobsService.d.ts.map +1 -1
  93. package/dist/services/ScheduledJobsService.js +6 -5
  94. package/dist/services/ScheduledJobsService.js.map +1 -1
  95. package/package.json +83 -74
  96. package/src/__tests__/RealtimeBridgeResolver.test.ts +100 -0
  97. package/src/__tests__/RealtimeClientSessionResolver.test.ts +2605 -0
  98. package/src/__tests__/RemoteBrowserAudioStream.test.ts +181 -0
  99. package/src/__tests__/RemoteBrowserGoalEngine.test.ts +144 -0
  100. package/src/__tests__/SessionJanitor.test.ts +234 -0
  101. package/src/__tests__/SessionManager.test.ts +465 -0
  102. package/src/__tests__/remoteBrowserGoalRegistry.test.ts +81 -0
  103. package/src/__tests__/subscriptionRedaction.test.ts +5 -0
  104. package/src/agentSessions/HostInstance.ts +53 -0
  105. package/src/agentSessions/SessionJanitor.ts +267 -0
  106. package/src/agentSessions/SessionManager.ts +446 -0
  107. package/src/agentSessions/index.ts +32 -0
  108. package/src/agentSessions/remoteBrowserGoalEngine.ts +191 -0
  109. package/src/agentSessions/remoteBrowserGoalRegistry.ts +122 -0
  110. package/src/auth/initializeProviders.ts +4 -2
  111. package/src/config.ts +13 -2
  112. package/src/context.ts +42 -7
  113. package/src/generated/generated.ts +33808 -29025
  114. package/src/generic/ResolverBase.ts +1 -1
  115. package/src/generic/RunViewResolver.ts +9 -9
  116. package/src/index.ts +128 -54
  117. package/src/integration/CustomColumnPromoter.ts +597 -0
  118. package/src/logging/StartupLogger.ts +441 -0
  119. package/src/logging/variablesLoggingMiddleware.ts +25 -5
  120. package/src/resolvers/AgentSessionResolver.ts +138 -0
  121. package/src/resolvers/EntityPermissionResolver.ts +73 -0
  122. package/src/resolvers/ExecuteRemoteOperationResolver.ts +108 -0
  123. package/src/resolvers/IntegrationDiscoveryResolver.ts +396 -37
  124. package/src/resolvers/RealtimeBridgeResolver.ts +450 -0
  125. package/src/resolvers/RealtimeClientSessionResolver.ts +2162 -0
  126. package/src/resolvers/RemoteBrowserActionResolver.ts +1122 -0
  127. package/src/resolvers/RunAIAgentResolver.ts +1 -9
  128. package/src/rest/setupRESTEndpoints.ts +5 -3
  129. package/src/services/ScheduledJobsService.ts +6 -5
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provi
7
7
  import { MJGlobal, MJEventType, UUIDsEqual, ShutdownRegistry } from '@memberjunction/global';
8
8
  import { setupSQLServerClient, SQLServerDataProvider, SQLServerProviderConfigData, UserCache } from '@memberjunction/sqlserver-dataprovider';
9
9
  import { extendConnectionPoolWithQuery } from './util.js';
10
+ import { registerIntegrationCustomColumnPromoter } from './integration/CustomColumnPromoter.js';
10
11
  import { default as BodyParser } from 'body-parser';
11
12
  import compression from 'compression'; // Add compression middleware
12
13
  import cors from 'cors';
@@ -15,6 +16,7 @@ import { default as fg } from 'fast-glob';
15
16
  import { useServer } from 'graphql-ws/lib/use/ws';
16
17
  import { createServer } from 'node:http';
17
18
  import { fileURLToPath } from 'node:url';
19
+ import { readFileSync } from 'node:fs';
18
20
  import { sep } from 'node:path';
19
21
  import 'reflect-metadata';
20
22
  import { ReplaySubject } from 'rxjs';
@@ -23,12 +25,14 @@ import { PubSub } from 'graphql-subscriptions';
23
25
  import sql from 'mssql';
24
26
  import { WebSocketServer } from 'ws';
25
27
  import buildApolloServer from './apolloServer/index.js';
26
- import { configInfo, dbDatabase, dbHost, dbPort, dbUsername, graphqlPort, graphqlRootPath, mj_core_schema, websiteRunFromPackage } from './config.js';
28
+ import { configInfo, configFilePath, dbDatabase, dbHost, dbPort, dbUsername, graphqlPort, graphqlRootPath, mj_core_schema, websiteRunFromPackage } from './config.js';
27
29
  import { default as jwt } from 'jsonwebtoken';
28
30
  import { contextFunction, createUnifiedAuthMiddleware, getUserPayload } from './context.js';
29
31
  import { requireSystemUserDirective, publicDirective } from './directives/index.js';
30
32
  import { variablesLoggingMiddleware } from './logging/variablesLoggingMiddleware.js';
31
33
  import { auditResolversForUndecoratedArgs } from './logging/bootAudit.js';
34
+ import { StartupLogger } from './logging/StartupLogger.js';
35
+ import { AuthProviderFactory } from '@memberjunction/auth-providers';
32
36
  import createMSSQLConfig from './orm.js';
33
37
  import { setupRESTEndpoints } from './rest/setupRESTEndpoints.js';
34
38
  import { createOAuthCallbackHandler } from './rest/OAuthCallbackHandler.js';
@@ -37,13 +41,14 @@ import { createMagicLinkHandler, registerMagicLinkAuthProvider, MAGIC_LINK_MOUNT
37
41
  import { resolve } from 'node:path';
38
42
  import { DataSourceInfo, raiseEvent } from './types.js';
39
43
  import { ScheduledJobsService } from './services/ScheduledJobsService.js';
40
- import { LocalCacheManager, TelemetryManager } from '@memberjunction/core';
44
+ import { LocalCacheManager, TelemetryManager, LogStatus, SetVerboseLogging } from '@memberjunction/core';
41
45
  import { GetAPIKeyEngine } from '@memberjunction/api-keys';
42
46
  import { RedisLocalStorageProvider } from '@memberjunction/redis-provider';
43
47
  import { PubSubManager } from './generic/PubSubManager.js';
44
48
  import { IntegrationProgressEmitter } from '@memberjunction/integration-progress-artifacts';
45
49
  import { PublishIntegrationProgress } from './resolvers/IntegrationProgressResolver.js';
46
50
  import { ClientToolRequestManager, AgentRunWatchdog } from '@memberjunction/ai-agents';
51
+ import { SessionJanitor } from './agentSessions/index.js';
47
52
  import { CACHE_INVALIDATION_TOPIC } from './generic/CacheInvalidationResolver.js';
48
53
  import { ConnectorFactory, IntegrationEngine } from '@memberjunction/integration-engine';
49
54
  import { CronExpressionHelper } from '@memberjunction/scheduling-engine';
@@ -140,6 +145,10 @@ export * from './resolvers/UserViewResolver.js';
140
145
  export * from './resolvers/VersionHistoryResolver.js';
141
146
  export * from './resolvers/CurrentUserContextResolver.js';
142
147
  export * from './resolvers/RSUResolver.js';
148
+ export * from './resolvers/AgentSessionResolver.js';
149
+ export * from './resolvers/RealtimeClientSessionResolver.js';
150
+ export * from './resolvers/RemoteBrowserActionResolver.js';
151
+ export * from './agentSessions/index.js';
143
152
  export { GetReadOnlyDataSource, GetReadWriteDataSource, GetReadWriteProvider, GetReadOnlyProvider } from './util.js';
144
153
  export * from './generated/generated.js';
145
154
  export * from './multiTenancy/index.js';
@@ -154,13 +163,37 @@ const localPath = (p) => {
154
163
  return resolvedPath;
155
164
  };
156
165
  export const createApp = () => express();
166
+ /**
167
+ * Resolves the MJServer package version for the startup summary header.
168
+ *
169
+ * Reads `@memberjunction/server`'s own `package.json` relative to this module.
170
+ * Returns `undefined` (and the header omits the version) if it can't be read,
171
+ * so version display is best-effort and never blocks startup.
172
+ */
173
+ function resolveServerVersion() {
174
+ try {
175
+ // package.json sits at the package root, two levels up from dist/ (or src/).
176
+ const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url));
177
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
178
+ return pkg.version;
179
+ }
180
+ catch {
181
+ return undefined;
182
+ }
183
+ }
157
184
  export const serve = async (resolverPaths, app = createApp(), options) => {
158
185
  const t0 = performance.now();
159
- const lap = (label, since) => {
160
- const ms = performance.now() - since;
161
- console.log(`⏱️ [Startup] ${label}: ${ms.toFixed(0)}ms`);
162
- return performance.now();
163
- };
186
+ // Level-gated startup logger. Resolves verbosity from telemetry.level (single
187
+ // operator knob). At `standard` (default), per-phase timings are collapsed into
188
+ // the one-line summary; at `verbose`+ each phase prints inline as before.
189
+ const startupLog = new StartupLogger();
190
+ // Wire the GLOBAL verbose-logging gate to the resolved server level, so cross-package
191
+ // LogStatusEx({ verboseOnly: true }) lines (cache, scheduling, integration, MCP, …) honor the
192
+ // configured level instead of defaulting to hidden. Without this, "verbose-only" is "always hidden".
193
+ SetVerboseLogging(startupLog.IsAtLeast('verbose'));
194
+ // Transient boot spinner (standard level + TTY only); cleared by PrintSummary.
195
+ startupLog.StartBoot();
196
+ const lap = (label, since) => startupLog.EndPhase(label, since);
164
197
  const localResolverPaths = ['resolvers/**/*Resolver.{js,ts}', 'generic/*Resolver.{js,ts}', 'generated/generated.{js,ts}'].map(localPath);
165
198
  const combinedResolverPaths = [...resolverPaths, ...localResolverPaths];
166
199
  const isWindows = sep === '\\';
@@ -175,7 +208,8 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
175
208
  const dataSources = [];
176
209
  if (dbType === 'postgresql') {
177
210
  // ─── PostgreSQL Path ───────────────────────────────────────────
178
- console.log('Database type: PostgreSQL');
211
+ startupLog.BeginPhase('Connecting to database');
212
+ startupLog.LogIf('verbose', 'Database type: PostgreSQL');
179
213
  const pg = await import('pg');
180
214
  const { PostgreSQLDataProvider, PostgreSQLProviderConfigData } = await import('@memberjunction/postgresql-dataprovider');
181
215
  const pgHost = process.env.PG_HOST || process.env.DB_HOST || 'localhost';
@@ -198,7 +232,7 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
198
232
  const testClient = await pgPool.connect();
199
233
  await testClient.query('SELECT 1');
200
234
  testClient.release();
201
- console.log(`PostgreSQL pool connected to ${pgHost}:${pgPort}/${pgDatabase}`);
235
+ startupLog.LogIf('verbose', `PostgreSQL pool connected to ${pgHost}:${pgPort}/${pgDatabase}`);
202
236
  // Create a DataSourceInfo with a MSSQL-compatible wrapper around pg.Pool
203
237
  // This allows existing code (types, util, context) to work without changes
204
238
  const mssqlCompatPool = createMSSQLCompatPool(pgPool);
@@ -273,11 +307,10 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
273
307
  const codegenPgProvider = new PostgreSQLDataProvider();
274
308
  await codegenPgProvider.Config(codegenPgConfigData); // separate pool (per-instance manager); does NOT touch the global API provider
275
309
  RuntimeSchemaManager.Instance.SetDDLProvider(codegenPgProvider);
276
- console.log('RSU DDL provider initialized with CodeGen credentials (PostgreSQL).');
310
+ startupLog.LogIf('verbose', 'RSU DDL provider initialized with CodeGen credentials (PostgreSQL).');
277
311
  // Set up in-process CodeGen runner for RSU
278
312
  try {
279
- const { RunCodeGenBase } = await import('@memberjunction/codegen-lib');
280
- const { PostgreSQLCodeGenConnection } = await import('@memberjunction/codegen-lib/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.js');
313
+ const { RunCodeGenBase, PostgreSQLCodeGenConnection } = await import('@memberjunction/codegen-lib');
281
314
  const codegenConnection = new PostgreSQLCodeGenConnection(codegenPgPool);
282
315
  const codegenCurrentUser = UserCache.Instance.Users.find(u => u.Type?.trim().toLowerCase() === 'owner') ?? UserCache.Instance.Users[0];
283
316
  const codegenDataSource = {
@@ -291,19 +324,19 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
291
324
  RuntimeSchemaManager.Instance.SetCodeGenRunner({
292
325
  RunInProcess: (skipDB) => runObject.RunInProcess(codegenDataSource, skipDB, rsuWorkDir),
293
326
  });
294
- console.log('RSU in-process CodeGen runner initialized (PostgreSQL).');
327
+ startupLog.LogIf('verbose', 'RSU in-process CodeGen runner initialized (PostgreSQL).');
295
328
  // Inject CodeGen output paths for targeted git staging
296
329
  const { initializeConfig } = await import('@memberjunction/codegen-lib');
297
330
  const cgConfig = initializeConfig(rsuWorkDir);
298
331
  const outputPaths = (cgConfig.output ?? []).map((o) => o.directory);
299
332
  RuntimeSchemaManager.Instance.SetCodeGenOutputPaths(outputPaths);
300
- console.log(`RSU CodeGen output paths: ${outputPaths.length} directories configured.`);
333
+ startupLog.LogIf('verbose', `RSU CodeGen output paths: ${outputPaths.length} directories configured.`);
301
334
  // Point RSU's soft PK/FK writer at the SAME file CodeGen reads (mj.config.cjs
302
335
  // `additionalSchemaInfo`), or RSU writes soft PKs to its own default path while
303
336
  // CodeGen reads a different one and skips integration tables with "No primary key found".
304
337
  if (cgConfig.additionalSchemaInfo) {
305
338
  RuntimeSchemaManager.Instance.SetAdditionalSchemaInfoPath(cgConfig.additionalSchemaInfo);
306
- console.log(`RSU additionalSchemaInfo path: ${cgConfig.additionalSchemaInfo}`);
339
+ startupLog.LogIf('verbose', `RSU additionalSchemaInfo path: ${cgConfig.additionalSchemaInfo}`);
307
340
  }
308
341
  }
309
342
  catch (codegenErr) {
@@ -315,12 +348,14 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
315
348
  }
316
349
  }
317
350
  const md = new Metadata(); // global-provider-ok: bootstrap
318
- console.log(`Data Source has been initialized. ${md?.Entities ? md.Entities.length : 0} entities loaded.`);
351
+ const pgEntityCount = md?.Entities ? md.Entities.length : 0;
352
+ startupLog.LogIf('verbose', `Data Source has been initialized. ${pgEntityCount} entities loaded.`);
353
+ startupLog.SetDatabaseInfo('PostgreSQL', `${pgHost}:${pgPort}/${pgDatabase}`, pgEntityCount);
319
354
  }
320
355
  else {
321
356
  // ─── SQL Server Path (existing behavior) ───────────────────────
322
- console.log('Database type: SQL Server');
323
- let tPhase = performance.now();
357
+ startupLog.LogIf('verbose', 'Database type: SQL Server');
358
+ let tPhase = startupLog.BeginPhase('Connecting to database');
324
359
  const pool = new sql.ConnectionPool(createMSSQLConfig());
325
360
  // Handle connection-level errors from dead/stale connections in the pool.
326
361
  // Without this handler, when Azure drops idle TCP connections, the pool silently
@@ -329,7 +364,8 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
329
364
  console.error('[ConnectionPool] Pool-level connection error (stale connection evicted):', err.message);
330
365
  });
331
366
  await pool.connect();
332
- tPhase = lap('DB Pool Connect', tPhase);
367
+ lap('DB Pool Connect', tPhase);
368
+ tPhase = startupLog.BeginPhase('Loading metadata + providers');
333
369
  dataSources.push(new DataSourceInfo({ dataSource: pool, type: 'Read-Write', host: dbHost, port: dbPort, database: dbDatabase, userName: dbUsername }));
334
370
  // Establish a second read-only connection to the database if dbReadOnlyUsername and dbReadOnlyPassword exist
335
371
  if (configInfo.dbReadOnlyUsername && configInfo.dbReadOnlyPassword) {
@@ -344,13 +380,16 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
344
380
  });
345
381
  await readOnlyPool.connect();
346
382
  dataSources.push(new DataSourceInfo({ dataSource: readOnlyPool, type: 'Read-Only', host: dbHost, port: dbPort, database: dbDatabase, userName: configInfo.dbReadOnlyUsername }));
347
- console.log('Read-only Connection Pool has been initialized.');
383
+ startupLog.LogIf('verbose', 'Read-only Connection Pool has been initialized.');
348
384
  }
349
385
  const config = new SQLServerProviderConfigData(pool, mj_core_schema, cacheRefreshInterval);
350
386
  await setupSQLServerClient(config);
351
- tPhase = lap('Metadata + Provider Setup', tPhase);
387
+ lap('Metadata + Provider Setup', tPhase);
388
+ startupLog.BeginPhase('Initializing data provider');
352
389
  const md = new Metadata(); // global-provider-ok: bootstrap
353
- console.log(`Data Source has been initialized. ${md?.Entities ? md.Entities.length : 0} entities loaded.`);
390
+ const entityCount = md?.Entities ? md.Entities.length : 0;
391
+ startupLog.LogIf('verbose', `Data Source has been initialized. ${entityCount} entities loaded.`);
392
+ startupLog.SetDatabaseInfo('SQL Server', `${dbHost}:${dbPort}/${dbDatabase}`, entityCount);
354
393
  // Set up CodeGen-credentialed provider for RSU DDL operations (CREATE TABLE, CREATE SCHEMA, etc.)
355
394
  const codegenUser = process.env.CODEGEN_DB_USERNAME;
356
395
  const codegenPass = process.env.CODEGEN_DB_PASSWORD;
@@ -377,11 +416,10 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
377
416
  const codegenProvider = new SQLServerDataProvider();
378
417
  await codegenProvider.Config(codegenConfig);
379
418
  RuntimeSchemaManager.Instance.SetDDLProvider(codegenProvider);
380
- console.log('RSU DDL provider initialized with CodeGen credentials.');
419
+ startupLog.LogIf('verbose', 'RSU DDL provider initialized with CodeGen credentials.');
381
420
  // Set up in-process CodeGen runner for RSU
382
421
  try {
383
- const { RunCodeGenBase } = await import('@memberjunction/codegen-lib');
384
- const { SQLServerCodeGenConnection } = await import('@memberjunction/codegen-lib/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.js');
422
+ const { RunCodeGenBase, SQLServerCodeGenConnection } = await import('@memberjunction/codegen-lib');
385
423
  const codegenConnection = new SQLServerCodeGenConnection(codegenPool);
386
424
  const codegenCurrentUser = UserCache.Instance.Users.find(u => u.Type?.trim().toLowerCase() === 'owner') ?? UserCache.Instance.Users[0];
387
425
  const codegenDataSource = {
@@ -395,20 +433,20 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
395
433
  RuntimeSchemaManager.Instance.SetCodeGenRunner({
396
434
  RunInProcess: (skipDB) => runObject.RunInProcess(codegenDataSource, skipDB, rsuWorkDir),
397
435
  });
398
- console.log('RSU in-process CodeGen runner initialized.');
436
+ startupLog.LogIf('verbose', 'RSU in-process CodeGen runner initialized.');
399
437
  // Inject CodeGen output paths for targeted git staging
400
438
  const { initializeConfig } = await import('@memberjunction/codegen-lib');
401
439
  const codegenConfig = initializeConfig(rsuWorkDir);
402
440
  const outputPaths = (codegenConfig.output ?? []).map((o) => o.directory);
403
441
  RuntimeSchemaManager.Instance.SetCodeGenOutputPaths(outputPaths);
404
- console.log(`RSU CodeGen output paths: ${outputPaths.length} directories configured.`);
442
+ startupLog.LogIf('verbose', `RSU CodeGen output paths: ${outputPaths.length} directories configured.`);
405
443
  // Point RSU's soft PK/FK writer at the SAME file CodeGen reads (mj.config.cjs
406
444
  // `additionalSchemaInfo`). Without this, RSU writes soft PKs to its own default
407
445
  // path while CodeGen reads a different one and skips every integration table
408
446
  // with "No primary key found".
409
447
  if (codegenConfig.additionalSchemaInfo) {
410
448
  RuntimeSchemaManager.Instance.SetAdditionalSchemaInfoPath(codegenConfig.additionalSchemaInfo);
411
- console.log(`RSU additionalSchemaInfo path: ${codegenConfig.additionalSchemaInfo}`);
449
+ startupLog.LogIf('verbose', `RSU additionalSchemaInfo path: ${codegenConfig.additionalSchemaInfo}`);
412
450
  }
413
451
  }
414
452
  catch (codegenErr) {
@@ -420,7 +458,15 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
420
458
  }
421
459
  }
422
460
  }
423
- let tServe = performance.now();
461
+ // Register the post-sync custom-column promotion hook (gaps.md §2). Safe to call regardless of
462
+ // RSU config: the hook self-gates on captured overflow data and uses RSU only at fire time.
463
+ try {
464
+ registerIntegrationCustomColumnPromoter();
465
+ }
466
+ catch (err) {
467
+ console.warn(`Custom-column promoter registration failed (post-sync promotion disabled): ${err.message}`);
468
+ }
469
+ let tServe = startupLog.BeginPhase('Initializing services');
424
470
  // Store queryDialects config in GlobalObjectStore so MJQueryEntityServer can
425
471
  // read it without a circular dependency on MJServer
426
472
  if (configInfo.queryDialects) {
@@ -433,11 +479,11 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
433
479
  if (configInfo.telemetry?.level) {
434
480
  tm.UpdateSettings({ level: configInfo.telemetry.level });
435
481
  }
436
- console.log(`Server telemetry enabled with level: ${configInfo.telemetry.level || 'standard'}`);
482
+ startupLog.LogIf('verbose', `Server telemetry enabled with level: ${configInfo.telemetry.level || 'standard'}`);
437
483
  }
438
484
  else {
439
485
  tm.SetEnabled(false);
440
- console.log('Server telemetry disabled');
486
+ startupLog.LogIf('verbose', 'Server telemetry disabled');
441
487
  }
442
488
  // Optionally inject Redis as the shared storage provider for cross-server cache invalidation
443
489
  if (process.env.REDIS_URL) {
@@ -468,14 +514,14 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
468
514
  });
469
515
  }
470
516
  });
471
- console.log(`Redis cache provider connected: ${process.env.REDIS_URL}`);
517
+ startupLog.LogIf('verbose', `Redis cache provider connected: ${process.env.REDIS_URL}`);
472
518
  }
473
519
  // If Redis is available, swap LocalCacheManager's storage provider to Redis.
474
520
  // LocalCacheManager may have already been initialized (with in-memory provider)
475
521
  // during engine loading. SetStorageProvider migrates cached data to Redis.
476
522
  if (process.env.REDIS_URL) {
477
523
  await LocalCacheManager.Instance.SetStorageProvider(Metadata.Provider.LocalStorageProvider); // global-provider-ok: bootstrap
478
- console.log('LocalCacheManager: storage provider swapped to Redis');
524
+ startupLog.LogIf('verbose', 'LocalCacheManager: storage provider swapped to Redis');
479
525
  }
480
526
  // Ensure LocalCacheManager is initialized (no-op if already done during engine loading)
481
527
  if (!LocalCacheManager.Instance.IsInitialized) {
@@ -489,11 +535,14 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
489
535
  verboseLogging: cs.verboseLogging ?? false,
490
536
  };
491
537
  await LocalCacheManager.Instance.Initialize(Metadata.Provider.LocalStorageProvider, cacheConfig); // global-provider-ok: bootstrap
492
- console.log('LocalCacheManager initialized with cache config:', JSON.stringify({
493
- maxMemoryMB: cs.maxMemoryMB ?? 150,
494
- maxPercentOfCachePerEntity: cs.maxPercentOfCachePerEntity ?? 50,
495
- evictionSweepIntervalSeconds: cs.evictionSweepIntervalSeconds ?? 300,
496
- }));
538
+ if (startupLog.IsAtLeast('verbose')) {
539
+ // eslint-disable-next-line no-console
540
+ console.log('LocalCacheManager initialized with cache config:', JSON.stringify({
541
+ maxMemoryMB: cs.maxMemoryMB ?? 150,
542
+ maxPercentOfCachePerEntity: cs.maxPercentOfCachePerEntity ?? 50,
543
+ evictionSweepIntervalSeconds: cs.evictionSweepIntervalSeconds ?? 300,
544
+ }));
545
+ }
497
546
  }
498
547
  // Initialize APIKeyEngine singleton — reads apiKeyGeneration from mj.config.cjs automatically
499
548
  // This must happen before any request handler calls GetAPIKeyEngine()
@@ -513,7 +562,8 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
513
562
  // });
514
563
  /******TEST HARNESS FOR CHANGE DETECTION */
515
564
  /******TEST HARNESS FOR CHANGE DETECTION */
516
- tServe = lap('Telemetry + Cache + APIKey Init', tServe);
565
+ lap('Telemetry + Cache + APIKey Init', tServe);
566
+ tServe = startupLog.BeginPhase('Discovering resolvers');
517
567
  const dynamicModules = await Promise.all(paths.map((modulePath) => {
518
568
  try {
519
569
  const module = import(isWindows ? `file://${modulePath}` : modulePath);
@@ -550,7 +600,7 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
550
600
  // Initialize all middleware
551
601
  for (const mw of middlewares) {
552
602
  await mw.Initialize();
553
- console.log(` [Middleware] ${mw.Label}`);
603
+ startupLog.LogIf('verbose', ` [Middleware] ${mw.Label}`);
554
604
  }
555
605
  // Collect middleware contributions for each pipeline stage
556
606
  const mwPreAuth = [];
@@ -593,7 +643,7 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
593
643
  }));
594
644
  const mwResolvers = mwModules.flatMap((module) => Object.values(module).filter((value) => typeof value === 'function'));
595
645
  allResolvers = [...resolvers, ...mwResolvers];
596
- console.log(` [Middleware Resolvers] Loaded ${mwResolverFiles.length} resolver file(s) from middleware`);
646
+ startupLog.LogIf('verbose', ` [Middleware Resolvers] Loaded ${mwResolverFiles.length} resolver file(s) from middleware`);
597
647
  }
598
648
  }
599
649
  // Create an explicit PubSub instance so we can reference it outside of resolvers
@@ -643,7 +693,8 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
643
693
  }
644
694
  }
645
695
  });
646
- tServe = lap('Resolver + Middleware Discovery', tServe);
696
+ lap('Resolver + Middleware Discovery', tServe);
697
+ tServe = startupLog.BeginPhase('Building GraphQL schema');
647
698
  let schema = mergeSchemas({
648
699
  schemas: [
649
700
  buildSchemaSync({
@@ -666,7 +717,8 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
666
717
  for (const transformer of mwSchemaTransformers) {
667
718
  schema = transformer(schema);
668
719
  }
669
- tServe = lap('Schema Build', tServe);
720
+ lap('Schema Build', tServe);
721
+ tServe = startupLog.BeginPhase('Starting HTTP server');
670
722
  const httpServer = createServer(app);
671
723
  const webSocketServer = new WebSocketServer({ server: httpServer, path: graphqlRootPath });
672
724
  // Track per-connection expiry timers so we can clean them up on close
@@ -766,7 +818,7 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
766
818
  }
767
819
  // ─── OAuth callback routes (unauthenticated, registered BEFORE auth) ─────
768
820
  const oauthPublicUrl = configInfo.publicUrl || `${configInfo.baseUrl}:${configInfo.graphqlPort}${configInfo.graphqlRootPath || ''}`;
769
- console.log(`[OAuth] publicUrl: ${oauthPublicUrl}`);
821
+ startupLog.LogIf('verbose', `[OAuth] publicUrl: ${oauthPublicUrl}`);
770
822
  let oauthAuthenticatedRouter;
771
823
  if (oauthPublicUrl) {
772
824
  const { callbackRouter, authenticatedRouter } = createOAuthCallbackHandler({
@@ -778,13 +830,13 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
778
830
  const oauthCors = cors();
779
831
  // OAuth callback is unauthenticated (called by external auth server)
780
832
  app.use('/oauth', oauthCors, callbackRouter);
781
- console.log('[OAuth] Callback route registered at /oauth/callback');
833
+ startupLog.LogIf('verbose', '[OAuth] Callback route registered at /oauth/callback');
782
834
  }
783
835
  // ─── eSignature webhook (unauthenticated, registered BEFORE auth) ─────
784
836
  // Called by external signature providers (DocuSign Connect, etc.) without an MJ bearer token.
785
837
  // The provider DRIVER verifies the payload signature/HMAC; MJ auth does not apply here.
786
838
  app.use('/esignature', cors(), createSignatureWebhookHandler());
787
- console.log('[eSignature] Webhook route registered at /esignature/webhook/:driverKey');
839
+ startupLog.LogIf('verbose', '[eSignature] Webhook route registered at /esignature/webhook/:driverKey');
788
840
  // ─── Magic-link routes (MJ-issued, app-scoped external access) ───────────
789
841
  // Public router (JWKS + redeem) mounts BEFORE the auth middleware; the
790
842
  // authenticated invite-creation router mounts AFTER it (see below).
@@ -794,7 +846,7 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
794
846
  magicLinkAuthenticatedRouter = authenticatedRouter;
795
847
  registerMagicLinkAuthProvider(oauthPublicUrl, configInfo.magicLink);
796
848
  app.use(MAGIC_LINK_MOUNT_PATH, cors(), publicRouter);
797
- console.log(`[MagicLink] Public routes registered at ${MAGIC_LINK_MOUNT_PATH}/redeem and ${MAGIC_LINK_MOUNT_PATH}/jwks.json`);
849
+ startupLog.LogIf('verbose', `[MagicLink] Public routes registered at ${MAGIC_LINK_MOUNT_PATH}/redeem and ${MAGIC_LINK_MOUNT_PATH}/jwks.json`);
798
850
  }
799
851
  // ─── Global CORS (before auth so 401 responses include CORS headers) ─────
800
852
  // Without this, the browser blocks 401 responses from the auth middleware
@@ -843,12 +895,12 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
843
895
  if (oauthAuthenticatedRouter) {
844
896
  const oauthCors = cors();
845
897
  app.use('/oauth', oauthCors, BodyParser.json(), oauthAuthenticatedRouter);
846
- console.log('[OAuth] Authenticated routes registered at /oauth/status, /oauth/initiate, and /oauth/exchange');
898
+ startupLog.LogIf('verbose', '[OAuth] Authenticated routes registered at /oauth/status, /oauth/initiate, and /oauth/exchange');
847
899
  }
848
900
  // ─── Magic-link authenticated route (invite creation) ─────────────────────
849
901
  if (magicLinkAuthenticatedRouter) {
850
902
  app.use(MAGIC_LINK_MOUNT_PATH, cors(), magicLinkAuthenticatedRouter);
851
- console.log(`[MagicLink] Authenticated route registered at ${MAGIC_LINK_MOUNT_PATH}/create`);
903
+ startupLog.LogIf('verbose', `[MagicLink] Authenticated route registered at ${MAGIC_LINK_MOUNT_PATH}/create`);
852
904
  }
853
905
  // ─── REST API endpoints (auth already handled by unified middleware) ─────
854
906
  const restApiConfig = {
@@ -862,9 +914,11 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
862
914
  if (process.env.MJ_REST_API_ENABLED !== undefined) {
863
915
  restApiConfig.enabled = process.env.MJ_REST_API_ENABLED === 'true';
864
916
  if (restApiConfig.enabled) {
865
- console.log('REST API is enabled via environment variable');
917
+ // Genuine config override actionable, surfaced at all levels.
918
+ LogStatus('REST API is enabled via environment variable');
866
919
  }
867
920
  }
921
+ startupLog.SetRestEnabled(restApiConfig.enabled);
868
922
  if (process.env.MJ_REST_API_INCLUDE_ENTITIES) {
869
923
  restApiConfig.includeEntities = process.env.MJ_REST_API_INCLUDE_ENTITIES.split(',').map(e => e.trim());
870
924
  }
@@ -908,11 +962,23 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
908
962
  if (options?.onBeforeServe) {
909
963
  await Promise.resolve(options.onBeforeServe());
910
964
  }
911
- tServe = lap('Apollo + Express Setup', tServe);
965
+ lap('Apollo + Express Setup', tServe);
912
966
  await new Promise((resolve) => httpServer.listen({ port: graphqlPort }, resolve));
913
- lap('Total Startup', t0);
914
- console.log(`📦 Connected to database: ${dbHost}:${dbPort}/${dbDatabase}`);
915
- console.log(`🚀 Server ready at http://localhost:${graphqlPort}/`);
967
+ // The summary headline reports TRUE wall-clock since process start (see
968
+ // StartupLogger.buildStartupLine), so we don't push a synthetic "total" phase row
969
+ // (that double-counted the headline). Keep a verbose-only line for the serve()-scoped span.
970
+ startupLog.LogIf('verbose', `serve() startup: ${((performance.now() - t0) / 1000).toFixed(1)}s`);
971
+ startupLog.SetReadyUrl(`http://localhost:${graphqlPort}/`);
972
+ // Populate summary fields that are known only now, then print the one summary block.
973
+ for (const provider of AuthProviderFactory.Instance.getAllProviders()) {
974
+ startupLog.AddAuthProvider(provider.name);
975
+ }
976
+ startupLog.SetVersion(resolveServerVersion());
977
+ startupLog.SetConfigPath(configFilePath);
978
+ if (scheduledJobsService) {
979
+ startupLog.SetScheduledJobCount(scheduledJobsService.GetStatus().activeJobs);
980
+ }
981
+ startupLog.PrintSummary();
916
982
  // Process pending RSU work from pre-restart (entity maps, field maps, sync)
917
983
  processRSUPendingWork().catch(err => console.warn(`RSU pending work processing failed: ${err}`));
918
984
  // Resume any integration syncs that were orphaned by the previous process restart
@@ -929,6 +995,13 @@ export const serve = async (resolverPaths, app = createApp(), options) => {
929
995
  AgentRunWatchdog.SweepOrphanedRuns(Metadata.Provider, resumeUser) // global-provider-ok: server startup recovery — one-shot orphaned-run sweep at boot
930
996
  .catch(err => console.warn(`[AgentRunWatchdog] Startup sweep failed: ${err}`));
931
997
  }
998
+ // Launch the AI Agent Session janitor: run own-host orphan recovery once at boot, then keep a
999
+ // periodic staleness sweep running. Self-registers with ShutdownRegistry, so its timer is cleared
1000
+ // by the gracefulShutdown drain below (no explicit Stop() wiring needed here).
1001
+ if (resumeUser && Metadata.Provider instanceof DatabaseProviderBase) { // global-provider-ok: server startup recovery — boot-time session janitor uses the server's own provider
1002
+ SessionJanitor.Instance.Start(Metadata.Provider, resumeUser) // global-provider-ok: server-owned background reconciler runs under the server's provider + system user
1003
+ .catch(err => console.warn(`[SessionJanitor] Startup failed: ${err}`));
1004
+ }
932
1005
  // Set up graceful shutdown handlers
933
1006
  const gracefulShutdown = async (signal) => {
934
1007
  console.log(`\n${signal} received, shutting down gracefully...`);