@memberjunction/server 6.1.0-edge.6 → 6.1.0-edge.7
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/README.md +24 -9
- package/dist/.tsbuildinfo +1 -1
- package/dist/config.d.ts +36 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts +30 -0
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts.map +1 -1
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js +58 -5
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +2842 -2144
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +5334 -2143
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +119 -4
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +264 -11
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +26 -2
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/generic/refusalExtensions.d.ts +52 -0
- package/dist/generic/refusalExtensions.d.ts.map +1 -0
- package/dist/generic/refusalExtensions.js +16 -0
- package/dist/generic/refusalExtensions.js.map +1 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +115 -91
- package/dist/index.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +34 -5
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +64 -7
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -2
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +80 -4
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/rest/RealtimeSdpBrokerHandler.d.ts +23 -0
- package/dist/rest/RealtimeSdpBrokerHandler.d.ts.map +1 -0
- package/dist/rest/RealtimeSdpBrokerHandler.js +78 -0
- package/dist/rest/RealtimeSdpBrokerHandler.js.map +1 -0
- package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
- package/dist/serverExtensionReservedRoots.js +2 -3
- package/dist/serverExtensionReservedRoots.js.map +1 -1
- package/package.json +92 -92
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +118 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +20 -0
- package/src/__tests__/RealtimeSdpBrokerHandler.test.ts +191 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +63 -0
- package/src/__tests__/ResolverBase.readableFieldsTransport.test.ts +106 -0
- package/src/__tests__/generatedGraphQLSchema.structural.test.ts +46 -0
- package/src/__tests__/resolverBase.fls.test.ts +483 -0
- package/src/__tests__/resolverBase.recordChangesFls.test.ts +358 -0
- package/src/__tests__/resolverBase.refusalExtensions.test.ts +101 -0
- package/src/__tests__/serverExtensionReservedRoots.test.ts +79 -11
- package/src/config.ts +12 -0
- package/src/entitySubclasses/MJEntityPermissionEntityServer.server.ts +65 -6
- package/src/generated/graphql-schemas/__mj.ts +6590 -4287
- package/src/generic/ResolverBase.ts +313 -10
- package/src/generic/RunViewResolver.ts +28 -2
- package/src/generic/refusalExtensions.ts +59 -0
- package/src/index.ts +137 -100
- package/src/resolvers/EntityRecordNameResolver.ts +74 -8
- package/src/resolvers/RealtimeClientSessionResolver.ts +128 -2
- package/src/rest/RealtimeSdpBrokerHandler.ts +101 -0
- package/src/serverExtensionReservedRoots.ts +2 -3
- package/dist/resolvers/RingCentralTelephonyResolver.d.ts +0 -27
- package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +0 -1
- package/dist/resolvers/RingCentralTelephonyResolver.js +0 -87
- package/dist/resolvers/RingCentralTelephonyResolver.js.map +0 -1
- package/dist/resolvers/TeamsMeetingsResolver.d.ts +0 -28
- package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +0 -1
- package/dist/resolvers/TeamsMeetingsResolver.js +0 -91
- package/dist/resolvers/TeamsMeetingsResolver.js.map +0 -1
- package/dist/resolvers/TelephonyResolver.d.ts +0 -26
- package/dist/resolvers/TelephonyResolver.d.ts.map +0 -1
- package/dist/resolvers/TelephonyResolver.js +0 -86
- package/dist/resolvers/TelephonyResolver.js.map +0 -1
- package/dist/resolvers/VonageTelephonyResolver.d.ts +0 -26
- package/dist/resolvers/VonageTelephonyResolver.d.ts.map +0 -1
- package/dist/resolvers/VonageTelephonyResolver.js +0 -86
- package/dist/resolvers/VonageTelephonyResolver.js.map +0 -1
- package/dist/telephony/RingCentralTelephonyService.d.ts +0 -115
- package/dist/telephony/RingCentralTelephonyService.d.ts.map +0 -1
- package/dist/telephony/RingCentralTelephonyService.js +0 -262
- package/dist/telephony/RingCentralTelephonyService.js.map +0 -1
- package/dist/telephony/TeamsMeetingsRouter.d.ts +0 -40
- package/dist/telephony/TeamsMeetingsRouter.d.ts.map +0 -1
- package/dist/telephony/TeamsMeetingsRouter.js +0 -112
- package/dist/telephony/TeamsMeetingsRouter.js.map +0 -1
- package/dist/telephony/TeamsMeetingsService.d.ts +0 -113
- package/dist/telephony/TeamsMeetingsService.d.ts.map +0 -1
- package/dist/telephony/TeamsMeetingsService.js +0 -196
- package/dist/telephony/TeamsMeetingsService.js.map +0 -1
- package/dist/telephony/TwilioTelephonyRouter.d.ts +0 -36
- package/dist/telephony/TwilioTelephonyRouter.d.ts.map +0 -1
- package/dist/telephony/TwilioTelephonyRouter.js +0 -143
- package/dist/telephony/TwilioTelephonyRouter.js.map +0 -1
- package/dist/telephony/TwilioTelephonyService.d.ts +0 -95
- package/dist/telephony/TwilioTelephonyService.d.ts.map +0 -1
- package/dist/telephony/TwilioTelephonyService.js +0 -193
- package/dist/telephony/TwilioTelephonyService.js.map +0 -1
- package/dist/telephony/VonageTelephonyRouter.d.ts +0 -41
- package/dist/telephony/VonageTelephonyRouter.d.ts.map +0 -1
- package/dist/telephony/VonageTelephonyRouter.js +0 -201
- package/dist/telephony/VonageTelephonyRouter.js.map +0 -1
- package/dist/telephony/VonageTelephonyService.d.ts +0 -90
- package/dist/telephony/VonageTelephonyService.d.ts.map +0 -1
- package/dist/telephony/VonageTelephonyService.js +0 -191
- package/dist/telephony/VonageTelephonyService.js.map +0 -1
- package/dist/telephony/calendar-scheduler.d.ts +0 -67
- package/dist/telephony/calendar-scheduler.d.ts.map +0 -1
- package/dist/telephony/calendar-scheduler.js +0 -133
- package/dist/telephony/calendar-scheduler.js.map +0 -1
- package/dist/telephony/index.d.ts +0 -29
- package/dist/telephony/index.d.ts.map +0 -1
- package/dist/telephony/index.js +0 -38
- package/dist/telephony/index.js.map +0 -1
- package/dist/telephony/media-upgrade-router.d.ts +0 -47
- package/dist/telephony/media-upgrade-router.d.ts.map +0 -1
- package/dist/telephony/media-upgrade-router.js +0 -72
- package/dist/telephony/media-upgrade-router.js.map +0 -1
- package/dist/telephony/ringcentral-runtime.d.ts +0 -14
- package/dist/telephony/ringcentral-runtime.d.ts.map +0 -1
- package/dist/telephony/ringcentral-runtime.js +0 -18
- package/dist/telephony/ringcentral-runtime.js.map +0 -1
- package/dist/telephony/teams-meetings-runtime.d.ts +0 -16
- package/dist/telephony/teams-meetings-runtime.d.ts.map +0 -1
- package/dist/telephony/teams-meetings-runtime.js +0 -20
- package/dist/telephony/teams-meetings-runtime.js.map +0 -1
- package/dist/telephony/teamsAcsMediaRegistry.d.ts +0 -69
- package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +0 -1
- package/dist/telephony/teamsAcsMediaRegistry.js +0 -118
- package/dist/telephony/teamsAcsMediaRegistry.js.map +0 -1
- package/dist/telephony/telephony-runtime.d.ts +0 -14
- package/dist/telephony/telephony-runtime.d.ts.map +0 -1
- package/dist/telephony/telephony-runtime.js +0 -18
- package/dist/telephony/telephony-runtime.js.map +0 -1
- package/dist/telephony/twilioMediaRegistry.d.ts +0 -60
- package/dist/telephony/twilioMediaRegistry.d.ts.map +0 -1
- package/dist/telephony/twilioMediaRegistry.js +0 -106
- package/dist/telephony/twilioMediaRegistry.js.map +0 -1
- package/dist/telephony/vonage-runtime.d.ts +0 -14
- package/dist/telephony/vonage-runtime.d.ts.map +0 -1
- package/dist/telephony/vonage-runtime.js +0 -18
- package/dist/telephony/vonage-runtime.js.map +0 -1
- package/dist/telephony/vonageMediaRegistry.d.ts +0 -78
- package/dist/telephony/vonageMediaRegistry.d.ts.map +0 -1
- package/dist/telephony/vonageMediaRegistry.js +0 -158
- package/dist/telephony/vonageMediaRegistry.js.map +0 -1
- package/src/__tests__/media-upgrade-router.test.ts +0 -43
- package/src/__tests__/teams-meetings-service.test.ts +0 -65
- package/src/__tests__/teamsAcsMediaRegistry.test.ts +0 -82
- package/src/__tests__/twilio-telephony-service.test.ts +0 -23
- package/src/__tests__/twilioMediaRegistry.test.ts +0 -103
- package/src/__tests__/vonageMediaRegistry.test.ts +0 -180
- package/src/resolvers/RingCentralTelephonyResolver.ts +0 -64
- package/src/resolvers/TeamsMeetingsResolver.ts +0 -68
- package/src/resolvers/TelephonyResolver.ts +0 -63
- package/src/resolvers/VonageTelephonyResolver.ts +0 -63
- package/src/telephony/RingCentralTelephonyService.ts +0 -342
- package/src/telephony/TeamsMeetingsRouter.ts +0 -143
- package/src/telephony/TeamsMeetingsService.ts +0 -268
- package/src/telephony/TwilioTelephonyRouter.ts +0 -184
- package/src/telephony/TwilioTelephonyService.ts +0 -261
- package/src/telephony/VonageTelephonyRouter.ts +0 -239
- package/src/telephony/VonageTelephonyService.ts +0 -259
- package/src/telephony/calendar-scheduler.ts +0 -176
- package/src/telephony/index.ts +0 -43
- package/src/telephony/media-upgrade-router.ts +0 -79
- package/src/telephony/ringcentral-runtime.ts +0 -22
- package/src/telephony/teams-meetings-runtime.ts +0 -24
- package/src/telephony/teamsAcsMediaRegistry.ts +0 -152
- package/src/telephony/telephony-runtime.ts +0 -22
- package/src/telephony/twilioMediaRegistry.ts +0 -137
- package/src/telephony/vonage-runtime.ts +0 -22
- package/src/telephony/vonageMediaRegistry.ts +0 -200
package/src/index.ts
CHANGED
|
@@ -44,14 +44,10 @@ import { setupRESTEndpoints } from './rest/setupRESTEndpoints.js';
|
|
|
44
44
|
import { createOAuthCallbackHandler } from './rest/OAuthCallbackHandler.js';
|
|
45
45
|
import { createSignatureWebhookHandler } from './rest/SignatureWebhookHandler.js';
|
|
46
46
|
import { createMediaStreamRouter } from './rest/MediaStreamHandler.js';
|
|
47
|
+
import { createRealtimeSdpBrokerRouter } from './rest/RealtimeSdpBrokerHandler.js';
|
|
48
|
+
import { REALTIME_SDP_EXCHANGE_PATH } from '@memberjunction/ai';
|
|
47
49
|
import { createMagicLinkHandler, createMagicLinkJwksRouter, registerMagicLinkAuthProvider, MAGIC_LINK_MOUNT_PATH } from './auth/magicLink/index.js';
|
|
48
50
|
import { createWidgetHandler, WIDGET_MOUNT_PATH } from './realtimeWidget/index.js';
|
|
49
|
-
import { createTwilioTelephonyHandler, TWILIO_TELEPHONY_MOUNT_PATH, SetTwilioTelephonyService } from './telephony/index.js';
|
|
50
|
-
import { createVonageTelephonyHandler, VONAGE_TELEPHONY_MOUNT_PATH, SetVonageTelephonyService } from './telephony/index.js';
|
|
51
|
-
import { RingCentralTelephonyService, SetRingCentralTelephonyService } from './telephony/index.js';
|
|
52
|
-
import { createTeamsMeetingsHandler, TEAMS_MEETINGS_MOUNT_PATH, SetTeamsMeetingsService, GetTeamsMeetingsService, StartCalendarScheduler } from './telephony/index.js';
|
|
53
|
-
import { InstallMediaUpgradeDispatcher, IsGraphQLWsPath } from './telephony/index.js';
|
|
54
|
-
|
|
55
51
|
import { resolve } from 'node:path';
|
|
56
52
|
import { DataSourceInfo, raiseEvent } from './types.js';
|
|
57
53
|
|
|
@@ -81,7 +77,7 @@ import {
|
|
|
81
77
|
MJCompanyIntegrationFieldMapEntity,
|
|
82
78
|
MJScheduledJobEntity,
|
|
83
79
|
} from '@memberjunction/core-entities';
|
|
84
|
-
import { ServerExtensionLoader, ServerExtensionConfig, mergeServerExtensionConfigs, prepareServerExtensionConfigs, describeServerExtensionMount } from '@memberjunction/server-extensions-core';
|
|
80
|
+
import { ServerExtensionLoader, ServerExtensionConfig, mergeServerExtensionConfigs, prepareServerExtensionConfigs, describeServerExtensionMount, InstallMediaUpgradeDispatcher, IsGraphQLWsPath } from '@memberjunction/server-extensions-core';
|
|
85
81
|
import { coreReservedServerExtensionRoots } from './serverExtensionReservedRoots.js';
|
|
86
82
|
import { MetadataCacheRefreshIntervalSeconds } from './providerConfigUnits.js';
|
|
87
83
|
|
|
@@ -113,8 +109,16 @@ export * from 'type-graphql';
|
|
|
113
109
|
export { Int, Float, ID } from 'type-graphql';
|
|
114
110
|
export { NewUserBase } from './auth/newUsers.js';
|
|
115
111
|
export { configInfo, DEFAULT_SERVER_CONFIG } from './config.js';
|
|
116
|
-
export { ServerExtensionLoader, BaseServerExtension } from '@memberjunction/server-extensions-core';
|
|
117
|
-
export type {
|
|
112
|
+
export { ServerExtensionLoader, BaseServerExtension, DefaultServerExtensionServiceRegistry } from '@memberjunction/server-extensions-core';
|
|
113
|
+
export type {
|
|
114
|
+
ServerExtensionConfig,
|
|
115
|
+
ServerExtensionPhase,
|
|
116
|
+
ServerExtensionServiceRegistry,
|
|
117
|
+
ServerExtensionInitContext,
|
|
118
|
+
ExtensionInitResult,
|
|
119
|
+
ExtensionHealthResult,
|
|
120
|
+
LoadExtensionsOptions,
|
|
121
|
+
} from '@memberjunction/server-extensions-core';
|
|
118
122
|
export * from './directives/index.js';
|
|
119
123
|
export { NoLog, hasNoLogParameter, getNoLogFields } from './logging/NoLog.js';
|
|
120
124
|
export * from './entitySubclasses/MJEntityPermissionEntityServer.server.js';
|
|
@@ -137,10 +141,21 @@ export { ResolveConfiguredPrincipal, resolvePrincipalFrom } from './auth/princip
|
|
|
137
141
|
export type { ResolvablePrincipal, PrincipalResolution, PrincipalResolutionReason } from './auth/principals.js';
|
|
138
142
|
export { CloneUserForSessionContext } from './auth/sessionUserClone.js';
|
|
139
143
|
|
|
144
|
+
let _currentExtensionLoader: ServerExtensionLoader | null = null;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Returns the active ServerExtensionLoader instance for the running server.
|
|
148
|
+
* Returns null if the server has not been started yet.
|
|
149
|
+
*/
|
|
150
|
+
export function GetServerExtensionLoader(): ServerExtensionLoader | null {
|
|
151
|
+
return _currentExtensionLoader;
|
|
152
|
+
}
|
|
153
|
+
|
|
140
154
|
export * from './generic/PushStatusResolver.js';
|
|
141
155
|
export * from './generic/PubSubManager.js';
|
|
142
156
|
export * from './generic/CacheInvalidationResolver.js';
|
|
143
157
|
export * from './generic/ResolverBase.js';
|
|
158
|
+
export * from './generic/refusalExtensions.js';
|
|
144
159
|
export * from './generic/RunViewResolver.js';
|
|
145
160
|
export * from './resolvers/RunTemplateResolver.js';
|
|
146
161
|
export * from './resolvers/RunAIPromptResolver.js';
|
|
@@ -1166,6 +1181,15 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1166
1181
|
startupLog.LogIf('verbose', '[OAuth] Callback route registered at /oauth/callback');
|
|
1167
1182
|
}
|
|
1168
1183
|
|
|
1184
|
+
// ─── Core HTTP Routes (Static Core Infrastructure) ─────────────────────────
|
|
1185
|
+
// IMPORTANT ARCHITECTURAL NOTE:
|
|
1186
|
+
// The route mounts below are strictly reserved for core framework infrastructure (OAuth, eSignature,
|
|
1187
|
+
// media streaming, realtime WebRTC broker, magic-link, widgets).
|
|
1188
|
+
// ALL new custom routes, external webhooks, vendor adapters, and application integrations MUST be
|
|
1189
|
+
// implemented as Server Extensions (subclasses of BaseServerExtension in @memberjunction/server-extensions-core)
|
|
1190
|
+
// mounted via ServerExtensionLoader — NEVER hardcoded as ad-hoc app.use() in serve().
|
|
1191
|
+
// See guides/SERVER_EXTENSIONS_GUIDE.md for complete patterns and best practices.
|
|
1192
|
+
|
|
1169
1193
|
// ─── eSignature webhook (unauthenticated, registered BEFORE auth) ─────
|
|
1170
1194
|
// Called by external signature providers (DocuSign Connect, etc.) without an MJ bearer token.
|
|
1171
1195
|
// The provider DRIVER verifies the payload signature/HMAC; MJ auth does not apply here.
|
|
@@ -1179,6 +1203,12 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1179
1203
|
app.use('/media', cors<cors.CorsRequest>(), createMediaStreamRouter());
|
|
1180
1204
|
startupLog.LogIf('verbose', '[Media] Streaming route registered at /media/:fileId');
|
|
1181
1205
|
|
|
1206
|
+
// ─── Realtime WebRTC SDP broker (ticket-gated, registered BEFORE auth) ───────
|
|
1207
|
+
if (configInfo.realtime?.enabled) {
|
|
1208
|
+
app.use(REALTIME_SDP_EXCHANGE_PATH, cors<cors.CorsRequest>(), createRealtimeSdpBrokerRouter());
|
|
1209
|
+
startupLog.LogIf('verbose', `[Realtime] WebRTC SDP broker registered at ${REALTIME_SDP_EXCHANGE_PATH}`);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1182
1212
|
// ─── Magic-link routes (MJ-issued, app-scoped external access) ───────────
|
|
1183
1213
|
// Public router (JWKS + redeem) mounts BEFORE the auth middleware; the
|
|
1184
1214
|
// authenticated invite-creation router mounts AFTER it (see below).
|
|
@@ -1212,62 +1242,75 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1212
1242
|
startupLog.LogIf('verbose', `[Widget] Public routes registered at ${WIDGET_MOUNT_PATH}/session and ${WIDGET_MOUNT_PATH}/session/refresh`);
|
|
1213
1243
|
}
|
|
1214
1244
|
|
|
1215
|
-
// ───
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
// to the shared HTTP server. The outbound PlaceTwilioCall mutation reuses the same service.
|
|
1219
|
-
if (configInfo.telephony?.enabled && configInfo.telephony.twilio) {
|
|
1220
|
-
const twilioHandler = createTwilioTelephonyHandler(oauthPublicUrl, configInfo.telephony.twilio);
|
|
1221
|
-
app.use(TWILIO_TELEPHONY_MOUNT_PATH, cors<cors.CorsRequest>(), twilioHandler.publicRouter);
|
|
1222
|
-
twilioHandler.attachMediaStreamServer();
|
|
1223
|
-
SetTwilioTelephonyService(twilioHandler.service);
|
|
1224
|
-
startupLog.LogIf('verbose', `[Telephony] Twilio routes registered at ${TWILIO_TELEPHONY_MOUNT_PATH}/voice + Media-Streams WSS`);
|
|
1225
|
-
}
|
|
1245
|
+
// ─── Server extensions loader & shared service registry ───────────────────
|
|
1246
|
+
const extensionLoader = new ServerExtensionLoader();
|
|
1247
|
+
_currentExtensionLoader = extensionLoader;
|
|
1226
1248
|
|
|
1227
|
-
//
|
|
1228
|
-
//
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1249
|
+
// Open App packages publish their extensions; host mj.config.cjs overlays by DriverClass
|
|
1250
|
+
// Backwards-compatibility shim: synthesize ServerExtensionConfig entries from legacy configInfo.telephony
|
|
1251
|
+
const telephonyExtensionConfigs: ServerExtensionConfig[] = [];
|
|
1252
|
+
if (configInfo.telephony?.enabled) {
|
|
1253
|
+
if (configInfo.telephony.twilio) {
|
|
1254
|
+
telephonyExtensionConfigs.push({
|
|
1255
|
+
Enabled: true,
|
|
1256
|
+
DriverClass: 'TwilioTelephonyExtension',
|
|
1257
|
+
RootPath: '/telephony/twilio',
|
|
1258
|
+
Phase: 'pre-auth',
|
|
1259
|
+
Settings: configInfo.telephony.twilio as unknown as Record<string, unknown>,
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
if (configInfo.telephony.vonage) {
|
|
1263
|
+
telephonyExtensionConfigs.push({
|
|
1264
|
+
Enabled: true,
|
|
1265
|
+
DriverClass: 'VonageTelephonyExtension',
|
|
1266
|
+
RootPath: '/telephony/vonage',
|
|
1267
|
+
Phase: 'pre-auth',
|
|
1268
|
+
Settings: configInfo.telephony.vonage as unknown as Record<string, unknown>,
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
if (configInfo.telephony.ringcentral) {
|
|
1272
|
+
telephonyExtensionConfigs.push({
|
|
1273
|
+
Enabled: true,
|
|
1274
|
+
DriverClass: 'RingCentralTelephonyExtension',
|
|
1275
|
+
RootPath: '/telephony/ringcentral',
|
|
1276
|
+
Phase: 'pre-auth',
|
|
1277
|
+
Settings: configInfo.telephony.ringcentral as unknown as Record<string, unknown>,
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
if (configInfo.telephony.teams?.enabled) {
|
|
1281
|
+
telephonyExtensionConfigs.push({
|
|
1282
|
+
Enabled: true,
|
|
1283
|
+
DriverClass: 'TeamsMeetingsExtension',
|
|
1284
|
+
RootPath: '/meetings/teams',
|
|
1285
|
+
Phase: 'pre-auth',
|
|
1286
|
+
Settings: configInfo.telephony.teams as unknown as Record<string, unknown>,
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1237
1289
|
}
|
|
1238
1290
|
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
// as SIP INVITEs on its own SIP/TLS connection, so there is no public webhook or media WSS to mount.
|
|
1242
|
-
// start() registers the softphone fire-and-forget so SIP registration never blocks boot; the outbound
|
|
1243
|
-
// PlaceRingCentralCall mutation reuses the same service via the runtime holder.
|
|
1244
|
-
if (configInfo.telephony?.enabled && configInfo.telephony.ringcentral) {
|
|
1245
|
-
const ringCentralService = new RingCentralTelephonyService(configInfo.telephony.ringcentral);
|
|
1246
|
-
SetRingCentralTelephonyService(ringCentralService);
|
|
1247
|
-
void ringCentralService.start();
|
|
1248
|
-
startupLog.LogIf('verbose', `[Telephony] RingCentral SIP softphone starting (codec ${configInfo.telephony.ringcentral.codec ?? 'OPUS/16000'})`);
|
|
1249
|
-
}
|
|
1291
|
+
const rawHostExtensions = (configInfo.serverExtensions ?? []) as ServerExtensionConfig[];
|
|
1292
|
+
const mergedHostExtensions = mergeServerExtensionConfigs(telephonyExtensionConfigs, rawHostExtensions);
|
|
1250
1293
|
|
|
1251
|
-
//
|
|
1252
|
-
//
|
|
1253
|
-
//
|
|
1254
|
-
//
|
|
1255
|
-
//
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1294
|
+
// Open App packages publish their extensions; host mj.config.cjs overlays by DriverClass
|
|
1295
|
+
// (and remains the only source for host-only extensions such as Slack/Teams).
|
|
1296
|
+
// extraReservedRoots is derived from the mounts registered above plus graphqlRootPath
|
|
1297
|
+
// so a new pre-auth app.use(...) in serve() must also be added to
|
|
1298
|
+
// coreReservedServerExtensionRoots() — otherwise an Open App can claim it.
|
|
1299
|
+
const extensionConfigs = prepareServerExtensionConfigs(
|
|
1300
|
+
mergeServerExtensionConfigs(
|
|
1301
|
+
options?.serverExtensions ?? [],
|
|
1302
|
+
mergedHostExtensions,
|
|
1303
|
+
),
|
|
1304
|
+
{
|
|
1305
|
+
onInvalid: (message) => LogError(message),
|
|
1306
|
+
onOverlap: (message) => LogStatus(message),
|
|
1307
|
+
extraReservedRoots: coreReservedServerExtensionRoots(graphqlRootPath),
|
|
1308
|
+
},
|
|
1309
|
+
);
|
|
1310
|
+
for (const cfg of extensionConfigs) {
|
|
1311
|
+
LogStatus(`Server extension ${describeServerExtensionMount(cfg)}`);
|
|
1263
1312
|
}
|
|
1264
1313
|
|
|
1265
|
-
// Install the single path-routing WebSocket-upgrade dispatcher AFTER all media WSS routes have
|
|
1266
|
-
// registered. ws 8.x has each {server}-bound WebSocketServer 400 paths it doesn't own, so the GraphQL
|
|
1267
|
-
// socket and the telephony media sockets cannot coexist as separate {server} servers — this strips the
|
|
1268
|
-
// auto-listeners and routes upgrades by path. No-op when no media routes registered (telephony off).
|
|
1269
|
-
InstallMediaUpgradeDispatcher(httpServer, webSocketServer, graphqlRootPath);
|
|
1270
|
-
|
|
1271
1314
|
// ─── Global CORS (before auth so 401 responses include CORS headers) ─────
|
|
1272
1315
|
// Without this, the browser blocks 401 responses from the auth middleware
|
|
1273
1316
|
// because they lack Access-Control-Allow-Origin headers, preventing the
|
|
@@ -1294,35 +1337,16 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1294
1337
|
maxAge: configInfo.cors?.maxAge ?? 86400,
|
|
1295
1338
|
}));
|
|
1296
1339
|
|
|
1297
|
-
// ───
|
|
1340
|
+
// ─── Pre-auth server extensions (before auth — extensions handle their own auth) ─────
|
|
1298
1341
|
// Slack uses HMAC signature verification, Teams uses Bot Framework JWT validation.
|
|
1299
1342
|
// These must be registered before the unified auth middleware so webhook
|
|
1300
1343
|
// requests aren't rejected for lacking an MJ bearer token.
|
|
1301
|
-
const extensionLoader = new ServerExtensionLoader();
|
|
1302
|
-
// Open App packages publish their extensions; host mj.config.cjs overlays by DriverClass
|
|
1303
|
-
// (and remains the only source for host-only extensions such as Slack/Teams).
|
|
1304
|
-
// extraReservedRoots is derived from the mounts registered above plus graphqlRootPath
|
|
1305
|
-
// so a new pre-auth app.use(...) in serve() must also be added to
|
|
1306
|
-
// coreReservedServerExtensionRoots() — otherwise an Open App can claim it.
|
|
1307
|
-
const extensionConfigs = prepareServerExtensionConfigs(
|
|
1308
|
-
mergeServerExtensionConfigs(
|
|
1309
|
-
options?.serverExtensions ?? [],
|
|
1310
|
-
(configInfo.serverExtensions ?? []) as ServerExtensionConfig[],
|
|
1311
|
-
),
|
|
1312
|
-
{
|
|
1313
|
-
onInvalid: (message) => LogError(message),
|
|
1314
|
-
onOverlap: (message) => LogStatus(message),
|
|
1315
|
-
extraReservedRoots: coreReservedServerExtensionRoots(graphqlRootPath),
|
|
1316
|
-
},
|
|
1317
|
-
);
|
|
1318
|
-
// These routes mount BEFORE createUnifiedAuthMiddleware. Name every one so an
|
|
1319
|
-
// operator who installed an Open App for its entities can see the pre-auth HTTP
|
|
1320
|
-
// surface and suppress it with host serverExtensions[].Enabled = false.
|
|
1321
|
-
for (const cfg of extensionConfigs) {
|
|
1322
|
-
LogStatus(`Server extension ${describeServerExtensionMount(cfg)}`);
|
|
1323
|
-
}
|
|
1324
1344
|
if (extensionConfigs.length > 0) {
|
|
1325
|
-
await extensionLoader.LoadExtensions(app, extensionConfigs
|
|
1345
|
+
await extensionLoader.LoadExtensions(app, extensionConfigs, {
|
|
1346
|
+
phase: 'pre-auth',
|
|
1347
|
+
httpServer,
|
|
1348
|
+
publicUrl: oauthPublicUrl,
|
|
1349
|
+
});
|
|
1326
1350
|
}
|
|
1327
1351
|
|
|
1328
1352
|
// Extension health endpoint (always available, returns empty array if no extensions)
|
|
@@ -1372,6 +1396,34 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1372
1396
|
startupLog.LogIf('verbose', `[Widget] Authenticated route registered at ${WIDGET_MOUNT_PATH}/resolve-identity`);
|
|
1373
1397
|
}
|
|
1374
1398
|
|
|
1399
|
+
// ─── Post-auth server extensions ──────────────────────────────────────────
|
|
1400
|
+
// Extensions declaring Phase: 'post-auth' mount after the unified auth middleware
|
|
1401
|
+
// and are protected by JWT authentication by default.
|
|
1402
|
+
if (extensionConfigs.length > 0) {
|
|
1403
|
+
await extensionLoader.LoadExtensions(app, extensionConfigs, {
|
|
1404
|
+
phase: 'post-auth',
|
|
1405
|
+
httpServer,
|
|
1406
|
+
publicUrl: oauthPublicUrl,
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// ─── Cross-extension lifecycle hook: OnAllExtensionsMounted ───────────────
|
|
1411
|
+
// Called after all extensions across both pre-auth and post-auth phases are mounted.
|
|
1412
|
+
// Extensions can wire themselves to services registered by other extensions or core.
|
|
1413
|
+
await extensionLoader.NotifyAllExtensionsMounted({
|
|
1414
|
+
httpServer,
|
|
1415
|
+
publicUrl: oauthPublicUrl,
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
// Install the single path-routing WebSocket-upgrade dispatcher AFTER all extensions across both
|
|
1419
|
+
// pre-auth and post-auth phases have mounted and registered their media WSS routes. ws 8.x has each
|
|
1420
|
+
// {server}-bound WebSocketServer 400 paths it doesn't own, so the GraphQL socket and the telephony
|
|
1421
|
+
// media sockets cannot coexist as separate {server} servers — this strips the auto-listeners and
|
|
1422
|
+
// routes upgrades by path. No-op when no media routes registered (telephony off).
|
|
1423
|
+
InstallMediaUpgradeDispatcher(httpServer, webSocketServer, graphqlRootPath, (req, socket, head) => {
|
|
1424
|
+
return RealtimeProxyServer.Instance.TryHandleUpgrade(req, socket, head);
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1375
1427
|
// ─── REST API endpoints (auth already handled by unified middleware) ─────
|
|
1376
1428
|
const restApiConfig = {
|
|
1377
1429
|
enabled: configInfo.restApiOptions?.enabled ?? false,
|
|
@@ -1537,22 +1589,6 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1537
1589
|
.catch(err => console.warn(`[TaskGraphDispatcher] Startup failed: ${err}`));
|
|
1538
1590
|
}
|
|
1539
1591
|
|
|
1540
|
-
// Launch the calendar / scheduled-bridge loop (M2): poll agent calendars for meeting invites and
|
|
1541
|
-
// start due meeting bridges. Mirrors the SessionJanitor lifecycle (run-once + interval, timer
|
|
1542
|
-
// unref'd). Gated on Teams meetings being enabled (the provider whose scheduled-join is wired) and
|
|
1543
|
-
// reuses the SAME meetings service as the ingress; identities without configured calendar creds are
|
|
1544
|
-
// skipped, so this is a harmless no-op until a Graph-backed identity + token are configured.
|
|
1545
|
-
if (resumeUser && configInfo.telephony?.teams?.enabled) { // global-provider-ok: server-owned background poller under the server's provider + system user
|
|
1546
|
-
const teamsMeetingsService = GetTeamsMeetingsService();
|
|
1547
|
-
if (teamsMeetingsService) {
|
|
1548
|
-
StartCalendarScheduler({
|
|
1549
|
-
Provider: Metadata.Provider, // global-provider-ok: server-owned background poller under the server's single default provider + system user
|
|
1550
|
-
ContextUser: resumeUser,
|
|
1551
|
-
TeamsService: teamsMeetingsService,
|
|
1552
|
-
TeamsConfig: configInfo.telephony.teams,
|
|
1553
|
-
});
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
1592
|
|
|
1557
1593
|
// Set up graceful shutdown handlers
|
|
1558
1594
|
const gracefulShutdown = async (signal: string) => {
|
|
@@ -1562,6 +1598,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1562
1598
|
if (extensionLoader.ExtensionCount > 0) {
|
|
1563
1599
|
try {
|
|
1564
1600
|
await extensionLoader.ShutdownAll();
|
|
1601
|
+
_currentExtensionLoader = null;
|
|
1565
1602
|
console.log('✅ Server extensions shut down');
|
|
1566
1603
|
} catch (error) {
|
|
1567
1604
|
console.error('❌ Error shutting down server extensions:', error);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CompositeKey, DatabaseProviderBase, EntityInfo, LogDebug, UserInfo } from '@memberjunction/core';
|
|
2
2
|
import { Arg, Ctx, Field, InputType, ObjectType, Query, Resolver } from 'type-graphql';
|
|
3
|
-
import { AppContext } from '../types.js';
|
|
3
|
+
import { AppContext, UserPayload } from '../types.js';
|
|
4
4
|
import { CompositeKeyInputType, CompositeKeyOutputType } from '../generic/KeyInputOutputTypes.js';
|
|
5
|
+
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
5
6
|
import { GetReadOnlyProvider } from '../util.js';
|
|
6
7
|
|
|
7
8
|
@InputType()
|
|
@@ -31,8 +32,27 @@ export class EntityRecordNameResult {
|
|
|
31
32
|
RecordName?: string;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Resolves a record's display NAME from its primary key — the lookup behind every foreign-key
|
|
37
|
+
* link, breadcrumb and picker label in the UI.
|
|
38
|
+
*
|
|
39
|
+
* **This is a read of entity data and is gated like one.** It historically took no user context
|
|
40
|
+
* at all, which made it an open side channel: a caller denied READ on an entity's name field
|
|
41
|
+
* could still obtain that name here, because the name never travelled through the resolver that
|
|
42
|
+
* strips denied fields. Field-level security turned that latent gap into a live bypass — the
|
|
43
|
+
* form-field FK control falls through to this query precisely WHEN the joined display column is
|
|
44
|
+
* denied, so the one case the fallback exists for was the case that leaked.
|
|
45
|
+
*
|
|
46
|
+
* Two gates now apply, matching what the generated single-record resolvers do:
|
|
47
|
+
* 1. entity-level read permission ({@link ResolverBase.CheckUserReadPermissions});
|
|
48
|
+
* 2. field-level read permission on the entity's own name field.
|
|
49
|
+
*
|
|
50
|
+
* A field-level denial answers `Success: false` with no name rather than throwing. Callers
|
|
51
|
+
* already treat "no name" as "show the primary key", which is the intended degradation — and it
|
|
52
|
+
* keeps this query from becoming a probe that distinguishes "restricted" from "missing".
|
|
53
|
+
*/
|
|
34
54
|
@Resolver(EntityRecordNameResult)
|
|
35
|
-
export class EntityRecordNameResolver {
|
|
55
|
+
export class EntityRecordNameResolver extends ResolverBase {
|
|
36
56
|
@Query(() => EntityRecordNameResult)
|
|
37
57
|
async GetEntityRecordName(
|
|
38
58
|
@Arg('EntityName', () => String) EntityName: string,
|
|
@@ -40,28 +60,53 @@ export class EntityRecordNameResolver {
|
|
|
40
60
|
@Ctx() { providers, userPayload }: AppContext
|
|
41
61
|
): Promise<EntityRecordNameResult> {
|
|
42
62
|
const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
|
|
63
|
+
this.CheckUserReadPermissions(EntityName, userPayload, md);
|
|
43
64
|
|
|
44
|
-
return await this.InnerGetEntityRecordName(md, EntityName, primaryKey);
|
|
65
|
+
return await this.InnerGetEntityRecordName(md, EntityName, primaryKey, userPayload);
|
|
45
66
|
}
|
|
46
67
|
|
|
47
68
|
@Query(() => [EntityRecordNameResult])
|
|
48
69
|
async GetEntityRecordNames(
|
|
49
70
|
@Arg('info', () => [EntityRecordNameInput]) info: EntityRecordNameInput[],
|
|
50
|
-
@Ctx() {providers}: AppContext
|
|
71
|
+
@Ctx() {providers, userPayload}: AppContext
|
|
51
72
|
): Promise<EntityRecordNameResult[]> {
|
|
52
73
|
const result: EntityRecordNameResult[] = [];
|
|
53
74
|
const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
|
|
54
75
|
for (const i of info) {
|
|
55
|
-
|
|
76
|
+
// Per item, because the batch spans entities: one denied entity must not fail the whole
|
|
77
|
+
// batch, and must not be answered from another entity's grant either.
|
|
78
|
+
this.CheckUserReadPermissions(i.EntityName, userPayload, md);
|
|
79
|
+
result.push(await this.InnerGetEntityRecordName(md, i.EntityName, i.CompositeKey, userPayload));
|
|
56
80
|
}
|
|
57
81
|
return result;
|
|
58
82
|
}
|
|
59
83
|
|
|
60
|
-
async InnerGetEntityRecordName(
|
|
84
|
+
async InnerGetEntityRecordName(
|
|
85
|
+
md: DatabaseProviderBase,
|
|
86
|
+
EntityName: string,
|
|
87
|
+
primaryKey: CompositeKeyInputType,
|
|
88
|
+
userPayload?: UserPayload
|
|
89
|
+
): Promise<EntityRecordNameResult> {
|
|
61
90
|
const pk = new CompositeKey(primaryKey.KeyValuePairs);
|
|
62
91
|
const e = md.Entities.find((e) => e.Name === EntityName);
|
|
63
92
|
if (e) {
|
|
64
|
-
const
|
|
93
|
+
const contextUser = userPayload ? this.GetUserFromPayload(userPayload) : undefined;
|
|
94
|
+
if (this.IsNameFieldDeniedToUser(e, contextUser)) {
|
|
95
|
+
LogDebug(
|
|
96
|
+
`[FieldSecurity] Suppressed GetEntityRecordName for '${EntityName}': ` +
|
|
97
|
+
`the name field is not readable by this user`
|
|
98
|
+
);
|
|
99
|
+
return {
|
|
100
|
+
Success: false,
|
|
101
|
+
Status: `Name for record, or record ${pk.ToString()} itself not found, could be an access issue`,
|
|
102
|
+
CompositeKey: pk,
|
|
103
|
+
EntityName,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Pass the acting user through: the provider signature accepts it, and omitting it ran the
|
|
107
|
+
// underlying lookup with no identity at all — so row-level security had nothing to filter on
|
|
108
|
+
// either. The field-level gate above is the column half; this is the row half.
|
|
109
|
+
const recordName = await md.GetEntityRecordName(e.Name, pk, contextUser);
|
|
65
110
|
if (recordName) return { Success: true, Status: 'OK', CompositeKey: pk, RecordName: recordName, EntityName };
|
|
66
111
|
else
|
|
67
112
|
return {
|
|
@@ -72,6 +117,27 @@ export class EntityRecordNameResolver {
|
|
|
72
117
|
};
|
|
73
118
|
} else return { Success: false, Status: `Entity ${EntityName} not found`, CompositeKey: pk, EntityName };
|
|
74
119
|
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Whether field security withholds this entity's NAME field from the user.
|
|
123
|
+
*
|
|
124
|
+
* Resolves the name field the same way the provider's own lookup does — the designated
|
|
125
|
+
* `IsNameField`, else a field literally called `Name` — so the check cannot drift from the
|
|
126
|
+
* value it is guarding. Fails OPEN only where field security does not apply at all (flag off,
|
|
127
|
+
* no user, no name field), matching every other FLS gate.
|
|
128
|
+
*/
|
|
129
|
+
protected IsNameFieldDeniedToUser(entity: EntityInfo, contextUser?: UserInfo): boolean {
|
|
130
|
+
if (!entity?.EnableFieldLevelSecurity || !contextUser) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const nameField =
|
|
134
|
+
entity.Fields?.find(f => f.IsNameField) ??
|
|
135
|
+
entity.Fields?.find(f => f.Name?.trim().toLowerCase() === 'name');
|
|
136
|
+
if (!nameField) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return entity.GetDeniedReadFields(contextUser).has(nameField.Name.trim().toLowerCase());
|
|
140
|
+
}
|
|
75
141
|
}
|
|
76
142
|
|
|
77
143
|
export default EntityRecordNameResolver;
|