@memberjunction/server 5.44.0 → 5.45.1

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 (267) hide show
  1. package/dist/agentSessions/ReturningVisitorRecap.d.ts +39 -0
  2. package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -0
  3. package/dist/agentSessions/ReturningVisitorRecap.js +198 -0
  4. package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -0
  5. package/dist/agentSessions/SessionJanitor.d.ts +13 -0
  6. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  7. package/dist/agentSessions/SessionJanitor.js +65 -7
  8. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  9. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  10. package/dist/agentSessions/SessionManager.js +37 -0
  11. package/dist/agentSessions/SessionManager.js.map +1 -1
  12. package/dist/auth/magicLink/MagicLinkRouter.d.ts +10 -0
  13. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  14. package/dist/auth/magicLink/MagicLinkRouter.js +16 -0
  15. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  16. package/dist/auth/magicLink/MagicLinkService.d.ts +7 -0
  17. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  18. package/dist/auth/magicLink/MagicLinkService.js +13 -3
  19. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  20. package/dist/auth/magicLink/index.d.ts +1 -1
  21. package/dist/auth/magicLink/index.d.ts.map +1 -1
  22. package/dist/auth/magicLink/index.js +1 -1
  23. package/dist/auth/magicLink/index.js.map +1 -1
  24. package/dist/auth/magicLink/magicLinkCore.d.ts +38 -1
  25. package/dist/auth/magicLink/magicLinkCore.d.ts.map +1 -1
  26. package/dist/auth/magicLink/magicLinkCore.js +42 -19
  27. package/dist/auth/magicLink/magicLinkCore.js.map +1 -1
  28. package/dist/auth/magicLink/types.d.ts +26 -0
  29. package/dist/auth/magicLink/types.d.ts.map +1 -1
  30. package/dist/config.d.ts +1883 -144
  31. package/dist/config.d.ts.map +1 -1
  32. package/dist/config.js +160 -36
  33. package/dist/config.js.map +1 -1
  34. package/dist/context.d.ts.map +1 -1
  35. package/dist/context.js +45 -2
  36. package/dist/context.js.map +1 -1
  37. package/dist/generated/generated.d.ts +522 -0
  38. package/dist/generated/generated.d.ts.map +1 -1
  39. package/dist/generated/generated.js +2860 -0
  40. package/dist/generated/generated.js.map +1 -1
  41. package/dist/generic/ResolverBase.d.ts +13 -0
  42. package/dist/generic/ResolverBase.d.ts.map +1 -1
  43. package/dist/generic/ResolverBase.js +22 -0
  44. package/dist/generic/ResolverBase.js.map +1 -1
  45. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  46. package/dist/generic/RunViewResolver.js +4 -0
  47. package/dist/generic/RunViewResolver.js.map +1 -1
  48. package/dist/index.d.ts +0 -2
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +101 -3
  51. package/dist/index.js.map +1 -1
  52. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
  53. package/dist/integration/CustomColumnPromoter.js +6 -0
  54. package/dist/integration/CustomColumnPromoter.js.map +1 -1
  55. package/dist/realtimeWidget/WidgetRouter.d.ts +26 -0
  56. package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -0
  57. package/dist/realtimeWidget/WidgetRouter.js +182 -0
  58. package/dist/realtimeWidget/WidgetRouter.js.map +1 -0
  59. package/dist/realtimeWidget/WidgetSessionService.d.ts +265 -0
  60. package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -0
  61. package/dist/realtimeWidget/WidgetSessionService.js +477 -0
  62. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -0
  63. package/dist/realtimeWidget/host-identity.d.ts +40 -0
  64. package/dist/realtimeWidget/host-identity.d.ts.map +1 -0
  65. package/dist/realtimeWidget/host-identity.js +58 -0
  66. package/dist/realtimeWidget/host-identity.js.map +1 -0
  67. package/dist/realtimeWidget/index.d.ts +10 -0
  68. package/dist/realtimeWidget/index.d.ts.map +1 -0
  69. package/dist/realtimeWidget/index.js +9 -0
  70. package/dist/realtimeWidget/index.js.map +1 -0
  71. package/dist/realtimeWidget/visitorIdentity.d.ts +76 -0
  72. package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -0
  73. package/dist/realtimeWidget/visitorIdentity.js +202 -0
  74. package/dist/realtimeWidget/visitorIdentity.js.map +1 -0
  75. package/dist/realtimeWidget/widgetCore.d.ts +106 -0
  76. package/dist/realtimeWidget/widgetCore.d.ts.map +1 -0
  77. package/dist/realtimeWidget/widgetCore.js +173 -0
  78. package/dist/realtimeWidget/widgetCore.js.map +1 -0
  79. package/dist/realtimeWidget/widgetGuestElevation.d.ts +51 -0
  80. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -0
  81. package/dist/realtimeWidget/widgetGuestElevation.js +79 -0
  82. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -0
  83. package/dist/resolvers/ComponentRegistryResolver.d.ts +7 -0
  84. package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
  85. package/dist/resolvers/ComponentRegistryResolver.js +31 -8
  86. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  87. package/dist/resolvers/EntityPermissionResolver.d.ts.map +1 -1
  88. package/dist/resolvers/EntityPermissionResolver.js +1 -2
  89. package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
  90. package/dist/resolvers/QuerySystemUserResolver.d.ts +9 -6
  91. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  92. package/dist/resolvers/QuerySystemUserResolver.js +15 -13
  93. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  94. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -0
  95. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  96. package/dist/resolvers/RealtimeClientSessionResolver.js +47 -3
  97. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  98. package/dist/resolvers/RingCentralTelephonyResolver.d.ts +27 -0
  99. package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +1 -0
  100. package/dist/resolvers/RingCentralTelephonyResolver.js +87 -0
  101. package/dist/resolvers/RingCentralTelephonyResolver.js.map +1 -0
  102. package/dist/resolvers/RunAIAgentResolver.d.ts +9 -0
  103. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  104. package/dist/resolvers/RunAIAgentResolver.js +20 -4
  105. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  106. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  107. package/dist/resolvers/SearchKnowledgeResolver.js +2 -0
  108. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  109. package/dist/resolvers/TeamsMeetingsResolver.d.ts +28 -0
  110. package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +1 -0
  111. package/dist/resolvers/TeamsMeetingsResolver.js +91 -0
  112. package/dist/resolvers/TeamsMeetingsResolver.js.map +1 -0
  113. package/dist/resolvers/TelephonyResolver.d.ts +26 -0
  114. package/dist/resolvers/TelephonyResolver.d.ts.map +1 -0
  115. package/dist/resolvers/TelephonyResolver.js +86 -0
  116. package/dist/resolvers/TelephonyResolver.js.map +1 -0
  117. package/dist/resolvers/TestQuerySQLResolver.d.ts +1 -1
  118. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  119. package/dist/resolvers/TestQuerySQLResolver.js +2 -3
  120. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  121. package/dist/resolvers/VonageTelephonyResolver.d.ts +26 -0
  122. package/dist/resolvers/VonageTelephonyResolver.d.ts.map +1 -0
  123. package/dist/resolvers/VonageTelephonyResolver.js +86 -0
  124. package/dist/resolvers/VonageTelephonyResolver.js.map +1 -0
  125. package/dist/rest/MediaStreamHandler.js +4 -1
  126. package/dist/rest/MediaStreamHandler.js.map +1 -1
  127. package/dist/telephony/RingCentralTelephonyService.d.ts +115 -0
  128. package/dist/telephony/RingCentralTelephonyService.d.ts.map +1 -0
  129. package/dist/telephony/RingCentralTelephonyService.js +262 -0
  130. package/dist/telephony/RingCentralTelephonyService.js.map +1 -0
  131. package/dist/telephony/TeamsMeetingsRouter.d.ts +40 -0
  132. package/dist/telephony/TeamsMeetingsRouter.d.ts.map +1 -0
  133. package/dist/telephony/TeamsMeetingsRouter.js +96 -0
  134. package/dist/telephony/TeamsMeetingsRouter.js.map +1 -0
  135. package/dist/telephony/TeamsMeetingsService.d.ts +113 -0
  136. package/dist/telephony/TeamsMeetingsService.d.ts.map +1 -0
  137. package/dist/telephony/TeamsMeetingsService.js +196 -0
  138. package/dist/telephony/TeamsMeetingsService.js.map +1 -0
  139. package/dist/telephony/TwilioTelephonyRouter.d.ts +36 -0
  140. package/dist/telephony/TwilioTelephonyRouter.d.ts.map +1 -0
  141. package/dist/telephony/TwilioTelephonyRouter.js +143 -0
  142. package/dist/telephony/TwilioTelephonyRouter.js.map +1 -0
  143. package/dist/telephony/TwilioTelephonyService.d.ts +95 -0
  144. package/dist/telephony/TwilioTelephonyService.d.ts.map +1 -0
  145. package/dist/telephony/TwilioTelephonyService.js +193 -0
  146. package/dist/telephony/TwilioTelephonyService.js.map +1 -0
  147. package/dist/telephony/VonageTelephonyRouter.d.ts +41 -0
  148. package/dist/telephony/VonageTelephonyRouter.d.ts.map +1 -0
  149. package/dist/telephony/VonageTelephonyRouter.js +201 -0
  150. package/dist/telephony/VonageTelephonyRouter.js.map +1 -0
  151. package/dist/telephony/VonageTelephonyService.d.ts +90 -0
  152. package/dist/telephony/VonageTelephonyService.d.ts.map +1 -0
  153. package/dist/telephony/VonageTelephonyService.js +191 -0
  154. package/dist/telephony/VonageTelephonyService.js.map +1 -0
  155. package/dist/telephony/calendar-scheduler.d.ts +67 -0
  156. package/dist/telephony/calendar-scheduler.d.ts.map +1 -0
  157. package/dist/telephony/calendar-scheduler.js +133 -0
  158. package/dist/telephony/calendar-scheduler.js.map +1 -0
  159. package/dist/telephony/index.d.ts +29 -0
  160. package/dist/telephony/index.d.ts.map +1 -0
  161. package/dist/telephony/index.js +38 -0
  162. package/dist/telephony/index.js.map +1 -0
  163. package/dist/telephony/media-upgrade-router.d.ts +33 -0
  164. package/dist/telephony/media-upgrade-router.d.ts.map +1 -0
  165. package/dist/telephony/media-upgrade-router.js +56 -0
  166. package/dist/telephony/media-upgrade-router.js.map +1 -0
  167. package/dist/telephony/ringcentral-runtime.d.ts +14 -0
  168. package/dist/telephony/ringcentral-runtime.d.ts.map +1 -0
  169. package/dist/telephony/ringcentral-runtime.js +18 -0
  170. package/dist/telephony/ringcentral-runtime.js.map +1 -0
  171. package/dist/telephony/teams-meetings-runtime.d.ts +16 -0
  172. package/dist/telephony/teams-meetings-runtime.d.ts.map +1 -0
  173. package/dist/telephony/teams-meetings-runtime.js +20 -0
  174. package/dist/telephony/teams-meetings-runtime.js.map +1 -0
  175. package/dist/telephony/teamsAcsMediaRegistry.d.ts +69 -0
  176. package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +1 -0
  177. package/dist/telephony/teamsAcsMediaRegistry.js +118 -0
  178. package/dist/telephony/teamsAcsMediaRegistry.js.map +1 -0
  179. package/dist/telephony/telephony-runtime.d.ts +14 -0
  180. package/dist/telephony/telephony-runtime.d.ts.map +1 -0
  181. package/dist/telephony/telephony-runtime.js +18 -0
  182. package/dist/telephony/telephony-runtime.js.map +1 -0
  183. package/dist/telephony/twilioMediaRegistry.d.ts +60 -0
  184. package/dist/telephony/twilioMediaRegistry.d.ts.map +1 -0
  185. package/dist/telephony/twilioMediaRegistry.js +106 -0
  186. package/dist/telephony/twilioMediaRegistry.js.map +1 -0
  187. package/dist/telephony/vonage-runtime.d.ts +14 -0
  188. package/dist/telephony/vonage-runtime.d.ts.map +1 -0
  189. package/dist/telephony/vonage-runtime.js +18 -0
  190. package/dist/telephony/vonage-runtime.js.map +1 -0
  191. package/dist/telephony/vonageMediaRegistry.d.ts +78 -0
  192. package/dist/telephony/vonageMediaRegistry.d.ts.map +1 -0
  193. package/dist/telephony/vonageMediaRegistry.js +158 -0
  194. package/dist/telephony/vonageMediaRegistry.js.map +1 -0
  195. package/package.json +88 -83
  196. package/src/__tests__/RunAIAgentResolver.streaming.test.ts +128 -0
  197. package/src/__tests__/magicLink.test.ts +53 -3
  198. package/src/__tests__/search-knowledge-tags.test.ts +9 -9
  199. package/src/__tests__/teams-meetings-service.test.ts +65 -0
  200. package/src/__tests__/teamsAcsMediaRegistry.test.ts +82 -0
  201. package/src/__tests__/twilio-telephony-service.test.ts +23 -0
  202. package/src/__tests__/twilioMediaRegistry.test.ts +103 -0
  203. package/src/__tests__/vonageMediaRegistry.test.ts +180 -0
  204. package/src/__tests__/widget.test.ts +204 -0
  205. package/src/__tests__/widgetGuestElevation.test.ts +57 -0
  206. package/src/__tests__/widgetHostIdentity.test.ts +117 -0
  207. package/src/agentSessions/ReturningVisitorRecap.ts +242 -0
  208. package/src/agentSessions/SessionJanitor.ts +66 -6
  209. package/src/agentSessions/SessionManager.ts +38 -0
  210. package/src/auth/magicLink/MagicLinkRouter.ts +17 -0
  211. package/src/auth/magicLink/MagicLinkService.ts +13 -3
  212. package/src/auth/magicLink/index.ts +1 -0
  213. package/src/auth/magicLink/magicLinkCore.ts +48 -20
  214. package/src/auth/magicLink/types.ts +26 -0
  215. package/src/config.ts +172 -38
  216. package/src/context.ts +50 -3
  217. package/src/generated/generated.ts +1991 -1
  218. package/src/generic/ResolverBase.ts +28 -0
  219. package/src/generic/RunViewResolver.ts +4 -0
  220. package/src/index.ts +109 -3
  221. package/src/integration/CustomColumnPromoter.ts +6 -0
  222. package/src/realtimeWidget/WidgetRouter.ts +204 -0
  223. package/src/realtimeWidget/WidgetSessionService.ts +714 -0
  224. package/src/realtimeWidget/host-identity.ts +84 -0
  225. package/src/realtimeWidget/index.ts +15 -0
  226. package/src/realtimeWidget/visitorIdentity.ts +258 -0
  227. package/src/realtimeWidget/widgetCore.ts +231 -0
  228. package/src/realtimeWidget/widgetGuestElevation.ts +115 -0
  229. package/src/resolvers/ComponentRegistryResolver.ts +36 -10
  230. package/src/resolvers/EntityPermissionResolver.ts +1 -2
  231. package/src/resolvers/QuerySystemUserResolver.ts +14 -10
  232. package/src/resolvers/RealtimeClientSessionResolver.ts +59 -2
  233. package/src/resolvers/RingCentralTelephonyResolver.ts +64 -0
  234. package/src/resolvers/RunAIAgentResolver.ts +32 -8
  235. package/src/resolvers/SearchKnowledgeResolver.ts +2 -0
  236. package/src/resolvers/TeamsMeetingsResolver.ts +68 -0
  237. package/src/resolvers/TelephonyResolver.ts +63 -0
  238. package/src/resolvers/TestQuerySQLResolver.ts +2 -3
  239. package/src/resolvers/VonageTelephonyResolver.ts +63 -0
  240. package/src/rest/MediaStreamHandler.ts +4 -1
  241. package/src/telephony/RingCentralTelephonyService.ts +342 -0
  242. package/src/telephony/TeamsMeetingsRouter.ts +124 -0
  243. package/src/telephony/TeamsMeetingsService.ts +268 -0
  244. package/src/telephony/TwilioTelephonyRouter.ts +184 -0
  245. package/src/telephony/TwilioTelephonyService.ts +261 -0
  246. package/src/telephony/VonageTelephonyRouter.ts +239 -0
  247. package/src/telephony/VonageTelephonyService.ts +259 -0
  248. package/src/telephony/calendar-scheduler.ts +176 -0
  249. package/src/telephony/index.ts +43 -0
  250. package/src/telephony/media-upgrade-router.ts +62 -0
  251. package/src/telephony/ringcentral-runtime.ts +22 -0
  252. package/src/telephony/teams-meetings-runtime.ts +24 -0
  253. package/src/telephony/teamsAcsMediaRegistry.ts +152 -0
  254. package/src/telephony/telephony-runtime.ts +22 -0
  255. package/src/telephony/twilioMediaRegistry.ts +137 -0
  256. package/src/telephony/vonage-runtime.ts +22 -0
  257. package/src/telephony/vonageMediaRegistry.ts +200 -0
  258. package/dist/agents/skip-agent.d.ts +0 -111
  259. package/dist/agents/skip-agent.d.ts.map +0 -1
  260. package/dist/agents/skip-agent.js +0 -1487
  261. package/dist/agents/skip-agent.js.map +0 -1
  262. package/dist/agents/skip-sdk.d.ts +0 -261
  263. package/dist/agents/skip-sdk.d.ts.map +0 -1
  264. package/dist/agents/skip-sdk.js +0 -909
  265. package/dist/agents/skip-sdk.js.map +0 -1
  266. package/src/agents/skip-agent.ts +0 -1594
  267. package/src/agents/skip-sdk.ts +0 -1210
