@memberjunction/server 5.48.0 → 5.50.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 (127) hide show
  1. package/README.md +9 -0
  2. package/dist/auth/magicLink/MagicLinkKeys.d.ts.map +1 -1
  3. package/dist/auth/magicLink/MagicLinkKeys.js +11 -4
  4. package/dist/auth/magicLink/MagicLinkKeys.js.map +1 -1
  5. package/dist/auth/magicLink/MagicLinkRouter.js +2 -2
  6. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  7. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  8. package/dist/auth/magicLink/MagicLinkService.js +4 -0
  9. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  10. package/dist/auth/newUsers.d.ts.map +1 -1
  11. package/dist/auth/newUsers.js +6 -4
  12. package/dist/auth/newUsers.js.map +1 -1
  13. package/dist/config.d.ts +40 -0
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +12 -0
  16. package/dist/config.js.map +1 -1
  17. package/dist/context.d.ts.map +1 -1
  18. package/dist/context.js +28 -2
  19. package/dist/context.js.map +1 -1
  20. package/dist/generated/generated.d.ts +373 -6
  21. package/dist/generated/generated.d.ts.map +1 -1
  22. package/dist/generated/generated.js +1975 -39
  23. package/dist/generated/generated.js.map +1 -1
  24. package/dist/generic/ResolverBase.d.ts.map +1 -1
  25. package/dist/generic/ResolverBase.js +4 -2
  26. package/dist/generic/ResolverBase.js.map +1 -1
  27. package/dist/generic/RunViewResolver.d.ts +1 -0
  28. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  29. package/dist/generic/RunViewResolver.js +12 -0
  30. package/dist/generic/RunViewResolver.js.map +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +73 -11
  33. package/dist/index.js.map +1 -1
  34. package/dist/integration/CustomColumnPromoter.d.ts +15 -2
  35. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
  36. package/dist/integration/CustomColumnPromoter.js +113 -11
  37. package/dist/integration/CustomColumnPromoter.js.map +1 -1
  38. package/dist/integration/EntityMapLifecycle.d.ts +66 -0
  39. package/dist/integration/EntityMapLifecycle.d.ts.map +1 -0
  40. package/dist/integration/EntityMapLifecycle.js +189 -0
  41. package/dist/integration/EntityMapLifecycle.js.map +1 -0
  42. package/dist/realtimeProxy/RealtimeProxyServer.d.ts +10 -0
  43. package/dist/realtimeProxy/RealtimeProxyServer.d.ts.map +1 -1
  44. package/dist/realtimeProxy/RealtimeProxyServer.js +67 -1
  45. package/dist/realtimeProxy/RealtimeProxyServer.js.map +1 -1
  46. package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -1
  47. package/dist/realtimeWidget/WidgetRouter.js +6 -2
  48. package/dist/realtimeWidget/WidgetRouter.js.map +1 -1
  49. package/dist/realtimeWidget/WidgetSessionService.d.ts +12 -1
  50. package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -1
  51. package/dist/realtimeWidget/WidgetSessionService.js +23 -8
  52. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
  53. package/dist/resolvers/AdhocQueryResolver.d.ts +17 -0
  54. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  55. package/dist/resolvers/AdhocQueryResolver.js +81 -25
  56. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  57. package/dist/resolvers/GetDataResolver.d.ts +16 -0
  58. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  59. package/dist/resolvers/GetDataResolver.js +26 -1
  60. package/dist/resolvers/GetDataResolver.js.map +1 -1
  61. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +53 -1
  62. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  63. package/dist/resolvers/IntegrationDiscoveryResolver.js +531 -64
  64. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  65. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  66. package/dist/resolvers/QueryResolver.js +0 -6
  67. package/dist/resolvers/QueryResolver.js.map +1 -1
  68. package/dist/resolvers/RSUResolver.d.ts +10 -0
  69. package/dist/resolvers/RSUResolver.d.ts.map +1 -1
  70. package/dist/resolvers/RSUResolver.js +28 -0
  71. package/dist/resolvers/RSUResolver.js.map +1 -1
  72. package/dist/resolvers/RunAIAgentResolver.d.ts +9 -6
  73. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  74. package/dist/resolvers/RunAIAgentResolver.js +135 -132
  75. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  76. package/dist/resolvers/TransactionGroupResolver.d.ts +8 -1
  77. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  78. package/dist/resolvers/TransactionGroupResolver.js +29 -1
  79. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  80. package/dist/resolvers/adhoc-query-helpers.d.ts +27 -0
  81. package/dist/resolvers/adhoc-query-helpers.d.ts.map +1 -0
  82. package/dist/resolvers/adhoc-query-helpers.js +42 -0
  83. package/dist/resolvers/adhoc-query-helpers.js.map +1 -0
  84. package/dist/rest/OAuthCallbackHandler.d.ts +9 -0
  85. package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
  86. package/dist/rest/OAuthCallbackHandler.js +21 -4
  87. package/dist/rest/OAuthCallbackHandler.js.map +1 -1
  88. package/dist/telephony/media-upgrade-router.d.ts +14 -0
  89. package/dist/telephony/media-upgrade-router.d.ts.map +1 -1
  90. package/dist/telephony/media-upgrade-router.js +17 -1
  91. package/dist/telephony/media-upgrade-router.js.map +1 -1
  92. package/package.json +91 -89
  93. package/src/__tests__/AdhocQueryResolver.pagination.test.ts +114 -0
  94. package/src/__tests__/EntityMapLifecycle.dagWarnings.test.ts +91 -0
  95. package/src/__tests__/OAuthCallbackHandler.xss.test.ts +83 -0
  96. package/src/__tests__/RealtimeProxyServer.test.ts +53 -1
  97. package/src/__tests__/WidgetSessionService.test.ts +96 -0
  98. package/src/__tests__/adhoc-query-helpers.test.ts +50 -0
  99. package/src/__tests__/magicLinkService.provisionLog.test.ts +106 -0
  100. package/src/__tests__/media-upgrade-router.test.ts +43 -0
  101. package/src/__tests__/unifiedAuth.test.ts +18 -3
  102. package/src/auth/magicLink/MagicLinkKeys.ts +12 -6
  103. package/src/auth/magicLink/MagicLinkRouter.ts +2 -2
  104. package/src/auth/magicLink/MagicLinkService.ts +4 -0
  105. package/src/auth/newUsers.ts +6 -5
  106. package/src/config.ts +14 -0
  107. package/src/context.ts +28 -2
  108. package/src/generated/generated.ts +1385 -28
  109. package/src/generic/ResolverBase.ts +5 -2
  110. package/src/generic/RunViewResolver.ts +11 -0
  111. package/src/index.ts +75 -11
  112. package/src/integration/CustomColumnPromoter.ts +123 -6
  113. package/src/integration/EntityMapLifecycle.ts +221 -0
  114. package/src/realtimeProxy/RealtimeProxyServer.ts +70 -1
  115. package/src/realtimeWidget/WidgetRouter.ts +6 -2
  116. package/src/realtimeWidget/WidgetSessionService.ts +24 -8
  117. package/src/resolvers/AdhocQueryResolver.ts +97 -30
  118. package/src/resolvers/GetDataResolver.ts +30 -1
  119. package/src/resolvers/IntegrationDiscoveryResolver.ts +523 -64
  120. package/src/resolvers/QueryResolver.ts +1 -7
  121. package/src/resolvers/RSUResolver.ts +28 -0
  122. package/src/resolvers/RunAIAgentResolver.ts +74 -70
  123. package/src/resolvers/TransactionGroupResolver.ts +36 -2
  124. package/src/resolvers/__tests__/GetDataResolver.tokens.test.ts +138 -0
  125. package/src/resolvers/adhoc-query-helpers.ts +50 -0
  126. package/src/rest/OAuthCallbackHandler.ts +22 -4
  127. package/src/telephony/media-upgrade-router.ts +18 -1
