@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,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Host-passed identity (D1 strategy `host-identity`). When a widget is
|
|
3
|
+
* embedded in an already-authenticated host portal, the host signs a short-lived
|
|
4
|
+
* assertion (its own RS256 key) describing the visitor; the widget posts it to
|
|
5
|
+
* `POST /widget/session`, which verifies it against the host's registered public key
|
|
6
|
+
* and folds the host-provided identity into the minted guest claims. The minted token
|
|
7
|
+
* is still validated by the same `magic-link` AuthProviderFactory path + synthesized by
|
|
8
|
+
* `buildMagicLinkSessionUser` — host-identity is a MINT-TIME strategy, not a second
|
|
9
|
+
* session-validation provider (it converges on the one pathway, per D1).
|
|
10
|
+
*
|
|
11
|
+
* Pure + unit-testable: verification takes the PEM as an argument.
|
|
12
|
+
*
|
|
13
|
+
* @module @memberjunction/server/widget
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type jwt from 'jsonwebtoken';
|
|
17
|
+
import { HostIdentityProvider, type HostAssertionError } from '@memberjunction/auth-providers';
|
|
18
|
+
|
|
19
|
+
export type { HostAssertionError };
|
|
20
|
+
|
|
21
|
+
/** The visitor identity a host asserts. Standard OIDC-ish claims so synthesis is uniform. */
|
|
22
|
+
export interface HostAssertedIdentity {
|
|
23
|
+
email: string;
|
|
24
|
+
firstName?: string;
|
|
25
|
+
lastName?: string;
|
|
26
|
+
/** The host's opaque user id for this visitor (audit correlation; not an MJ user id). */
|
|
27
|
+
hostUserId?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Result of verifying a host assertion. `identity` is present iff `ok` is true. */
|
|
31
|
+
export interface HostAssertionResult {
|
|
32
|
+
ok: boolean;
|
|
33
|
+
identity?: HostAssertedIdentity;
|
|
34
|
+
errorCode?: HostAssertionError;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The single shared {@link HostIdentityProvider} instance the mint path verifies through. The base
|
|
39
|
+
* `BaseAuthProvider` constructor builds a (never-contacted) JWKS client; host-identity verifies against
|
|
40
|
+
* static per-widget PEM keys, so we pass a placeholder `jwksUri`. Built once — verification is stateless.
|
|
41
|
+
*/
|
|
42
|
+
const hostIdentityProvider = new HostIdentityProvider({
|
|
43
|
+
name: 'host-identity',
|
|
44
|
+
type: 'host-identity',
|
|
45
|
+
issuer: 'host-identity',
|
|
46
|
+
audience: 'host-identity',
|
|
47
|
+
jwksUri: 'https://host-identity.local/unused',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Verifies a host-signed RS256 assertion against the host's registered public key (PEM) and extracts the
|
|
52
|
+
* asserted visitor identity. Thin adapter over {@link HostIdentityProvider.VerifyHostAssertion} (the single
|
|
53
|
+
* implementation, registered in the AuthProviderFactory) preserving this module's result shape. Never throws.
|
|
54
|
+
*/
|
|
55
|
+
export function verifyHostAssertion(
|
|
56
|
+
assertion: string | undefined,
|
|
57
|
+
hostPublicKeyPem: string | undefined,
|
|
58
|
+
expectedAudience: string,
|
|
59
|
+
): HostAssertionResult {
|
|
60
|
+
const result = hostIdentityProvider.VerifyHostAssertion(assertion, hostPublicKeyPem, expectedAudience);
|
|
61
|
+
if (!result.ok || !result.userInfo?.email) {
|
|
62
|
+
return { ok: false, errorCode: result.errorCode };
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
identity: {
|
|
67
|
+
email: result.userInfo.email,
|
|
68
|
+
firstName: result.userInfo.firstName,
|
|
69
|
+
lastName: result.userInfo.lastName,
|
|
70
|
+
hostUserId: result.hostUserId,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Pulls the visitor identity from a verified host-assertion payload (delegates to the provider). */
|
|
76
|
+
export function extractHostIdentity(payload: jwt.JwtPayload): HostAssertedIdentity {
|
|
77
|
+
const info = hostIdentityProvider.extractUserInfo(payload);
|
|
78
|
+
return {
|
|
79
|
+
email: info.email ?? '',
|
|
80
|
+
firstName: info.firstName,
|
|
81
|
+
lastName: info.lastName,
|
|
82
|
+
hostUserId: typeof payload.sub === 'string' ? payload.sub : undefined,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Public web widget guest-session module barrel.
|
|
3
|
+
* @module @memberjunction/server/widget
|
|
4
|
+
*/
|
|
5
|
+
export * from './widgetCore.js';
|
|
6
|
+
export {
|
|
7
|
+
verifyHostAssertion,
|
|
8
|
+
extractHostIdentity,
|
|
9
|
+
type HostAssertedIdentity,
|
|
10
|
+
type HostAssertionResult,
|
|
11
|
+
type HostAssertionError,
|
|
12
|
+
} from './host-identity.js';
|
|
13
|
+
export { WidgetSessionService } from './WidgetSessionService.js';
|
|
14
|
+
export type { MintGuestSessionInput, MintGuestSessionResult, WidgetMintAuditContext } from './WidgetSessionService.js';
|
|
15
|
+
export { createWidgetHandler, WIDGET_MOUNT_PATH } from './WidgetRouter.js';
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Returning-visitor identity resolution + merge (RV4) and "forget me" (RV5).
|
|
3
|
+
*
|
|
4
|
+
* A widget visitor starts anonymous: their memory (recap notes) is filed under the polymorphic pair
|
|
5
|
+
* `(MJ: Conversations entity, conversation.ID)` and chained across visits by the durable `VisitorKey`
|
|
6
|
+
* cookie (RV1/RV2). When the visitor later proves who they are — magic-link "verify it's you", or a
|
|
7
|
+
* host-asserted identity — RV4 promotes that anonymous trail to a real, deployment-configurable record:
|
|
8
|
+
*
|
|
9
|
+
* 1. resolve the verified email → a polymorphic `(entityId, recordId)` pair (NOT assumed to be a
|
|
10
|
+
* User — {@link resolveIdentityByEmail} honors the per-deployment `widget.identityResolution`
|
|
11
|
+
* target so a CRM `Persons` row works as well as the core `Users` row),
|
|
12
|
+
* 2. stamp the existing `LinkedEntityID/LinkedRecordID` on every conversation that shares the visitor's key
|
|
13
|
+
* in this application (back-fill), and
|
|
14
|
+
* 3. re-key the visitor's anonymous recap notes onto the resolved pair (merge), so the existing
|
|
15
|
+
* memory injector (which already filters by the PrimaryScope pair) now pulls the prior context
|
|
16
|
+
* via the resolved identity rather than the cookie chain.
|
|
17
|
+
*
|
|
18
|
+
* "Forget me" (RV5) is the inverse: archive the visitor's auto-generated memory and clear the
|
|
19
|
+
* `VisitorKey` linkage so neither the cookie nor a resolved pair resolves their trail anymore.
|
|
20
|
+
*
|
|
21
|
+
* Everything here is BEST-EFFORT and multi-provider-safe (it takes the request/session
|
|
22
|
+
* `IMetadataProvider`, never the global `Metadata`). A failure never blocks the auth flow.
|
|
23
|
+
*
|
|
24
|
+
* @module @memberjunction/server/widget
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { RunView, UserInfo, LogError, LogStatus, type IMetadataProvider } from '@memberjunction/core';
|
|
28
|
+
import type { MJConversationEntity, MJAIAgentNoteEntity } from '@memberjunction/core-entities';
|
|
29
|
+
|
|
30
|
+
const CONVERSATIONS_ENTITY = 'MJ: Conversations';
|
|
31
|
+
const AGENT_NOTES_ENTITY = 'MJ: AI Agent Notes';
|
|
32
|
+
|
|
33
|
+
/** A resolved polymorphic identity: the entity the visitor maps to, and that record's id. */
|
|
34
|
+
export interface ResolvedVisitorIdentity {
|
|
35
|
+
entityId: string;
|
|
36
|
+
recordId: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The deployment-configurable resolution target (defaults to the core `Users` entity keyed by `Email`). */
|
|
40
|
+
export interface IdentityResolutionTarget {
|
|
41
|
+
entityName?: string;
|
|
42
|
+
emailField?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolves a verified email to a polymorphic `(entityId, recordId)` pair via the deployment-configured
|
|
47
|
+
* target (default: the `Users` entity keyed by `Email`). Returns undefined when no record matches — the
|
|
48
|
+
* visitor simply stays anonymous. Never throws; email is escaped for the filter literal.
|
|
49
|
+
*/
|
|
50
|
+
export async function resolveIdentityByEmail(
|
|
51
|
+
email: string,
|
|
52
|
+
contextUser: UserInfo,
|
|
53
|
+
provider: IMetadataProvider,
|
|
54
|
+
target?: IdentityResolutionTarget,
|
|
55
|
+
): Promise<ResolvedVisitorIdentity | undefined> {
|
|
56
|
+
try {
|
|
57
|
+
const trimmed = (email ?? '').trim();
|
|
58
|
+
if (!trimmed) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const entityName = target?.entityName?.trim() || 'Users';
|
|
62
|
+
const emailField = target?.emailField?.trim() || 'Email';
|
|
63
|
+
const entityId = provider.EntityByName(entityName)?.ID;
|
|
64
|
+
if (!entityId) {
|
|
65
|
+
LogError(`[VisitorIdentity] identity-resolution entity '${entityName}' not found in metadata.`);
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const rv = new RunView();
|
|
69
|
+
const result = await rv.RunView<{ ID: string }>(
|
|
70
|
+
{
|
|
71
|
+
EntityName: entityName,
|
|
72
|
+
ExtraFilter: `${emailField} = '${trimmed.replace(/'/g, "''")}'`,
|
|
73
|
+
Fields: ['ID'],
|
|
74
|
+
MaxRows: 1,
|
|
75
|
+
ResultType: 'simple',
|
|
76
|
+
},
|
|
77
|
+
contextUser,
|
|
78
|
+
);
|
|
79
|
+
if (!result.Success) {
|
|
80
|
+
LogError(`[VisitorIdentity] identity lookup failed for '${entityName}.${emailField}': ${result.ErrorMessage}`);
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const recordId = result.Results?.[0]?.ID;
|
|
84
|
+
return recordId ? { entityId, recordId } : undefined;
|
|
85
|
+
} catch (e) {
|
|
86
|
+
LogError(`[VisitorIdentity] resolveIdentityByEmail failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Loads every conversation sharing a VisitorKey within one application (entity objects, for mutation). */
|
|
92
|
+
async function loadVisitorConversations(
|
|
93
|
+
visitorKey: string,
|
|
94
|
+
applicationId: string,
|
|
95
|
+
contextUser: UserInfo,
|
|
96
|
+
): Promise<MJConversationEntity[]> {
|
|
97
|
+
const rv = new RunView();
|
|
98
|
+
// visitorKey is validated base64url by the caller; applicationId is a server-trusted UUID.
|
|
99
|
+
const result = await rv.RunView<MJConversationEntity>(
|
|
100
|
+
{
|
|
101
|
+
EntityName: CONVERSATIONS_ENTITY,
|
|
102
|
+
ExtraFilter: `VisitorKey = '${visitorKey}' AND ApplicationID = '${applicationId.replace(/'/g, "''")}'`,
|
|
103
|
+
ResultType: 'entity_object',
|
|
104
|
+
},
|
|
105
|
+
contextUser,
|
|
106
|
+
);
|
|
107
|
+
if (!result.Success) {
|
|
108
|
+
LogError(`[VisitorIdentity] failed to load visitor conversations: ${result.ErrorMessage}`);
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
return result.Results ?? [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Promotes a visitor's anonymous trail to a resolved identity (RV4): stamps the resolved pair on every
|
|
116
|
+
* conversation sharing the visitor's key in this application, then re-keys the visitor's anonymous
|
|
117
|
+
* recap notes (those filed under `(MJ: Conversations, conversationId)`) onto the resolved pair. Idempotent
|
|
118
|
+
* and best-effort — already-resolved conversations are skipped; per-record save failures are logged, not thrown.
|
|
119
|
+
*
|
|
120
|
+
* @returns the number of conversations stamped (0 when nothing matched the key).
|
|
121
|
+
*/
|
|
122
|
+
export async function mergeVisitorIdentity(args: {
|
|
123
|
+
visitorKey: string;
|
|
124
|
+
applicationId: string;
|
|
125
|
+
identity: ResolvedVisitorIdentity;
|
|
126
|
+
contextUser: UserInfo;
|
|
127
|
+
provider: IMetadataProvider;
|
|
128
|
+
}): Promise<number> {
|
|
129
|
+
try {
|
|
130
|
+
const conversations = await loadVisitorConversations(args.visitorKey, args.applicationId, args.contextUser);
|
|
131
|
+
if (conversations.length === 0) {
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
const conversationsEntityId = args.provider.EntityByName(CONVERSATIONS_ENTITY)?.ID;
|
|
135
|
+
|
|
136
|
+
let stamped = 0;
|
|
137
|
+
for (const convo of conversations) {
|
|
138
|
+
if (convo.LinkedEntityID === args.identity.entityId && convo.LinkedRecordID === args.identity.recordId) {
|
|
139
|
+
continue; // already resolved to this identity
|
|
140
|
+
}
|
|
141
|
+
convo.LinkedEntityID = args.identity.entityId;
|
|
142
|
+
convo.LinkedRecordID = args.identity.recordId;
|
|
143
|
+
if (await convo.Save()) {
|
|
144
|
+
stamped++;
|
|
145
|
+
} else {
|
|
146
|
+
LogError(`[VisitorIdentity] failed to stamp resolved identity on conversation ${convo.ID}: ${convo.LatestResult?.CompleteMessage ?? 'unknown'}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (conversationsEntityId) {
|
|
151
|
+
await rekeyAnonymousNotes(conversations.map((c) => c.ID), conversationsEntityId, args.identity, args.contextUser, args.provider);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
LogStatus(`[VisitorIdentity] merged visitor ${args.visitorKey} → ${args.identity.entityId}/${args.identity.recordId} across ${stamped} conversation(s)`);
|
|
155
|
+
return stamped;
|
|
156
|
+
} catch (e) {
|
|
157
|
+
LogError(`[VisitorIdentity] mergeVisitorIdentity failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Re-keys notes scoped to `(Conversations entity, conversationId)` onto the resolved pair. */
|
|
163
|
+
async function rekeyAnonymousNotes(
|
|
164
|
+
conversationIds: string[],
|
|
165
|
+
conversationsEntityId: string,
|
|
166
|
+
identity: ResolvedVisitorIdentity,
|
|
167
|
+
contextUser: UserInfo,
|
|
168
|
+
provider: IMetadataProvider,
|
|
169
|
+
): Promise<void> {
|
|
170
|
+
if (conversationIds.length === 0) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const inList = conversationIds.map((id) => `'${id.replace(/'/g, "''")}'`).join(', ');
|
|
174
|
+
const rv = new RunView();
|
|
175
|
+
const result = await rv.RunView<MJAIAgentNoteEntity>(
|
|
176
|
+
{
|
|
177
|
+
EntityName: AGENT_NOTES_ENTITY,
|
|
178
|
+
ExtraFilter: `PrimaryScopeEntityID = '${conversationsEntityId}' AND PrimaryScopeRecordID IN (${inList})`,
|
|
179
|
+
ResultType: 'entity_object',
|
|
180
|
+
},
|
|
181
|
+
contextUser,
|
|
182
|
+
);
|
|
183
|
+
if (!result.Success) {
|
|
184
|
+
LogError(`[VisitorIdentity] failed to load anonymous notes for re-key: ${result.ErrorMessage}`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
for (const note of result.Results ?? []) {
|
|
188
|
+
note.PrimaryScopeEntityID = identity.entityId;
|
|
189
|
+
note.PrimaryScopeRecordID = identity.recordId;
|
|
190
|
+
if (!(await note.Save())) {
|
|
191
|
+
LogError(`[VisitorIdentity] failed to re-key note ${note.ID}: ${note.LatestResult?.CompleteMessage ?? 'unknown'}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* "Forget me" (RV5): archives the visitor's auto-generated memory and severs the cookie linkage.
|
|
198
|
+
* Archives every auto-generated note whose `SourceConversationID` is one of the visitor's conversations
|
|
199
|
+
* (so it reaps recaps whether they're still anonymous-scoped or already merged onto a resolved record —
|
|
200
|
+
* without touching unrelated memory on a shared resolved record), then clears `VisitorKey` on those
|
|
201
|
+
* conversations so neither the cookie nor a resolved pair re-resolves the trail. Best-effort.
|
|
202
|
+
*
|
|
203
|
+
* @returns a summary of what was archived/cleared.
|
|
204
|
+
*/
|
|
205
|
+
export async function forgetVisitor(args: {
|
|
206
|
+
visitorKey: string;
|
|
207
|
+
applicationId: string;
|
|
208
|
+
contextUser: UserInfo;
|
|
209
|
+
provider: IMetadataProvider;
|
|
210
|
+
}): Promise<{ notesArchived: number; conversationsCleared: number }> {
|
|
211
|
+
let notesArchived = 0;
|
|
212
|
+
let conversationsCleared = 0;
|
|
213
|
+
try {
|
|
214
|
+
const conversations = await loadVisitorConversations(args.visitorKey, args.applicationId, args.contextUser);
|
|
215
|
+
if (conversations.length === 0) {
|
|
216
|
+
return { notesArchived, conversationsCleared };
|
|
217
|
+
}
|
|
218
|
+
const inList = conversations.map((c) => `'${c.ID.replace(/'/g, "''")}'`).join(', ');
|
|
219
|
+
|
|
220
|
+
// Archive the visitor's auto-generated recaps, keyed by source conversation (scope-agnostic).
|
|
221
|
+
const rv = new RunView();
|
|
222
|
+
const notes = await rv.RunView<MJAIAgentNoteEntity>(
|
|
223
|
+
{
|
|
224
|
+
EntityName: AGENT_NOTES_ENTITY,
|
|
225
|
+
ExtraFilter: `IsAutoGenerated = 1 AND SourceConversationID IN (${inList}) AND Status != 'Archived'`,
|
|
226
|
+
ResultType: 'entity_object',
|
|
227
|
+
},
|
|
228
|
+
args.contextUser,
|
|
229
|
+
);
|
|
230
|
+
if (notes.Success) {
|
|
231
|
+
for (const note of notes.Results ?? []) {
|
|
232
|
+
note.Status = 'Archived';
|
|
233
|
+
if (await note.Save()) {
|
|
234
|
+
notesArchived++;
|
|
235
|
+
} else {
|
|
236
|
+
LogError(`[VisitorIdentity] failed to archive note ${note.ID}: ${note.LatestResult?.CompleteMessage ?? 'unknown'}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
LogError(`[VisitorIdentity] failed to load notes for forget: ${notes.ErrorMessage}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Sever the cookie linkage: clear VisitorKey so the key no longer resolves these conversations.
|
|
244
|
+
for (const convo of conversations) {
|
|
245
|
+
convo.VisitorKey = null;
|
|
246
|
+
if (await convo.Save()) {
|
|
247
|
+
conversationsCleared++;
|
|
248
|
+
} else {
|
|
249
|
+
LogError(`[VisitorIdentity] failed to clear VisitorKey on conversation ${convo.ID}: ${convo.LatestResult?.CompleteMessage ?? 'unknown'}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
LogStatus(`[VisitorIdentity] forgot visitor ${args.visitorKey}: archived ${notesArchived} note(s), cleared ${conversationsCleared} conversation link(s)`);
|
|
254
|
+
} catch (e) {
|
|
255
|
+
LogError(`[VisitorIdentity] forgetVisitor failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
256
|
+
}
|
|
257
|
+
return { notesArchived, conversationsCleared };
|
|
258
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure functional core for the public web widget guest-session
|
|
3
|
+
* mint — no DB, no MJ runtime, no Express. Deterministic given inputs (modulo
|
|
4
|
+
* the session-id randomness it delegates to magicLinkCore) and unit-testable
|
|
5
|
+
* with plain assertions. Mirrors the magic-link `magicLinkCore.ts` split.
|
|
6
|
+
*
|
|
7
|
+
* The widget reuses the magic-link RS256 mint + `anonymous-embed` synthesis. This
|
|
8
|
+
* module owns only the widget-specific pure logic: origin-allowlist enforcement,
|
|
9
|
+
* widget-instance eligibility, and assembling the guest claims (anonymous + the
|
|
10
|
+
* additive `mj_widget_id` claim) on top of magic-link's `buildSessionClaims`.
|
|
11
|
+
*
|
|
12
|
+
* @module @memberjunction/server/widget
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { buildSessionClaims } from '../auth/magicLink/magicLinkCore.js';
|
|
16
|
+
import type { MagicLinkJWTClaims } from '../auth/magicLink/types.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The magic-link resource-scope `resourceType` used for a widget guest session. Pairs with the
|
|
20
|
+
* opaque session id as `resourceId` so the synthesized principal's MagicLinkScope.ResourceID is
|
|
21
|
+
* the session id — the discriminator the Widget Guest RLS filters key on for per-session isolation.
|
|
22
|
+
*/
|
|
23
|
+
export const WIDGET_SESSION_RESOURCE_TYPE = 'Widget Session';
|
|
24
|
+
|
|
25
|
+
/** Why a guest-session mint was rejected. Drives the HTTP status in the router. */
|
|
26
|
+
export type WidgetMintErrorCode =
|
|
27
|
+
| 'not_found'
|
|
28
|
+
| 'disabled'
|
|
29
|
+
| 'origin_not_allowed'
|
|
30
|
+
| 'modality_not_enabled'
|
|
31
|
+
| 'host_assertion_invalid'
|
|
32
|
+
| 'upgrade_not_enabled'
|
|
33
|
+
| 'invalid_email'
|
|
34
|
+
| 'server_error';
|
|
35
|
+
|
|
36
|
+
/** The minimal widget-instance shape the pure validators need (a subset of the entity). */
|
|
37
|
+
export interface WidgetInstanceEligibilityInput {
|
|
38
|
+
Status: string;
|
|
39
|
+
AllowedOrigins: string | null;
|
|
40
|
+
Modality: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parses the widget's `AllowedOrigins` column into a normalized origin list.
|
|
45
|
+
* Accepts a JSON array (preferred, e.g. `["https://a.com","https://b.com"]`) or a
|
|
46
|
+
* comma-separated string (tolerated). Returns an empty array for null/blank/garbage
|
|
47
|
+
* — which makes the allowlist FAIL-CLOSED (no origin is allowed) by construction.
|
|
48
|
+
*/
|
|
49
|
+
export function parseAllowedOrigins(allowedOrigins: string | null | undefined): string[] {
|
|
50
|
+
const raw = allowedOrigins?.trim();
|
|
51
|
+
if (!raw) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
let list: unknown;
|
|
55
|
+
try {
|
|
56
|
+
list = JSON.parse(raw);
|
|
57
|
+
} catch {
|
|
58
|
+
// Not JSON — tolerate a CSV form.
|
|
59
|
+
return raw.split(',').map((s) => normalizeOrigin(s)).filter((s): s is string => !!s);
|
|
60
|
+
}
|
|
61
|
+
if (!Array.isArray(list)) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
return list.filter((s): s is string => typeof s === 'string').map(normalizeOrigin).filter((s): s is string => !!s);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Lowercases + trims an origin and strips a trailing slash for stable comparison. */
|
|
68
|
+
function normalizeOrigin(origin: string): string {
|
|
69
|
+
return origin.trim().toLowerCase().replace(/\/+$/, '');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parses the widget's `EnabledChannels` column into a clean list of channel names (Phase 2). Accepts a
|
|
74
|
+
* JSON array (preferred, e.g. `["Whiteboard"]`) or a comma-separated string (tolerated). Returns an
|
|
75
|
+
* empty array for null/blank/garbage — the backwards-compatible default (no channels attached).
|
|
76
|
+
*/
|
|
77
|
+
export function parseEnabledChannels(enabledChannels: string | null | undefined): string[] {
|
|
78
|
+
const raw = enabledChannels?.trim();
|
|
79
|
+
if (!raw) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
const clean = (list: unknown[]): string[] =>
|
|
83
|
+
list.filter((s): s is string => typeof s === 'string').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(raw);
|
|
86
|
+
return Array.isArray(parsed) ? clean(parsed) : [];
|
|
87
|
+
} catch {
|
|
88
|
+
return clean(raw.split(','));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* FAIL-CLOSED origin check. The request's `Origin` header must exactly match one of
|
|
94
|
+
* the widget's allowed origins (after normalization). A missing request origin, or
|
|
95
|
+
* an empty allowlist, is rejected — a public mint endpoint must never accept "*".
|
|
96
|
+
*/
|
|
97
|
+
export function isOriginAllowed(requestOrigin: string | null | undefined, allowedOrigins: readonly string[]): boolean {
|
|
98
|
+
if (!requestOrigin || allowedOrigins.length === 0) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const candidate = normalizeOrigin(requestOrigin);
|
|
102
|
+
return allowedOrigins.some((o) => o === candidate);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** True when the requested modality (or the widget's default render) is enabled by the instance. */
|
|
106
|
+
export function isModalityEnabled(widgetModality: string, requested: 'Text' | 'Voice'): boolean {
|
|
107
|
+
const m = widgetModality.trim().toLowerCase();
|
|
108
|
+
if (m === 'both') {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return m === requested.toLowerCase();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Pure eligibility check for a mint: the widget must be Active and the request's
|
|
116
|
+
* origin must be on the allowlist. Returns an error code on the first failure so the
|
|
117
|
+
* router can map it to a precise HTTP status. Modality is checked separately by the
|
|
118
|
+
* voice path (W4) since a text mint is always permitted for an enabled widget.
|
|
119
|
+
*/
|
|
120
|
+
export function evaluateWidgetMint(
|
|
121
|
+
widget: WidgetInstanceEligibilityInput,
|
|
122
|
+
requestOrigin: string | null | undefined,
|
|
123
|
+
): { ok: boolean; errorCode?: WidgetMintErrorCode } {
|
|
124
|
+
if (widget.Status.trim().toLowerCase() !== 'active') {
|
|
125
|
+
return { ok: false, errorCode: 'disabled' };
|
|
126
|
+
}
|
|
127
|
+
if (!isOriginAllowed(requestOrigin, parseAllowedOrigins(widget.AllowedOrigins))) {
|
|
128
|
+
return { ok: false, errorCode: 'origin_not_allowed' };
|
|
129
|
+
}
|
|
130
|
+
return { ok: true };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Known automated-client User-Agent substrings (lowercased). A public mint endpoint is a cheap
|
|
135
|
+
* target for crawlers and scripted abuse; this catches the obvious non-browser callers. It is a
|
|
136
|
+
* coarse FIRST line of defense (W6 "basic behavioural and user-agent checks"), NOT a CAPTCHA — the
|
|
137
|
+
* real boundaries remain the origin allowlist, rate limits, the restricted guest role, and short
|
|
138
|
+
* token TTLs. Legitimate headless integrations should embed via host-identity, not the public mint.
|
|
139
|
+
*/
|
|
140
|
+
const BOT_USER_AGENT_MARKERS = [
|
|
141
|
+
'bot', 'crawler', 'spider', 'scrape', 'curl', 'wget', 'python-requests',
|
|
142
|
+
'httpclient', 'okhttp', 'java/', 'go-http-client', 'libwww', 'headlesschrome', 'phantomjs',
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Coarse bot heuristic for the public mint route. Returns `true` when the request should be treated as
|
|
147
|
+
* an automated client: a missing/blank User-Agent (real browsers always send one) or a UA containing a
|
|
148
|
+
* known automation marker. Deliberately conservative to avoid false positives on real browsers; pair
|
|
149
|
+
* with the origin allowlist + rate limits, which are the hard controls.
|
|
150
|
+
*/
|
|
151
|
+
export function looksLikeBot(userAgent: string | null | undefined): boolean {
|
|
152
|
+
const ua = (userAgent ?? '').trim().toLowerCase();
|
|
153
|
+
if (!ua) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
return BOT_USER_AGENT_MARKERS.some((marker) => ua.includes(marker));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Builds the guest-session JWT claims for a widget visitor. Reuses magic-link's
|
|
161
|
+
* `buildSessionClaims` (anonymous mode → the shared Anonymous principal + claims-based
|
|
162
|
+
* role synthesis) and layers on the additive `mj_widget_id` claim so the synthesized
|
|
163
|
+
* principal is locked to one widget instance. The role name carried here is the
|
|
164
|
+
* widget's restricted guest role (D5 backstop).
|
|
165
|
+
*/
|
|
166
|
+
export function buildWidgetGuestClaims(args: {
|
|
167
|
+
issuer: string;
|
|
168
|
+
audience: string;
|
|
169
|
+
widgetId: string;
|
|
170
|
+
sessionId: string;
|
|
171
|
+
anonymousEmail: string;
|
|
172
|
+
applicationId: string;
|
|
173
|
+
guestRoleName: string;
|
|
174
|
+
nowSeconds: number;
|
|
175
|
+
ttlSeconds: number;
|
|
176
|
+
/**
|
|
177
|
+
* Host-asserted identity for a `host-identity` session. Its email/name are carried as
|
|
178
|
+
* INFORMATIONAL claims only — the principal-resolving `email`/`sub` remain the shared
|
|
179
|
+
* Anonymous principal, so a host can never escalate a guest into a real account.
|
|
180
|
+
*/
|
|
181
|
+
hostIdentity?: { email: string; firstName?: string; lastName?: string };
|
|
182
|
+
/**
|
|
183
|
+
* Returning-visitor anchor + linked identity (RV1/RV2/RV4) carried as claims so the VOICE path
|
|
184
|
+
* (server-created conversation) stamps the same fields the text path stamps client-side. Set only
|
|
185
|
+
* when the widget remembers returning visitors. Omitted ⇒ no returning-visitor claims (default off).
|
|
186
|
+
*/
|
|
187
|
+
returningVisitor?: {
|
|
188
|
+
visitorKey?: string;
|
|
189
|
+
lastConversationId?: string;
|
|
190
|
+
linkedEntityId?: string;
|
|
191
|
+
linkedRecordId?: string;
|
|
192
|
+
};
|
|
193
|
+
}): MagicLinkJWTClaims {
|
|
194
|
+
const claims = buildSessionClaims({
|
|
195
|
+
issuer: args.issuer,
|
|
196
|
+
audience: args.audience,
|
|
197
|
+
// No invite row for a direct-mint guest; the opaque session id stands in as the
|
|
198
|
+
// subject discriminator (sub = `magic-link|<sessionId>`) and scope-entry id.
|
|
199
|
+
inviteId: args.sessionId,
|
|
200
|
+
// ALWAYS the Anonymous principal email — never the host-provided one (that would let
|
|
201
|
+
// the auth middleware resolve to a real user and leak its permissions).
|
|
202
|
+
email: args.anonymousEmail,
|
|
203
|
+
applicationId: args.applicationId,
|
|
204
|
+
roleName: args.guestRoleName,
|
|
205
|
+
anonymous: true,
|
|
206
|
+
sessionId: args.sessionId,
|
|
207
|
+
// Per-session resource scope → MagicLinkScope.ResourceID on the synthesized principal.
|
|
208
|
+
// This is what the Widget Guest RLS filters key on ({{ScopeResourceID}}) to isolate one
|
|
209
|
+
// anonymous guest's conversations from another's despite the shared Anonymous UserID.
|
|
210
|
+
// It rides the SIGNED token, so a guest cannot forge another session's scope.
|
|
211
|
+
resourceType: WIDGET_SESSION_RESOURCE_TYPE,
|
|
212
|
+
resourceId: args.sessionId,
|
|
213
|
+
nowSeconds: args.nowSeconds,
|
|
214
|
+
ttlSeconds: args.ttlSeconds,
|
|
215
|
+
});
|
|
216
|
+
// Additive widget claim — lets the synthesized principal be bound to one widget.
|
|
217
|
+
claims.mj_widget_id = args.widgetId;
|
|
218
|
+
if (args.hostIdentity) {
|
|
219
|
+
claims.given_name = args.hostIdentity.firstName ?? claims.given_name;
|
|
220
|
+
claims.family_name = args.hostIdentity.lastName ?? claims.family_name;
|
|
221
|
+
claims.name = [args.hostIdentity.firstName, args.hostIdentity.lastName].filter(Boolean).join(' ') || claims.name;
|
|
222
|
+
claims.mj_host_email = args.hostIdentity.email;
|
|
223
|
+
}
|
|
224
|
+
if (args.returningVisitor) {
|
|
225
|
+
claims.mj_visitor_key = args.returningVisitor.visitorKey;
|
|
226
|
+
claims.mj_last_conversation_id = args.returningVisitor.lastConversationId;
|
|
227
|
+
claims.mj_linked_entity_id = args.returningVisitor.linkedEntityId;
|
|
228
|
+
claims.mj_linked_record_id = args.returningVisitor.linkedRecordId;
|
|
229
|
+
}
|
|
230
|
+
return claims;
|
|
231
|
+
}
|