@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.
Files changed (287) hide show
  1. package/README.md +20 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/agentSessions/SessionJanitor.d.ts +91 -30
  4. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  5. package/dist/agentSessions/SessionJanitor.js +311 -81
  6. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  7. package/dist/agentSessions/SessionManager.d.ts +5 -1
  8. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  9. package/dist/agentSessions/SessionManager.js +13 -1
  10. package/dist/agentSessions/SessionManager.js.map +1 -1
  11. package/dist/apiRoot.d.ts +32 -0
  12. package/dist/apiRoot.d.ts.map +1 -0
  13. package/dist/apiRoot.js +67 -0
  14. package/dist/apiRoot.js.map +1 -0
  15. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
  16. package/dist/auth/AuthProviderCatalogRouter.js +25 -0
  17. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
  18. package/dist/auth/LoginScreenEngine.d.ts +22 -0
  19. package/dist/auth/LoginScreenEngine.d.ts.map +1 -0
  20. package/dist/auth/LoginScreenEngine.js +57 -0
  21. package/dist/auth/LoginScreenEngine.js.map +1 -0
  22. package/dist/auth/index.d.ts +1 -0
  23. package/dist/auth/index.d.ts.map +1 -1
  24. package/dist/auth/index.js +1 -0
  25. package/dist/auth/index.js.map +1 -1
  26. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  27. package/dist/auth/magicLink/MagicLinkRouter.js +3 -2
  28. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  29. package/dist/config.d.ts +136 -22
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +43 -4
  32. package/dist/config.js.map +1 -1
  33. package/dist/coreRoutes.d.ts +29 -0
  34. package/dist/coreRoutes.d.ts.map +1 -0
  35. package/dist/coreRoutes.js +142 -0
  36. package/dist/coreRoutes.js.map +1 -0
  37. package/dist/generated/graphql-schemas/__mj.d.ts +369 -3
  38. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  39. package/dist/generated/graphql-schemas/__mj.js +1750 -7
  40. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  41. package/dist/generic/CacheInvalidationResolver.d.ts +43 -8
  42. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  43. package/dist/generic/CacheInvalidationResolver.js +59 -19
  44. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  45. package/dist/generic/PushStatusResolver.js +1 -1
  46. package/dist/generic/PushStatusResolver.js.map +1 -1
  47. package/dist/generic/ResolverBase.d.ts +103 -44
  48. package/dist/generic/ResolverBase.d.ts.map +1 -1
  49. package/dist/generic/ResolverBase.js +264 -305
  50. package/dist/generic/ResolverBase.js.map +1 -1
  51. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  52. package/dist/generic/RunViewResolver.js +12 -0
  53. package/dist/generic/RunViewResolver.js.map +1 -1
  54. package/dist/globalCors.d.ts +33 -0
  55. package/dist/globalCors.d.ts.map +1 -0
  56. package/dist/globalCors.js +58 -0
  57. package/dist/globalCors.js.map +1 -0
  58. package/dist/index.d.ts +11 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +112 -160
  61. package/dist/index.js.map +1 -1
  62. package/dist/multiTenancy/index.d.ts.map +1 -1
  63. package/dist/multiTenancy/index.js +22 -2
  64. package/dist/multiTenancy/index.js.map +1 -1
  65. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -1
  66. package/dist/realtimeSessions/RealtimeSessionVerificationService.js +2 -1
  67. package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -1
  68. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -1
  69. package/dist/realtimeSessions/RealtimeVerifyRouter.js +2 -1
  70. package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -1
  71. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts +100 -0
  72. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts.map +1 -0
  73. package/dist/realtimeSessions/RecordingAssemblyScheduler.js +173 -0
  74. package/dist/realtimeSessions/RecordingAssemblyScheduler.js.map +1 -0
  75. package/dist/realtimeSessions/index.d.ts +2 -0
  76. package/dist/realtimeSessions/index.d.ts.map +1 -1
  77. package/dist/realtimeSessions/index.js +2 -0
  78. package/dist/realtimeSessions/index.js.map +1 -1
  79. package/dist/realtimeSessions/ownedRealtimeSession.d.ts +30 -0
  80. package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -0
  81. package/dist/realtimeSessions/ownedRealtimeSession.js +47 -0
  82. package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -0
  83. package/dist/realtimeSessions/sessionAccess.d.ts +6 -0
  84. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
  85. package/dist/realtimeSessions/sessionAccess.js +7 -3
  86. package/dist/realtimeSessions/sessionAccess.js.map +1 -1
  87. package/dist/resolvers/ArtifactFileResolver.d.ts +5 -1
  88. package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -1
  89. package/dist/resolvers/ArtifactFileResolver.js +9 -7
  90. package/dist/resolvers/ArtifactFileResolver.js.map +1 -1
  91. package/dist/resolvers/ConversationRunEventsResolver.d.ts +8 -4
  92. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
  93. package/dist/resolvers/ConversationRunEventsResolver.js +6 -6
  94. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
  95. package/dist/resolvers/DatasetResolver.d.ts +24 -3
  96. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  97. package/dist/resolvers/DatasetResolver.js +57 -8
  98. package/dist/resolvers/DatasetResolver.js.map +1 -1
  99. package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -1
  100. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  101. package/dist/resolvers/EntityRecordNameResolver.js +3 -2
  102. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  103. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +18 -3
  104. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  105. package/dist/resolvers/FetchEntityVectorsResolver.js +100 -35
  106. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  107. package/dist/resolvers/FileResolver.d.ts +82 -11
  108. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  109. package/dist/resolvers/FileResolver.js +279 -101
  110. package/dist/resolvers/FileResolver.js.map +1 -1
  111. package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
  112. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  113. package/dist/resolvers/GetDataContextDataResolver.js +117 -38
  114. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  115. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  116. package/dist/resolvers/GetDataResolver.js +6 -3
  117. package/dist/resolvers/GetDataResolver.js.map +1 -1
  118. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  119. package/dist/resolvers/IntegrationDiscoveryResolver.js +4 -2
  120. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  121. package/dist/resolvers/QueryResolver.d.ts +2 -0
  122. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  123. package/dist/resolvers/QueryResolver.js +13 -0
  124. package/dist/resolvers/QueryResolver.js.map +1 -1
  125. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +2 -8
  126. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  127. package/dist/resolvers/RealtimeClientSessionResolver.js +10 -26
  128. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  129. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  130. package/dist/resolvers/RunAIAgentResolver.js +35 -6
  131. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  132. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts +12 -1
  133. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -1
  134. package/dist/resolvers/SearchKnowledgeStreamResolver.js +15 -2
  135. package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
  136. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  137. package/dist/resolvers/SqlLoggingConfigResolver.js +2 -3
  138. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  139. package/dist/resolvers/TaskGraphFrameResolver.d.ts +2 -1
  140. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
  141. package/dist/resolvers/TaskGraphFrameResolver.js +28 -1
  142. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
  143. package/dist/resolvers/TestQuerySQLResolver.d.ts +24 -0
  144. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  145. package/dist/resolvers/TestQuerySQLResolver.js +65 -13
  146. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  147. package/dist/resolvers/TransactionGroupResolver.d.ts +19 -2
  148. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  149. package/dist/resolvers/TransactionGroupResolver.js +102 -24
  150. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  151. package/dist/resolvers/agent-run-data-guard.d.ts +31 -0
  152. package/dist/resolvers/agent-run-data-guard.d.ts.map +1 -0
  153. package/dist/resolvers/agent-run-data-guard.js +72 -0
  154. package/dist/resolvers/agent-run-data-guard.js.map +1 -0
  155. package/dist/rest/RealtimeRecordingHandler.d.ts +49 -0
  156. package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -0
  157. package/dist/rest/RealtimeRecordingHandler.js +383 -0
  158. package/dist/rest/RealtimeRecordingHandler.js.map +1 -0
  159. package/dist/rest/ViewOperationsHandler.d.ts +3 -1
  160. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  161. package/dist/rest/ViewOperationsHandler.js +21 -12
  162. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  163. package/dist/rest/setupRESTEndpoints.d.ts +6 -0
  164. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
  165. package/dist/rest/setupRESTEndpoints.js +6 -0
  166. package/dist/rest/setupRESTEndpoints.js.map +1 -1
  167. package/dist/serverExtensionReservedRoots.d.ts +12 -10
  168. package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
  169. package/dist/serverExtensionReservedRoots.js +14 -17
  170. package/dist/serverExtensionReservedRoots.js.map +1 -1
  171. package/dist/services/MJStorageBlobStore.d.ts +8 -1
  172. package/dist/services/MJStorageBlobStore.d.ts.map +1 -1
  173. package/dist/services/MJStorageBlobStore.js +25 -19
  174. package/dist/services/MJStorageBlobStore.js.map +1 -1
  175. package/dist/services/StartTaskGraphDispatcher.js +2 -2
  176. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  177. package/dist/services/TaskGraphActionRunner.d.ts +5 -3
  178. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  179. package/dist/services/TaskGraphActionRunner.js +27 -5
  180. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  181. package/dist/services/TaskGraphAgentRunner.d.ts +14 -2
  182. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  183. package/dist/services/TaskGraphAgentRunner.js +157 -9
  184. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  185. package/dist/services/TaskGraphContinuationDeliverer.d.ts +54 -2
  186. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  187. package/dist/services/TaskGraphContinuationDeliverer.js +598 -15
  188. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  189. package/dist/sharedCache.d.ts.map +1 -1
  190. package/dist/sharedCache.js +3 -0
  191. package/dist/sharedCache.js.map +1 -1
  192. package/package.json +99 -98
  193. package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
  194. package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
  195. package/src/__tests__/FetchEntityVectorsResolver.test.ts +163 -15
  196. package/src/__tests__/FileResolver.storageAccess.test.ts +562 -0
  197. package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
  198. package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
  199. package/src/__tests__/LoginScreenEngine.test.ts +86 -0
  200. package/src/__tests__/MJStorageBlobStore.storageGate.test.ts +63 -0
  201. package/src/__tests__/RecordingAssemblyScheduler.test.ts +178 -0
  202. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +133 -2
  203. package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
  204. package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
  205. package/src/__tests__/ResolverBase.clientOldValues.test.ts +209 -6
  206. package/src/__tests__/ResolverBase.filterEscaping.test.ts +5 -2
  207. package/src/__tests__/RunAIAgentResolver.branch.test.ts +1 -0
  208. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +5 -3
  209. package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +258 -0
  210. package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
  211. package/src/__tests__/SearchKnowledgeStreamResolver.test.ts +161 -0
  212. package/src/__tests__/SessionJanitor.test.ts +365 -10
  213. package/src/__tests__/SessionManager.test.ts +27 -0
  214. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +25 -0
  215. package/src/__tests__/TaskGraphAgentRunner.test.ts +266 -0
  216. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +356 -22
  217. package/src/__tests__/TaskGraphFrameResolver.test.ts +22 -0
  218. package/src/__tests__/TestQuerySQLResolver.test.ts +274 -0
  219. package/src/__tests__/TransactionGroupResolver.refusals.test.ts +98 -1
  220. package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
  221. package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
  222. package/src/__tests__/agent-run-data-guard.test.ts +130 -0
  223. package/src/__tests__/apiRootBoot.test.ts +202 -0
  224. package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
  225. package/src/__tests__/conversationRunEvents.test.ts +56 -14
  226. package/src/__tests__/globalCors.test.ts +217 -0
  227. package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
  228. package/src/__tests__/multiTenancy.security.test.ts +51 -0
  229. package/src/__tests__/multiTenancy.test.ts +1 -1
  230. package/src/__tests__/ownedRealtimeSession.test.ts +39 -0
  231. package/src/__tests__/pushStatusResolver.filter.test.ts +7 -0
  232. package/src/__tests__/realtimeRecordingHandler.test.ts +652 -0
  233. package/src/__tests__/serverExtensionHostOverlay.test.ts +66 -0
  234. package/src/__tests__/serverExtensionReservedRoots.test.ts +11 -98
  235. package/src/__tests__/sharedCache.test.ts +5 -3
  236. package/src/agentSessions/SessionJanitor.ts +369 -93
  237. package/src/agentSessions/SessionManager.ts +13 -1
  238. package/src/apiRoot.ts +78 -0
  239. package/src/auth/AuthProviderCatalogRouter.ts +26 -0
  240. package/src/auth/LoginScreenEngine.ts +50 -0
  241. package/src/auth/index.ts +1 -0
  242. package/src/auth/magicLink/MagicLinkRouter.ts +3 -2
  243. package/src/config.ts +46 -5
  244. package/src/coreRoutes.ts +166 -0
  245. package/src/generated/graphql-schemas/__mj.ts +1273 -10
  246. package/src/generic/CacheInvalidationResolver.ts +73 -18
  247. package/src/generic/PushStatusResolver.ts +1 -1
  248. package/src/generic/ResolverBase.ts +304 -325
  249. package/src/generic/RunViewResolver.ts +17 -0
  250. package/src/globalCors.ts +71 -0
  251. package/src/index.ts +116 -174
  252. package/src/multiTenancy/index.ts +24 -2
  253. package/src/realtimeSessions/RealtimeSessionVerificationService.ts +2 -1
  254. package/src/realtimeSessions/RealtimeVerifyRouter.ts +2 -1
  255. package/src/realtimeSessions/RecordingAssemblyScheduler.ts +234 -0
  256. package/src/realtimeSessions/index.ts +2 -0
  257. package/src/realtimeSessions/ownedRealtimeSession.ts +55 -0
  258. package/src/realtimeSessions/sessionAccess.ts +7 -3
  259. package/src/resolvers/ArtifactFileResolver.ts +9 -8
  260. package/src/resolvers/ConversationRunEventsResolver.ts +15 -11
  261. package/src/resolvers/DatasetResolver.ts +70 -10
  262. package/src/resolvers/EntityRecordNameResolver.ts +3 -2
  263. package/src/resolvers/FetchEntityVectorsResolver.ts +121 -39
  264. package/src/resolvers/FileResolver.ts +321 -106
  265. package/src/resolvers/GetDataContextDataResolver.ts +139 -41
  266. package/src/resolvers/GetDataResolver.ts +6 -3
  267. package/src/resolvers/IntegrationDiscoveryResolver.ts +4 -2
  268. package/src/resolvers/QueryResolver.ts +11 -0
  269. package/src/resolvers/RealtimeClientSessionResolver.ts +10 -33
  270. package/src/resolvers/RunAIAgentResolver.ts +39 -4
  271. package/src/resolvers/SearchKnowledgeStreamResolver.ts +19 -3
  272. package/src/resolvers/SqlLoggingConfigResolver.ts +2 -4
  273. package/src/resolvers/TaskGraphFrameResolver.ts +33 -1
  274. package/src/resolvers/TestQuerySQLResolver.ts +68 -13
  275. package/src/resolvers/TransactionGroupResolver.ts +131 -27
  276. package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
  277. package/src/resolvers/agent-run-data-guard.ts +89 -0
  278. package/src/rest/RealtimeRecordingHandler.ts +464 -0
  279. package/src/rest/ViewOperationsHandler.ts +29 -13
  280. package/src/rest/setupRESTEndpoints.ts +6 -0
  281. package/src/serverExtensionReservedRoots.ts +14 -17
  282. package/src/services/MJStorageBlobStore.ts +31 -30
  283. package/src/services/StartTaskGraphDispatcher.ts +2 -2
  284. package/src/services/TaskGraphActionRunner.ts +25 -5
  285. package/src/services/TaskGraphAgentRunner.ts +188 -11
  286. package/src/services/TaskGraphContinuationDeliverer.ts +704 -14
  287. 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
- /** Loads a session entity by ID, returning null when it cannot be loaded. */
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
- const serverExtensionSchema = z.object({
230
- Enabled: z.boolean().default(true),
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(serverExtensionSchema).optional().default([]),
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 serverExtensionSchema>;
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
+ }