@memberjunction/server 5.44.0 → 5.45.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agentSessions/ReturningVisitorRecap.d.ts +39 -0
- package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -0
- package/dist/agentSessions/ReturningVisitorRecap.js +198 -0
- package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -0
- package/dist/agentSessions/SessionJanitor.d.ts +13 -0
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +65 -7
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +37 -0
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.d.ts +10 -0
- package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.js +16 -0
- package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts +7 -0
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +13 -3
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/magicLink/index.d.ts +1 -1
- package/dist/auth/magicLink/index.d.ts.map +1 -1
- package/dist/auth/magicLink/index.js +1 -1
- package/dist/auth/magicLink/index.js.map +1 -1
- package/dist/auth/magicLink/magicLinkCore.d.ts +38 -1
- package/dist/auth/magicLink/magicLinkCore.d.ts.map +1 -1
- package/dist/auth/magicLink/magicLinkCore.js +42 -19
- package/dist/auth/magicLink/magicLinkCore.js.map +1 -1
- package/dist/auth/magicLink/types.d.ts +26 -0
- package/dist/auth/magicLink/types.d.ts.map +1 -1
- package/dist/config.d.ts +1883 -144
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +160 -36
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +45 -2
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +522 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +2860 -0
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +13 -0
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +22 -0
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +4 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +101 -3
- package/dist/index.js.map +1 -1
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
- package/dist/integration/CustomColumnPromoter.js +6 -0
- package/dist/integration/CustomColumnPromoter.js.map +1 -1
- package/dist/realtimeWidget/WidgetRouter.d.ts +26 -0
- package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -0
- package/dist/realtimeWidget/WidgetRouter.js +182 -0
- package/dist/realtimeWidget/WidgetRouter.js.map +1 -0
- package/dist/realtimeWidget/WidgetSessionService.d.ts +265 -0
- package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -0
- package/dist/realtimeWidget/WidgetSessionService.js +477 -0
- package/dist/realtimeWidget/WidgetSessionService.js.map +1 -0
- package/dist/realtimeWidget/host-identity.d.ts +40 -0
- package/dist/realtimeWidget/host-identity.d.ts.map +1 -0
- package/dist/realtimeWidget/host-identity.js +58 -0
- package/dist/realtimeWidget/host-identity.js.map +1 -0
- package/dist/realtimeWidget/index.d.ts +10 -0
- package/dist/realtimeWidget/index.d.ts.map +1 -0
- package/dist/realtimeWidget/index.js +9 -0
- package/dist/realtimeWidget/index.js.map +1 -0
- package/dist/realtimeWidget/visitorIdentity.d.ts +76 -0
- package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -0
- package/dist/realtimeWidget/visitorIdentity.js +202 -0
- package/dist/realtimeWidget/visitorIdentity.js.map +1 -0
- package/dist/realtimeWidget/widgetCore.d.ts +106 -0
- package/dist/realtimeWidget/widgetCore.d.ts.map +1 -0
- package/dist/realtimeWidget/widgetCore.js +173 -0
- package/dist/realtimeWidget/widgetCore.js.map +1 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +51 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -0
- package/dist/realtimeWidget/widgetGuestElevation.js +79 -0
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -0
- package/dist/resolvers/ComponentRegistryResolver.d.ts +7 -0
- package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +31 -8
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/EntityPermissionResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityPermissionResolver.js +1 -2
- package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +9 -6
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +15 -13
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +47 -3
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RingCentralTelephonyResolver.d.ts +27 -0
- package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +1 -0
- package/dist/resolvers/RingCentralTelephonyResolver.js +87 -0
- package/dist/resolvers/RingCentralTelephonyResolver.js.map +1 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts +9 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +20 -4
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +2 -0
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/dist/resolvers/TeamsMeetingsResolver.d.ts +28 -0
- package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +1 -0
- package/dist/resolvers/TeamsMeetingsResolver.js +91 -0
- package/dist/resolvers/TeamsMeetingsResolver.js.map +1 -0
- package/dist/resolvers/TelephonyResolver.d.ts +26 -0
- package/dist/resolvers/TelephonyResolver.d.ts.map +1 -0
- package/dist/resolvers/TelephonyResolver.js +86 -0
- package/dist/resolvers/TelephonyResolver.js.map +1 -0
- package/dist/resolvers/TestQuerySQLResolver.d.ts +1 -1
- package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.js +2 -3
- package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
- package/dist/resolvers/VonageTelephonyResolver.d.ts +26 -0
- package/dist/resolvers/VonageTelephonyResolver.d.ts.map +1 -0
- package/dist/resolvers/VonageTelephonyResolver.js +86 -0
- package/dist/resolvers/VonageTelephonyResolver.js.map +1 -0
- package/dist/rest/MediaStreamHandler.js +4 -1
- package/dist/rest/MediaStreamHandler.js.map +1 -1
- package/dist/telephony/RingCentralTelephonyService.d.ts +115 -0
- package/dist/telephony/RingCentralTelephonyService.d.ts.map +1 -0
- package/dist/telephony/RingCentralTelephonyService.js +262 -0
- package/dist/telephony/RingCentralTelephonyService.js.map +1 -0
- package/dist/telephony/TeamsMeetingsRouter.d.ts +40 -0
- package/dist/telephony/TeamsMeetingsRouter.d.ts.map +1 -0
- package/dist/telephony/TeamsMeetingsRouter.js +96 -0
- package/dist/telephony/TeamsMeetingsRouter.js.map +1 -0
- package/dist/telephony/TeamsMeetingsService.d.ts +113 -0
- package/dist/telephony/TeamsMeetingsService.d.ts.map +1 -0
- package/dist/telephony/TeamsMeetingsService.js +196 -0
- package/dist/telephony/TeamsMeetingsService.js.map +1 -0
- package/dist/telephony/TwilioTelephonyRouter.d.ts +36 -0
- package/dist/telephony/TwilioTelephonyRouter.d.ts.map +1 -0
- package/dist/telephony/TwilioTelephonyRouter.js +143 -0
- package/dist/telephony/TwilioTelephonyRouter.js.map +1 -0
- package/dist/telephony/TwilioTelephonyService.d.ts +95 -0
- package/dist/telephony/TwilioTelephonyService.d.ts.map +1 -0
- package/dist/telephony/TwilioTelephonyService.js +193 -0
- package/dist/telephony/TwilioTelephonyService.js.map +1 -0
- package/dist/telephony/VonageTelephonyRouter.d.ts +41 -0
- package/dist/telephony/VonageTelephonyRouter.d.ts.map +1 -0
- package/dist/telephony/VonageTelephonyRouter.js +201 -0
- package/dist/telephony/VonageTelephonyRouter.js.map +1 -0
- package/dist/telephony/VonageTelephonyService.d.ts +90 -0
- package/dist/telephony/VonageTelephonyService.d.ts.map +1 -0
- package/dist/telephony/VonageTelephonyService.js +191 -0
- package/dist/telephony/VonageTelephonyService.js.map +1 -0
- package/dist/telephony/calendar-scheduler.d.ts +67 -0
- package/dist/telephony/calendar-scheduler.d.ts.map +1 -0
- package/dist/telephony/calendar-scheduler.js +133 -0
- package/dist/telephony/calendar-scheduler.js.map +1 -0
- package/dist/telephony/index.d.ts +29 -0
- package/dist/telephony/index.d.ts.map +1 -0
- package/dist/telephony/index.js +38 -0
- package/dist/telephony/index.js.map +1 -0
- package/dist/telephony/media-upgrade-router.d.ts +33 -0
- package/dist/telephony/media-upgrade-router.d.ts.map +1 -0
- package/dist/telephony/media-upgrade-router.js +56 -0
- package/dist/telephony/media-upgrade-router.js.map +1 -0
- package/dist/telephony/ringcentral-runtime.d.ts +14 -0
- package/dist/telephony/ringcentral-runtime.d.ts.map +1 -0
- package/dist/telephony/ringcentral-runtime.js +18 -0
- package/dist/telephony/ringcentral-runtime.js.map +1 -0
- package/dist/telephony/teams-meetings-runtime.d.ts +16 -0
- package/dist/telephony/teams-meetings-runtime.d.ts.map +1 -0
- package/dist/telephony/teams-meetings-runtime.js +20 -0
- package/dist/telephony/teams-meetings-runtime.js.map +1 -0
- package/dist/telephony/teamsAcsMediaRegistry.d.ts +69 -0
- package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +1 -0
- package/dist/telephony/teamsAcsMediaRegistry.js +118 -0
- package/dist/telephony/teamsAcsMediaRegistry.js.map +1 -0
- package/dist/telephony/telephony-runtime.d.ts +14 -0
- package/dist/telephony/telephony-runtime.d.ts.map +1 -0
- package/dist/telephony/telephony-runtime.js +18 -0
- package/dist/telephony/telephony-runtime.js.map +1 -0
- package/dist/telephony/twilioMediaRegistry.d.ts +60 -0
- package/dist/telephony/twilioMediaRegistry.d.ts.map +1 -0
- package/dist/telephony/twilioMediaRegistry.js +106 -0
- package/dist/telephony/twilioMediaRegistry.js.map +1 -0
- package/dist/telephony/vonage-runtime.d.ts +14 -0
- package/dist/telephony/vonage-runtime.d.ts.map +1 -0
- package/dist/telephony/vonage-runtime.js +18 -0
- package/dist/telephony/vonage-runtime.js.map +1 -0
- package/dist/telephony/vonageMediaRegistry.d.ts +78 -0
- package/dist/telephony/vonageMediaRegistry.d.ts.map +1 -0
- package/dist/telephony/vonageMediaRegistry.js +158 -0
- package/dist/telephony/vonageMediaRegistry.js.map +1 -0
- package/package.json +88 -83
- package/src/__tests__/RunAIAgentResolver.streaming.test.ts +128 -0
- package/src/__tests__/magicLink.test.ts +53 -3
- package/src/__tests__/search-knowledge-tags.test.ts +9 -9
- package/src/__tests__/teams-meetings-service.test.ts +65 -0
- package/src/__tests__/teamsAcsMediaRegistry.test.ts +82 -0
- package/src/__tests__/twilio-telephony-service.test.ts +23 -0
- package/src/__tests__/twilioMediaRegistry.test.ts +103 -0
- package/src/__tests__/vonageMediaRegistry.test.ts +180 -0
- package/src/__tests__/widget.test.ts +204 -0
- package/src/__tests__/widgetGuestElevation.test.ts +57 -0
- package/src/__tests__/widgetHostIdentity.test.ts +117 -0
- package/src/agentSessions/ReturningVisitorRecap.ts +242 -0
- package/src/agentSessions/SessionJanitor.ts +66 -6
- package/src/agentSessions/SessionManager.ts +38 -0
- package/src/auth/magicLink/MagicLinkRouter.ts +17 -0
- package/src/auth/magicLink/MagicLinkService.ts +13 -3
- package/src/auth/magicLink/index.ts +1 -0
- package/src/auth/magicLink/magicLinkCore.ts +48 -20
- package/src/auth/magicLink/types.ts +26 -0
- package/src/config.ts +172 -38
- package/src/context.ts +50 -3
- package/src/generated/generated.ts +1991 -1
- package/src/generic/ResolverBase.ts +28 -0
- package/src/generic/RunViewResolver.ts +4 -0
- package/src/index.ts +109 -3
- package/src/integration/CustomColumnPromoter.ts +6 -0
- package/src/realtimeWidget/WidgetRouter.ts +204 -0
- package/src/realtimeWidget/WidgetSessionService.ts +714 -0
- package/src/realtimeWidget/host-identity.ts +84 -0
- package/src/realtimeWidget/index.ts +15 -0
- package/src/realtimeWidget/visitorIdentity.ts +258 -0
- package/src/realtimeWidget/widgetCore.ts +231 -0
- package/src/realtimeWidget/widgetGuestElevation.ts +115 -0
- package/src/resolvers/ComponentRegistryResolver.ts +36 -10
- package/src/resolvers/EntityPermissionResolver.ts +1 -2
- package/src/resolvers/QuerySystemUserResolver.ts +14 -10
- package/src/resolvers/RealtimeClientSessionResolver.ts +59 -2
- package/src/resolvers/RingCentralTelephonyResolver.ts +64 -0
- package/src/resolvers/RunAIAgentResolver.ts +32 -8
- package/src/resolvers/SearchKnowledgeResolver.ts +2 -0
- package/src/resolvers/TeamsMeetingsResolver.ts +68 -0
- package/src/resolvers/TelephonyResolver.ts +63 -0
- package/src/resolvers/TestQuerySQLResolver.ts +2 -3
- package/src/resolvers/VonageTelephonyResolver.ts +63 -0
- package/src/rest/MediaStreamHandler.ts +4 -1
- package/src/telephony/RingCentralTelephonyService.ts +342 -0
- package/src/telephony/TeamsMeetingsRouter.ts +124 -0
- package/src/telephony/TeamsMeetingsService.ts +268 -0
- package/src/telephony/TwilioTelephonyRouter.ts +184 -0
- package/src/telephony/TwilioTelephonyService.ts +261 -0
- package/src/telephony/VonageTelephonyRouter.ts +239 -0
- package/src/telephony/VonageTelephonyService.ts +259 -0
- package/src/telephony/calendar-scheduler.ts +176 -0
- package/src/telephony/index.ts +43 -0
- package/src/telephony/media-upgrade-router.ts +62 -0
- package/src/telephony/ringcentral-runtime.ts +22 -0
- package/src/telephony/teams-meetings-runtime.ts +24 -0
- package/src/telephony/teamsAcsMediaRegistry.ts +152 -0
- package/src/telephony/telephony-runtime.ts +22 -0
- package/src/telephony/twilioMediaRegistry.ts +137 -0
- package/src/telephony/vonage-runtime.ts +22 -0
- package/src/telephony/vonageMediaRegistry.ts +200 -0
- package/dist/agents/skip-agent.d.ts +0 -111
- package/dist/agents/skip-agent.d.ts.map +0 -1
- package/dist/agents/skip-agent.js +0 -1487
- package/dist/agents/skip-agent.js.map +0 -1
- package/dist/agents/skip-sdk.d.ts +0 -261
- package/dist/agents/skip-sdk.d.ts.map +0 -1
- package/dist/agents/skip-sdk.js +0 -909
- package/dist/agents/skip-sdk.js.map +0 -1
- package/src/agents/skip-agent.ts +0 -1594
- 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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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,
|
|
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
|
+
}
|
|
@@ -4,7 +4,7 @@ import { DatabaseProviderBase, LogError, LogStatus, Metadata, RunView, UserInfo,
|
|
|
4
4
|
import { MJConversationDetailEntity, MJConversationDetailAttachmentEntity, MJConversationDetailArtifactEntity, MJArtifactVersionEntity, MJAIAgentRequestEntity, ArtifactMetadataEngine } from '@memberjunction/core-entities';
|
|
5
5
|
import { RouteArtifact } from './artifact-routing.js';
|
|
6
6
|
import { AgentRunner, ArtifactToolManager } from '@memberjunction/ai-agents';
|
|
7
|
-
import { MJAIAgentEntityExtended, MJAIAgentRunEntityExtended, ExecuteAgentResult, ConversationUtility, AttachmentData } from '@memberjunction/ai-core-plus';
|
|
7
|
+
import { MJAIAgentEntityExtended, MJAIAgentRunEntityExtended, ExecuteAgentResult, ConversationUtility, AttachmentData, AgentExecutionStreamingCallback } from '@memberjunction/ai-core-plus';
|
|
8
8
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
9
9
|
import { ChatMessage, ChatMessageContent } from '@memberjunction/ai';
|
|
10
10
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
@@ -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';
|
|
@@ -77,6 +78,17 @@ export class AgentStreamingContent {
|
|
|
77
78
|
|
|
78
79
|
@Field({ nullable: true })
|
|
79
80
|
agentName?: string;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Content discriminator passed through from the agent's streaming chunk (see
|
|
84
|
+
* `AgentExecutionStreamingCallback` in @memberjunction/ai-core-plus).
|
|
85
|
+
* 'final-response' marks user-facing reply deltas the conversation client
|
|
86
|
+
* accumulates + renders into the message bubble; chunks without a kind are
|
|
87
|
+
* raw prompt output (e.g. a Loop agent's JSON turn envelope) and are not
|
|
88
|
+
* rendered by the conversation client.
|
|
89
|
+
*/
|
|
90
|
+
@Field({ nullable: true })
|
|
91
|
+
kind?: string;
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
@ObjectType()
|
|
@@ -321,8 +333,8 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
321
333
|
/**
|
|
322
334
|
* Create streaming content callback
|
|
323
335
|
*/
|
|
324
|
-
private createStreamingCallback(pubSub: PubSubEngine, sessionId: string, userPayload: UserPayload, agentRunRef: { current:
|
|
325
|
-
return (chunk
|
|
336
|
+
private createStreamingCallback(pubSub: PubSubEngine, sessionId: string, userPayload: UserPayload, agentRunRef: { current: MJAIAgentRunEntityExtended | null }): AgentExecutionStreamingCallback {
|
|
337
|
+
return (chunk) => {
|
|
326
338
|
// Use the agent run from the ref
|
|
327
339
|
const agentRun = agentRunRef.current;
|
|
328
340
|
if (!agentRun) {
|
|
@@ -340,7 +352,8 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
340
352
|
content: chunk.content,
|
|
341
353
|
isPartial: !chunk.isComplete,
|
|
342
354
|
stepName: chunk.stepType,
|
|
343
|
-
agentName: chunk.modelName
|
|
355
|
+
agentName: chunk.modelName,
|
|
356
|
+
kind: chunk.kind
|
|
344
357
|
},
|
|
345
358
|
timestamp: new Date()
|
|
346
359
|
};
|
|
@@ -974,6 +987,17 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
974
987
|
};
|
|
975
988
|
}
|
|
976
989
|
|
|
990
|
+
// PUBLIC WEB-WIDGET PRIVILEGED DISPATCH (public-web-widget.md Phase 0): when the request is a
|
|
991
|
+
// widget guest, the agent runs under a TRUSTED SERVER PRINCIPAL and the agent id is taken
|
|
992
|
+
// AUTHORITATIVELY from the widget instance (never the client-supplied arg) — so a guest needs
|
|
993
|
+
// no grants to WRITE the AI run entities and cannot run an arbitrary agent under elevation.
|
|
994
|
+
// Conversation OWNERSHIP is still enforced under the guest principal below: the guest loads its
|
|
995
|
+
// own ConversationDetail through the Widget Guest RLS filters, so a detail id from another
|
|
996
|
+
// session resolves to "not found" before any elevated work happens.
|
|
997
|
+
const widgetElevation = await resolveWidgetGuestRunContext(userPayload, p);
|
|
998
|
+
const effectiveAgentId = widgetElevation ? widgetElevation.pinnedAgentId : agentId;
|
|
999
|
+
const effectiveUserPayload = widgetElevation ? elevateUserPayload(userPayload, widgetElevation.elevatedUser) : userPayload;
|
|
1000
|
+
|
|
977
1001
|
try {
|
|
978
1002
|
// LATENCY OPTIMIZATION (Opt #2 + #3): Load ConversationDetail once here to extract
|
|
979
1003
|
// conversationId, then pass it downstream. Previously this record was loaded multiple
|
|
@@ -1004,13 +1028,13 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1004
1028
|
// Fire-and-forget mode: start execution in background, return immediately.
|
|
1005
1029
|
// The client will receive the result via WebSocket PubSub completion event.
|
|
1006
1030
|
this.executeAgentInBackground(
|
|
1007
|
-
p, dataSource,
|
|
1031
|
+
p, dataSource, effectiveAgentId, effectiveUserPayload, messagesJson, sessionId, pubSub,
|
|
1008
1032
|
data, payload, lastRunId, autoPopulateLastRunPayload, configurationId,
|
|
1009
1033
|
conversationDetailId, createArtifacts || false, createNotification || false,
|
|
1010
1034
|
sourceArtifactId, sourceArtifactVersionId, conversationId, planMode, requestedSkillIDs
|
|
1011
1035
|
);
|
|
1012
1036
|
|
|
1013
|
-
LogStatus(`🔥 Fire-and-forget: Agent ${
|
|
1037
|
+
LogStatus(`🔥 Fire-and-forget: Agent ${effectiveAgentId} execution started in background for session ${sessionId}`);
|
|
1014
1038
|
|
|
1015
1039
|
return {
|
|
1016
1040
|
success: true,
|
|
@@ -1022,8 +1046,8 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1022
1046
|
return this.executeAIAgent(
|
|
1023
1047
|
p,
|
|
1024
1048
|
dataSource,
|
|
1025
|
-
|
|
1026
|
-
|
|
1049
|
+
effectiveAgentId,
|
|
1050
|
+
effectiveUserPayload,
|
|
1027
1051
|
messagesJson,
|
|
1028
1052
|
sessionId,
|
|
1029
1053
|
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);
|