@memberjunction/server 5.44.0 → 5.45.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agentSessions/ReturningVisitorRecap.d.ts +39 -0
- package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -0
- package/dist/agentSessions/ReturningVisitorRecap.js +198 -0
- package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -0
- package/dist/agentSessions/SessionJanitor.d.ts +13 -0
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +65 -7
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +37 -0
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.d.ts +10 -0
- package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.js +16 -0
- package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts +7 -0
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +13 -3
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/magicLink/index.d.ts +1 -1
- package/dist/auth/magicLink/index.d.ts.map +1 -1
- package/dist/auth/magicLink/index.js +1 -1
- package/dist/auth/magicLink/index.js.map +1 -1
- package/dist/auth/magicLink/magicLinkCore.d.ts +38 -1
- package/dist/auth/magicLink/magicLinkCore.d.ts.map +1 -1
- package/dist/auth/magicLink/magicLinkCore.js +42 -19
- package/dist/auth/magicLink/magicLinkCore.js.map +1 -1
- package/dist/auth/magicLink/types.d.ts +26 -0
- package/dist/auth/magicLink/types.d.ts.map +1 -1
- package/dist/config.d.ts +1883 -144
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +160 -36
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +45 -2
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +522 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +2860 -0
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +13 -0
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +22 -0
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +4 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +101 -3
- package/dist/index.js.map +1 -1
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
- package/dist/integration/CustomColumnPromoter.js +6 -0
- package/dist/integration/CustomColumnPromoter.js.map +1 -1
- package/dist/realtimeWidget/WidgetRouter.d.ts +26 -0
- package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -0
- package/dist/realtimeWidget/WidgetRouter.js +182 -0
- package/dist/realtimeWidget/WidgetRouter.js.map +1 -0
- package/dist/realtimeWidget/WidgetSessionService.d.ts +265 -0
- package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -0
- package/dist/realtimeWidget/WidgetSessionService.js +477 -0
- package/dist/realtimeWidget/WidgetSessionService.js.map +1 -0
- package/dist/realtimeWidget/host-identity.d.ts +40 -0
- package/dist/realtimeWidget/host-identity.d.ts.map +1 -0
- package/dist/realtimeWidget/host-identity.js +58 -0
- package/dist/realtimeWidget/host-identity.js.map +1 -0
- package/dist/realtimeWidget/index.d.ts +10 -0
- package/dist/realtimeWidget/index.d.ts.map +1 -0
- package/dist/realtimeWidget/index.js +9 -0
- package/dist/realtimeWidget/index.js.map +1 -0
- package/dist/realtimeWidget/visitorIdentity.d.ts +76 -0
- package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -0
- package/dist/realtimeWidget/visitorIdentity.js +202 -0
- package/dist/realtimeWidget/visitorIdentity.js.map +1 -0
- package/dist/realtimeWidget/widgetCore.d.ts +106 -0
- package/dist/realtimeWidget/widgetCore.d.ts.map +1 -0
- package/dist/realtimeWidget/widgetCore.js +173 -0
- package/dist/realtimeWidget/widgetCore.js.map +1 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +51 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -0
- package/dist/realtimeWidget/widgetGuestElevation.js +79 -0
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -0
- package/dist/resolvers/ComponentRegistryResolver.d.ts +7 -0
- package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +31 -8
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/EntityPermissionResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityPermissionResolver.js +1 -2
- package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +9 -6
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +15 -13
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +47 -3
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RingCentralTelephonyResolver.d.ts +27 -0
- package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +1 -0
- package/dist/resolvers/RingCentralTelephonyResolver.js +87 -0
- package/dist/resolvers/RingCentralTelephonyResolver.js.map +1 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts +9 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +20 -4
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +2 -0
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/dist/resolvers/TeamsMeetingsResolver.d.ts +28 -0
- package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +1 -0
- package/dist/resolvers/TeamsMeetingsResolver.js +91 -0
- package/dist/resolvers/TeamsMeetingsResolver.js.map +1 -0
- package/dist/resolvers/TelephonyResolver.d.ts +26 -0
- package/dist/resolvers/TelephonyResolver.d.ts.map +1 -0
- package/dist/resolvers/TelephonyResolver.js +86 -0
- package/dist/resolvers/TelephonyResolver.js.map +1 -0
- package/dist/resolvers/TestQuerySQLResolver.d.ts +1 -1
- package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.js +2 -3
- package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
- package/dist/resolvers/VonageTelephonyResolver.d.ts +26 -0
- package/dist/resolvers/VonageTelephonyResolver.d.ts.map +1 -0
- package/dist/resolvers/VonageTelephonyResolver.js +86 -0
- package/dist/resolvers/VonageTelephonyResolver.js.map +1 -0
- package/dist/rest/MediaStreamHandler.js +4 -1
- package/dist/rest/MediaStreamHandler.js.map +1 -1
- package/dist/telephony/RingCentralTelephonyService.d.ts +115 -0
- package/dist/telephony/RingCentralTelephonyService.d.ts.map +1 -0
- package/dist/telephony/RingCentralTelephonyService.js +262 -0
- package/dist/telephony/RingCentralTelephonyService.js.map +1 -0
- package/dist/telephony/TeamsMeetingsRouter.d.ts +40 -0
- package/dist/telephony/TeamsMeetingsRouter.d.ts.map +1 -0
- package/dist/telephony/TeamsMeetingsRouter.js +96 -0
- package/dist/telephony/TeamsMeetingsRouter.js.map +1 -0
- package/dist/telephony/TeamsMeetingsService.d.ts +113 -0
- package/dist/telephony/TeamsMeetingsService.d.ts.map +1 -0
- package/dist/telephony/TeamsMeetingsService.js +196 -0
- package/dist/telephony/TeamsMeetingsService.js.map +1 -0
- package/dist/telephony/TwilioTelephonyRouter.d.ts +36 -0
- package/dist/telephony/TwilioTelephonyRouter.d.ts.map +1 -0
- package/dist/telephony/TwilioTelephonyRouter.js +143 -0
- package/dist/telephony/TwilioTelephonyRouter.js.map +1 -0
- package/dist/telephony/TwilioTelephonyService.d.ts +95 -0
- package/dist/telephony/TwilioTelephonyService.d.ts.map +1 -0
- package/dist/telephony/TwilioTelephonyService.js +193 -0
- package/dist/telephony/TwilioTelephonyService.js.map +1 -0
- package/dist/telephony/VonageTelephonyRouter.d.ts +41 -0
- package/dist/telephony/VonageTelephonyRouter.d.ts.map +1 -0
- package/dist/telephony/VonageTelephonyRouter.js +201 -0
- package/dist/telephony/VonageTelephonyRouter.js.map +1 -0
- package/dist/telephony/VonageTelephonyService.d.ts +90 -0
- package/dist/telephony/VonageTelephonyService.d.ts.map +1 -0
- package/dist/telephony/VonageTelephonyService.js +191 -0
- package/dist/telephony/VonageTelephonyService.js.map +1 -0
- package/dist/telephony/calendar-scheduler.d.ts +67 -0
- package/dist/telephony/calendar-scheduler.d.ts.map +1 -0
- package/dist/telephony/calendar-scheduler.js +133 -0
- package/dist/telephony/calendar-scheduler.js.map +1 -0
- package/dist/telephony/index.d.ts +29 -0
- package/dist/telephony/index.d.ts.map +1 -0
- package/dist/telephony/index.js +38 -0
- package/dist/telephony/index.js.map +1 -0
- package/dist/telephony/media-upgrade-router.d.ts +33 -0
- package/dist/telephony/media-upgrade-router.d.ts.map +1 -0
- package/dist/telephony/media-upgrade-router.js +56 -0
- package/dist/telephony/media-upgrade-router.js.map +1 -0
- package/dist/telephony/ringcentral-runtime.d.ts +14 -0
- package/dist/telephony/ringcentral-runtime.d.ts.map +1 -0
- package/dist/telephony/ringcentral-runtime.js +18 -0
- package/dist/telephony/ringcentral-runtime.js.map +1 -0
- package/dist/telephony/teams-meetings-runtime.d.ts +16 -0
- package/dist/telephony/teams-meetings-runtime.d.ts.map +1 -0
- package/dist/telephony/teams-meetings-runtime.js +20 -0
- package/dist/telephony/teams-meetings-runtime.js.map +1 -0
- package/dist/telephony/teamsAcsMediaRegistry.d.ts +69 -0
- package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +1 -0
- package/dist/telephony/teamsAcsMediaRegistry.js +118 -0
- package/dist/telephony/teamsAcsMediaRegistry.js.map +1 -0
- package/dist/telephony/telephony-runtime.d.ts +14 -0
- package/dist/telephony/telephony-runtime.d.ts.map +1 -0
- package/dist/telephony/telephony-runtime.js +18 -0
- package/dist/telephony/telephony-runtime.js.map +1 -0
- package/dist/telephony/twilioMediaRegistry.d.ts +60 -0
- package/dist/telephony/twilioMediaRegistry.d.ts.map +1 -0
- package/dist/telephony/twilioMediaRegistry.js +106 -0
- package/dist/telephony/twilioMediaRegistry.js.map +1 -0
- package/dist/telephony/vonage-runtime.d.ts +14 -0
- package/dist/telephony/vonage-runtime.d.ts.map +1 -0
- package/dist/telephony/vonage-runtime.js +18 -0
- package/dist/telephony/vonage-runtime.js.map +1 -0
- package/dist/telephony/vonageMediaRegistry.d.ts +78 -0
- package/dist/telephony/vonageMediaRegistry.d.ts.map +1 -0
- package/dist/telephony/vonageMediaRegistry.js +158 -0
- package/dist/telephony/vonageMediaRegistry.js.map +1 -0
- package/package.json +88 -83
- package/src/__tests__/RunAIAgentResolver.streaming.test.ts +128 -0
- package/src/__tests__/magicLink.test.ts +53 -3
- package/src/__tests__/search-knowledge-tags.test.ts +9 -9
- package/src/__tests__/teams-meetings-service.test.ts +65 -0
- package/src/__tests__/teamsAcsMediaRegistry.test.ts +82 -0
- package/src/__tests__/twilio-telephony-service.test.ts +23 -0
- package/src/__tests__/twilioMediaRegistry.test.ts +103 -0
- package/src/__tests__/vonageMediaRegistry.test.ts +180 -0
- package/src/__tests__/widget.test.ts +204 -0
- package/src/__tests__/widgetGuestElevation.test.ts +57 -0
- package/src/__tests__/widgetHostIdentity.test.ts +117 -0
- package/src/agentSessions/ReturningVisitorRecap.ts +242 -0
- package/src/agentSessions/SessionJanitor.ts +66 -6
- package/src/agentSessions/SessionManager.ts +38 -0
- package/src/auth/magicLink/MagicLinkRouter.ts +17 -0
- package/src/auth/magicLink/MagicLinkService.ts +13 -3
- package/src/auth/magicLink/index.ts +1 -0
- package/src/auth/magicLink/magicLinkCore.ts +48 -20
- package/src/auth/magicLink/types.ts +26 -0
- package/src/config.ts +172 -38
- package/src/context.ts +50 -3
- package/src/generated/generated.ts +1991 -1
- package/src/generic/ResolverBase.ts +28 -0
- package/src/generic/RunViewResolver.ts +4 -0
- package/src/index.ts +109 -3
- package/src/integration/CustomColumnPromoter.ts +6 -0
- package/src/realtimeWidget/WidgetRouter.ts +204 -0
- package/src/realtimeWidget/WidgetSessionService.ts +714 -0
- package/src/realtimeWidget/host-identity.ts +84 -0
- package/src/realtimeWidget/index.ts +15 -0
- package/src/realtimeWidget/visitorIdentity.ts +258 -0
- package/src/realtimeWidget/widgetCore.ts +231 -0
- package/src/realtimeWidget/widgetGuestElevation.ts +115 -0
- package/src/resolvers/ComponentRegistryResolver.ts +36 -10
- package/src/resolvers/EntityPermissionResolver.ts +1 -2
- package/src/resolvers/QuerySystemUserResolver.ts +14 -10
- package/src/resolvers/RealtimeClientSessionResolver.ts +59 -2
- package/src/resolvers/RingCentralTelephonyResolver.ts +64 -0
- package/src/resolvers/RunAIAgentResolver.ts +32 -8
- package/src/resolvers/SearchKnowledgeResolver.ts +2 -0
- package/src/resolvers/TeamsMeetingsResolver.ts +68 -0
- package/src/resolvers/TelephonyResolver.ts +63 -0
- package/src/resolvers/TestQuerySQLResolver.ts +2 -3
- package/src/resolvers/VonageTelephonyResolver.ts +63 -0
- package/src/rest/MediaStreamHandler.ts +4 -1
- package/src/telephony/RingCentralTelephonyService.ts +342 -0
- package/src/telephony/TeamsMeetingsRouter.ts +124 -0
- package/src/telephony/TeamsMeetingsService.ts +268 -0
- package/src/telephony/TwilioTelephonyRouter.ts +184 -0
- package/src/telephony/TwilioTelephonyService.ts +261 -0
- package/src/telephony/VonageTelephonyRouter.ts +239 -0
- package/src/telephony/VonageTelephonyService.ts +259 -0
- package/src/telephony/calendar-scheduler.ts +176 -0
- package/src/telephony/index.ts +43 -0
- package/src/telephony/media-upgrade-router.ts +62 -0
- package/src/telephony/ringcentral-runtime.ts +22 -0
- package/src/telephony/teams-meetings-runtime.ts +24 -0
- package/src/telephony/teamsAcsMediaRegistry.ts +152 -0
- package/src/telephony/telephony-runtime.ts +22 -0
- package/src/telephony/twilioMediaRegistry.ts +137 -0
- package/src/telephony/vonage-runtime.ts +22 -0
- package/src/telephony/vonageMediaRegistry.ts +200 -0
- package/dist/agents/skip-agent.d.ts +0 -111
- package/dist/agents/skip-agent.d.ts.map +0 -1
- package/dist/agents/skip-agent.js +0 -1487
- package/dist/agents/skip-agent.js.map +0 -1
- package/dist/agents/skip-sdk.d.ts +0 -261
- package/dist/agents/skip-sdk.d.ts.map +0 -1
- package/dist/agents/skip-sdk.js +0 -909
- package/dist/agents/skip-sdk.js.map +0 -1
- package/src/agents/skip-agent.ts +0 -1594
- package/src/agents/skip-sdk.ts +0 -1210
|
@@ -194,6 +194,34 @@ export class ResolverBase {
|
|
|
194
194
|
return dataObjectArray;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Loads a single external-data-source-backed entity record by primary key and returns it in
|
|
199
|
+
* GraphQL field-name (CodeName) shape, or null if not found.
|
|
200
|
+
*
|
|
201
|
+
* External entities (`Entity.ExternalDataSourceID` set) have no MJ base view or sproc — their
|
|
202
|
+
* data is proxied live from a remote system — so the generated single-record resolver cannot run
|
|
203
|
+
* `SELECT * FROM <baseView>`. Instead it loads through a BaseEntity object, whose `InnerLoad`
|
|
204
|
+
* the data provider dispatches to the external read router's `LoadExternalRecord` (a composite-key
|
|
205
|
+
* aware, quoted, parameter-bound single-record lookup), applying the same RLS gate and field
|
|
206
|
+
* post-processing (decryption / datetime normalization) as the MJ-DB path. The caller is
|
|
207
|
+
* responsible for the `CheckUserReadPermissions` gate beforehand.
|
|
208
|
+
*/
|
|
209
|
+
protected async LoadExternalRecordByKey<T>(
|
|
210
|
+
entityName: string,
|
|
211
|
+
compositeKey: CompositeKey,
|
|
212
|
+
provider: DatabaseProviderBase,
|
|
213
|
+
userPayload: UserPayload,
|
|
214
|
+
): Promise<T | null> {
|
|
215
|
+
const contextUser = this.GetUserFromPayload(userPayload);
|
|
216
|
+
const entityObject = await provider.GetEntityObject(entityName, contextUser);
|
|
217
|
+
const loaded = await entityObject.InnerLoad(compositeKey);
|
|
218
|
+
if (!loaded) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const mapped = await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
|
|
222
|
+
return mapped as T;
|
|
223
|
+
}
|
|
224
|
+
|
|
197
225
|
/**
|
|
198
226
|
* Filters encrypted field values before sending to the API client.
|
|
199
227
|
*
|
|
@@ -715,6 +715,7 @@ export class RunViewResolver extends ResolverBase {
|
|
|
715
715
|
pubSub: PubSubEngine
|
|
716
716
|
) {
|
|
717
717
|
try {
|
|
718
|
+
await this.CheckAPIKeyScopeAuthorization('view:run', input.ViewName, userPayload);
|
|
718
719
|
const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
|
|
719
720
|
const rawData = await super.RunViewByNameGeneric(input, provider, userPayload, pubSub);
|
|
720
721
|
if (rawData === null)
|
|
@@ -746,6 +747,7 @@ export class RunViewResolver extends ResolverBase {
|
|
|
746
747
|
pubSub: PubSubEngine
|
|
747
748
|
) {
|
|
748
749
|
try {
|
|
750
|
+
await this.CheckAPIKeyScopeAuthorization('view:run', input.ViewID, userPayload);
|
|
749
751
|
const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
|
|
750
752
|
const rawData = await super.RunViewByIDGeneric(input, provider, userPayload, pubSub);
|
|
751
753
|
if (rawData === null)
|
|
@@ -777,6 +779,7 @@ export class RunViewResolver extends ResolverBase {
|
|
|
777
779
|
pubSub: PubSubEngine
|
|
778
780
|
) {
|
|
779
781
|
try {
|
|
782
|
+
await this.CheckAPIKeyScopeAuthorization('view:run', input.EntityName, userPayload);
|
|
780
783
|
const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
|
|
781
784
|
const rawData = await super.RunDynamicViewGeneric(input, provider, userPayload, pubSub);
|
|
782
785
|
if (rawData === null) return null;
|
|
@@ -806,6 +809,7 @@ export class RunViewResolver extends ResolverBase {
|
|
|
806
809
|
pubSub: PubSubEngine
|
|
807
810
|
) {
|
|
808
811
|
try {
|
|
812
|
+
await this.CheckAPIKeyScopeAuthorization('view:batch', '*', userPayload);
|
|
809
813
|
const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
|
|
810
814
|
// Note: RunViewsGeneric returns the core RunViewResult type, not the GraphQL type
|
|
811
815
|
const rawData = await super.RunViewsGeneric(input, provider, userPayload);
|
package/src/index.ts
CHANGED
|
@@ -41,7 +41,13 @@ import { setupRESTEndpoints } from './rest/setupRESTEndpoints.js';
|
|
|
41
41
|
import { createOAuthCallbackHandler } from './rest/OAuthCallbackHandler.js';
|
|
42
42
|
import { createSignatureWebhookHandler } from './rest/SignatureWebhookHandler.js';
|
|
43
43
|
import { createMediaStreamRouter } from './rest/MediaStreamHandler.js';
|
|
44
|
-
import { createMagicLinkHandler, registerMagicLinkAuthProvider, MAGIC_LINK_MOUNT_PATH } from './auth/magicLink/index.js';
|
|
44
|
+
import { createMagicLinkHandler, createMagicLinkJwksRouter, registerMagicLinkAuthProvider, MAGIC_LINK_MOUNT_PATH } from './auth/magicLink/index.js';
|
|
45
|
+
import { createWidgetHandler, WIDGET_MOUNT_PATH } from './realtimeWidget/index.js';
|
|
46
|
+
import { createTwilioTelephonyHandler, TWILIO_TELEPHONY_MOUNT_PATH, SetTwilioTelephonyService } from './telephony/index.js';
|
|
47
|
+
import { createVonageTelephonyHandler, VONAGE_TELEPHONY_MOUNT_PATH, SetVonageTelephonyService } from './telephony/index.js';
|
|
48
|
+
import { RingCentralTelephonyService, SetRingCentralTelephonyService } from './telephony/index.js';
|
|
49
|
+
import { createTeamsMeetingsHandler, TEAMS_MEETINGS_MOUNT_PATH, SetTeamsMeetingsService, GetTeamsMeetingsService, StartCalendarScheduler } from './telephony/index.js';
|
|
50
|
+
import { InstallMediaUpgradeDispatcher } from './telephony/index.js';
|
|
45
51
|
|
|
46
52
|
import { resolve } from 'node:path';
|
|
47
53
|
import { DataSourceInfo, raiseEvent } from './types.js';
|
|
@@ -132,8 +138,6 @@ export * from './generic/KeyInputOutputTypes.js';
|
|
|
132
138
|
export * from './generic/DeleteOptionsInput.js';
|
|
133
139
|
export * from './generic/RestoreContextInput.js';
|
|
134
140
|
|
|
135
|
-
export * from './agents/skip-agent.js';
|
|
136
|
-
export * from './agents/skip-sdk.js';
|
|
137
141
|
|
|
138
142
|
export * from './resolvers/GeoResolver.js';
|
|
139
143
|
export * from './resolvers/ColorResolver.js';
|
|
@@ -1004,6 +1008,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1004
1008
|
// ─── Magic-link routes (MJ-issued, app-scoped external access) ───────────
|
|
1005
1009
|
// Public router (JWKS + redeem) mounts BEFORE the auth middleware; the
|
|
1006
1010
|
// authenticated invite-creation router mounts AFTER it (see below).
|
|
1011
|
+
let widgetAuthenticatedRouter: ReturnType<typeof createWidgetHandler>['authenticatedRouter'] | undefined;
|
|
1007
1012
|
let magicLinkAuthenticatedRouter: ReturnType<typeof createMagicLinkHandler>['authenticatedRouter'] | undefined;
|
|
1008
1013
|
if (configInfo.magicLink?.enabled) {
|
|
1009
1014
|
const { publicRouter, authenticatedRouter } = createMagicLinkHandler(oauthPublicUrl, configInfo.magicLink);
|
|
@@ -1013,6 +1018,82 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1013
1018
|
startupLog.LogIf('verbose', `[MagicLink] Public routes registered at ${MAGIC_LINK_MOUNT_PATH}/redeem and ${MAGIC_LINK_MOUNT_PATH}/jwks.json`);
|
|
1014
1019
|
}
|
|
1015
1020
|
|
|
1021
|
+
// ─── Public web widget: guest-session mint (PUBLIC, before auth mw) ───────
|
|
1022
|
+
// Visitors hold no MJ JWT yet, so POST /widget/session is public. It reuses the
|
|
1023
|
+
// magic-link RS256 key + `magic-link` auth provider (ensured idempotently inside
|
|
1024
|
+
// createWidgetHandler), so it stands on its own even if magicLink.enabled is false.
|
|
1025
|
+
if (configInfo.widget?.enabled) {
|
|
1026
|
+
const { publicRouter: widgetRouter, authenticatedRouter: widgetAuthRouter } = createWidgetHandler(oauthPublicUrl, configInfo.widget);
|
|
1027
|
+
widgetAuthenticatedRouter = widgetAuthRouter;
|
|
1028
|
+
app.use(WIDGET_MOUNT_PATH, cors<cors.CorsRequest>(), widgetRouter);
|
|
1029
|
+
// The widget reuses the magic-link RS256 key + auth provider to validate guest
|
|
1030
|
+
// tokens, which validates by fetching the JWKS at MAGIC_LINK_MOUNT_PATH/jwks.json.
|
|
1031
|
+
// When magic-link itself is disabled its public router (which serves JWKS) never
|
|
1032
|
+
// mounts, so publish the key here — otherwise every guest token fails validation
|
|
1033
|
+
// (the auth middleware can't fetch the public key) and the widget 401s.
|
|
1034
|
+
if (!configInfo.magicLink?.enabled) {
|
|
1035
|
+
app.use(MAGIC_LINK_MOUNT_PATH, cors<cors.CorsRequest>(), createMagicLinkJwksRouter());
|
|
1036
|
+
startupLog.LogIf('verbose', `[Widget] Published reused signing key at ${MAGIC_LINK_MOUNT_PATH}/jwks.json (magic-link flow disabled)`);
|
|
1037
|
+
}
|
|
1038
|
+
startupLog.LogIf('verbose', `[Widget] Public routes registered at ${WIDGET_MOUNT_PATH}/session and ${WIDGET_MOUNT_PATH}/session/refresh`);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// ─── Telephony (Twilio) ingress: inbound voice webhook + Media-Streams WSS (PUBLIC) ──
|
|
1042
|
+
// Carriers cannot present an MJ JWT — the X-Twilio-Signature HMAC is the gate. The
|
|
1043
|
+
// public webhook router mounts BEFORE the auth middleware; the Media-Streams WSS attaches
|
|
1044
|
+
// to the shared HTTP server. The outbound PlaceTwilioCall mutation reuses the same service.
|
|
1045
|
+
if (configInfo.telephony?.enabled && configInfo.telephony.twilio) {
|
|
1046
|
+
const twilioHandler = createTwilioTelephonyHandler(oauthPublicUrl, configInfo.telephony.twilio);
|
|
1047
|
+
app.use(TWILIO_TELEPHONY_MOUNT_PATH, cors<cors.CorsRequest>(), twilioHandler.publicRouter);
|
|
1048
|
+
twilioHandler.attachMediaStreamServer();
|
|
1049
|
+
SetTwilioTelephonyService(twilioHandler.service);
|
|
1050
|
+
startupLog.LogIf('verbose', `[Telephony] Twilio routes registered at ${TWILIO_TELEPHONY_MOUNT_PATH}/voice + Media-Streams WSS`);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// ─── Telephony (Vonage) ingress: inbound answer/event webhooks + media WSS (PUBLIC) ──
|
|
1054
|
+
// Carriers cannot present an MJ JWT — the Vonage signed-request HMAC / webhook JWT is the gate.
|
|
1055
|
+
// The public router mounts BEFORE the auth middleware; the media WSS attaches to the shared
|
|
1056
|
+
// HTTP server. The outbound PlaceVonageCall mutation reuses the same service.
|
|
1057
|
+
if (configInfo.telephony?.enabled && configInfo.telephony.vonage) {
|
|
1058
|
+
const vonageHandler = createVonageTelephonyHandler(oauthPublicUrl, configInfo.telephony.vonage);
|
|
1059
|
+
app.use(VONAGE_TELEPHONY_MOUNT_PATH, cors<cors.CorsRequest>(), vonageHandler.publicRouter);
|
|
1060
|
+
vonageHandler.attachMediaStreamServer();
|
|
1061
|
+
SetVonageTelephonyService(vonageHandler.service);
|
|
1062
|
+
startupLog.LogIf('verbose', `[Telephony] Vonage routes registered at ${VONAGE_TELEPHONY_MOUNT_PATH}/answer + /event + media WSS`);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// ─── Telephony (RingCentral) ingress: SIP softphone registration (no HTTP webhook / media WSS) ──
|
|
1066
|
+
// RingCentral's only bidirectional-audio transport is a registered SIP softphone — inbound calls arrive
|
|
1067
|
+
// as SIP INVITEs on its own SIP/TLS connection, so there is no public webhook or media WSS to mount.
|
|
1068
|
+
// start() registers the softphone fire-and-forget so SIP registration never blocks boot; the outbound
|
|
1069
|
+
// PlaceRingCentralCall mutation reuses the same service via the runtime holder.
|
|
1070
|
+
if (configInfo.telephony?.enabled && configInfo.telephony.ringcentral) {
|
|
1071
|
+
const ringCentralService = new RingCentralTelephonyService(configInfo.telephony.ringcentral);
|
|
1072
|
+
SetRingCentralTelephonyService(ringCentralService);
|
|
1073
|
+
void ringCentralService.start();
|
|
1074
|
+
startupLog.LogIf('verbose', `[Telephony] RingCentral SIP softphone starting (codec ${configInfo.telephony.ringcentral.codec ?? 'OPUS/16000'})`);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// ─── Teams meetings ingress: Graph change-notification webhook (PUBLIC) ──────────────
|
|
1078
|
+
// Graph cannot present an MJ JWT — the subscription validationToken handshake + the per-
|
|
1079
|
+
// notification clientState shared secret are the gate. The public webhook router mounts
|
|
1080
|
+
// BEFORE the auth middleware. The ACS application-hosted-media audio plane is owned by the
|
|
1081
|
+
// server's native ACS media adapter, which attaches transports to the shared registry
|
|
1082
|
+
// (a media WSS is not needed here). The StartTeamsMeetingSession mutation reuses the same
|
|
1083
|
+
// service via the runtime holder.
|
|
1084
|
+
if (configInfo.telephony?.enabled && configInfo.telephony.teams?.enabled) {
|
|
1085
|
+
const teamsHandler = createTeamsMeetingsHandler(configInfo.telephony.teams);
|
|
1086
|
+
app.use(TEAMS_MEETINGS_MOUNT_PATH, cors<cors.CorsRequest>(), teamsHandler.publicRouter);
|
|
1087
|
+
SetTeamsMeetingsService(teamsHandler.service);
|
|
1088
|
+
startupLog.LogIf('verbose', `[Meetings] Teams routes registered at ${TEAMS_MEETINGS_MOUNT_PATH}/notifications`);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Install the single path-routing WebSocket-upgrade dispatcher AFTER all media WSS routes have
|
|
1092
|
+
// registered. ws 8.x has each {server}-bound WebSocketServer 400 paths it doesn't own, so the GraphQL
|
|
1093
|
+
// socket and the telephony media sockets cannot coexist as separate {server} servers — this strips the
|
|
1094
|
+
// auto-listeners and routes upgrades by path. No-op when no media routes registered (telephony off).
|
|
1095
|
+
InstallMediaUpgradeDispatcher(httpServer, webSocketServer, graphqlRootPath);
|
|
1096
|
+
|
|
1016
1097
|
// ─── Global CORS (before auth so 401 responses include CORS headers) ─────
|
|
1017
1098
|
// Without this, the browser blocks 401 responses from the auth middleware
|
|
1018
1099
|
// because they lack Access-Control-Allow-Origin headers, preventing the
|
|
@@ -1073,6 +1154,14 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1073
1154
|
startupLog.LogIf('verbose', `[MagicLink] Authenticated route registered at ${MAGIC_LINK_MOUNT_PATH}/create`);
|
|
1074
1155
|
}
|
|
1075
1156
|
|
|
1157
|
+
// ─── Widget authenticated route (RV4 resolve-identity) ────────────────────
|
|
1158
|
+
// Mounts after the unified auth middleware so a verified visitor (post magic-link upgrade) can
|
|
1159
|
+
// promote their anonymous returning-visitor trail to the verified record.
|
|
1160
|
+
if (widgetAuthenticatedRouter) {
|
|
1161
|
+
app.use(WIDGET_MOUNT_PATH, cors<cors.CorsRequest>(), widgetAuthenticatedRouter);
|
|
1162
|
+
startupLog.LogIf('verbose', `[Widget] Authenticated route registered at ${WIDGET_MOUNT_PATH}/resolve-identity`);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1076
1165
|
// ─── REST API endpoints (auth already handled by unified middleware) ─────
|
|
1077
1166
|
const restApiConfig = {
|
|
1078
1167
|
enabled: configInfo.restApiOptions?.enabled ?? false,
|
|
@@ -1193,6 +1282,23 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1193
1282
|
.catch(err => console.warn(`[SessionJanitor] Startup failed: ${err}`));
|
|
1194
1283
|
}
|
|
1195
1284
|
|
|
1285
|
+
// Launch the calendar / scheduled-bridge loop (M2): poll agent calendars for meeting invites and
|
|
1286
|
+
// start due meeting bridges. Mirrors the SessionJanitor lifecycle (run-once + interval, timer
|
|
1287
|
+
// unref'd). Gated on Teams meetings being enabled (the provider whose scheduled-join is wired) and
|
|
1288
|
+
// reuses the SAME meetings service as the ingress; identities without configured calendar creds are
|
|
1289
|
+
// skipped, so this is a harmless no-op until a Graph-backed identity + token are configured.
|
|
1290
|
+
if (resumeUser && configInfo.telephony?.teams?.enabled) { // global-provider-ok: server-owned background poller under the server's provider + system user
|
|
1291
|
+
const teamsMeetingsService = GetTeamsMeetingsService();
|
|
1292
|
+
if (teamsMeetingsService) {
|
|
1293
|
+
StartCalendarScheduler({
|
|
1294
|
+
Provider: Metadata.Provider, // global-provider-ok: server-owned background poller under the server's single default provider + system user
|
|
1295
|
+
ContextUser: resumeUser,
|
|
1296
|
+
TeamsService: teamsMeetingsService,
|
|
1297
|
+
TeamsConfig: configInfo.telephony.teams,
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1196
1302
|
// Set up graceful shutdown handlers
|
|
1197
1303
|
const gracefulShutdown = async (signal: string) => {
|
|
1198
1304
|
console.log(`\n${signal} received, shutting down gracefully...`);
|
|
@@ -217,6 +217,12 @@ export class IntegrationCustomColumnPromoter {
|
|
|
217
217
|
const overflowJson = await this.scanOverflow(entityName);
|
|
218
218
|
if (overflowJson.length === 0) return null; // no customs captured
|
|
219
219
|
|
|
220
|
+
// U3 note (rkihm-BC review, #3061): this in-repo promotion path passes no `LockUntilFullSync`, so it
|
|
221
|
+
// does NOT yet enforce "hold promotion until a full sync since the last schema change." The lever
|
|
222
|
+
// exists on `PromotionPlanOptions`, but pulling it here requires this caller to know whether a full
|
|
223
|
+
// sync has completed post-rediscovery — DEFERRED and tracked as a follow-up. The engine ships the
|
|
224
|
+
// gate; wiring MJServer's own consumer to set it is a separate change. Until then, this path retains
|
|
225
|
+
// the pre-U3 behavior for a rediscover-then-incremental sequence.
|
|
220
226
|
const passing = planPromotions(buildOverflowStats(overflowJson), {});
|
|
221
227
|
if (passing.length === 0) return null;
|
|
222
228
|
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Express wiring for the public web widget guest-session endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the magic-link public-router pattern: a single PUBLIC router
|
|
5
|
+
* (unauthenticated) mounted BEFORE the unified auth middleware. The widget reuses
|
|
6
|
+
* the magic-link RS256 key + the registered `magic-link` auth provider for token
|
|
7
|
+
* validation, so this module ensures both are initialized (idempotently) even when
|
|
8
|
+
* `magicLink.enabled` is false — the widget can stand on its own.
|
|
9
|
+
*
|
|
10
|
+
* @module @memberjunction/server/widget
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Router, json, type Request, type Response } from 'express';
|
|
14
|
+
import { rateLimit } from 'express-rate-limit';
|
|
15
|
+
import { LogStatus } from '@memberjunction/core';
|
|
16
|
+
import { configInfo, type WidgetConfig } from '../config.js';
|
|
17
|
+
import { MagicLinkKeyManager } from '../auth/magicLink/MagicLinkKeys.js';
|
|
18
|
+
import { registerMagicLinkAuthProvider } from '../auth/magicLink/MagicLinkRouter.js';
|
|
19
|
+
import { WidgetSessionService, type MintGuestSessionResult } from './WidgetSessionService.js';
|
|
20
|
+
import { looksLikeBot } from './widgetCore.js';
|
|
21
|
+
|
|
22
|
+
/** The mount path for the widget public router (`/widget`). */
|
|
23
|
+
export const WIDGET_MOUNT_PATH = '/widget';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ensures the magic-link signing key is initialized and the `magic-link` auth
|
|
27
|
+
* provider is registered, so widget tokens (signed with that key) validate. Safe to
|
|
28
|
+
* call when magic-link is already enabled — both operations are idempotent.
|
|
29
|
+
*/
|
|
30
|
+
function ensureWidgetSigning(publicUrl: string, widgetConfig: WidgetConfig): void {
|
|
31
|
+
const mlConfig = configInfo.magicLink;
|
|
32
|
+
// The widget shares the magic-link audience so the same provider validates its
|
|
33
|
+
// tokens; surface a clear log if a deployment diverged them.
|
|
34
|
+
if (mlConfig?.audience && mlConfig.audience !== widgetConfig.audience) {
|
|
35
|
+
LogStatus(
|
|
36
|
+
`[Widget] WARNING: widget.audience ('${widgetConfig.audience}') != magicLink.audience ('${mlConfig.audience}'). ` +
|
|
37
|
+
`Widget tokens will not validate unless an auth provider is registered for the widget audience.`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
MagicLinkKeyManager.Instance.Initialize(mlConfig?.rsaPrivateKey);
|
|
41
|
+
// Register the magic-link provider against the magic-link config (its audience),
|
|
42
|
+
// which is what validates widget tokens. No-op if already registered.
|
|
43
|
+
if (mlConfig) {
|
|
44
|
+
registerMagicLinkAuthProvider(publicUrl, mlConfig);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Builds the widget routers. Mount `publicRouter` at WIDGET_MOUNT_PATH BEFORE the unified auth
|
|
50
|
+
* middleware (the mint/forget endpoints are public — visitors have no MJ JWT yet) and
|
|
51
|
+
* `authenticatedRouter` at the same path AFTER it (the RV4 resolve-identity endpoint needs the
|
|
52
|
+
* verified principal from `req.userPayload`).
|
|
53
|
+
*/
|
|
54
|
+
export function createWidgetHandler(publicUrl: string, config: WidgetConfig): { publicRouter: Router; authenticatedRouter: Router } {
|
|
55
|
+
ensureWidgetSigning(publicUrl, config);
|
|
56
|
+
|
|
57
|
+
const service = new WidgetSessionService(publicUrl, config);
|
|
58
|
+
|
|
59
|
+
// Per-instance DYNAMIC mint limiter (W6 hardening): keyed by (widgetKey + IP) so each widget
|
|
60
|
+
// deployment gets its own bucket and one abusive IP can't exhaust a shared instance's budget for
|
|
61
|
+
// everyone. The per-request `limit` is the widget instance's stored `RateLimitPerMinute` (cached,
|
|
62
|
+
// resolved from the parsed body), falling back to the deployment-wide default for an unknown key.
|
|
63
|
+
// `json()` runs FIRST so the key generator + limit can read `req.body.widgetKey`.
|
|
64
|
+
const mintLimiter = rateLimit({
|
|
65
|
+
windowMs: config.rateLimitWindowMs,
|
|
66
|
+
standardHeaders: 'draft-7',
|
|
67
|
+
legacyHeaders: false,
|
|
68
|
+
keyGenerator: (req: Request) => `${readWidgetKey(req)}:${req.ip ?? 'noip'}`,
|
|
69
|
+
limit: async (req: Request) => service.ResolvePerInstanceRateLimit(readWidgetKey(req)),
|
|
70
|
+
message: { success: false, errorCode: 'rate_limited', error: 'Too many widget session requests. Try again later.' },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const publicRouter = Router();
|
|
74
|
+
|
|
75
|
+
publicRouter.post('/session', json(), mintLimiter, async (req: Request, res: Response) => {
|
|
76
|
+
await handleMint(service, req, res, false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
publicRouter.post('/session/refresh', json(), mintLimiter, async (req: Request, res: Response) => {
|
|
80
|
+
await handleMint(service, req, res, true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// W5 magic-link upgrade ("Verify it's you"): emails a verification link scoped to the widget's app.
|
|
84
|
+
// Same dynamic limiter as mint — it issues an email invite, a comparable cost/abuse surface.
|
|
85
|
+
publicRouter.post('/upgrade', json(), mintLimiter, async (req: Request, res: Response) => {
|
|
86
|
+
await handleUpgrade(service, req, res);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// RV5 "forget me": archive the visitor's memory + clear the VisitorKey linkage. Public — the visitor
|
|
90
|
+
// proves possession of the durable key (not an identity). Same dynamic limiter as mint.
|
|
91
|
+
publicRouter.post('/forget', json(), mintLimiter, async (req: Request, res: Response) => {
|
|
92
|
+
await handleForget(service, req, res);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// RV4 resolve-identity: promote the visitor's anonymous trail to the verified record. AUTHENTICATED —
|
|
96
|
+
// it reads the verified principal's email from req.userPayload, so it mounts after the auth middleware.
|
|
97
|
+
const authenticatedRouter = Router();
|
|
98
|
+
authenticatedRouter.post('/resolve-identity', json(), mintLimiter, async (req: Request, res: Response) => {
|
|
99
|
+
await handleResolveIdentity(service, req, res);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return { publicRouter, authenticatedRouter };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Reads the `widgetKey` from a parsed JSON body, or '' when absent (used to key the dynamic limiter). */
|
|
106
|
+
function readWidgetKey(req: Request): string {
|
|
107
|
+
const body = (req.body ?? {}) as { widgetKey?: string };
|
|
108
|
+
return typeof body.widgetKey === 'string' ? body.widgetKey.trim() : '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Handles "forget me" (RV5) — parses the body, calls the service, maps the status. */
|
|
112
|
+
async function handleForget(service: WidgetSessionService, req: Request, res: Response): Promise<void> {
|
|
113
|
+
const body = (req.body ?? {}) as { widgetKey?: string; visitorKey?: string };
|
|
114
|
+
const widgetKey = typeof body.widgetKey === 'string' ? body.widgetKey : '';
|
|
115
|
+
const visitorKey = typeof body.visitorKey === 'string' ? body.visitorKey : '';
|
|
116
|
+
if (!widgetKey) {
|
|
117
|
+
res.status(400).json({ success: false, errorCode: 'not_found', error: 'widgetKey is required.' });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const result = await service.ForgetVisitor({ widgetKey, visitorKey, origin: req.get('origin') ?? undefined });
|
|
121
|
+
res.status(result.success ? 200 : result.errorCode === 'server_error' ? 500 : 403).json(result);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Handles RV4 resolve-identity — reads the verified email from the authenticated principal
|
|
126
|
+
* (`req.userPayload.userRecord`), parses the body, calls the service, maps the status.
|
|
127
|
+
*/
|
|
128
|
+
async function handleResolveIdentity(service: WidgetSessionService, req: Request, res: Response): Promise<void> {
|
|
129
|
+
const verifiedUser = req.userPayload?.userRecord;
|
|
130
|
+
if (!verifiedUser?.Email) {
|
|
131
|
+
res.status(401).json({ success: false, errorCode: 'server_error', error: 'Authentication required.' });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const body = (req.body ?? {}) as { widgetKey?: string; visitorKey?: string };
|
|
135
|
+
const widgetKey = typeof body.widgetKey === 'string' ? body.widgetKey : '';
|
|
136
|
+
const visitorKey = typeof body.visitorKey === 'string' ? body.visitorKey : '';
|
|
137
|
+
if (!widgetKey) {
|
|
138
|
+
res.status(400).json({ success: false, errorCode: 'not_found', error: 'widgetKey is required.' });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const result = await service.ResolveVisitorIdentity({
|
|
142
|
+
widgetKey,
|
|
143
|
+
visitorKey,
|
|
144
|
+
verifiedEmail: verifiedUser.Email,
|
|
145
|
+
origin: req.get('origin') ?? undefined,
|
|
146
|
+
});
|
|
147
|
+
res.status(result.success ? 200 : result.errorCode === 'server_error' ? 500 : 403).json(result);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Handles the magic-link upgrade request — parses the body, calls the service, maps the status. */
|
|
151
|
+
async function handleUpgrade(service: WidgetSessionService, req: Request, res: Response): Promise<void> {
|
|
152
|
+
const body = (req.body ?? {}) as { widgetKey?: string; email?: string };
|
|
153
|
+
const widgetKey = typeof body.widgetKey === 'string' ? body.widgetKey : '';
|
|
154
|
+
const email = typeof body.email === 'string' ? body.email : '';
|
|
155
|
+
if (!widgetKey) {
|
|
156
|
+
res.status(400).json({ success: false, errorCode: 'not_found', error: 'widgetKey is required.' });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const result = await service.RequestUpgrade({ widgetKey, email, origin: req.get('origin') ?? undefined });
|
|
160
|
+
// 400 for a bad email (client can fix it); 403 for any policy rejection (uniform, non-enumerable); 500 for faults.
|
|
161
|
+
const status = result.success ? 200 : result.errorCode === 'invalid_email' ? 400 : result.errorCode === 'server_error' ? 500 : 403;
|
|
162
|
+
res.status(status).json(result);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Shared mint/refresh handler — parses the request, calls the service, maps the status. */
|
|
166
|
+
async function handleMint(service: WidgetSessionService, req: Request, res: Response, refresh: boolean): Promise<void> {
|
|
167
|
+
const body = (req.body ?? {}) as { widgetKey?: string; hostAssertion?: string; visitorKey?: string };
|
|
168
|
+
const widgetKey = typeof body.widgetKey === 'string' ? body.widgetKey : '';
|
|
169
|
+
if (!widgetKey) {
|
|
170
|
+
res.status(400).json({ success: false, errorCode: 'not_found', error: 'widgetKey is required.' });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// W6 hardening: coarse bot/automation rejection before any DB lookup or token mint. Returns the same
|
|
175
|
+
// 403 as a policy rejection so a probe can't distinguish "bot-blocked" from "bad key/origin".
|
|
176
|
+
if (looksLikeBot(req.get('user-agent'))) {
|
|
177
|
+
res.status(403).json({ success: false, errorCode: 'origin_not_allowed', error: 'Widget mint rejected.' });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const origin = req.get('origin') ?? undefined;
|
|
182
|
+
const input = {
|
|
183
|
+
widgetKey,
|
|
184
|
+
origin,
|
|
185
|
+
hostAssertion: typeof body.hostAssertion === 'string' ? body.hostAssertion : undefined,
|
|
186
|
+
visitorKey: typeof body.visitorKey === 'string' ? body.visitorKey : undefined,
|
|
187
|
+
audit: { ipAddress: req.ip, userAgent: req.get('user-agent') ?? undefined, origin },
|
|
188
|
+
};
|
|
189
|
+
const result = refresh ? await service.RefreshGuestSession(input) : await service.MintGuestSession(input);
|
|
190
|
+
res.status(statusForResult(result)).json(result);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Maps a mint result to an HTTP status. All client-side rejections (unknown key,
|
|
195
|
+
* disabled widget, disallowed origin, modality off) return 403 — deliberately
|
|
196
|
+
* uniform so a probe cannot distinguish "no such widget" from "wrong origin" and
|
|
197
|
+
* enumerate valid keys. Only true server faults return 500.
|
|
198
|
+
*/
|
|
199
|
+
function statusForResult(result: MintGuestSessionResult): number {
|
|
200
|
+
if (result.success) {
|
|
201
|
+
return 200;
|
|
202
|
+
}
|
|
203
|
+
return result.errorCode === 'server_error' ? 500 : 403;
|
|
204
|
+
}
|