@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
@@ -0,0 +1,122 @@
1
+ import { BaseSingleton } from '@memberjunction/global';
2
+
3
+ /**
4
+ * Terminal/structural outcome of a remote-browser goal run — the fields the client needs to render
5
+ * the result, decoupled from the GraphQL `RemoteBrowserGoalResultType` (avoids a resolver↔registry
6
+ * import cycle) and from the engine's `RemoteBrowserGoalResult` (whose `Strategy` is a string union).
7
+ */
8
+ export interface RemoteBrowserGoalOutcome {
9
+ Success: boolean;
10
+ Strategy?: string;
11
+ CurrentUrl?: string;
12
+ Status?: string;
13
+ StepCount?: number;
14
+ Detail?: string;
15
+ }
16
+
17
+ type GoalRunStatus = 'Running' | 'Complete';
18
+
19
+ /** One tracked goal run: its lifecycle status plus the terminal outcome once it finishes. */
20
+ export interface GoalRunRecord {
21
+ GoalRunID: string;
22
+ /** Lowercased `AIAgentSession` id the goal runs against (one in-flight goal per session). */
23
+ AgentSessionID: string;
24
+ Status: GoalRunStatus;
25
+ Outcome?: RemoteBrowserGoalOutcome;
26
+ StartedAt: number;
27
+ CompletedAt?: number;
28
+ }
29
+
30
+ /** Default time a COMPLETED record is retained so a slow poller can still read it (then swept). */
31
+ const COMPLETED_TTL_MS = 5 * 60 * 1000;
32
+ /** Safety cap: a record that never completes (e.g. a crashed loop) is swept after this long. */
33
+ const RUNNING_MAX_MS = 30 * 60 * 1000;
34
+
35
+ /**
36
+ * Process-local registry of in-flight + recently-completed remote-browser GOAL runs.
37
+ *
38
+ * ## Why this exists
39
+ * A goal-driven browser run (computer-use loop) can take many minutes. Running it inside a single
40
+ * synchronous GraphQL mutation means the client holds one long-lived HTTP request open for the whole
41
+ * loop — fragile against browser fetch timeouts, proxy/ngrok idle limits, and session-janitor churn.
42
+ * Observed symptom: the loop completes successfully server-side but the client's request has already
43
+ * died, so the agent gets "no response from the server" and never learns the outcome.
44
+ *
45
+ * The fix is async: the start mutation kicks the goal off and registers it here (status `Running`),
46
+ * returning a `GoalRunID` immediately; the client then POLLS a short query that reads this registry
47
+ * until the run is `Complete`. Every request stays short, so no transport boundary times out.
48
+ *
49
+ * Keyed by `AIAgentSession` id (at most one in-flight goal per session, mirroring the engine's
50
+ * per-session abort registry). Process-local is correct: the browser session — and thus the goal —
51
+ * is pinned to the MJAPI instance hosting its CDP connection, the same instance the client's
52
+ * session-scoped mutations already target.
53
+ */
54
+ export class RemoteBrowserGoalRegistry extends BaseSingleton<RemoteBrowserGoalRegistry> {
55
+ private runs = new Map<string, GoalRunRecord>();
56
+
57
+ protected constructor() {
58
+ super();
59
+ }
60
+
61
+ public static get Instance(): RemoteBrowserGoalRegistry {
62
+ return super.getInstance<RemoteBrowserGoalRegistry>();
63
+ }
64
+
65
+ private key(agentSessionID: string): string {
66
+ return (agentSessionID ?? '').toLowerCase();
67
+ }
68
+
69
+ /** Registers a newly-started goal as `Running`, replacing any prior record for the session. */
70
+ public Begin(agentSessionID: string, goalRunID: string): void {
71
+ this.sweep();
72
+ this.runs.set(this.key(agentSessionID), {
73
+ GoalRunID: goalRunID,
74
+ AgentSessionID: this.key(agentSessionID),
75
+ Status: 'Running',
76
+ StartedAt: Date.now(),
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Marks a goal run `Complete` with its terminal outcome. No-op when the session's current record is
82
+ * for a DIFFERENT `goalRunID` (a newer goal superseded it) — the stale completion is dropped.
83
+ */
84
+ public Complete(agentSessionID: string, goalRunID: string, outcome: RemoteBrowserGoalOutcome): void {
85
+ const rec = this.runs.get(this.key(agentSessionID));
86
+ if (!rec || rec.GoalRunID !== goalRunID) {
87
+ return;
88
+ }
89
+ rec.Status = 'Complete';
90
+ rec.Outcome = outcome;
91
+ rec.CompletedAt = Date.now();
92
+ }
93
+
94
+ /**
95
+ * Returns the tracked record for a session, or `undefined` when none exists or `goalRunID` is
96
+ * supplied and doesn't match the current run (so a poll for an expired/superseded id reads nothing).
97
+ */
98
+ public Get(agentSessionID: string, goalRunID?: string): GoalRunRecord | undefined {
99
+ this.sweep();
100
+ const rec = this.runs.get(this.key(agentSessionID));
101
+ if (!rec) {
102
+ return undefined;
103
+ }
104
+ if (goalRunID && rec.GoalRunID !== goalRunID) {
105
+ return undefined;
106
+ }
107
+ return rec;
108
+ }
109
+
110
+ /** Drops completed records past their TTL and never-completing records past the safety cap. */
111
+ private sweep(): void {
112
+ const now = Date.now();
113
+ for (const [k, rec] of this.runs) {
114
+ const expired =
115
+ (rec.Status === 'Complete' && rec.CompletedAt != null && now - rec.CompletedAt > COMPLETED_TTL_MS) ||
116
+ now - rec.StartedAt > RUNNING_MAX_MS;
117
+ if (expired) {
118
+ this.runs.delete(k);
119
+ }
120
+ }
121
+ }
122
+ }
@@ -1,5 +1,5 @@
1
1
  import { configInfo } from '../config.js';
2
- import { AuthProviderConfig, LogError, LogStatus } from '@memberjunction/core';
2
+ import { AuthProviderConfig, LogError } from '@memberjunction/core';
3
3
  import { AuthProviderFactory } from '@memberjunction/auth-providers';
4
4
 
5
5
  /**
@@ -16,8 +16,10 @@ export function initializeAuthProviders(): void {
16
16
  for (const providerConfig of configInfo.authProviders) {
17
17
  try {
18
18
  const provider = AuthProviderFactory.createProvider(providerConfig as AuthProviderConfig);
19
+ // register() emits the verbose-only "Registered auth provider: … with issuer: …" line
20
+ // (gated by the global verbose flag). Provider NAMES are surfaced compactly in the
21
+ // startup summary `Auth` line at `standard`.
19
22
  factory.register(provider);
20
- LogStatus(`Registered auth provider: ${provider.name} (type: ${providerConfig.type})`);
21
23
  } catch (error) {
22
24
  LogError(`Failed to initialize auth provider ${providerConfig.name}: ${error}`);
23
25
  }
package/src/config.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { cosmiconfigSync } from 'cosmiconfig';
3
- import { LogError, LogStatus } from '@memberjunction/core';
3
+ import { LogError, LogStatus, LogStatusEx } from '@memberjunction/core';
4
4
  import { mergeConfigs, parseBooleanEnv } from '@memberjunction/config';
5
5
 
6
6
  const explorer = cosmiconfigSync('mj', { searchStrategy: 'global' });
@@ -586,6 +586,14 @@ export const DEFAULT_SERVER_CONFIG: Partial<ConfigInfo> = {
586
586
  ].filter(Boolean),
587
587
  };
588
588
 
589
+ /**
590
+ * Absolute path to the resolved config file, captured during {@link loadConfig}.
591
+ * `undefined` when no config file was found (defaults in use). Rendered in the
592
+ * startup summary `Config` line. Declared before `configInfo` so the assignment
593
+ * inside `loadConfig()` (invoked below) is not in its temporal dead zone.
594
+ */
595
+ export let configFilePath: string | undefined;
596
+
589
597
  export const configInfo: ConfigInfo = loadConfig();
590
598
 
591
599
  export const {
@@ -621,7 +629,10 @@ export function loadConfig() {
621
629
 
622
630
  // If user config exists, merge it with defaults
623
631
  if (configSearchResult && !configSearchResult.isEmpty) {
624
- LogStatus(`Config file found at ${configSearchResult.filepath}`);
632
+ // Resolved config-file path. Surfaced in the startup summary `Config` line at standard
633
+ // level (see StartupLogger). Demoted to verbose-only here to avoid a duplicate inline line.
634
+ configFilePath = configSearchResult.filepath;
635
+ LogStatusEx({ message: `Config file found at ${configSearchResult.filepath}`, verboseOnly: true });
625
636
 
626
637
  // Merge user config with defaults (user config takes precedence)
627
638
  mergedConfig = mergeConfigs(DEFAULT_SERVER_CONFIG, configSearchResult.config);
package/src/context.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { IncomingMessage } from 'http';
2
- import * as url from 'url';
3
2
  import { default as jwt } from 'jsonwebtoken';
4
3
  import 'reflect-metadata';
5
4
  import { Subject, firstValueFrom } from 'rxjs';
@@ -10,6 +9,7 @@ import { TokenExpiredError, AuthProviderFactory } from '@memberjunction/auth-pro
10
9
  import { authCache } from './cache.js';
11
10
  import { userEmailMap, apiKey, mj_core_schema } from './config.js';
12
11
  import { buildBoundaryLogPayload } from './logging/boundaryLogPayload.js';
12
+ import { StartupLogger } from './logging/StartupLogger.js';
13
13
  import { DataSourceInfo, UserPayload } from './types.js';
14
14
  import { GetReadOnlyDataSource, GetReadWriteDataSource } from './util.js';
15
15
  import { v4 as uuidv4 } from 'uuid';
@@ -30,6 +30,20 @@ import { GetAPIKeyEngine } from '@memberjunction/api-keys';
30
30
  // long-lived sessions every hour.) Reviewers asked to audit all user access with
31
31
  // timestamp/IP/browser — that payload lives in the Details JSON; the table has no
32
32
  // dedicated columns. Best-effort throughout: an audit failure NEVER blocks auth.
33
+ /**
34
+ * Memoized "is the server log level `debug`?" check for the per-request log
35
+ * gates below. Resolved once from `telemetry.level` (the single log-level knob)
36
+ * — config is immutable after boot, so caching is safe and keeps the hot request
37
+ * path free of repeated config reads.
38
+ */
39
+ let isDebugLevelCache: boolean | undefined;
40
+ function isDebugLogLevel(): boolean {
41
+ if (isDebugLevelCache === undefined) {
42
+ isDebugLevelCache = StartupLogger.resolveLevelFromConfig() === 'debug';
43
+ }
44
+ return isDebugLevelCache;
45
+ }
46
+
33
47
  const SESSION_AUDIT_TYPE = 'Session Established';
34
48
  const LOGIN_FAILED_AUDIT_TYPE = 'Login Failed';
35
49
  const SESSION_AUDIT_CACHE_MAX = 50_000;
@@ -269,9 +283,12 @@ const verifyAsync = async (issuer: string, token: string): Promise<jwt.JwtPayloa
269
283
  if (jwt && typeof jwt !== 'string' && !err) {
270
284
  const payload = jwt.payload ?? jwt;
271
285
 
272
- // Use provider to extract user info for logging
273
- const userInfo = extractUserInfoFromPayload(payload);
274
- console.log(`Valid token: ${userInfo.fullName || 'Unknown'} (${userInfo.email || userInfo.preferredUsername || 'Unknown'})`);
286
+ // Per-request token confirmation debug-only (one of the worst
287
+ // "constantly on" offenders on an authenticated server).
288
+ if (isDebugLogLevel()) {
289
+ const userInfo = extractUserInfoFromPayload(payload);
290
+ console.log(`Valid token: ${userInfo.fullName || 'Unknown'} (${userInfo.email || userInfo.preferredUsername || 'Unknown'})`);
291
+ }
275
292
  resolve(payload);
276
293
  } else {
277
294
  console.warn('Invalid token');
@@ -462,6 +479,22 @@ export const getUserPayload = async (
462
479
  * Extracts auth headers and builds a RequestContext from an Express request.
463
480
  * Shared by both the unified auth middleware and the WebSocket context.
464
481
  */
482
+ /**
483
+ * Extracts the hostname from a request `Origin` header using the WHATWG `URL` API (replacing the
484
+ * deprecated, security-flagged `url.parse()` — Node DEP0169). Returns `undefined` for a missing,
485
+ * empty, or unparseable origin, matching the prior `url.parse().hostname ?? undefined` semantics.
486
+ */
487
+ function parseRequestHostname(origin: string | undefined): string | undefined {
488
+ if (!origin) {
489
+ return undefined;
490
+ }
491
+ try {
492
+ return new URL(origin).hostname || undefined;
493
+ } catch {
494
+ return undefined;
495
+ }
496
+ }
497
+
465
498
  function extractAuthInputs(req: IncomingMessage): {
466
499
  bearerToken: string;
467
500
  sessionId: string;
@@ -471,7 +504,7 @@ function extractAuthInputs(req: IncomingMessage): {
471
504
  requestContext: RequestContext;
472
505
  } {
473
506
  const sessionIdRaw = req.headers['x-session-id'];
474
- const requestDomain = url.parse(req.headers.origin || '');
507
+ const requestDomain = parseRequestHostname(req.headers.origin);
475
508
  const sessionId = sessionIdRaw ? sessionIdRaw.toString() : '';
476
509
  const bearerToken = req.headers.authorization ?? '';
477
510
  const systemApiKey = String(req.headers['x-mj-api-key']);
@@ -489,7 +522,7 @@ function extractAuthInputs(req: IncomingMessage): {
489
522
  return {
490
523
  bearerToken,
491
524
  sessionId,
492
- requestDomain: requestDomain?.hostname ?? undefined,
525
+ requestDomain,
493
526
  systemApiKey,
494
527
  userApiKey,
495
528
  requestContext,
@@ -573,7 +606,9 @@ export const contextFunction =
573
606
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
574
607
  const reqAny = req as any;
575
608
  const operationName: string | undefined = reqAny.body?.operationName;
576
- if (operationName !== 'IntrospectionQuery') {
609
+ // Per-request GraphQL boundary line — debug-only. Kept (not deleted) so the
610
+ // data is available when an operator opts into debug, but off by default.
611
+ if (operationName !== 'IntrospectionQuery' && isDebugLogLevel()) {
577
612
  console.dir(buildBoundaryLogPayload(operationName), { depth: null, breakLength: 200 });
578
613
  }
579
614