@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,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Meeting-Room recording registration — the server-side path that closes the LiveKit
|
|
3
|
+
* meeting-room recording loop. After a room's composite egress completes, this module:
|
|
4
|
+
*
|
|
5
|
+
* 1. Resolves the **Meeting-Room Conversation** for the room (prefer by `EgressID`, fall back to the
|
|
6
|
+
* room name → `ExternalID` + `Type='Meeting Room'`, else create one via the sink's pattern).
|
|
7
|
+
* 2. Resolves the **meeting-recording storage account** — a configured `MJStorage` account that points
|
|
8
|
+
* at the same sink LiveKit egress wrote the MP4 to, so MJ can read/stream it.
|
|
9
|
+
* 3. Creates the `MJ: Files` row for the recording (v1 points DIRECTLY at the egress output — no byte
|
|
10
|
+
* copy — so playback streams straight from the sink). An OPTIONAL copy-to-canonical path (OFF by
|
|
11
|
+
* default, behind config) reads the bytes and re-uploads into a separate canonical provider.
|
|
12
|
+
* 4. Stamps `Conversation.RecordingFileID` (+ `EgressID`) and saves.
|
|
13
|
+
*
|
|
14
|
+
* Lives in `@memberjunction/server` (the one package that depends on the egress service, MJStorage, and
|
|
15
|
+
* core-entities). It is invoked from `RealtimeBridgeResolver.StopLiveKitRecording`. Everything is
|
|
16
|
+
* best-effort: any failure returns a non-throwing failure result and logs — a recording that fails to
|
|
17
|
+
* register never crashes the stop-recording mutation.
|
|
18
|
+
*
|
|
19
|
+
* ## Required configuration
|
|
20
|
+
*
|
|
21
|
+
* The egress sink and the MJStorage account must point at the **same** bucket/container so MJ can read the
|
|
22
|
+
* file LiveKit wrote. Configure the **provider** whose accounts target that sink via either:
|
|
23
|
+
*
|
|
24
|
+
* - env: `MJ_MEETING_RECORDING_STORAGE_PROVIDER=<MJ: File Storage Providers ID>`
|
|
25
|
+
* - config (`mj.config.cjs`): `meetingRecordingStorageProviderID: '<provider id>'`
|
|
26
|
+
*
|
|
27
|
+
* When unset, registration returns a clear, non-throwing failure explaining the config is required (the
|
|
28
|
+
* recording still stopped; it just isn't registered as a `MJ: Files` row).
|
|
29
|
+
*
|
|
30
|
+
* ### Optional copy-to-canonical ("copy into Box")
|
|
31
|
+
*
|
|
32
|
+
* To copy the MP4 OUT of the egress sink into a separate canonical provider (e.g. Box) instead of pointing
|
|
33
|
+
* the Files row at the sink, set a DIFFERENT provider via:
|
|
34
|
+
*
|
|
35
|
+
* - env: `MJ_MEETING_RECORDING_CANONICAL_STORAGE_PROVIDER=<provider id>`
|
|
36
|
+
* - config: `meetingRecordingCanonicalStorageProviderID: '<provider id>'`
|
|
37
|
+
*
|
|
38
|
+
* When set and different from the sink provider, the bytes are read from the sink and uploaded into the
|
|
39
|
+
* canonical provider, and the Files row points there. OFF by default.
|
|
40
|
+
*
|
|
41
|
+
* @module @memberjunction/server
|
|
42
|
+
* @author MemberJunction.com
|
|
43
|
+
*/
|
|
44
|
+
import { RunView, LogError, LogStatus } from '@memberjunction/core';
|
|
45
|
+
import { FileStorageEngine } from '@memberjunction/storage';
|
|
46
|
+
const CONVERSATION_ENTITY = 'MJ: Conversations';
|
|
47
|
+
const FILES_ENTITY = 'MJ: Files';
|
|
48
|
+
const MEETING_ROOM_CONVERSATION_TYPE = 'Meeting Room';
|
|
49
|
+
/**
|
|
50
|
+
* Registers a completed meeting recording: resolves the Meeting-Room Conversation, creates the `MJ: Files`
|
|
51
|
+
* row for the egress MP4, and stamps `Conversation.RecordingFileID` (+ `EgressID`). Best-effort — never
|
|
52
|
+
* throws; returns a failure result with a clear message instead.
|
|
53
|
+
*
|
|
54
|
+
* @param egress The completed egress result (must carry an `OutputLocation`).
|
|
55
|
+
* @param contextUser The authenticated user (for DB writes + credential access).
|
|
56
|
+
* @param provider The metadata provider servicing this request.
|
|
57
|
+
* @param config Optional config overrides (otherwise read from env).
|
|
58
|
+
*/
|
|
59
|
+
export async function registerMeetingRecordingFile(egress, contextUser, provider, config) {
|
|
60
|
+
try {
|
|
61
|
+
if (!egress.OutputLocation) {
|
|
62
|
+
return {
|
|
63
|
+
Success: false,
|
|
64
|
+
ErrorMessage: 'The recording has no output yet (egress still in progress) — nothing to register. ' + 'Register only after the egress has stopped/completed.',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const sinkProviderID = config?.sinkStorageProviderID ?? process.env.MJ_MEETING_RECORDING_STORAGE_PROVIDER;
|
|
68
|
+
if (!sinkProviderID) {
|
|
69
|
+
return {
|
|
70
|
+
Success: false,
|
|
71
|
+
ErrorMessage: 'No meeting-recording storage provider is configured. Set MJ_MEETING_RECORDING_STORAGE_PROVIDER ' +
|
|
72
|
+
'(or meetingRecordingStorageProviderID in mj.config.cjs) to the MJ: File Storage Providers id ' +
|
|
73
|
+
'whose accounts target the same sink LiveKit egress writes to.',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
await FileStorageEngine.Instance.Config(false, contextUser, provider);
|
|
77
|
+
const sinkAccountID = resolveStorageAccountForProvider(sinkProviderID);
|
|
78
|
+
if (!sinkAccountID) {
|
|
79
|
+
return {
|
|
80
|
+
Success: false,
|
|
81
|
+
ErrorMessage: `No active MJStorage account is linked to the configured meeting-recording provider '${sinkProviderID}'. ` +
|
|
82
|
+
'Create a File Storage Account for that provider pointing at the egress sink bucket/container.',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const conversationID = await resolveMeetingConversation(egress, contextUser, provider);
|
|
86
|
+
if (!conversationID) {
|
|
87
|
+
return { Success: false, ErrorMessage: 'Could not resolve or create the Meeting-Room Conversation for the recording.' };
|
|
88
|
+
}
|
|
89
|
+
// v1: point the Files row directly at the egress output in the sink account (no byte copy). The
|
|
90
|
+
// OPTIONAL copy-to-canonical path overrides both the provider+key when a differing canonical
|
|
91
|
+
// provider is configured.
|
|
92
|
+
const fileLocation = await resolveFileLocation(egress, sinkProviderID, sinkAccountID, contextUser, config);
|
|
93
|
+
if (!fileLocation.Success) {
|
|
94
|
+
return { Success: false, ErrorMessage: fileLocation.ErrorMessage, ConversationID: conversationID };
|
|
95
|
+
}
|
|
96
|
+
const fileID = await createRecordingFileRow(egress, fileLocation.ProviderID, fileLocation.ProviderKey, contextUser, provider);
|
|
97
|
+
if (!fileID) {
|
|
98
|
+
return { Success: false, ErrorMessage: 'Failed to create the MJ: Files row for the recording.', ConversationID: conversationID };
|
|
99
|
+
}
|
|
100
|
+
const stamped = await stampConversationRecording(conversationID, fileID, egress.EgressID, contextUser, provider);
|
|
101
|
+
if (!stamped) {
|
|
102
|
+
return {
|
|
103
|
+
Success: false,
|
|
104
|
+
ErrorMessage: 'Created the recording file but failed to stamp it onto the Conversation.',
|
|
105
|
+
ConversationID: conversationID,
|
|
106
|
+
RecordingFileID: fileID,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
LogStatus(`registerMeetingRecordingFile: registered recording ${fileID} for room '${egress.RoomName}' on conversation ${conversationID}.`);
|
|
110
|
+
return { Success: true, RecordingFileID: fileID, ConversationID: conversationID };
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
114
|
+
LogError(`registerMeetingRecordingFile failed: ${msg}`);
|
|
115
|
+
return { Success: false, ErrorMessage: msg };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Best-effort correlation when a recording STARTS: stamps `Conversation.EgressID` onto the room's
|
|
120
|
+
* Meeting-Room Conversation (if it already exists) so a live recording is tracked against the room. If the
|
|
121
|
+
* conversation doesn't exist yet, this resolves to `false` silently — the stop-flow will resolve/create it.
|
|
122
|
+
* Never throws.
|
|
123
|
+
*
|
|
124
|
+
* @param roomName The LiveKit room name (= the Conversation's `ExternalID`).
|
|
125
|
+
* @param egressID The egress id that just started.
|
|
126
|
+
* @param contextUser The authenticated user.
|
|
127
|
+
* @param provider The metadata provider.
|
|
128
|
+
* @returns `true` when an existing conversation was stamped, else `false`.
|
|
129
|
+
*/
|
|
130
|
+
export async function correlateRecordingStart(roomName, egressID, contextUser, provider) {
|
|
131
|
+
try {
|
|
132
|
+
const conversationID = await findConversationByRoomName(roomName, contextUser, provider);
|
|
133
|
+
if (!conversationID) {
|
|
134
|
+
return false; // not created yet — stop-flow resolves/creates it
|
|
135
|
+
}
|
|
136
|
+
const conversation = await provider.GetEntityObject(CONVERSATION_ENTITY, contextUser);
|
|
137
|
+
if (!(await conversation.Load(conversationID))) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
conversation.EgressID = egressID;
|
|
141
|
+
if (await conversation.Save()) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
LogError(`correlateRecordingStart: failed to stamp EgressID: ${conversation.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
// Never fail the recording start on correlation.
|
|
149
|
+
LogError(`correlateRecordingStart (best-effort) failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Resolves the Meeting-Room Conversation for a completed recording: prefer matching by `EgressID` (set on
|
|
155
|
+
* start), fall back to the room name (`ExternalID` + `Type`), else create one following the transcript
|
|
156
|
+
* sink's pattern (scoped `Application` so it stays out of the normal chat list).
|
|
157
|
+
*/
|
|
158
|
+
export async function resolveMeetingConversation(egress, contextUser, provider) {
|
|
159
|
+
const byEgress = await findConversationByEgressID(egress.EgressID, contextUser, provider);
|
|
160
|
+
if (byEgress) {
|
|
161
|
+
return byEgress;
|
|
162
|
+
}
|
|
163
|
+
const byRoom = await findConversationByRoomName(egress.RoomName, contextUser, provider);
|
|
164
|
+
if (byRoom) {
|
|
165
|
+
return byRoom;
|
|
166
|
+
}
|
|
167
|
+
return createMeetingConversation(egress.RoomName, contextUser, provider);
|
|
168
|
+
}
|
|
169
|
+
/** Finds a non-archived Meeting-Room Conversation by its `EgressID`. */
|
|
170
|
+
async function findConversationByEgressID(egressID, contextUser, provider) {
|
|
171
|
+
if (!egressID) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const rv = RunView.FromMetadataProvider(provider);
|
|
175
|
+
const found = await rv.RunView({
|
|
176
|
+
EntityName: CONVERSATION_ENTITY,
|
|
177
|
+
ExtraFilter: `EgressID='${escapeSql(egressID)}' AND Type='${escapeSql(MEETING_ROOM_CONVERSATION_TYPE)}' AND (IsArchived IS NULL OR IsArchived=0)`,
|
|
178
|
+
Fields: ['ID'],
|
|
179
|
+
OrderBy: '__mj_CreatedAt DESC',
|
|
180
|
+
MaxRows: 1,
|
|
181
|
+
ResultType: 'simple',
|
|
182
|
+
}, contextUser);
|
|
183
|
+
return found.Success && found.Results.length > 0 ? found.Results[0].ID : null;
|
|
184
|
+
}
|
|
185
|
+
/** Finds a non-archived Meeting-Room Conversation by room name (`ExternalID` + `Type`). */
|
|
186
|
+
async function findConversationByRoomName(roomName, contextUser, provider) {
|
|
187
|
+
if (!roomName) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const rv = RunView.FromMetadataProvider(provider);
|
|
191
|
+
const found = await rv.RunView({
|
|
192
|
+
EntityName: CONVERSATION_ENTITY,
|
|
193
|
+
ExtraFilter: `ExternalID='${escapeSql(roomName)}' AND Type='${escapeSql(MEETING_ROOM_CONVERSATION_TYPE)}' AND (IsArchived IS NULL OR IsArchived=0)`,
|
|
194
|
+
Fields: ['ID'],
|
|
195
|
+
OrderBy: '__mj_CreatedAt DESC',
|
|
196
|
+
MaxRows: 1,
|
|
197
|
+
ResultType: 'simple',
|
|
198
|
+
}, contextUser);
|
|
199
|
+
return found.Success && found.Results.length > 0 ? found.Results[0].ID : null;
|
|
200
|
+
}
|
|
201
|
+
/** Creates a Meeting-Room Conversation for a room (scoped `Application`, keyed by room name) — mirrors the transcript sink. */
|
|
202
|
+
async function createMeetingConversation(roomName, contextUser, provider) {
|
|
203
|
+
const conversation = await provider.GetEntityObject(CONVERSATION_ENTITY, contextUser);
|
|
204
|
+
conversation.NewRecord();
|
|
205
|
+
conversation.UserID = contextUser.ID;
|
|
206
|
+
conversation.Name = `Meeting Room ${roomName}`;
|
|
207
|
+
conversation.Type = MEETING_ROOM_CONVERSATION_TYPE;
|
|
208
|
+
conversation.ExternalID = roomName;
|
|
209
|
+
conversation.ApplicationScope = 'Application';
|
|
210
|
+
if (await conversation.Save()) {
|
|
211
|
+
return conversation.ID;
|
|
212
|
+
}
|
|
213
|
+
LogError(`registerMeetingRecordingFile: failed to create Meeting-Room conversation for room '${roomName}': ${conversation.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Resolves the `(ProviderID, ProviderKey)` the Files row should point at. v1 default: the egress output in
|
|
218
|
+
* the sink provider (no copy). When a DIFFERING canonical provider is configured, copies the bytes into it
|
|
219
|
+
* and points there instead.
|
|
220
|
+
*/
|
|
221
|
+
async function resolveFileLocation(egress, sinkProviderID, sinkAccountID, contextUser, config) {
|
|
222
|
+
const outputLocation = egress.OutputLocation;
|
|
223
|
+
const canonicalProviderID = config?.canonicalStorageProviderID ?? process.env.MJ_MEETING_RECORDING_CANONICAL_STORAGE_PROVIDER;
|
|
224
|
+
// Default (v1): point directly at the sink output — playback streams straight from it, no byte copy.
|
|
225
|
+
if (!canonicalProviderID || canonicalProviderID === sinkProviderID) {
|
|
226
|
+
return { Success: true, ProviderID: sinkProviderID, ProviderKey: outputLocation };
|
|
227
|
+
}
|
|
228
|
+
// OPTIONAL "copy into Box": a separate canonical provider IS configured and differs from the sink —
|
|
229
|
+
// read the bytes from the sink and upload them into the canonical provider, then point there.
|
|
230
|
+
const canonicalKey = await copyEgressOutputToCanonical(outputLocation, sinkAccountID, canonicalProviderID, contextUser);
|
|
231
|
+
if (!canonicalKey.Success) {
|
|
232
|
+
return { Success: false, ErrorMessage: canonicalKey.ErrorMessage, ProviderID: '', ProviderKey: '' };
|
|
233
|
+
}
|
|
234
|
+
return { Success: true, ProviderID: canonicalProviderID, ProviderKey: canonicalKey.ProviderKey };
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* OPTIONAL — copies the egress MP4 OUT of the sink account into a configured canonical provider (the "copy
|
|
238
|
+
* into Box" option). Reads the bytes via the sink driver's `GetObject({ fullPath })` and re-uploads them
|
|
239
|
+
* into the canonical provider via `FileStorageEngine.UploadFile`. Returns the canonical `ProviderKey`.
|
|
240
|
+
*/
|
|
241
|
+
export async function copyEgressOutputToCanonical(outputLocation, sinkAccountID, canonicalProviderID, contextUser) {
|
|
242
|
+
const engine = FileStorageEngine.Instance;
|
|
243
|
+
const canonicalAccountID = resolveStorageAccountForProvider(canonicalProviderID);
|
|
244
|
+
if (!canonicalAccountID) {
|
|
245
|
+
return {
|
|
246
|
+
Success: false,
|
|
247
|
+
ErrorMessage: `No active MJStorage account is linked to the configured canonical provider '${canonicalProviderID}'.`,
|
|
248
|
+
ProviderKey: '',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const sinkDriver = await engine.GetDriver(sinkAccountID, contextUser);
|
|
252
|
+
const bytes = await sinkDriver.GetObject({ fullPath: outputLocation });
|
|
253
|
+
const fileName = outputLocation.split('/').pop() || `recording-${Date.now()}.mp4`;
|
|
254
|
+
const uploaded = await engine.UploadFile({
|
|
255
|
+
content: bytes,
|
|
256
|
+
fileName,
|
|
257
|
+
mimeType: 'video/mp4',
|
|
258
|
+
contextUser,
|
|
259
|
+
storageAccountId: canonicalAccountID,
|
|
260
|
+
pathPrefix: `meeting-recordings/${new Date().toISOString().slice(0, 10)}`,
|
|
261
|
+
});
|
|
262
|
+
return { Success: true, ProviderKey: uploaded.StoragePath };
|
|
263
|
+
}
|
|
264
|
+
/** Creates the `MJ: Files` row pointing at the recording (no byte copy in the default v1 path). */
|
|
265
|
+
async function createRecordingFileRow(egress, providerID, providerKey, contextUser, provider) {
|
|
266
|
+
const file = await provider.GetEntityObject(FILES_ENTITY, contextUser);
|
|
267
|
+
file.NewRecord();
|
|
268
|
+
file.Name = `Meeting Recording — ${egress.RoomName} — ${new Date().toISOString().slice(0, 10)}`;
|
|
269
|
+
file.ContentType = 'video/mp4';
|
|
270
|
+
file.ProviderID = providerID;
|
|
271
|
+
file.ProviderKey = providerKey;
|
|
272
|
+
file.Status = 'Uploaded';
|
|
273
|
+
// NOTE: `MJ: Files` has no size column, so `egress.OutputSizeBytes` is not persisted here (it is logged
|
|
274
|
+
// and used by the player's range-streaming on demand). If a size column is added, set it here.
|
|
275
|
+
if (await file.Save()) {
|
|
276
|
+
return file.ID;
|
|
277
|
+
}
|
|
278
|
+
LogError(`registerMeetingRecordingFile: failed to save MJ: Files row: ${file.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
/** Loads the Conversation and stamps `RecordingFileID` (+ `EgressID` if unset), then saves. */
|
|
282
|
+
async function stampConversationRecording(conversationID, fileID, egressID, contextUser, provider) {
|
|
283
|
+
const conversation = await provider.GetEntityObject(CONVERSATION_ENTITY, contextUser);
|
|
284
|
+
if (!(await conversation.Load(conversationID))) {
|
|
285
|
+
LogError(`registerMeetingRecordingFile: could not load conversation ${conversationID} to stamp the recording.`);
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
conversation.RecordingFileID = fileID;
|
|
289
|
+
if (!conversation.EgressID) {
|
|
290
|
+
conversation.EgressID = egressID;
|
|
291
|
+
}
|
|
292
|
+
if (await conversation.Save()) {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
LogError(`registerMeetingRecordingFile: failed to stamp the Conversation: ${conversation.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Resolves an MJStorage account linked to a provider id (the first one). `IsActive` lives on the
|
|
300
|
+
* *provider*, not the account, so provider-level inactivity is handled upstream by the engine's driver
|
|
301
|
+
* cache; here we just pick the provider's account.
|
|
302
|
+
*/
|
|
303
|
+
function resolveStorageAccountForProvider(providerID) {
|
|
304
|
+
const accounts = FileStorageEngine.Instance.GetAccountsByProviderID(providerID);
|
|
305
|
+
return accounts.length > 0 ? accounts[0].ID : null;
|
|
306
|
+
}
|
|
307
|
+
/** Escapes single quotes for safe embedding in an `ExtraFilter` literal. */
|
|
308
|
+
function escapeSql(value) {
|
|
309
|
+
return value.replace(/'/g, "''");
|
|
310
|
+
}
|
|
311
|
+
//# sourceMappingURL=meetingRecordingRegistration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meetingRecordingRegistration.js","sourceRoot":"","sources":["../../src/resolvers/meetingRecordingRegistration.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure helpers for the `peaks.json` waveform sidecar that rides alongside a stored
|
|
3
|
+
* recording. Kept free of type-graphql / resolver / DB dependencies so they can be unit-tested in
|
|
4
|
+
* isolation (the resolver class itself can't be imported under vitest without standing up the full
|
|
5
|
+
* decorated schema). {@link FileResolver.tryReadPeaksSidecar} composes these with a storage driver.
|
|
6
|
+
*
|
|
7
|
+
* @module @memberjunction/server/resolvers/peaksSidecar
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Derives the `peaks.json` sidecar path that sits in the SAME folder as a file's `ProviderKey`:
|
|
11
|
+
* the final path segment of `providerKey` is replaced with `peaks.json`. Returns `null` when
|
|
12
|
+
* `providerKey` is empty (no folder to derive from).
|
|
13
|
+
*
|
|
14
|
+
* @example deriveSidecarPath('realtime-recordings/sess-1/recording.wav') === 'realtime-recordings/sess-1/peaks.json'
|
|
15
|
+
* @example deriveSidecarPath('recording.wav') === 'peaks.json'
|
|
16
|
+
*/
|
|
17
|
+
export declare function deriveSidecarPath(providerKey: string | null | undefined): string | null;
|
|
18
|
+
/** Generous ceiling on the number of peaks returned — matches the capture-side cap. */
|
|
19
|
+
export declare const MAX_PEAKS = 4096;
|
|
20
|
+
/**
|
|
21
|
+
* Parses + sanitizes a `peaks.json` payload (UTF-8 bytes). The content must be a JSON array of
|
|
22
|
+
* finite numbers; each value is clamped to `[0, 1]` and the array is capped at {@link MAX_PEAKS}
|
|
23
|
+
* entries so a malformed/hostile sidecar can't bloat the response. Returns `undefined` for anything
|
|
24
|
+
* that isn't a non-empty array of finite numbers, and NEVER throws (a parse error → `undefined`).
|
|
25
|
+
*
|
|
26
|
+
* @param bytes The raw sidecar bytes (e.g. from a storage `GetObject`).
|
|
27
|
+
* @returns Sanitized `0..1` peaks, or `undefined`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parsePeaksSidecar(bytes: Buffer): number[] | undefined;
|
|
30
|
+
//# sourceMappingURL=peaksSidecar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peaksSidecar.d.ts","sourceRoot":"","sources":["../../src/resolvers/peaksSidecar.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAMvF;AAED,uFAAuF;AACvF,eAAO,MAAM,SAAS,OAAO,CAAC;AAE9B;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAcrE"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure helpers for the `peaks.json` waveform sidecar that rides alongside a stored
|
|
3
|
+
* recording. Kept free of type-graphql / resolver / DB dependencies so they can be unit-tested in
|
|
4
|
+
* isolation (the resolver class itself can't be imported under vitest without standing up the full
|
|
5
|
+
* decorated schema). {@link FileResolver.tryReadPeaksSidecar} composes these with a storage driver.
|
|
6
|
+
*
|
|
7
|
+
* @module @memberjunction/server/resolvers/peaksSidecar
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Derives the `peaks.json` sidecar path that sits in the SAME folder as a file's `ProviderKey`:
|
|
11
|
+
* the final path segment of `providerKey` is replaced with `peaks.json`. Returns `null` when
|
|
12
|
+
* `providerKey` is empty (no folder to derive from).
|
|
13
|
+
*
|
|
14
|
+
* @example deriveSidecarPath('realtime-recordings/sess-1/recording.wav') === 'realtime-recordings/sess-1/peaks.json'
|
|
15
|
+
* @example deriveSidecarPath('recording.wav') === 'peaks.json'
|
|
16
|
+
*/
|
|
17
|
+
export function deriveSidecarPath(providerKey) {
|
|
18
|
+
if (!providerKey) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const lastSlash = providerKey.lastIndexOf('/');
|
|
22
|
+
return lastSlash >= 0 ? `${providerKey.slice(0, lastSlash)}/peaks.json` : 'peaks.json';
|
|
23
|
+
}
|
|
24
|
+
/** Generous ceiling on the number of peaks returned — matches the capture-side cap. */
|
|
25
|
+
export const MAX_PEAKS = 4096;
|
|
26
|
+
/**
|
|
27
|
+
* Parses + sanitizes a `peaks.json` payload (UTF-8 bytes). The content must be a JSON array of
|
|
28
|
+
* finite numbers; each value is clamped to `[0, 1]` and the array is capped at {@link MAX_PEAKS}
|
|
29
|
+
* entries so a malformed/hostile sidecar can't bloat the response. Returns `undefined` for anything
|
|
30
|
+
* that isn't a non-empty array of finite numbers, and NEVER throws (a parse error → `undefined`).
|
|
31
|
+
*
|
|
32
|
+
* @param bytes The raw sidecar bytes (e.g. from a storage `GetObject`).
|
|
33
|
+
* @returns Sanitized `0..1` peaks, or `undefined`.
|
|
34
|
+
*/
|
|
35
|
+
export function parsePeaksSidecar(bytes) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(bytes.toString('utf8'));
|
|
38
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const cleaned = parsed
|
|
42
|
+
.slice(0, MAX_PEAKS)
|
|
43
|
+
.filter((v) => typeof v === 'number' && Number.isFinite(v))
|
|
44
|
+
.map((v) => (v < 0 ? 0 : v > 1 ? 1 : v));
|
|
45
|
+
return cleaned.length > 0 ? cleaned : undefined;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=peaksSidecar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peaksSidecar.js","sourceRoot":"","sources":["../../src/resolvers/peaksSidecar.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,WAAsC;IACpE,IAAI,CAAC,WAAW,EAAE,CAAC;QACf,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,MAAM,SAAS,GAAG,WAAW,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAC/C,OAAO,SAAS,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,YAAY,CAAC;AAC3F,CAAC;AAED,uFAAuF;AACvF,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC;AAE9B;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC3C,IAAI,CAAC;QACD,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QAC3D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAChD,OAAO,SAAS,CAAC;QACrB,CAAC;QACD,MAAM,OAAO,GAAG,MAAM;aACjB,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC;aACnB,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;aACvE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,SAAS,CAAC;IACrB,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Short-lived HS256 token manager for authenticated media streaming.
|
|
3
|
+
*
|
|
4
|
+
* Media-access tokens are minted by the `CreateMediaAccessToken` GraphQL mutation
|
|
5
|
+
* (after a per-user permission check on the `MJ: Files` row) and verified by the
|
|
6
|
+
* `/media/:fileId` streaming route. The token IS the capability: access is checked
|
|
7
|
+
* once at mint time, and the route trusts a valid, unexpired, file-matching token.
|
|
8
|
+
*
|
|
9
|
+
* This is intentionally a SEPARATE, self-contained mechanism from the magic-link
|
|
10
|
+
* RS256 manager: media streaming must work on EVERY deployment, whereas the
|
|
11
|
+
* magic-link keypair is only initialized when `magicLink.enabled` is configured.
|
|
12
|
+
* We sign HS256 with the deployment's symmetric secret (`MJ_BASE_ENCRYPTION_KEY`),
|
|
13
|
+
* falling back to an ephemeral per-process secret (with a warning) when unset —
|
|
14
|
+
* the same degradation MagicLinkKeyManager uses for its ephemeral keypair.
|
|
15
|
+
*
|
|
16
|
+
* @module @memberjunction/server/rest/MediaAccessKeys
|
|
17
|
+
*/
|
|
18
|
+
import { BaseSingleton } from '@memberjunction/global';
|
|
19
|
+
/** The token's `typ` claim — the route rejects any token whose `typ` differs. */
|
|
20
|
+
export declare const MEDIA_ACCESS_TOKEN_TYPE = "media-access";
|
|
21
|
+
/** Default media-access token lifetime (hours). Long enough to cover a full playback session. */
|
|
22
|
+
export declare const MEDIA_ACCESS_DEFAULT_TTL_HOURS = 4;
|
|
23
|
+
/** The claims carried by a media-access token. */
|
|
24
|
+
export interface MediaAccessTokenClaims {
|
|
25
|
+
/** The `MJ: Files` id this token grants streaming access to. */
|
|
26
|
+
fileId: string;
|
|
27
|
+
/** The id of the user the token was minted for (audit/forensics only — access was already checked). */
|
|
28
|
+
userId: string;
|
|
29
|
+
/** Discriminator — always {@link MEDIA_ACCESS_TOKEN_TYPE}. */
|
|
30
|
+
typ: typeof MEDIA_ACCESS_TOKEN_TYPE;
|
|
31
|
+
}
|
|
32
|
+
/** Outcome of verifying a media-access token. */
|
|
33
|
+
export type MediaTokenVerifyResult = {
|
|
34
|
+
Valid: true;
|
|
35
|
+
Claims: MediaAccessTokenClaims;
|
|
36
|
+
} | {
|
|
37
|
+
Valid: false;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Signs and verifies short-lived HS256 media-access tokens. Singleton so the
|
|
41
|
+
* signing secret is stable for the process lifetime. The secret is resolved
|
|
42
|
+
* lazily on first use from `MJ_BASE_ENCRYPTION_KEY`; if unset, an ephemeral
|
|
43
|
+
* 32-byte secret is generated (outstanding tokens die on restart — acceptable
|
|
44
|
+
* for a 4h-TTL streaming credential).
|
|
45
|
+
*/
|
|
46
|
+
export declare class MediaAccessKeyManager extends BaseSingleton<MediaAccessKeyManager> {
|
|
47
|
+
private secret;
|
|
48
|
+
private _initialized;
|
|
49
|
+
constructor();
|
|
50
|
+
static get Instance(): MediaAccessKeyManager;
|
|
51
|
+
/** Resolves the signing secret once, preferring the deployment's symmetric key. */
|
|
52
|
+
private ensureSecret;
|
|
53
|
+
/**
|
|
54
|
+
* Mints a signed media-access token for a file + user, expiring after `ttlHours`.
|
|
55
|
+
*
|
|
56
|
+
* @returns `{ Token, ExpiresAt }` — the compact JWT and its absolute expiry.
|
|
57
|
+
*/
|
|
58
|
+
Sign(fileId: string, userId: string, ttlHours?: number): {
|
|
59
|
+
Token: string;
|
|
60
|
+
ExpiresAt: Date;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Verifies a token's signature, expiry, and `typ`. Returns the typed claims on
|
|
64
|
+
* success. Never throws — an invalid/expired/malformed token returns `{ Valid: false }`.
|
|
65
|
+
*/
|
|
66
|
+
Verify(token: string): MediaTokenVerifyResult;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=MediaAccessKeys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MediaAccessKeys.d.ts","sourceRoot":"","sources":["../../src/rest/MediaAccessKeys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAKvD,iFAAiF;AACjF,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AAEtD,iGAAiG;AACjG,eAAO,MAAM,8BAA8B,IAAI,CAAC;AAEhD,kDAAkD;AAClD,MAAM,WAAW,sBAAsB;IACrC,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,uGAAuG;IACvG,MAAM,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,GAAG,EAAE,OAAO,uBAAuB,CAAC;CACrC;AAED,iDAAiD;AACjD,MAAM,MAAM,sBAAsB,GAC9B;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,sBAAsB,CAAA;CAAE,GAC/C;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAErB;;;;;;GAMG;AACH,qBAAa,qBAAsB,SAAQ,aAAa,CAAC,qBAAqB,CAAC;IAC7E,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,YAAY,CAAS;;IAO7B,WAAkB,QAAQ,IAAI,qBAAqB,CAElD;IAED,mFAAmF;IACnF,OAAO,CAAC,YAAY;IAkBpB;;;;OAIG;IACI,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAuC,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,IAAI,CAAA;KAAE;IASlI;;;OAGG;IACI,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,sBAAsB;CAmBrD"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Short-lived HS256 token manager for authenticated media streaming.
|
|
3
|
+
*
|
|
4
|
+
* Media-access tokens are minted by the `CreateMediaAccessToken` GraphQL mutation
|
|
5
|
+
* (after a per-user permission check on the `MJ: Files` row) and verified by the
|
|
6
|
+
* `/media/:fileId` streaming route. The token IS the capability: access is checked
|
|
7
|
+
* once at mint time, and the route trusts a valid, unexpired, file-matching token.
|
|
8
|
+
*
|
|
9
|
+
* This is intentionally a SEPARATE, self-contained mechanism from the magic-link
|
|
10
|
+
* RS256 manager: media streaming must work on EVERY deployment, whereas the
|
|
11
|
+
* magic-link keypair is only initialized when `magicLink.enabled` is configured.
|
|
12
|
+
* We sign HS256 with the deployment's symmetric secret (`MJ_BASE_ENCRYPTION_KEY`),
|
|
13
|
+
* falling back to an ephemeral per-process secret (with a warning) when unset —
|
|
14
|
+
* the same degradation MagicLinkKeyManager uses for its ephemeral keypair.
|
|
15
|
+
*
|
|
16
|
+
* @module @memberjunction/server/rest/MediaAccessKeys
|
|
17
|
+
*/
|
|
18
|
+
import { BaseSingleton } from '@memberjunction/global';
|
|
19
|
+
import { LogStatus } from '@memberjunction/core';
|
|
20
|
+
import { randomBytes } from 'node:crypto';
|
|
21
|
+
import jwt from 'jsonwebtoken';
|
|
22
|
+
/** The token's `typ` claim — the route rejects any token whose `typ` differs. */
|
|
23
|
+
export const MEDIA_ACCESS_TOKEN_TYPE = 'media-access';
|
|
24
|
+
/** Default media-access token lifetime (hours). Long enough to cover a full playback session. */
|
|
25
|
+
export const MEDIA_ACCESS_DEFAULT_TTL_HOURS = 4;
|
|
26
|
+
/**
|
|
27
|
+
* Signs and verifies short-lived HS256 media-access tokens. Singleton so the
|
|
28
|
+
* signing secret is stable for the process lifetime. The secret is resolved
|
|
29
|
+
* lazily on first use from `MJ_BASE_ENCRYPTION_KEY`; if unset, an ephemeral
|
|
30
|
+
* 32-byte secret is generated (outstanding tokens die on restart — acceptable
|
|
31
|
+
* for a 4h-TTL streaming credential).
|
|
32
|
+
*/
|
|
33
|
+
export class MediaAccessKeyManager extends BaseSingleton {
|
|
34
|
+
// Public to satisfy BaseSingleton.getInstance's `this: new () => T` constraint.
|
|
35
|
+
constructor() {
|
|
36
|
+
super();
|
|
37
|
+
this.secret = '';
|
|
38
|
+
this._initialized = false;
|
|
39
|
+
}
|
|
40
|
+
static get Instance() {
|
|
41
|
+
return MediaAccessKeyManager.getInstance();
|
|
42
|
+
}
|
|
43
|
+
/** Resolves the signing secret once, preferring the deployment's symmetric key. */
|
|
44
|
+
ensureSecret() {
|
|
45
|
+
if (this._initialized) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const configured = process.env.MJ_BASE_ENCRYPTION_KEY?.trim();
|
|
49
|
+
if (configured) {
|
|
50
|
+
this.secret = configured;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
this.secret = randomBytes(32).toString('base64');
|
|
54
|
+
LogStatus('[MediaAccess] No MJ_BASE_ENCRYPTION_KEY configured — generated an EPHEMERAL HS256 secret for ' +
|
|
55
|
+
'media-access tokens. Outstanding media URLs will be invalidated on restart. Set ' +
|
|
56
|
+
'MJ_BASE_ENCRYPTION_KEY for stable signing.');
|
|
57
|
+
}
|
|
58
|
+
this._initialized = true;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Mints a signed media-access token for a file + user, expiring after `ttlHours`.
|
|
62
|
+
*
|
|
63
|
+
* @returns `{ Token, ExpiresAt }` — the compact JWT and its absolute expiry.
|
|
64
|
+
*/
|
|
65
|
+
Sign(fileId, userId, ttlHours = MEDIA_ACCESS_DEFAULT_TTL_HOURS) {
|
|
66
|
+
this.ensureSecret();
|
|
67
|
+
const claims = { fileId, userId, typ: MEDIA_ACCESS_TOKEN_TYPE };
|
|
68
|
+
const expiresInSeconds = Math.floor(ttlHours * 3600);
|
|
69
|
+
const token = jwt.sign(claims, this.secret, { algorithm: 'HS256', expiresIn: expiresInSeconds });
|
|
70
|
+
const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
|
|
71
|
+
return { Token: token, ExpiresAt: expiresAt };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Verifies a token's signature, expiry, and `typ`. Returns the typed claims on
|
|
75
|
+
* success. Never throws — an invalid/expired/malformed token returns `{ Valid: false }`.
|
|
76
|
+
*/
|
|
77
|
+
Verify(token) {
|
|
78
|
+
this.ensureSecret();
|
|
79
|
+
try {
|
|
80
|
+
const decoded = jwt.verify(token, this.secret, { algorithms: ['HS256'] });
|
|
81
|
+
if (typeof decoded === 'object' && decoded !== null &&
|
|
82
|
+
decoded.typ === MEDIA_ACCESS_TOKEN_TYPE &&
|
|
83
|
+
typeof decoded.fileId === 'string' &&
|
|
84
|
+
typeof decoded.userId === 'string') {
|
|
85
|
+
const d = decoded;
|
|
86
|
+
return { Valid: true, Claims: { fileId: d.fileId, userId: d.userId, typ: MEDIA_ACCESS_TOKEN_TYPE } };
|
|
87
|
+
}
|
|
88
|
+
return { Valid: false };
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Signature mismatch, expiry, or malformed token — all map to "not valid".
|
|
92
|
+
return { Valid: false };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=MediaAccessKeys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MediaAccessKeys.js","sourceRoot":"","sources":["../../src/rest/MediaAccessKeys.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Authenticated HTTP-Range media streaming route (`GET /media/:fileId`).
|
|
3
|
+
*
|
|
4
|
+
* Streams an `MJ: Files` object's bytes (optionally a byte range) to the browser so
|
|
5
|
+
* `<audio>`/`<video>` elements get native progressive playback + seek-before-download
|
|
6
|
+
* for large media — instead of the whole file being base64'd over GraphQL.
|
|
7
|
+
*
|
|
8
|
+
* Auth model: the request carries a short-lived signed token (`?token=`) minted by
|
|
9
|
+
* `CreateMediaAccessToken` AFTER a per-user permission check. The token IS the
|
|
10
|
+
* capability: this route re-verifies its signature/expiry/`typ` and that it matches
|
|
11
|
+
* the `:fileId`, then loads the file under a SYSTEM context (it only needs
|
|
12
|
+
* `ProviderKey`/`ProviderID` to locate the bytes — no row-level re-check, since
|
|
13
|
+
* access was already authorized at mint).
|
|
14
|
+
*
|
|
15
|
+
* Mount unauthenticated, BEFORE the unified auth middleware:
|
|
16
|
+
* `app.use('/media', cors<cors.CorsRequest>(), createMediaStreamRouter())`
|
|
17
|
+
*
|
|
18
|
+
* @module @memberjunction/server/rest/MediaStreamHandler
|
|
19
|
+
*/
|
|
20
|
+
import { type Router } from 'express';
|
|
21
|
+
/**
|
|
22
|
+
* Builds the Express router exposing `GET /media/:fileId`. Stateless — verification
|
|
23
|
+
* and byte-source resolution happen per request.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createMediaStreamRouter(): Router;
|
|
26
|
+
//# sourceMappingURL=MediaStreamHandler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MediaStreamHandler.d.ts","sourceRoot":"","sources":["../../src/rest/MediaStreamHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAgB,EAAE,KAAK,MAAM,EAA+B,MAAM,SAAS,CAAC;AAiB5E;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAMhD"}
|