@@ -194,6 +194,34 @@ export class ResolverBase {
194
194
  return dataObjectArray;
195
195
  }
196
196
 
197
+ /**
198
+ * Loads a single external-data-source-backed entity record by primary key and returns it in
199
+ * GraphQL field-name (CodeName) shape, or null if not found.
200
+ *
201
+ * External entities (`Entity.ExternalDataSourceID` set) have no MJ base view or sproc — their
202
+ * data is proxied live from a remote system — so the generated single-record resolver cannot run
203
+ * `SELECT * FROM <baseView>`. Instead it loads through a BaseEntity object, whose `InnerLoad`
204
+ * the data provider dispatches to the external read router's `LoadExternalRecord` (a composite-key
205
+ * aware, quoted, parameter-bound single-record lookup), applying the same RLS gate and field
206
+ * post-processing (decryption / datetime normalization) as the MJ-DB path. The caller is
207
+ * responsible for the `CheckUserReadPermissions` gate beforehand.
208
+ */
209
+ protected async LoadExternalRecordByKey<T>(
210
+ entityName: string,
211
+ compositeKey: CompositeKey,
212
+ provider: DatabaseProviderBase,
213
+ userPayload: UserPayload,
214
+ ): Promise<T | null> {
215
+ const contextUser = this.GetUserFromPayload(userPayload);
216
+ const entityObject = await provider.GetEntityObject(entityName, contextUser);
217
+ const loaded = await entityObject.InnerLoad(compositeKey);
218
+ if (!loaded) {
219
+ return null;
220
+ }
221
+ const mapped = await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
222
+ return mapped as T;
223
+ }
224
+
197
225
  /**
198
226
  * Filters encrypted field values before sending to the API client.
199
227
  *
@@ -715,6 +715,7 @@ export class RunViewResolver extends ResolverBase {
715
715
  pubSub: PubSubEngine
716
716
  ) {
717
717
  try {
718
+ await this.CheckAPIKeyScopeAuthorization('view:run', input.ViewName, userPayload);
718
719
  const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
719
720
  const rawData = await super.RunViewByNameGeneric(input, provider, userPayload, pubSub);
720
721
  if (rawData === null)
@@ -746,6 +747,7 @@ export class RunViewResolver extends ResolverBase {
746
747
  pubSub: PubSubEngine
747
748
  ) {
748
749
  try {
750
+ await this.CheckAPIKeyScopeAuthorization('view:run', input.ViewID, userPayload);
749
751
  const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
750
752
  const rawData = await super.RunViewByIDGeneric(input, provider, userPayload, pubSub);
751
753
  if (rawData === null)
@@ -777,6 +779,7 @@ export class RunViewResolver extends ResolverBase {
777
779
  pubSub: PubSubEngine
778
780
  ) {
779
781
  try {
782
+ await this.CheckAPIKeyScopeAuthorization('view:run', input.EntityName, userPayload);
780
783
  const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
781
784
  const rawData = await super.RunDynamicViewGeneric(input, provider, userPayload, pubSub);
782
785
  if (rawData === null) return null;
@@ -806,6 +809,7 @@ export class RunViewResolver extends ResolverBase {
806
809
  pubSub: PubSubEngine
807
810
  ) {
808
811
  try {
812
+ await this.CheckAPIKeyScopeAuthorization('view:batch', '*', userPayload);
809
813
  const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
810
814
  // Note: RunViewsGeneric returns the core RunViewResult type, not the GraphQL type
811
815
  const rawData = await super.RunViewsGeneric(input, provider, userPayload);
package/src/index.ts CHANGED
@@ -41,7 +41,13 @@ import { setupRESTEndpoints } from './rest/setupRESTEndpoints.js';
41
41
  import { createOAuthCallbackHandler } from './rest/OAuthCallbackHandler.js';
42
42
  import { createSignatureWebhookHandler } from './rest/SignatureWebhookHandler.js';
43
43
  import { createMediaStreamRouter } from './rest/MediaStreamHandler.js';
44
- import { createMagicLinkHandler, registerMagicLinkAuthProvider, MAGIC_LINK_MOUNT_PATH } from './auth/magicLink/index.js';
44
+ import { createMagicLinkHandler, createMagicLinkJwksRouter, registerMagicLinkAuthProvider, MAGIC_LINK_MOUNT_PATH } from './auth/magicLink/index.js';
45
+ import { createWidgetHandler, WIDGET_MOUNT_PATH } from './realtimeWidget/index.js';
46
+ import { createTwilioTelephonyHandler, TWILIO_TELEPHONY_MOUNT_PATH, SetTwilioTelephonyService } from './telephony/index.js';
47
+ import { createVonageTelephonyHandler, VONAGE_TELEPHONY_MOUNT_PATH, SetVonageTelephonyService } from './telephony/index.js';
48
+ import { RingCentralTelephonyService, SetRingCentralTelephonyService } from './telephony/index.js';
49
+ import { createTeamsMeetingsHandler, TEAMS_MEETINGS_MOUNT_PATH, SetTeamsMeetingsService, GetTeamsMeetingsService, StartCalendarScheduler } from './telephony/index.js';
50
+ import { InstallMediaUpgradeDispatcher } from './telephony/index.js';
45
51
 
46
52
  import { resolve } from 'node:path';
47
53
  import { DataSourceInfo, raiseEvent } from './types.js';
@@ -132,8 +138,6 @@ export * from './generic/KeyInputOutputTypes.js';
132
138
  export * from './generic/DeleteOptionsInput.js';
133
139
  export * from './generic/RestoreContextInput.js';
134
140
 
135
- export * from './agents/skip-agent.js';
136
- export * from './agents/skip-sdk.js';
137
141
 
138
142
  export * from './resolvers/GeoResolver.js';
139
143
  export * from './resolvers/ColorResolver.js';
@@ -1004,6 +1008,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
1004
1008
  // ─── Magic-link routes (MJ-issued, app-scoped external access) ───────────
1005
1009
  // Public router (JWKS + redeem) mounts BEFORE the auth middleware; the
1006
1010
  // authenticated invite-creation router mounts AFTER it (see below).
1011
+ let widgetAuthenticatedRouter: ReturnType<typeof createWidgetHandler>['authenticatedRouter'] | undefined;
1007
1012
  let magicLinkAuthenticatedRouter: ReturnType<typeof createMagicLinkHandler>['authenticatedRouter'] | undefined;
1008
1013
  if (configInfo.magicLink?.enabled) {
1009
1014
  const { publicRouter, authenticatedRouter } = createMagicLinkHandler(oauthPublicUrl, configInfo.magicLink);
@@ -1013,6 +1018,82 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
1013
1018
  startupLog.LogIf('verbose', `[MagicLink] Public routes registered at ${MAGIC_LINK_MOUNT_PATH}/redeem and ${MAGIC_LINK_MOUNT_PATH}/jwks.json`);
1014
1019
  }
1015
1020
 
1021
+ // ─── Public web widget: guest-session mint (PUBLIC, before auth mw) ───────
1022
+ // Visitors hold no MJ JWT yet, so POST /widget/session is public. It reuses the
1023
+ // magic-link RS256 key + `magic-link` auth provider (ensured idempotently inside
1024
+ // createWidgetHandler), so it stands on its own even if magicLink.enabled is false.
1025
+ if (configInfo.widget?.enabled) {
1026
+ const { publicRouter: widgetRouter, authenticatedRouter: widgetAuthRouter } = createWidgetHandler(oauthPublicUrl, configInfo.widget);
1027
+ widgetAuthenticatedRouter = widgetAuthRouter;
1028
+ app.use(WIDGET_MOUNT_PATH, cors<cors.CorsRequest>(), widgetRouter);
1029
+ // The widget reuses the magic-link RS256 key + auth provider to validate guest
1030
+ // tokens, which validates by fetching the JWKS at MAGIC_LINK_MOUNT_PATH/jwks.json.
1031
+ // When magic-link itself is disabled its public router (which serves JWKS) never
1032
+ // mounts, so publish the key here — otherwise every guest token fails validation
1033
+ // (the auth middleware can't fetch the public key) and the widget 401s.
1034
+ if (!configInfo.magicLink?.enabled) {
1035
+ app.use(MAGIC_LINK_MOUNT_PATH, cors<cors.CorsRequest>(), createMagicLinkJwksRouter());
1036
+ startupLog.LogIf('verbose', `[Widget] Published reused signing key at ${MAGIC_LINK_MOUNT_PATH}/jwks.json (magic-link flow disabled)`);
1037
+ }
1038
+ startupLog.LogIf('verbose', `[Widget] Public routes registered at ${WIDGET_MOUNT_PATH}/session and ${WIDGET_MOUNT_PATH}/session/refresh`);
1039
+ }
1040
+
1041
+ // ─── Telephony (Twilio) ingress: inbound voice webhook + Media-Streams WSS (PUBLIC) ──
1042
+ // Carriers cannot present an MJ JWT — the X-Twilio-Signature HMAC is the gate. The
1043
+ // public webhook router mounts BEFORE the auth middleware; the Media-Streams WSS attaches
1044
+ // to the shared HTTP server. The outbound PlaceTwilioCall mutation reuses the same service.
1045
+ if (configInfo.telephony?.enabled && configInfo.telephony.twilio) {
1046
+ const twilioHandler = createTwilioTelephonyHandler(oauthPublicUrl, configInfo.telephony.twilio);
1047
+ app.use(TWILIO_TELEPHONY_MOUNT_PATH, cors<cors.CorsRequest>(), twilioHandler.publicRouter);
1048
+ twilioHandler.attachMediaStreamServer();
1049
+ SetTwilioTelephonyService(twilioHandler.service);
1050
+ startupLog.LogIf('verbose', `[Telephony] Twilio routes registered at ${TWILIO_TELEPHONY_MOUNT_PATH}/voice + Media-Streams WSS`);
1051
+ }
1052
+
1053
+ // ─── Telephony (Vonage) ingress: inbound answer/event webhooks + media WSS (PUBLIC) ──
1054
+ // Carriers cannot present an MJ JWT — the Vonage signed-request HMAC / webhook JWT is the gate.
1055
+ // The public router mounts BEFORE the auth middleware; the media WSS attaches to the shared
1056
+ // HTTP server. The outbound PlaceVonageCall mutation reuses the same service.
1057
+ if (configInfo.telephony?.enabled && configInfo.telephony.vonage) {
1058
+ const vonageHandler = createVonageTelephonyHandler(oauthPublicUrl, configInfo.telephony.vonage);
1059
+ app.use(VONAGE_TELEPHONY_MOUNT_PATH, cors<cors.CorsRequest>(), vonageHandler.publicRouter);
1060
+ vonageHandler.attachMediaStreamServer();
1061
+ SetVonageTelephonyService(vonageHandler.service);
1062
+ startupLog.LogIf('verbose', `[Telephony] Vonage routes registered at ${VONAGE_TELEPHONY_MOUNT_PATH}/answer + /event + media WSS`);
1063
+ }
1064
+
1065
+ // ─── Telephony (RingCentral) ingress: SIP softphone registration (no HTTP webhook / media WSS) ──
1066
+ // RingCentral's only bidirectional-audio transport is a registered SIP softphone — inbound calls arrive
1067
+ // as SIP INVITEs on its own SIP/TLS connection, so there is no public webhook or media WSS to mount.
1068
+ // start() registers the softphone fire-and-forget so SIP registration never blocks boot; the outbound
1069
+ // PlaceRingCentralCall mutation reuses the same service via the runtime holder.
1070
+ if (configInfo.telephony?.enabled && configInfo.telephony.ringcentral) {
1071
+ const ringCentralService = new RingCentralTelephonyService(configInfo.telephony.ringcentral);
1072
+ SetRingCentralTelephonyService(ringCentralService);
1073
+ void ringCentralService.start();
1074
+ startupLog.LogIf('verbose', `[Telephony] RingCentral SIP softphone starting (codec ${configInfo.telephony.ringcentral.codec ?? 'OPUS/16000'})`);
1075
+ }
1076
+
1077
+ // ─── Teams meetings ingress: Graph change-notification webhook (PUBLIC) ──────────────
1078
+ // Graph cannot present an MJ JWT — the subscription validationToken handshake + the per-
1079
+ // notification clientState shared secret are the gate. The public webhook router mounts
1080
+ // BEFORE the auth middleware. The ACS application-hosted-media audio plane is owned by the
1081
+ // server's native ACS media adapter, which attaches transports to the shared registry
1082
+ // (a media WSS is not needed here). The StartTeamsMeetingSession mutation reuses the same
1083
+ // service via the runtime holder.
1084
+ if (configInfo.telephony?.enabled && configInfo.telephony.teams?.enabled) {
1085
+ const teamsHandler = createTeamsMeetingsHandler(configInfo.telephony.teams);
1086
+ app.use(TEAMS_MEETINGS_MOUNT_PATH, cors<cors.CorsRequest>(), teamsHandler.publicRouter);
1087
+ SetTeamsMeetingsService(teamsHandler.service);
1088
+ startupLog.LogIf('verbose', `[Meetings] Teams routes registered at ${TEAMS_MEETINGS_MOUNT_PATH}/notifications`);
1089
+ }
1090
+
1091
+ // Install the single path-routing WebSocket-upgrade dispatcher AFTER all media WSS routes have
1092
+ // registered. ws 8.x has each {server}-bound WebSocketServer 400 paths it doesn't own, so the GraphQL
1093
+ // socket and the telephony media sockets cannot coexist as separate {server} servers — this strips the
1094
+ // auto-listeners and routes upgrades by path. No-op when no media routes registered (telephony off).
1095
+ InstallMediaUpgradeDispatcher(httpServer, webSocketServer, graphqlRootPath);
1096
+
1016
1097
  // ─── Global CORS (before auth so 401 responses include CORS headers) ─────
1017
1098
  // Without this, the browser blocks 401 responses from the auth middleware
1018
1099
  // because they lack Access-Control-Allow-Origin headers, preventing the
@@ -1073,6 +1154,14 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
1073
1154
  startupLog.LogIf('verbose', `[MagicLink] Authenticated route registered at ${MAGIC_LINK_MOUNT_PATH}/create`);
1074
1155
  }
1075
1156
 
1157
+ // ─── Widget authenticated route (RV4 resolve-identity) ────────────────────
1158
+ // Mounts after the unified auth middleware so a verified visitor (post magic-link upgrade) can
1159
+ // promote their anonymous returning-visitor trail to the verified record.
1160
+ if (widgetAuthenticatedRouter) {
1161
+ app.use(WIDGET_MOUNT_PATH, cors<cors.CorsRequest>(), widgetAuthenticatedRouter);
1162
+ startupLog.LogIf('verbose', `[Widget] Authenticated route registered at ${WIDGET_MOUNT_PATH}/resolve-identity`);
1163
+ }
1164
+
1076
1165
  // ─── REST API endpoints (auth already handled by unified middleware) ─────
1077
1166
  const restApiConfig = {
1078
1167
  enabled: configInfo.restApiOptions?.enabled ?? false,
@@ -1193,6 +1282,23 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
1193
1282
  .catch(err => console.warn(`[SessionJanitor] Startup failed: ${err}`));
1194
1283
  }
1195
1284
 
1285
+ // Launch the calendar / scheduled-bridge loop (M2): poll agent calendars for meeting invites and
1286
+ // start due meeting bridges. Mirrors the SessionJanitor lifecycle (run-once + interval, timer
1287
+ // unref'd). Gated on Teams meetings being enabled (the provider whose scheduled-join is wired) and
1288
+ // reuses the SAME meetings service as the ingress; identities without configured calendar creds are
1289
+ // skipped, so this is a harmless no-op until a Graph-backed identity + token are configured.
1290
+ if (resumeUser && configInfo.telephony?.teams?.enabled) { // global-provider-ok: server-owned background poller under the server's provider + system user
1291
+ const teamsMeetingsService = GetTeamsMeetingsService();
1292
+ if (teamsMeetingsService) {
1293
+ StartCalendarScheduler({
1294
+ Provider: Metadata.Provider, // global-provider-ok: server-owned background poller under the server's single default provider + system user
1295
+ ContextUser: resumeUser,
1296
+ TeamsService: teamsMeetingsService,
1297
+ TeamsConfig: configInfo.telephony.teams,
1298
+ });
1299
+ }
1300
+ }
1301
+
1196
1302
  // Set up graceful shutdown handlers
1197
1303
  const gracefulShutdown = async (signal: string) => {
1198
1304
  console.log(`\n${signal} received, shutting down gracefully...`);
@@ -217,6 +217,12 @@ export class IntegrationCustomColumnPromoter {
217
217
  const overflowJson = await this.scanOverflow(entityName);
218
218
  if (overflowJson.length === 0) return null; // no customs captured
219
219
 
220
+ // U3 note (rkihm-BC review, #3061): this in-repo promotion path passes no `LockUntilFullSync`, so it
221
+ // does NOT yet enforce "hold promotion until a full sync since the last schema change." The lever
222
+ // exists on `PromotionPlanOptions`, but pulling it here requires this caller to know whether a full
223
+ // sync has completed post-rediscovery — DEFERRED and tracked as a follow-up. The engine ships the
224
+ // gate; wiring MJServer's own consumer to set it is a separate change. Until then, this path retains
225
+ // the pre-U3 behavior for a rediscover-then-incremental sequence.
220
226
  const passing = planPromotions(buildOverflowStats(overflowJson), {});
221
227
  if (passing.length === 0) return null;
222
228
 
@@ -0,0 +1,204 @@
1
+ /**
2
+ * @fileoverview Express wiring for the public web widget guest-session endpoints.
3
+ *
4
+ * Mirrors the magic-link public-router pattern: a single PUBLIC router
5
+ * (unauthenticated) mounted BEFORE the unified auth middleware. The widget reuses
6
+ * the magic-link RS256 key + the registered `magic-link` auth provider for token
7
+ * validation, so this module ensures both are initialized (idempotently) even when
8
+ * `magicLink.enabled` is false — the widget can stand on its own.
9
+ *
10
+ * @module @memberjunction/server/widget
11
+ */
12
+
13
+ import { Router, json, type Request, type Response } from 'express';
14
+ import { rateLimit } from 'express-rate-limit';
15
+ import { LogStatus } from '@memberjunction/core';
16
+ import { configInfo, type WidgetConfig } from '../config.js';
17
+ import { MagicLinkKeyManager } from '../auth/magicLink/MagicLinkKeys.js';
18
+ import { registerMagicLinkAuthProvider } from '../auth/magicLink/MagicLinkRouter.js';
19
+ import { WidgetSessionService, type MintGuestSessionResult } from './WidgetSessionService.js';
20
+ import { looksLikeBot } from './widgetCore.js';
21
+
22
+ /** The mount path for the widget public router (`/widget`). */
23
+ export const WIDGET_MOUNT_PATH = '/widget';
24
+
25
+ /**
26
+ * Ensures the magic-link signing key is initialized and the `magic-link` auth
27
+ * provider is registered, so widget tokens (signed with that key) validate. Safe to
28
+ * call when magic-link is already enabled — both operations are idempotent.
29
+ */
30
+ function ensureWidgetSigning(publicUrl: string, widgetConfig: WidgetConfig): void {
31
+ const mlConfig = configInfo.magicLink;
32
+ // The widget shares the magic-link audience so the same provider validates its
33
+ // tokens; surface a clear log if a deployment diverged them.
34
+ if (mlConfig?.audience && mlConfig.audience !== widgetConfig.audience) {
35
+ LogStatus(
36
+ `[Widget] WARNING: widget.audience ('${widgetConfig.audience}') != magicLink.audience ('${mlConfig.audience}'). ` +
37
+ `Widget tokens will not validate unless an auth provider is registered for the widget audience.`,
38
+ );
39
+ }
40
+ MagicLinkKeyManager.Instance.Initialize(mlConfig?.rsaPrivateKey);
41
+ // Register the magic-link provider against the magic-link config (its audience),
42
+ // which is what validates widget tokens. No-op if already registered.
43
+ if (mlConfig) {
44
+ registerMagicLinkAuthProvider(publicUrl, mlConfig);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Builds the widget routers. Mount `publicRouter` at WIDGET_MOUNT_PATH BEFORE the unified auth
50
+ * middleware (the mint/forget endpoints are public — visitors have no MJ JWT yet) and
51
+ * `authenticatedRouter` at the same path AFTER it (the RV4 resolve-identity endpoint needs the
52
+ * verified principal from `req.userPayload`).
53
+ */
54
+ export function createWidgetHandler(publicUrl: string, config: WidgetConfig): { publicRouter: Router; authenticatedRouter: Router } {
55
+ ensureWidgetSigning(publicUrl, config);
56
+
57
+ const service = new WidgetSessionService(publicUrl, config);
58
+
59
+ // Per-instance DYNAMIC mint limiter (W6 hardening): keyed by (widgetKey + IP) so each widget
60
+ // deployment gets its own bucket and one abusive IP can't exhaust a shared instance's budget for
61
+ // everyone. The per-request `limit` is the widget instance's stored `RateLimitPerMinute` (cached,
62
+ // resolved from the parsed body), falling back to the deployment-wide default for an unknown key.
63
+ // `json()` runs FIRST so the key generator + limit can read `req.body.widgetKey`.
64
+ const mintLimiter = rateLimit({
65
+ windowMs: config.rateLimitWindowMs,
66
+ standardHeaders: 'draft-7',
67
+ legacyHeaders: false,
68
+ keyGenerator: (req: Request) => `${readWidgetKey(req)}:${req.ip ?? 'noip'}`,
69
+ limit: async (req: Request) => service.ResolvePerInstanceRateLimit(readWidgetKey(req)),
70
+ message: { success: false, errorCode: 'rate_limited', error: 'Too many widget session requests. Try again later.' },
71
+ });
72
+
73
+ const publicRouter = Router();
74
+
75
+ publicRouter.post('/session', json(), mintLimiter, async (req: Request, res: Response) => {
76
+ await handleMint(service, req, res, false);
77
+ });
78
+
79
+ publicRouter.post('/session/refresh', json(), mintLimiter, async (req: Request, res: Response) => {
80
+ await handleMint(service, req, res, true);
81
+ });
82
+
83
+ // W5 magic-link upgrade ("Verify it's you"): emails a verification link scoped to the widget's app.
84
+ // Same dynamic limiter as mint — it issues an email invite, a comparable cost/abuse surface.
85
+ publicRouter.post('/upgrade', json(), mintLimiter, async (req: Request, res: Response) => {
86
+ await handleUpgrade(service, req, res);
87
+ });
88
+
89
+ // RV5 "forget me": archive the visitor's memory + clear the VisitorKey linkage. Public — the visitor
90
+ // proves possession of the durable key (not an identity). Same dynamic limiter as mint.
91
+ publicRouter.post('/forget', json(), mintLimiter, async (req: Request, res: Response) => {
92
+ await handleForget(service, req, res);
93
+ });
94
+
95
+ // RV4 resolve-identity: promote the visitor's anonymous trail to the verified record. AUTHENTICATED —
96
+ // it reads the verified principal's email from req.userPayload, so it mounts after the auth middleware.
97
+ const authenticatedRouter = Router();
98
+ authenticatedRouter.post('/resolve-identity', json(), mintLimiter, async (req: Request, res: Response) => {
99
+ await handleResolveIdentity(service, req, res);
100
+ });
101
+
102
+ return { publicRouter, authenticatedRouter };
103
+ }
104
+
105
+ /** Reads the `widgetKey` from a parsed JSON body, or '' when absent (used to key the dynamic limiter). */
106
+ function readWidgetKey(req: Request): string {
107
+ const body = (req.body ?? {}) as { widgetKey?: string };
108
+ return typeof body.widgetKey === 'string' ? body.widgetKey.trim() : '';
109
+ }
110
+
111
+ /** Handles "forget me" (RV5) — parses the body, calls the service, maps the status. */
112
+ async function handleForget(service: WidgetSessionService, req: Request, res: Response): Promise<void> {
113
+ const body = (req.body ?? {}) as { widgetKey?: string; visitorKey?: string };
114
+ const widgetKey = typeof body.widgetKey === 'string' ? body.widgetKey : '';
115
+ const visitorKey = typeof body.visitorKey === 'string' ? body.visitorKey : '';
116
+ if (!widgetKey) {
117
+ res.status(400).json({ success: false, errorCode: 'not_found', error: 'widgetKey is required.' });
118
+ return;
119
+ }
120
+ const result = await service.ForgetVisitor({ widgetKey, visitorKey, origin: req.get('origin') ?? undefined });
121
+ res.status(result.success ? 200 : result.errorCode === 'server_error' ? 500 : 403).json(result);
122
+ }
123
+
124
+ /**
125
+ * Handles RV4 resolve-identity — reads the verified email from the authenticated principal
126
+ * (`req.userPayload.userRecord`), parses the body, calls the service, maps the status.
127
+ */
128
+ async function handleResolveIdentity(service: WidgetSessionService, req: Request, res: Response): Promise<void> {
129
+ const verifiedUser = req.userPayload?.userRecord;
130
+ if (!verifiedUser?.Email) {
131
+ res.status(401).json({ success: false, errorCode: 'server_error', error: 'Authentication required.' });
132
+ return;
133
+ }
134
+ const body = (req.body ?? {}) as { widgetKey?: string; visitorKey?: string };
135
+ const widgetKey = typeof body.widgetKey === 'string' ? body.widgetKey : '';
136
+ const visitorKey = typeof body.visitorKey === 'string' ? body.visitorKey : '';
137
+ if (!widgetKey) {
138
+ res.status(400).json({ success: false, errorCode: 'not_found', error: 'widgetKey is required.' });
139
+ return;
140
+ }
141
+ const result = await service.ResolveVisitorIdentity({
142
+ widgetKey,
143
+ visitorKey,
144
+ verifiedEmail: verifiedUser.Email,
145
+ origin: req.get('origin') ?? undefined,
146
+ });
147
+ res.status(result.success ? 200 : result.errorCode === 'server_error' ? 500 : 403).json(result);
148
+ }
149
+
150
+ /** Handles the magic-link upgrade request — parses the body, calls the service, maps the status. */
151
+ async function handleUpgrade(service: WidgetSessionService, req: Request, res: Response): Promise<void> {
152
+ const body = (req.body ?? {}) as { widgetKey?: string; email?: string };
153
+ const widgetKey = typeof body.widgetKey === 'string' ? body.widgetKey : '';
154
+ const email = typeof body.email === 'string' ? body.email : '';
155
+ if (!widgetKey) {
156
+ res.status(400).json({ success: false, errorCode: 'not_found', error: 'widgetKey is required.' });
157
+ return;
158
+ }
159
+ const result = await service.RequestUpgrade({ widgetKey, email, origin: req.get('origin') ?? undefined });
160
+ // 400 for a bad email (client can fix it); 403 for any policy rejection (uniform, non-enumerable); 500 for faults.
161
+ const status = result.success ? 200 : result.errorCode === 'invalid_email' ? 400 : result.errorCode === 'server_error' ? 500 : 403;
162
+ res.status(status).json(result);
163
+ }
164
+
165
+ /** Shared mint/refresh handler — parses the request, calls the service, maps the status. */
166
+ async function handleMint(service: WidgetSessionService, req: Request, res: Response, refresh: boolean): Promise<void> {
167
+ const body = (req.body ?? {}) as { widgetKey?: string; hostAssertion?: string; visitorKey?: string };
168
+ const widgetKey = typeof body.widgetKey === 'string' ? body.widgetKey : '';
169
+ if (!widgetKey) {
170
+ res.status(400).json({ success: false, errorCode: 'not_found', error: 'widgetKey is required.' });
171
+ return;
172
+ }
173
+
174
+ // W6 hardening: coarse bot/automation rejection before any DB lookup or token mint. Returns the same
175
+ // 403 as a policy rejection so a probe can't distinguish "bot-blocked" from "bad key/origin".
176
+ if (looksLikeBot(req.get('user-agent'))) {
177
+ res.status(403).json({ success: false, errorCode: 'origin_not_allowed', error: 'Widget mint rejected.' });
178
+ return;
179
+ }
180
+
181
+ const origin = req.get('origin') ?? undefined;
182
+ const input = {
183
+ widgetKey,
184
+ origin,
185
+ hostAssertion: typeof body.hostAssertion === 'string' ? body.hostAssertion : undefined,
186
+ visitorKey: typeof body.visitorKey === 'string' ? body.visitorKey : undefined,
187
+ audit: { ipAddress: req.ip, userAgent: req.get('user-agent') ?? undefined, origin },
188
+ };
189
+ const result = refresh ? await service.RefreshGuestSession(input) : await service.MintGuestSession(input);
190
+ res.status(statusForResult(result)).json(result);
191
+ }
192
+
193
+ /**
194
+ * Maps a mint result to an HTTP status. All client-side rejections (unknown key,
195
+ * disabled widget, disallowed origin, modality off) return 403 — deliberately
196
+ * uniform so a probe cannot distinguish "no such widget" from "wrong origin" and
197
+ * enumerate valid keys. Only true server faults return 500.
198
+ */
199
+ function statusForResult(result: MintGuestSessionResult): number {
200
+ if (result.success) {
201
+ return 200;
202
+ }
203
+ return result.errorCode === 'server_error' ? 500 : 403;
204
+ }