@memberjunction/server 5.40.1 → 5.41.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.
- package/dist/agentSessions/HostInstance.d.ts +19 -0
- package/dist/agentSessions/HostInstance.d.ts.map +1 -0
- package/dist/agentSessions/HostInstance.js +48 -0
- package/dist/agentSessions/HostInstance.js.map +1 -0
- package/dist/agentSessions/SessionJanitor.d.ts +97 -0
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -0
- package/dist/agentSessions/SessionJanitor.js +222 -0
- package/dist/agentSessions/SessionJanitor.js.map +1 -0
- package/dist/agentSessions/SessionManager.d.ts +142 -0
- package/dist/agentSessions/SessionManager.d.ts.map +1 -0
- package/dist/agentSessions/SessionManager.js +308 -0
- package/dist/agentSessions/SessionManager.js.map +1 -0
- package/dist/agentSessions/index.d.ts +4 -0
- package/dist/agentSessions/index.d.ts.map +1 -0
- package/dist/agentSessions/index.js +26 -0
- package/dist/agentSessions/index.js.map +1 -0
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +6 -1
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/config.d.ts +245 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +34 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +41 -7
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +659 -3
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +5459 -1759
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.js +1 -1
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.js +9 -10
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +112 -42
- package/dist/index.js.map +1 -1
- package/dist/logging/StartupLogger.d.ts +121 -0
- package/dist/logging/StartupLogger.d.ts.map +1 -0
- package/dist/logging/StartupLogger.js +245 -0
- package/dist/logging/StartupLogger.js.map +1 -0
- package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -1
- package/dist/logging/variablesLoggingMiddleware.js +21 -2
- package/dist/logging/variablesLoggingMiddleware.js.map +1 -1
- package/dist/middleware/RateLimitMiddleware.d.ts +16 -0
- package/dist/middleware/RateLimitMiddleware.d.ts.map +1 -0
- package/dist/middleware/RateLimitMiddleware.js +77 -0
- package/dist/middleware/RateLimitMiddleware.js.map +1 -0
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +1 -0
- package/dist/middleware/index.js.map +1 -1
- package/dist/resolvers/AgentSessionResolver.d.ts +42 -0
- package/dist/resolvers/AgentSessionResolver.d.ts.map +1 -0
- package/dist/resolvers/AgentSessionResolver.js +152 -0
- package/dist/resolvers/AgentSessionResolver.js.map +1 -0
- package/dist/resolvers/EntityPermissionResolver.d.ts +16 -0
- package/dist/resolvers/EntityPermissionResolver.d.ts.map +1 -0
- package/dist/resolvers/EntityPermissionResolver.js +95 -0
- package/dist/resolvers/EntityPermissionResolver.js.map +1 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +688 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeClientSessionResolver.js +1774 -0
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +359 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -0
- package/dist/resolvers/RemoteBrowserActionResolver.js +896 -0
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -0
- package/dist/rest/SignatureWebhookHandler.js +3 -2
- package/dist/rest/SignatureWebhookHandler.js.map +1 -1
- package/dist/services/ScheduledJobsService.d.ts.map +1 -1
- package/dist/services/ScheduledJobsService.js +6 -5
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/package.json +78 -74
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +2605 -0
- package/src/__tests__/RemoteBrowserAudioStream.test.ts +174 -0
- package/src/__tests__/SessionJanitor.test.ts +234 -0
- package/src/__tests__/SessionManager.test.ts +465 -0
- package/src/__tests__/subscriptionRedaction.test.ts +5 -0
- package/src/agentSessions/HostInstance.ts +53 -0
- package/src/agentSessions/SessionJanitor.ts +267 -0
- package/src/agentSessions/SessionManager.ts +446 -0
- package/src/agentSessions/index.ts +27 -0
- package/src/auth/initializeProviders.ts +6 -1
- package/src/config.ts +38 -0
- package/src/context.ts +42 -7
- package/src/generated/generated.ts +3111 -567
- package/src/generic/ResolverBase.ts +1 -1
- package/src/generic/RunViewResolver.ts +9 -9
- package/src/index.ts +112 -42
- package/src/logging/StartupLogger.ts +317 -0
- package/src/logging/variablesLoggingMiddleware.ts +25 -5
- package/src/middleware/RateLimitMiddleware.ts +87 -0
- package/src/middleware/index.ts +1 -0
- package/src/resolvers/AgentSessionResolver.ts +138 -0
- package/src/resolvers/EntityPermissionResolver.ts +73 -0
- package/src/resolvers/RealtimeClientSessionResolver.ts +2162 -0
- package/src/resolvers/RemoteBrowserActionResolver.ts +907 -0
- package/src/rest/SignatureWebhookHandler.ts +3 -2
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
@@ -17,6 +17,7 @@ import { default as fg } from 'fast-glob';
|
|
|
17
17
|
import { useServer } from 'graphql-ws/lib/use/ws';
|
|
18
18
|
import { createServer } from 'node:http';
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
20
21
|
import { sep } from 'node:path';
|
|
21
22
|
import 'reflect-metadata';
|
|
22
23
|
import { ReplaySubject } from 'rxjs';
|
|
@@ -32,6 +33,8 @@ import { UserPayload } from './types.js';
|
|
|
32
33
|
import { requireSystemUserDirective, publicDirective } from './directives/index.js';
|
|
33
34
|
import { variablesLoggingMiddleware } from './logging/variablesLoggingMiddleware.js';
|
|
34
35
|
import { auditResolversForUndecoratedArgs } from './logging/bootAudit.js';
|
|
36
|
+
import { StartupLogger } from './logging/StartupLogger.js';
|
|
37
|
+
import { AuthProviderFactory } from '@memberjunction/auth-providers';
|
|
35
38
|
import createMSSQLConfig from './orm.js';
|
|
36
39
|
import { setupRESTEndpoints } from './rest/setupRESTEndpoints.js';
|
|
37
40
|
import { createOAuthCallbackHandler } from './rest/OAuthCallbackHandler.js';
|
|
@@ -43,7 +46,7 @@ import { DataSourceInfo, raiseEvent } from './types.js';
|
|
|
43
46
|
|
|
44
47
|
import { ExternalChangeDetectorEngine } from '@memberjunction/external-change-detection';
|
|
45
48
|
import { ScheduledJobsService } from './services/ScheduledJobsService.js';
|
|
46
|
-
import { LocalCacheManager, StartupManager, TelemetryManager, TelemetryLevel } from '@memberjunction/core';
|
|
49
|
+
import { LocalCacheManager, StartupManager, TelemetryManager, TelemetryLevel, LogStatus, SetVerboseLogging } from '@memberjunction/core';
|
|
47
50
|
import { getSystemUser } from './auth/index.js';
|
|
48
51
|
import { GetAPIKeyEngine } from '@memberjunction/api-keys';
|
|
49
52
|
import { RedisLocalStorageProvider } from '@memberjunction/redis-provider';
|
|
@@ -52,6 +55,7 @@ import { PubSubManager } from './generic/PubSubManager.js';
|
|
|
52
55
|
import { IntegrationProgressEmitter } from '@memberjunction/integration-progress-artifacts';
|
|
53
56
|
import { PublishIntegrationProgress } from './resolvers/IntegrationProgressResolver.js';
|
|
54
57
|
import { ClientToolRequestManager, AgentRunWatchdog } from '@memberjunction/ai-agents';
|
|
58
|
+
import { SessionJanitor } from './agentSessions/index.js';
|
|
55
59
|
import { CACHE_INVALIDATION_TOPIC } from './generic/CacheInvalidationResolver.js';
|
|
56
60
|
import { ConnectorFactory, IntegrationEngine, IntegrationSyncOptions } from '@memberjunction/integration-engine';
|
|
57
61
|
import { CronExpressionHelper } from '@memberjunction/scheduling-engine';
|
|
@@ -167,6 +171,10 @@ export * from './resolvers/UserViewResolver.js';
|
|
|
167
171
|
export * from './resolvers/VersionHistoryResolver.js';
|
|
168
172
|
export * from './resolvers/CurrentUserContextResolver.js';
|
|
169
173
|
export * from './resolvers/RSUResolver.js';
|
|
174
|
+
export * from './resolvers/AgentSessionResolver.js';
|
|
175
|
+
export * from './resolvers/RealtimeClientSessionResolver.js';
|
|
176
|
+
export * from './resolvers/RemoteBrowserActionResolver.js';
|
|
177
|
+
export * from './agentSessions/index.js';
|
|
170
178
|
export { GetReadOnlyDataSource, GetReadWriteDataSource, GetReadWriteProvider, GetReadOnlyProvider } from './util.js';
|
|
171
179
|
|
|
172
180
|
export * from './generated/generated.js';
|
|
@@ -194,13 +202,35 @@ const localPath = (p: string) => {
|
|
|
194
202
|
|
|
195
203
|
export const createApp = (): Application => express();
|
|
196
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Resolves the MJServer package version for the startup summary header.
|
|
207
|
+
*
|
|
208
|
+
* Reads `@memberjunction/server`'s own `package.json` relative to this module.
|
|
209
|
+
* Returns `undefined` (and the header omits the version) if it can't be read,
|
|
210
|
+
* so version display is best-effort and never blocks startup.
|
|
211
|
+
*/
|
|
212
|
+
function resolveServerVersion(): string | undefined {
|
|
213
|
+
try {
|
|
214
|
+
// package.json sits at the package root, two levels up from dist/ (or src/).
|
|
215
|
+
const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url));
|
|
216
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version?: string };
|
|
217
|
+
return pkg.version;
|
|
218
|
+
} catch {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
197
223
|
export const serve = async (resolverPaths: Array<string>, app: Application = createApp(), options?: MJServerOptions): Promise<void> => {
|
|
198
224
|
const t0 = performance.now();
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
225
|
+
// Level-gated startup logger. Resolves verbosity from telemetry.level (single
|
|
226
|
+
// operator knob). At `standard` (default), per-phase timings are collapsed into
|
|
227
|
+
// the one-line summary; at `verbose`+ each phase prints inline as before.
|
|
228
|
+
const startupLog = new StartupLogger();
|
|
229
|
+
// Wire the GLOBAL verbose-logging gate to the resolved server level, so cross-package
|
|
230
|
+
// LogStatusEx({ verboseOnly: true }) lines (cache, scheduling, integration, MCP, …) honor the
|
|
231
|
+
// configured level instead of defaulting to hidden. Without this, "verbose-only" is "always hidden".
|
|
232
|
+
SetVerboseLogging(startupLog.IsAtLeast('verbose'));
|
|
233
|
+
const lap = (label: string, since: number) => startupLog.EndPhase(label, since);
|
|
204
234
|
|
|
205
235
|
const localResolverPaths = ['resolvers/**/*Resolver.{js,ts}', 'generic/*Resolver.{js,ts}', 'generated/generated.{js,ts}'].map(localPath);
|
|
206
236
|
|
|
@@ -220,7 +250,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
220
250
|
|
|
221
251
|
if (dbType === 'postgresql') {
|
|
222
252
|
// ─── PostgreSQL Path ───────────────────────────────────────────
|
|
223
|
-
|
|
253
|
+
startupLog.LogIf('verbose', 'Database type: PostgreSQL');
|
|
224
254
|
const pg = await import('pg');
|
|
225
255
|
const { PostgreSQLDataProvider, PostgreSQLProviderConfigData } = await import('@memberjunction/postgresql-dataprovider');
|
|
226
256
|
|
|
@@ -246,7 +276,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
246
276
|
const testClient = await pgPool.connect();
|
|
247
277
|
await testClient.query('SELECT 1');
|
|
248
278
|
testClient.release();
|
|
249
|
-
|
|
279
|
+
startupLog.LogIf('verbose', `PostgreSQL pool connected to ${pgHost}:${pgPort}/${pgDatabase}`);
|
|
250
280
|
|
|
251
281
|
// Create a DataSourceInfo with a MSSQL-compatible wrapper around pg.Pool
|
|
252
282
|
// This allows existing code (types, util, context) to work without changes
|
|
@@ -338,7 +368,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
338
368
|
const codegenPgProvider = new PostgreSQLDataProvider();
|
|
339
369
|
await codegenPgProvider.Config(codegenPgConfigData); // separate pool (per-instance manager); does NOT touch the global API provider
|
|
340
370
|
RuntimeSchemaManager.Instance.SetDDLProvider(codegenPgProvider);
|
|
341
|
-
|
|
371
|
+
startupLog.LogIf('verbose', 'RSU DDL provider initialized with CodeGen credentials (PostgreSQL).');
|
|
342
372
|
|
|
343
373
|
// Set up in-process CodeGen runner for RSU
|
|
344
374
|
try {
|
|
@@ -361,21 +391,21 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
361
391
|
RuntimeSchemaManager.Instance.SetCodeGenRunner({
|
|
362
392
|
RunInProcess: (skipDB) => runObject.RunInProcess(codegenDataSource, skipDB, rsuWorkDir),
|
|
363
393
|
});
|
|
364
|
-
|
|
394
|
+
startupLog.LogIf('verbose', 'RSU in-process CodeGen runner initialized (PostgreSQL).');
|
|
365
395
|
|
|
366
396
|
// Inject CodeGen output paths for targeted git staging
|
|
367
397
|
const { initializeConfig } = await import('@memberjunction/codegen-lib');
|
|
368
398
|
const cgConfig = initializeConfig(rsuWorkDir);
|
|
369
399
|
const outputPaths = (cgConfig.output ?? []).map((o: { directory: string }) => o.directory);
|
|
370
400
|
RuntimeSchemaManager.Instance.SetCodeGenOutputPaths(outputPaths);
|
|
371
|
-
|
|
401
|
+
startupLog.LogIf('verbose', `RSU CodeGen output paths: ${outputPaths.length} directories configured.`);
|
|
372
402
|
|
|
373
403
|
// Point RSU's soft PK/FK writer at the SAME file CodeGen reads (mj.config.cjs
|
|
374
404
|
// `additionalSchemaInfo`), or RSU writes soft PKs to its own default path while
|
|
375
405
|
// CodeGen reads a different one and skips integration tables with "No primary key found".
|
|
376
406
|
if (cgConfig.additionalSchemaInfo) {
|
|
377
407
|
RuntimeSchemaManager.Instance.SetAdditionalSchemaInfoPath(cgConfig.additionalSchemaInfo);
|
|
378
|
-
|
|
408
|
+
startupLog.LogIf('verbose', `RSU additionalSchemaInfo path: ${cgConfig.additionalSchemaInfo}`);
|
|
379
409
|
}
|
|
380
410
|
} catch (codegenErr) {
|
|
381
411
|
console.warn(`RSU in-process CodeGen runner setup failed (will fall back to child process): ${(codegenErr as Error).message}`);
|
|
@@ -386,10 +416,12 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
386
416
|
}
|
|
387
417
|
|
|
388
418
|
const md = new Metadata(); // global-provider-ok: bootstrap
|
|
389
|
-
|
|
419
|
+
const pgEntityCount = md?.Entities ? md.Entities.length : 0;
|
|
420
|
+
startupLog.LogIf('verbose', `Data Source has been initialized. ${pgEntityCount} entities loaded.`);
|
|
421
|
+
startupLog.SetDatabaseInfo('PostgreSQL', `${pgHost}:${pgPort}/${pgDatabase}`, pgEntityCount);
|
|
390
422
|
} else {
|
|
391
423
|
// ─── SQL Server Path (existing behavior) ───────────────────────
|
|
392
|
-
|
|
424
|
+
startupLog.LogIf('verbose', 'Database type: SQL Server');
|
|
393
425
|
let tPhase = performance.now();
|
|
394
426
|
const pool = new sql.ConnectionPool(createMSSQLConfig());
|
|
395
427
|
|
|
@@ -421,14 +453,16 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
421
453
|
await readOnlyPool.connect();
|
|
422
454
|
|
|
423
455
|
dataSources.push(new DataSourceInfo({dataSource: readOnlyPool, type: 'Read-Only', host: dbHost, port: dbPort, database: dbDatabase, userName: configInfo.dbReadOnlyUsername}));
|
|
424
|
-
|
|
456
|
+
startupLog.LogIf('verbose', 'Read-only Connection Pool has been initialized.');
|
|
425
457
|
}
|
|
426
458
|
|
|
427
459
|
const config = new SQLServerProviderConfigData(pool, mj_core_schema, cacheRefreshInterval);
|
|
428
460
|
await setupSQLServerClient(config);
|
|
429
461
|
tPhase = lap('Metadata + Provider Setup', tPhase);
|
|
430
462
|
const md = new Metadata(); // global-provider-ok: bootstrap
|
|
431
|
-
|
|
463
|
+
const entityCount = md?.Entities ? md.Entities.length : 0;
|
|
464
|
+
startupLog.LogIf('verbose', `Data Source has been initialized. ${entityCount} entities loaded.`);
|
|
465
|
+
startupLog.SetDatabaseInfo('SQL Server', `${dbHost}:${dbPort}/${dbDatabase}`, entityCount);
|
|
432
466
|
|
|
433
467
|
// Set up CodeGen-credentialed provider for RSU DDL operations (CREATE TABLE, CREATE SCHEMA, etc.)
|
|
434
468
|
const codegenUser = process.env.CODEGEN_DB_USERNAME;
|
|
@@ -457,7 +491,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
457
491
|
const codegenProvider = new SQLServerDataProvider();
|
|
458
492
|
await codegenProvider.Config(codegenConfig);
|
|
459
493
|
RuntimeSchemaManager.Instance.SetDDLProvider(codegenProvider);
|
|
460
|
-
|
|
494
|
+
startupLog.LogIf('verbose', 'RSU DDL provider initialized with CodeGen credentials.');
|
|
461
495
|
|
|
462
496
|
// Set up in-process CodeGen runner for RSU
|
|
463
497
|
try {
|
|
@@ -480,14 +514,14 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
480
514
|
RuntimeSchemaManager.Instance.SetCodeGenRunner({
|
|
481
515
|
RunInProcess: (skipDB) => runObject.RunInProcess(codegenDataSource, skipDB, rsuWorkDir),
|
|
482
516
|
});
|
|
483
|
-
|
|
517
|
+
startupLog.LogIf('verbose', 'RSU in-process CodeGen runner initialized.');
|
|
484
518
|
|
|
485
519
|
// Inject CodeGen output paths for targeted git staging
|
|
486
520
|
const { initializeConfig } = await import('@memberjunction/codegen-lib');
|
|
487
521
|
const codegenConfig = initializeConfig(rsuWorkDir);
|
|
488
522
|
const outputPaths = (codegenConfig.output ?? []).map((o: { directory: string }) => o.directory);
|
|
489
523
|
RuntimeSchemaManager.Instance.SetCodeGenOutputPaths(outputPaths);
|
|
490
|
-
|
|
524
|
+
startupLog.LogIf('verbose', `RSU CodeGen output paths: ${outputPaths.length} directories configured.`);
|
|
491
525
|
|
|
492
526
|
// Point RSU's soft PK/FK writer at the SAME file CodeGen reads (mj.config.cjs
|
|
493
527
|
// `additionalSchemaInfo`). Without this, RSU writes soft PKs to its own default
|
|
@@ -495,7 +529,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
495
529
|
// with "No primary key found".
|
|
496
530
|
if (codegenConfig.additionalSchemaInfo) {
|
|
497
531
|
RuntimeSchemaManager.Instance.SetAdditionalSchemaInfoPath(codegenConfig.additionalSchemaInfo);
|
|
498
|
-
|
|
532
|
+
startupLog.LogIf('verbose', `RSU additionalSchemaInfo path: ${codegenConfig.additionalSchemaInfo}`);
|
|
499
533
|
}
|
|
500
534
|
} catch (codegenErr) {
|
|
501
535
|
console.warn(`RSU in-process CodeGen runner setup failed (will fall back to child process): ${(codegenErr as Error).message}`);
|
|
@@ -521,10 +555,10 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
521
555
|
if (configInfo.telemetry?.level) {
|
|
522
556
|
tm.UpdateSettings({ level: configInfo.telemetry.level as TelemetryLevel });
|
|
523
557
|
}
|
|
524
|
-
|
|
558
|
+
startupLog.LogIf('verbose', `Server telemetry enabled with level: ${configInfo.telemetry.level || 'standard'}`);
|
|
525
559
|
} else {
|
|
526
560
|
tm.SetEnabled(false);
|
|
527
|
-
|
|
561
|
+
startupLog.LogIf('verbose', 'Server telemetry disabled');
|
|
528
562
|
}
|
|
529
563
|
|
|
530
564
|
// Optionally inject Redis as the shared storage provider for cross-server cache invalidation
|
|
@@ -559,7 +593,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
559
593
|
}
|
|
560
594
|
});
|
|
561
595
|
|
|
562
|
-
|
|
596
|
+
startupLog.LogIf('verbose', `Redis cache provider connected: ${process.env.REDIS_URL}`);
|
|
563
597
|
}
|
|
564
598
|
|
|
565
599
|
// If Redis is available, swap LocalCacheManager's storage provider to Redis.
|
|
@@ -567,7 +601,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
567
601
|
// during engine loading. SetStorageProvider migrates cached data to Redis.
|
|
568
602
|
if (process.env.REDIS_URL) {
|
|
569
603
|
await LocalCacheManager.Instance.SetStorageProvider(Metadata.Provider.LocalStorageProvider); // global-provider-ok: bootstrap
|
|
570
|
-
|
|
604
|
+
startupLog.LogIf('verbose', 'LocalCacheManager: storage provider swapped to Redis');
|
|
571
605
|
}
|
|
572
606
|
// Ensure LocalCacheManager is initialized (no-op if already done during engine loading)
|
|
573
607
|
if (!LocalCacheManager.Instance.IsInitialized) {
|
|
@@ -581,11 +615,14 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
581
615
|
verboseLogging: cs.verboseLogging ?? false,
|
|
582
616
|
};
|
|
583
617
|
await LocalCacheManager.Instance.Initialize(Metadata.Provider.LocalStorageProvider, cacheConfig); // global-provider-ok: bootstrap
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
618
|
+
if (startupLog.IsAtLeast('verbose')) {
|
|
619
|
+
// eslint-disable-next-line no-console
|
|
620
|
+
console.log('LocalCacheManager initialized with cache config:', JSON.stringify({
|
|
621
|
+
maxMemoryMB: cs.maxMemoryMB ?? 150,
|
|
622
|
+
maxPercentOfCachePerEntity: cs.maxPercentOfCachePerEntity ?? 50,
|
|
623
|
+
evictionSweepIntervalSeconds: cs.evictionSweepIntervalSeconds ?? 300,
|
|
624
|
+
}));
|
|
625
|
+
}
|
|
589
626
|
}
|
|
590
627
|
|
|
591
628
|
// Initialize APIKeyEngine singleton — reads apiKeyGeneration from mj.config.cjs automatically
|
|
@@ -657,7 +694,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
657
694
|
// Initialize all middleware
|
|
658
695
|
for (const mw of middlewares) {
|
|
659
696
|
await mw.Initialize();
|
|
660
|
-
|
|
697
|
+
startupLog.LogIf('verbose', ` [Middleware] ${mw.Label}`);
|
|
661
698
|
}
|
|
662
699
|
|
|
663
700
|
// Collect middleware contributions for each pipeline stage
|
|
@@ -708,7 +745,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
708
745
|
Object.values(module).filter((value) => typeof value === 'function')
|
|
709
746
|
);
|
|
710
747
|
allResolvers = [...resolvers, ...mwResolvers] as BuildSchemaOptions['resolvers'];
|
|
711
|
-
|
|
748
|
+
startupLog.LogIf('verbose', ` [Middleware Resolvers] Loaded ${mwResolverFiles.length} resolver file(s) from middleware`);
|
|
712
749
|
}
|
|
713
750
|
}
|
|
714
751
|
|
|
@@ -911,7 +948,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
911
948
|
|
|
912
949
|
// ─── OAuth callback routes (unauthenticated, registered BEFORE auth) ─────
|
|
913
950
|
const oauthPublicUrl = configInfo.publicUrl || `${configInfo.baseUrl}:${configInfo.graphqlPort}${configInfo.graphqlRootPath || ''}`;
|
|
914
|
-
|
|
951
|
+
startupLog.LogIf('verbose', `[OAuth] publicUrl: ${oauthPublicUrl}`);
|
|
915
952
|
|
|
916
953
|
let oauthAuthenticatedRouter: ReturnType<typeof createOAuthCallbackHandler>['authenticatedRouter'] | undefined;
|
|
917
954
|
if (oauthPublicUrl) {
|
|
@@ -926,14 +963,14 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
926
963
|
|
|
927
964
|
// OAuth callback is unauthenticated (called by external auth server)
|
|
928
965
|
app.use('/oauth', oauthCors, callbackRouter);
|
|
929
|
-
|
|
966
|
+
startupLog.LogIf('verbose', '[OAuth] Callback route registered at /oauth/callback');
|
|
930
967
|
}
|
|
931
968
|
|
|
932
969
|
// ─── eSignature webhook (unauthenticated, registered BEFORE auth) ─────
|
|
933
970
|
// Called by external signature providers (DocuSign Connect, etc.) without an MJ bearer token.
|
|
934
971
|
// The provider DRIVER verifies the payload signature/HMAC; MJ auth does not apply here.
|
|
935
972
|
app.use('/esignature', cors<cors.CorsRequest>(), createSignatureWebhookHandler());
|
|
936
|
-
|
|
973
|
+
startupLog.LogIf('verbose', '[eSignature] Webhook route registered at /esignature/webhook/:driverKey');
|
|
937
974
|
|
|
938
975
|
// ─── Magic-link routes (MJ-issued, app-scoped external access) ───────────
|
|
939
976
|
// Public router (JWKS + redeem) mounts BEFORE the auth middleware; the
|
|
@@ -944,14 +981,27 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
944
981
|
magicLinkAuthenticatedRouter = authenticatedRouter;
|
|
945
982
|
registerMagicLinkAuthProvider(oauthPublicUrl, configInfo.magicLink);
|
|
946
983
|
app.use(MAGIC_LINK_MOUNT_PATH, cors<cors.CorsRequest>(), publicRouter);
|
|
947
|
-
|
|
984
|
+
startupLog.LogIf('verbose', `[MagicLink] Public routes registered at ${MAGIC_LINK_MOUNT_PATH}/redeem and ${MAGIC_LINK_MOUNT_PATH}/jwks.json`);
|
|
948
985
|
}
|
|
949
986
|
|
|
950
987
|
// ─── Global CORS (before auth so 401 responses include CORS headers) ─────
|
|
951
988
|
// Without this, the browser blocks 401 responses from the auth middleware
|
|
952
989
|
// because they lack Access-Control-Allow-Origin headers, preventing the
|
|
953
990
|
// client from reading the error code and triggering token refresh.
|
|
954
|
-
|
|
991
|
+
const corsAllowed = configInfo.cors?.allowedOrigins ?? ['*'];
|
|
992
|
+
app.use(cors<cors.CorsRequest>({
|
|
993
|
+
origin: (origin, callback) => {
|
|
994
|
+
// Allow all origins when ['*'] (default/backward-compatible),
|
|
995
|
+
// or when no Origin header (server-to-server calls).
|
|
996
|
+
if (corsAllowed.includes('*') || !origin || corsAllowed.includes(origin)) {
|
|
997
|
+
callback(null, true);
|
|
998
|
+
} else {
|
|
999
|
+
callback(new Error(`Origin ${origin} not allowed by CORS`));
|
|
1000
|
+
}
|
|
1001
|
+
},
|
|
1002
|
+
credentials: configInfo.cors?.allowCredentials ?? true,
|
|
1003
|
+
maxAge: configInfo.cors?.maxAge ?? 86400,
|
|
1004
|
+
}));
|
|
955
1005
|
|
|
956
1006
|
// ─── Server extensions (before auth — extensions handle their own auth) ─────
|
|
957
1007
|
// Slack uses HMAC signature verification, Teams uses Bot Framework JWT validation.
|
|
@@ -985,13 +1035,13 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
985
1035
|
if (oauthAuthenticatedRouter) {
|
|
986
1036
|
const oauthCors = cors<cors.CorsRequest>();
|
|
987
1037
|
app.use('/oauth', oauthCors, BodyParser.json(), oauthAuthenticatedRouter);
|
|
988
|
-
|
|
1038
|
+
startupLog.LogIf('verbose', '[OAuth] Authenticated routes registered at /oauth/status, /oauth/initiate, and /oauth/exchange');
|
|
989
1039
|
}
|
|
990
1040
|
|
|
991
1041
|
// ─── Magic-link authenticated route (invite creation) ─────────────────────
|
|
992
1042
|
if (magicLinkAuthenticatedRouter) {
|
|
993
1043
|
app.use(MAGIC_LINK_MOUNT_PATH, cors<cors.CorsRequest>(), magicLinkAuthenticatedRouter);
|
|
994
|
-
|
|
1044
|
+
startupLog.LogIf('verbose', `[MagicLink] Authenticated route registered at ${MAGIC_LINK_MOUNT_PATH}/create`);
|
|
995
1045
|
}
|
|
996
1046
|
|
|
997
1047
|
// ─── REST API endpoints (auth already handled by unified middleware) ─────
|
|
@@ -1008,9 +1058,11 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1008
1058
|
if (process.env.MJ_REST_API_ENABLED !== undefined) {
|
|
1009
1059
|
restApiConfig.enabled = process.env.MJ_REST_API_ENABLED === 'true';
|
|
1010
1060
|
if (restApiConfig.enabled) {
|
|
1011
|
-
|
|
1061
|
+
// Genuine config override — actionable, surfaced at all levels.
|
|
1062
|
+
LogStatus('REST API is enabled via environment variable');
|
|
1012
1063
|
}
|
|
1013
1064
|
}
|
|
1065
|
+
startupLog.SetRestEnabled(restApiConfig.enabled);
|
|
1014
1066
|
|
|
1015
1067
|
if (process.env.MJ_REST_API_INCLUDE_ENTITIES) {
|
|
1016
1068
|
restApiConfig.includeEntities = process.env.MJ_REST_API_INCLUDE_ENTITIES.split(',').map(e => e.trim());
|
|
@@ -1069,9 +1121,19 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1069
1121
|
tServe = lap('Apollo + Express Setup', tServe);
|
|
1070
1122
|
|
|
1071
1123
|
await new Promise<void>((resolve) => httpServer.listen({ port: graphqlPort }, resolve));
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1124
|
+
// Total startup measured but not pushed as a phase row — the per-phase rows already
|
|
1125
|
+
// sum to the total in the collapsed summary line. Keep the measurement for verbose.
|
|
1126
|
+
startupLog.EndPhase('Total Startup', t0);
|
|
1127
|
+
startupLog.SetReadyUrl(`http://localhost:${graphqlPort}/`);
|
|
1128
|
+
// Populate summary fields that are known only now, then print the one summary block.
|
|
1129
|
+
for (const provider of AuthProviderFactory.Instance.getAllProviders()) {
|
|
1130
|
+
startupLog.AddAuthProvider(provider.name);
|
|
1131
|
+
}
|
|
1132
|
+
startupLog.SetVersion(resolveServerVersion());
|
|
1133
|
+
if (scheduledJobsService) {
|
|
1134
|
+
startupLog.SetScheduledJobCount(scheduledJobsService.GetStatus().activeJobs);
|
|
1135
|
+
}
|
|
1136
|
+
startupLog.PrintSummary();
|
|
1075
1137
|
|
|
1076
1138
|
// Process pending RSU work from pre-restart (entity maps, field maps, sync)
|
|
1077
1139
|
processRSUPendingWork().catch(err => console.warn(`RSU pending work processing failed: ${err}`));
|
|
@@ -1092,6 +1154,14 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1092
1154
|
.catch(err => console.warn(`[AgentRunWatchdog] Startup sweep failed: ${err}`));
|
|
1093
1155
|
}
|
|
1094
1156
|
|
|
1157
|
+
// Launch the AI Agent Session janitor: run own-host orphan recovery once at boot, then keep a
|
|
1158
|
+
// periodic staleness sweep running. Self-registers with ShutdownRegistry, so its timer is cleared
|
|
1159
|
+
// by the gracefulShutdown drain below (no explicit Stop() wiring needed here).
|
|
1160
|
+
if (resumeUser && Metadata.Provider instanceof DatabaseProviderBase) { // global-provider-ok: server startup recovery — boot-time session janitor uses the server's own provider
|
|
1161
|
+
SessionJanitor.Instance.Start(Metadata.Provider, resumeUser) // global-provider-ok: server-owned background reconciler runs under the server's provider + system user
|
|
1162
|
+
.catch(err => console.warn(`[SessionJanitor] Startup failed: ${err}`));
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1095
1165
|
// Set up graceful shutdown handlers
|
|
1096
1166
|
const gracefulShutdown = async (signal: string) => {
|
|
1097
1167
|
console.log(`\n${signal} received, shutting down gracefully...`);
|