@memberjunction/server 5.43.0 → 5.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +40 -0
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/agentSessions/index.d.ts.map +1 -1
- package/dist/agentSessions/index.js +3 -1
- package/dist/agentSessions/index.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/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/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 +1684 -54
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +57105 -48029
- 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 +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +110 -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/FileResolver.d.ts +74 -0
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +211 -2
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +6 -4
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +29 -14
- package/dist/resolvers/QueryResolver.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/RealtimeBridgeResolver.d.ts +8 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +28 -4
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +91 -2
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +337 -17
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +17 -2
- package/dist/resolvers/RemoteBrowserActionResolver.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 +12 -3
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +50 -17
- 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/resolvers/meetingRecordingRegistration.d.ts +124 -0
- package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -0
- package/dist/resolvers/meetingRecordingRegistration.js +311 -0
- package/dist/resolvers/meetingRecordingRegistration.js.map +1 -0
- package/dist/resolvers/peaksSidecar.d.ts +30 -0
- package/dist/resolvers/peaksSidecar.d.ts.map +1 -0
- package/dist/resolvers/peaksSidecar.js +51 -0
- package/dist/resolvers/peaksSidecar.js.map +1 -0
- package/dist/rest/MediaAccessKeys.d.ts +68 -0
- package/dist/rest/MediaAccessKeys.d.ts.map +1 -0
- package/dist/rest/MediaAccessKeys.js +96 -0
- package/dist/rest/MediaAccessKeys.js.map +1 -0
- package/dist/rest/MediaStreamHandler.d.ts +26 -0
- package/dist/rest/MediaStreamHandler.d.ts.map +1 -0
- package/dist/rest/MediaStreamHandler.js +195 -0
- package/dist/rest/MediaStreamHandler.js.map +1 -0
- package/dist/rest/mediaRange.d.ts +41 -0
- package/dist/rest/mediaRange.d.ts.map +1 -0
- package/dist/rest/mediaRange.js +60 -0
- package/dist/rest/mediaRange.js.map +1 -0
- 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 -82
- package/src/__tests__/FileResolverPeaks.test.ts +64 -0
- package/src/__tests__/MediaAccessKeys.test.ts +68 -0
- package/src/__tests__/MediaStreamHandler.test.ts +91 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -1
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +69 -0
- package/src/__tests__/RealtimeCoAgentAppAwareness.integration.test.ts +191 -0
- package/src/__tests__/RemoteBrowserSnapshot.test.ts +123 -0
- package/src/__tests__/SessionManager.test.ts +2 -0
- package/src/__tests__/meetingRecordingRegistration.test.ts +248 -0
- 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 +41 -0
- package/src/agentSessions/index.ts +3 -1
- package/src/auth/magicLink/MagicLinkRouter.ts +17 -0
- package/src/auth/magicLink/index.ts +1 -0
- 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 +26726 -20464
- package/src/generic/ResolverBase.ts +28 -0
- package/src/generic/RunViewResolver.ts +4 -0
- package/src/index.ts +119 -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/FileResolver.ts +199 -1
- package/src/resolvers/QueryResolver.ts +33 -19
- package/src/resolvers/QuerySystemUserResolver.ts +14 -10
- package/src/resolvers/RealtimeBridgeResolver.ts +36 -4
- package/src/resolvers/RealtimeClientSessionResolver.ts +383 -9
- package/src/resolvers/RemoteBrowserActionResolver.ts +18 -2
- package/src/resolvers/RingCentralTelephonyResolver.ts +64 -0
- package/src/resolvers/RunAIAgentResolver.ts +60 -15
- 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/resolvers/meetingRecordingRegistration.ts +432 -0
- package/src/resolvers/peaksSidecar.ts +52 -0
- package/src/rest/MediaAccessKeys.ts +121 -0
- package/src/rest/MediaStreamHandler.ts +223 -0
- package/src/rest/mediaRange.ts +76 -0
- 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,6 +1,7 @@
|
|
|
1
|
-
import { EntityPermissionType, FieldValueCollection, EntitySaveOptions } from '@memberjunction/core';
|
|
1
|
+
import { EntityPermissionType, FieldValueCollection, EntitySaveOptions, LogError, UserInfo } from '@memberjunction/core';
|
|
2
2
|
import { NormalizeUUID } from '@memberjunction/global';
|
|
3
3
|
import { MJFileEntity, MJFileStorageProviderEntity, MJFileStorageAccountEntity } from '@memberjunction/core-entities';
|
|
4
|
+
import { readRealtimeRecordingFile } from '@memberjunction/ai-agents';
|
|
4
5
|
import {
|
|
5
6
|
AppContext,
|
|
6
7
|
Arg,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
DeleteOptionsInput,
|
|
9
10
|
Field,
|
|
10
11
|
FieldResolver,
|
|
12
|
+
Float,
|
|
11
13
|
InputType,
|
|
12
14
|
Int,
|
|
13
15
|
Mutation,
|
|
@@ -37,6 +39,9 @@ import {
|
|
|
37
39
|
import { CreateMJFileInput, MJFileResolver as FileResolverBase, MJFile_, UpdateMJFileInput } from '../generated/generated.js';
|
|
38
40
|
import { FieldMapper } from '@memberjunction/graphql-dataprovider';
|
|
39
41
|
import { GetReadOnlyProvider } from '../util.js';
|
|
42
|
+
import { configInfo } from '../config.js';
|
|
43
|
+
import { MediaAccessKeyManager } from '../rest/MediaAccessKeys.js';
|
|
44
|
+
import { deriveSidecarPath, parsePeaksSidecar } from './peaksSidecar.js';
|
|
40
45
|
|
|
41
46
|
@InputType()
|
|
42
47
|
export class CreateUploadURLInput {
|
|
@@ -312,8 +317,161 @@ export class SearchAcrossAccountsPayload {
|
|
|
312
317
|
failedAccounts: number;
|
|
313
318
|
}
|
|
314
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Result of {@link FileResolver.GetFileContents} — the bytes of an `MJ: Files` record returned as
|
|
322
|
+
* base64, read server-side through authenticated MJStorage (never a public pre-signed link).
|
|
323
|
+
*/
|
|
324
|
+
@ObjectType()
|
|
325
|
+
export class FileContentsResult {
|
|
326
|
+
@Field(() => Boolean)
|
|
327
|
+
Success: boolean;
|
|
328
|
+
|
|
329
|
+
@Field(() => String, { nullable: true })
|
|
330
|
+
Base64?: string;
|
|
331
|
+
|
|
332
|
+
@Field(() => String, { nullable: true })
|
|
333
|
+
MimeType?: string;
|
|
334
|
+
|
|
335
|
+
@Field(() => String, { nullable: true })
|
|
336
|
+
ErrorMessage?: string;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Result of {@link FileResolver.CreateMediaAccessToken} — a short-lived signed token plus the
|
|
341
|
+
* authenticated streaming URL the browser hands to a `<audio>`/`<video>` element. The token is
|
|
342
|
+
* minted only after a per-user permission check on the `MJ: Files` row; the `/media/:fileId`
|
|
343
|
+
* route re-verifies it (signature + expiry + file match) on each Range request.
|
|
344
|
+
*/
|
|
345
|
+
@ObjectType()
|
|
346
|
+
export class MediaAccessTokenResult {
|
|
347
|
+
@Field(() => Boolean)
|
|
348
|
+
Success: boolean;
|
|
349
|
+
|
|
350
|
+
@Field(() => String, { nullable: true })
|
|
351
|
+
Token?: string;
|
|
352
|
+
|
|
353
|
+
@Field(() => String, { nullable: true })
|
|
354
|
+
Url?: string;
|
|
355
|
+
|
|
356
|
+
@Field(() => Date, { nullable: true })
|
|
357
|
+
ExpiresAt?: Date;
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* The file's MIME type (from the already-loaded `MJ: Files` row). Lets the wrapper choose
|
|
361
|
+
* `<audio>` vs `<video>` WITHOUT re-downloading any bytes; the stream itself sets Content-Type.
|
|
362
|
+
*/
|
|
363
|
+
@Field(() => String, { nullable: true })
|
|
364
|
+
MimeType?: string;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Optional precomputed waveform peaks (normalized `0..1`, one per rendered bar) read from a
|
|
368
|
+
* `peaks.json` sidecar that sits beside the file in storage. When present, the player renders the
|
|
369
|
+
* real waveform instantly with NO client-side fetch/decode of the audio. Best-effort: a missing or
|
|
370
|
+
* malformed sidecar simply omits this field — it never blocks token minting or fails the mutation.
|
|
371
|
+
*/
|
|
372
|
+
@Field(() => [Float], { nullable: true })
|
|
373
|
+
Peaks?: number[];
|
|
374
|
+
|
|
375
|
+
@Field(() => String, { nullable: true })
|
|
376
|
+
ErrorMessage?: string;
|
|
377
|
+
}
|
|
378
|
+
|
|
315
379
|
@Resolver(MJFile_)
|
|
316
380
|
export class FileResolver extends FileResolverBase {
|
|
381
|
+
/**
|
|
382
|
+
* Resolves the server's public base URL the same way callbacks/magic-link do:
|
|
383
|
+
* `publicUrl` if configured (e.g. an ngrok URL), else `baseUrl:graphqlPort`. Trailing
|
|
384
|
+
* slash stripped so callers can append `/media/...`.
|
|
385
|
+
*/
|
|
386
|
+
private resolvePublicBaseUrl(): string {
|
|
387
|
+
const base = configInfo.publicUrl || `${configInfo.baseUrl}:${configInfo.graphqlPort}${configInfo.graphqlRootPath || ''}`;
|
|
388
|
+
return base.replace(/\/+$/, '');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Mints a short-lived signed media-access token for an `MJ: Files` record and returns the
|
|
393
|
+
* authenticated streaming URL (`<publicBase>/media/<fileId>?token=<token>`). Permission-gated
|
|
394
|
+
* IDENTICALLY to {@link GetFileContents}: the file is loaded under the CALLING USER's context, so
|
|
395
|
+
* MJ row-level security determines access. The returned token is the capability — the streaming
|
|
396
|
+
* route re-verifies it without re-checking row-level access. Never throws to the client.
|
|
397
|
+
*
|
|
398
|
+
* @param fileId The `MJ: Files` id to grant streaming access to.
|
|
399
|
+
* @returns `{ Success, Token?, Url?, ExpiresAt?, ErrorMessage? }`.
|
|
400
|
+
*/
|
|
401
|
+
@Mutation(() => MediaAccessTokenResult)
|
|
402
|
+
async CreateMediaAccessToken(@Arg('fileId', () => String) fileId: string, @Ctx() context: AppContext): Promise<MediaAccessTokenResult> {
|
|
403
|
+
try {
|
|
404
|
+
const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
405
|
+
const contextUser = this.GetUserFromPayload(context.userPayload);
|
|
406
|
+
|
|
407
|
+
// Permission gate: load the file under the USER's context. A failed load means the file
|
|
408
|
+
// does not exist OR the user lacks read access under MJ row-level security.
|
|
409
|
+
const fileEntity = await provider.GetEntityObject<MJFileEntity>('MJ: Files', contextUser);
|
|
410
|
+
const loaded = await fileEntity.Load(fileId);
|
|
411
|
+
if (!loaded) {
|
|
412
|
+
return { Success: false, ErrorMessage: 'You do not have access to this file or it does not exist.' };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Access authorized — mint the capability token.
|
|
416
|
+
const { Token, ExpiresAt } = MediaAccessKeyManager.Instance.Sign(fileId, contextUser.ID);
|
|
417
|
+
const url = `${this.resolvePublicBaseUrl()}/media/${encodeURIComponent(fileId)}?token=${encodeURIComponent(Token)}`;
|
|
418
|
+
|
|
419
|
+
// Best-effort: surface precomputed waveform peaks from a peaks.json sidecar beside the file, so
|
|
420
|
+
// the player renders the real waveform instantly without fetching/decoding the audio. Never
|
|
421
|
+
// blocks token minting — any failure just omits Peaks.
|
|
422
|
+
const peaks = await this.tryReadPeaksSidecar(fileEntity, contextUser);
|
|
423
|
+
|
|
424
|
+
return { Success: true, Token, Url: url, ExpiresAt, MimeType: fileEntity.ContentType ?? undefined, Peaks: peaks };
|
|
425
|
+
} catch (error) {
|
|
426
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
427
|
+
LogError(`CreateMediaAccessToken failed (file ${fileId}): ${message}`);
|
|
428
|
+
return { Success: false, ErrorMessage: message };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Best-effort read of a `peaks.json` waveform sidecar that sits in the SAME storage folder as the
|
|
434
|
+
* given file (a JSON array of normalized `0..1` numbers, written at capture time). Derives the
|
|
435
|
+
* folder from the file's `ProviderKey` (strips the final path segment), reads `<folder>/peaks.json`
|
|
436
|
+
* via the file's own storage driver, parses + validates it, and returns sanitized peaks. Returns
|
|
437
|
+
* `undefined` on ANY failure (no ProviderKey, no sidecar, parse error, garbage) — the caller treats
|
|
438
|
+
* peaks as a pure optimization and must never let a sidecar problem affect token minting.
|
|
439
|
+
*
|
|
440
|
+
* @param file The already-loaded (under the user's context) `MJ: Files` row.
|
|
441
|
+
* @param contextUser The calling user — used to resolve the storage driver.
|
|
442
|
+
* @returns Sanitized `0..1` peaks (length-capped), or `undefined`.
|
|
443
|
+
*/
|
|
444
|
+
private async tryReadPeaksSidecar(file: MJFileEntity, contextUser: UserInfo): Promise<number[] | undefined> {
|
|
445
|
+
try {
|
|
446
|
+
// The sidecar lives next to the recording: replace the final path segment with peaks.json.
|
|
447
|
+
const sidecarPath = deriveSidecarPath(file.ProviderKey);
|
|
448
|
+
if (!sidecarPath) {
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Resolve the file's storage account → driver (mirror of readRealtimeRecordingFile's pattern).
|
|
453
|
+
await FileStorageEngine.Instance.Config(false, contextUser);
|
|
454
|
+
let accounts = FileStorageEngine.Instance.GetAccountsByProviderID(file.ProviderID);
|
|
455
|
+
if (accounts.length === 0) {
|
|
456
|
+
await FileStorageEngine.Instance.Config(true, contextUser);
|
|
457
|
+
accounts = FileStorageEngine.Instance.GetAccountsByProviderID(file.ProviderID);
|
|
458
|
+
}
|
|
459
|
+
const account = accounts[0];
|
|
460
|
+
if (!account) {
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
const driver = await FileStorageEngine.Instance.GetDriver(account.ID, contextUser);
|
|
464
|
+
const bytes = await driver.GetObject({ fullPath: sidecarPath });
|
|
465
|
+
if (!bytes || bytes.length === 0) {
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
return parsePeaksSidecar(bytes);
|
|
469
|
+
} catch {
|
|
470
|
+
// No sidecar / unreadable / parse failure — peaks are optional, never surface the error.
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
317
475
|
/**
|
|
318
476
|
* Builds UserContextOptions for storage operations that may require OAuth authentication.
|
|
319
477
|
* This passes the current user's ID and context to allow the storage utilities to
|
|
@@ -431,6 +589,46 @@ export class FileResolver extends FileResolverBase {
|
|
|
431
589
|
return url;
|
|
432
590
|
}
|
|
433
591
|
|
|
592
|
+
/**
|
|
593
|
+
* Returns an `MJ: Files` record's bytes as base64, read server-side through authenticated MJStorage
|
|
594
|
+
* (`GetObject`) — NOT a public pre-signed link. Permission-gated: the file is first loaded under the
|
|
595
|
+
* calling user's context, so MJ row-level security determines access. Never throws to the client.
|
|
596
|
+
*
|
|
597
|
+
* @param fileId The `MJ: Files` id whose bytes to return.
|
|
598
|
+
* @returns `{ Success, Base64?, MimeType?, ErrorMessage? }`.
|
|
599
|
+
*/
|
|
600
|
+
@Query(() => FileContentsResult)
|
|
601
|
+
async GetFileContents(@Arg('fileId', () => String) fileId: string, @Ctx() context: AppContext): Promise<FileContentsResult> {
|
|
602
|
+
try {
|
|
603
|
+
const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
604
|
+
const contextUser = this.GetUserFromPayload(context.userPayload);
|
|
605
|
+
|
|
606
|
+
// Permission gate FIRST: load the file record under the USER's context. A failed load means the
|
|
607
|
+
// file does not exist OR the user lacks read access under MJ row-level security.
|
|
608
|
+
const fileEntity = await provider.GetEntityObject<MJFileEntity>('MJ: Files', contextUser);
|
|
609
|
+
const loaded = await fileEntity.Load(fileId);
|
|
610
|
+
if (!loaded) {
|
|
611
|
+
return { Success: false, ErrorMessage: 'You do not have access to this file or it does not exist.' };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Read the bytes via authenticated MJStorage (server-side GetObject on the file's own account).
|
|
615
|
+
const result = await readRealtimeRecordingFile(fileId, contextUser, provider);
|
|
616
|
+
if (!result) {
|
|
617
|
+
return { Success: false, ErrorMessage: 'The file could not be read from storage.' };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return {
|
|
621
|
+
Success: true,
|
|
622
|
+
Base64: result.Bytes.toString('base64'),
|
|
623
|
+
MimeType: result.MimeType ?? fileEntity.ContentType ?? undefined,
|
|
624
|
+
};
|
|
625
|
+
} catch (error) {
|
|
626
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
627
|
+
LogError(`GetFileContents failed (file ${fileId}): ${message}`);
|
|
628
|
+
return { Success: false, ErrorMessage: message };
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
434
632
|
@Mutation(() => MJFile_)
|
|
435
633
|
async UpdateFile(@Arg('input', () => UpdateMJFileInput) input: UpdateMJFileInput, @Ctx() context: AppContext, @PubSub() pubSub: PubSubEngine) {
|
|
436
634
|
// if the name is changing, rename the target object as well
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Arg, Ctx, ObjectType, Query, Resolver, Field, Int, InputType } from 'type-graphql';
|
|
2
|
-
import { RunQuery, IRunQueryProvider, IMetadataProvider, RunQueryParams, LogError, RunQueryWithCacheCheckParams, RunQueriesWithCacheCheckResponse, RunQueryWithCacheCheckResult } from '@memberjunction/core';
|
|
2
|
+
import { RunQuery, IRunQueryProvider, IMetadataProvider, RunQueryParams, LogError, RunQueryWithCacheCheckParams, RunQueriesWithCacheCheckResponse, RunQueryWithCacheCheckResult, RunQueryEnrichment } from '@memberjunction/core';
|
|
3
3
|
import { AppContext } from '../types.js';
|
|
4
4
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
5
5
|
import { GraphQLJSONObject } from 'graphql-type-json';
|
|
@@ -40,6 +40,9 @@ export class RunQueryInput {
|
|
|
40
40
|
|
|
41
41
|
@Field(() => String, { nullable: true, description: 'Description to use in audit log' })
|
|
42
42
|
AuditLogDescription?: string;
|
|
43
|
+
|
|
44
|
+
@Field(() => GraphQLJSONObject, { nullable: true, description: 'Optional runtime-only directive to post-process result rows through a registered query result enricher ({ EnricherKey, Config })' })
|
|
45
|
+
Enrichment?: RunQueryEnrichment;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
@ObjectType()
|
|
@@ -191,7 +194,8 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
191
194
|
@Arg('MaxRows', () => Int, {nullable: true}) MaxRows?: number,
|
|
192
195
|
@Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
|
|
193
196
|
@Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
|
|
194
|
-
@Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string
|
|
197
|
+
@Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string,
|
|
198
|
+
@Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment): Promise<RunQueryResultType> {
|
|
195
199
|
// Check API key scope authorization for query execution
|
|
196
200
|
await this.CheckAPIKeyScopeAuthorization('query:run', QueryID, context.userPayload);
|
|
197
201
|
|
|
@@ -200,7 +204,7 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
200
204
|
const rq = new RunQuery(provider as unknown as IRunQueryProvider);
|
|
201
205
|
console.log('GetQueryData called with:', { QueryID, Parameters, MaxRows, StartRow, ForceAuditLog, AuditLogDescription });
|
|
202
206
|
const result = await rq.RunQuery(
|
|
203
|
-
{
|
|
207
|
+
{
|
|
204
208
|
QueryID: QueryID,
|
|
205
209
|
CategoryID: CategoryID,
|
|
206
210
|
CategoryPath: CategoryPath,
|
|
@@ -208,8 +212,9 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
208
212
|
MaxRows: MaxRows,
|
|
209
213
|
StartRow: StartRow,
|
|
210
214
|
ForceAuditLog: ForceAuditLog,
|
|
211
|
-
AuditLogDescription: AuditLogDescription
|
|
212
|
-
|
|
215
|
+
AuditLogDescription: AuditLogDescription,
|
|
216
|
+
Enrichment: Enrichment
|
|
217
|
+
},
|
|
213
218
|
context.userPayload.userRecord);
|
|
214
219
|
console.log('RunQuery result:', {
|
|
215
220
|
Success: result.Success,
|
|
@@ -256,22 +261,24 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
256
261
|
@Arg('MaxRows', () => Int, {nullable: true}) MaxRows?: number,
|
|
257
262
|
@Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
|
|
258
263
|
@Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
|
|
259
|
-
@Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string
|
|
264
|
+
@Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string,
|
|
265
|
+
@Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment): Promise<RunQueryResultType> {
|
|
260
266
|
// Check API key scope authorization for query execution
|
|
261
267
|
await this.CheckAPIKeyScopeAuthorization('query:run', QueryName, context.userPayload);
|
|
262
268
|
|
|
263
269
|
const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
|
|
264
270
|
const rq = new RunQuery(provider as unknown as IRunQueryProvider);
|
|
265
271
|
const result = await rq.RunQuery(
|
|
266
|
-
{
|
|
267
|
-
QueryName: QueryName,
|
|
272
|
+
{
|
|
273
|
+
QueryName: QueryName,
|
|
268
274
|
CategoryID: CategoryID,
|
|
269
275
|
CategoryPath: CategoryPath,
|
|
270
276
|
Parameters: Parameters,
|
|
271
277
|
MaxRows: MaxRows,
|
|
272
278
|
StartRow: StartRow,
|
|
273
279
|
ForceAuditLog: ForceAuditLog,
|
|
274
|
-
AuditLogDescription: AuditLogDescription
|
|
280
|
+
AuditLogDescription: AuditLogDescription,
|
|
281
|
+
Enrichment: Enrichment
|
|
275
282
|
},
|
|
276
283
|
context.userPayload.userRecord);
|
|
277
284
|
|
|
@@ -302,13 +309,14 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
302
309
|
@Arg('MaxRows', () => Int, {nullable: true}) MaxRows?: number,
|
|
303
310
|
@Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
|
|
304
311
|
@Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
|
|
305
|
-
@Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string
|
|
312
|
+
@Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string,
|
|
313
|
+
@Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment): Promise<RunQueryResultType> {
|
|
306
314
|
const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
|
|
307
315
|
const md = provider as unknown as IMetadataProvider;
|
|
308
316
|
const rq = new RunQuery(provider as unknown as IRunQueryProvider);
|
|
309
317
|
|
|
310
318
|
const result = await rq.RunQuery(
|
|
311
|
-
{
|
|
319
|
+
{
|
|
312
320
|
QueryID: QueryID,
|
|
313
321
|
CategoryID: CategoryID,
|
|
314
322
|
CategoryPath: CategoryPath,
|
|
@@ -316,8 +324,9 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
316
324
|
MaxRows: MaxRows,
|
|
317
325
|
StartRow: StartRow,
|
|
318
326
|
ForceAuditLog: ForceAuditLog,
|
|
319
|
-
AuditLogDescription: AuditLogDescription
|
|
320
|
-
|
|
327
|
+
AuditLogDescription: AuditLogDescription,
|
|
328
|
+
Enrichment: Enrichment
|
|
329
|
+
},
|
|
321
330
|
context.userPayload.userRecord);
|
|
322
331
|
|
|
323
332
|
// If QueryName is not populated by the provider, use efficient lookup
|
|
@@ -360,12 +369,13 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
360
369
|
@Arg('MaxRows', () => Int, {nullable: true}) MaxRows?: number,
|
|
361
370
|
@Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
|
|
362
371
|
@Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
|
|
363
|
-
@Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string
|
|
372
|
+
@Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string,
|
|
373
|
+
@Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment): Promise<RunQueryResultType> {
|
|
364
374
|
const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
|
|
365
375
|
const rq = new RunQuery(provider as unknown as IRunQueryProvider);
|
|
366
376
|
|
|
367
377
|
const result = await rq.RunQuery(
|
|
368
|
-
{
|
|
378
|
+
{
|
|
369
379
|
QueryName: QueryName,
|
|
370
380
|
CategoryID: CategoryID,
|
|
371
381
|
CategoryPath: CategoryPath,
|
|
@@ -373,8 +383,9 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
373
383
|
MaxRows: MaxRows,
|
|
374
384
|
StartRow: StartRow,
|
|
375
385
|
ForceAuditLog: ForceAuditLog,
|
|
376
|
-
AuditLogDescription: AuditLogDescription
|
|
377
|
-
|
|
386
|
+
AuditLogDescription: AuditLogDescription,
|
|
387
|
+
Enrichment: Enrichment
|
|
388
|
+
},
|
|
378
389
|
context.userPayload.userRecord);
|
|
379
390
|
|
|
380
391
|
return {
|
|
@@ -420,7 +431,8 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
420
431
|
MaxRows: i.MaxRows,
|
|
421
432
|
StartRow: i.StartRow,
|
|
422
433
|
ForceAuditLog: i.ForceAuditLog,
|
|
423
|
-
AuditLogDescription: i.AuditLogDescription
|
|
434
|
+
AuditLogDescription: i.AuditLogDescription,
|
|
435
|
+
Enrichment: i.Enrichment
|
|
424
436
|
}));
|
|
425
437
|
|
|
426
438
|
// Execute all queries in parallel using the batch method
|
|
@@ -469,7 +481,8 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
469
481
|
MaxRows: i.MaxRows,
|
|
470
482
|
StartRow: i.StartRow,
|
|
471
483
|
ForceAuditLog: i.ForceAuditLog,
|
|
472
|
-
AuditLogDescription: i.AuditLogDescription
|
|
484
|
+
AuditLogDescription: i.AuditLogDescription,
|
|
485
|
+
Enrichment: i.Enrichment
|
|
473
486
|
}));
|
|
474
487
|
|
|
475
488
|
// Execute all queries in parallel using the batch method
|
|
@@ -532,6 +545,7 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
532
545
|
StartRow: item.params.StartRow,
|
|
533
546
|
ForceAuditLog: item.params.ForceAuditLog,
|
|
534
547
|
AuditLogDescription: item.params.AuditLogDescription,
|
|
548
|
+
Enrichment: item.params.Enrichment,
|
|
535
549
|
},
|
|
536
550
|
cacheStatus: item.cacheStatus ? {
|
|
537
551
|
maxUpdatedAt: item.cacheStatus.maxUpdatedAt,
|