@memberjunction/server 6.2.0-edge.4 → 6.2.0-edge.5
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 +20 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/agentSessions/SessionJanitor.d.ts +91 -30
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +311 -81
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts +5 -1
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +13 -1
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/apiRoot.d.ts +32 -0
- package/dist/apiRoot.d.ts.map +1 -0
- package/dist/apiRoot.js +67 -0
- package/dist/apiRoot.js.map +1 -0
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.js +25 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
- package/dist/auth/LoginScreenEngine.d.ts +22 -0
- package/dist/auth/LoginScreenEngine.d.ts.map +1 -0
- package/dist/auth/LoginScreenEngine.js +57 -0
- package/dist/auth/LoginScreenEngine.js.map +1 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.js +3 -2
- package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
- package/dist/config.d.ts +136 -22
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -4
- package/dist/config.js.map +1 -1
- package/dist/coreRoutes.d.ts +29 -0
- package/dist/coreRoutes.d.ts.map +1 -0
- package/dist/coreRoutes.js +142 -0
- package/dist/coreRoutes.js.map +1 -0
- package/dist/generated/graphql-schemas/__mj.d.ts +369 -3
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +1750 -7
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +43 -8
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +59 -19
- package/dist/generic/CacheInvalidationResolver.js.map +1 -1
- package/dist/generic/PushStatusResolver.js +1 -1
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +103 -44
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +264 -305
- package/dist/generic/ResolverBase.js.map +1 -1
- 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/globalCors.d.ts +33 -0
- package/dist/globalCors.d.ts.map +1 -0
- package/dist/globalCors.js +58 -0
- package/dist/globalCors.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +112 -160
- package/dist/index.js.map +1 -1
- package/dist/multiTenancy/index.d.ts.map +1 -1
- package/dist/multiTenancy/index.js +22 -2
- package/dist/multiTenancy/index.js.map +1 -1
- package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -1
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js +2 -1
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -1
- package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -1
- package/dist/realtimeSessions/RealtimeVerifyRouter.js +2 -1
- package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -1
- package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts +100 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts.map +1 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.js +173 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.js.map +1 -0
- package/dist/realtimeSessions/index.d.ts +2 -0
- package/dist/realtimeSessions/index.d.ts.map +1 -1
- package/dist/realtimeSessions/index.js +2 -0
- package/dist/realtimeSessions/index.js.map +1 -1
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts +30 -0
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -0
- package/dist/realtimeSessions/ownedRealtimeSession.js +47 -0
- package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -0
- package/dist/realtimeSessions/sessionAccess.d.ts +6 -0
- package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
- package/dist/realtimeSessions/sessionAccess.js +7 -3
- package/dist/realtimeSessions/sessionAccess.js.map +1 -1
- package/dist/resolvers/ArtifactFileResolver.d.ts +5 -1
- package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -1
- package/dist/resolvers/ArtifactFileResolver.js +9 -7
- package/dist/resolvers/ArtifactFileResolver.js.map +1 -1
- package/dist/resolvers/ConversationRunEventsResolver.d.ts +8 -4
- package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
- package/dist/resolvers/ConversationRunEventsResolver.js +6 -6
- package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts +24 -3
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/DatasetResolver.js +57 -8
- package/dist/resolvers/DatasetResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +3 -2
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts +18 -3
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +100 -35
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +82 -11
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +279 -101
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +117 -38
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +6 -3
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +4 -2
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +2 -0
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +13 -0
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +2 -8
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +10 -26
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +35 -6
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts +12 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.js +15 -2
- package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +2 -3
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +2 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.js +28 -1
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.d.ts +24 -0
- package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.js +65 -13
- package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts +19 -2
- package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.js +102 -24
- package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
- package/dist/resolvers/agent-run-data-guard.d.ts +31 -0
- package/dist/resolvers/agent-run-data-guard.d.ts.map +1 -0
- package/dist/resolvers/agent-run-data-guard.js +72 -0
- package/dist/resolvers/agent-run-data-guard.js.map +1 -0
- package/dist/rest/RealtimeRecordingHandler.d.ts +49 -0
- package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -0
- package/dist/rest/RealtimeRecordingHandler.js +383 -0
- package/dist/rest/RealtimeRecordingHandler.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts +3 -1
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +21 -12
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/rest/setupRESTEndpoints.d.ts +6 -0
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
- package/dist/rest/setupRESTEndpoints.js +6 -0
- package/dist/rest/setupRESTEndpoints.js.map +1 -1
- package/dist/serverExtensionReservedRoots.d.ts +12 -10
- package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
- package/dist/serverExtensionReservedRoots.js +14 -17
- package/dist/serverExtensionReservedRoots.js.map +1 -1
- package/dist/services/MJStorageBlobStore.d.ts +8 -1
- package/dist/services/MJStorageBlobStore.d.ts.map +1 -1
- package/dist/services/MJStorageBlobStore.js +25 -19
- package/dist/services/MJStorageBlobStore.js.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +2 -2
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +5 -3
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -5
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/TaskGraphAgentRunner.d.ts +14 -2
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +157 -9
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts +54 -2
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +598 -15
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/sharedCache.d.ts.map +1 -1
- package/dist/sharedCache.js +3 -0
- package/dist/sharedCache.js.map +1 -1
- package/package.json +99 -98
- package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
- package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
- package/src/__tests__/FetchEntityVectorsResolver.test.ts +163 -15
- package/src/__tests__/FileResolver.storageAccess.test.ts +562 -0
- package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
- package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
- package/src/__tests__/LoginScreenEngine.test.ts +86 -0
- package/src/__tests__/MJStorageBlobStore.storageGate.test.ts +63 -0
- package/src/__tests__/RecordingAssemblyScheduler.test.ts +178 -0
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +133 -2
- package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
- package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +209 -6
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +5 -2
- package/src/__tests__/RunAIAgentResolver.branch.test.ts +1 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +5 -3
- package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +258 -0
- package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
- package/src/__tests__/SearchKnowledgeStreamResolver.test.ts +161 -0
- package/src/__tests__/SessionJanitor.test.ts +365 -10
- package/src/__tests__/SessionManager.test.ts +27 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +25 -0
- package/src/__tests__/TaskGraphAgentRunner.test.ts +266 -0
- package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +356 -22
- package/src/__tests__/TaskGraphFrameResolver.test.ts +22 -0
- package/src/__tests__/TestQuerySQLResolver.test.ts +274 -0
- package/src/__tests__/TransactionGroupResolver.refusals.test.ts +98 -1
- package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
- package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
- package/src/__tests__/agent-run-data-guard.test.ts +130 -0
- package/src/__tests__/apiRootBoot.test.ts +202 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
- package/src/__tests__/conversationRunEvents.test.ts +56 -14
- package/src/__tests__/globalCors.test.ts +217 -0
- package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
- package/src/__tests__/multiTenancy.security.test.ts +51 -0
- package/src/__tests__/multiTenancy.test.ts +1 -1
- package/src/__tests__/ownedRealtimeSession.test.ts +39 -0
- package/src/__tests__/pushStatusResolver.filter.test.ts +7 -0
- package/src/__tests__/realtimeRecordingHandler.test.ts +652 -0
- package/src/__tests__/serverExtensionHostOverlay.test.ts +66 -0
- package/src/__tests__/serverExtensionReservedRoots.test.ts +11 -98
- package/src/__tests__/sharedCache.test.ts +5 -3
- package/src/agentSessions/SessionJanitor.ts +369 -93
- package/src/agentSessions/SessionManager.ts +13 -1
- package/src/apiRoot.ts +78 -0
- package/src/auth/AuthProviderCatalogRouter.ts +26 -0
- package/src/auth/LoginScreenEngine.ts +50 -0
- package/src/auth/index.ts +1 -0
- package/src/auth/magicLink/MagicLinkRouter.ts +3 -2
- package/src/config.ts +46 -5
- package/src/coreRoutes.ts +166 -0
- package/src/generated/graphql-schemas/__mj.ts +1273 -10
- package/src/generic/CacheInvalidationResolver.ts +73 -18
- package/src/generic/PushStatusResolver.ts +1 -1
- package/src/generic/ResolverBase.ts +304 -325
- package/src/generic/RunViewResolver.ts +17 -0
- package/src/globalCors.ts +71 -0
- package/src/index.ts +116 -174
- package/src/multiTenancy/index.ts +24 -2
- package/src/realtimeSessions/RealtimeSessionVerificationService.ts +2 -1
- package/src/realtimeSessions/RealtimeVerifyRouter.ts +2 -1
- package/src/realtimeSessions/RecordingAssemblyScheduler.ts +234 -0
- package/src/realtimeSessions/index.ts +2 -0
- package/src/realtimeSessions/ownedRealtimeSession.ts +55 -0
- package/src/realtimeSessions/sessionAccess.ts +7 -3
- package/src/resolvers/ArtifactFileResolver.ts +9 -8
- package/src/resolvers/ConversationRunEventsResolver.ts +15 -11
- package/src/resolvers/DatasetResolver.ts +70 -10
- package/src/resolvers/EntityRecordNameResolver.ts +3 -2
- package/src/resolvers/FetchEntityVectorsResolver.ts +121 -39
- package/src/resolvers/FileResolver.ts +321 -106
- package/src/resolvers/GetDataContextDataResolver.ts +139 -41
- package/src/resolvers/GetDataResolver.ts +6 -3
- package/src/resolvers/IntegrationDiscoveryResolver.ts +4 -2
- package/src/resolvers/QueryResolver.ts +11 -0
- package/src/resolvers/RealtimeClientSessionResolver.ts +10 -33
- package/src/resolvers/RunAIAgentResolver.ts +39 -4
- package/src/resolvers/SearchKnowledgeStreamResolver.ts +19 -3
- package/src/resolvers/SqlLoggingConfigResolver.ts +2 -4
- package/src/resolvers/TaskGraphFrameResolver.ts +33 -1
- package/src/resolvers/TestQuerySQLResolver.ts +68 -13
- package/src/resolvers/TransactionGroupResolver.ts +131 -27
- package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
- package/src/resolvers/agent-run-data-guard.ts +89 -0
- package/src/rest/RealtimeRecordingHandler.ts +464 -0
- package/src/rest/ViewOperationsHandler.ts +29 -13
- package/src/rest/setupRESTEndpoints.ts +6 -0
- package/src/serverExtensionReservedRoots.ts +14 -17
- package/src/services/MJStorageBlobStore.ts +31 -30
- package/src/services/StartTaskGraphDispatcher.ts +2 -2
- package/src/services/TaskGraphActionRunner.ts +25 -5
- package/src/services/TaskGraphAgentRunner.ts +188 -11
- package/src/services/TaskGraphContinuationDeliverer.ts +704 -14
- package/src/sharedCache.ts +3 -0
|
@@ -12,6 +12,7 @@ import { GetHostInstanceID } from './HostInstance.js';
|
|
|
12
12
|
import { WriteReturningVisitorRecap } from './ReturningVisitorRecap.js';
|
|
13
13
|
import { SessionRunIDVerifier, SessionRunIDs } from './SessionRunIDVerifier.js';
|
|
14
14
|
import { ResolveScopedAnonymousRunUser } from '../realtimeWidget/widgetGuestElevation.js';
|
|
15
|
+
import { RecordingAssemblyScheduler } from '../realtimeSessions/RecordingAssemblyScheduler.js';
|
|
15
16
|
|
|
16
17
|
/** Entity names — centralised so the `MJ:`-prefix convention is applied in exactly one place. */
|
|
17
18
|
const SESSION_ENTITY = 'MJ: AI Agent Sessions';
|
|
@@ -212,6 +213,13 @@ export class SessionManager {
|
|
|
212
213
|
if (!closed) {
|
|
213
214
|
return false;
|
|
214
215
|
}
|
|
216
|
+
if (session.RecordingStartedAt) {
|
|
217
|
+
// A client-direct recording is assembled only after the grace period while its call is open
|
|
218
|
+
// (a hidden tab must not rewrite recording.wav on every switch; see RealtimeRecordingHandler).
|
|
219
|
+
// The call has ended now, so assemble it right away. The ledger's claim arbitrates instances,
|
|
220
|
+
// and a recording with nothing to assemble (none, or server-bridged) is simply not claimable.
|
|
221
|
+
RecordingAssemblyScheduler.Instance.ScheduleAssembly(agentSessionID, 0, false);
|
|
222
|
+
}
|
|
215
223
|
await this.disconnectChannels(agentSessionID, contextUser, provider, preloadedChannels);
|
|
216
224
|
await this.finalizeObservabilityRuns(session, contextUser, provider);
|
|
217
225
|
await this.notifyChannelPluginsSessionClosed(agentSessionID, closeReason);
|
|
@@ -530,7 +538,11 @@ export class SessionManager {
|
|
|
530
538
|
return session;
|
|
531
539
|
}
|
|
532
540
|
|
|
533
|
-
/**
|
|
541
|
+
/**
|
|
542
|
+
* Loads a session entity by ID, returning null when it cannot be loaded. The server entity
|
|
543
|
+
* (`MJAIAgentSessionEntityServer`) keeps the lifecycle saves that follow from writing back the
|
|
544
|
+
* recording ledger's columns (#5294).
|
|
545
|
+
*/
|
|
534
546
|
private async loadSession(
|
|
535
547
|
agentSessionID: string,
|
|
536
548
|
contextUser: UserInfo,
|
package/src/apiRoot.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MJAPI's base path: the single prefix every route MJAPI serves is mounted under (MJ #5674).
|
|
3
|
+
*
|
|
4
|
+
* MJAPI does not know where a proxy exposes it. Behind a proxy that forwards a prefix WITHOUT stripping it
|
|
5
|
+
* (CloudFront `/api/*` → MJAPI), MJAPI must listen under that prefix, so the host sets
|
|
6
|
+
* `MJAPI_BASE_PATH=/api`. Behind a stripping proxy, or with no proxy, it stays `/`. Every mount and every
|
|
7
|
+
* URL handed to a browser, an email or a webhook provider goes through {@link ApiRoute}, so one setting moves
|
|
8
|
+
* them all. CI enforces this (`check:api-root` and `__tests__/apiRootBoot.test.ts`).
|
|
9
|
+
*
|
|
10
|
+
* `GRAPHQL_ROOT_PATH` is a separate thing: only where Apollo listens. Do not derive routes from it.
|
|
11
|
+
*
|
|
12
|
+
* At the default base path `/` every value here is byte-identical to the root-mounted paths MJAPI always used.
|
|
13
|
+
*/
|
|
14
|
+
import { TrimTrailingSlashes } from '@memberjunction/global';
|
|
15
|
+
import { configInfo } from './config.js';
|
|
16
|
+
|
|
17
|
+
/** Options for {@link ApiRoute}. */
|
|
18
|
+
export interface ApiRouteOptions {
|
|
19
|
+
/** Return the public absolute URL (`MJAPI_PUBLIC_URL` + subpath) instead of the server mount path. */
|
|
20
|
+
absolute?: boolean; // case-violation-ok-legacy-back-compat: the `{ absolute: true }` option shape is the documented #5674 contract
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Normalizes a base-path value to a prefix with no trailing slash: `''` for `/`, `'/api'` for `/api/` or
|
|
25
|
+
* `/api`. Throws on a value that does not start with `/`, since Express would mount it nowhere.
|
|
26
|
+
*/
|
|
27
|
+
export function NormalizeApiRootPath(rootPath: string | undefined): string {
|
|
28
|
+
const trimmed = (rootPath ?? '').trim();
|
|
29
|
+
if (trimmed && !trimmed.startsWith('/')) {
|
|
30
|
+
throw new Error(`MJAPI_BASE_PATH must start with '/', got '${trimmed}'`);
|
|
31
|
+
}
|
|
32
|
+
return TrimTrailingSlashes(trimmed);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assertSubpath(subpath: string): void {
|
|
36
|
+
if (typeof subpath !== 'string' || !subpath.startsWith('/')) {
|
|
37
|
+
throw new Error(`ApiRoute subpath must start with '/', got '${String(subpath)}'`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Joins a base path and a subpath relative to it. Pure, so tests can drive any base path. */
|
|
42
|
+
export function JoinApiRoute(apiRootPath: string, subpath: string): string {
|
|
43
|
+
assertSubpath(subpath);
|
|
44
|
+
return `${NormalizeApiRootPath(apiRootPath)}${subpath}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The running server's base path (`MJAPI_BASE_PATH`, no trailing slash; `''` at `/`). */
|
|
48
|
+
export function ApiRootPath(): string {
|
|
49
|
+
return NormalizeApiRootPath(configInfo.apiBasePath);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The server's configured public URL, derived exactly as it always has been: `MJAPI_PUBLIC_URL`, else
|
|
54
|
+
* `baseUrl:graphqlPort` + `graphqlRootPath`. Unchanged and untrimmed on purpose: it is the magic-link JWT
|
|
55
|
+
* issuer and the `publicUrl` handed to OAuth and server extensions, and changing it would invalidate issued
|
|
56
|
+
* tokens. Build URLs with `ApiRoute(subpath, { absolute: true })`, not from this.
|
|
57
|
+
*/
|
|
58
|
+
export function ConfiguredPublicUrl(): string {
|
|
59
|
+
return configInfo.publicUrl || `${configInfo.baseUrl}:${configInfo.graphqlPort}${configInfo.graphqlRootPath || ''}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The path (or, with `{ absolute: true }`, the public URL) of a route under MJAPI's base path.
|
|
64
|
+
*
|
|
65
|
+
* `subpath` is relative to the base path and must start with `/`.
|
|
66
|
+
* - `ApiRoute('/media')` → `/media` at base `/`, `/api/media` at `MJAPI_BASE_PATH=/api`.
|
|
67
|
+
* - `ApiRoute('/media/abc', { absolute: true })` → `${MJAPI_PUBLIC_URL}/media/abc`. `MJAPI_PUBLIC_URL` is
|
|
68
|
+
* wherever the world reaches MJAPI (prefix included), so the base path is never added to it. Without it,
|
|
69
|
+
* the local fallback is `baseUrl:graphqlPort` + the base path.
|
|
70
|
+
*/
|
|
71
|
+
export function ApiRoute(subpath: string, options?: ApiRouteOptions): string {
|
|
72
|
+
if (!options?.absolute) {
|
|
73
|
+
return JoinApiRoute(ApiRootPath(), subpath);
|
|
74
|
+
}
|
|
75
|
+
assertSubpath(subpath);
|
|
76
|
+
const base = configInfo.publicUrl || `${configInfo.baseUrl}:${configInfo.graphqlPort}${ApiRootPath()}`;
|
|
77
|
+
return `${TrimTrailingSlashes(base)}${subpath}`;
|
|
78
|
+
}
|
|
@@ -12,6 +12,7 @@ import { Router, type Request, type Response } from 'express';
|
|
|
12
12
|
import { rateLimit } from 'express-rate-limit';
|
|
13
13
|
import { LogError, type PublicAuthProviderCatalog } from '@memberjunction/core';
|
|
14
14
|
import { AuthProviderEngine } from './AuthProviderEngine.js';
|
|
15
|
+
import { EMPTY_PUBLIC_LOGIN_SCREEN, LoginScreenEngine } from './LoginScreenEngine.js';
|
|
15
16
|
|
|
16
17
|
/** Mount path for the public catalog router. */
|
|
17
18
|
export const AUTH_CATALOG_MOUNT_PATH = '/auth';
|
|
@@ -72,6 +73,31 @@ export function CreateAuthProviderCatalogRouter(): Router {
|
|
|
72
73
|
}
|
|
73
74
|
});
|
|
74
75
|
|
|
76
|
+
/**
|
|
77
|
+
* `GET /auth/login-screen`: the default theme's PUBLISHED sign-in page, logos and seeds, for the login
|
|
78
|
+
* screen to draw before anyone signs in. Its own rate-limit budget (a 429 must answer in this route's
|
|
79
|
+
* shape), the catalog's cache window, and the same failure rule: empty, never an error.
|
|
80
|
+
*/
|
|
81
|
+
const loginScreenLimiter = rateLimit({
|
|
82
|
+
windowMs: DEFAULT_RATE_LIMIT_WINDOW_MS,
|
|
83
|
+
limit: DEFAULT_RATE_LIMIT_MAX,
|
|
84
|
+
standardHeaders: 'draft-7',
|
|
85
|
+
legacyHeaders: false,
|
|
86
|
+
message: EMPTY_PUBLIC_LOGIN_SCREEN
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
router.get('/login-screen', loginScreenLimiter, (_req: Request, res: Response) => {
|
|
90
|
+
try {
|
|
91
|
+
const body = LoginScreenEngine.Instance.GetPublicLoginScreen();
|
|
92
|
+
res.set('Cache-Control', `public, max-age=${CACHE_MAX_AGE_SECONDS}`);
|
|
93
|
+
res.status(200).json(body);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
96
|
+
LogError(`[Auth] Failed to serve the public login screen: ${message}`);
|
|
97
|
+
res.status(200).json(EMPTY_PUBLIC_LOGIN_SCREEN);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
75
101
|
return router;
|
|
76
102
|
}
|
|
77
103
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Caches the default theme for the public login-screen route.
|
|
3
|
+
*
|
|
4
|
+
* @module @memberjunction/server/auth
|
|
5
|
+
*/
|
|
6
|
+
import { BaseEngine, BaseEnginePropertyConfig, RegisterForStartup, UserInfo, type IMetadataProvider } from '@memberjunction/core';
|
|
7
|
+
import { MJThemeEntity } from '@memberjunction/core-entities';
|
|
8
|
+
import { PublicLoginScreenFromTheme, type PublicLoginScreen } from '@memberjunction/theme-engine';
|
|
9
|
+
|
|
10
|
+
/** What the route serves when there is no default theme, or it cannot be read. */
|
|
11
|
+
export const EMPTY_PUBLIC_LOGIN_SCREEN: Readonly<PublicLoginScreen> = { ThemeName: null, Design: null, Seeds: null, Logos: {} };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Holds the default Active theme, so `GET /auth/login-screen` answers every anonymous page load from
|
|
15
|
+
* memory. Saving a theme refreshes it, so a publish in Login Studio reaches the login screen without a
|
|
16
|
+
* restart. Severity `warn`: a failed load must not stop the server; the route then serves the stock page.
|
|
17
|
+
*/
|
|
18
|
+
@RegisterForStartup({
|
|
19
|
+
priority: 20,
|
|
20
|
+
severity: 'warn',
|
|
21
|
+
deferred: false,
|
|
22
|
+
description: 'Caches the default theme that GET /auth/login-screen serves to the login screen'
|
|
23
|
+
})
|
|
24
|
+
export class LoginScreenEngine extends BaseEngine<LoginScreenEngine> {
|
|
25
|
+
private _defaultThemes: MJThemeEntity[] = [];
|
|
26
|
+
|
|
27
|
+
public static get Instance(): LoginScreenEngine {
|
|
28
|
+
return super.getInstance<LoginScreenEngine>();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async Config(forceRefresh?: boolean, contextUser?: UserInfo, provider?: IMetadataProvider): Promise<void> {
|
|
32
|
+
const configs: Partial<BaseEnginePropertyConfig>[] = [
|
|
33
|
+
{
|
|
34
|
+
PropertyName: '_defaultThemes',
|
|
35
|
+
EntityName: 'MJ: Themes',
|
|
36
|
+
// One default is enforced by the app, not the schema; the most recently changed one wins (as in Login Studio).
|
|
37
|
+
Filter: `IsDefault = 1 AND Status = 'Active'`,
|
|
38
|
+
OrderBy: '__mj_UpdatedAt DESC',
|
|
39
|
+
CacheLocal: true
|
|
40
|
+
}
|
|
41
|
+
];
|
|
42
|
+
await this.Load(configs, provider, forceRefresh, contextUser);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The default theme's public projection: its PUBLISHED design (never the draft), seeds and logos made safe. */
|
|
46
|
+
public GetPublicLoginScreen(): PublicLoginScreen {
|
|
47
|
+
const theme = this.GetConfigData<MJThemeEntity>('_defaultThemes')[0];
|
|
48
|
+
return theme ? PublicLoginScreenFromTheme(theme) : EMPTY_PUBLIC_LOGIN_SCREEN;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/auth/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export * from './APIKeyScopeAuth.js';
|
|
|
13
13
|
export * from './scopeLimitedPrincipal.js';
|
|
14
14
|
export * from './actingContextResolver.js';
|
|
15
15
|
export * from './AuthProviderEngine.js';
|
|
16
|
+
export * from './LoginScreenEngine.js';
|
|
16
17
|
export * from './AuthProviderCatalogRouter.js';
|
|
17
18
|
export { InitializeAuthProviders, initializeAuthProviders, InitializeAuthProvidersFromMetadata, initializeAuthProvidersFromMetadata, RefreshAuthProviders, refreshAuthProviders, ValidateAuthProvidersRegistered, validateAuthProvidersRegistered } from './initializeProviders.js';
|
|
18
19
|
|
|
@@ -17,6 +17,7 @@ import { MagicLinkKeyManager } from './MagicLinkKeys.js';
|
|
|
17
17
|
import { MagicLinkService } from './MagicLinkService.js';
|
|
18
18
|
import { BuildRedeemLandingHtml } from './redeemLanding.js';
|
|
19
19
|
import type { CreateMagicLinkInviteParams, RedeemMagicLinkResult } from './types.js';
|
|
20
|
+
import { ApiRoute } from '../../apiRoot.js';
|
|
20
21
|
|
|
21
22
|
/** The mount path for both routers (`/magic-link`). */
|
|
22
23
|
export const MAGIC_LINK_MOUNT_PATH = '/magic-link';
|
|
@@ -107,13 +108,13 @@ export function CreateMagicLinkHandler(publicUrl: string, config: MagicLinkConfi
|
|
|
107
108
|
}
|
|
108
109
|
// API callers must POST — GET never redeems.
|
|
109
110
|
if (req.query.format === 'json') {
|
|
110
|
-
res.status(405).json({ success: false, error: 'Redemption requires POST.', method: 'POST', path: `${MAGIC_LINK_MOUNT_PATH}/redeem` });
|
|
111
|
+
res.status(405).json({ success: false, error: 'Redemption requires POST.', method: 'POST', path: ApiRoute(`${MAGIC_LINK_MOUNT_PATH}/redeem`) });
|
|
111
112
|
return;
|
|
112
113
|
}
|
|
113
114
|
res
|
|
114
115
|
.status(200)
|
|
115
116
|
.type('html')
|
|
116
|
-
.send(BuildRedeemLandingHtml(token, `${MAGIC_LINK_MOUNT_PATH}/redeem`));
|
|
117
|
+
.send(BuildRedeemLandingHtml(token, ApiRoute(`${MAGIC_LINK_MOUNT_PATH}/redeem`)));
|
|
117
118
|
});
|
|
118
119
|
|
|
119
120
|
// POST /redeem performs the actual (side-effectful) redemption. Token arrives
|
package/src/config.ts
CHANGED
|
@@ -226,10 +226,19 @@ const telemetrySchema = z.object({
|
|
|
226
226
|
level: z.enum(['minimal', 'standard', 'verbose', 'debug']).optional().default('standard'),
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
/**
|
|
230
|
+
* A host `mj.config.cjs` `serverExtensions[]` entry. Only `DriverClass` is required: an entry whose
|
|
231
|
+
* DriverClass an Open App package already publishes is an OVERLAY (MergeServerExtensionConfigs) and
|
|
232
|
+
* may set just the fields it overrides — `{ DriverClass: 'X', OwnsCORS: false }` — without repeating
|
|
233
|
+
* `RootPath`. `Enabled` has no schema default for the same reason: a defaulted `true` would silently
|
|
234
|
+
* re-enable an extension the package ships disabled. Whether the MERGED entry is complete (a valid
|
|
235
|
+
* `RootPath`; `Enabled` defaults to true) is validated after the merge, by prepareServerExtensionConfigs,
|
|
236
|
+
* which reports an incomplete host-only entry instead of mounting it.
|
|
237
|
+
*/
|
|
238
|
+
export const ServerExtensionSchema = z.object({
|
|
239
|
+
Enabled: z.boolean().optional(),
|
|
231
240
|
DriverClass: z.string(),
|
|
232
|
-
RootPath: z.string(),
|
|
241
|
+
RootPath: z.string().optional(),
|
|
233
242
|
Settings: z.record(z.unknown()).default({})
|
|
234
243
|
}).passthrough();
|
|
235
244
|
|
|
@@ -238,6 +247,14 @@ const cacheSettingsSchema = z.object({
|
|
|
238
247
|
maxMemoryMB: z.number().optional().default(150),
|
|
239
248
|
/** Maximum percentage of total cache memory that any single entity can occupy. Default: 50. Set to 0 to disable. */
|
|
240
249
|
maxPercentOfCachePerEntity: z.number().optional().default(50),
|
|
250
|
+
/**
|
|
251
|
+
* Ceiling on any ONE cache entry. 'auto' (the default) derives it from maxMemoryMB, so the
|
|
252
|
+
* largest entity the server caches scales with the memory the cache was given; a number is an
|
|
253
|
+
* explicit stricter percentage of maxMemoryMB; 0 removes the ceiling. Leave it on 'auto' unless
|
|
254
|
+
* a specific entry must be kept out of the cache — an entry declined by the ceiling is re-read
|
|
255
|
+
* from the database on every call, and the server logs which maxMemoryMB would admit it.
|
|
256
|
+
*/
|
|
257
|
+
maxEntryPercentOfCache: z.union([z.number(), z.literal('auto')]).optional().default('auto'),
|
|
241
258
|
/** Default TTL in seconds. 0 = no TTL, rely on event-based invalidation. Default: 0. */
|
|
242
259
|
defaultTTLSeconds: z.number().optional().default(0),
|
|
243
260
|
/** Interval in seconds for periodic eviction sweep. 0 = disabled. Default: 300 (5 minutes). */
|
|
@@ -316,6 +333,15 @@ const loggingSettingsSchema = z.object({
|
|
|
316
333
|
}).optional().default({}),
|
|
317
334
|
});
|
|
318
335
|
|
|
336
|
+
const aiPromptRunsSchema = z.object({
|
|
337
|
+
/**
|
|
338
|
+
* Server default for which template inputs prompt runs store in AIPromptRun.Messages:
|
|
339
|
+
* 'None' (key sizes only), 'Referenced' (keys the templates read) or 'Full'.
|
|
340
|
+
* AIPrompt.TemplateDataCapture and AIPromptParams.TemplateDataCapture override it.
|
|
341
|
+
*/
|
|
342
|
+
templateDataCapture: z.enum(['None', 'Referenced', 'Full']).optional(),
|
|
343
|
+
});
|
|
344
|
+
|
|
319
345
|
const corsSchema = z.object({
|
|
320
346
|
/** Allowed origins for CORS. Default ['*'] preserves backward-compatible "allow all" behavior. */
|
|
321
347
|
allowedOrigins: z.array(z.string()).default(['*']),
|
|
@@ -810,6 +836,11 @@ const realtimeSchema = z.object({
|
|
|
810
836
|
enabled: zodBooleanWithTransforms().default(true),
|
|
811
837
|
/** Mid-session identity verification (off by default). */
|
|
812
838
|
identityVerification: identityVerificationSchema.optional().default({}),
|
|
839
|
+
/**
|
|
840
|
+
* Seconds a client-direct call's recording waits for missing shards after its terminal signal before it
|
|
841
|
+
* is assembled anyway as `Partial` (gaps become silence). Defaults to 90.
|
|
842
|
+
*/
|
|
843
|
+
recordingAssemblyGraceSeconds: z.coerce.number().positive().optional().default(90),
|
|
813
844
|
}).passthrough();
|
|
814
845
|
|
|
815
846
|
const configInfoSchema = z.object({
|
|
@@ -830,9 +861,10 @@ const configInfoSchema = z.object({
|
|
|
830
861
|
telemetry: telemetrySchema.optional().default({}),
|
|
831
862
|
queryDialects: queryDialectSchema.optional().default({}),
|
|
832
863
|
multiTenancy: multiTenancySchema.optional().default({}),
|
|
833
|
-
serverExtensions: z.array(
|
|
864
|
+
serverExtensions: z.array(ServerExtensionSchema).optional().default([]),
|
|
834
865
|
cacheSettings: cacheSettingsSchema.optional().default({}),
|
|
835
866
|
loggingSettings: loggingSettingsSchema.optional().default({}),
|
|
867
|
+
aiPromptRuns: aiPromptRunsSchema.optional(),
|
|
836
868
|
feedbackSettings: feedbackSettingsSchema.optional().default({}),
|
|
837
869
|
cors: corsSchema.optional().default({}),
|
|
838
870
|
rateLimiting: rateLimitSchema.optional().default({}),
|
|
@@ -860,6 +892,12 @@ const configInfoSchema = z.object({
|
|
|
860
892
|
___codeGenAPIPort: z.coerce.number().optional().default(3999),
|
|
861
893
|
___codeGenAPISubmissionDelay: z.coerce.number().optional().default(5000),
|
|
862
894
|
graphqlRootPath: z.string().optional().default('/'),
|
|
895
|
+
/**
|
|
896
|
+
* Path prefix MJAPI mounts every route it serves under (core routes and server extensions), on its own
|
|
897
|
+
* listener. `/` unless a proxy forwards a prefix WITHOUT stripping it (e.g. CloudFront `/api/*` → MJAPI):
|
|
898
|
+
* then set `MJAPI_BASE_PATH=/api`. Independent of `graphqlRootPath`, which is only Apollo's path. MJ #5674.
|
|
899
|
+
*/
|
|
900
|
+
apiBasePath: z.string().optional().default('/'),
|
|
863
901
|
enableIntrospection: z.coerce.boolean().optional().default(false),
|
|
864
902
|
websiteRunFromPackage: z.coerce.number().optional(),
|
|
865
903
|
userEmailMap: z
|
|
@@ -893,9 +931,10 @@ export type { WorkQueueConfig } from './services/workQueueConfig.js';
|
|
|
893
931
|
export type TelemetryConfig = z.infer<typeof telemetrySchema>;
|
|
894
932
|
export type QueryDialectConfig = z.infer<typeof queryDialectSchema>;
|
|
895
933
|
export type MultiTenancyConfig = z.infer<typeof multiTenancySchema>;
|
|
896
|
-
export type ServerExtensionConfig = z.infer<typeof
|
|
934
|
+
export type ServerExtensionConfig = z.infer<typeof ServerExtensionSchema>;
|
|
897
935
|
export type CacheSettingsConfig = z.infer<typeof cacheSettingsSchema>;
|
|
898
936
|
export type LoggingSettingsConfig = z.infer<typeof loggingSettingsSchema>;
|
|
937
|
+
export type AIPromptRunsConfig = z.infer<typeof aiPromptRunsSchema>;
|
|
899
938
|
export type FeedbackGithubSettingsConfig = z.infer<typeof feedbackGithubSettingsSchema>;
|
|
900
939
|
export type FeedbackSettingsConfig = z.infer<typeof feedbackSettingsSchema>;
|
|
901
940
|
export type CorsConfig = z.infer<typeof corsSchema>;
|
|
@@ -931,6 +970,7 @@ export const DEFAULT_SERVER_CONFIG: Partial<ConfigInfo> = {
|
|
|
931
970
|
// GraphQL server settings (environment-driven with defaults)
|
|
932
971
|
graphqlPort: process.env.GRAPHQL_PORT ? parseInt(process.env.GRAPHQL_PORT, 10) : 4000,
|
|
933
972
|
graphqlRootPath: process.env.GRAPHQL_ROOT_PATH ?? '/',
|
|
973
|
+
apiBasePath: process.env.MJAPI_BASE_PATH ?? '/',
|
|
934
974
|
baseUrl: process.env.GRAPHQL_BASE_URL ?? 'http://localhost',
|
|
935
975
|
publicUrl: process.env.MJAPI_PUBLIC_URL,
|
|
936
976
|
enableIntrospection: process.env.ENABLE_INTROSPECTION === 'true',
|
|
@@ -1047,6 +1087,7 @@ export const DEFAULT_SERVER_CONFIG: Partial<ConfigInfo> = {
|
|
|
1047
1087
|
cacheSettings: {
|
|
1048
1088
|
maxMemoryMB: 150,
|
|
1049
1089
|
maxPercentOfCachePerEntity: 50,
|
|
1090
|
+
maxEntryPercentOfCache: 'auto',
|
|
1050
1091
|
defaultTTLSeconds: 0,
|
|
1051
1092
|
evictionSweepIntervalSeconds: 300,
|
|
1052
1093
|
verboseLogging: false,
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core HTTP routes MJServer mounts itself, every one under MJAPI's base path (`MJAPI_BASE_PATH`) via {@link ApiRoute}.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `serve()` so `__tests__/apiRootBoot.test.ts` can mount them on a bare Express app with
|
|
5
|
+
* `MJAPI_BASE_PATH=/api` and check every registered path. `serve()` still decides the ORDER (relative
|
|
6
|
+
* to compression, the auth middleware, server extensions and Apollo); these functions only mount.
|
|
7
|
+
*
|
|
8
|
+
* ALL new custom routes, webhooks and integrations belong in Server Extensions
|
|
9
|
+
* (guides/SERVER_EXTENSIONS_GUIDE.md), not here.
|
|
10
|
+
*/
|
|
11
|
+
import cors from 'cors';
|
|
12
|
+
import { default as BodyParser } from 'body-parser';
|
|
13
|
+
import type { Application, Router } from 'express';
|
|
14
|
+
import { REALTIME_SDP_EXCHANGE_PATH } from '@memberjunction/ai';
|
|
15
|
+
import type { ServerExtensionLoader } from '@memberjunction/server-extensions-core';
|
|
16
|
+
import { configInfo } from './config.js';
|
|
17
|
+
import { ApiRoute } from './apiRoot.js';
|
|
18
|
+
import { CreateOAuthCallbackHandler } from './rest/OAuthCallbackHandler.js';
|
|
19
|
+
import { CreateSignatureWebhookHandler } from './rest/SignatureWebhookHandler.js';
|
|
20
|
+
import { CreateMediaStreamRouter } from './rest/MediaStreamHandler.js';
|
|
21
|
+
import { CreateRealtimeSdpBrokerRouter } from './rest/RealtimeSdpBrokerHandler.js';
|
|
22
|
+
import { CreateMagicLinkHandler, CreateMagicLinkJwksRouter, RegisterMagicLinkAuthProvider, MAGIC_LINK_MOUNT_PATH } from './auth/magicLink/index.js';
|
|
23
|
+
import { CreateWidgetHandler, WIDGET_MOUNT_PATH } from './realtimeWidget/index.js';
|
|
24
|
+
import { CreateRealtimeVerifyRouter, REALTIME_VERIFY_MOUNT_PATH, RealtimeSessionVerificationService } from './realtimeSessions/index.js';
|
|
25
|
+
import { CreateAuthProviderCatalogRouter, AUTH_CATALOG_MOUNT_PATH } from './auth/AuthProviderCatalogRouter.js';
|
|
26
|
+
import type { StartupLogger } from './logging/StartupLogger.js';
|
|
27
|
+
|
|
28
|
+
type RouteLog = Pick<StartupLogger, 'LogIf'>;
|
|
29
|
+
|
|
30
|
+
/** Routers the public mounts create that must be mounted again after the auth middleware. */
|
|
31
|
+
export interface CoreAuthenticatedRouters {
|
|
32
|
+
OAuth?: Router;
|
|
33
|
+
MagicLink?: Router;
|
|
34
|
+
Widget?: Router;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `/healthcheck` — mounted first so platform probes never meet the auth middleware. */
|
|
38
|
+
export function MountHealthcheckRoute(app: Application): void {
|
|
39
|
+
app.get(ApiRoute('/healthcheck'), cors<cors.CorsRequest>(), (_req, res) => {
|
|
40
|
+
res.status(200).json({ status: 'ok' });
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The unauthenticated core routes (OAuth callback, eSignature webhook, media streaming, realtime SDP
|
|
46
|
+
* broker, magic-link, widget, realtime verify). Each verifies its own capability (signature, token,
|
|
47
|
+
* ticket), so they mount BEFORE the auth middleware. Returns the routers that belong after it.
|
|
48
|
+
*/
|
|
49
|
+
export function MountPublicCoreRoutes(app: Application, publicUrl: string, log: RouteLog): CoreAuthenticatedRouters {
|
|
50
|
+
const routers: CoreAuthenticatedRouters = { OAuth: mountOAuthCallback(app, publicUrl, log) };
|
|
51
|
+
|
|
52
|
+
app.use(ApiRoute('/esignature'), cors<cors.CorsRequest>(), CreateSignatureWebhookHandler());
|
|
53
|
+
log.LogIf('verbose', `[eSignature] Webhook route registered at ${ApiRoute('/esignature')}/webhook/:driverKey`);
|
|
54
|
+
|
|
55
|
+
// The `?token=` query param is the capability; an <audio>/<video> element can't send Authorization headers.
|
|
56
|
+
app.use(ApiRoute('/media'), cors<cors.CorsRequest>(), CreateMediaStreamRouter());
|
|
57
|
+
log.LogIf('verbose', `[Media] Streaming route registered at ${ApiRoute('/media')}/:fileId`);
|
|
58
|
+
|
|
59
|
+
if (configInfo.realtime?.enabled) {
|
|
60
|
+
app.use(ApiRoute(REALTIME_SDP_EXCHANGE_PATH), cors<cors.CorsRequest>(), CreateRealtimeSdpBrokerRouter());
|
|
61
|
+
log.LogIf('verbose', `[Realtime] WebRTC SDP broker registered at ${ApiRoute(REALTIME_SDP_EXCHANGE_PATH)}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
routers.MagicLink = mountMagicLinkPublic(app, publicUrl, log);
|
|
65
|
+
routers.Widget = mountWidgetPublic(app, publicUrl, log);
|
|
66
|
+
mountRealtimeVerify(app, log);
|
|
67
|
+
return routers;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function mountOAuthCallback(app: Application, publicUrl: string, log: RouteLog): Router | undefined {
|
|
71
|
+
if (!publicUrl) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const { callbackRouter, authenticatedRouter } = CreateOAuthCallbackHandler({
|
|
75
|
+
publicUrl,
|
|
76
|
+
successRedirectUrl: ApiRoute('/oauth/success', { absolute: true }),
|
|
77
|
+
errorRedirectUrl: ApiRoute('/oauth/error', { absolute: true }),
|
|
78
|
+
// Constrains where a caller-supplied frontendReturnUrl may point (open-redirect guard).
|
|
79
|
+
allowedFrontendOrigins: configInfo.cors?.allowedOrigins ?? ['*'],
|
|
80
|
+
});
|
|
81
|
+
// The callback is unauthenticated (called by the external auth server).
|
|
82
|
+
app.use(ApiRoute('/oauth'), cors<cors.CorsRequest>(), callbackRouter);
|
|
83
|
+
log.LogIf('verbose', `[OAuth] Callback route registered at ${ApiRoute('/oauth')}/callback`);
|
|
84
|
+
return authenticatedRouter;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Public magic-link router (JWKS + redeem). The invite-creation router is returned for after auth. */
|
|
88
|
+
function mountMagicLinkPublic(app: Application, publicUrl: string, log: RouteLog): Router | undefined {
|
|
89
|
+
if (!configInfo.magicLink?.enabled) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const { publicRouter, authenticatedRouter } = CreateMagicLinkHandler(publicUrl, configInfo.magicLink);
|
|
93
|
+
RegisterMagicLinkAuthProvider(publicUrl, configInfo.magicLink);
|
|
94
|
+
app.use(ApiRoute(MAGIC_LINK_MOUNT_PATH), cors<cors.CorsRequest>(), publicRouter);
|
|
95
|
+
log.LogIf('verbose', `[MagicLink] Public routes registered at ${ApiRoute(MAGIC_LINK_MOUNT_PATH)}/redeem and ${ApiRoute(MAGIC_LINK_MOUNT_PATH)}/jwks.json`);
|
|
96
|
+
return authenticatedRouter;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Public widget guest-session mint. It reuses the magic-link RS256 key, so when magic-link itself is
|
|
101
|
+
* disabled the JWKS is published here — otherwise every guest token fails validation and the widget 401s.
|
|
102
|
+
*/
|
|
103
|
+
function mountWidgetPublic(app: Application, publicUrl: string, log: RouteLog): Router | undefined {
|
|
104
|
+
if (!configInfo.widget?.enabled) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
const { publicRouter, authenticatedRouter } = CreateWidgetHandler(publicUrl, configInfo.widget);
|
|
108
|
+
app.use(ApiRoute(WIDGET_MOUNT_PATH), cors<cors.CorsRequest>(), publicRouter);
|
|
109
|
+
if (!configInfo.magicLink?.enabled) {
|
|
110
|
+
app.use(ApiRoute(MAGIC_LINK_MOUNT_PATH), cors<cors.CorsRequest>(), CreateMagicLinkJwksRouter());
|
|
111
|
+
log.LogIf('verbose', `[Widget] Published reused signing key at ${ApiRoute(MAGIC_LINK_MOUNT_PATH)}/jwks.json (magic-link flow disabled)`);
|
|
112
|
+
}
|
|
113
|
+
log.LogIf('verbose', `[Widget] Public routes registered at ${ApiRoute(WIDGET_MOUNT_PATH)}/session and ${ApiRoute(WIDGET_MOUNT_PATH)}/session/refresh`);
|
|
114
|
+
return authenticatedRouter;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Realtime identity-verification links. The emailed single-use token is the capability. Mounted only when
|
|
119
|
+
* verification can send mail AND protect its codes (realtime.identityVerification config).
|
|
120
|
+
*/
|
|
121
|
+
function mountRealtimeVerify(app: Application, log: RouteLog): void {
|
|
122
|
+
const verification = RealtimeSessionVerificationService.Instance;
|
|
123
|
+
if (configInfo.realtime?.identityVerification?.enabled && !verification.IsEnabled) {
|
|
124
|
+
console.warn('[RealtimeVerify] realtime.identityVerification.enabled is true but verification is unavailable: it needs a communicationProvider and an hmacSecret of at least 16 characters.');
|
|
125
|
+
}
|
|
126
|
+
if (!verification.IsEnabled) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
app.use(ApiRoute(REALTIME_VERIFY_MOUNT_PATH), cors<cors.CorsRequest>(), CreateRealtimeVerifyRouter());
|
|
130
|
+
log.LogIf('verbose', `[RealtimeVerify] Public routes registered at ${ApiRoute(REALTIME_VERIFY_MOUNT_PATH)}/:token (GET) and ${ApiRoute(REALTIME_VERIFY_MOUNT_PATH)} (POST)`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** `/health/extensions` — always mounted; an empty array when no extensions loaded. */
|
|
134
|
+
export function MountExtensionHealthRoute(app: Application, loader: ServerExtensionLoader): void {
|
|
135
|
+
app.get(ApiRoute('/health/extensions'), async (_req, res) => {
|
|
136
|
+
const results = await loader.HealthCheckAll();
|
|
137
|
+
const allHealthy = results.length === 0 || results.every((r) => r.Healthy);
|
|
138
|
+
res.status(allHealthy ? 200 : 503).json({ extensions: results });
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Public authentication-provider catalog. The browser needs it BEFORE it holds a token; it publishes only
|
|
144
|
+
* the non-secret allow-list (AuthProviderEngine.GetPublicCatalog).
|
|
145
|
+
*/
|
|
146
|
+
export function MountAuthCatalogRoute(app: Application, log: RouteLog): void {
|
|
147
|
+
app.use(ApiRoute(AUTH_CATALOG_MOUNT_PATH), cors<cors.CorsRequest>(), CreateAuthProviderCatalogRouter());
|
|
148
|
+
log.LogIf('verbose', `[Auth] Public provider catalog and login screen registered at ${ApiRoute(AUTH_CATALOG_MOUNT_PATH)}/providers and ${ApiRoute(AUTH_CATALOG_MOUNT_PATH)}/login-screen`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The routers {@link MountPublicCoreRoutes} returned, mounted AFTER the unified auth middleware. */
|
|
152
|
+
export function MountAuthenticatedCoreRoutes(app: Application, routers: CoreAuthenticatedRouters, log: RouteLog): void {
|
|
153
|
+
if (routers.OAuth) {
|
|
154
|
+
app.use(ApiRoute('/oauth'), cors<cors.CorsRequest>(), BodyParser.json(), routers.OAuth);
|
|
155
|
+
log.LogIf('verbose', `[OAuth] Authenticated routes registered at ${ApiRoute('/oauth')}/status, /initiate and /exchange`);
|
|
156
|
+
}
|
|
157
|
+
if (routers.MagicLink) {
|
|
158
|
+
app.use(ApiRoute(MAGIC_LINK_MOUNT_PATH), cors<cors.CorsRequest>(), routers.MagicLink);
|
|
159
|
+
log.LogIf('verbose', `[MagicLink] Authenticated route registered at ${ApiRoute(MAGIC_LINK_MOUNT_PATH)}/create`);
|
|
160
|
+
}
|
|
161
|
+
// A verified visitor (post magic-link upgrade) promotes their anonymous trail here.
|
|
162
|
+
if (routers.Widget) {
|
|
163
|
+
app.use(ApiRoute(WIDGET_MOUNT_PATH), cors<cors.CorsRequest>(), routers.Widget);
|
|
164
|
+
log.LogIf('verbose', `[Widget] Authenticated route registered at ${ApiRoute(WIDGET_MOUNT_PATH)}/resolve-identity`);
|
|
165
|
+
}
|
|
166
|
+
}
|