@memberjunction/server 5.43.0 → 5.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) 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 +40 -0
  11. package/dist/agentSessions/SessionManager.js.map +1 -1
  12. package/dist/agentSessions/index.d.ts.map +1 -1
  13. package/dist/agentSessions/index.js +3 -1
  14. package/dist/agentSessions/index.js.map +1 -1
  15. package/dist/auth/magicLink/MagicLinkRouter.d.ts +10 -0
  16. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  17. package/dist/auth/magicLink/MagicLinkRouter.js +16 -0
  18. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  19. package/dist/auth/magicLink/index.d.ts +1 -1
  20. package/dist/auth/magicLink/index.d.ts.map +1 -1
  21. package/dist/auth/magicLink/index.js +1 -1
  22. package/dist/auth/magicLink/index.js.map +1 -1
  23. package/dist/auth/magicLink/types.d.ts +26 -0
  24. package/dist/auth/magicLink/types.d.ts.map +1 -1
  25. package/dist/config.d.ts +1883 -144
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +160 -36
  28. package/dist/config.js.map +1 -1
  29. package/dist/context.d.ts.map +1 -1
  30. package/dist/context.js +45 -2
  31. package/dist/context.js.map +1 -1
  32. package/dist/generated/generated.d.ts +1684 -54
  33. package/dist/generated/generated.d.ts.map +1 -1
  34. package/dist/generated/generated.js +57105 -48029
  35. package/dist/generated/generated.js.map +1 -1
  36. package/dist/generic/ResolverBase.d.ts +13 -0
  37. package/dist/generic/ResolverBase.d.ts.map +1 -1
  38. package/dist/generic/ResolverBase.js +22 -0
  39. package/dist/generic/ResolverBase.js.map +1 -1
  40. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  41. package/dist/generic/RunViewResolver.js +4 -0
  42. package/dist/generic/RunViewResolver.js.map +1 -1
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +110 -3
  46. package/dist/index.js.map +1 -1
  47. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
  48. package/dist/integration/CustomColumnPromoter.js +6 -0
  49. package/dist/integration/CustomColumnPromoter.js.map +1 -1
  50. package/dist/realtimeWidget/WidgetRouter.d.ts +26 -0
  51. package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -0
  52. package/dist/realtimeWidget/WidgetRouter.js +182 -0
  53. package/dist/realtimeWidget/WidgetRouter.js.map +1 -0
  54. package/dist/realtimeWidget/WidgetSessionService.d.ts +265 -0
  55. package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -0
  56. package/dist/realtimeWidget/WidgetSessionService.js +477 -0
  57. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -0
  58. package/dist/realtimeWidget/host-identity.d.ts +40 -0
  59. package/dist/realtimeWidget/host-identity.d.ts.map +1 -0
  60. package/dist/realtimeWidget/host-identity.js +58 -0
  61. package/dist/realtimeWidget/host-identity.js.map +1 -0
  62. package/dist/realtimeWidget/index.d.ts +10 -0
  63. package/dist/realtimeWidget/index.d.ts.map +1 -0
  64. package/dist/realtimeWidget/index.js +9 -0
  65. package/dist/realtimeWidget/index.js.map +1 -0
  66. package/dist/realtimeWidget/visitorIdentity.d.ts +76 -0
  67. package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -0
  68. package/dist/realtimeWidget/visitorIdentity.js +202 -0
  69. package/dist/realtimeWidget/visitorIdentity.js.map +1 -0
  70. package/dist/realtimeWidget/widgetCore.d.ts +106 -0
  71. package/dist/realtimeWidget/widgetCore.d.ts.map +1 -0
  72. package/dist/realtimeWidget/widgetCore.js +173 -0
  73. package/dist/realtimeWidget/widgetCore.js.map +1 -0
  74. package/dist/realtimeWidget/widgetGuestElevation.d.ts +51 -0
  75. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -0
  76. package/dist/realtimeWidget/widgetGuestElevation.js +79 -0
  77. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -0
  78. package/dist/resolvers/ComponentRegistryResolver.d.ts +7 -0
  79. package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
  80. package/dist/resolvers/ComponentRegistryResolver.js +31 -8
  81. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  82. package/dist/resolvers/EntityPermissionResolver.d.ts.map +1 -1
  83. package/dist/resolvers/EntityPermissionResolver.js +1 -2
  84. package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
  85. package/dist/resolvers/FileResolver.d.ts +74 -0
  86. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  87. package/dist/resolvers/FileResolver.js +211 -2
  88. package/dist/resolvers/FileResolver.js.map +1 -1
  89. package/dist/resolvers/QueryResolver.d.ts +6 -4
  90. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  91. package/dist/resolvers/QueryResolver.js +29 -14
  92. package/dist/resolvers/QueryResolver.js.map +1 -1
  93. package/dist/resolvers/QuerySystemUserResolver.d.ts +9 -6
  94. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  95. package/dist/resolvers/QuerySystemUserResolver.js +15 -13
  96. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  97. package/dist/resolvers/RealtimeBridgeResolver.d.ts +8 -1
  98. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  99. package/dist/resolvers/RealtimeBridgeResolver.js +28 -4
  100. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  101. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +91 -2
  102. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  103. package/dist/resolvers/RealtimeClientSessionResolver.js +337 -17
  104. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  105. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  106. package/dist/resolvers/RemoteBrowserActionResolver.js +17 -2
  107. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  108. package/dist/resolvers/RingCentralTelephonyResolver.d.ts +27 -0
  109. package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +1 -0
  110. package/dist/resolvers/RingCentralTelephonyResolver.js +87 -0
  111. package/dist/resolvers/RingCentralTelephonyResolver.js.map +1 -0
  112. package/dist/resolvers/RunAIAgentResolver.d.ts +12 -3
  113. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  114. package/dist/resolvers/RunAIAgentResolver.js +50 -17
  115. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  116. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  117. package/dist/resolvers/SearchKnowledgeResolver.js +2 -0
  118. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  119. package/dist/resolvers/TeamsMeetingsResolver.d.ts +28 -0
  120. package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +1 -0
  121. package/dist/resolvers/TeamsMeetingsResolver.js +91 -0
  122. package/dist/resolvers/TeamsMeetingsResolver.js.map +1 -0
  123. package/dist/resolvers/TelephonyResolver.d.ts +26 -0
  124. package/dist/resolvers/TelephonyResolver.d.ts.map +1 -0
  125. package/dist/resolvers/TelephonyResolver.js +86 -0
  126. package/dist/resolvers/TelephonyResolver.js.map +1 -0
  127. package/dist/resolvers/TestQuerySQLResolver.d.ts +1 -1
  128. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  129. package/dist/resolvers/TestQuerySQLResolver.js +2 -3
  130. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  131. package/dist/resolvers/VonageTelephonyResolver.d.ts +26 -0
  132. package/dist/resolvers/VonageTelephonyResolver.d.ts.map +1 -0
  133. package/dist/resolvers/VonageTelephonyResolver.js +86 -0
  134. package/dist/resolvers/VonageTelephonyResolver.js.map +1 -0
  135. package/dist/resolvers/meetingRecordingRegistration.d.ts +124 -0
  136. package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -0
  137. package/dist/resolvers/meetingRecordingRegistration.js +311 -0
  138. package/dist/resolvers/meetingRecordingRegistration.js.map +1 -0
  139. package/dist/resolvers/peaksSidecar.d.ts +30 -0
  140. package/dist/resolvers/peaksSidecar.d.ts.map +1 -0
  141. package/dist/resolvers/peaksSidecar.js +51 -0
  142. package/dist/resolvers/peaksSidecar.js.map +1 -0
  143. package/dist/rest/MediaAccessKeys.d.ts +68 -0
  144. package/dist/rest/MediaAccessKeys.d.ts.map +1 -0
  145. package/dist/rest/MediaAccessKeys.js +96 -0
  146. package/dist/rest/MediaAccessKeys.js.map +1 -0
  147. package/dist/rest/MediaStreamHandler.d.ts +26 -0
  148. package/dist/rest/MediaStreamHandler.d.ts.map +1 -0
  149. package/dist/rest/MediaStreamHandler.js +195 -0
  150. package/dist/rest/MediaStreamHandler.js.map +1 -0
  151. package/dist/rest/mediaRange.d.ts +41 -0
  152. package/dist/rest/mediaRange.d.ts.map +1 -0
  153. package/dist/rest/mediaRange.js +60 -0
  154. package/dist/rest/mediaRange.js.map +1 -0
  155. package/dist/telephony/RingCentralTelephonyService.d.ts +115 -0
  156. package/dist/telephony/RingCentralTelephonyService.d.ts.map +1 -0
  157. package/dist/telephony/RingCentralTelephonyService.js +262 -0
  158. package/dist/telephony/RingCentralTelephonyService.js.map +1 -0
  159. package/dist/telephony/TeamsMeetingsRouter.d.ts +40 -0
  160. package/dist/telephony/TeamsMeetingsRouter.d.ts.map +1 -0
  161. package/dist/telephony/TeamsMeetingsRouter.js +96 -0
  162. package/dist/telephony/TeamsMeetingsRouter.js.map +1 -0
  163. package/dist/telephony/TeamsMeetingsService.d.ts +113 -0
  164. package/dist/telephony/TeamsMeetingsService.d.ts.map +1 -0
  165. package/dist/telephony/TeamsMeetingsService.js +196 -0
  166. package/dist/telephony/TeamsMeetingsService.js.map +1 -0
  167. package/dist/telephony/TwilioTelephonyRouter.d.ts +36 -0
  168. package/dist/telephony/TwilioTelephonyRouter.d.ts.map +1 -0
  169. package/dist/telephony/TwilioTelephonyRouter.js +143 -0
  170. package/dist/telephony/TwilioTelephonyRouter.js.map +1 -0
  171. package/dist/telephony/TwilioTelephonyService.d.ts +95 -0
  172. package/dist/telephony/TwilioTelephonyService.d.ts.map +1 -0
  173. package/dist/telephony/TwilioTelephonyService.js +193 -0
  174. package/dist/telephony/TwilioTelephonyService.js.map +1 -0
  175. package/dist/telephony/VonageTelephonyRouter.d.ts +41 -0
  176. package/dist/telephony/VonageTelephonyRouter.d.ts.map +1 -0
  177. package/dist/telephony/VonageTelephonyRouter.js +201 -0
  178. package/dist/telephony/VonageTelephonyRouter.js.map +1 -0
  179. package/dist/telephony/VonageTelephonyService.d.ts +90 -0
  180. package/dist/telephony/VonageTelephonyService.d.ts.map +1 -0
  181. package/dist/telephony/VonageTelephonyService.js +191 -0
  182. package/dist/telephony/VonageTelephonyService.js.map +1 -0
  183. package/dist/telephony/calendar-scheduler.d.ts +67 -0
  184. package/dist/telephony/calendar-scheduler.d.ts.map +1 -0
  185. package/dist/telephony/calendar-scheduler.js +133 -0
  186. package/dist/telephony/calendar-scheduler.js.map +1 -0
  187. package/dist/telephony/index.d.ts +29 -0
  188. package/dist/telephony/index.d.ts.map +1 -0
  189. package/dist/telephony/index.js +38 -0
  190. package/dist/telephony/index.js.map +1 -0
  191. package/dist/telephony/media-upgrade-router.d.ts +33 -0
  192. package/dist/telephony/media-upgrade-router.d.ts.map +1 -0
  193. package/dist/telephony/media-upgrade-router.js +56 -0
  194. package/dist/telephony/media-upgrade-router.js.map +1 -0
  195. package/dist/telephony/ringcentral-runtime.d.ts +14 -0
  196. package/dist/telephony/ringcentral-runtime.d.ts.map +1 -0
  197. package/dist/telephony/ringcentral-runtime.js +18 -0
  198. package/dist/telephony/ringcentral-runtime.js.map +1 -0
  199. package/dist/telephony/teams-meetings-runtime.d.ts +16 -0
  200. package/dist/telephony/teams-meetings-runtime.d.ts.map +1 -0
  201. package/dist/telephony/teams-meetings-runtime.js +20 -0
  202. package/dist/telephony/teams-meetings-runtime.js.map +1 -0
  203. package/dist/telephony/teamsAcsMediaRegistry.d.ts +69 -0
  204. package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +1 -0
  205. package/dist/telephony/teamsAcsMediaRegistry.js +118 -0
  206. package/dist/telephony/teamsAcsMediaRegistry.js.map +1 -0
  207. package/dist/telephony/telephony-runtime.d.ts +14 -0
  208. package/dist/telephony/telephony-runtime.d.ts.map +1 -0
  209. package/dist/telephony/telephony-runtime.js +18 -0
  210. package/dist/telephony/telephony-runtime.js.map +1 -0
  211. package/dist/telephony/twilioMediaRegistry.d.ts +60 -0
  212. package/dist/telephony/twilioMediaRegistry.d.ts.map +1 -0
  213. package/dist/telephony/twilioMediaRegistry.js +106 -0
  214. package/dist/telephony/twilioMediaRegistry.js.map +1 -0
  215. package/dist/telephony/vonage-runtime.d.ts +14 -0
  216. package/dist/telephony/vonage-runtime.d.ts.map +1 -0
  217. package/dist/telephony/vonage-runtime.js +18 -0
  218. package/dist/telephony/vonage-runtime.js.map +1 -0
  219. package/dist/telephony/vonageMediaRegistry.d.ts +78 -0
  220. package/dist/telephony/vonageMediaRegistry.d.ts.map +1 -0
  221. package/dist/telephony/vonageMediaRegistry.js +158 -0
  222. package/dist/telephony/vonageMediaRegistry.js.map +1 -0
  223. package/package.json +88 -82
  224. package/src/__tests__/FileResolverPeaks.test.ts +64 -0
  225. package/src/__tests__/MediaAccessKeys.test.ts +68 -0
  226. package/src/__tests__/MediaStreamHandler.test.ts +91 -0
  227. package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -1
  228. package/src/__tests__/RealtimeClientSessionResolver.test.ts +69 -0
  229. package/src/__tests__/RealtimeCoAgentAppAwareness.integration.test.ts +191 -0
  230. package/src/__tests__/RemoteBrowserSnapshot.test.ts +123 -0
  231. package/src/__tests__/SessionManager.test.ts +2 -0
  232. package/src/__tests__/meetingRecordingRegistration.test.ts +248 -0
  233. package/src/__tests__/search-knowledge-tags.test.ts +9 -9
  234. package/src/__tests__/teams-meetings-service.test.ts +65 -0
  235. package/src/__tests__/teamsAcsMediaRegistry.test.ts +82 -0
  236. package/src/__tests__/twilio-telephony-service.test.ts +23 -0
  237. package/src/__tests__/twilioMediaRegistry.test.ts +103 -0
  238. package/src/__tests__/vonageMediaRegistry.test.ts +180 -0
  239. package/src/__tests__/widget.test.ts +204 -0
  240. package/src/__tests__/widgetGuestElevation.test.ts +57 -0
  241. package/src/__tests__/widgetHostIdentity.test.ts +117 -0
  242. package/src/agentSessions/ReturningVisitorRecap.ts +242 -0
  243. package/src/agentSessions/SessionJanitor.ts +66 -6
  244. package/src/agentSessions/SessionManager.ts +41 -0
  245. package/src/agentSessions/index.ts +3 -1
  246. package/src/auth/magicLink/MagicLinkRouter.ts +17 -0
  247. package/src/auth/magicLink/index.ts +1 -0
  248. package/src/auth/magicLink/types.ts +26 -0
  249. package/src/config.ts +172 -38
  250. package/src/context.ts +50 -3
  251. package/src/generated/generated.ts +26726 -20464
  252. package/src/generic/ResolverBase.ts +28 -0
  253. package/src/generic/RunViewResolver.ts +4 -0
  254. package/src/index.ts +119 -3
  255. package/src/integration/CustomColumnPromoter.ts +6 -0
  256. package/src/realtimeWidget/WidgetRouter.ts +204 -0
  257. package/src/realtimeWidget/WidgetSessionService.ts +714 -0
  258. package/src/realtimeWidget/host-identity.ts +84 -0
  259. package/src/realtimeWidget/index.ts +15 -0
  260. package/src/realtimeWidget/visitorIdentity.ts +258 -0
  261. package/src/realtimeWidget/widgetCore.ts +231 -0
  262. package/src/realtimeWidget/widgetGuestElevation.ts +115 -0
  263. package/src/resolvers/ComponentRegistryResolver.ts +36 -10
  264. package/src/resolvers/EntityPermissionResolver.ts +1 -2
  265. package/src/resolvers/FileResolver.ts +199 -1
  266. package/src/resolvers/QueryResolver.ts +33 -19
  267. package/src/resolvers/QuerySystemUserResolver.ts +14 -10
  268. package/src/resolvers/RealtimeBridgeResolver.ts +36 -4
  269. package/src/resolvers/RealtimeClientSessionResolver.ts +383 -9
  270. package/src/resolvers/RemoteBrowserActionResolver.ts +18 -2
  271. package/src/resolvers/RingCentralTelephonyResolver.ts +64 -0
  272. package/src/resolvers/RunAIAgentResolver.ts +60 -15
  273. package/src/resolvers/SearchKnowledgeResolver.ts +2 -0
  274. package/src/resolvers/TeamsMeetingsResolver.ts +68 -0
  275. package/src/resolvers/TelephonyResolver.ts +63 -0
  276. package/src/resolvers/TestQuerySQLResolver.ts +2 -3
  277. package/src/resolvers/VonageTelephonyResolver.ts +63 -0
  278. package/src/resolvers/meetingRecordingRegistration.ts +432 -0
  279. package/src/resolvers/peaksSidecar.ts +52 -0
  280. package/src/rest/MediaAccessKeys.ts +121 -0
  281. package/src/rest/MediaStreamHandler.ts +223 -0
  282. package/src/rest/mediaRange.ts +76 -0
  283. package/src/telephony/RingCentralTelephonyService.ts +342 -0
  284. package/src/telephony/TeamsMeetingsRouter.ts +124 -0
  285. package/src/telephony/TeamsMeetingsService.ts +268 -0
  286. package/src/telephony/TwilioTelephonyRouter.ts +184 -0
  287. package/src/telephony/TwilioTelephonyService.ts +261 -0
  288. package/src/telephony/VonageTelephonyRouter.ts +239 -0
  289. package/src/telephony/VonageTelephonyService.ts +259 -0
  290. package/src/telephony/calendar-scheduler.ts +176 -0
  291. package/src/telephony/index.ts +43 -0
  292. package/src/telephony/media-upgrade-router.ts +62 -0
  293. package/src/telephony/ringcentral-runtime.ts +22 -0
  294. package/src/telephony/teams-meetings-runtime.ts +24 -0
  295. package/src/telephony/teamsAcsMediaRegistry.ts +152 -0
  296. package/src/telephony/telephony-runtime.ts +22 -0
  297. package/src/telephony/twilioMediaRegistry.ts +137 -0
  298. package/src/telephony/vonage-runtime.ts +22 -0
  299. package/src/telephony/vonageMediaRegistry.ts +200 -0
  300. package/dist/agents/skip-agent.d.ts +0 -111
  301. package/dist/agents/skip-agent.d.ts.map +0 -1
  302. package/dist/agents/skip-agent.js +0 -1487
  303. package/dist/agents/skip-agent.js.map +0 -1
  304. package/dist/agents/skip-sdk.d.ts +0 -261
  305. package/dist/agents/skip-sdk.d.ts.map +0 -1
  306. package/dist/agents/skip-sdk.js +0 -909
  307. package/dist/agents/skip-sdk.js.map +0 -1
  308. package/src/agents/skip-agent.ts +0 -1594
  309. package/src/agents/skip-sdk.ts +0 -1210
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @fileoverview Privileged-dispatch elevation for public web-widget guests (public-web-widget.md
3
+ * Phase 0). A widget guest's session JWT authorizes ONLY its own Conversation + Conversation
4
+ * Details (RLS-scoped). The agent itself runs under a TRUSTED SERVER PRINCIPAL — so a guest needs
5
+ * no grants to WRITE the AI run entities, and cannot run an arbitrary agent under the elevated
6
+ * principal: the pinned agent is resolved AUTHORITATIVELY from the widget instance named by the
7
+ * signed `mj_widget_id` claim, never from a client-supplied agent id.
8
+ *
9
+ * The companion read-side control is the `Widget Guest: Own Agent Runs` RLS filter (seeded in the
10
+ * Phase 0 migration), which confines a guest to reading only its own session's run rows — closing
11
+ * the cross-guest leak for the voice path, where runs are still written under the guest principal.
12
+ *
13
+ * @module @memberjunction/server/widget
14
+ */
15
+
16
+ import { RunView, LogError, type UserInfo, type DatabaseProviderBase } from '@memberjunction/core';
17
+ import type { MJConversationWidgetInstanceEntity } from '@memberjunction/core-entities';
18
+ import { UserCache } from '@memberjunction/sqlserver-dataprovider';
19
+ import type { UserPayload } from '../types.js';
20
+
21
+ const WIDGET_ENTITY = 'MJ: Conversation Widget Instances';
22
+
23
+ /**
24
+ * Resolved elevation context for a widget-guest agent run. Returned only when the request is a
25
+ * widget guest; the dispatch resolver runs the agent under {@link WidgetGuestRunContext.elevatedUser}
26
+ * with {@link WidgetGuestRunContext.pinnedAgentId} (authoritative), keeping ownership checks under
27
+ * the guest principal.
28
+ */
29
+ export interface WidgetGuestRunContext {
30
+ /** The trusted server principal under which the agent run (and its run-entity writes) executes. */
31
+ elevatedUser: UserInfo;
32
+ /** The widget instance the guest session is bound to (authoritative config). */
33
+ widget: MJConversationWidgetInstanceEntity;
34
+ /** The authoritative pinned support agent id — overrides any client-supplied agent id (D5). */
35
+ pinnedAgentId: string;
36
+ /** The guest's per-session scope id (Conversation.ExternalID), used to validate conversation ownership. */
37
+ sessionScopeId?: string;
38
+ }
39
+
40
+ /**
41
+ * Returns elevation context when the request is a public web-widget guest, else `null` (the common
42
+ * case — every non-widget request). A widget guest is identified by the synthesized principal's
43
+ * `IsMagicLinkAnonymous` flag plus a `WidgetGuestContext.WidgetID` (sourced from the signed
44
+ * `mj_widget_id` claim by `buildMagicLinkSessionUser`). The widget instance is loaded under the
45
+ * SYSTEM principal so resolving the pinned agent does not depend on the guest's narrow grants.
46
+ *
47
+ * Never throws: a resolution failure (no system user, widget not found, read error) returns `null`,
48
+ * so the caller falls back to the normal (guest-principal) path rather than failing the request.
49
+ */
50
+ export async function resolveWidgetGuestRunContext(
51
+ userPayload: UserPayload,
52
+ provider: DatabaseProviderBase,
53
+ ): Promise<WidgetGuestRunContext | null> {
54
+ const guest = userPayload?.userRecord as UserInfo | undefined;
55
+ const widgetId = guest?.WidgetGuestContext?.WidgetID;
56
+ if (!guest?.IsMagicLinkAnonymous || !widgetId) {
57
+ return null;
58
+ }
59
+
60
+ const elevatedUser = UserCache.Instance.GetSystemUser();
61
+ if (!elevatedUser) {
62
+ LogError('[Widget] Cannot elevate widget-guest agent run: no system user available.');
63
+ return null;
64
+ }
65
+
66
+ const widget = await loadWidgetById(widgetId, elevatedUser, provider);
67
+ if (!widget) {
68
+ LogError(`[Widget] Widget instance ${widgetId} (from token) not found; cannot elevate dispatch.`);
69
+ return null;
70
+ }
71
+
72
+ return {
73
+ elevatedUser,
74
+ widget,
75
+ pinnedAgentId: widget.PinnedAgentID,
76
+ sessionScopeId: guest.MagicLinkScope?.ResourceID,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Builds an elevated {@link UserPayload} that runs subsequent agent work as `elevatedUser` while
82
+ * preserving the guest's `sessionId` — so progress/streaming PubSub still routes to the guest's
83
+ * live websocket, but all AI run-entity writes happen under the trusted server principal.
84
+ */
85
+ export function elevateUserPayload(userPayload: UserPayload, elevatedUser: UserInfo): UserPayload {
86
+ return {
87
+ ...userPayload,
88
+ email: elevatedUser.Email,
89
+ userRecord: elevatedUser,
90
+ isSystemUser: true,
91
+ };
92
+ }
93
+
94
+ /** Loads a single widget instance by id under the elevated principal (read-only). */
95
+ async function loadWidgetById(
96
+ widgetId: string,
97
+ elevatedUser: UserInfo,
98
+ provider: DatabaseProviderBase,
99
+ ): Promise<MJConversationWidgetInstanceEntity | null> {
100
+ const rv = new RunView(provider);
101
+ const result = await rv.RunView<MJConversationWidgetInstanceEntity>(
102
+ {
103
+ EntityName: WIDGET_ENTITY,
104
+ ExtraFilter: `ID = '${widgetId.replace(/'/g, "''")}'`,
105
+ MaxRows: 1,
106
+ ResultType: 'entity_object',
107
+ },
108
+ elevatedUser,
109
+ );
110
+ if (!result.Success) {
111
+ LogError(`[Widget] Failed to load widget instance ${widgetId}: ${result.ErrorMessage}`);
112
+ return null;
113
+ }
114
+ return result.Results?.[0] ?? null;
115
+ }
@@ -3,6 +3,7 @@ import { UserInfo, LogError, LogStatus } from '@memberjunction/core';
3
3
  import { UUIDsEqual, MJLruCache } from '@memberjunction/global';
4
4
  import { UserCache } from '@memberjunction/sqlserver-dataprovider';
5
5
  import { MJComponentRegistryEntity, ComponentMetadataEngine } from '@memberjunction/core-entities';
6
+ import { CredentialEngine } from '@memberjunction/credentials';
6
7
  import { ComponentSpec } from '@memberjunction/interactive-component-types';
7
8
  import {
8
9
  ComponentRegistryClient,
@@ -249,7 +250,7 @@ export class ComponentRegistryExtendedResolver {
249
250
  }
250
251
 
251
252
  // --- Tier 2: Fetch from external registry ---
252
- const registryClient = this.createClientForRegistry(registry);
253
+ const registryClient = await this.createClientForRegistry(registry, user);
253
254
 
254
255
  const response = await registryClient.getComponentWithHash({
255
256
  registry: registry.Name,
@@ -329,7 +330,7 @@ export class ComponentRegistryExtendedResolver {
329
330
  throw new Error(`Registry not found: ${params.registryId}`);
330
331
  }
331
332
 
332
- const client = this.createClientForRegistry(registry);
333
+ const client = await this.createClientForRegistry(registry, user);
333
334
 
334
335
  const result = await client.searchComponents({
335
336
  namespace: params.namespace,
@@ -356,7 +357,7 @@ export class ComponentRegistryExtendedResolver {
356
357
  try {
357
358
  await this.checkUserAccess(user, registry.ID);
358
359
 
359
- const client = this.createClientForRegistry(registry);
360
+ const client = await this.createClientForRegistry(registry, user);
360
361
  const result = await client.searchComponents({
361
362
  namespace: params.namespace,
362
363
  query: params.query,
@@ -413,8 +414,8 @@ export class ComponentRegistryExtendedResolver {
413
414
  await this.checkUserAccess(user, registry.ID);
414
415
 
415
416
  // Create client on-demand
416
- const client = this.createClientForRegistry(registry);
417
-
417
+ const client = await this.createClientForRegistry(registry, user);
418
+
418
419
  const tree = await client.resolveDependencies(componentId);
419
420
  return tree as ComponentDependencyTreeType;
420
421
  } catch (error) {
@@ -499,17 +500,22 @@ export class ComponentRegistryExtendedResolver {
499
500
  * Create a client for a registry on-demand
500
501
  * Checks configuration first, then falls back to default settings
501
502
  */
502
- private createClientForRegistry(registry: MJComponentRegistryEntity): ComponentRegistryClient {
503
+ private async createClientForRegistry(registry: MJComponentRegistryEntity, user?: UserInfo): Promise<ComponentRegistryClient> {
503
504
  // Check if there's a configuration for this registry
504
505
  const config = configInfo.componentRegistries?.find(r =>
505
506
  UUIDsEqual(r.id, registry.ID) || r.name === registry.Name
506
507
  );
507
508
 
508
- // Get API key from environment or config
509
+ // Get API key from environment or config, falling back to the encrypted
510
+ // credential store (a credential named "Component Registry: <RegistryName>").
511
+ // The credential-store path lets an installer provision the key once, without
512
+ // an env var or a plaintext entry in mj.config.cjs. Env/config still win so
513
+ // existing deployments are unchanged.
509
514
  const apiKey = process.env[`REGISTRY_API_KEY_${registry.ID.replace(/-/g, '_').toUpperCase()}`] ||
510
515
  process.env[`REGISTRY_API_KEY_${registry.Name?.replace(/-/g, '_').toUpperCase()}`] ||
511
- config?.apiKey;
512
-
516
+ config?.apiKey ||
517
+ await this.resolveRegistryApiKeyFromCredentialStore(registry, user);
518
+
513
519
  // Get the registry URI (with possible override)
514
520
  const baseUrl = this.getRegistryUri(registry);
515
521
 
@@ -531,6 +537,26 @@ export class ComponentRegistryExtendedResolver {
531
537
  });
532
538
  }
533
539
 
540
+ /**
541
+ * Resolves a registry's API key from the encrypted credential store, using the
542
+ * convention credential name "Component Registry: <RegistryName>". Returns undefined
543
+ * when no such credential exists (e.g. public registries that need no key) or the
544
+ * store is unavailable — callers treat the missing key as "unauthenticated".
545
+ */
546
+ private async resolveRegistryApiKeyFromCredentialStore(registry: MJComponentRegistryEntity, user?: UserInfo): Promise<string | undefined> {
547
+ try {
548
+ await CredentialEngine.Instance.Config(false, user);
549
+ const resolved = await CredentialEngine.Instance.getCredential<{ apiKey: string }>(
550
+ `Component Registry: ${registry.Name}`,
551
+ { contextUser: user, subsystem: 'ComponentRegistry' }
552
+ );
553
+ return resolved?.values?.apiKey || undefined;
554
+ } catch {
555
+ // Credential not found or store unavailable — fall through to no key.
556
+ return undefined;
557
+ }
558
+ }
559
+
534
560
  /**
535
561
  * Map search result to GraphQL type
536
562
  */
@@ -585,7 +611,7 @@ export class ComponentRegistryExtendedResolver {
585
611
 
586
612
  // Create client using the same pattern as GetRegistryComponent
587
613
  // This respects REGISTRY_URI_OVERRIDE_* and REGISTRY_API_KEY_* environment variables
588
- const registryClient = this.createClientForRegistry(registry);
614
+ const registryClient = await this.createClientForRegistry(registry, user);
589
615
 
590
616
  const sdkParams: SDKComponentFeedbackParams = {
591
617
  componentName: feedback.componentName,
@@ -2,7 +2,6 @@ import { Arg, Ctx, Field, ObjectType, Query, Resolver } from 'type-graphql';
2
2
  import { LogError } from '@memberjunction/core';
3
3
  import { AppContext } from '../types.js';
4
4
  import { ResolverBase } from '../generic/ResolverBase.js';
5
- import { RequireSystemUser } from '../directives/RequireSystemUser.js';
6
5
  import { GetReadOnlyProvider } from '../util.js';
7
6
  import { UserCache } from '@memberjunction/sqlserver-dataprovider';
8
7
 
@@ -30,7 +29,6 @@ class CheckEntityPermissionsResult {
30
29
  @Resolver()
31
30
  export class EntityPermissionResolver extends ResolverBase {
32
31
 
33
- @RequireSystemUser()
34
32
  @Query(() => CheckEntityPermissionsResult)
35
33
  async CheckEntityPermissionsSystemUser(
36
34
  @Arg('EntityNames', () => [String]) entityNames: string[],
@@ -38,6 +36,7 @@ export class EntityPermissionResolver extends ResolverBase {
38
36
  @Ctx() context: AppContext
39
37
  ): Promise<CheckEntityPermissionsResult> {
40
38
  try {
39
+ await this.CheckAPIKeyScopeAuthorization('view:run', '*', context.userPayload);
41
40
  const user = UserCache.Instance.Users.find(
42
41
  u => u.Email.toLowerCase().trim() === userEmail.toLowerCase().trim()
43
42
  );
@@ -1,6 +1,7 @@
1
- import { EntityPermissionType, FieldValueCollection, EntitySaveOptions } from '@memberjunction/core';
1
+ import { EntityPermissionType, FieldValueCollection, EntitySaveOptions, LogError, UserInfo } from '@memberjunction/core';
2
2
  import { NormalizeUUID } from '@memberjunction/global';
3
3
  import { MJFileEntity, MJFileStorageProviderEntity, MJFileStorageAccountEntity } from '@memberjunction/core-entities';
4
+ import { readRealtimeRecordingFile } from '@memberjunction/ai-agents';
4
5
  import {
5
6
  AppContext,
6
7
  Arg,
@@ -8,6 +9,7 @@ import {
8
9
  DeleteOptionsInput,
9
10
  Field,
10
11
  FieldResolver,
12
+ Float,
11
13
  InputType,
12
14
  Int,
13
15
  Mutation,
@@ -37,6 +39,9 @@ import {
37
39
  import { CreateMJFileInput, MJFileResolver as FileResolverBase, MJFile_, UpdateMJFileInput } from '../generated/generated.js';
38
40
  import { FieldMapper } from '@memberjunction/graphql-dataprovider';
39
41
  import { GetReadOnlyProvider } from '../util.js';
42
+ import { configInfo } from '../config.js';
43
+ import { MediaAccessKeyManager } from '../rest/MediaAccessKeys.js';
44
+ import { deriveSidecarPath, parsePeaksSidecar } from './peaksSidecar.js';
40
45
 
41
46
  @InputType()
42
47
  export class CreateUploadURLInput {
@@ -312,8 +317,161 @@ export class SearchAcrossAccountsPayload {
312
317
  failedAccounts: number;
313
318
  }
314
319
 
320
+ /**
321
+ * Result of {@link FileResolver.GetFileContents} — the bytes of an `MJ: Files` record returned as
322
+ * base64, read server-side through authenticated MJStorage (never a public pre-signed link).
323
+ */
324
+ @ObjectType()
325
+ export class FileContentsResult {
326
+ @Field(() => Boolean)
327
+ Success: boolean;
328
+
329
+ @Field(() => String, { nullable: true })
330
+ Base64?: string;
331
+
332
+ @Field(() => String, { nullable: true })
333
+ MimeType?: string;
334
+
335
+ @Field(() => String, { nullable: true })
336
+ ErrorMessage?: string;
337
+ }
338
+
339
+ /**
340
+ * Result of {@link FileResolver.CreateMediaAccessToken} — a short-lived signed token plus the
341
+ * authenticated streaming URL the browser hands to a `<audio>`/`<video>` element. The token is
342
+ * minted only after a per-user permission check on the `MJ: Files` row; the `/media/:fileId`
343
+ * route re-verifies it (signature + expiry + file match) on each Range request.
344
+ */
345
+ @ObjectType()
346
+ export class MediaAccessTokenResult {
347
+ @Field(() => Boolean)
348
+ Success: boolean;
349
+
350
+ @Field(() => String, { nullable: true })
351
+ Token?: string;
352
+
353
+ @Field(() => String, { nullable: true })
354
+ Url?: string;
355
+
356
+ @Field(() => Date, { nullable: true })
357
+ ExpiresAt?: Date;
358
+
359
+ /**
360
+ * The file's MIME type (from the already-loaded `MJ: Files` row). Lets the wrapper choose
361
+ * `<audio>` vs `<video>` WITHOUT re-downloading any bytes; the stream itself sets Content-Type.
362
+ */
363
+ @Field(() => String, { nullable: true })
364
+ MimeType?: string;
365
+
366
+ /**
367
+ * Optional precomputed waveform peaks (normalized `0..1`, one per rendered bar) read from a
368
+ * `peaks.json` sidecar that sits beside the file in storage. When present, the player renders the
369
+ * real waveform instantly with NO client-side fetch/decode of the audio. Best-effort: a missing or
370
+ * malformed sidecar simply omits this field — it never blocks token minting or fails the mutation.
371
+ */
372
+ @Field(() => [Float], { nullable: true })
373
+ Peaks?: number[];
374
+
375
+ @Field(() => String, { nullable: true })
376
+ ErrorMessage?: string;
377
+ }
378
+
315
379
  @Resolver(MJFile_)
316
380
  export class FileResolver extends FileResolverBase {
381
+ /**
382
+ * Resolves the server's public base URL the same way callbacks/magic-link do:
383
+ * `publicUrl` if configured (e.g. an ngrok URL), else `baseUrl:graphqlPort`. Trailing
384
+ * slash stripped so callers can append `/media/...`.
385
+ */
386
+ private resolvePublicBaseUrl(): string {
387
+ const base = configInfo.publicUrl || `${configInfo.baseUrl}:${configInfo.graphqlPort}${configInfo.graphqlRootPath || ''}`;
388
+ return base.replace(/\/+$/, '');
389
+ }
390
+
391
+ /**
392
+ * Mints a short-lived signed media-access token for an `MJ: Files` record and returns the
393
+ * authenticated streaming URL (`<publicBase>/media/<fileId>?token=<token>`). Permission-gated
394
+ * IDENTICALLY to {@link GetFileContents}: the file is loaded under the CALLING USER's context, so
395
+ * MJ row-level security determines access. The returned token is the capability — the streaming
396
+ * route re-verifies it without re-checking row-level access. Never throws to the client.
397
+ *
398
+ * @param fileId The `MJ: Files` id to grant streaming access to.
399
+ * @returns `{ Success, Token?, Url?, ExpiresAt?, ErrorMessage? }`.
400
+ */
401
+ @Mutation(() => MediaAccessTokenResult)
402
+ async CreateMediaAccessToken(@Arg('fileId', () => String) fileId: string, @Ctx() context: AppContext): Promise<MediaAccessTokenResult> {
403
+ try {
404
+ const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
405
+ const contextUser = this.GetUserFromPayload(context.userPayload);
406
+
407
+ // Permission gate: load the file under the USER's context. A failed load means the file
408
+ // does not exist OR the user lacks read access under MJ row-level security.
409
+ const fileEntity = await provider.GetEntityObject<MJFileEntity>('MJ: Files', contextUser);
410
+ const loaded = await fileEntity.Load(fileId);
411
+ if (!loaded) {
412
+ return { Success: false, ErrorMessage: 'You do not have access to this file or it does not exist.' };
413
+ }
414
+
415
+ // Access authorized — mint the capability token.
416
+ const { Token, ExpiresAt } = MediaAccessKeyManager.Instance.Sign(fileId, contextUser.ID);
417
+ const url = `${this.resolvePublicBaseUrl()}/media/${encodeURIComponent(fileId)}?token=${encodeURIComponent(Token)}`;
418
+
419
+ // Best-effort: surface precomputed waveform peaks from a peaks.json sidecar beside the file, so
420
+ // the player renders the real waveform instantly without fetching/decoding the audio. Never
421
+ // blocks token minting — any failure just omits Peaks.
422
+ const peaks = await this.tryReadPeaksSidecar(fileEntity, contextUser);
423
+
424
+ return { Success: true, Token, Url: url, ExpiresAt, MimeType: fileEntity.ContentType ?? undefined, Peaks: peaks };
425
+ } catch (error) {
426
+ const message = error instanceof Error ? error.message : String(error);
427
+ LogError(`CreateMediaAccessToken failed (file ${fileId}): ${message}`);
428
+ return { Success: false, ErrorMessage: message };
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Best-effort read of a `peaks.json` waveform sidecar that sits in the SAME storage folder as the
434
+ * given file (a JSON array of normalized `0..1` numbers, written at capture time). Derives the
435
+ * folder from the file's `ProviderKey` (strips the final path segment), reads `<folder>/peaks.json`
436
+ * via the file's own storage driver, parses + validates it, and returns sanitized peaks. Returns
437
+ * `undefined` on ANY failure (no ProviderKey, no sidecar, parse error, garbage) — the caller treats
438
+ * peaks as a pure optimization and must never let a sidecar problem affect token minting.
439
+ *
440
+ * @param file The already-loaded (under the user's context) `MJ: Files` row.
441
+ * @param contextUser The calling user — used to resolve the storage driver.
442
+ * @returns Sanitized `0..1` peaks (length-capped), or `undefined`.
443
+ */
444
+ private async tryReadPeaksSidecar(file: MJFileEntity, contextUser: UserInfo): Promise<number[] | undefined> {
445
+ try {
446
+ // The sidecar lives next to the recording: replace the final path segment with peaks.json.
447
+ const sidecarPath = deriveSidecarPath(file.ProviderKey);
448
+ if (!sidecarPath) {
449
+ return undefined;
450
+ }
451
+
452
+ // Resolve the file's storage account → driver (mirror of readRealtimeRecordingFile's pattern).
453
+ await FileStorageEngine.Instance.Config(false, contextUser);
454
+ let accounts = FileStorageEngine.Instance.GetAccountsByProviderID(file.ProviderID);
455
+ if (accounts.length === 0) {
456
+ await FileStorageEngine.Instance.Config(true, contextUser);
457
+ accounts = FileStorageEngine.Instance.GetAccountsByProviderID(file.ProviderID);
458
+ }
459
+ const account = accounts[0];
460
+ if (!account) {
461
+ return undefined;
462
+ }
463
+ const driver = await FileStorageEngine.Instance.GetDriver(account.ID, contextUser);
464
+ const bytes = await driver.GetObject({ fullPath: sidecarPath });
465
+ if (!bytes || bytes.length === 0) {
466
+ return undefined;
467
+ }
468
+ return parsePeaksSidecar(bytes);
469
+ } catch {
470
+ // No sidecar / unreadable / parse failure — peaks are optional, never surface the error.
471
+ return undefined;
472
+ }
473
+ }
474
+
317
475
  /**
318
476
  * Builds UserContextOptions for storage operations that may require OAuth authentication.
319
477
  * This passes the current user's ID and context to allow the storage utilities to
@@ -431,6 +589,46 @@ export class FileResolver extends FileResolverBase {
431
589
  return url;
432
590
  }
433
591
 
592
+ /**
593
+ * Returns an `MJ: Files` record's bytes as base64, read server-side through authenticated MJStorage
594
+ * (`GetObject`) — NOT a public pre-signed link. Permission-gated: the file is first loaded under the
595
+ * calling user's context, so MJ row-level security determines access. Never throws to the client.
596
+ *
597
+ * @param fileId The `MJ: Files` id whose bytes to return.
598
+ * @returns `{ Success, Base64?, MimeType?, ErrorMessage? }`.
599
+ */
600
+ @Query(() => FileContentsResult)
601
+ async GetFileContents(@Arg('fileId', () => String) fileId: string, @Ctx() context: AppContext): Promise<FileContentsResult> {
602
+ try {
603
+ const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
604
+ const contextUser = this.GetUserFromPayload(context.userPayload);
605
+
606
+ // Permission gate FIRST: load the file record under the USER's context. A failed load means the
607
+ // file does not exist OR the user lacks read access under MJ row-level security.
608
+ const fileEntity = await provider.GetEntityObject<MJFileEntity>('MJ: Files', contextUser);
609
+ const loaded = await fileEntity.Load(fileId);
610
+ if (!loaded) {
611
+ return { Success: false, ErrorMessage: 'You do not have access to this file or it does not exist.' };
612
+ }
613
+
614
+ // Read the bytes via authenticated MJStorage (server-side GetObject on the file's own account).
615
+ const result = await readRealtimeRecordingFile(fileId, contextUser, provider);
616
+ if (!result) {
617
+ return { Success: false, ErrorMessage: 'The file could not be read from storage.' };
618
+ }
619
+
620
+ return {
621
+ Success: true,
622
+ Base64: result.Bytes.toString('base64'),
623
+ MimeType: result.MimeType ?? fileEntity.ContentType ?? undefined,
624
+ };
625
+ } catch (error) {
626
+ const message = error instanceof Error ? error.message : String(error);
627
+ LogError(`GetFileContents failed (file ${fileId}): ${message}`);
628
+ return { Success: false, ErrorMessage: message };
629
+ }
630
+ }
631
+
434
632
  @Mutation(() => MJFile_)
435
633
  async UpdateFile(@Arg('input', () => UpdateMJFileInput) input: UpdateMJFileInput, @Ctx() context: AppContext, @PubSub() pubSub: PubSubEngine) {
436
634
  // if the name is changing, rename the target object as well
@@ -1,5 +1,5 @@
1
1
  import { Arg, Ctx, ObjectType, Query, Resolver, Field, Int, InputType } from 'type-graphql';
2
- import { RunQuery, IRunQueryProvider, IMetadataProvider, RunQueryParams, LogError, RunQueryWithCacheCheckParams, RunQueriesWithCacheCheckResponse, RunQueryWithCacheCheckResult } from '@memberjunction/core';
2
+ import { RunQuery, IRunQueryProvider, IMetadataProvider, RunQueryParams, LogError, RunQueryWithCacheCheckParams, RunQueriesWithCacheCheckResponse, RunQueryWithCacheCheckResult, RunQueryEnrichment } from '@memberjunction/core';
3
3
  import { AppContext } from '../types.js';
4
4
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
5
5
  import { GraphQLJSONObject } from 'graphql-type-json';
@@ -40,6 +40,9 @@ export class RunQueryInput {
40
40
 
41
41
  @Field(() => String, { nullable: true, description: 'Description to use in audit log' })
42
42
  AuditLogDescription?: string;
43
+
44
+ @Field(() => GraphQLJSONObject, { nullable: true, description: 'Optional runtime-only directive to post-process result rows through a registered query result enricher ({ EnricherKey, Config })' })
45
+ Enrichment?: RunQueryEnrichment;
43
46
  }
44
47
 
45
48
  @ObjectType()
@@ -191,7 +194,8 @@ export class RunQueryResolver extends ResolverBase {
191
194
  @Arg('MaxRows', () => Int, {nullable: true}) MaxRows?: number,
192
195
  @Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
193
196
  @Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
194
- @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string): Promise<RunQueryResultType> {
197
+ @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string,
198
+ @Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment): Promise<RunQueryResultType> {
195
199
  // Check API key scope authorization for query execution
196
200
  await this.CheckAPIKeyScopeAuthorization('query:run', QueryID, context.userPayload);
197
201
 
@@ -200,7 +204,7 @@ export class RunQueryResolver extends ResolverBase {
200
204
  const rq = new RunQuery(provider as unknown as IRunQueryProvider);
201
205
  console.log('GetQueryData called with:', { QueryID, Parameters, MaxRows, StartRow, ForceAuditLog, AuditLogDescription });
202
206
  const result = await rq.RunQuery(
203
- {
207
+ {
204
208
  QueryID: QueryID,
205
209
  CategoryID: CategoryID,
206
210
  CategoryPath: CategoryPath,
@@ -208,8 +212,9 @@ export class RunQueryResolver extends ResolverBase {
208
212
  MaxRows: MaxRows,
209
213
  StartRow: StartRow,
210
214
  ForceAuditLog: ForceAuditLog,
211
- AuditLogDescription: AuditLogDescription
212
- },
215
+ AuditLogDescription: AuditLogDescription,
216
+ Enrichment: Enrichment
217
+ },
213
218
  context.userPayload.userRecord);
214
219
  console.log('RunQuery result:', {
215
220
  Success: result.Success,
@@ -256,22 +261,24 @@ export class RunQueryResolver extends ResolverBase {
256
261
  @Arg('MaxRows', () => Int, {nullable: true}) MaxRows?: number,
257
262
  @Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
258
263
  @Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
259
- @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string): Promise<RunQueryResultType> {
264
+ @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string,
265
+ @Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment): Promise<RunQueryResultType> {
260
266
  // Check API key scope authorization for query execution
261
267
  await this.CheckAPIKeyScopeAuthorization('query:run', QueryName, context.userPayload);
262
268
 
263
269
  const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
264
270
  const rq = new RunQuery(provider as unknown as IRunQueryProvider);
265
271
  const result = await rq.RunQuery(
266
- {
267
- QueryName: QueryName,
272
+ {
273
+ QueryName: QueryName,
268
274
  CategoryID: CategoryID,
269
275
  CategoryPath: CategoryPath,
270
276
  Parameters: Parameters,
271
277
  MaxRows: MaxRows,
272
278
  StartRow: StartRow,
273
279
  ForceAuditLog: ForceAuditLog,
274
- AuditLogDescription: AuditLogDescription
280
+ AuditLogDescription: AuditLogDescription,
281
+ Enrichment: Enrichment
275
282
  },
276
283
  context.userPayload.userRecord);
277
284
 
@@ -302,13 +309,14 @@ export class RunQueryResolver extends ResolverBase {
302
309
  @Arg('MaxRows', () => Int, {nullable: true}) MaxRows?: number,
303
310
  @Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
304
311
  @Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
305
- @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string): Promise<RunQueryResultType> {
312
+ @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string,
313
+ @Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment): Promise<RunQueryResultType> {
306
314
  const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
307
315
  const md = provider as unknown as IMetadataProvider;
308
316
  const rq = new RunQuery(provider as unknown as IRunQueryProvider);
309
317
 
310
318
  const result = await rq.RunQuery(
311
- {
319
+ {
312
320
  QueryID: QueryID,
313
321
  CategoryID: CategoryID,
314
322
  CategoryPath: CategoryPath,
@@ -316,8 +324,9 @@ export class RunQueryResolver extends ResolverBase {
316
324
  MaxRows: MaxRows,
317
325
  StartRow: StartRow,
318
326
  ForceAuditLog: ForceAuditLog,
319
- AuditLogDescription: AuditLogDescription
320
- },
327
+ AuditLogDescription: AuditLogDescription,
328
+ Enrichment: Enrichment
329
+ },
321
330
  context.userPayload.userRecord);
322
331
 
323
332
  // If QueryName is not populated by the provider, use efficient lookup
@@ -360,12 +369,13 @@ export class RunQueryResolver extends ResolverBase {
360
369
  @Arg('MaxRows', () => Int, {nullable: true}) MaxRows?: number,
361
370
  @Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
362
371
  @Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
363
- @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string): Promise<RunQueryResultType> {
372
+ @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string,
373
+ @Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment): Promise<RunQueryResultType> {
364
374
  const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
365
375
  const rq = new RunQuery(provider as unknown as IRunQueryProvider);
366
376
 
367
377
  const result = await rq.RunQuery(
368
- {
378
+ {
369
379
  QueryName: QueryName,
370
380
  CategoryID: CategoryID,
371
381
  CategoryPath: CategoryPath,
@@ -373,8 +383,9 @@ export class RunQueryResolver extends ResolverBase {
373
383
  MaxRows: MaxRows,
374
384
  StartRow: StartRow,
375
385
  ForceAuditLog: ForceAuditLog,
376
- AuditLogDescription: AuditLogDescription
377
- },
386
+ AuditLogDescription: AuditLogDescription,
387
+ Enrichment: Enrichment
388
+ },
378
389
  context.userPayload.userRecord);
379
390
 
380
391
  return {
@@ -420,7 +431,8 @@ export class RunQueryResolver extends ResolverBase {
420
431
  MaxRows: i.MaxRows,
421
432
  StartRow: i.StartRow,
422
433
  ForceAuditLog: i.ForceAuditLog,
423
- AuditLogDescription: i.AuditLogDescription
434
+ AuditLogDescription: i.AuditLogDescription,
435
+ Enrichment: i.Enrichment
424
436
  }));
425
437
 
426
438
  // Execute all queries in parallel using the batch method
@@ -469,7 +481,8 @@ export class RunQueryResolver extends ResolverBase {
469
481
  MaxRows: i.MaxRows,
470
482
  StartRow: i.StartRow,
471
483
  ForceAuditLog: i.ForceAuditLog,
472
- AuditLogDescription: i.AuditLogDescription
484
+ AuditLogDescription: i.AuditLogDescription,
485
+ Enrichment: i.Enrichment
473
486
  }));
474
487
 
475
488
  // Execute all queries in parallel using the batch method
@@ -532,6 +545,7 @@ export class RunQueryResolver extends ResolverBase {
532
545
  StartRow: item.params.StartRow,
533
546
  ForceAuditLog: item.params.ForceAuditLog,
534
547
  AuditLogDescription: item.params.AuditLogDescription,
548
+ Enrichment: item.params.Enrichment,
535
549
  },
536
550
  cacheStatus: item.cacheStatus ? {
537
551
  maxUpdatedAt: item.cacheStatus.maxUpdatedAt,