@@ -258,9 +258,11 @@ describe('createUnifiedAuthMiddleware', () => {
258
258
  });
259
259
 
260
260
  describe('Authentication failures', () => {
261
- it('should return 401 when no authorization header is provided', async () => {
261
+ it('should return 401 when no authorization header is provided, quietly (no stack-trace log)', async () => {
262
262
  mockGetReadOnlyDS.mockReturnValue(MOCK_POOL);
263
263
  mockGetReadWriteDS.mockReturnValue(MOCK_POOL);
264
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
265
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
264
266
  // No auth header → empty bearer token → empty token after strip → "Missing token"
265
267
  const req = makeMockReq({ headers: {} });
266
268
  const res = makeMockRes();
@@ -270,7 +272,14 @@ describe('createUnifiedAuthMiddleware', () => {
270
272
 
271
273
  expect(next).not.toHaveBeenCalled();
272
274
  expect(res._status).toBe(401);
273
- expect(res._json).toEqual({ error: 'Authentication failed' });
275
+ expect(res._json).toEqual({ error: 'Authentication required' });
276
+ // An anonymous/unauthenticated request (health check, CORS-preflight-adjacent probe,
277
+ // a client mid-handshake) is routine — it must NOT log a warning or a stack trace.
278
+ // Same treatment as the existing TokenExpiredError branch.
279
+ expect(warnSpy).not.toHaveBeenCalled();
280
+ expect(errorSpy).not.toHaveBeenCalled();
281
+ warnSpy.mockRestore();
282
+ errorSpy.mockRestore();
274
283
  });
275
284
 
276
285
  it('should return 401 with JWT_EXPIRED code for expired tokens', async () => {
@@ -301,10 +310,11 @@ describe('createUnifiedAuthMiddleware', () => {
301
310
  });
302
311
  });
303
312
 
304
- it('should return 401 when jwt.decode returns null (corrupt token)', async () => {
313
+ it('should return 401 when jwt.decode returns null (corrupt token), and still log it in full', async () => {
305
314
  mockGetReadOnlyDS.mockReturnValue(MOCK_POOL);
306
315
  mockGetReadWriteDS.mockReturnValue(MOCK_POOL);
307
316
  mockJwtDecode.mockReturnValue(null);
317
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
308
318
 
309
319
  const req = makeMockReq({
310
320
  headers: { authorization: 'Bearer corrupt-token' },
@@ -317,6 +327,11 @@ describe('createUnifiedAuthMiddleware', () => {
317
327
  expect(next).not.toHaveBeenCalled();
318
328
  expect(res._status).toBe(401);
319
329
  expect(res._json).toEqual({ error: 'Authentication failed' });
330
+ // Unlike a bare "no credentials supplied" request, a token that IS present but
331
+ // fails to decode is worth a human's attention — the quiet path above is scoped
332
+ // to the "Missing token" case only, not a blanket suppression of auth logging.
333
+ expect(errorSpy).toHaveBeenCalledWith('Auth error:', expect.any(Error));
334
+ errorSpy.mockRestore();
320
335
  });
321
336
 
322
337
  it('should return 401 when verifyUserRecord returns null', async () => {
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { BaseSingleton } from '@memberjunction/global';
13
- import { LogStatus } from '@memberjunction/core';
13
+ import { LogStatusEx } from '@memberjunction/core';
14
14
  import { createPrivateKey, createPublicKey, generateKeyPairSync, createHash, type KeyObject } from 'node:crypto';
15
15
  import jwt from 'jsonwebtoken';
16
16
  import type { MagicLinkJWTClaims } from './types.js';
@@ -69,11 +69,17 @@ export class MagicLinkKeyManager extends BaseSingleton<MagicLinkKeyManager> {
69
69
  } else {
70
70
  const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
71
71
  priv = privateKey;
72
- LogStatus(
73
- '[MagicLink] No rsaPrivateKey configuredgenerated an EPHEMERAL RS256 keypair. ' +
74
- 'Outstanding magic-link sessions will be invalidated on restart. Set magicLink.rsaPrivateKey ' +
75
- '(or MJ_MAGIC_LINK_PRIVATE_KEY) for stable production keys.'
76
- );
72
+ // Verbose-only: on dev boxes with no configured key this fires on EVERY boot, and the
73
+ // ephemeral fallback is the designed behavior there not a condition worth a log line in
74
+ // the default startup output. Deployments that care set the key; operators diagnosing
75
+ // magic-link session loss can re-run with verbose logging.
76
+ LogStatusEx({
77
+ message:
78
+ '[MagicLink] No rsaPrivateKey configured — generated an EPHEMERAL RS256 keypair. ' +
79
+ 'Outstanding magic-link sessions will be invalidated on restart. Set magicLink.rsaPrivateKey ' +
80
+ '(or MJ_MAGIC_LINK_PRIVATE_KEY) for stable production keys.',
81
+ verboseOnly: true
82
+ });
77
83
  }
78
84
 
79
85
  this.privateKeyPem = priv.export({ type: 'pkcs8', format: 'pem' }) as string;
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { Router, json, urlencoded, type Request, type Response } from 'express';
12
12
  import { rateLimit } from 'express-rate-limit';
13
- import { LogError, LogStatus, type AuthProviderConfig } from '@memberjunction/core';
13
+ import { LogError, LogStatus, LogStatusEx, type AuthProviderConfig } from '@memberjunction/core';
14
14
  import { AuthProviderFactory } from '@memberjunction/auth-providers';
15
15
  import { configInfo, type MagicLinkConfig } from '../../config.js';
16
16
  import { MagicLinkKeyManager } from './MagicLinkKeys.js';
@@ -219,7 +219,7 @@ export function registerMagicLinkAuthProvider(publicUrl: string, config: MagicLi
219
219
  }
220
220
  const factory = AuthProviderFactory.Instance;
221
221
  factory.register(AuthProviderFactory.createProvider(providerConfig));
222
- LogStatus(`[MagicLink] Registered auth provider (issuer: ${publicUrl}, jwks: ${providerConfig.jwksUri})`);
222
+ LogStatusEx({ message: `[MagicLink] Registered auth provider (issuer: ${publicUrl}, jwks: ${providerConfig.jwksUri})`, verboseOnly: true });
223
223
  } catch (e) {
224
224
  LogError(`[MagicLink] Failed to register auth provider: ${e instanceof Error ? e.message : String(e)}`);
225
225
  }
@@ -549,6 +549,10 @@ export class MagicLinkService {
549
549
  // resolves this user without a full cache refresh.
550
550
  this.pushUserToCache(user, role);
551
551
 
552
+ // Deliberately NOT verbose-gated, unlike the MagicLink startup notices (2ddb865e40):
553
+ // provisioning a new external account via magic-link redemption is a security-relevant
554
+ // event that belongs in the default server log. At-scale deployments can revisit
555
+ // tapering this if volume ever makes it noise.
552
556
  LogStatus(`[MagicLink] Provisioned external user ${email} with role '${role.Name}'.`);
553
557
  return { success: true, userId: user.ID, email, firstName, lastName };
554
558
  } catch (txErr) {
@@ -2,7 +2,7 @@ import { ApplicationInfo, DatabaseProviderBase, EntitySaveOptions, LogError, Log
2
2
  import { RegisterClass } from "@memberjunction/global";
3
3
  import { UserCache } from "@memberjunction/sqlserver-dataprovider";
4
4
  import { configInfo } from "../config.js";
5
- import { MJUserEntity, MJUserRoleEntity, MJUserApplicationEntity, MJUserApplicationEntityEntity, MJApplicationEntityType, MJApplicationEntityEntityType } from "@memberjunction/core-entities";
5
+ import { MJUserEntity, MJUserRoleEntity, MJUserApplicationEntity, MJUserApplicationEntityEntity, MJApplicationEntityType, MJApplicationEntityEntityType, UserInfoEngine } from "@memberjunction/core-entities";
6
6
 
7
7
  export class NewUserBase {
8
8
  public async createNewUser(firstName: string, lastName: string, email: string, linkedRecordType: string = 'None', linkedEntityId?: string, linkedEntityRecordId?: string): Promise<MJUserEntity | null> {
@@ -90,10 +90,11 @@ export class NewUserBase {
90
90
  }
91
91
  } else {
92
92
  LogStatus(`No UserApplications configured, using DefaultForNewUser applications for new user ${user.Name}`);
93
- applicationsToCreate = md.Applications
94
- .filter(a => a.DefaultForNewUser)
95
- .sort((a, b) => (a.DefaultSequence ?? 100) - (b.DefaultSequence ?? 100));
96
- LogStatus(`Found ${applicationsToCreate.length} applications with DefaultForNewUser=true`);
93
+ // Bug F2: use the shared selector, which filters to Active apps flagged
94
+ // DefaultForNewUser (in DefaultSequence order). This path previously omitted the
95
+ // Status = 'Active' check, so an inactive default app could be provisioned here.
96
+ applicationsToCreate = UserInfoEngine.GetDefaultApplicationsForNewUser(md);
97
+ LogStatus(`Found ${applicationsToCreate.length} active applications with DefaultForNewUser=true`);
97
98
  }
98
99
 
99
100
  for (const [appIndex, application] of applicationsToCreate.entries()) {
package/src/config.ts CHANGED
@@ -212,6 +212,18 @@ const corsSchema = z.object({
212
212
  maxAge: z.number().default(86400),
213
213
  });
214
214
 
215
+ const startupSchema = z.object({
216
+ /**
217
+ * Startup mode for engine pre-warm: 'full' (all @RegisterForStartup engines run at boot —
218
+ * the right choice for MJAPI) or 'task' (no engines pre-warm; everything lazy-loads on
219
+ * first touch — the right choice for short-lived CLI/script processes). Because
220
+ * mj.config.cjs is shared by every process in a repo, this can be overridden per
221
+ * invocation via the MJ_STARTUP_MODE env var (highest precedence) or programmatically
222
+ * by each entry point. Unset ⇒ each entry point's own default applies.
223
+ */
224
+ mode: z.enum(['full', 'task']).optional(),
225
+ });
226
+
215
227
  const rateLimitSchema = z.object({
216
228
  /** Master switch — when false (default), no rate limiting is applied. */
217
229
  enabled: z.boolean().default(false),
@@ -498,6 +510,7 @@ const configInfoSchema = z.object({
498
510
  feedbackSettings: feedbackSettingsSchema.optional().default({}),
499
511
  cors: corsSchema.optional().default({}),
500
512
  rateLimiting: rateLimitSchema.optional().default({}),
513
+ startup: startupSchema.optional().default({}),
501
514
 
502
515
  apiKey: z.string().optional(),
503
516
  baseUrl: z.string().default('http://localhost'),
@@ -556,6 +569,7 @@ export type FeedbackGithubSettingsConfig = z.infer<typeof feedbackGithubSettings
556
569
  export type FeedbackSettingsConfig = z.infer<typeof feedbackSettingsSchema>;
557
570
  export type CorsConfig = z.infer<typeof corsSchema>;
558
571
  export type RateLimitConfig = z.infer<typeof rateLimitSchema>;
572
+ export type StartupConfig = z.infer<typeof startupSchema>;
559
573
  export type ConfigInfo = z.infer<typeof configInfoSchema>;
560
574
 
561
575
  /**
package/src/context.ts CHANGED
@@ -436,7 +436,10 @@ export const getUserPayload = async (
436
436
  const token = bearerToken.replace('Bearer ', '');
437
437
 
438
438
  if (!token) {
439
- console.warn('No token to validate');
439
+ // No log here — an anonymous/unauthenticated request is routine (a health check, a
440
+ // CORS preflight-adjacent probe, a client that hasn't finished its auth handshake
441
+ // yet), not exceptional. `createUnifiedAuthMiddleware`'s catch block below is the
442
+ // single place that decides how (and whether) to log this — same as `TokenExpiredError`.
440
443
  throw new AuthenticationError('Missing token');
441
444
  }
442
445
 
@@ -512,12 +515,24 @@ export const getUserPayload = async (
512
515
 
513
516
  return { userRecord: sessionUser, email: sessionUser.Email, sessionId };
514
517
  } catch (error) {
515
- console.error(error);
518
+ // An anonymous request presenting no credentials at all (a health check, a CORS
519
+ // preflight-adjacent probe, a client mid-handshake) is routine, same as an expired
520
+ // long-lived session — neither is a bug worth a raw console dump here. Both still
521
+ // propagate (audited below, in the "missing token" case) so the single call site
522
+ // that actually decides final logging/response policy — createUnifiedAuthMiddleware's
523
+ // catch block — sees the real error instead of a generic, indistinguishable one.
524
+ const isMissingToken = error instanceof Error && error.message === 'Missing token';
525
+ if (!(error instanceof TokenExpiredError) && !isMissingToken) {
526
+ console.error(error);
527
+ }
516
528
  if (error instanceof TokenExpiredError) {
517
529
  throw error; // expected for long-lived sessions; not a failure worth auditing
518
530
  }
519
531
  // Best-effort failure audit (token scanning / brute-force signal). Never blocks auth.
520
532
  void auditLoginFailure(bearerToken, error, requestContext, requestDomain);
533
+ if (isMissingToken) {
534
+ throw error; // preserve the original message — see comment above
535
+ }
521
536
  throw new AuthenticationError('Unable to authenticate user');
522
537
  }
523
538
  };
@@ -632,6 +647,17 @@ export function createUnifiedAuthMiddleware(
632
647
  });
633
648
  return;
634
649
  }
650
+ // An unauthenticated request presenting no credentials at all is routine (health
651
+ // checks, CORS preflight-adjacent probes, a client mid-handshake) — same category as
652
+ // TokenExpiredError above, not a bug to surface with a full stack trace. Anything else
653
+ // (a malformed/tampered token, an invalid system API key) is still logged in full below,
654
+ // since those ARE worth a human's attention. Checked by message rather than
655
+ // `instanceof AuthenticationError` — `type-graphql`'s class can resolve to a distinct
656
+ // copy across module/bundler boundaries, where `instanceof` silently never matches.
657
+ if (error instanceof Error && error.message === 'Missing token') {
658
+ res.status(401).json({ error: 'Authentication required' });
659
+ return;
660
+ }
635
661
  console.error('Auth error:', error);
636
662
  res.status(401).json({ error: 'Authentication failed' });
637
663
  }