@memberjunction/server 5.44.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 (254) 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/index.d.ts +1 -1
  17. package/dist/auth/magicLink/index.d.ts.map +1 -1
  18. package/dist/auth/magicLink/index.js +1 -1
  19. package/dist/auth/magicLink/index.js.map +1 -1
  20. package/dist/auth/magicLink/types.d.ts +26 -0
  21. package/dist/auth/magicLink/types.d.ts.map +1 -1
  22. package/dist/config.d.ts +1883 -144
  23. package/dist/config.d.ts.map +1 -1
  24. package/dist/config.js +160 -36
  25. package/dist/config.js.map +1 -1
  26. package/dist/context.d.ts.map +1 -1
  27. package/dist/context.js +45 -2
  28. package/dist/context.js.map +1 -1
  29. package/dist/generated/generated.d.ts +522 -0
  30. package/dist/generated/generated.d.ts.map +1 -1
  31. package/dist/generated/generated.js +2860 -0
  32. package/dist/generated/generated.js.map +1 -1
  33. package/dist/generic/ResolverBase.d.ts +13 -0
  34. package/dist/generic/ResolverBase.d.ts.map +1 -1
  35. package/dist/generic/ResolverBase.js +22 -0
  36. package/dist/generic/ResolverBase.js.map +1 -1
  37. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  38. package/dist/generic/RunViewResolver.js +4 -0
  39. package/dist/generic/RunViewResolver.js.map +1 -1
  40. package/dist/index.d.ts +0 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +101 -3
  43. package/dist/index.js.map +1 -1
  44. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
  45. package/dist/integration/CustomColumnPromoter.js +6 -0
  46. package/dist/integration/CustomColumnPromoter.js.map +1 -1
  47. package/dist/realtimeWidget/WidgetRouter.d.ts +26 -0
  48. package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -0
  49. package/dist/realtimeWidget/WidgetRouter.js +182 -0
  50. package/dist/realtimeWidget/WidgetRouter.js.map +1 -0
  51. package/dist/realtimeWidget/WidgetSessionService.d.ts +265 -0
  52. package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -0
  53. package/dist/realtimeWidget/WidgetSessionService.js +477 -0
  54. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -0
  55. package/dist/realtimeWidget/host-identity.d.ts +40 -0
  56. package/dist/realtimeWidget/host-identity.d.ts.map +1 -0
  57. package/dist/realtimeWidget/host-identity.js +58 -0
  58. package/dist/realtimeWidget/host-identity.js.map +1 -0
  59. package/dist/realtimeWidget/index.d.ts +10 -0
  60. package/dist/realtimeWidget/index.d.ts.map +1 -0
  61. package/dist/realtimeWidget/index.js +9 -0
  62. package/dist/realtimeWidget/index.js.map +1 -0
  63. package/dist/realtimeWidget/visitorIdentity.d.ts +76 -0
  64. package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -0
  65. package/dist/realtimeWidget/visitorIdentity.js +202 -0
  66. package/dist/realtimeWidget/visitorIdentity.js.map +1 -0
  67. package/dist/realtimeWidget/widgetCore.d.ts +106 -0
  68. package/dist/realtimeWidget/widgetCore.d.ts.map +1 -0
  69. package/dist/realtimeWidget/widgetCore.js +173 -0
  70. package/dist/realtimeWidget/widgetCore.js.map +1 -0
  71. package/dist/realtimeWidget/widgetGuestElevation.d.ts +51 -0
  72. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -0
  73. package/dist/realtimeWidget/widgetGuestElevation.js +79 -0
  74. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -0
  75. package/dist/resolvers/ComponentRegistryResolver.d.ts +7 -0
  76. package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
  77. package/dist/resolvers/ComponentRegistryResolver.js +31 -8
  78. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  79. package/dist/resolvers/EntityPermissionResolver.d.ts.map +1 -1
  80. package/dist/resolvers/EntityPermissionResolver.js +1 -2
  81. package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
  82. package/dist/resolvers/QuerySystemUserResolver.d.ts +9 -6
  83. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  84. package/dist/resolvers/QuerySystemUserResolver.js +15 -13
  85. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  86. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -0
  87. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  88. package/dist/resolvers/RealtimeClientSessionResolver.js +47 -3
  89. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  90. package/dist/resolvers/RingCentralTelephonyResolver.d.ts +27 -0
  91. package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +1 -0
  92. package/dist/resolvers/RingCentralTelephonyResolver.js +87 -0
  93. package/dist/resolvers/RingCentralTelephonyResolver.js.map +1 -0
  94. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  95. package/dist/resolvers/RunAIAgentResolver.js +14 -3
  96. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  97. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  98. package/dist/resolvers/SearchKnowledgeResolver.js +2 -0
  99. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  100. package/dist/resolvers/TeamsMeetingsResolver.d.ts +28 -0
  101. package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +1 -0
  102. package/dist/resolvers/TeamsMeetingsResolver.js +91 -0
  103. package/dist/resolvers/TeamsMeetingsResolver.js.map +1 -0
  104. package/dist/resolvers/TelephonyResolver.d.ts +26 -0
  105. package/dist/resolvers/TelephonyResolver.d.ts.map +1 -0
  106. package/dist/resolvers/TelephonyResolver.js +86 -0
  107. package/dist/resolvers/TelephonyResolver.js.map +1 -0
  108. package/dist/resolvers/TestQuerySQLResolver.d.ts +1 -1
  109. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  110. package/dist/resolvers/TestQuerySQLResolver.js +2 -3
  111. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  112. package/dist/resolvers/VonageTelephonyResolver.d.ts +26 -0
  113. package/dist/resolvers/VonageTelephonyResolver.d.ts.map +1 -0
  114. package/dist/resolvers/VonageTelephonyResolver.js +86 -0
  115. package/dist/resolvers/VonageTelephonyResolver.js.map +1 -0
  116. package/dist/rest/MediaStreamHandler.js +4 -1
  117. package/dist/rest/MediaStreamHandler.js.map +1 -1
  118. package/dist/telephony/RingCentralTelephonyService.d.ts +115 -0
  119. package/dist/telephony/RingCentralTelephonyService.d.ts.map +1 -0
  120. package/dist/telephony/RingCentralTelephonyService.js +262 -0
  121. package/dist/telephony/RingCentralTelephonyService.js.map +1 -0
  122. package/dist/telephony/TeamsMeetingsRouter.d.ts +40 -0
  123. package/dist/telephony/TeamsMeetingsRouter.d.ts.map +1 -0
  124. package/dist/telephony/TeamsMeetingsRouter.js +96 -0
  125. package/dist/telephony/TeamsMeetingsRouter.js.map +1 -0
  126. package/dist/telephony/TeamsMeetingsService.d.ts +113 -0
  127. package/dist/telephony/TeamsMeetingsService.d.ts.map +1 -0
  128. package/dist/telephony/TeamsMeetingsService.js +196 -0
  129. package/dist/telephony/TeamsMeetingsService.js.map +1 -0
  130. package/dist/telephony/TwilioTelephonyRouter.d.ts +36 -0
  131. package/dist/telephony/TwilioTelephonyRouter.d.ts.map +1 -0
  132. package/dist/telephony/TwilioTelephonyRouter.js +143 -0
  133. package/dist/telephony/TwilioTelephonyRouter.js.map +1 -0
  134. package/dist/telephony/TwilioTelephonyService.d.ts +95 -0
  135. package/dist/telephony/TwilioTelephonyService.d.ts.map +1 -0
  136. package/dist/telephony/TwilioTelephonyService.js +193 -0
  137. package/dist/telephony/TwilioTelephonyService.js.map +1 -0
  138. package/dist/telephony/VonageTelephonyRouter.d.ts +41 -0
  139. package/dist/telephony/VonageTelephonyRouter.d.ts.map +1 -0
  140. package/dist/telephony/VonageTelephonyRouter.js +201 -0
  141. package/dist/telephony/VonageTelephonyRouter.js.map +1 -0
  142. package/dist/telephony/VonageTelephonyService.d.ts +90 -0
  143. package/dist/telephony/VonageTelephonyService.d.ts.map +1 -0
  144. package/dist/telephony/VonageTelephonyService.js +191 -0
  145. package/dist/telephony/VonageTelephonyService.js.map +1 -0
  146. package/dist/telephony/calendar-scheduler.d.ts +67 -0
  147. package/dist/telephony/calendar-scheduler.d.ts.map +1 -0
  148. package/dist/telephony/calendar-scheduler.js +133 -0
  149. package/dist/telephony/calendar-scheduler.js.map +1 -0
  150. package/dist/telephony/index.d.ts +29 -0
  151. package/dist/telephony/index.d.ts.map +1 -0
  152. package/dist/telephony/index.js +38 -0
  153. package/dist/telephony/index.js.map +1 -0
  154. package/dist/telephony/media-upgrade-router.d.ts +33 -0
  155. package/dist/telephony/media-upgrade-router.d.ts.map +1 -0
  156. package/dist/telephony/media-upgrade-router.js +56 -0
  157. package/dist/telephony/media-upgrade-router.js.map +1 -0
  158. package/dist/telephony/ringcentral-runtime.d.ts +14 -0
  159. package/dist/telephony/ringcentral-runtime.d.ts.map +1 -0
  160. package/dist/telephony/ringcentral-runtime.js +18 -0
  161. package/dist/telephony/ringcentral-runtime.js.map +1 -0
  162. package/dist/telephony/teams-meetings-runtime.d.ts +16 -0
  163. package/dist/telephony/teams-meetings-runtime.d.ts.map +1 -0
  164. package/dist/telephony/teams-meetings-runtime.js +20 -0
  165. package/dist/telephony/teams-meetings-runtime.js.map +1 -0
  166. package/dist/telephony/teamsAcsMediaRegistry.d.ts +69 -0
  167. package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +1 -0
  168. package/dist/telephony/teamsAcsMediaRegistry.js +118 -0
  169. package/dist/telephony/teamsAcsMediaRegistry.js.map +1 -0
  170. package/dist/telephony/telephony-runtime.d.ts +14 -0
  171. package/dist/telephony/telephony-runtime.d.ts.map +1 -0
  172. package/dist/telephony/telephony-runtime.js +18 -0
  173. package/dist/telephony/telephony-runtime.js.map +1 -0
  174. package/dist/telephony/twilioMediaRegistry.d.ts +60 -0
  175. package/dist/telephony/twilioMediaRegistry.d.ts.map +1 -0
  176. package/dist/telephony/twilioMediaRegistry.js +106 -0
  177. package/dist/telephony/twilioMediaRegistry.js.map +1 -0
  178. package/dist/telephony/vonage-runtime.d.ts +14 -0
  179. package/dist/telephony/vonage-runtime.d.ts.map +1 -0
  180. package/dist/telephony/vonage-runtime.js +18 -0
  181. package/dist/telephony/vonage-runtime.js.map +1 -0
  182. package/dist/telephony/vonageMediaRegistry.d.ts +78 -0
  183. package/dist/telephony/vonageMediaRegistry.d.ts.map +1 -0
  184. package/dist/telephony/vonageMediaRegistry.js +158 -0
  185. package/dist/telephony/vonageMediaRegistry.js.map +1 -0
  186. package/package.json +88 -83
  187. package/src/__tests__/search-knowledge-tags.test.ts +9 -9
  188. package/src/__tests__/teams-meetings-service.test.ts +65 -0
  189. package/src/__tests__/teamsAcsMediaRegistry.test.ts +82 -0
  190. package/src/__tests__/twilio-telephony-service.test.ts +23 -0
  191. package/src/__tests__/twilioMediaRegistry.test.ts +103 -0
  192. package/src/__tests__/vonageMediaRegistry.test.ts +180 -0
  193. package/src/__tests__/widget.test.ts +204 -0
  194. package/src/__tests__/widgetGuestElevation.test.ts +57 -0
  195. package/src/__tests__/widgetHostIdentity.test.ts +117 -0
  196. package/src/agentSessions/ReturningVisitorRecap.ts +242 -0
  197. package/src/agentSessions/SessionJanitor.ts +66 -6
  198. package/src/agentSessions/SessionManager.ts +38 -0
  199. package/src/auth/magicLink/MagicLinkRouter.ts +17 -0
  200. package/src/auth/magicLink/index.ts +1 -0
  201. package/src/auth/magicLink/types.ts +26 -0
  202. package/src/config.ts +172 -38
  203. package/src/context.ts +50 -3
  204. package/src/generated/generated.ts +1991 -1
  205. package/src/generic/ResolverBase.ts +28 -0
  206. package/src/generic/RunViewResolver.ts +4 -0
  207. package/src/index.ts +109 -3
  208. package/src/integration/CustomColumnPromoter.ts +6 -0
  209. package/src/realtimeWidget/WidgetRouter.ts +204 -0
  210. package/src/realtimeWidget/WidgetSessionService.ts +714 -0
  211. package/src/realtimeWidget/host-identity.ts +84 -0
  212. package/src/realtimeWidget/index.ts +15 -0
  213. package/src/realtimeWidget/visitorIdentity.ts +258 -0
  214. package/src/realtimeWidget/widgetCore.ts +231 -0
  215. package/src/realtimeWidget/widgetGuestElevation.ts +115 -0
  216. package/src/resolvers/ComponentRegistryResolver.ts +36 -10
  217. package/src/resolvers/EntityPermissionResolver.ts +1 -2
  218. package/src/resolvers/QuerySystemUserResolver.ts +14 -10
  219. package/src/resolvers/RealtimeClientSessionResolver.ts +59 -2
  220. package/src/resolvers/RingCentralTelephonyResolver.ts +64 -0
  221. package/src/resolvers/RunAIAgentResolver.ts +16 -4
  222. package/src/resolvers/SearchKnowledgeResolver.ts +2 -0
  223. package/src/resolvers/TeamsMeetingsResolver.ts +68 -0
  224. package/src/resolvers/TelephonyResolver.ts +63 -0
  225. package/src/resolvers/TestQuerySQLResolver.ts +2 -3
  226. package/src/resolvers/VonageTelephonyResolver.ts +63 -0
  227. package/src/rest/MediaStreamHandler.ts +4 -1
  228. package/src/telephony/RingCentralTelephonyService.ts +342 -0
  229. package/src/telephony/TeamsMeetingsRouter.ts +124 -0
  230. package/src/telephony/TeamsMeetingsService.ts +268 -0
  231. package/src/telephony/TwilioTelephonyRouter.ts +184 -0
  232. package/src/telephony/TwilioTelephonyService.ts +261 -0
  233. package/src/telephony/VonageTelephonyRouter.ts +239 -0
  234. package/src/telephony/VonageTelephonyService.ts +259 -0
  235. package/src/telephony/calendar-scheduler.ts +176 -0
  236. package/src/telephony/index.ts +43 -0
  237. package/src/telephony/media-upgrade-router.ts +62 -0
  238. package/src/telephony/ringcentral-runtime.ts +22 -0
  239. package/src/telephony/teams-meetings-runtime.ts +24 -0
  240. package/src/telephony/teamsAcsMediaRegistry.ts +152 -0
  241. package/src/telephony/telephony-runtime.ts +22 -0
  242. package/src/telephony/twilioMediaRegistry.ts +137 -0
  243. package/src/telephony/vonage-runtime.ts +22 -0
  244. package/src/telephony/vonageMediaRegistry.ts +200 -0
  245. package/dist/agents/skip-agent.d.ts +0 -111
  246. package/dist/agents/skip-agent.d.ts.map +0 -1
  247. package/dist/agents/skip-agent.js +0 -1487
  248. package/dist/agents/skip-agent.js.map +0 -1
  249. package/dist/agents/skip-sdk.d.ts +0 -261
  250. package/dist/agents/skip-sdk.d.ts.map +0 -1
  251. package/dist/agents/skip-sdk.js +0 -909
  252. package/dist/agents/skip-sdk.js.map +0 -1
  253. package/src/agents/skip-agent.ts +0 -1594
  254. 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,7 +1,6 @@
