@memberjunction/server 5.47.0 → 5.49.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/README.md +9 -0
- package/dist/auth/magicLink/MagicLinkKeys.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkKeys.js +11 -4
- package/dist/auth/magicLink/MagicLinkKeys.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.js +2 -2
- package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +4 -0
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/config.d.ts +40 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +65 -3
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +122 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +629 -0
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +4 -2
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +1 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +12 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +99 -11
- package/dist/index.js.map +1 -1
- package/dist/integration/CustomColumnPromoter.d.ts +15 -2
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
- package/dist/integration/CustomColumnPromoter.js +113 -11
- package/dist/integration/CustomColumnPromoter.js.map +1 -1
- package/dist/integration/EntityMapLifecycle.d.ts +66 -0
- package/dist/integration/EntityMapLifecycle.d.ts.map +1 -0
- package/dist/integration/EntityMapLifecycle.js +189 -0
- package/dist/integration/EntityMapLifecycle.js.map +1 -0
- package/dist/realtimeProxy/RealtimeProxyServer.d.ts +58 -0
- package/dist/realtimeProxy/RealtimeProxyServer.d.ts.map +1 -0
- package/dist/realtimeProxy/RealtimeProxyServer.js +246 -0
- package/dist/realtimeProxy/RealtimeProxyServer.js.map +1 -0
- package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -1
- package/dist/realtimeWidget/WidgetRouter.js +6 -2
- package/dist/realtimeWidget/WidgetRouter.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts +17 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +81 -25
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts +16 -0
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +26 -1
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +53 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +522 -60
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +0 -6
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/RSUResolver.d.ts +10 -0
- package/dist/resolvers/RSUResolver.d.ts.map +1 -1
- package/dist/resolvers/RSUResolver.js +28 -0
- package/dist/resolvers/RSUResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +9 -6
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +135 -132
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.d.ts +1 -0
- package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.js +5 -0
- package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts +8 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.js +29 -1
- package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
- package/dist/resolvers/adhoc-query-helpers.d.ts +27 -0
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +1 -0
- package/dist/resolvers/adhoc-query-helpers.js +42 -0
- package/dist/resolvers/adhoc-query-helpers.js.map +1 -0
- package/dist/rest/OAuthCallbackHandler.d.ts +9 -0
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +21 -4
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/telephony/media-upgrade-router.d.ts +14 -0
- package/dist/telephony/media-upgrade-router.d.ts.map +1 -1
- package/dist/telephony/media-upgrade-router.js +17 -1
- package/dist/telephony/media-upgrade-router.js.map +1 -1
- package/package.json +88 -88
- package/src/__tests__/AdhocQueryResolver.pagination.test.ts +114 -0
- package/src/__tests__/EntityMapLifecycle.dagWarnings.test.ts +91 -0
- package/src/__tests__/OAuthCallbackHandler.xss.test.ts +83 -0
- package/src/__tests__/RealtimeProxyServer.test.ts +122 -0
- package/src/__tests__/adhoc-query-helpers.test.ts +50 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +106 -0
- package/src/__tests__/media-upgrade-router.test.ts +43 -0
- package/src/__tests__/unifiedAuth.test.ts +18 -3
- package/src/auth/magicLink/MagicLinkKeys.ts +12 -6
- package/src/auth/magicLink/MagicLinkRouter.ts +2 -2
- package/src/auth/magicLink/MagicLinkService.ts +4 -0
- package/src/config.ts +14 -0
- package/src/context.ts +74 -3
- package/src/generated/generated.ts +446 -1
- package/src/generic/ResolverBase.ts +5 -2
- package/src/generic/RunViewResolver.ts +11 -0
- package/src/index.ts +100 -11
- package/src/integration/CustomColumnPromoter.ts +123 -6
- package/src/integration/EntityMapLifecycle.ts +221 -0
- package/src/realtimeProxy/RealtimeProxyServer.ts +265 -0
- package/src/realtimeWidget/WidgetRouter.ts +6 -2
- package/src/resolvers/AdhocQueryResolver.ts +97 -30
- package/src/resolvers/GetDataResolver.ts +30 -1
- package/src/resolvers/IntegrationDiscoveryResolver.ts +514 -60
- package/src/resolvers/QueryResolver.ts +1 -7
- package/src/resolvers/RSUResolver.ts +28 -0
- package/src/resolvers/RunAIAgentResolver.ts +74 -70
- package/src/resolvers/TestQuerySQLResolver.ts +4 -0
- package/src/resolvers/TransactionGroupResolver.ts +36 -2
- package/src/resolvers/__tests__/GetDataResolver.tokens.test.ts +138 -0
- package/src/resolvers/adhoc-query-helpers.ts +50 -0
- package/src/rest/OAuthCallbackHandler.ts +22 -4
- package/src/telephony/media-upgrade-router.ts +18 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { BaseSingleton } from '@memberjunction/global';
|
|
13
|
-
import {
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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) {
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -705,7 +731,12 @@ async function createPerRequestProviders(
|
|
|
705
731
|
{ provider: p, type: 'Read-Write' }
|
|
706
732
|
];
|
|
707
733
|
|
|
708
|
-
if (
|
|
734
|
+
if (isPostgres) {
|
|
735
|
+
const rp = await tryCreateReadOnlyPostgresProvider();
|
|
736
|
+
if (rp) {
|
|
737
|
+
providers.push({ provider: rp, type: 'Read-Only' });
|
|
738
|
+
}
|
|
739
|
+
} else {
|
|
709
740
|
const rp = await tryCreateReadOnlyProvider(dataSources);
|
|
710
741
|
if (rp) {
|
|
711
742
|
providers.push({ provider: rp, type: 'Read-Only' });
|
|
@@ -748,6 +779,46 @@ async function createPostgresProvider(): Promise<DatabaseProviderBase> {
|
|
|
748
779
|
return pgProvider;
|
|
749
780
|
}
|
|
750
781
|
|
|
782
|
+
/**
|
|
783
|
+
* Attempts to create a read-only PostgreSQL provider using DB_READ_ONLY_USERNAME/PASSWORD.
|
|
784
|
+
* Shares the connection pool from the primary provider. Returns null if no read-only credentials are configured.
|
|
785
|
+
*/
|
|
786
|
+
async function tryCreateReadOnlyPostgresProvider(): Promise<DatabaseProviderBase | null> {
|
|
787
|
+
const roUser = process.env.PG_READ_ONLY_USERNAME || process.env.DB_READ_ONLY_USERNAME;
|
|
788
|
+
const roPass = process.env.PG_READ_ONLY_PASSWORD || process.env.DB_READ_ONLY_PASSWORD;
|
|
789
|
+
if (!roUser || !roPass) {
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
try {
|
|
794
|
+
const { PostgreSQLDataProvider, PostgreSQLProviderConfigData } = await import('@memberjunction/postgresql-dataprovider');
|
|
795
|
+
const pgHost = process.env.PG_HOST || process.env.DB_HOST || 'localhost';
|
|
796
|
+
const pgPort = parseInt(process.env.PG_PORT || process.env.DB_PORT || '5432', 10);
|
|
797
|
+
const pgDatabase = process.env.PG_DATABASE || process.env.DB_DATABASE || '';
|
|
798
|
+
|
|
799
|
+
const roProvider = new PostgreSQLDataProvider();
|
|
800
|
+
const roConfig = new PostgreSQLProviderConfigData(
|
|
801
|
+
{ Host: pgHost, Port: pgPort, Database: pgDatabase, User: roUser, Password: roPass },
|
|
802
|
+
mj_core_schema,
|
|
803
|
+
0,
|
|
804
|
+
undefined,
|
|
805
|
+
undefined,
|
|
806
|
+
false,
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
const primaryProvider = Metadata.Provider as unknown as { DatabaseConnection?: import('pg').Pool }; // global-provider-ok: bootstrap (share primary provider's PG pool with the read-only provider)
|
|
810
|
+
if (primaryProvider?.DatabaseConnection) {
|
|
811
|
+
await roProvider.ConfigWithSharedPool(roConfig, primaryProvider.DatabaseConnection);
|
|
812
|
+
} else {
|
|
813
|
+
await roProvider.Config(roConfig);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return roProvider;
|
|
817
|
+
} catch (_err) {
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
751
822
|
/**
|
|
752
823
|
* Attempts to create a read-only SQL Server provider.
|
|
753
824
|
* Returns null if no read-only data source is available.
|