@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
|
@@ -170,6 +170,49 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
|
|
|
170
170
|
return closed;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Server-authoritative MAX-DURATION enforcement (public web-widget voice cap, public-web-widget.md
|
|
175
|
+
* W4). Closes any `Active`/`Idle` session whose stored absolute deadline (`Config_.maxSessionDeadlineIso`)
|
|
176
|
+
* has passed — a hard wall-clock ceiling that fires regardless of activity, so a public voice guest
|
|
177
|
+
* cannot run a session indefinitely (cost-bombing) even if the client ignores its own abuse guard.
|
|
178
|
+
*
|
|
179
|
+
* Only sessions that CARRY a deadline are loaded (a cheap `Config_ LIKE` pre-filter), then the exact
|
|
180
|
+
* ISO deadline is parsed and compared in JS (`Config_` is JSON, not a queryable column). Stamped
|
|
181
|
+
* `CloseReason = 'Janitor'`, idempotent + concurrency-safe like the other sweeps. Returns the count closed.
|
|
182
|
+
*/
|
|
183
|
+
public async RunMaxDurationSweep(provider: IMetadataProvider, systemUser: UserInfo): Promise<number> {
|
|
184
|
+
const nowMs = Date.now();
|
|
185
|
+
// Only consider sessions that carry a deadline marker — keeps this sweep cheap (it never touches
|
|
186
|
+
// the far-more-common uncapped sessions) while the JS check below enforces the exact deadline.
|
|
187
|
+
const filter = `Status IN ('Active','Idle') AND Config LIKE '%maxSessionDeadlineIso%'`;
|
|
188
|
+
const isExpired = (session: MJAIAgentSessionEntity): boolean => {
|
|
189
|
+
const deadlineMs = this.parseSessionDeadlineMs(session.Config_);
|
|
190
|
+
return deadlineMs != null && deadlineMs <= nowMs;
|
|
191
|
+
};
|
|
192
|
+
const closed = await this.sweepAndClose(filter, provider, systemUser, 'Janitor', isExpired);
|
|
193
|
+
if (closed > 0) {
|
|
194
|
+
LogStatus(`[SessionJanitor] Max-duration sweep closed ${closed} session(s) past their hard duration cap`);
|
|
195
|
+
}
|
|
196
|
+
return closed;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Parses the absolute deadline (ms) from a session's `Config_` JSON, or null when absent/malformed. */
|
|
200
|
+
private parseSessionDeadlineMs(configJson: string | null | undefined): number | null {
|
|
201
|
+
if (!configJson) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const parsed = JSON.parse(configJson) as { maxSessionDeadlineIso?: string };
|
|
206
|
+
if (typeof parsed.maxSessionDeadlineIso !== 'string') {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const ms = Date.parse(parsed.maxSessionDeadlineIso);
|
|
210
|
+
return Number.isNaN(ms) ? null : ms;
|
|
211
|
+
} catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
173
216
|
// ----- internals -------------------------------------------------------------------------
|
|
174
217
|
|
|
175
218
|
/** Register for graceful shutdown exactly once. */
|
|
@@ -197,6 +240,9 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
|
|
|
197
240
|
this._sweepRunning = true;
|
|
198
241
|
try {
|
|
199
242
|
await this.RunStalenessSweep(this._provider, this._systemUser);
|
|
243
|
+
// Hard wall-clock cap (public web-widget voice). Runs alongside the idle sweep so a capped
|
|
244
|
+
// session is finalized within one sweep interval of its deadline even if it's still "active".
|
|
245
|
+
await this.RunMaxDurationSweep(this._provider, this._systemUser);
|
|
200
246
|
} catch (err) {
|
|
201
247
|
LogError(`SessionJanitor periodic sweep failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
202
248
|
} finally {
|
|
@@ -215,19 +261,33 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
|
|
|
215
261
|
provider: IMetadataProvider,
|
|
216
262
|
systemUser: UserInfo,
|
|
217
263
|
closeReason: SessionCloseReason,
|
|
264
|
+
shouldClose?: (session: MJAIAgentSessionEntity) => boolean,
|
|
218
265
|
): Promise<number> {
|
|
219
266
|
let closedCount = 0;
|
|
220
267
|
let afterKey: CompositeKey | undefined;
|
|
221
268
|
|
|
222
269
|
// eslint-disable-next-line no-constant-condition
|
|
223
270
|
while (true) {
|
|
224
|
-
const
|
|
225
|
-
if (
|
|
271
|
+
const fetched = await this.fetchPage(filter, afterKey, provider, systemUser);
|
|
272
|
+
if (fetched == null) {
|
|
226
273
|
break; // load failure already logged
|
|
227
274
|
}
|
|
228
|
-
if (
|
|
275
|
+
if (fetched.length === 0) {
|
|
229
276
|
break;
|
|
230
277
|
}
|
|
278
|
+
// Advance the keyset BEFORE the optional JS predicate narrows the page (we must page by the
|
|
279
|
+
// SQL-matched set, not the post-filtered subset, or pagination would stall/skip).
|
|
280
|
+
const lastFetchedId = fetched[fetched.length - 1].ID;
|
|
281
|
+
const fetchedCount = fetched.length;
|
|
282
|
+
// Optional in-JS narrowing (e.g. exact deadline check the SQL pre-filter can't express).
|
|
283
|
+
const page = shouldClose ? fetched.filter(shouldClose) : fetched;
|
|
284
|
+
if (page.length === 0) {
|
|
285
|
+
if (fetchedCount < SWEEP_PAGE_SIZE) {
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
afterKey = CompositeKey.FromID(lastFetchedId);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
231
291
|
// Batch-load every channel for this whole page of sessions in ONE query, then hand each
|
|
232
292
|
// session its own slice to CloseSession — avoids the N+1 channel read (one RunView per
|
|
233
293
|
// closing session) that tripped the sequential / multiple-same-entity telemetry.
|
|
@@ -248,10 +308,10 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
|
|
|
248
308
|
closedCount++;
|
|
249
309
|
}
|
|
250
310
|
}
|
|
251
|
-
if (
|
|
252
|
-
break; // partial page => end of data
|
|
311
|
+
if (fetchedCount < SWEEP_PAGE_SIZE) {
|
|
312
|
+
break; // partial page (by the SQL-matched set) => end of data
|
|
253
313
|
}
|
|
254
|
-
afterKey = CompositeKey.FromID(
|
|
314
|
+
afterKey = CompositeKey.FromID(lastFetchedId);
|
|
255
315
|
}
|
|
256
316
|
return closedCount;
|
|
257
317
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { AIAgentPermissionHelper } from '@memberjunction/ai-engine-base';
|
|
8
8
|
import { RealtimeClientSessionService, RealtimeChannelServerHost } from '@memberjunction/ai-agents';
|
|
9
9
|
import { GetHostInstanceID } from './HostInstance.js';
|
|
10
|
+
import { writeReturningVisitorRecap } from './ReturningVisitorRecap.js';
|
|
10
11
|
|
|
11
12
|
/** Entity names — centralised so the `MJ:`-prefix convention is applied in exactly one place. */
|
|
12
13
|
const SESSION_ENTITY = 'MJ: AI Agent Sessions';
|
|
@@ -160,6 +161,10 @@ export class SessionManager {
|
|
|
160
161
|
await this.disconnectChannels(agentSessionID, contextUser, provider, preloadedChannels);
|
|
161
162
|
await this.finalizeObservabilityRuns(session, contextUser, provider);
|
|
162
163
|
await this.notifyChannelPluginsSessionClosed(agentSessionID, closeReason);
|
|
164
|
+
// Returning-visitor recap (RV2): if this session's conversation is returning-visitor-enabled,
|
|
165
|
+
// summarize it into an Active memory note so the visitor's next session opens with prior context.
|
|
166
|
+
// Best-effort + no-op for non-returning-visitor conversations; never blocks teardown.
|
|
167
|
+
await writeReturningVisitorRecap(session.ConversationID, session.AgentID, contextUser, provider);
|
|
163
168
|
return true;
|
|
164
169
|
}
|
|
165
170
|
|
|
@@ -181,6 +186,9 @@ export class SessionManager {
|
|
|
181
186
|
AgentID: session.AgentID,
|
|
182
187
|
UserID: session.UserID,
|
|
183
188
|
ConversationID: session.ConversationID ?? null,
|
|
189
|
+
// Hand the verbatim per-session config blob to data-aware channels (e.g. the Media
|
|
190
|
+
// channel reads a per-session mediaCollectionID override from it). Opaque here.
|
|
191
|
+
AgentSessionConfig: session.Config_ ?? null,
|
|
184
192
|
},
|
|
185
193
|
contextUser,
|
|
186
194
|
provider,
|
|
@@ -313,6 +321,31 @@ export class SessionManager {
|
|
|
313
321
|
conversation.NewRecord();
|
|
314
322
|
conversation.UserID = input.userID;
|
|
315
323
|
conversation.Name = 'Agent Session';
|
|
324
|
+
// For a magic-link-anonymous principal (e.g. a public web-widget voice guest), stamp the
|
|
325
|
+
// session's conversation with the signed per-session scope so the Widget Guest RLS filters
|
|
326
|
+
// ({{ScopeResourceID}}) isolate this session — and everything chained to it (AI Agent
|
|
327
|
+
// Sessions via ConversationID, Session Channels via the session) — from other guests
|
|
328
|
+
// sharing the Anonymous UserID. No-op for named principals (no scope → default ExternalID).
|
|
329
|
+
if (contextUser.MagicLinkScope?.ResourceID) {
|
|
330
|
+
conversation.ExternalID = contextUser.MagicLinkScope.ResourceID;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Returning-visitor memory (RV1/RV2/RV4) for the VOICE path: the widget guest token carries the
|
|
334
|
+
// resolved VisitorKey / prior-conversation / linked identity (surfaced on ReturningVisitorContext),
|
|
335
|
+
// so a server-created voice conversation gets the same anchor the text path stamps client-side.
|
|
336
|
+
// The resolved counterparty reuses the existing polymorphic LinkedEntityID/LinkedRecordID pair.
|
|
337
|
+
// Absent for non-widget sessions and widgets with returning-visitor memory off — a no-op default.
|
|
338
|
+
const visitor = contextUser.ReturningVisitorContext;
|
|
339
|
+
if (visitor?.VisitorKey) {
|
|
340
|
+
conversation.VisitorKey = visitor.VisitorKey;
|
|
341
|
+
if (visitor.LastConversationID) {
|
|
342
|
+
conversation.LastConversationID = visitor.LastConversationID;
|
|
343
|
+
}
|
|
344
|
+
if (visitor.LinkedEntityID && visitor.LinkedRecordID) {
|
|
345
|
+
conversation.LinkedEntityID = visitor.LinkedEntityID;
|
|
346
|
+
conversation.LinkedRecordID = visitor.LinkedRecordID;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
316
349
|
|
|
317
350
|
const saved = await conversation.Save();
|
|
318
351
|
if (!saved) {
|
|
@@ -340,6 +373,14 @@ export class SessionManager {
|
|
|
340
373
|
session.HostInstanceID = GetHostInstanceID();
|
|
341
374
|
session.Config_ = input.config ?? null;
|
|
342
375
|
session.LastActiveAt = new Date();
|
|
376
|
+
// Mirror the conversation's resolved counterparty onto the session's own polymorphic pair, so a
|
|
377
|
+
// session carries its linked identity directly (parallels Conversation.LinkedEntityID/RecordID).
|
|
378
|
+
// Sourced from the same widget visitor context; a no-op for non-widget / anonymous sessions.
|
|
379
|
+
const visitor = contextUser.ReturningVisitorContext;
|
|
380
|
+
if (visitor?.LinkedEntityID && visitor.LinkedRecordID) {
|
|
381
|
+
session.LinkedEntityID = visitor.LinkedEntityID;
|
|
382
|
+
session.LinkedRecordID = visitor.LinkedRecordID;
|
|
383
|
+
}
|
|
343
384
|
|
|
344
385
|
const saved = await session.Save();
|
|
345
386
|
if (!saved) {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module @memberjunction/server
|
|
9
9
|
*/
|
|
10
|
-
import { LoadWhiteboardChannelServer, LoadMeetingControlsChannelServer } from '@memberjunction/ai-agents';
|
|
10
|
+
import { LoadWhiteboardChannelServer, LoadMeetingControlsChannelServer, LoadMediaChannelServer, LoadClientContextChannelServer } from '@memberjunction/ai-agents';
|
|
11
11
|
import { LoadRemoteBrowserChannel } from '@memberjunction/remote-browser-server';
|
|
12
12
|
import { LoadSelfHostRemoteBrowser } from '@memberjunction/remote-browser-selfhost';
|
|
13
13
|
import { BindRemoteBrowserGoalEngine } from './remoteBrowserGoalEngine.js';
|
|
@@ -17,6 +17,8 @@ import { BindRemoteBrowserGoalEngine } from './remoteBrowserGoalEngine.js';
|
|
|
17
17
|
// the session lifecycle — `SessionManager.CreateSession` resolves them via the ClassFactory.
|
|
18
18
|
LoadWhiteboardChannelServer();
|
|
19
19
|
LoadMeetingControlsChannelServer();
|
|
20
|
+
LoadMediaChannelServer();
|
|
21
|
+
LoadClientContextChannelServer();
|
|
20
22
|
// Remote Browser native channel (client-direct): the lifecycle-only server channel plugin + the
|
|
21
23
|
// Self-Hosted Chrome backend driver (whose default runner launches a local headless Chromium via
|
|
22
24
|
// Playwright — pulled in transitively through the SelfHost package, documented and acceptable).
|
|
@@ -177,6 +177,23 @@ export function createMagicLinkHandler(publicUrl: string, config: MagicLinkConfi
|
|
|
177
177
|
return { publicRouter, authenticatedRouter };
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* A minimal PUBLIC router that serves ONLY the JWKS endpoint (the magic-link
|
|
182
|
+
* signing key's public half). Mount this when the full magic-link flow is
|
|
183
|
+
* disabled but another feature reuses the same RS256 key + `magic-link` auth
|
|
184
|
+
* provider — today, the public web widget. Without the published public key the
|
|
185
|
+
* unified auth middleware cannot validate the reused-key guest tokens and every
|
|
186
|
+
* request 401s. Assumes the key is already initialized (the widget handler does
|
|
187
|
+
* this via `MagicLinkKeyManager.Instance.Initialize` at startup).
|
|
188
|
+
*/
|
|
189
|
+
export function createMagicLinkJwksRouter(): Router {
|
|
190
|
+
const router = Router();
|
|
191
|
+
router.get('/jwks.json', (_req: Request, res: Response) => {
|
|
192
|
+
res.status(200).json(MagicLinkKeyManager.Instance.GetJWKS());
|
|
193
|
+
});
|
|
194
|
+
return router;
|
|
195
|
+
}
|
|
196
|
+
|
|
180
197
|
/**
|
|
181
198
|
* Registers the `magic-link` auth provider so MJServer's issuer-driven JWT
|
|
182
199
|
* validation accepts MJ-issued session tokens. Issuer = public URL, JWKS URL =
|
|
@@ -11,6 +11,7 @@ export { MagicLinkKeyManager } from './MagicLinkKeys.js';
|
|
|
11
11
|
export { MagicLinkService } from './MagicLinkService.js';
|
|
12
12
|
export {
|
|
13
13
|
createMagicLinkHandler,
|
|
14
|
+
createMagicLinkJwksRouter,
|
|
14
15
|
registerMagicLinkAuthProvider,
|
|
15
16
|
MAGIC_LINK_MOUNT_PATH,
|
|
16
17
|
MAGIC_LINK_JWKS_PATH,
|
|
@@ -58,6 +58,32 @@ export interface MagicLinkJWTClaims {
|
|
|
58
58
|
mj_anon?: boolean;
|
|
59
59
|
/** Opaque per-session id — correlates one anonymous session's activity across audit rows without a real user. */
|
|
60
60
|
mj_sid?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Public web widget instance id (additive — set only for widget guest sessions
|
|
63
|
+
* minted by WidgetSessionService). Binds the synthesized guest principal to one
|
|
64
|
+
* widget instance so its pinned agent / guest role can be locked down. Absent on
|
|
65
|
+
* ordinary magic-link sessions.
|
|
66
|
+
*/
|
|
67
|
+
mj_widget_id?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Host-asserted visitor email for a widget `host-identity` session (additive). The
|
|
70
|
+
* authoritative `email`/`sub` still resolve the constrained shared Anonymous principal;
|
|
71
|
+
* this carries WHO the host says the visitor is, for the agent to look up their account —
|
|
72
|
+
* without granting that account's permissions. Absent on anonymous/ordinary sessions.
|
|
73
|
+
*/
|
|
74
|
+
mj_host_email?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Returning-visitor anchor for a widget guest session (additive, RV1). Carried so the VOICE path —
|
|
77
|
+
* whose conversation is created server-side — can stamp the same Conversation.VisitorKey the text
|
|
78
|
+
* path stamps client-side. Present only when the widget remembers returning visitors.
|
|
79
|
+
*/
|
|
80
|
+
mj_visitor_key?: string;
|
|
81
|
+
/** The prior conversation this visit chains from (RV2), for the voice path to stamp Conversation.LastConversationID. */
|
|
82
|
+
mj_last_conversation_id?: string;
|
|
83
|
+
/** Resolved polymorphic identity entity id (RV4), for the voice path to stamp the existing Conversation.LinkedEntityID / AIAgentSession.LinkedEntityID. */
|
|
84
|
+
mj_linked_entity_id?: string;
|
|
85
|
+
/** Resolved polymorphic identity record id (RV4), for the voice path to stamp the existing Conversation.LinkedRecordID / AIAgentSession.LinkedRecordID. */
|
|
86
|
+
mj_linked_record_id?: string;
|
|
61
87
|
/** Marks the session as magic-link so the Explorer can confine the UI. */
|
|
62
88
|
mj_magic_link: true;
|
|
63
89
|
}
|
package/src/config.ts
CHANGED
|
@@ -68,23 +68,6 @@ const zodBooleanWithTransforms = () => {
|
|
|
68
68
|
})
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
const askSkipInfoSchema = z.object({
|
|
72
|
-
url: z.string().optional(), // Base URL for Skip API
|
|
73
|
-
apiKey: z.string().optional(),
|
|
74
|
-
orgID: z.string().optional(),
|
|
75
|
-
organizationInfo: z.string().optional(),
|
|
76
|
-
entitiesToSend: z
|
|
77
|
-
.object({
|
|
78
|
-
excludeSchemas: z.array(z.string()).optional(),
|
|
79
|
-
includeEntitiesFromExcludedSchemas: z.array(z.string()).optional(),
|
|
80
|
-
})
|
|
81
|
-
.optional(),
|
|
82
|
-
chatURL: z.string().optional(),
|
|
83
|
-
learningCycleRunUponStartup: zodBooleanWithTransforms(),
|
|
84
|
-
learningCycleEnabled: zodBooleanWithTransforms(),
|
|
85
|
-
learningCycleURL: z.string().optional(),
|
|
86
|
-
learningCycleIntervalInMinutes: z.coerce.number().optional(),
|
|
87
|
-
});
|
|
88
71
|
|
|
89
72
|
const sqlLoggingOptionsSchema = z.object({
|
|
90
73
|
formatAsMigration: z.boolean().optional().default(false),
|
|
@@ -330,13 +313,178 @@ const magicLinkSchema = z.object({
|
|
|
330
313
|
explorerUrl: z.string().optional(),
|
|
331
314
|
}).passthrough();
|
|
332
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Public web widget config (plans/realtime/bridges-and-widget/public-web-widget.md).
|
|
318
|
+
* The widget REUSES the magic-link RS256 key + `anonymous-embed` synthesis, so it
|
|
319
|
+
* shares the magic-link `audience`/issuer/JWKS by default — the same auth provider
|
|
320
|
+
* validates both. When `enabled`, the server ensures the magic-link key manager is
|
|
321
|
+
* initialized and the provider registered even if `magicLink.enabled` is false.
|
|
322
|
+
*/
|
|
323
|
+
const widgetSchema = z.object({
|
|
324
|
+
/** Master switch. When false, the /widget routes are not mounted. */
|
|
325
|
+
enabled: zodBooleanWithTransforms().default(false),
|
|
326
|
+
/**
|
|
327
|
+
* Which signing mechanism the widget reuses. Only `magic-link` is supported today
|
|
328
|
+
* (shares MagicLinkKeyManager + the magic-link auth provider). Recorded so a future
|
|
329
|
+
* dedicated widget key is a config flip, not a code change.
|
|
330
|
+
*/
|
|
331
|
+
signingReuse: z.enum(['magic-link']).optional().default('magic-link'),
|
|
332
|
+
/**
|
|
333
|
+
* Audience claim for minted guest JWTs. MUST equal the magic-link audience so the
|
|
334
|
+
* auto-registered `magic-link` auth provider validates widget tokens.
|
|
335
|
+
*/
|
|
336
|
+
audience: z.string().optional().default('mj-magic-link'),
|
|
337
|
+
/** Email of the shared Anonymous principal used in guest claims (resolves the synthesized guest user). */
|
|
338
|
+
anonymousEmail: z.string().optional().default('anonymous@magic-link.local'),
|
|
339
|
+
/** Fallback session-token TTL (minutes) when a widget instance does not set its own. */
|
|
340
|
+
defaultSessionTtlMinutes: z.coerce.number().optional().default(15),
|
|
341
|
+
/** Fallback mint rate-limit (per IP per window) when a widget instance does not set its own. */
|
|
342
|
+
defaultRateLimitPerMinute: z.coerce.number().optional().default(30),
|
|
343
|
+
/** Rate-limit window (ms) for the public /widget/session endpoints. Default 60s. */
|
|
344
|
+
rateLimitWindowMs: z.coerce.number().optional().default(60_000),
|
|
345
|
+
/** Server-wide default hard ceiling (minutes) on a voice session when an instance omits one (W4). */
|
|
346
|
+
voiceDefaultMaxSessionMinutes: z.coerce.number().optional().default(10),
|
|
347
|
+
/** Email/name of the internal user whose context READS widget config at mint time (falls back to system/Owner). */
|
|
348
|
+
contextUserForLookup: z.string().optional(),
|
|
349
|
+
/**
|
|
350
|
+
* Host-identity public keys (PEM), keyed by widget PublicKey, for the `host-identity` auth
|
|
351
|
+
* strategy (D1). The host signs a short-lived RS256 assertion; the mint verifies it against
|
|
352
|
+
* the key here. INTERIM location — production should store the host key per widget instance
|
|
353
|
+
* (a HostPublicKey column on WidgetInstance, pending a migration). When a widget's strategy is
|
|
354
|
+
* HostIdentity and no key is configured here, host mints fail closed.
|
|
355
|
+
*/
|
|
356
|
+
hostPublicKeys: z.record(z.string(), z.string()).optional().default({}),
|
|
357
|
+
/**
|
|
358
|
+
* Returning-visitor identity-resolution target (RV4). When a visitor verifies (magic-link upgrade)
|
|
359
|
+
* or a host asserts their identity, their email is resolved to a record `(entityName, recordId)`
|
|
360
|
+
* and prior anonymous memory is merged onto that polymorphic pair. Deployment-configurable so the
|
|
361
|
+
* resolved record need NOT be an MJ User — point it at any entity that carries the visitor's email
|
|
362
|
+
* (e.g. a CRM `Persons` table). Defaults to the core `Users` entity keyed by `Email`.
|
|
363
|
+
*/
|
|
364
|
+
identityResolution: z.object({
|
|
365
|
+
entityName: z.string().optional().default('Users'),
|
|
366
|
+
emailField: z.string().optional().default('Email'),
|
|
367
|
+
}).optional(),
|
|
368
|
+
}).passthrough();
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Telephony bridge config (plans/realtime/bridges-and-widget/telephony-vendor-bindings.md).
|
|
372
|
+
* Holds the per-vendor credentials + public stream URL the MJAPI telephony ingress wires onto
|
|
373
|
+
* the already-stubbed native SDK seams. Credentials resolve here from `mj.config.cjs` / env —
|
|
374
|
+
* never inlined in code. When `twilio` is omitted, the Twilio telephony routes are not mounted.
|
|
375
|
+
*/
|
|
376
|
+
const twilioTelephonySchema = z.object({
|
|
377
|
+
/** Twilio Account SID (`AC…`). */
|
|
378
|
+
accountSid: z.string(),
|
|
379
|
+
/** Account auth token — REST auth (when no API key pair) AND the HMAC key for X-Twilio-Signature verification. */
|
|
380
|
+
authToken: z.string().optional(),
|
|
381
|
+
/** API Key SID (`SK…`) — preferred over the auth token for REST auth when paired with apiKeySecret. */
|
|
382
|
+
apiKeySid: z.string().optional(),
|
|
383
|
+
/** API Key secret — paired with apiKeySid. */
|
|
384
|
+
apiKeySecret: z.string().optional(),
|
|
385
|
+
/** The publicly reachable `wss://…/telephony/twilio/media` URL Twilio's <Connect><Stream> connects to. */
|
|
386
|
+
streamPublicUrl: z.string(),
|
|
387
|
+
/** Optional shared secret gating the public webhook/WSS endpoints (defense-in-depth beyond signature verification). */
|
|
388
|
+
webhookSigningSecret: z.string().optional(),
|
|
389
|
+
/** Optional status-callback URL Twilio posts call lifecycle events to. */
|
|
390
|
+
statusCallbackUrl: z.string().optional(),
|
|
391
|
+
}).passthrough();
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Vonage Voice + WebSocket-media telephony binding. Voice-API auth is application-scoped
|
|
395
|
+
* (application id + RSA private key) with an optional account API key pair for key-scoped ops;
|
|
396
|
+
* signatureSecret is the HMAC key for signed-request `sig` AND HS256 webhook-JWT verification.
|
|
397
|
+
* When `vonage` is omitted, the Vonage telephony routes are not mounted.
|
|
398
|
+
*/
|
|
399
|
+
const vonageTelephonySchema = z.object({
|
|
400
|
+
/** Vonage Application ID (UUID) — the JWT-auth identity for the Voice API. */
|
|
401
|
+
applicationId: z.string().optional(),
|
|
402
|
+
/** The application's RSA private key (PEM) used to sign Voice-API JWTs. */
|
|
403
|
+
privateKey: z.string().optional(),
|
|
404
|
+
/** Account API key — used for key-scoped operations when no application credential pair is supplied. */
|
|
405
|
+
apiKey: z.string().optional(),
|
|
406
|
+
/** Account API secret — paired with apiKey. */
|
|
407
|
+
apiSecret: z.string().optional(),
|
|
408
|
+
/** The publicly reachable `wss://…/telephony/vonage/media` URL the call's connect NCCO opens. */
|
|
409
|
+
mediaPublicUrl: z.string(),
|
|
410
|
+
/** Vonage account signature secret — HMAC key for signed-request `sig` AND HS256 webhook-JWT verification. */
|
|
411
|
+
signatureSecret: z.string().optional(),
|
|
412
|
+
/** Optional event-webhook URL Vonage posts call lifecycle events to (passed on outbound createCall). */
|
|
413
|
+
eventUrl: z.string().optional(),
|
|
414
|
+
}).passthrough();
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* RingCentral telephony binding: **SIP device credentials** for the headless softphone (the
|
|
418
|
+
* `ringcentral-softphone` SIP/RTP path — the only RingCentral product that carries bidirectional call
|
|
419
|
+
* audio; the WebSocket "Call Streaming" product is receive-only). These come from a RingCentral
|
|
420
|
+
* "Other Phone" (BYO-device) registration's SIP info, resolved here from `mj.config.cjs` / env — never
|
|
421
|
+
* inlined. When `ringcentral` is omitted, the RingCentral softphone is not started.
|
|
422
|
+
*
|
|
423
|
+
* Unlike Twilio/Vonage, there is NO public HTTP webhook or media WSS: the softphone is an outbound SIP
|
|
424
|
+
* registration that receives inbound INVITEs directly over its own SIP/TLS connection. So this block has
|
|
425
|
+
* no signing secret or public URL — just the SIP device creds + codec.
|
|
426
|
+
*/
|
|
427
|
+
const ringcentralTelephonySchema = z.object({
|
|
428
|
+
/** SIP domain (e.g. `sip.ringcentral.com`). */
|
|
429
|
+
sipDomain: z.string(),
|
|
430
|
+
/** SIP outbound proxy (`host:port`, e.g. `sip10.ringcentral.com:5096`). */
|
|
431
|
+
sipOutboundProxy: z.string(),
|
|
432
|
+
/** SIP auth username (the device's phone number / extension). */
|
|
433
|
+
sipUsername: z.string(),
|
|
434
|
+
/** SIP auth password (resolved upstream — never inlined). */
|
|
435
|
+
sipPassword: z.string(),
|
|
436
|
+
/** SIP authorization id (the device's RingCentral authorization id). */
|
|
437
|
+
sipAuthorizationId: z.string(),
|
|
438
|
+
/** Codec to negotiate. Defaults to `OPUS/16000` (clean wideband PCM16 — the least-friction realtime path). */
|
|
439
|
+
codec: z.enum(['OPUS/16000', 'OPUS/48000/2', 'PCMU/8000']).optional(),
|
|
440
|
+
/** Skip TLS cert validation (sandbox/test only — never in production). */
|
|
441
|
+
ignoreTlsCertErrors: zodBooleanWithTransforms().optional().default(false),
|
|
442
|
+
}).passthrough();
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Teams meetings bridge config (plans/realtime/bridges-and-widget/meeting-vendor-bindings-teams-slack.md M1).
|
|
446
|
+
* Holds the bot credentials + Graph webhook shared secret + ACS audio rate the MJAPI meetings ingress wires
|
|
447
|
+
* onto the Teams binding seams. Credentials resolve here from `mj.config.cjs` / env — never inlined in code.
|
|
448
|
+
* When `teams` is omitted (or disabled), the Teams meetings routes are not mounted.
|
|
449
|
+
*/
|
|
450
|
+
const teamsMeetingsSchema = z.object({
|
|
451
|
+
/** Master switch for the Teams meetings ingress. */
|
|
452
|
+
enabled: zodBooleanWithTransforms().default(false),
|
|
453
|
+
/** The bot's Azure AD application (client) id (resolved upstream; carried for diagnostics). */
|
|
454
|
+
appId: z.string().optional(),
|
|
455
|
+
/** The Azure tenant id the bot operates in. */
|
|
456
|
+
tenantId: z.string().optional(),
|
|
457
|
+
/** A pre-resolved OAuth bearer / application token for the bot's Graph calls (resolved upstream — never inline). */
|
|
458
|
+
botAccessToken: z.string().optional(),
|
|
459
|
+
/** The shared secret set as `clientState` on the Graph subscription; gates the change-notification webhook. */
|
|
460
|
+
notificationClientState: z.string().optional(),
|
|
461
|
+
/** The ACS application-hosted-media PCM sample rate (Hz) the audio plane negotiates. Defaults to 16000. */
|
|
462
|
+
acsSampleRate: z.coerce.number().optional().default(16000),
|
|
463
|
+
/** The realtime model's PCM16 sample rate (Hz) the binding resamples to/from. Defaults to 16000. */
|
|
464
|
+
modelSampleRate: z.coerce.number().optional().default(16000),
|
|
465
|
+
}).passthrough();
|
|
466
|
+
|
|
467
|
+
const telephonySchema = z.object({
|
|
468
|
+
/** Master switch. When false (or when no vendor block is present), telephony routes are not mounted. */
|
|
469
|
+
enabled: zodBooleanWithTransforms().default(false),
|
|
470
|
+
/** Twilio Programmable Voice + Media Streams binding. */
|
|
471
|
+
twilio: twilioTelephonySchema.optional(),
|
|
472
|
+
/** Vonage Voice + WebSocket-media binding. */
|
|
473
|
+
vonage: vonageTelephonySchema.optional(),
|
|
474
|
+
/** RingCentral Call Control + media-stream binding. */
|
|
475
|
+
ringcentral: ringcentralTelephonySchema.optional(),
|
|
476
|
+
/** Microsoft Teams meetings (Graph cloud-communications + ACS application-hosted media) binding. */
|
|
477
|
+
teams: teamsMeetingsSchema.optional(),
|
|
478
|
+
}).passthrough();
|
|
479
|
+
|
|
333
480
|
const configInfoSchema = z.object({
|
|
334
481
|
userHandling: userHandlingInfoSchema,
|
|
335
482
|
magicLink: magicLinkSchema.optional().default({}),
|
|
483
|
+
widget: widgetSchema.optional().default({}),
|
|
484
|
+
telephony: telephonySchema.optional().default({}),
|
|
336
485
|
databaseSettings: databaseSettingsInfoSchema,
|
|
337
486
|
viewingSystem: viewingSystemInfoSchema.optional(),
|
|
338
487
|
restApiOptions: restApiOptionsSchema.optional().default({}),
|
|
339
|
-
askSkip: askSkipInfoSchema.optional(),
|
|
340
488
|
sqlLogging: sqlLoggingSchema.optional(),
|
|
341
489
|
authProviders: z.array(authProviderSchema).optional(),
|
|
342
490
|
componentRegistries: z.array(componentRegistrySchema).optional(),
|
|
@@ -384,10 +532,15 @@ const configInfoSchema = z.object({
|
|
|
384
532
|
|
|
385
533
|
export type UserHandlingInfo = z.infer<typeof userHandlingInfoSchema>;
|
|
386
534
|
export type MagicLinkConfig = z.infer<typeof magicLinkSchema>;
|
|
535
|
+
export type WidgetConfig = z.infer<typeof widgetSchema>;
|
|
536
|
+
export type TelephonyConfig = z.infer<typeof telephonySchema>;
|
|
537
|
+
export type TwilioTelephonyConfig = z.infer<typeof twilioTelephonySchema>;
|
|
538
|
+
export type VonageTelephonyConfig = z.infer<typeof vonageTelephonySchema>;
|
|
539
|
+
export type RingCentralTelephonyConfig = z.infer<typeof ringcentralTelephonySchema>;
|
|
540
|
+
export type TeamsMeetingsConfig = z.infer<typeof teamsMeetingsSchema>;
|
|
387
541
|
export type DatabaseSettingsInfo = z.infer<typeof databaseSettingsInfoSchema>;
|
|
388
542
|
export type ViewingSystemSettingsInfo = z.infer<typeof viewingSystemInfoSchema>;
|
|
389
543
|
export type RESTApiOptions = z.infer<typeof restApiOptionsSchema>;
|
|
390
|
-
export type AskSkipInfo = z.infer<typeof askSkipInfoSchema>;
|
|
391
544
|
export type SqlLoggingOptions = z.infer<typeof sqlLoggingOptionsSchema>;
|
|
392
545
|
export type SqlLoggingInfo = z.infer<typeof sqlLoggingSchema>;
|
|
393
546
|
export type AuthProviderConfig = z.infer<typeof authProviderSchema>;
|
|
@@ -477,25 +630,6 @@ export const DEFAULT_SERVER_CONFIG: Partial<ConfigInfo> = {
|
|
|
477
630
|
enabled: false,
|
|
478
631
|
},
|
|
479
632
|
|
|
480
|
-
// Ask Skip configuration (environment-driven)
|
|
481
|
-
askSkip: {
|
|
482
|
-
url: process.env.ASK_SKIP_URL,
|
|
483
|
-
chatURL: process.env.ASK_SKIP_CHAT_URL,
|
|
484
|
-
learningCycleURL: process.env.ASK_SKIP_LEARNING_URL,
|
|
485
|
-
learningCycleIntervalInMinutes: process.env.ASK_SKIP_LEARNING_CYCLE_INTERVAL_IN_MINUTES
|
|
486
|
-
? parseInt(process.env.ASK_SKIP_LEARNING_CYCLE_INTERVAL_IN_MINUTES, 10)
|
|
487
|
-
: undefined,
|
|
488
|
-
learningCycleEnabled: process.env.ASK_SKIP_RUN_LEARNING_CYCLES === 'true',
|
|
489
|
-
learningCycleRunUponStartup: process.env.ASK_SKIP_RUN_LEARNING_CYCLES_UPON_STARTUP === 'true',
|
|
490
|
-
orgID: process.env.ASK_SKIP_ORGANIZATION_ID,
|
|
491
|
-
apiKey: process.env.ASK_SKIP_API_KEY,
|
|
492
|
-
organizationInfo: process.env.ASK_SKIP_ORGANIZATION_INFO,
|
|
493
|
-
entitiesToSend: {
|
|
494
|
-
excludeSchemas: [],
|
|
495
|
-
includeEntitiesFromExcludedSchemas: [],
|
|
496
|
-
},
|
|
497
|
-
},
|
|
498
|
-
|
|
499
633
|
// SQL logging defaults
|
|
500
634
|
sqlLogging: {
|
|
501
635
|
enabled: true,
|
package/src/context.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { GetReadOnlyDataSource, GetReadWriteDataSource } from './util.js';
|
|
|
15
15
|
import { v4 as uuidv4 } from 'uuid';
|
|
16
16
|
import e from 'express';
|
|
17
17
|
import type { RequestHandler, Request, Response, NextFunction } from 'express';
|
|
18
|
-
import { DatabaseProviderBase, UserInfo, type MagicLinkScope } from '@memberjunction/core';
|
|
18
|
+
import { DatabaseProviderBase, UserInfo, type MagicLinkScope, type ReturningVisitorContext, type WidgetGuestContext } from '@memberjunction/core';
|
|
19
19
|
import { SQLServerDataProvider, SQLServerProviderConfigData, UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
20
20
|
import { Metadata } from '@memberjunction/core';
|
|
21
21
|
import { UUIDsEqual } from '@memberjunction/global';
|
|
@@ -242,8 +242,17 @@ function buildMagicLinkSessionUser(userRecord: UserInfo, payload: jwt.JwtPayload
|
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
//
|
|
246
|
-
|
|
245
|
+
// Returning-visitor context (RV1/RV2/RV4): carried on a widget guest token so the voice path
|
|
246
|
+
// (server-created conversation) stamps the same returning-visitor anchor + resolved identity.
|
|
247
|
+
const visitorContext = extractReturningVisitorContext(payload);
|
|
248
|
+
|
|
249
|
+
// Widget-instance identity (mj_widget_id): present on every public web-widget guest token. The
|
|
250
|
+
// privileged agent-dispatch path reads it to resolve the AUTHORITATIVE pinned agent for the guest
|
|
251
|
+
// (so a guest can never run an arbitrary agent under the elevated server principal).
|
|
252
|
+
const widgetGuestContext = extractWidgetGuestContext(payload);
|
|
253
|
+
|
|
254
|
+
// Named session with no resource scope and no widget/visitor context → no per-request state needed.
|
|
255
|
+
if (!isAnon && !scope && !visitorContext && !widgetGuestContext) {
|
|
247
256
|
return userRecord;
|
|
248
257
|
}
|
|
249
258
|
|
|
@@ -255,6 +264,12 @@ function buildMagicLinkSessionUser(userRecord: UserInfo, payload: jwt.JwtPayload
|
|
|
255
264
|
if (scope) {
|
|
256
265
|
sessionUser.MagicLinkScope = scope;
|
|
257
266
|
}
|
|
267
|
+
if (visitorContext) {
|
|
268
|
+
sessionUser.ReturningVisitorContext = visitorContext;
|
|
269
|
+
}
|
|
270
|
+
if (widgetGuestContext) {
|
|
271
|
+
sessionUser.WidgetGuestContext = widgetGuestContext;
|
|
272
|
+
}
|
|
258
273
|
if (isAnon) {
|
|
259
274
|
// Mark the session so the CurrentUser field resolver serves these synthesized roles
|
|
260
275
|
// (the shared Anonymous principal holds none in the DB). See UserInfo.IsMagicLinkAnonymous.
|
|
@@ -263,6 +278,38 @@ function buildMagicLinkSessionUser(userRecord: UserInfo, payload: jwt.JwtPayload
|
|
|
263
278
|
return sessionUser;
|
|
264
279
|
}
|
|
265
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Extracts the returning-visitor context (RV1/RV2/RV4) from a widget guest token's claims, or
|
|
283
|
+
* undefined when the token carries no VisitorKey (the default, remembering-off case). Used so the
|
|
284
|
+
* voice path can stamp the conversation with the same anchor/identity the text path stamps client-side.
|
|
285
|
+
*/
|
|
286
|
+
function extractReturningVisitorContext(payload: jwt.JwtPayload): ReturningVisitorContext | undefined {
|
|
287
|
+
const visitorKey = payload['mj_visitor_key'];
|
|
288
|
+
if (typeof visitorKey !== 'string' || !visitorKey) {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
const str = (v: unknown): string | undefined => (typeof v === 'string' && v ? v : undefined);
|
|
292
|
+
return {
|
|
293
|
+
VisitorKey: visitorKey,
|
|
294
|
+
LastConversationID: str(payload['mj_last_conversation_id']),
|
|
295
|
+
LinkedEntityID: str(payload['mj_linked_entity_id']),
|
|
296
|
+
LinkedRecordID: str(payload['mj_linked_record_id']),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Extracts the widget-instance identity (`mj_widget_id`) from a public web-widget guest token, or
|
|
302
|
+
* undefined when the token carries none (every non-widget session). Carried so the privileged
|
|
303
|
+
* agent-dispatch path can resolve the authoritative pinned agent for the guest from the trusted token.
|
|
304
|
+
*/
|
|
305
|
+
function extractWidgetGuestContext(payload: jwt.JwtPayload): WidgetGuestContext | undefined {
|
|
306
|
+
const widgetId = payload['mj_widget_id'];
|
|
307
|
+
if (typeof widgetId !== 'string' || !widgetId) {
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
return { WidgetID: widgetId };
|
|
311
|
+
}
|
|
312
|
+
|
|
266
313
|
const verifyAsync = async (issuer: string, token: string): Promise<jwt.JwtPayload> =>
|
|
267
314
|
new Promise((resolve, reject) => {
|
|
268
315
|
const options = getValidationOptions(issuer);
|