@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
@@ -638,7 +638,7 @@ export class ResolverBase {
638
638
  // user has. The API key's associated user (in userPayload.userRecord) is
639
639
  // used later when the actual operation executes - their permissions are
640
640
  // the ultimate ceiling that scopes can only narrow, never expand.
641
- const systemUser = UserCache.Instance.Users.find(u => u.Type === 'System');
641
+ const systemUser = UserCache.Instance.GetSystemUser();
642
642
  if (!systemUser) {
643
643
  throw new Error('System user not found');
644
644
  }
@@ -721,7 +721,7 @@ export class RunViewResolver extends ResolverBase {
721
721
  return null;
722
722
 
723
723
  const viewInfo = super.safeFirstArrayElement<MJUserViewEntityExtended>(await super.findBy<MJUserViewEntityExtended>(provider, "MJ: User Views", { Name: input.ViewName }, userPayload.userRecord));
724
- const entity = provider.Entities.find((e) => UUIDsEqual(e.ID, viewInfo.EntityID));
724
+ const entity = provider.EntityByID(viewInfo.EntityID);
725
725
  const returnData = this.processRawData(rawData.Results, viewInfo.EntityID, entity);
726
726
  return {
727
727
  Results: returnData,
@@ -752,7 +752,7 @@ export class RunViewResolver extends ResolverBase {
752
752
  return null;
753
753
 
754
754
  const viewInfo = super.safeFirstArrayElement<MJUserViewEntityExtended>(await super.findBy<MJUserViewEntityExtended>(provider, "MJ: User Views", { ID: input.ViewID }, userPayload.userRecord));
755
- const entity = provider.Entities.find((e) => UUIDsEqual(e.ID, viewInfo.EntityID));
755
+ const entity = provider.EntityByID(viewInfo.EntityID);
756
756
  const returnData = this.processRawData(rawData.Results, viewInfo.EntityID, entity);
757
757
  return {
758
758
  Results: returnData,
@@ -781,7 +781,7 @@ export class RunViewResolver extends ResolverBase {
781
781
  const rawData = await super.RunDynamicViewGeneric(input, provider, userPayload, pubSub);
782
782
  if (rawData === null) return null;
783
783
 
784
- const entity = provider.Entities.find((e) => e.Name === input.EntityName);
784
+ const entity = provider.EntityByName(input.EntityName);
785
785
  const returnData = this.processRawData(rawData.Results, entity.ID, entity);
786
786
  return {
787
787
  Results: returnData,
@@ -816,7 +816,7 @@ export class RunViewResolver extends ResolverBase {
816
816
  let results: RunViewGenericResult[] = [];
817
817
  for (const [index, data] of rawData.entries()) {
818
818
  // EntityName is backfilled by RunViewsGeneric when ViewID/ViewName was used
819
- const entity = input[index].EntityName ? provider.Entities.find((e) => e.Name === input[index].EntityName) : null;
819
+ const entity = input[index].EntityName ? provider.EntityByName(input[index].EntityName) : null;
820
820
 
821
821
  const returnData: any[] = this.processRawData(data.Results, entity ? entity.ID : null, entity);
822
822
 
@@ -860,7 +860,7 @@ export class RunViewResolver extends ResolverBase {
860
860
  };
861
861
  }
862
862
 
863
- const entity = provider.Entities.find((e) => e.Name === input.ViewName);
863
+ const entity = provider.EntityByName(input.ViewName);
864
864
  const entityId = entity ? entity.ID : null;
865
865
  const returnData = this.processRawData(rawData.Results, entityId, entity);
866
866
  return {
@@ -908,7 +908,7 @@ export class RunViewResolver extends ResolverBase {
908
908
  }
909
909
 
910
910
  const viewInfo = super.safeFirstArrayElement<MJUserViewEntityExtended>(await super.findBy<MJUserViewEntityExtended>(provider, "MJ: User Views", { ID: input.ViewID }, userPayload.userRecord));
911
- const entity = provider.Entities.find((e) => UUIDsEqual(e.ID, viewInfo.EntityID));
911
+ const entity = provider.EntityByID(viewInfo.EntityID);
912
912
  const returnData = this.processRawData(rawData.Results, viewInfo.EntityID, entity);
913
913
  return {
914
914
  Results: returnData,
@@ -954,7 +954,7 @@ export class RunViewResolver extends ResolverBase {
954
954
  };
955
955
  }
956
956
 
957
- const entity = provider.Entities.find((e) => e.Name === input.EntityName);
957
+ const entity = provider.EntityByName(input.EntityName);
958
958
  if (!entity) {
959
959
  const errorMsg = `Entity ${input.EntityName} not found in metadata`;
960
960
  LogError(new Error(errorMsg));
@@ -1007,7 +1007,7 @@ export class RunViewResolver extends ResolverBase {
1007
1007
 
1008
1008
  let results: RunViewGenericResult[] = [];
1009
1009
  for (const [index, data] of rawData.entries()) {
1010
- const entity = provider.Entities.find((e) => e.Name === input[index].EntityName);
1010
+ const entity = provider.EntityByName(input[index].EntityName);
1011
1011
  if (!entity) {
1012
1012
  LogError(new Error(`Entity with name ${input[index].EntityName} not found`));
1013
1013
  continue;
@@ -1084,7 +1084,7 @@ export class RunViewResolver extends ResolverBase {
1084
1084
  // Transform results to include processed data rows
1085
1085
  const transformedResults: RunViewWithCacheCheckResultOutput[] = response.results.map((result, index) => {
1086
1086
  const inputItem = input[index];
1087
- const entity = provider.Entities.find(e => e.Name === inputItem.params.EntityName);
1087
+ const entity = inputItem.params.EntityName ? provider.EntityByName(inputItem.params.EntityName) : undefined;
1088
1088
 
1089
1089
  // If we have differential data but no entity, that's a configuration error
1090
1090
  if (result.status === 'differential' && result.differentialData && !entity) {
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ import { resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provi
9
9
  import { MJGlobal, MJEventType, UUIDsEqual, ShutdownRegistry } from '@memberjunction/global';
10
10
  import { setupSQLServerClient, SQLServerDataProvider, SQLServerProviderConfigData, UserCache } from '@memberjunction/sqlserver-dataprovider';
11
11
  import { extendConnectionPoolWithQuery } from './util.js';
12
+ import { registerIntegrationCustomColumnPromoter } from './integration/CustomColumnPromoter.js';
12
13
  import { default as BodyParser } from 'body-parser';
13
14
  import compression from 'compression'; // Add compression middleware
14
15
  import cors from 'cors';
@@ -17,6 +18,7 @@ import { default as fg } from 'fast-glob';
17
18
  import { useServer } from 'graphql-ws/lib/use/ws';
18
19
  import { createServer } from 'node:http';
19
20
  import { fileURLToPath } from 'node:url';
21
+ import { readFileSync } from 'node:fs';
20
22
  import { sep } from 'node:path';
21
23
  import 'reflect-metadata';
22
24
  import { ReplaySubject } from 'rxjs';
@@ -25,13 +27,15 @@ import { PubSub } from 'graphql-subscriptions';
25
27
  import sql from 'mssql';
26
28
  import { WebSocketServer } from 'ws';
27
29
  import buildApolloServer from './apolloServer/index.js';
28
- import { configInfo, dbDatabase, dbHost, dbPort, dbUsername, graphqlPort, graphqlRootPath, mj_core_schema, websiteRunFromPackage, RESTApiOptions } from './config.js';
30
+ import { configInfo, configFilePath, dbDatabase, dbHost, dbPort, dbUsername, graphqlPort, graphqlRootPath, mj_core_schema, websiteRunFromPackage, RESTApiOptions } from './config.js';
29
31
  import { default as jwt } from 'jsonwebtoken';
30
32
  import { contextFunction, createUnifiedAuthMiddleware, getUserPayload } from './context.js';
31
33
  import { UserPayload } from './types.js';
32
34
  import { requireSystemUserDirective, publicDirective } from './directives/index.js';
33
35
  import { variablesLoggingMiddleware } from './logging/variablesLoggingMiddleware.js';
34
36
  import { auditResolversForUndecoratedArgs } from './logging/bootAudit.js';
37
+ import { StartupLogger } from './logging/StartupLogger.js';
38
+ import { AuthProviderFactory } from '@memberjunction/auth-providers';
35
39
  import createMSSQLConfig from './orm.js';
36
40
  import { setupRESTEndpoints } from './rest/setupRESTEndpoints.js';
37
41
  import { createOAuthCallbackHandler } from './rest/OAuthCallbackHandler.js';
@@ -43,7 +47,7 @@ import { DataSourceInfo, raiseEvent } from './types.js';
43
47
 
44
48
  import { ExternalChangeDetectorEngine } from '@memberjunction/external-change-detection';
45
49
  import { ScheduledJobsService } from './services/ScheduledJobsService.js';
46
- import { LocalCacheManager, StartupManager, TelemetryManager, TelemetryLevel } from '@memberjunction/core';
50
+ import { LocalCacheManager, StartupManager, TelemetryManager, TelemetryLevel, LogStatus, SetVerboseLogging } from '@memberjunction/core';
47
51
  import { getSystemUser } from './auth/index.js';
48
52
  import { GetAPIKeyEngine } from '@memberjunction/api-keys';
49
53
  import { RedisLocalStorageProvider } from '@memberjunction/redis-provider';
@@ -52,6 +56,7 @@ import { PubSubManager } from './generic/PubSubManager.js';
52
56
  import { IntegrationProgressEmitter } from '@memberjunction/integration-progress-artifacts';
53
57
  import { PublishIntegrationProgress } from './resolvers/IntegrationProgressResolver.js';
54
58
  import { ClientToolRequestManager, AgentRunWatchdog } from '@memberjunction/ai-agents';
59
+ import { SessionJanitor } from './agentSessions/index.js';
55
60
  import { CACHE_INVALIDATION_TOPIC } from './generic/CacheInvalidationResolver.js';
56
61
  import { ConnectorFactory, IntegrationEngine, IntegrationSyncOptions } from '@memberjunction/integration-engine';
57
62
  import { CronExpressionHelper } from '@memberjunction/scheduling-engine';
@@ -167,6 +172,10 @@ export * from './resolvers/UserViewResolver.js';
167
172
  export * from './resolvers/VersionHistoryResolver.js';
168
173
  export * from './resolvers/CurrentUserContextResolver.js';
169
174
  export * from './resolvers/RSUResolver.js';
175
+ export * from './resolvers/AgentSessionResolver.js';
176
+ export * from './resolvers/RealtimeClientSessionResolver.js';
177
+ export * from './resolvers/RemoteBrowserActionResolver.js';
178
+ export * from './agentSessions/index.js';
170
179
  export { GetReadOnlyDataSource, GetReadWriteDataSource, GetReadWriteProvider, GetReadOnlyProvider } from './util.js';
171
180
 
172
181
  export * from './generated/generated.js';
@@ -194,13 +203,37 @@ const localPath = (p: string) => {
194
203
 
195
204
  export const createApp = (): Application => express();
196
205
 
206
+ /**
207
+ * Resolves the MJServer package version for the startup summary header.
208
+ *
209
+ * Reads `@memberjunction/server`'s own `package.json` relative to this module.
210
+ * Returns `undefined` (and the header omits the version) if it can't be read,
211
+ * so version display is best-effort and never blocks startup.
212
+ */
213
+ function resolveServerVersion(): string | undefined {
214
+ try {
215
+ // package.json sits at the package root, two levels up from dist/ (or src/).
216
+ const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url));
217
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version?: string };
218
+ return pkg.version;
219
+ } catch {
220
+ return undefined;
221
+ }
222
+ }
223
+
197
224
  export const serve = async (resolverPaths: Array<string>, app: Application = createApp(), options?: MJServerOptions): Promise<void> => {
198
225
  const t0 = performance.now();
199
- const lap = (label: string, since: number) => {
200
- const ms = performance.now() - since;
201
- console.log(`⏱️ [Startup] ${label}: ${ms.toFixed(0)}ms`);
202
- return performance.now();
203
- };
226
+ // Level-gated startup logger. Resolves verbosity from telemetry.level (single
227
+ // operator knob). At `standard` (default), per-phase timings are collapsed into
228
+ // the one-line summary; at `verbose`+ each phase prints inline as before.
229
+ const startupLog = new StartupLogger();
230
+ // Wire the GLOBAL verbose-logging gate to the resolved server level, so cross-package
231
+ // LogStatusEx({ verboseOnly: true }) lines (cache, scheduling, integration, MCP, …) honor the
232
+ // configured level instead of defaulting to hidden. Without this, "verbose-only" is "always hidden".
233
+ SetVerboseLogging(startupLog.IsAtLeast('verbose'));
234
+ // Transient boot spinner (standard level + TTY only); cleared by PrintSummary.
235
+ startupLog.StartBoot();
236
+ const lap = (label: string, since: number) => startupLog.EndPhase(label, since);
204
237
 
205
238
  const localResolverPaths = ['resolvers/**/*Resolver.{js,ts}', 'generic/*Resolver.{js,ts}', 'generated/generated.{js,ts}'].map(localPath);
206
239
 
@@ -220,7 +253,8 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
220
253
 
221
254
  if (dbType === 'postgresql') {
222
255
  // ─── PostgreSQL Path ───────────────────────────────────────────
223
- console.log('Database type: PostgreSQL');
256
+ startupLog.BeginPhase('Connecting to database');
257
+ startupLog.LogIf('verbose', 'Database type: PostgreSQL');
224
258
  const pg = await import('pg');
225
259
  const { PostgreSQLDataProvider, PostgreSQLProviderConfigData } = await import('@memberjunction/postgresql-dataprovider');
226
260
 
@@ -246,7 +280,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
246
280
  const testClient = await pgPool.connect();
247
281
  await testClient.query('SELECT 1');
248
282
  testClient.release();
249
- console.log(`PostgreSQL pool connected to ${pgHost}:${pgPort}/${pgDatabase}`);
283
+ startupLog.LogIf('verbose', `PostgreSQL pool connected to ${pgHost}:${pgPort}/${pgDatabase}`);
250
284
 
251
285
  // Create a DataSourceInfo with a MSSQL-compatible wrapper around pg.Pool
252
286
  // This allows existing code (types, util, context) to work without changes
@@ -338,12 +372,11 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
338
372
  const codegenPgProvider = new PostgreSQLDataProvider();
339
373
  await codegenPgProvider.Config(codegenPgConfigData); // separate pool (per-instance manager); does NOT touch the global API provider
340
374
  RuntimeSchemaManager.Instance.SetDDLProvider(codegenPgProvider);
341
- console.log('RSU DDL provider initialized with CodeGen credentials (PostgreSQL).');
375
+ startupLog.LogIf('verbose', 'RSU DDL provider initialized with CodeGen credentials (PostgreSQL).');
342
376
 
343
377
  // Set up in-process CodeGen runner for RSU
344
378
  try {
345
- const { RunCodeGenBase } = await import('@memberjunction/codegen-lib');
346
- const { PostgreSQLCodeGenConnection } = await import('@memberjunction/codegen-lib/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.js');
379
+ const { RunCodeGenBase, PostgreSQLCodeGenConnection } = await import('@memberjunction/codegen-lib');
347
380
 
348
381
  const codegenConnection = new PostgreSQLCodeGenConnection(codegenPgPool);
349
382
  const codegenCurrentUser = UserCache.Instance.Users.find(u => u.Type?.trim().toLowerCase() === 'owner') ?? UserCache.Instance.Users[0];
@@ -361,21 +394,21 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
361
394
  RuntimeSchemaManager.Instance.SetCodeGenRunner({
362
395
  RunInProcess: (skipDB) => runObject.RunInProcess(codegenDataSource, skipDB, rsuWorkDir),
363
396
  });
364
- console.log('RSU in-process CodeGen runner initialized (PostgreSQL).');
397
+ startupLog.LogIf('verbose', 'RSU in-process CodeGen runner initialized (PostgreSQL).');
365
398
 
366
399
  // Inject CodeGen output paths for targeted git staging
367
400
  const { initializeConfig } = await import('@memberjunction/codegen-lib');
368
401
  const cgConfig = initializeConfig(rsuWorkDir);
369
402
  const outputPaths = (cgConfig.output ?? []).map((o: { directory: string }) => o.directory);
370
403
  RuntimeSchemaManager.Instance.SetCodeGenOutputPaths(outputPaths);
371
- console.log(`RSU CodeGen output paths: ${outputPaths.length} directories configured.`);
404
+ startupLog.LogIf('verbose', `RSU CodeGen output paths: ${outputPaths.length} directories configured.`);
372
405
 
373
406
  // Point RSU's soft PK/FK writer at the SAME file CodeGen reads (mj.config.cjs
374
407
  // `additionalSchemaInfo`), or RSU writes soft PKs to its own default path while
375
408
  // CodeGen reads a different one and skips integration tables with "No primary key found".
376
409
  if (cgConfig.additionalSchemaInfo) {
377
410
  RuntimeSchemaManager.Instance.SetAdditionalSchemaInfoPath(cgConfig.additionalSchemaInfo);
378
- console.log(`RSU additionalSchemaInfo path: ${cgConfig.additionalSchemaInfo}`);
411
+ startupLog.LogIf('verbose', `RSU additionalSchemaInfo path: ${cgConfig.additionalSchemaInfo}`);
379
412
  }
380
413
  } catch (codegenErr) {
381
414
  console.warn(`RSU in-process CodeGen runner setup failed (will fall back to child process): ${(codegenErr as Error).message}`);
@@ -386,11 +419,13 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
386
419
  }
387
420
 
388
421
  const md = new Metadata(); // global-provider-ok: bootstrap
389
- console.log(`Data Source has been initialized. ${md?.Entities ? md.Entities.length : 0} entities loaded.`);
422
+ const pgEntityCount = md?.Entities ? md.Entities.length : 0;
423
+ startupLog.LogIf('verbose', `Data Source has been initialized. ${pgEntityCount} entities loaded.`);
424
+ startupLog.SetDatabaseInfo('PostgreSQL', `${pgHost}:${pgPort}/${pgDatabase}`, pgEntityCount);
390
425
  } else {
391
426
  // ─── SQL Server Path (existing behavior) ───────────────────────
392
- console.log('Database type: SQL Server');
393
- let tPhase = performance.now();
427
+ startupLog.LogIf('verbose', 'Database type: SQL Server');
428
+ let tPhase = startupLog.BeginPhase('Connecting to database');
394
429
  const pool = new sql.ConnectionPool(createMSSQLConfig());
395
430
 
396
431
  // Handle connection-level errors from dead/stale connections in the pool.
@@ -401,7 +436,8 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
401
436
  });
402
437
 
403
438
  await pool.connect();
404
- tPhase = lap('DB Pool Connect', tPhase);
439
+ lap('DB Pool Connect', tPhase);
440
+ tPhase = startupLog.BeginPhase('Loading metadata + providers');
405
441
 
406
442
  dataSources.push(new DataSourceInfo({dataSource: pool, type: 'Read-Write', host: dbHost, port: dbPort, database: dbDatabase, userName: dbUsername}));
407
443
 
@@ -421,14 +457,17 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
421
457
  await readOnlyPool.connect();
422
458
 
423
459
  dataSources.push(new DataSourceInfo({dataSource: readOnlyPool, type: 'Read-Only', host: dbHost, port: dbPort, database: dbDatabase, userName: configInfo.dbReadOnlyUsername}));
424
- console.log('Read-only Connection Pool has been initialized.');
460
+ startupLog.LogIf('verbose', 'Read-only Connection Pool has been initialized.');
425
461
  }
426
462
 
427
463
  const config = new SQLServerProviderConfigData(pool, mj_core_schema, cacheRefreshInterval);
428
464
  await setupSQLServerClient(config);
429
- tPhase = lap('Metadata + Provider Setup', tPhase);
465
+ lap('Metadata + Provider Setup', tPhase);
466
+ startupLog.BeginPhase('Initializing data provider');
430
467
  const md = new Metadata(); // global-provider-ok: bootstrap
431
- console.log(`Data Source has been initialized. ${md?.Entities ? md.Entities.length : 0} entities loaded.`);
468
+ const entityCount = md?.Entities ? md.Entities.length : 0;
469
+ startupLog.LogIf('verbose', `Data Source has been initialized. ${entityCount} entities loaded.`);
470
+ startupLog.SetDatabaseInfo('SQL Server', `${dbHost}:${dbPort}/${dbDatabase}`, entityCount);
432
471
 
433
472
  // Set up CodeGen-credentialed provider for RSU DDL operations (CREATE TABLE, CREATE SCHEMA, etc.)
434
473
  const codegenUser = process.env.CODEGEN_DB_USERNAME;
@@ -457,12 +496,11 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
457
496
  const codegenProvider = new SQLServerDataProvider();
458
497
  await codegenProvider.Config(codegenConfig);
459
498
  RuntimeSchemaManager.Instance.SetDDLProvider(codegenProvider);
460
- console.log('RSU DDL provider initialized with CodeGen credentials.');
499
+ startupLog.LogIf('verbose', 'RSU DDL provider initialized with CodeGen credentials.');
461
500
 
462
501
  // Set up in-process CodeGen runner for RSU
463
502
  try {
464
- const { RunCodeGenBase } = await import('@memberjunction/codegen-lib');
465
- const { SQLServerCodeGenConnection } = await import('@memberjunction/codegen-lib/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.js');
503
+ const { RunCodeGenBase, SQLServerCodeGenConnection } = await import('@memberjunction/codegen-lib');
466
504
 
467
505
  const codegenConnection = new SQLServerCodeGenConnection(codegenPool);
468
506
  const codegenCurrentUser = UserCache.Instance.Users.find(u => u.Type?.trim().toLowerCase() === 'owner') ?? UserCache.Instance.Users[0];
@@ -480,14 +518,14 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
480
518
  RuntimeSchemaManager.Instance.SetCodeGenRunner({
481
519
  RunInProcess: (skipDB) => runObject.RunInProcess(codegenDataSource, skipDB, rsuWorkDir),
482
520
  });
483
- console.log('RSU in-process CodeGen runner initialized.');
521
+ startupLog.LogIf('verbose', 'RSU in-process CodeGen runner initialized.');
484
522
 
485
523
  // Inject CodeGen output paths for targeted git staging
486
524
  const { initializeConfig } = await import('@memberjunction/codegen-lib');
487
525
  const codegenConfig = initializeConfig(rsuWorkDir);
488
526
  const outputPaths = (codegenConfig.output ?? []).map((o: { directory: string }) => o.directory);
489
527
  RuntimeSchemaManager.Instance.SetCodeGenOutputPaths(outputPaths);
490
- console.log(`RSU CodeGen output paths: ${outputPaths.length} directories configured.`);
528
+ startupLog.LogIf('verbose', `RSU CodeGen output paths: ${outputPaths.length} directories configured.`);
491
529
 
492
530
  // Point RSU's soft PK/FK writer at the SAME file CodeGen reads (mj.config.cjs
493
531
  // `additionalSchemaInfo`). Without this, RSU writes soft PKs to its own default
@@ -495,7 +533,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
495
533
  // with "No primary key found".
496
534
  if (codegenConfig.additionalSchemaInfo) {
497
535
  RuntimeSchemaManager.Instance.SetAdditionalSchemaInfoPath(codegenConfig.additionalSchemaInfo);
498
- console.log(`RSU additionalSchemaInfo path: ${codegenConfig.additionalSchemaInfo}`);
536
+ startupLog.LogIf('verbose', `RSU additionalSchemaInfo path: ${codegenConfig.additionalSchemaInfo}`);
499
537
  }
500
538
  } catch (codegenErr) {
501
539
  console.warn(`RSU in-process CodeGen runner setup failed (will fall back to child process): ${(codegenErr as Error).message}`);
@@ -506,7 +544,15 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
506
544
  }
507
545
  }
508
546
 
509
- let tServe = performance.now();
547
+ // Register the post-sync custom-column promotion hook (gaps.md §2). Safe to call regardless of
548
+ // RSU config: the hook self-gates on captured overflow data and uses RSU only at fire time.
549
+ try {
550
+ registerIntegrationCustomColumnPromoter();
551
+ } catch (err) {
552
+ console.warn(`Custom-column promoter registration failed (post-sync promotion disabled): ${(err as Error).message}`);
553
+ }
554
+
555
+ let tServe = startupLog.BeginPhase('Initializing services');
510
556
 
511
557
  // Store queryDialects config in GlobalObjectStore so MJQueryEntityServer can
512
558
  // read it without a circular dependency on MJServer
@@ -521,10 +567,10 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
521
567
  if (configInfo.telemetry?.level) {
522
568
  tm.UpdateSettings({ level: configInfo.telemetry.level as TelemetryLevel });
523
569
  }
524
- console.log(`Server telemetry enabled with level: ${configInfo.telemetry.level || 'standard'}`);
570
+ startupLog.LogIf('verbose', `Server telemetry enabled with level: ${configInfo.telemetry.level || 'standard'}`);
525
571
  } else {
526
572
  tm.SetEnabled(false);
527
- console.log('Server telemetry disabled');
573
+ startupLog.LogIf('verbose', 'Server telemetry disabled');
528
574
  }
529
575
 
530
576
  // Optionally inject Redis as the shared storage provider for cross-server cache invalidation
@@ -559,7 +605,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
559
605
  }
560
606
  });
561
607
 
562
- console.log(`Redis cache provider connected: ${process.env.REDIS_URL}`);
608
+ startupLog.LogIf('verbose', `Redis cache provider connected: ${process.env.REDIS_URL}`);
563
609
  }
564
610
 
565
611
  // If Redis is available, swap LocalCacheManager's storage provider to Redis.
@@ -567,7 +613,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
567
613
  // during engine loading. SetStorageProvider migrates cached data to Redis.
568
614
  if (process.env.REDIS_URL) {
569
615
  await LocalCacheManager.Instance.SetStorageProvider(Metadata.Provider.LocalStorageProvider); // global-provider-ok: bootstrap
570
- console.log('LocalCacheManager: storage provider swapped to Redis');
616
+ startupLog.LogIf('verbose', 'LocalCacheManager: storage provider swapped to Redis');
571
617
  }
572
618
  // Ensure LocalCacheManager is initialized (no-op if already done during engine loading)
573
619
  if (!LocalCacheManager.Instance.IsInitialized) {
@@ -581,11 +627,14 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
581
627
  verboseLogging: cs.verboseLogging ?? false,
582
628
  };
583
629
  await LocalCacheManager.Instance.Initialize(Metadata.Provider.LocalStorageProvider, cacheConfig); // global-provider-ok: bootstrap
584
- console.log('LocalCacheManager initialized with cache config:', JSON.stringify({
585
- maxMemoryMB: cs.maxMemoryMB ?? 150,
586
- maxPercentOfCachePerEntity: cs.maxPercentOfCachePerEntity ?? 50,
587
- evictionSweepIntervalSeconds: cs.evictionSweepIntervalSeconds ?? 300,
588
- }));
630
+ if (startupLog.IsAtLeast('verbose')) {
631
+ // eslint-disable-next-line no-console
632
+ console.log('LocalCacheManager initialized with cache config:', JSON.stringify({
633
+ maxMemoryMB: cs.maxMemoryMB ?? 150,
634
+ maxPercentOfCachePerEntity: cs.maxPercentOfCachePerEntity ?? 50,
635
+ evictionSweepIntervalSeconds: cs.evictionSweepIntervalSeconds ?? 300,
636
+ }));
637
+ }
589
638
  }
590
639
 
591
640
  // Initialize APIKeyEngine singleton — reads apiKeyGeneration from mj.config.cjs automatically
@@ -610,7 +659,8 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
610
659
  /******TEST HARNESS FOR CHANGE DETECTION */
611
660
  /******TEST HARNESS FOR CHANGE DETECTION */
612
661
 
613
- tServe = lap('Telemetry + Cache + APIKey Init', tServe);
662
+ lap('Telemetry + Cache + APIKey Init', tServe);
663
+ tServe = startupLog.BeginPhase('Discovering resolvers');
614
664
 
615
665
  const dynamicModules = await Promise.all(
616
666
  paths.map((modulePath) => {
@@ -657,7 +707,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
657
707
  // Initialize all middleware
658
708
  for (const mw of middlewares) {
659
709
  await mw.Initialize();
660
- console.log(` [Middleware] ${mw.Label}`);
710
+ startupLog.LogIf('verbose', ` [Middleware] ${mw.Label}`);
661
711
  }
662
712
 
663
713
  // Collect middleware contributions for each pipeline stage
@@ -708,7 +758,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
708
758
  Object.values(module).filter((value) => typeof value === 'function')
709
759
  );
710
760
  allResolvers = [...resolvers, ...mwResolvers] as BuildSchemaOptions['resolvers'];
711
- console.log(` [Middleware Resolvers] Loaded ${mwResolverFiles.length} resolver file(s) from middleware`);
761
+ startupLog.LogIf('verbose', ` [Middleware Resolvers] Loaded ${mwResolverFiles.length} resolver file(s) from middleware`);
712
762
  }
713
763
  }
714
764
 
@@ -767,7 +817,8 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
767
817
  }
768
818
  });
769
819
 
770
- tServe = lap('Resolver + Middleware Discovery', tServe);
820
+ lap('Resolver + Middleware Discovery', tServe);
821
+ tServe = startupLog.BeginPhase('Building GraphQL schema');
771
822
 
772
823
  let schema = mergeSchemas({
773
824
  schemas: [
@@ -794,7 +845,8 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
794
845
  schema = transformer(schema);
795
846
  }
796
847
 
797
- tServe = lap('Schema Build', tServe);
848
+ lap('Schema Build', tServe);
849
+ tServe = startupLog.BeginPhase('Starting HTTP server');
798
850
 
799
851
  const httpServer = createServer(app);
800
852
 
@@ -911,7 +963,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
911
963
 
912
964
  // ─── OAuth callback routes (unauthenticated, registered BEFORE auth) ─────
913
965
  const oauthPublicUrl = configInfo.publicUrl || `${configInfo.baseUrl}:${configInfo.graphqlPort}${configInfo.graphqlRootPath || ''}`;
914
- console.log(`[OAuth] publicUrl: ${oauthPublicUrl}`);
966
+ startupLog.LogIf('verbose', `[OAuth] publicUrl: ${oauthPublicUrl}`);
915
967
 
916
968
  let oauthAuthenticatedRouter: ReturnType<typeof createOAuthCallbackHandler>['authenticatedRouter'] | undefined;
917
969
  if (oauthPublicUrl) {
@@ -926,14 +978,14 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
926
978
 
927
979
  // OAuth callback is unauthenticated (called by external auth server)
928
980
  app.use('/oauth', oauthCors, callbackRouter);
929
- console.log('[OAuth] Callback route registered at /oauth/callback');
981
+ startupLog.LogIf('verbose', '[OAuth] Callback route registered at /oauth/callback');
930
982
  }
931
983
 
932
984
  // ─── eSignature webhook (unauthenticated, registered BEFORE auth) ─────
933
985
  // Called by external signature providers (DocuSign Connect, etc.) without an MJ bearer token.
934
986
  // The provider DRIVER verifies the payload signature/HMAC; MJ auth does not apply here.
935
987
  app.use('/esignature', cors<cors.CorsRequest>(), createSignatureWebhookHandler());
936
- console.log('[eSignature] Webhook route registered at /esignature/webhook/:driverKey');
988
+ startupLog.LogIf('verbose', '[eSignature] Webhook route registered at /esignature/webhook/:driverKey');
937
989
 
938
990
  // ─── Magic-link routes (MJ-issued, app-scoped external access) ───────────
939
991
  // Public router (JWKS + redeem) mounts BEFORE the auth middleware; the
@@ -944,7 +996,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
944
996
  magicLinkAuthenticatedRouter = authenticatedRouter;
945
997
  registerMagicLinkAuthProvider(oauthPublicUrl, configInfo.magicLink);
946
998
  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`);
999
+ startupLog.LogIf('verbose', `[MagicLink] Public routes registered at ${MAGIC_LINK_MOUNT_PATH}/redeem and ${MAGIC_LINK_MOUNT_PATH}/jwks.json`);
948
1000
  }
949
1001
 
950
1002
  // ─── Global CORS (before auth so 401 responses include CORS headers) ─────
@@ -998,13 +1050,13 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
998
1050
  if (oauthAuthenticatedRouter) {
999
1051
  const oauthCors = cors<cors.CorsRequest>();
1000
1052
  app.use('/oauth', oauthCors, BodyParser.json(), oauthAuthenticatedRouter);
1001
- console.log('[OAuth] Authenticated routes registered at /oauth/status, /oauth/initiate, and /oauth/exchange');
1053
+ startupLog.LogIf('verbose', '[OAuth] Authenticated routes registered at /oauth/status, /oauth/initiate, and /oauth/exchange');
1002
1054
  }
1003
1055
 
1004
1056
  // ─── Magic-link authenticated route (invite creation) ─────────────────────
1005
1057
  if (magicLinkAuthenticatedRouter) {
1006
1058
  app.use(MAGIC_LINK_MOUNT_PATH, cors<cors.CorsRequest>(), magicLinkAuthenticatedRouter);
1007
- console.log(`[MagicLink] Authenticated route registered at ${MAGIC_LINK_MOUNT_PATH}/create`);
1059
+ startupLog.LogIf('verbose', `[MagicLink] Authenticated route registered at ${MAGIC_LINK_MOUNT_PATH}/create`);
1008
1060
  }
1009
1061
 
1010
1062
  // ─── REST API endpoints (auth already handled by unified middleware) ─────
@@ -1021,9 +1073,11 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
1021
1073
  if (process.env.MJ_REST_API_ENABLED !== undefined) {
1022
1074
  restApiConfig.enabled = process.env.MJ_REST_API_ENABLED === 'true';
1023
1075
  if (restApiConfig.enabled) {
1024
- console.log('REST API is enabled via environment variable');
1076
+ // Genuine config override actionable, surfaced at all levels.
1077
+ LogStatus('REST API is enabled via environment variable');
1025
1078
  }
1026
1079
  }
1080
+ startupLog.SetRestEnabled(restApiConfig.enabled);
1027
1081
 
1028
1082
  if (process.env.MJ_REST_API_INCLUDE_ENTITIES) {
1029
1083
  restApiConfig.includeEntities = process.env.MJ_REST_API_INCLUDE_ENTITIES.split(',').map(e => e.trim());
@@ -1079,12 +1133,24 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
1079
1133
  await Promise.resolve(options.onBeforeServe());
1080
1134
  }
1081
1135
 
1082
- tServe = lap('Apollo + Express Setup', tServe);
1136
+ lap('Apollo + Express Setup', tServe);
1083
1137
 
1084
1138
  await new Promise<void>((resolve) => httpServer.listen({ port: graphqlPort }, resolve));
1085
- lap('Total Startup', t0);
1086
- console.log(`📦 Connected to database: ${dbHost}:${dbPort}/${dbDatabase}`);
1087
- console.log(`🚀 Server ready at http://localhost:${graphqlPort}/`);
1139
+ // The summary headline reports TRUE wall-clock since process start (see
1140
+ // StartupLogger.buildStartupLine), so we don't push a synthetic "total" phase row
1141
+ // (that double-counted the headline). Keep a verbose-only line for the serve()-scoped span.
1142
+ startupLog.LogIf('verbose', `serve() startup: ${((performance.now() - t0) / 1000).toFixed(1)}s`);
1143
+ startupLog.SetReadyUrl(`http://localhost:${graphqlPort}/`);
1144
+ // Populate summary fields that are known only now, then print the one summary block.
1145
+ for (const provider of AuthProviderFactory.Instance.getAllProviders()) {
1146
+ startupLog.AddAuthProvider(provider.name);
1147
+ }
1148
+ startupLog.SetVersion(resolveServerVersion());
1149
+ startupLog.SetConfigPath(configFilePath);
1150
+ if (scheduledJobsService) {
1151
+ startupLog.SetScheduledJobCount(scheduledJobsService.GetStatus().activeJobs);
1152
+ }
1153
+ startupLog.PrintSummary();
1088
1154
 
1089
1155
  // Process pending RSU work from pre-restart (entity maps, field maps, sync)
1090
1156
  processRSUPendingWork().catch(err => console.warn(`RSU pending work processing failed: ${err}`));
@@ -1105,6 +1171,14 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
1105
1171
  .catch(err => console.warn(`[AgentRunWatchdog] Startup sweep failed: ${err}`));
1106
1172
  }
1107
1173
 
1174
+ // Launch the AI Agent Session janitor: run own-host orphan recovery once at boot, then keep a
1175
+ // periodic staleness sweep running. Self-registers with ShutdownRegistry, so its timer is cleared
1176
+ // by the gracefulShutdown drain below (no explicit Stop() wiring needed here).
1177
+ if (resumeUser && Metadata.Provider instanceof DatabaseProviderBase) { // global-provider-ok: server startup recovery — boot-time session janitor uses the server's own provider
1178
+ SessionJanitor.Instance.Start(Metadata.Provider, resumeUser) // global-provider-ok: server-owned background reconciler runs under the server's provider + system user
1179
+ .catch(err => console.warn(`[SessionJanitor] Startup failed: ${err}`));
1180
+ }
1181
+
1108
1182
  // Set up graceful shutdown handlers
1109
1183
  const gracefulShutdown = async (signal: string) => {
1110
1184
  console.log(`\n${signal} received, shutting down gracefully...`);