1
1
  import { Arg, Ctx, Field, InputType, Mutation, ObjectType, registerEnumType, Resolver, PubSub, PubSubEngine } from 'type-graphql';
2
2
  import { AppContext } from '../types.js';
3
3
  import { LogError, UserInfo, CompositeKey, DatabaseProviderBase, LogStatus } from '@memberjunction/core';
4
- import { RequireSystemUser } from '../directives/RequireSystemUser.js';
5
4
  import { MJQueryCategoryEntity, MJQueryPermissionEntity, MJQueryFieldEntity, MJQueryParameterEntity, MJQueryEntityEntity, QueryEngine } from '@memberjunction/core-entities';
6
5
  import { MJQueryResolver, MJQuery_, MJQueryField_, MJQueryParameter_, MJQueryEntity_, MJQueryPermission_ } from '../generated/generated.js';
7
6
  import { GetReadWriteProvider } from '../util.js';
@@ -229,19 +228,21 @@ export class DeleteQueryResultType {
229
228
  @Resolver()
230
229
  export class MJQueryResolverExtended extends MJQueryResolver {
231
230
  /**
232
- * Creates a new query with the provided attributes. This mutation is restricted to system users only.
231
+ * Creates a new query with the provided attributes. Requires `query:create` scope for API key users;
232
+ * entity-level create permission on MJ: Queries for all users.
233
233
  * @param input - CreateQuerySystemUserInput containing all the query attributes
234
234
  * @param context - Application context containing user information
235
235
  * @returns CreateQueryResultType with success status and query data
236
236
  */
237
- @RequireSystemUser()
238
237
  @Mutation(() => QueryMutationResultType)
239
- async CreateQuerySystemUser(
238
+ async CreateQueryExtended(
240
239
  @Arg('input', () => CreateQuerySystemUserInput) input: CreateQuerySystemUserInput,
241
240
  @Ctx() context: AppContext,
242
241
  @PubSub() pubSub: PubSubEngine
243
242
  ): Promise<QueryMutationResultType> {
244
243
  try {
244
+ await this.CheckAPIKeyScopeAuthorization('query:create', '*', context.userPayload);
245
+
245
246
  // Handle CategoryPath if provided
246
247
  let finalCategoryID = input.CategoryID;
247
248
  const provider = GetReadWriteProvider(context.providers);
@@ -443,19 +444,21 @@ export class MJQueryResolverExtended extends MJQueryResolver {
443
444
  }
444
445
 
445
446
  /**
446
- * Updates an existing query with the provided attributes. This mutation is restricted to system users only.
447
+ * Updates an existing query with the provided attributes. Requires `query:update` scope for API key users;
448
+ * entity-level update permission on MJ: Queries for all users.
447
449
  * @param input - UpdateQuerySystemUserInput containing the query ID and fields to update
448
450
  * @param context - Application context containing user information
449
451
  * @returns UpdateQueryResultType with success status and updated query data including related entities
450
452
  */
451
- @RequireSystemUser()
452
453
  @Mutation(() => QueryMutationResultType)
453
- async UpdateQuerySystemUser(
454
+ async UpdateQueryExtended(
454
455
  @Arg('input', () => UpdateQuerySystemUserInput) input: UpdateQuerySystemUserInput,
455
456
  @Ctx() context: AppContext,
456
457
  @PubSub() pubSub: PubSubEngine
457
458
  ): Promise<QueryMutationResultType> {
458
459
  try {
460
+ await this.CheckAPIKeyScopeAuthorization('query:update', input.ID, context.userPayload);
461
+
459
462
  // Load the existing query using MJQueryEntityServer
460
463
  const provider = GetReadWriteProvider(context.providers);
461
464
  const queryEntity = await provider.GetEntityObject<MJQueryEntityServer>('MJ: Queries', context.userPayload.userRecord);
@@ -547,21 +550,22 @@ export class MJQueryResolverExtended extends MJQueryResolver {
547
550
  }
548
551
 
549
552
  /**
550
- * Deletes a query by ID. This mutation is restricted to system users only.
553
+ * Deletes a query by ID. Requires `query:delete` scope for API key users;
554
+ * entity-level delete permission on MJ: Queries is enforced by the entity framework.
551
555
  * @param ID - The ID of the query to delete
552
556
  * @param options - Delete options controlling action execution
553
557
  * @param context - Application context containing user information
554
558
  * @returns DeleteQueryResultType with success status and deleted query data
555
559
  */
556
- @RequireSystemUser()
557
560
  @Mutation(() => DeleteQueryResultType)
558
- async DeleteQuerySystemResolver(
561
+ async DeleteQueryExtended(
559
562
  @Arg('ID', () => String) ID: string,
560
563
  @Arg('options', () => DeleteOptionsInput, { nullable: true }) options: DeleteOptionsInput | null,
561
564
  @Ctx() context: AppContext,
562
565
  @PubSub() pubSub: PubSubEngine
563
566
  ): Promise<DeleteQueryResultType> {
564
567
  try {
568
+ await this.CheckAPIKeyScopeAuthorization('query:delete', ID, context.userPayload);
565
569
  // Validate ID is not null/undefined/empty
566
570
  if (!ID || ID.trim() === '') {
567
571
  return {
@@ -60,6 +60,7 @@ import { ResolverBase } from '../generic/ResolverBase.js';
60
60
  import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
61
61
  import { GetReadWriteProvider } from '../util.js';
62
62
  import { SessionManager } from '../agentSessions/index.js';
63
+ import { resolveWidgetGuestRunContext } from '../realtimeWidget/widgetGuestElevation.js';
63
64
 
64
65
  /**
65
66
  * Progress steps worth narrating to the realtime model — mirrors the normal agent-run path's filter
@@ -161,6 +162,13 @@ interface RealtimeSessionConfig {
161
162
  * rather than starting fresh. Re-stored if the resumed run pauses again.
162
163
  */
163
164
  pendingFeedbackRunID?: string;
165
+ /**
166
+ * Absolute server-authoritative deadline (ISO 8601) past which this session is hard-closed by the
167
+ * {@link SessionJanitor}, independent of idle activity. Stamped at session start for abuse-sensitive
168
+ * deployments (a public web-widget voice guest's `VoiceMaxSessionMinutes`); absent for uncapped
169
+ * sessions. Preserved across config rewrites so the cap survives run-id / feedback updates.
170
+ */
171
+ maxSessionDeadlineIso?: string;
164
172
  /**
165
173
  * The application the session runs in (sourced the app cascade layer + RelevantAgents). Persisted
166
174
  * for observability / continuity; absent when the session carries no app context.
@@ -412,7 +420,15 @@ export class RealtimeClientSessionResolver extends ResolverBase {
412
420
  // silent ignore). Plain starts and within-pairing target selection are never gated here.
413
421
  await this.assertRuntimeOverridesAuthorized(coAgentID, configOverridesJson, preferredModelId, contextUser, provider);
414
422
 
423
+ // PUBLIC WEB-WIDGET VOICE CAP (public-web-widget.md W4): when the caller is a widget guest,
424
+ // resolve the deployment's hard duration ceiling and stamp an absolute deadline on the session.
425
+ // The SessionJanitor force-closes (finalizing runs) past the deadline — a server-authoritative
426
+ // bound that does not depend on the client honoring its own abuse guard or the ephemeral token TTL.
427
+ const maxSessionSeconds = await this.resolveWidgetVoiceCapSeconds(userPayload, providers);
415
428
  const config: RealtimeSessionConfig = { targetAgentID: effectiveTargetId };
429
+ if (maxSessionSeconds) {
430
+ config.maxSessionDeadlineIso = new Date(Date.now() + maxSessionSeconds * 1000).toISOString();
431
+ }
416
432
  // Per-session media-kit override: stored on the session config so the server-side MediaChannelServer
417
433
  // resolves it (over the agent default) at start. Validate the id here — a malformed value is
418
434
  // dropped (logged), never interpolated; the session still starts and falls back to the agent kit.
@@ -449,7 +465,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
449
465
  const priorTranscript = await this.loadPriorTranscript(lastSessionId, contextUser, provider);
450
466
  const result = await this.prepareClientSessionOrClose(
451
467
  session, coAgentID, effectiveTargetId, contextUser, provider, preferredModelId, clientTools, priorTranscript,
452
- configOverridesJson, applicationId, this.parseAppContext(appContextJson),
468
+ configOverridesJson, maxSessionSeconds, applicationId, this.parseAppContext(appContextJson),
453
469
  );
454
470
  // Best-effort restore of the PRIOR session's persisted channel states (e.g. the whiteboard
455
471
  // board). Strictly tolerant — any problem yields a null field, never a failed start.
@@ -562,6 +578,8 @@ export class RealtimeClientSessionResolver extends ResolverBase {
562
578
  promptRunID: config.promptRunID,
563
579
  coAgentRunStepID: config.coAgentRunStepID,
564
580
  pendingFeedbackRunID: pausedRunID,
581
+ // Preserve the server-authoritative voice deadline across config rewrites.
582
+ maxSessionDeadlineIso: config.maxSessionDeadlineIso,
565
583
  applicationID: config.applicationID,
566
584
  allowedAgents: config.allowedAgents,
567
585
  };
@@ -1535,6 +1553,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1535
1553
  clientTools?: RealtimeToolDefinition[],
1536
1554
  priorTranscript?: string,
1537
1555
  configOverridesJson?: string,
1556
+ maxSessionSeconds?: number,
1538
1557
  applicationId?: string,
1539
1558
  appContext?: AppContextSnapshot,
1540
1559
  ): Promise<StartRealtimeClientSessionResult> {
@@ -1544,6 +1563,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1544
1563
  TargetAgentID: targetAgentId,
1545
1564
  AgentSessionID: session.ID,
1546
1565
  ConversationID: session.ConversationID ?? undefined,
1566
+ // Server-authoritative voice duration cap (widget guests) — bounds the driver session
1567
+ // where supported; the janitor enforces the session deadline regardless.
1568
+ MaxSessionSeconds: maxSessionSeconds,
1547
1569
  // MVP: conversation history is not yet hydrated into ChatMessage[]; the co-agent
1548
1570
  // companion prompt runs without prior turns. A later phase loads the session's
1549
1571
  // Conversation into ChatMessage[] for richer context.
@@ -1608,8 +1630,16 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1608
1630
  applicationID?: string,
1609
1631
  allowedAgents?: RealtimeAllowedAgent[],
1610
1632
  ): Promise<void> {
1633
+ // Preserve any server-authoritative voice deadline stamped at session start (this rebuilds the
1634
+ // full config, so read the existing value forward rather than dropping it).
1635
+ const existing = this.tryReadSessionConfig(session);
1611
1636
  const config: RealtimeSessionConfig = {
1612
- targetAgentID, coAgentRunID, promptRunID, coAgentRunStepID, applicationID,
1637
+ targetAgentID,
1638
+ coAgentRunID,
1639
+ promptRunID,
1640
+ coAgentRunStepID,
1641
+ maxSessionDeadlineIso: existing?.maxSessionDeadlineIso,
1642
+ applicationID,
1613
1643
  allowedAgents: allowedAgents && allowedAgents.length > 0 ? allowedAgents : undefined,
1614
1644
  };
1615
1645
  session.Config_ = JSON.stringify(config);
@@ -2335,6 +2365,8 @@ export class RealtimeClientSessionResolver extends ResolverBase {
2335
2365
  coAgentRunStepID: typeof parsed.coAgentRunStepID === 'string' ? parsed.coAgentRunStepID : undefined,
2336
2366
  pendingFeedbackRunID:
2337
2367
  typeof parsed.pendingFeedbackRunID === 'string' ? parsed.pendingFeedbackRunID : undefined,
2368
+ maxSessionDeadlineIso:
2369
+ typeof parsed.maxSessionDeadlineIso === 'string' ? parsed.maxSessionDeadlineIso : undefined,
2338
2370
  applicationID: typeof parsed.applicationID === 'string' ? parsed.applicationID : undefined,
2339
2371
  allowedAgents: Array.isArray(parsed.allowedAgents) ? parsed.allowedAgents : undefined,
2340
2372
  };
@@ -2346,6 +2378,31 @@ export class RealtimeClientSessionResolver extends ResolverBase {
2346
2378
  throw new Error(`Realtime session ${session.ID} has no target agent configured`);
2347
2379
  }
2348
2380
 
2381
+ /** Like {@link readSessionConfig} but returns `undefined` instead of throwing (for best-effort reads). */
2382
+ private tryReadSessionConfig(session: MJAIAgentSessionEntity): RealtimeSessionConfig | undefined {
2383
+ try {
2384
+ return this.readSessionConfig(session);
2385
+ } catch {
2386
+ return undefined;
2387
+ }
2388
+ }
2389
+
2390
+ /**
2391
+ * Resolves the server-authoritative voice duration cap (seconds) for a session start, or `undefined`
2392
+ * when no cap applies (the common, uncapped case). A cap applies only when the caller is a public
2393
+ * web-widget guest with a configured `VoiceMaxSessionMinutes` on its widget instance (the same value
2394
+ * surfaced to the client guard at mint). Never throws — a resolution failure yields no cap rather
2395
+ * than blocking the start.
2396
+ */
2397
+ private async resolveWidgetVoiceCapSeconds(
2398
+ userPayload: UserPayload,
2399
+ providers: AppContext['providers'],
2400
+ ): Promise<number | undefined> {
2401
+ const elevation = await resolveWidgetGuestRunContext(userPayload, GetReadWriteProvider(providers));
2402
+ const minutes = elevation?.widget.VoiceMaxSessionMinutes;
2403
+ return minutes && minutes > 0 ? minutes * 60 : undefined;
2404
+ }
2405
+
2349
2406
  /**
2350
2407
  * Persists a single transcript turn as a `Conversation Detail` stamped with the session's
2351
2408
  * conversation, the mapped role, the turn text, the session id, and the owning user.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @fileoverview GraphQL surface for OUTBOUND RingCentral telephony: `PlaceRingCentralCall` rings a real
3
+ * phone from an agent identity and connects the pinned agent. Thin — it authorizes the caller, then
4
+ * delegates to the startup-constructed {@link RingCentralTelephonyService} (which owns the bridge
5
+ * orchestration + the shared media registry). Inbound calls arrive via the public
6
+ * `POST /telephony/ringcentral/webhook` notification, not GraphQL.
7
+ *
8
+ * @module @memberjunction/server
9
+ */
10
+
11
+ import { Resolver, Mutation, Arg, Ctx, ObjectType, Field } from 'type-graphql';
12
+ import { LogError, IMetadataProvider } from '@memberjunction/core';
13
+ import { AppContext } from '../types.js';
14
+ import { ResolverBase } from '../generic/ResolverBase.js';
15
+ import { GetReadWriteProvider } from '../util.js';
16
+ import { GetRingCentralTelephonyService } from '../telephony/ringcentral-runtime.js';
17
+
18
+ /** Result of an outbound RingCentral place-call attempt. */
19
+ @ObjectType()
20
+ export class PlaceRingCentralCallResult {
21
+ @Field(() => Boolean)
22
+ Success: boolean;
23
+
24
+ @Field(() => String, { nullable: true })
25
+ ErrorMessage?: string;
26
+
27
+ /** The placed RingCentral telephony session id (the bridge's external connection id). */
28
+ @Field(() => String)
29
+ SessionId: string;
30
+ }
31
+
32
+ @Resolver()
33
+ export class RingCentralTelephonyResolver extends ResolverBase {
34
+ /**
35
+ * Places an outbound call from the given agent identity (its phone number is the caller-id) to a
36
+ * destination number, connecting the pinned agent over the realtime bridge. Returns the telephony
37
+ * session id.
38
+ */
39
+ @Mutation(() => PlaceRingCentralCallResult)
40
+ async PlaceRingCentralCall(
41
+ @Arg('agentIdentityId', () => String) agentIdentityId: string,
42
+ @Arg('toNumber', () => String) toNumber: string,
43
+ @Ctx() context: AppContext = {} as AppContext,
44
+ ): Promise<PlaceRingCentralCallResult> {
45
+ const failure = (msg: string): PlaceRingCentralCallResult => ({ Success: false, ErrorMessage: msg, SessionId: '' });
46
+ try {
47
+ const user = this.GetUserFromPayload(context.userPayload);
48
+ if (!user) {
49
+ return failure('Unable to determine current user.');
50
+ }
51
+ const service = GetRingCentralTelephonyService();
52
+ if (!service) {
53
+ return failure('RingCentral telephony is not configured on this server.');
54
+ }
55
+ const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
56
+ const sessionId = await service.PlaceOutboundCall(agentIdentityId, toNumber, user, provider);
57
+ return { Success: true, SessionId: sessionId };
58
+ } catch (error) {
59
+ const msg = error instanceof Error ? error.message : String(error);
60
+ LogError(`PlaceRingCentralCall failed: ${msg}`);
61
+ return failure(msg);
62
+ }
63
+ }
64
+ }
@@ -12,6 +12,7 @@ import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
12
12
  import { startLivenessPulse } from '../generic/FireAndForgetHeartbeat.js';
13
13
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
14
14
  import { GetReadWriteProvider } from '../util.js';
15
+ import { resolveWidgetGuestRunContext, elevateUserPayload } from '../realtimeWidget/widgetGuestElevation.js';
15
16
  import { SafeJSONParse, UUIDsEqual } from '@memberjunction/global';
16
17
  import { GetAttachmentService } from '@memberjunction/aiengine';
17
18
  import { NotificationEngine } from '@memberjunction/notifications';
@@ -974,6 +975,17 @@ export class RunAIAgentResolver extends ResolverBase {
974
975
  };
975
976
  }
976
977
 
978
+ // PUBLIC WEB-WIDGET PRIVILEGED DISPATCH (public-web-widget.md Phase 0): when the request is a
979
+ // widget guest, the agent runs under a TRUSTED SERVER PRINCIPAL and the agent id is taken
980
+ // AUTHORITATIVELY from the widget instance (never the client-supplied arg) — so a guest needs
981
+ // no grants to WRITE the AI run entities and cannot run an arbitrary agent under elevation.
982
+ // Conversation OWNERSHIP is still enforced under the guest principal below: the guest loads its
983
+ // own ConversationDetail through the Widget Guest RLS filters, so a detail id from another
984
+ // session resolves to "not found" before any elevated work happens.
985
+ const widgetElevation = await resolveWidgetGuestRunContext(userPayload, p);
986
+ const effectiveAgentId = widgetElevation ? widgetElevation.pinnedAgentId : agentId;
987
+ const effectiveUserPayload = widgetElevation ? elevateUserPayload(userPayload, widgetElevation.elevatedUser) : userPayload;
988
+
977
989
  try {
978
990
  // LATENCY OPTIMIZATION (Opt #2 + #3): Load ConversationDetail once here to extract
979
991
  // conversationId, then pass it downstream. Previously this record was loaded multiple
@@ -1004,13 +1016,13 @@ export class RunAIAgentResolver extends ResolverBase {
1004
1016
  // Fire-and-forget mode: start execution in background, return immediately.
1005
1017
  // The client will receive the result via WebSocket PubSub completion event.
1006
1018
  this.executeAgentInBackground(
1007
- p, dataSource, agentId, userPayload, messagesJson, sessionId, pubSub,
1019
+ p, dataSource, effectiveAgentId, effectiveUserPayload, messagesJson, sessionId, pubSub,
1008
1020
  data, payload, lastRunId, autoPopulateLastRunPayload, configurationId,
1009
1021
  conversationDetailId, createArtifacts || false, createNotification || false,
1010
1022
  sourceArtifactId, sourceArtifactVersionId, conversationId, planMode, requestedSkillIDs
1011
1023
  );
1012
1024
 
1013
- LogStatus(`🔥 Fire-and-forget: Agent ${agentId} execution started in background for session ${sessionId}`);
1025
+ LogStatus(`🔥 Fire-and-forget: Agent ${effectiveAgentId} execution started in background for session ${sessionId}`);
1014
1026
 
1015
1027
  return {
1016
1028
  success: true,
@@ -1022,8 +1034,8 @@ export class RunAIAgentResolver extends ResolverBase {
1022
1034
  return this.executeAIAgent(
1023
1035
  p,
1024
1036
  dataSource,
1025
- agentId,
1026
- userPayload,
1037
+ effectiveAgentId,
1038
+ effectiveUserPayload,
1027
1039
  messagesJson,
1028
1040
  sessionId,
1029
1041
  pubSub,
@@ -213,6 +213,7 @@ export class SearchKnowledgeResolver extends ResolverBase {
213
213
  ): Promise<SearchKnowledgeResult> {
214
214
  const startTime = Date.now();
215
215
  try {
216
+ await this.CheckAPIKeyScopeAuthorization('search:execute', '*', userPayload);
216
217
  const currentUser = this.GetUserFromPayload(userPayload);
217
218
  if (!currentUser) {
218
219
  return this.errorResult('Unable to determine current user', startTime);
@@ -400,6 +401,7 @@ export class SearchKnowledgeResolver extends ResolverBase {
400
401
  ): Promise<SearchKnowledgeResult> {
401
402
  const startTime = Date.now();
402
403
  try {
404
+ await this.CheckAPIKeyScopeAuthorization('search:execute', '*', userPayload);
403
405
  const currentUser = this.GetUserFromPayload(userPayload);
404
406
  if (!currentUser) {
405
407
  return this.errorResult('Unable to determine current user', startTime);