@memberjunction/server 6.1.0-edge.2 → 6.1.0-edge.4
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/LICENSE +180 -4
- package/dist/agentSessions/SessionManager.d.ts +19 -5
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +27 -9
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.d.ts +31 -0
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -0
- package/dist/auth/AuthProviderCatalogRouter.js +69 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -0
- package/dist/auth/AuthProviderEngine.d.ts +96 -0
- package/dist/auth/AuthProviderEngine.d.ts.map +1 -0
- package/dist/auth/AuthProviderEngine.js +245 -0
- package/dist/auth/AuthProviderEngine.js.map +1 -0
- package/dist/auth/index.d.ts +12 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +57 -13
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.d.ts +64 -1
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +117 -5
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/auth/scopeLimitedPrincipal.d.ts +39 -0
- package/dist/auth/scopeLimitedPrincipal.d.ts.map +1 -0
- package/dist/auth/scopeLimitedPrincipal.js +49 -0
- package/dist/auth/scopeLimitedPrincipal.js.map +1 -0
- package/dist/config.d.ts +89 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +52 -35
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +21 -1
- package/dist/context.js.map +1 -1
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts.map +1 -1
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js +8 -7
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js.map +1 -1
- package/dist/generated/generated.d.ts +619 -1597
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +3341 -17021
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +28 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +57 -8
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +31 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +147 -14
- package/dist/index.js.map +1 -1
- package/dist/integration/CustomColumnPromoter.d.ts +79 -15
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
- package/dist/integration/CustomColumnPromoter.js +472 -97
- package/dist/integration/CustomColumnPromoter.js.map +1 -1
- package/dist/integration/InactiveRowWarnings.d.ts +48 -0
- package/dist/integration/InactiveRowWarnings.d.ts.map +1 -0
- package/dist/integration/InactiveRowWarnings.js +57 -0
- package/dist/integration/InactiveRowWarnings.js.map +1 -0
- package/dist/integration/RSUProgressBridge.d.ts +83 -0
- package/dist/integration/RSUProgressBridge.d.ts.map +1 -0
- package/dist/integration/RSUProgressBridge.js +230 -0
- package/dist/integration/RSUProgressBridge.js.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts +62 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.js +67 -0
- package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
- package/dist/providerConfigUnits.d.ts +20 -0
- package/dist/providerConfigUnits.d.ts.map +1 -0
- package/dist/providerConfigUnits.js +22 -0
- package/dist/providerConfigUnits.js.map +1 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts +4 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +20 -6
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +34 -4
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +218 -28
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/IdentityClaimRedemptionResolver.d.ts +37 -0
- package/dist/resolvers/IdentityClaimRedemptionResolver.d.ts.map +1 -0
- package/dist/resolvers/IdentityClaimRedemptionResolver.js +220 -0
- package/dist/resolvers/IdentityClaimRedemptionResolver.js.map +1 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +108 -3
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +374 -87
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +5 -4
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +29 -14
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +12 -3
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +33 -14
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +3 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +20 -7
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +18 -0
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.js +75 -0
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
- package/dist/resolvers/UserResolver.d.ts +19 -17
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +66 -67
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/currentUserRoles.d.ts +19 -0
- package/dist/resolvers/currentUserRoles.d.ts.map +1 -0
- package/dist/resolvers/currentUserRoles.js +36 -0
- package/dist/resolvers/currentUserRoles.js.map +1 -0
- package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -1
- package/dist/resolvers/meetingRecordingRegistration.js +3 -6
- package/dist/resolvers/meetingRecordingRegistration.js.map +1 -1
- package/dist/rest/MediaAccessKeys.d.ts +17 -0
- package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
- package/dist/rest/MediaAccessKeys.js +42 -1
- package/dist/rest/MediaAccessKeys.js.map +1 -1
- package/dist/rest/MediaStreamHandler.d.ts +3 -2
- package/dist/rest/MediaStreamHandler.d.ts.map +1 -1
- package/dist/rest/MediaStreamHandler.js +83 -2
- package/dist/rest/MediaStreamHandler.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.d.ts +49 -1
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +129 -43
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/UploadTokenManager.d.ts +90 -0
- package/dist/rest/UploadTokenManager.d.ts.map +1 -0
- package/dist/rest/UploadTokenManager.js +156 -0
- package/dist/rest/UploadTokenManager.js.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts +48 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.js +132 -0
- package/dist/services/IntegrationSyncWorkerService.js.map +1 -0
- package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
- package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +21 -1
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphAgentRunner.d.ts +7 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +23 -0
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +12 -1
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +92 -92
- package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +84 -0
- package/src/__tests__/AuthProviderEngine.test.ts +278 -0
- package/src/__tests__/CustomColumnPromoterOnePass.test.ts +387 -0
- package/src/__tests__/EntityCRUDHandler.test.ts +378 -0
- package/src/__tests__/InactiveRowWarnings.test.ts +87 -0
- package/src/__tests__/IntegrationSyncWorkerService.test.ts +275 -0
- package/src/__tests__/OAuthCallbackHandler.openRedirect.test.ts +117 -0
- package/src/__tests__/OAuthCallbackHandler.xss.test.ts +4 -1
- package/src/__tests__/RESTEndpointHandler.test.ts +672 -0
- package/src/__tests__/RSUProgressBridge.test.ts +250 -0
- package/src/__tests__/ReactivateRefreshMessage.test.ts +85 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -2
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +147 -0
- package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
- package/src/__tests__/SessionManager.test.ts +55 -0
- package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
- package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
- package/src/__tests__/config-units.test.ts +74 -0
- package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
- package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
- package/src/__tests__/newUsers.test.ts +726 -0
- package/src/__tests__/uploadTokenManager.test.ts +159 -0
- package/src/agentSessions/SessionManager.ts +27 -9
- package/src/auth/AuthProviderCatalogRouter.ts +76 -0
- package/src/auth/AuthProviderEngine.ts +253 -0
- package/src/auth/index.ts +60 -15
- package/src/auth/initializeProviders.ts +128 -7
- package/src/auth/scopeLimitedPrincipal.ts +50 -0
- package/src/config.ts +55 -36
- package/src/context.ts +26 -2
- package/src/entitySubclasses/MJEntityPermissionEntityServer.server.ts +8 -7
- package/src/generated/generated.ts +2347 -10551
- package/src/generic/ResolverBase.ts +67 -8
- package/src/generic/RunViewResolver.ts +31 -0
- package/src/index.ts +165 -16
- package/src/integration/CustomColumnPromoter.ts +520 -98
- package/src/integration/InactiveRowWarnings.ts +85 -0
- package/src/integration/RSUProgressBridge.ts +248 -0
- package/src/integration/SchemaRefreshLaunch.ts +93 -0
- package/src/providerConfigUnits.ts +21 -0
- package/src/resolvers/AdhocQueryResolver.ts +21 -6
- package/src/resolvers/FileResolver.ts +200 -40
- package/src/resolvers/IdentityClaimRedemptionResolver.ts +184 -0
- package/src/resolvers/IntegrationDiscoveryResolver.ts +390 -71
- package/src/resolvers/QueryResolver.ts +25 -11
- package/src/resolvers/QuerySystemUserResolver.ts +28 -13
- package/src/resolvers/RunAIAgentResolver.ts +21 -6
- package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
- package/src/resolvers/UserResolver.ts +99 -60
- package/src/resolvers/currentUserRoles.ts +47 -0
- package/src/resolvers/meetingRecordingRegistration.ts +3 -7
- package/src/rest/MediaAccessKeys.ts +47 -1
- package/src/rest/MediaStreamHandler.ts +98 -3
- package/src/rest/OAuthCallbackHandler.ts +149 -41
- package/src/rest/UploadTokenManager.ts +211 -0
- package/src/services/IntegrationSyncWorkerService.ts +141 -0
- package/src/services/StartTaskGraphDispatcher.ts +21 -1
- package/src/services/TaskGraphAgentRunner.ts +23 -0
- package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
- package/src/types.ts +14 -0
|
@@ -1,16 +1,45 @@
|
|
|
1
1
|
import { configInfo } from '../config.js';
|
|
2
|
-
import { AuthProviderConfig, LogError } from '@memberjunction/core';
|
|
2
|
+
import { AuthProviderConfig, LogError, LogStatus, LogStatusEx, UserInfo, type IMetadataProvider } from '@memberjunction/core';
|
|
3
3
|
import { AuthProviderFactory } from '@memberjunction/auth-providers';
|
|
4
|
+
import { AuthProviderEngine } from './AuthProviderEngine.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* Initialize authentication providers from configuration
|
|
7
|
+
* Initialize authentication providers from configuration.
|
|
8
|
+
*
|
|
9
|
+
* Registers everything declared in `mj.config.cjs`'s `authProviders` array. This remains the
|
|
10
|
+
* baseline path and runs on every boot: it needs no database, so it is what lets the server
|
|
11
|
+
* authenticate even when metadata is unreachable.
|
|
12
|
+
*
|
|
13
|
+
* Providers defined in the `MJ: Authentication Providers` catalog are layered on top by
|
|
14
|
+
* {@link initializeAuthProvidersFromMetadata}, which runs later in startup once a database
|
|
15
|
+
* connection and a system user exist.
|
|
7
16
|
*/
|
|
8
17
|
export function initializeAuthProviders(): void {
|
|
9
18
|
const factory = AuthProviderFactory.Instance;
|
|
10
|
-
|
|
19
|
+
|
|
11
20
|
// Clear any existing providers
|
|
12
21
|
factory.clear();
|
|
13
22
|
|
|
23
|
+
// Environment-derived providers are DEFAULTS: each provider class maps its own variables via
|
|
24
|
+
// the ConfigFromEnvironment hook, and they apply only when the config file declares no
|
|
25
|
+
// providers of its own. That mirrors the previous behaviour exactly — the env block used to sit
|
|
26
|
+
// in DEFAULT_SERVER_CONFIG, and mergeConfigs replaces (rather than concatenates) arrays, so an
|
|
27
|
+
// explicit `authProviders` in mj.config.cjs has always suppressed the env-derived set.
|
|
28
|
+
//
|
|
29
|
+
// Written back into configInfo.authProviders rather than kept local because that array is the
|
|
30
|
+
// live registry other code reads and appends to (registerMagicLinkAuthProvider pushes into it so
|
|
31
|
+
// a later re-initialize keeps magic-link).
|
|
32
|
+
if (!configInfo.authProviders || configInfo.authProviders.length === 0) {
|
|
33
|
+
const discovered = AuthProviderFactory.DiscoverFromEnvironment();
|
|
34
|
+
if (discovered.length > 0) {
|
|
35
|
+
configInfo.authProviders = discovered;
|
|
36
|
+
LogStatusEx({
|
|
37
|
+
message: `[Auth] Configured ${discovered.length} provider(s) from environment variables: ${discovered.map((p) => p.name).join(', ')}`,
|
|
38
|
+
verboseOnly: true
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
14
43
|
// Initialize providers from authProviders config
|
|
15
44
|
if (configInfo.authProviders && configInfo.authProviders.length > 0) {
|
|
16
45
|
for (const providerConfig of configInfo.authProviders) {
|
|
@@ -26,8 +55,100 @@ export function initializeAuthProviders(): void {
|
|
|
26
55
|
}
|
|
27
56
|
}
|
|
28
57
|
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
58
|
+
// NOTE: the "no providers configured" check deliberately does NOT live here any more — see
|
|
59
|
+
// validateAuthProvidersRegistered(). A metadata-only deployment legitimately has an empty
|
|
60
|
+
// authProviders array at this point, and erroring here would cry wolf on every boot.
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Layers the metadata provider catalog on top of the config-declared providers.
|
|
65
|
+
*
|
|
66
|
+
* Called after the data provider and system user are available. Metadata rows are registered in
|
|
67
|
+
* addition to — not instead of — the config-declared ones: a deployment mid-migration may
|
|
68
|
+
* legitimately have both, and a name collision resolves to the metadata row because it registers
|
|
69
|
+
* last and `AuthProviderFactory.register` keys by provider name.
|
|
70
|
+
*
|
|
71
|
+
* Never throws. An unreachable or empty catalog leaves the config-declared providers exactly as
|
|
72
|
+
* they were, which is what makes adopting this feature a no-op for existing deployments and keeps
|
|
73
|
+
* a metadata problem from turning into a lockout.
|
|
74
|
+
*
|
|
75
|
+
* @returns the number of providers registered from metadata.
|
|
76
|
+
*/
|
|
77
|
+
export async function initializeAuthProvidersFromMetadata(contextUser?: UserInfo, provider?: IMetadataProvider): Promise<number> {
|
|
78
|
+
try {
|
|
79
|
+
await AuthProviderEngine.Instance.Config(false, contextUser, provider);
|
|
80
|
+
const count = await AuthProviderEngine.Instance.RegisterAll(contextUser);
|
|
81
|
+
if (count > 0) {
|
|
82
|
+
LogStatus(`[Auth] ${count} authentication provider(s) registered from metadata.`);
|
|
83
|
+
}
|
|
84
|
+
return count;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
LogError(`[Auth] Could not load the authentication provider catalog from metadata (${message}). Continuing with providers from mj.config.cjs.`);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Rebuilds the provider registry from scratch: config-declared providers first, then the
|
|
94
|
+
* metadata catalog layered on top — the same two-source order as startup.
|
|
95
|
+
*
|
|
96
|
+
* This is THE entry point for reacting to a catalog edit at runtime (an admin page's Save
|
|
97
|
+
* button, a future invalidation hook). A full rebuild is required because the factory caches
|
|
98
|
+
* issuer→provider resolutions, so an edited row would otherwise keep serving its old
|
|
99
|
+
* configuration until the process restarted. It lives here rather than on `AuthProviderEngine`
|
|
100
|
+
* because a bare `factory.clear()` + metadata re-registration would silently drop everything
|
|
101
|
+
* the config path registered — magic-link included — and no caller should have to know to put
|
|
102
|
+
* those back. `initializeAuthProviders()` clears the factory itself, so no separate clear step
|
|
103
|
+
* exists for a caller to forget.
|
|
104
|
+
*
|
|
105
|
+
* **Failure policy: abort-before-mutate.** Everything that can throw — the catalog reload and
|
|
106
|
+
* the permission probe on its result — runs BEFORE the factory is touched, so a failed refresh
|
|
107
|
+
* leaves the registry serving the previous (stale but complete) provider set and surfaces the
|
|
108
|
+
* error to the caller. This is deliberately the opposite of `initializeAuthProvidersFromMetadata`,
|
|
109
|
+
* which swallows metadata failures: at boot there is no previous registry to preserve and a
|
|
110
|
+
* metadata problem must not become a lockout, while at refresh time there is one and the caller
|
|
111
|
+
* (an admin UI) wants the error. That difference in failure policy is why this function does not
|
|
112
|
+
* simply delegate to its sibling.
|
|
113
|
+
*
|
|
114
|
+
* Known limitation, acceptable for an admin-triggered operation: metadata providers are absent
|
|
115
|
+
* from the registry for the duration of `RegisterAll`'s per-row config builds (config-declared
|
|
116
|
+
* providers are restored synchronously). A request bearing a metadata-declared IdP's token in
|
|
117
|
+
* that window is rejected. If a caller ever needs an atomic swap, build the configs first and
|
|
118
|
+
* register them in one synchronous pass.
|
|
119
|
+
*
|
|
120
|
+
* @returns the number of providers registered from metadata after the refresh.
|
|
121
|
+
*/
|
|
122
|
+
export async function refreshAuthProviders(contextUser?: UserInfo, provider?: IMetadataProvider): Promise<number> {
|
|
123
|
+
await AuthProviderEngine.Instance.Config(true, contextUser, provider);
|
|
124
|
+
|
|
125
|
+
// Probe the reloaded catalog BEFORE clearing anything: reading Providers throws
|
|
126
|
+
// PermissionConstrainedError when the load was skipped for permission reasons, and that
|
|
127
|
+
// failure must abort the refresh while the registry is still intact.
|
|
128
|
+
const pendingRows = AuthProviderEngine.Instance.Providers.length;
|
|
129
|
+
|
|
130
|
+
initializeAuthProviders();
|
|
131
|
+
const count = await AuthProviderEngine.Instance.RegisterAll(contextUser);
|
|
132
|
+
LogStatus(
|
|
133
|
+
`[Auth] Provider registry refreshed — ${count} of ${pendingRows} metadata provider(s) registered on top of the config-declared set.`
|
|
134
|
+
);
|
|
135
|
+
return count;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Reports whether any provider is registered, from either source, and logs a diagnostic when none
|
|
140
|
+
* is.
|
|
141
|
+
*
|
|
142
|
+
* Split out from {@link initializeAuthProviders} because "no providers" is only genuinely wrong
|
|
143
|
+
* once BOTH sources have had their turn.
|
|
144
|
+
*/
|
|
145
|
+
export function validateAuthProvidersRegistered(): boolean {
|
|
146
|
+
const hasProviders = AuthProviderFactory.Instance.hasProviders();
|
|
147
|
+
if (!hasProviders) {
|
|
148
|
+
LogError(
|
|
149
|
+
'No authentication providers are configured. Define them in the "MJ: Authentication Providers" metadata entity, ' +
|
|
150
|
+
'or in the authProviders array in mj.config.cjs.'
|
|
151
|
+
);
|
|
32
152
|
}
|
|
33
|
-
|
|
153
|
+
return hasProviders;
|
|
154
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { UserInfo } from '@memberjunction/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* True when a session's read authority is deliberately narrower than the role grant it
|
|
5
|
+
* authenticates with — i.e. the principal is confined by per-session scope rather than by
|
|
6
|
+
* its roles alone.
|
|
7
|
+
*
|
|
8
|
+
* Two such principals exist today, both from the magic-link surface:
|
|
9
|
+
*
|
|
10
|
+
* - **Anonymous magic-link guests** (`IsMagicLinkAnonymous`) authenticate as the *shared*
|
|
11
|
+
* Anonymous principal, so their role grant is common to every guest. What separates one
|
|
12
|
+
* guest from another is not the role — it is the per-session scope.
|
|
13
|
+
* - **Resource-scoped magic-link sessions** (`MagicLinkScope`) hold a normal role, pinned to
|
|
14
|
+
* one shared resource (and its FK-reachable dependents) for the life of the session.
|
|
15
|
+
*
|
|
16
|
+
* In both cases the confinement is expressed as row-level-security filter tokens
|
|
17
|
+
* (`{{ScopeResourceID}}` / `{{ScopeResourceType}}`) that the entity-read path substitutes.
|
|
18
|
+
* That means the confinement only exists on code paths that go **through** entity
|
|
19
|
+
* permissions and RLS — `RunView`, `BaseEntity`, the generated resolvers. Any path that
|
|
20
|
+
* reaches the database *around* those layers (raw SQL on the read-only pool, for instance)
|
|
21
|
+
* sees no scope at all, so such a path must refuse these principals outright rather than
|
|
22
|
+
* assume a filter it never applies.
|
|
23
|
+
*
|
|
24
|
+
* Callers should treat a `true` result as "not permitted here", never as "apply a narrower
|
|
25
|
+
* filter" — a caller that could narrow correctly would not need this predicate.
|
|
26
|
+
*
|
|
27
|
+
* @param user the principal to classify. A missing principal returns `true`: an
|
|
28
|
+
* unidentifiable session is the one case where the scope certainly cannot be
|
|
29
|
+
* established, so it fails closed.
|
|
30
|
+
* @returns `true` when the principal is scope-limited (or unidentifiable), else `false`.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* if (IsScopeLimitedPrincipal(context.userPayload?.userRecord)) {
|
|
35
|
+
* return this.buildErrorResult('Not permitted for scope-limited sessions.');
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function IsScopeLimitedPrincipal(user: UserInfo | undefined | null): boolean {
|
|
40
|
+
if (!user) {
|
|
41
|
+
return true; // no identifiable principal — fail closed
|
|
42
|
+
}
|
|
43
|
+
if (user.IsMagicLinkAnonymous) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
const scope = user.MagicLinkScope;
|
|
47
|
+
// An empty scope object carries no confinement; only a populated one does. (Truthiness
|
|
48
|
+
// of the object alone would classify `{}` as scoped and lock out a session that is not.)
|
|
49
|
+
return !!(scope?.ResourceID || scope?.ResourceType);
|
|
50
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -7,7 +7,19 @@ const explorer = cosmiconfigSync('mj', { searchStrategy: 'global' });
|
|
|
7
7
|
|
|
8
8
|
const userHandlingInfoSchema = z.object({
|
|
9
9
|
autoCreateNewUsers: z.boolean().optional().default(false),
|
|
10
|
+
/** When true, auto-provisioning is restricted to the domains in `newUserAuthorizedDomains`. */
|
|
10
11
|
newUserLimitedToAuthorizedDomains: z.boolean().optional().default(false),
|
|
12
|
+
/**
|
|
13
|
+
* Authorized **email domains** for auto-provisioned users — e.g. `['example.com', '*.example.org']`.
|
|
14
|
+
*
|
|
15
|
+
* These are matched against the domain of the email address in the verified identity token, NOT
|
|
16
|
+
* against the browser `Origin` / frontend hostname. If you are upgrading from a build that
|
|
17
|
+
* compared these to the request origin, replace any frontend hostnames here (`app.example.com`,
|
|
18
|
+
* `localhost`) with the email domains your users actually sign in with.
|
|
19
|
+
*
|
|
20
|
+
* `*` wildcards are supported and match in full: `*.example.com` matches `mail.example.com` but
|
|
21
|
+
* NOT `example.com` — list both if you need both.
|
|
22
|
+
*/
|
|
11
23
|
newUserAuthorizedDomains: z.array(z.string()).optional().default([]),
|
|
12
24
|
newUserRoles: z.array(z.string()).optional().default([]),
|
|
13
25
|
updateCacheWhenNotFound: z.boolean().optional().default(false),
|
|
@@ -131,6 +143,23 @@ const scheduledJobsSchema = z.object({
|
|
|
131
143
|
staleLockCleanupInterval: z.number().optional().default(300000), // 5 minutes in ms
|
|
132
144
|
});
|
|
133
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Integration sync worker (PR 1 item 8). When enabled, this process polls
|
|
148
|
+
* `CompanyIntegrationRun` for `Status='Queued'` rows, atomically claims them, executes
|
|
149
|
+
* the sync, and releases. The claim sproc is the mutual exclusion, so any number of
|
|
150
|
+
* processes may run a worker against the same database.
|
|
151
|
+
*/
|
|
152
|
+
const integrationSyncWorkerSchema = z.object({
|
|
153
|
+
/** Master switch — off by default so existing deployments keep running syncs inline. */
|
|
154
|
+
enabled: z.boolean().optional().default(false),
|
|
155
|
+
/** Email of the user the worker executes syncs as. */
|
|
156
|
+
systemUserEmail: z.string().optional().default('system@memberjunction.org'),
|
|
157
|
+
/** How often to poll the queue, in ms. */
|
|
158
|
+
pollingIntervalMs: z.number().optional().default(15000),
|
|
159
|
+
/** Maximum runs this worker executes concurrently. */
|
|
160
|
+
maxConcurrentRuns: z.number().optional().default(3),
|
|
161
|
+
});
|
|
162
|
+
|
|
134
163
|
const queryDialectSchema = z.object({
|
|
135
164
|
/** When true, saving a Query entity auto-generates QuerySQL entries for configured target dialects */
|
|
136
165
|
autoConvertOnSave: zodBooleanWithTransforms().default(false),
|
|
@@ -501,6 +530,7 @@ const configInfoSchema = z.object({
|
|
|
501
530
|
authProviders: z.array(authProviderSchema).optional(),
|
|
502
531
|
componentRegistries: z.array(componentRegistrySchema).optional(),
|
|
503
532
|
scheduledJobs: scheduledJobsSchema.optional().default({}),
|
|
533
|
+
integrationSyncWorker: integrationSyncWorkerSchema.optional().default({}),
|
|
504
534
|
telemetry: telemetrySchema.optional().default({}),
|
|
505
535
|
queryDialects: queryDialectSchema.optional().default({}),
|
|
506
536
|
multiTenancy: multiTenancySchema.optional().default({}),
|
|
@@ -559,6 +589,7 @@ export type SqlLoggingInfo = z.infer<typeof sqlLoggingSchema>;
|
|
|
559
589
|
export type AuthProviderConfig = z.infer<typeof authProviderSchema>;
|
|
560
590
|
export type ComponentRegistryConfig = z.infer<typeof componentRegistrySchema>;
|
|
561
591
|
export type ScheduledJobsConfig = z.infer<typeof scheduledJobsSchema>;
|
|
592
|
+
export type IntegrationSyncWorkerConfig = z.infer<typeof integrationSyncWorkerSchema>;
|
|
562
593
|
export type TelemetryConfig = z.infer<typeof telemetrySchema>;
|
|
563
594
|
export type QueryDialectConfig = z.infer<typeof queryDialectSchema>;
|
|
564
595
|
export type MultiTenancyConfig = z.infer<typeof multiTenancySchema>;
|
|
@@ -671,6 +702,14 @@ export const DEFAULT_SERVER_CONFIG: Partial<ConfigInfo> = {
|
|
|
671
702
|
staleLockCleanupInterval: 300000
|
|
672
703
|
},
|
|
673
704
|
|
|
705
|
+
// Integration sync worker defaults (off — syncs run inline unless a worker is enabled)
|
|
706
|
+
integrationSyncWorker: {
|
|
707
|
+
enabled: false,
|
|
708
|
+
systemUserEmail: 'not.set@nowhere.com',
|
|
709
|
+
pollingIntervalMs: 15000,
|
|
710
|
+
maxConcurrentRuns: 3
|
|
711
|
+
},
|
|
712
|
+
|
|
674
713
|
// Telemetry defaults
|
|
675
714
|
telemetry: {
|
|
676
715
|
enabled: true,
|
|
@@ -696,42 +735,22 @@ export const DEFAULT_SERVER_CONFIG: Partial<ConfigInfo> = {
|
|
|
696
735
|
},
|
|
697
736
|
},
|
|
698
737
|
|
|
699
|
-
// Auth providers
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
type: 'auth0',
|
|
716
|
-
issuer: `https://${process.env.AUTH0_DOMAIN}/`,
|
|
717
|
-
audience: process.env.AUTH0_CLIENT_ID,
|
|
718
|
-
jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`,
|
|
719
|
-
clientId: process.env.AUTH0_CLIENT_ID,
|
|
720
|
-
clientSecret: process.env.AUTH0_CLIENT_SECRET,
|
|
721
|
-
domain: process.env.AUTH0_DOMAIN
|
|
722
|
-
} : null,
|
|
723
|
-
// AWS Cognito
|
|
724
|
-
process.env.COGNITO_USER_POOL_ID && process.env.COGNITO_CLIENT_ID && process.env.AWS_REGION ? {
|
|
725
|
-
name: 'cognito',
|
|
726
|
-
type: 'cognito',
|
|
727
|
-
issuer: `https://cognito-idp.${process.env.AWS_REGION}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID}`,
|
|
728
|
-
audience: process.env.COGNITO_CLIENT_ID,
|
|
729
|
-
jwksUri: `https://cognito-idp.${process.env.AWS_REGION}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID}/.well-known/jwks.json`,
|
|
730
|
-
clientId: process.env.COGNITO_CLIENT_ID,
|
|
731
|
-
region: process.env.AWS_REGION,
|
|
732
|
-
userPoolId: process.env.COGNITO_USER_POOL_ID
|
|
733
|
-
} : null,
|
|
734
|
-
].filter(Boolean),
|
|
738
|
+
// Auth providers.
|
|
739
|
+
//
|
|
740
|
+
// Empty by design. This used to be a hard-coded block that enumerated Entra / Auth0 / Cognito
|
|
741
|
+
// inline and built each config from its environment variables. That made env-var configuration
|
|
742
|
+
// a closed domain: a third-party provider could register a driver class and take a metadata row
|
|
743
|
+
// or an explicit entry here, but it could never offer the "set two variables and you're done"
|
|
744
|
+
// experience, because the enumeration lived in core.
|
|
745
|
+
//
|
|
746
|
+
// Each provider class now owns its own mapping via the optional static
|
|
747
|
+
// `ConfigFromEnvironment` (see IEnvironmentConfigurableProvider in @memberjunction/auth-providers),
|
|
748
|
+
// and `initializeAuthProviders()` collects them through the ClassFactory registry.
|
|
749
|
+
//
|
|
750
|
+
// Discovery cannot happen here: this literal is evaluated when config.ts is imported, which is
|
|
751
|
+
// BEFORE @memberjunction/auth-providers loads and the driver classes register. It is deferred to
|
|
752
|
+
// registration time, where the registry is populated.
|
|
753
|
+
authProviders: [],
|
|
735
754
|
};
|
|
736
755
|
|
|
737
756
|
/**
|
package/src/context.ts
CHANGED
|
@@ -18,9 +18,10 @@ import { GetReadOnlyDataSource, GetReadWriteDataSource } from './util.js';
|
|
|
18
18
|
import { v4 as uuidv4 } from 'uuid';
|
|
19
19
|
import e from 'express';
|
|
20
20
|
import type { RequestHandler, Request, Response, NextFunction } from 'express';
|
|
21
|
-
import { DatabaseProviderBase, UserInfo, type MagicLinkScope, type ReturningVisitorContext, type WidgetGuestContext } from '@memberjunction/core';
|
|
21
|
+
import { DatabaseProviderBase, UserInfo, type IMetadataProvider, type MagicLinkScope, type ReturningVisitorContext, type WidgetGuestContext } from '@memberjunction/core';
|
|
22
22
|
import { SQLServerDataProvider, SQLServerProviderConfigData } from '@memberjunction/sqlserver-dataprovider';
|
|
23
23
|
import { Metadata } from '@memberjunction/core';
|
|
24
|
+
import { IdentityClaimEngineServer } from '@memberjunction/core-entities-server';
|
|
24
25
|
import { UUIDsEqual } from '@memberjunction/global';
|
|
25
26
|
import { UserCache, resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provider';
|
|
26
27
|
import { GetAPIKeyEngine } from '@memberjunction/api-keys';
|
|
@@ -554,7 +555,30 @@ export const getUserPayload = async (
|
|
|
554
555
|
});
|
|
555
556
|
}
|
|
556
557
|
|
|
557
|
-
|
|
558
|
+
// The IdP's OIDC email_verified assertion, read off the JWKS-verified payload. Standard
|
|
559
|
+
// claim name across Auth0/Okta/Cognito/WorkOS; MSAL and others simply omit it (undefined).
|
|
560
|
+
const emailVerified = typeof payload.email_verified === 'boolean' ? payload.email_verified : undefined;
|
|
561
|
+
|
|
562
|
+
// Automatic claim-on-login — once per issued token (deduped, own prefix so it can't eat the
|
|
563
|
+
// audit's first-seen slot), fire-and-forget so it never adds latency. Discovers pending
|
|
564
|
+
// MJ: Identity Claims addressed to this user's email and redeems each through the FULL
|
|
565
|
+
// RedeemClaim gate (email/verification rules, atomic CAS, driver error handling), passing
|
|
566
|
+
// the IdP's email_verified assertion so an unverified email can never auto-claim.
|
|
567
|
+
// Anonymous magic-link guests are skipped: their principal is synthetic and per-session.
|
|
568
|
+
if (sessionUser.Email && !sessionUser.IsMagicLinkAnonymous && markSessionAuditSeen(sessionAuditKey('claims', payload))) {
|
|
569
|
+
// Pre-context auth path: per-request providers do not exist yet here, so the global is the
|
|
570
|
+
// only provider available — same posture as every other pre-context call on this path.
|
|
571
|
+
// The allowlist marker has to sit on the reference line itself; MultiProviderCompliance
|
|
572
|
+
// matches per-line, so a marker on its own line above is invisible to the scanner.
|
|
573
|
+
const claimProvider = Metadata.Provider as unknown as IMetadataProvider; // global-provider-ok: pre-context auth path
|
|
574
|
+
if (claimProvider) {
|
|
575
|
+
void IdentityClaimEngineServer.Instance.AutoClaimForUser(sessionUser, claimProvider, { EmailVerified: emailVerified }).catch(
|
|
576
|
+
(err) => console.warn(`Auto-claim on login failed for ${sessionUser.Email}: ${err instanceof Error ? err.message : err}`),
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return { userRecord: sessionUser, email: sessionUser.Email, sessionId, emailVerified };
|
|
558
582
|
} catch (error) {
|
|
559
583
|
// An anonymous request presenting no credentials at all (a health check, a CORS
|
|
560
584
|
// preflight-adjacent probe, a client mid-handshake) is routine, same as an expired
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RegisterClass } from '@memberjunction/global';
|
|
2
2
|
import { BaseEntity, EntityDeleteOptions, EntitySaveOptions } from '@memberjunction/core';
|
|
3
3
|
import { MJEntityPermissionEntity } from '@memberjunction/core-entities';
|
|
4
|
-
import
|
|
4
|
+
import { HttpPost } from '@memberjunction/network-utils';
|
|
5
5
|
import { ___codeGenAPIPort, ___codeGenAPISubmissionDelay, ___codeGenAPIURL } from '../config.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -59,23 +59,24 @@ export class MJEntityPermissionEntityServer extends MJEntityPermissionEntity {
|
|
|
59
59
|
protected static async SubmitQueue(): Promise<void> {
|
|
60
60
|
this._lastModifiedTime = null;
|
|
61
61
|
|
|
62
|
-
// now,
|
|
62
|
+
// now, submit the queue to the API server
|
|
63
63
|
// Check if there's anything to submit
|
|
64
64
|
if (this._entityIDQueue.length > 0) {
|
|
65
65
|
try {
|
|
66
|
-
//
|
|
67
|
-
|
|
66
|
+
// The submission URL is our OWN CodeGen API (typically localhost) — deliberately NOT
|
|
67
|
+
// routed through the SSRF guard, which would block an internal address by design.
|
|
68
|
+
const response = await HttpPost<{ status?: string; errorMessage?: string }>(this.getSubmissionURL(), {
|
|
68
69
|
entityIDArray: this._entityIDQueue,
|
|
69
70
|
});
|
|
70
71
|
|
|
71
|
-
// Check the
|
|
72
|
-
if (response.
|
|
72
|
+
// Check the HTTP status implicitly and API response explicitly
|
|
73
|
+
if (response.Status === 200 && response.Data?.status === 'ok') {
|
|
73
74
|
console.log('Queue submitted successfully.');
|
|
74
75
|
// now, clear the queue and timer
|
|
75
76
|
this.ClearQueue();
|
|
76
77
|
} else {
|
|
77
78
|
// Handle API indicating a failure
|
|
78
|
-
console.error('Failed to submit queue:', response.
|
|
79
|
+
console.error('Failed to submit queue:', response.Data?.errorMessage || 'Unknown error');
|
|
79
80
|
}
|
|
80
81
|
} catch (error) {
|
|
81
82
|
// Handle errors here
|