@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,714 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Imperative shell for public web widget guest sessions: resolves a
|
|
3
|
+
* widget instance by its public key, enforces the origin allowlist + status, and
|
|
4
|
+
* mints a short-lived anonymous guest session JWT by reusing the magic-link RS256
|
|
5
|
+
* key + `anonymous-embed` synthesis. The pure logic lives in `widgetCore.ts`.
|
|
6
|
+
*
|
|
7
|
+
* Design (per public-web-widget.md W1 + open-Q #1): DIRECT-MINT for anonymous
|
|
8
|
+
* guests — no MagicLinkInvite row is created (those model single-use invites; a
|
|
9
|
+
* widget guest is ephemeral). Forensics ride the opaque per-session id (`mj_sid`)
|
|
10
|
+
* carried in the token, plus best-effort structured audit logging. The minted
|
|
11
|
+
* token is validated by the SAME `magic-link` auth provider (same issuer/audience/
|
|
12
|
+
* JWKS) and synthesized into a constrained guest principal by `buildMagicLinkSessionUser`.
|
|
13
|
+
*
|
|
14
|
+
* @module @memberjunction/server/widget
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Metadata, RunView, UserInfo, LogError, LogStatus } from '@memberjunction/core';
|
|
18
|
+
import { UUIDsEqual } from '@memberjunction/global';
|
|
19
|
+
import type { MJConversationWidgetInstanceEntity, MJConversationEntity } from '@memberjunction/core-entities';
|
|
20
|
+
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
21
|
+
import { MagicLinkKeyManager } from '../auth/magicLink/MagicLinkKeys.js';
|
|
22
|
+
import { generateSessionId } from '../auth/magicLink/magicLinkCore.js';
|
|
23
|
+
import { MagicLinkService } from '../auth/magicLink/MagicLinkService.js';
|
|
24
|
+
import { configInfo, type WidgetConfig } from '../config.js';
|
|
25
|
+
import { buildWidgetGuestClaims, evaluateWidgetMint, parseEnabledChannels, type WidgetMintErrorCode } from './widgetCore.js';
|
|
26
|
+
import { verifyHostAssertion, type HostAssertedIdentity } from './host-identity.js';
|
|
27
|
+
import { writeReturningVisitorRecap } from '../agentSessions/ReturningVisitorRecap.js';
|
|
28
|
+
import { resolveIdentityByEmail, mergeVisitorIdentity, forgetVisitor, type ResolvedVisitorIdentity } from './visitorIdentity.js';
|
|
29
|
+
|
|
30
|
+
const WIDGET_ENTITY = 'MJ: Conversation Widget Instances';
|
|
31
|
+
const CONVERSATIONS_ENTITY = 'MJ: Conversations';
|
|
32
|
+
|
|
33
|
+
/** Shape of the durable visitor anchor key (opaque base64url, same alphabet as generateSessionId). */
|
|
34
|
+
const VISITOR_KEY_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
|
35
|
+
|
|
36
|
+
/** Resolved returning-visitor anchor for a mint: the durable key + the prior conversation to chain from. */
|
|
37
|
+
interface ReturningVisitorResolution {
|
|
38
|
+
/** The durable VisitorKey to persist as a cookie (validated-presented or freshly minted). */
|
|
39
|
+
visitorKey: string;
|
|
40
|
+
/** The most-recent prior conversation for this anchor in the widget's app, when this is a returning visit. */
|
|
41
|
+
lastConversationId?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Per-request forensic context captured for audit (sourced from the HTTP request). */
|
|
45
|
+
export interface WidgetMintAuditContext {
|
|
46
|
+
ipAddress?: string;
|
|
47
|
+
userAgent?: string;
|
|
48
|
+
origin?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Input to mint a guest session. */
|
|
52
|
+
export interface MintGuestSessionInput {
|
|
53
|
+
/** The widget's public embed key (pk_live_…). */
|
|
54
|
+
widgetKey: string;
|
|
55
|
+
/** The request's Origin header — enforced against the instance allowlist. */
|
|
56
|
+
origin?: string;
|
|
57
|
+
/**
|
|
58
|
+
* A host-signed RS256 identity assertion (D1 `host-identity` strategy). Required when the
|
|
59
|
+
* resolved widget's AuthStrategy is `HostIdentity`; ignored otherwise. Verified against the
|
|
60
|
+
* host's registered public key; its identity is carried as informational claims only.
|
|
61
|
+
*/
|
|
62
|
+
hostAssertion?: string;
|
|
63
|
+
/**
|
|
64
|
+
* The durable returning-visitor anchor (RV1) the client presents from its first-party cookie.
|
|
65
|
+
* Honored only when the widget's `RememberReturningVisitors` toggle is on: a valid presented key
|
|
66
|
+
* chains this visit to the visitor's prior conversation; absent/invalid on a first visit, the
|
|
67
|
+
* server mints a fresh one. Ignored entirely when remembering is off.
|
|
68
|
+
*/
|
|
69
|
+
visitorKey?: string;
|
|
70
|
+
audit?: WidgetMintAuditContext;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Result of a guest-session mint. */
|
|
74
|
+
export interface MintGuestSessionResult {
|
|
75
|
+
success: boolean;
|
|
76
|
+
/** The minted session JWT (RS256), held by the widget in memory and refreshed before expiry. */
|
|
77
|
+
token?: string;
|
|
78
|
+
/** ISO expiry of the session token. */
|
|
79
|
+
expiresAt?: string;
|
|
80
|
+
/** The widget instance id (also carried as the `mj_widget_id` claim). */
|
|
81
|
+
widgetId?: string;
|
|
82
|
+
/** The Application the session is scoped to. */
|
|
83
|
+
applicationId?: string;
|
|
84
|
+
/** The pinned support agent the widget passes as explicitAgentId for every turn (D5). */
|
|
85
|
+
pinnedAgentId?: string;
|
|
86
|
+
/** Which modalities this widget exposes (drives the widget UI). */
|
|
87
|
+
modality?: string;
|
|
88
|
+
/**
|
|
89
|
+
* The opaque per-session id (also the signed scope resourceId). The widget stamps
|
|
90
|
+
* Conversation.ExternalID with this so the Widget Guest RLS filters isolate this guest's rows.
|
|
91
|
+
*/
|
|
92
|
+
sessionId?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Optional hard ceiling (minutes) on a voice session for this widget. Surfaced so the client
|
|
95
|
+
* voice-abuse guard uses the deployment-configured limit; also enforced server-side at mint.
|
|
96
|
+
*/
|
|
97
|
+
voiceMaxSessionMinutes?: number;
|
|
98
|
+
/** Whether returning-visitor memory is enabled for this widget (gates the cookie + chaining). */
|
|
99
|
+
rememberReturningVisitors?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* The durable visitor anchor to persist as a cookie (RV1). Set only when remembering is on:
|
|
102
|
+
* the validated key the client presented, or a freshly minted one on a first visit.
|
|
103
|
+
*/
|
|
104
|
+
visitorKey?: string;
|
|
105
|
+
/** The visitor's prior conversation for this anchor (RV2 chain); set only on a returning visit. */
|
|
106
|
+
lastConversationId?: string;
|
|
107
|
+
/**
|
|
108
|
+
* The resolved polymorphic identity entity id (RV4), set only when a host-identity widget asserted a
|
|
109
|
+
* resolvable identity at mint. The client stamps it (with {@link MintGuestSessionResult.linkedRecordId})
|
|
110
|
+
* onto the new conversation so memory injection (RV3) keys off the resolved record, not the cookie.
|
|
111
|
+
*/
|
|
112
|
+
linkedEntityId?: string;
|
|
113
|
+
/** The resolved polymorphic identity record id (RV4); paired with {@link MintGuestSessionResult.linkedEntityId}. */
|
|
114
|
+
linkedRecordId?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Interactive channels (by name, e.g. `["Whiteboard"]`) this widget may attach when voice is active
|
|
117
|
+
* (Phase 2). Mirrors the widget instance's `EnabledChannels`; empty (the default) = no channels.
|
|
118
|
+
*/
|
|
119
|
+
enabledChannels?: string[];
|
|
120
|
+
error?: string;
|
|
121
|
+
errorCode?: WidgetMintErrorCode;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Input to resolve a returning visitor's identity after they verify (RV4, magic-link upgrade path). */
|
|
125
|
+
export interface ResolveVisitorIdentityInput {
|
|
126
|
+
/** The widget's public embed key. */
|
|
127
|
+
widgetKey: string;
|
|
128
|
+
/** The durable returning-visitor anchor whose trail should be promoted to the verified identity. */
|
|
129
|
+
visitorKey: string;
|
|
130
|
+
/** The verified visitor's email (from the authenticated session) — resolved to a record via config. */
|
|
131
|
+
verifiedEmail: string;
|
|
132
|
+
/** The request's Origin header — enforced against the instance allowlist. */
|
|
133
|
+
origin?: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Result of an RV4 identity resolve+merge. */
|
|
137
|
+
export interface ResolveVisitorIdentityResult {
|
|
138
|
+
success: boolean;
|
|
139
|
+
/** The resolved polymorphic pair, when a record matched the verified email. */
|
|
140
|
+
resolved?: ResolvedVisitorIdentity;
|
|
141
|
+
/** How many conversations were stamped with the resolved identity. */
|
|
142
|
+
mergedConversations?: number;
|
|
143
|
+
error?: string;
|
|
144
|
+
errorCode?: WidgetMintErrorCode;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Input to a "forget me" request (RV5). */
|
|
148
|
+
export interface ForgetVisitorInput {
|
|
149
|
+
/** The widget's public embed key. */
|
|
150
|
+
widgetKey: string;
|
|
151
|
+
/** The durable returning-visitor anchor to forget. */
|
|
152
|
+
visitorKey: string;
|
|
153
|
+
/** The request's Origin header — enforced against the instance allowlist. */
|
|
154
|
+
origin?: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Result of the shared RV4/RV5 visitor-privacy gate. A single flat shape (not a discriminated union)
|
|
159
|
+
* because MJServer compiles without strictNullChecks, where literal-discriminant narrowing is disabled.
|
|
160
|
+
* On `ok`, `widget`/`contextUser` are set; otherwise `errorCode`/`error` are set.
|
|
161
|
+
*/
|
|
162
|
+
interface VisitorPrivacyGateResult {
|
|
163
|
+
ok: boolean;
|
|
164
|
+
widget?: MJConversationWidgetInstanceEntity;
|
|
165
|
+
contextUser?: UserInfo;
|
|
166
|
+
errorCode?: WidgetMintErrorCode;
|
|
167
|
+
error?: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Result of a "forget me" request (RV5). */
|
|
171
|
+
export interface ForgetVisitorResult {
|
|
172
|
+
success: boolean;
|
|
173
|
+
/** Count of memory notes archived. */
|
|
174
|
+
notesArchived?: number;
|
|
175
|
+
/** Count of conversations whose VisitorKey linkage was cleared. */
|
|
176
|
+
conversationsCleared?: number;
|
|
177
|
+
error?: string;
|
|
178
|
+
errorCode?: WidgetMintErrorCode;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Input to request a magic-link identity upgrade for a live guest session (W5). */
|
|
182
|
+
export interface UpgradeGuestSessionInput {
|
|
183
|
+
/** The widget's public embed key. */
|
|
184
|
+
widgetKey: string;
|
|
185
|
+
/** The email to send the verification link to (becomes the provisioned user's email). */
|
|
186
|
+
email: string;
|
|
187
|
+
/** The request's Origin header — enforced against the instance allowlist. */
|
|
188
|
+
origin?: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Result of a magic-link upgrade request. Never carries the token (delivered by email, out-of-band). */
|
|
192
|
+
export interface UpgradeGuestSessionResult {
|
|
193
|
+
success: boolean;
|
|
194
|
+
/** Whether the verification email was dispatched (false when no comms provider is configured). */
|
|
195
|
+
emailSent?: boolean;
|
|
196
|
+
error?: string;
|
|
197
|
+
errorCode?: WidgetMintErrorCode;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Minimal email sanity check (presence + single `@` with a dotted domain) — not full RFC validation. */
|
|
201
|
+
function isLikelyEmail(email: string): boolean {
|
|
202
|
+
return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Mints constrained, short-lived guest sessions for the public web widget.
|
|
207
|
+
* Stateless; constructed once at server startup alongside the magic-link service.
|
|
208
|
+
*/
|
|
209
|
+
export class WidgetSessionService {
|
|
210
|
+
constructor(
|
|
211
|
+
private readonly publicUrl: string,
|
|
212
|
+
private readonly config: WidgetConfig,
|
|
213
|
+
) {}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Resolves the widget instance, enforces guardrails, and mints a guest JWT.
|
|
217
|
+
* Never throws for business failures — returns `{ success: false, errorCode }`.
|
|
218
|
+
*/
|
|
219
|
+
public async MintGuestSession(input: MintGuestSessionInput): Promise<MintGuestSessionResult> {
|
|
220
|
+
try {
|
|
221
|
+
const contextUser = this.resolveLookupUser();
|
|
222
|
+
if (!contextUser) {
|
|
223
|
+
LogError('[Widget] No lookup context user available; server not configured.');
|
|
224
|
+
return { success: false, errorCode: 'server_error', error: 'Server not configured for widget sessions.' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const widget = await this.loadWidgetByKey(input.widgetKey, contextUser);
|
|
228
|
+
if (!widget) {
|
|
229
|
+
return this.audited({ success: false, errorCode: 'not_found', error: 'Unknown widget key.' }, input, undefined);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const eligibility = evaluateWidgetMint(
|
|
233
|
+
{ Status: widget.Status, AllowedOrigins: widget.AllowedOrigins, Modality: widget.Modality },
|
|
234
|
+
input.origin,
|
|
235
|
+
);
|
|
236
|
+
if (!eligibility.ok) {
|
|
237
|
+
return this.audited({ success: false, errorCode: eligibility.errorCode, error: 'Widget mint rejected.' }, input, widget);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const guestRoleName = this.resolveGuestRoleName(widget.GuestRoleID);
|
|
241
|
+
if (!guestRoleName) {
|
|
242
|
+
LogError(`[Widget] Guest role ${widget.GuestRoleID} for widget ${widget.ID} not found in metadata.`);
|
|
243
|
+
return this.audited({ success: false, errorCode: 'server_error', error: 'Guest role not resolvable.' }, input, widget);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// D1 host-identity strategy: a HostIdentity widget MUST present a valid host assertion.
|
|
247
|
+
let hostIdentity: HostAssertedIdentity | undefined;
|
|
248
|
+
if (widget.AuthStrategy === 'HostIdentity') {
|
|
249
|
+
const verified = this.verifyHost(widget, input.hostAssertion);
|
|
250
|
+
if (!verified) {
|
|
251
|
+
return this.audited({ success: false, errorCode: 'host_assertion_invalid', error: 'Host identity assertion invalid.' }, input, widget);
|
|
252
|
+
}
|
|
253
|
+
hostIdentity = verified;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Returning-visitor anchor (RV1): resolve/mint the durable VisitorKey and chain to the prior
|
|
257
|
+
// conversation — only when this widget opts in. Never blocks the mint (best-effort on lookup failure).
|
|
258
|
+
const returningVisitor = await this.resolveReturningVisitor(widget, input.visitorKey, contextUser);
|
|
259
|
+
|
|
260
|
+
// RV2 (text path): the text widget runs client-side and has no server "session closed" event, so
|
|
261
|
+
// we recap the visitor's PRIOR conversation lazily, at the moment they return — which is exactly
|
|
262
|
+
// when the recap is needed for RV3 to inject it into this new session. Idempotent (skips when a
|
|
263
|
+
// recap already exists) and best-effort (never blocks or fails the mint).
|
|
264
|
+
await this.recapPriorConversationIfNeeded(widget, returningVisitor, contextUser);
|
|
265
|
+
|
|
266
|
+
// RV4 (host-identity path): when a host asserts a resolvable identity, promote the visitor's prior
|
|
267
|
+
// anonymous trail to that record and surface the pair so the client stamps the new conversation.
|
|
268
|
+
const resolvedIdentity = await this.resolveHostIdentityIfApplicable(widget, hostIdentity, returningVisitor, contextUser);
|
|
269
|
+
|
|
270
|
+
return this.audited(this.mintToken(widget, guestRoleName, hostIdentity, returningVisitor, resolvedIdentity), input, widget);
|
|
271
|
+
} catch (e) {
|
|
272
|
+
LogError(e);
|
|
273
|
+
return { success: false, errorCode: 'server_error', error: e instanceof Error ? e.message : String(e) };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Refreshes a live guest session. The widget always holds its public key, so a
|
|
279
|
+
* refresh is a fresh mint by the same key + origin — short-TTL tokens with no
|
|
280
|
+
* refresh-token machinery (mirrors magic-link's no-refresh-token stance).
|
|
281
|
+
*/
|
|
282
|
+
public async RefreshGuestSession(input: MintGuestSessionInput): Promise<MintGuestSessionResult> {
|
|
283
|
+
return this.MintGuestSession(input);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Short-TTL cache of resolved per-instance rate limits, so the limiter doesn't hit the DB per request. */
|
|
287
|
+
private readonly rateLimitCache = new Map<string, { limit: number; expiresAtMs: number }>();
|
|
288
|
+
private static readonly RATE_LIMIT_CACHE_TTL_MS = 60_000;
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Resolves the per-instance `RateLimitPerMinute` for a widget key (W6 hardening), falling back to the
|
|
292
|
+
* deployment-wide `defaultRateLimitPerMinute` for an unknown/invalid key (so an enumeration probe is
|
|
293
|
+
* still bounded by the coarse default). Cached for a short TTL so the dynamic limiter — which runs on
|
|
294
|
+
* EVERY mint request — does not issue a DB read per request. Never throws.
|
|
295
|
+
*/
|
|
296
|
+
public async ResolvePerInstanceRateLimit(widgetKey: string): Promise<number> {
|
|
297
|
+
const key = (widgetKey ?? '').trim();
|
|
298
|
+
const fallback = this.config.defaultRateLimitPerMinute;
|
|
299
|
+
if (!key) {
|
|
300
|
+
return fallback;
|
|
301
|
+
}
|
|
302
|
+
const cached = this.rateLimitCache.get(key);
|
|
303
|
+
const now = Date.now();
|
|
304
|
+
if (cached && cached.expiresAtMs > now) {
|
|
305
|
+
return cached.limit;
|
|
306
|
+
}
|
|
307
|
+
let limit = fallback;
|
|
308
|
+
try {
|
|
309
|
+
const contextUser = this.resolveLookupUser();
|
|
310
|
+
if (contextUser) {
|
|
311
|
+
const widget = await this.loadWidgetByKey(key, contextUser);
|
|
312
|
+
if (widget && widget.RateLimitPerMinute > 0) {
|
|
313
|
+
limit = widget.RateLimitPerMinute;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} catch (e) {
|
|
317
|
+
LogError(e);
|
|
318
|
+
}
|
|
319
|
+
this.rateLimitCache.set(key, { limit, expiresAtMs: now + WidgetSessionService.RATE_LIMIT_CACHE_TTL_MS });
|
|
320
|
+
return limit;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* W5 magic-link upgrade — "Verify it's you". For a widget whose AuthStrategy is `MagicLinkUpgrade`,
|
|
325
|
+
* a guest supplies their email and we issue a single-use **email-mode** magic-link invite scoped to
|
|
326
|
+
* the widget's Application (under the server principal — `CreateInvite` is Owner-gated). The visitor
|
|
327
|
+
* clicks the emailed link → the existing public `POST /magic-link/redeem` mints a VERIFIED session
|
|
328
|
+
* JWT → the widget swaps it in via `transport.UpdateToken`, preserving the live conversationId.
|
|
329
|
+
*
|
|
330
|
+
* This converges on the SAME `AuthProviderFactory` + `buildMagicLinkSessionUser` path as the guest
|
|
331
|
+
* mint (D1), so the verified session is just a more-privileged principal on the one pathway. Never
|
|
332
|
+
* throws for business failures — returns a structured result.
|
|
333
|
+
*/
|
|
334
|
+
public async RequestUpgrade(input: UpgradeGuestSessionInput): Promise<UpgradeGuestSessionResult> {
|
|
335
|
+
try {
|
|
336
|
+
const email = (input.email ?? '').trim();
|
|
337
|
+
if (!isLikelyEmail(email)) {
|
|
338
|
+
return { success: false, errorCode: 'invalid_email', error: 'A valid email is required.' };
|
|
339
|
+
}
|
|
340
|
+
const contextUser = this.resolveLookupUser();
|
|
341
|
+
if (!contextUser) {
|
|
342
|
+
return { success: false, errorCode: 'server_error', error: 'Server not configured for widget sessions.' };
|
|
343
|
+
}
|
|
344
|
+
const widget = await this.loadWidgetByKey(input.widgetKey, contextUser);
|
|
345
|
+
if (!widget) {
|
|
346
|
+
return { success: false, errorCode: 'not_found', error: 'Unknown widget key.' };
|
|
347
|
+
}
|
|
348
|
+
const eligibility = evaluateWidgetMint({ Status: widget.Status, AllowedOrigins: widget.AllowedOrigins, Modality: widget.Modality }, input.origin);
|
|
349
|
+
if (!eligibility.ok) {
|
|
350
|
+
return { success: false, errorCode: eligibility.errorCode, error: 'Widget upgrade rejected.' };
|
|
351
|
+
}
|
|
352
|
+
if (widget.AuthStrategy !== 'MagicLinkUpgrade') {
|
|
353
|
+
return { success: false, errorCode: 'upgrade_not_enabled', error: 'This widget does not offer magic-link upgrade.' };
|
|
354
|
+
}
|
|
355
|
+
const magicLinkConfig = configInfo.magicLink;
|
|
356
|
+
if (!magicLinkConfig) {
|
|
357
|
+
LogError('[Widget] Upgrade requested but magicLink config is absent.');
|
|
358
|
+
return { success: false, errorCode: 'server_error', error: 'Identity verification is not configured.' };
|
|
359
|
+
}
|
|
360
|
+
const service = new MagicLinkService(this.publicUrl, magicLinkConfig);
|
|
361
|
+
const result = await service.CreateInvite({ email, applicationId: widget.ApplicationID }, contextUser);
|
|
362
|
+
if (!result.success) {
|
|
363
|
+
LogError(`[Widget] Upgrade invite failed for widget ${widget.ID}: ${result.error ?? result.errorCode ?? 'unknown'}`);
|
|
364
|
+
return { success: false, errorCode: 'server_error', error: 'Could not start identity verification.' };
|
|
365
|
+
}
|
|
366
|
+
// Deliberately do NOT echo the raw token/redemption URL to the public caller — the verified link
|
|
367
|
+
// is delivered out-of-band by email so a widget-key holder cannot mint verified sessions at will.
|
|
368
|
+
return { success: true, emailSent: result.emailSent ?? false };
|
|
369
|
+
} catch (e) {
|
|
370
|
+
LogError(e);
|
|
371
|
+
return { success: false, errorCode: 'server_error', error: e instanceof Error ? e.message : String(e) };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Verifies a host-identity assertion for a HostIdentity widget against the host's
|
|
377
|
+
* registered public key (config interim store, keyed by widget PublicKey). Fails closed
|
|
378
|
+
* when the key is absent or the assertion is missing/invalid.
|
|
379
|
+
*/
|
|
380
|
+
private verifyHost(widget: MJConversationWidgetInstanceEntity, assertion: string | undefined): HostAssertedIdentity | null {
|
|
381
|
+
// Prefer the per-instance HostPublicKey column (Phase 3 — no config-resident keys); fall back to the
|
|
382
|
+
// interim config map (keyed by PublicKey) for deployments that haven't migrated their key yet.
|
|
383
|
+
const hostKey = widget.HostPublicKey ?? this.config.hostPublicKeys?.[widget.PublicKey];
|
|
384
|
+
const result = verifyHostAssertion(assertion, hostKey, widget.PublicKey);
|
|
385
|
+
if (result.ok && result.identity) {
|
|
386
|
+
return result.identity;
|
|
387
|
+
}
|
|
388
|
+
LogError(`[Widget] Host assertion rejected for widget ${widget.ID}: ${result.errorCode ?? 'unknown'}`);
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Builds + signs the guest JWT for an eligible widget instance. */
|
|
393
|
+
private mintToken(
|
|
394
|
+
widget: MJConversationWidgetInstanceEntity,
|
|
395
|
+
guestRoleName: string,
|
|
396
|
+
hostIdentity?: HostAssertedIdentity,
|
|
397
|
+
returningVisitor?: ReturningVisitorResolution,
|
|
398
|
+
resolvedIdentity?: ResolvedVisitorIdentity,
|
|
399
|
+
): MintGuestSessionResult {
|
|
400
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
401
|
+
const ttlMinutes = widget.SessionTTLMinutes || this.config.defaultSessionTtlMinutes;
|
|
402
|
+
// Generated once and returned to the client: the widget stamps Conversation.ExternalID with
|
|
403
|
+
// this id so the Widget Guest RLS filters ({{ScopeResourceID}}) isolate this guest's rows.
|
|
404
|
+
const sessionId = generateSessionId();
|
|
405
|
+
const claims = buildWidgetGuestClaims({
|
|
406
|
+
issuer: this.publicUrl,
|
|
407
|
+
audience: this.config.audience,
|
|
408
|
+
widgetId: widget.ID,
|
|
409
|
+
sessionId,
|
|
410
|
+
anonymousEmail: this.config.anonymousEmail,
|
|
411
|
+
applicationId: widget.ApplicationID,
|
|
412
|
+
guestRoleName,
|
|
413
|
+
nowSeconds,
|
|
414
|
+
ttlSeconds: ttlMinutes * 60,
|
|
415
|
+
hostIdentity,
|
|
416
|
+
// RV1/RV2/RV4: carry the returning-visitor anchor + resolved identity as claims so the voice path
|
|
417
|
+
// (server-created conversation) stamps the same fields the text path stamps client-side.
|
|
418
|
+
returningVisitor: returningVisitor
|
|
419
|
+
? {
|
|
420
|
+
visitorKey: returningVisitor.visitorKey,
|
|
421
|
+
lastConversationId: returningVisitor.lastConversationId,
|
|
422
|
+
linkedEntityId: resolvedIdentity?.entityId,
|
|
423
|
+
linkedRecordId: resolvedIdentity?.recordId,
|
|
424
|
+
}
|
|
425
|
+
: undefined,
|
|
426
|
+
});
|
|
427
|
+
const token = MagicLinkKeyManager.Instance.Sign(claims);
|
|
428
|
+
return {
|
|
429
|
+
success: true,
|
|
430
|
+
token,
|
|
431
|
+
expiresAt: new Date(claims.exp * 1000).toISOString(),
|
|
432
|
+
widgetId: widget.ID,
|
|
433
|
+
applicationId: widget.ApplicationID,
|
|
434
|
+
pinnedAgentId: widget.PinnedAgentID,
|
|
435
|
+
modality: widget.Modality,
|
|
436
|
+
sessionId,
|
|
437
|
+
// Phase 2: which interactive channels this widget may attach during a voice session. Read from the
|
|
438
|
+
// per-instance EnabledChannels column (added by the Widget_Public_Hardening migration + CodeGen).
|
|
439
|
+
enabledChannels: parseEnabledChannels(widget.EnabledChannels),
|
|
440
|
+
voiceMaxSessionMinutes: widget.VoiceMaxSessionMinutes ?? undefined,
|
|
441
|
+
rememberReturningVisitors: !!returningVisitor,
|
|
442
|
+
visitorKey: returningVisitor?.visitorKey,
|
|
443
|
+
lastConversationId: returningVisitor?.lastConversationId,
|
|
444
|
+
linkedEntityId: resolvedIdentity?.entityId,
|
|
445
|
+
linkedRecordId: resolvedIdentity?.recordId,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* RV4 (host-identity path): when a `HostIdentity` widget asserts a resolvable identity AND remembering
|
|
451
|
+
* is on, resolve the asserted email to a polymorphic record and merge the visitor's prior anonymous
|
|
452
|
+
* trail onto it. Returns the resolved pair (so the client stamps the new conversation), or undefined
|
|
453
|
+
* when not applicable or no record matched. Best-effort — never blocks the mint.
|
|
454
|
+
*/
|
|
455
|
+
private async resolveHostIdentityIfApplicable(
|
|
456
|
+
widget: MJConversationWidgetInstanceEntity,
|
|
457
|
+
hostIdentity: HostAssertedIdentity | undefined,
|
|
458
|
+
returningVisitor: ReturningVisitorResolution | undefined,
|
|
459
|
+
contextUser: UserInfo,
|
|
460
|
+
): Promise<ResolvedVisitorIdentity | undefined> {
|
|
461
|
+
if (!widget.RememberReturningVisitors || !hostIdentity?.email || !returningVisitor?.visitorKey) {
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
const identity = await resolveIdentityByEmail(hostIdentity.email, contextUser, Metadata.Provider, this.config.identityResolution); // global-provider-ok: server-side mint under the single default provider
|
|
465
|
+
if (!identity) {
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
await mergeVisitorIdentity({
|
|
469
|
+
visitorKey: returningVisitor.visitorKey,
|
|
470
|
+
applicationId: widget.ApplicationID,
|
|
471
|
+
identity,
|
|
472
|
+
contextUser,
|
|
473
|
+
provider: Metadata.Provider, // global-provider-ok: server-side mint under the single default provider
|
|
474
|
+
});
|
|
475
|
+
return identity;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* RV4 (magic-link upgrade path): after a visitor verifies their identity, promote their anonymous
|
|
480
|
+
* trail to the verified record. Called from the AUTHENTICATED route with the verified session's email,
|
|
481
|
+
* so the resolved record is the deployment-configured target for that email (default: the MJ User).
|
|
482
|
+
* Validates the widget + origin + opt-in + key, then resolves and merges. Never throws.
|
|
483
|
+
*/
|
|
484
|
+
public async ResolveVisitorIdentity(input: ResolveVisitorIdentityInput): Promise<ResolveVisitorIdentityResult> {
|
|
485
|
+
try {
|
|
486
|
+
const gate = await this.gateVisitorPrivacyRequest(input.widgetKey, input.visitorKey, input.origin);
|
|
487
|
+
if (!gate.ok) {
|
|
488
|
+
return { success: false, errorCode: gate.errorCode, error: gate.error };
|
|
489
|
+
}
|
|
490
|
+
const identity = await resolveIdentityByEmail(input.verifiedEmail, gate.contextUser, Metadata.Provider, this.config.identityResolution); // global-provider-ok: server-side mint under the single default provider
|
|
491
|
+
if (!identity) {
|
|
492
|
+
// Verified, but no record matches the email under the configured target — nothing to merge.
|
|
493
|
+
return { success: true, mergedConversations: 0 };
|
|
494
|
+
}
|
|
495
|
+
const mergedConversations = await mergeVisitorIdentity({
|
|
496
|
+
visitorKey: input.visitorKey,
|
|
497
|
+
applicationId: gate.widget.ApplicationID,
|
|
498
|
+
identity,
|
|
499
|
+
contextUser: gate.contextUser,
|
|
500
|
+
provider: Metadata.Provider, // global-provider-ok: server-side mint under the single default provider
|
|
501
|
+
});
|
|
502
|
+
return { success: true, resolved: identity, mergedConversations };
|
|
503
|
+
} catch (e) {
|
|
504
|
+
LogError(e);
|
|
505
|
+
return { success: false, errorCode: 'server_error', error: e instanceof Error ? e.message : String(e) };
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* RV5 "forget me": archives the visitor's auto-generated memory and clears the VisitorKey linkage for
|
|
511
|
+
* this anchor in the widget's application. Public (the visitor proves possession of the durable key,
|
|
512
|
+
* not an identity). Validates the widget + origin + opt-in + key, then delegates to {@link forgetVisitor}.
|
|
513
|
+
*/
|
|
514
|
+
public async ForgetVisitor(input: ForgetVisitorInput): Promise<ForgetVisitorResult> {
|
|
515
|
+
try {
|
|
516
|
+
const gate = await this.gateVisitorPrivacyRequest(input.widgetKey, input.visitorKey, input.origin);
|
|
517
|
+
if (!gate.ok) {
|
|
518
|
+
return { success: false, errorCode: gate.errorCode, error: gate.error };
|
|
519
|
+
}
|
|
520
|
+
const { notesArchived, conversationsCleared } = await forgetVisitor({
|
|
521
|
+
visitorKey: input.visitorKey,
|
|
522
|
+
applicationId: gate.widget.ApplicationID,
|
|
523
|
+
contextUser: gate.contextUser,
|
|
524
|
+
provider: Metadata.Provider, // global-provider-ok: server-side mint under the single default provider
|
|
525
|
+
});
|
|
526
|
+
return { success: true, notesArchived, conversationsCleared };
|
|
527
|
+
} catch (e) {
|
|
528
|
+
LogError(e);
|
|
529
|
+
return { success: false, errorCode: 'server_error', error: e instanceof Error ? e.message : String(e) };
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Shared guard for the RV4/RV5 visitor-privacy endpoints: resolves the lookup user, loads + validates
|
|
535
|
+
* the widget (status/origin), requires returning-visitor memory to be ON, and validates the presented
|
|
536
|
+
* VisitorKey shape. Returns the resolved widget + context user on success, or a structured failure.
|
|
537
|
+
*/
|
|
538
|
+
private async gateVisitorPrivacyRequest(
|
|
539
|
+
widgetKey: string,
|
|
540
|
+
visitorKey: string,
|
|
541
|
+
origin: string | undefined,
|
|
542
|
+
): Promise<VisitorPrivacyGateResult> {
|
|
543
|
+
const contextUser = this.resolveLookupUser();
|
|
544
|
+
if (!contextUser) {
|
|
545
|
+
return { ok: false, errorCode: 'server_error', error: 'Server not configured for widget sessions.' };
|
|
546
|
+
}
|
|
547
|
+
const widget = await this.loadWidgetByKey(widgetKey, contextUser);
|
|
548
|
+
if (!widget) {
|
|
549
|
+
return { ok: false, errorCode: 'not_found', error: 'Unknown widget key.' };
|
|
550
|
+
}
|
|
551
|
+
const eligibility = evaluateWidgetMint(
|
|
552
|
+
{ Status: widget.Status, AllowedOrigins: widget.AllowedOrigins, Modality: widget.Modality },
|
|
553
|
+
origin,
|
|
554
|
+
);
|
|
555
|
+
if (!eligibility.ok) {
|
|
556
|
+
return { ok: false, errorCode: eligibility.errorCode ?? 'server_error', error: 'Widget request rejected.' };
|
|
557
|
+
}
|
|
558
|
+
if (!widget.RememberReturningVisitors) {
|
|
559
|
+
return { ok: false, errorCode: 'upgrade_not_enabled', error: 'Returning-visitor memory is not enabled for this widget.' };
|
|
560
|
+
}
|
|
561
|
+
if (!VISITOR_KEY_PATTERN.test((visitorKey ?? '').trim())) {
|
|
562
|
+
return { ok: false, errorCode: 'not_found', error: 'A valid visitorKey is required.' };
|
|
563
|
+
}
|
|
564
|
+
return { ok: true, widget, contextUser };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Resolves the returning-visitor anchor for a mint (RV1). Returns undefined when the widget does NOT
|
|
569
|
+
* opt in (RememberReturningVisitors off) — the default, fully-off path: no cookie, no chaining.
|
|
570
|
+
* When on: validates the presented key (else mints a fresh one) and, for a returning key, finds the
|
|
571
|
+
* visitor's most-recent prior conversation in this widget's application to chain from. Best-effort:
|
|
572
|
+
* a lookup failure degrades to "no prior conversation" rather than blocking the session.
|
|
573
|
+
*/
|
|
574
|
+
private async resolveReturningVisitor(
|
|
575
|
+
widget: MJConversationWidgetInstanceEntity,
|
|
576
|
+
presentedKey: string | undefined,
|
|
577
|
+
contextUser: UserInfo,
|
|
578
|
+
): Promise<ReturningVisitorResolution | undefined> {
|
|
579
|
+
if (!widget.RememberReturningVisitors) {
|
|
580
|
+
return undefined;
|
|
581
|
+
}
|
|
582
|
+
const presented = (presentedKey ?? '').trim();
|
|
583
|
+
const isReturning = VISITOR_KEY_PATTERN.test(presented);
|
|
584
|
+
const visitorKey = isReturning ? presented : generateSessionId();
|
|
585
|
+
const lastConversationId = isReturning
|
|
586
|
+
? await this.findPreviousConversationByVisitorKey(visitorKey, widget.ApplicationID, contextUser)
|
|
587
|
+
: undefined;
|
|
588
|
+
return { visitorKey, lastConversationId };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Recaps the visitor's prior conversation when they return (RV2, text path). No-op unless this is a
|
|
593
|
+
* returning visit (a resolved `lastConversationId`). Delegates to the shared, idempotent,
|
|
594
|
+
* best-effort {@link writeReturningVisitorRecap}, attributing the recap to the widget's pinned agent
|
|
595
|
+
* and stamping the widget's `VisitorMemoryRetentionDays` as the note's expiry. Awaited so the recap
|
|
596
|
+
* note exists before the new session's memory injection (RV3) runs; the recap's own idempotency
|
|
597
|
+
* bounds the LLM cost to the first return after each prior conversation.
|
|
598
|
+
*/
|
|
599
|
+
private async recapPriorConversationIfNeeded(
|
|
600
|
+
widget: MJConversationWidgetInstanceEntity,
|
|
601
|
+
returningVisitor: ReturningVisitorResolution | undefined,
|
|
602
|
+
contextUser: UserInfo,
|
|
603
|
+
): Promise<void> {
|
|
604
|
+
const priorConversationId = returningVisitor?.lastConversationId;
|
|
605
|
+
if (!priorConversationId) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
// global-provider-ok: server-side mint under the single default provider (same rationale as resolveGuestRoleName).
|
|
609
|
+
await writeReturningVisitorRecap(
|
|
610
|
+
priorConversationId,
|
|
611
|
+
widget.PinnedAgentID,
|
|
612
|
+
contextUser,
|
|
613
|
+
Metadata.Provider, // global-provider-ok: server-side mint under the single default provider
|
|
614
|
+
widget.VisitorMemoryRetentionDays,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Finds the visitor's most-recent prior conversation for a VisitorKey, scoped to the widget's
|
|
620
|
+
* application so a key can never resolve a conversation from a different deployment. Returns the
|
|
621
|
+
* id or undefined (first visit, or read failure — never throws).
|
|
622
|
+
*/
|
|
623
|
+
private async findPreviousConversationByVisitorKey(
|
|
624
|
+
visitorKey: string,
|
|
625
|
+
applicationId: string,
|
|
626
|
+
contextUser: UserInfo,
|
|
627
|
+
): Promise<string | undefined> {
|
|
628
|
+
const rv = new RunView();
|
|
629
|
+
const result = await rv.RunView<MJConversationEntity>(
|
|
630
|
+
{
|
|
631
|
+
EntityName: CONVERSATIONS_ENTITY,
|
|
632
|
+
// visitorKey is validated base64url ([A-Za-z0-9_-]) so the literal is injection-safe; applicationId
|
|
633
|
+
// is a server-trusted UUID. Scope to the application to prevent cross-widget anchor reuse.
|
|
634
|
+
ExtraFilter: `VisitorKey = '${visitorKey}' AND ApplicationID = '${applicationId.replace(/'/g, "''")}'`,
|
|
635
|
+
OrderBy: '__mj_CreatedAt DESC',
|
|
636
|
+
MaxRows: 1,
|
|
637
|
+
Fields: ['ID'],
|
|
638
|
+
ResultType: 'simple',
|
|
639
|
+
},
|
|
640
|
+
contextUser,
|
|
641
|
+
);
|
|
642
|
+
if (!result.Success) {
|
|
643
|
+
LogError(`[Widget] Returning-visitor lookup failed: ${result.ErrorMessage}`);
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
return result.Results?.[0]?.ID ?? undefined;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Loads a single widget instance by its public key (read-only, simple result). */
|
|
650
|
+
private async loadWidgetByKey(widgetKey: string, contextUser: UserInfo): Promise<MJConversationWidgetInstanceEntity | null> {
|
|
651
|
+
const key = (widgetKey ?? '').trim();
|
|
652
|
+
if (!key) {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
const rv = new RunView();
|
|
656
|
+
const result = await rv.RunView<MJConversationWidgetInstanceEntity>(
|
|
657
|
+
{
|
|
658
|
+
EntityName: WIDGET_ENTITY,
|
|
659
|
+
ExtraFilter: `PublicKey = '${key.replace(/'/g, "''")}'`,
|
|
660
|
+
MaxRows: 1,
|
|
661
|
+
ResultType: 'entity_object',
|
|
662
|
+
},
|
|
663
|
+
contextUser,
|
|
664
|
+
);
|
|
665
|
+
if (!result.Success) {
|
|
666
|
+
LogError(`[Widget] Failed to load widget by key: ${result.ErrorMessage}`);
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
return result.Results?.[0] ?? null;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** Resolves the guest role's NAME from its id (claims carry the name, not the id). */
|
|
673
|
+
private resolveGuestRoleName(guestRoleId: string): string | null {
|
|
674
|
+
const md = Metadata.Provider; // global-provider-ok: pre-auth server-side mint under the single default provider
|
|
675
|
+
const role = md?.Roles.find((r) => UUIDsEqual(r.ID, guestRoleId));
|
|
676
|
+
return role?.Name ?? null;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** Resolves the server-side context user used to READ widget config (not the guest principal). */
|
|
680
|
+
private resolveLookupUser(): UserInfo | null {
|
|
681
|
+
const candidate = this.config.contextUserForLookup;
|
|
682
|
+
if (candidate) {
|
|
683
|
+
const byName = UserCache.Instance.UserByName(candidate);
|
|
684
|
+
if (byName) {
|
|
685
|
+
return byName;
|
|
686
|
+
}
|
|
687
|
+
LogError(`[Widget] Configured lookup user '${candidate}' not found; falling back to system/Owner.`);
|
|
688
|
+
}
|
|
689
|
+
return (
|
|
690
|
+
UserCache.Instance.GetSystemUser() ??
|
|
691
|
+
UserCache.Users.find((u) => u.IsActive && u.Type?.trim().toLowerCase() === 'owner') ??
|
|
692
|
+
null
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Best-effort structured audit of a mint attempt. Never alters the result — losing
|
|
698
|
+
* an audit line must not change whether a visitor got (or was denied) a session.
|
|
699
|
+
* (A dedicated `MJ: Widget Session` audit entity is a follow-on; for now this is a
|
|
700
|
+
* structured log line carrying the same forensics as the magic-link redemption row.)
|
|
701
|
+
*/
|
|
702
|
+
private audited(result: MintGuestSessionResult, input: MintGuestSessionInput, widget: MJConversationWidgetInstanceEntity | null): MintGuestSessionResult {
|
|
703
|
+
try {
|
|
704
|
+
LogStatus(
|
|
705
|
+
`[Widget][audit] mint outcome=${result.success ? 'success' : result.errorCode} ` +
|
|
706
|
+
`widget=${widget?.ID ?? 'unknown'} origin=${input.audit?.origin ?? input.origin ?? '-'} ` +
|
|
707
|
+
`ip=${input.audit?.ipAddress ?? '-'}`,
|
|
708
|
+
);
|
|
709
|
+
} catch {
|
|
710
|
+
/* audit is best-effort */
|
|
711
|
+
}
|
|
712
|
+
return result;
|
|
713
|
+
}
|
|
714
|
+
}
|