@memberjunction/server 6.1.0-edge.5 → 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 +30 -11
- package/dist/.tsbuildinfo +1 -1
- package/dist/agentSessions/SessionJanitor.js +2 -2
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/auth/exampleNewUserSubClass.d.ts.map +1 -1
- package/dist/auth/exampleNewUserSubClass.js +9 -4
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +2 -8
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +4 -12
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/auth/principals.d.ts +92 -0
- package/dist/auth/principals.d.ts.map +1 -0
- package/dist/auth/principals.js +193 -0
- package/dist/auth/principals.js.map +1 -0
- package/dist/config.d.ts +106 -12
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +41 -4
- 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 +7607 -6853
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +25141 -21645
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +152 -2
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +389 -10
- 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 +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +131 -93
- package/dist/index.js.map +1 -1
- package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -1
- package/dist/realtimeWidget/WidgetSessionService.js +2 -12
- package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
- package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -1
- package/dist/realtimeWidget/visitorIdentity.js +20 -5
- package/dist/realtimeWidget/visitorIdentity.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/FileCategoryResolver.d.ts.map +1 -1
- package/dist/resolvers/FileCategoryResolver.js +1 -2
- package/dist/resolvers/FileCategoryResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +2 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +1 -1
- package/dist/resolvers/QuerySystemUserResolver.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/resolvers/RemoteBrowserActionResolver.d.ts +13 -8
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +45 -8
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +11 -2
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
- package/dist/resolvers/UserViewResolver.js +8 -4
- package/dist/resolvers/UserViewResolver.js.map +1 -1
- package/dist/rest/EntityCRUDHandler.d.ts +2 -1
- package/dist/rest/EntityCRUDHandler.d.ts.map +1 -1
- package/dist/rest/EntityCRUDHandler.js +3 -18
- package/dist/rest/EntityCRUDHandler.js.map +1 -1
- package/dist/rest/RESTEndpointHandler.d.ts +2 -1
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
- package/dist/rest/RESTEndpointHandler.js +3 -16
- package/dist/rest/RESTEndpointHandler.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 -91
- package/src/__tests__/EntityCRUDHandler.test.ts +18 -5
- 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__/RemoteBrowserAudioStream.test.ts +81 -2
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +190 -3
- package/src/__tests__/ResolverBase.readableFieldsTransport.test.ts +106 -0
- package/src/__tests__/UserViewResolver.updateWhereClause.test.ts +114 -0
- package/src/__tests__/WidgetSessionService.test.ts +4 -1
- package/src/__tests__/generatedGraphQLSchema.structural.test.ts +46 -0
- package/src/__tests__/newUserRolesDefault.test.ts +81 -0
- package/src/__tests__/newUsers.test.ts +36 -14
- package/src/__tests__/principals.callSites.test.ts +112 -0
- package/src/__tests__/principals.logging.test.ts +160 -0
- package/src/__tests__/principals.test.ts +305 -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__/resolverBase.refusalMessage.test.ts +88 -0
- package/src/__tests__/serverExtensionReservedRoots.test.ts +79 -11
- package/src/__tests__/visitorIdentity.resolve.test.ts +61 -0
- package/src/agentSessions/SessionJanitor.ts +2 -2
- package/src/auth/exampleNewUserSubClass.ts +9 -6
- package/src/auth/magicLink/MagicLinkService.ts +2 -8
- package/src/auth/newUsers.ts +7 -15
- package/src/auth/principals.ts +257 -0
- package/src/config.ts +44 -4
- package/src/entitySubclasses/MJEntityPermissionEntityServer.server.ts +65 -6
- package/src/generated/graphql-schemas/__mj.ts +18966 -16452
- package/src/generic/ResolverBase.ts +467 -9
- package/src/generic/RunViewResolver.ts +28 -2
- package/src/generic/refusalExtensions.ts +59 -0
- package/src/index.ts +154 -102
- package/src/realtimeWidget/WidgetSessionService.ts +2 -14
- package/src/realtimeWidget/visitorIdentity.ts +21 -6
- package/src/resolvers/EntityRecordNameResolver.ts +74 -8
- package/src/resolvers/FileCategoryResolver.ts +2 -3
- package/src/resolvers/IntegrationDiscoveryResolver.ts +2 -0
- package/src/resolvers/QuerySystemUserResolver.ts +1 -1
- package/src/resolvers/RealtimeClientSessionResolver.ts +128 -2
- package/src/resolvers/RemoteBrowserActionResolver.ts +47 -8
- package/src/resolvers/SyncRolesUsersResolver.ts +11 -2
- package/src/resolvers/UserViewResolver.ts +8 -4
- package/src/rest/EntityCRUDHandler.ts +3 -19
- package/src/rest/RESTEndpointHandler.ts +3 -17
- 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 -96
- 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 -124
- 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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The `extensions` object every write-refusal `GraphQLError` in `ResolverBase` carries.
|
|
3
|
+
*
|
|
4
|
+
* A save refused by `Validate()` / `ValidateAsync()` leaves its reasons — one `ValidationErrorInfo`
|
|
5
|
+
* per field — in `LatestResult.Errors`. Until now the resolver threw only `CompleteMessage`, the
|
|
6
|
+
* flattened prose, so the client got readable text but lost the *structure*: which field each
|
|
7
|
+
* sentence belonged to. That is why a server-side `ValidateAsync()` refusal could only ever toast,
|
|
8
|
+
* while the same rule written in a synchronous `Validate()` painted the offending field red — the
|
|
9
|
+
* client runs sync validation locally and never needed the wire to tell it the field.
|
|
10
|
+
*
|
|
11
|
+
* `RefusalExtensions` keeps the message exactly as before and ADDS `validationErrors`, the
|
|
12
|
+
* `Errors` array flattened by `SerializeValidationErrors` (so a class instance never has to survive
|
|
13
|
+
* JSON), plus `messageIncludesValidationErrors: true` — the server stating that the message it threw
|
|
14
|
+
* is `CompleteMessage`, so the client can keep its own `CompleteMessage` from repeating the text
|
|
15
|
+
* without assuming anything about what the server put in `message`. `GraphQLDataProvider.Save()` rehydrates it into `LatestResult.Errors` on the client, and the
|
|
16
|
+
* form paints fields from there — the same path the sync branch already uses.
|
|
17
|
+
*
|
|
18
|
+
* Kept out of `ResolverBase.ts` so it is unit-testable without booting the resolver's dependency
|
|
19
|
+
* graph, and so the three resolvers (create / update / delete) share one definition of the shape.
|
|
20
|
+
*/
|
|
21
|
+
import type { BaseEntityResult } from '@memberjunction/core';
|
|
22
|
+
import { SerializeValidationErrors, type SerializedValidationError } from '@memberjunction/global';
|
|
23
|
+
|
|
24
|
+
/** Error codes the write resolvers use; kept distinct so a client can tell the operations apart. */
|
|
25
|
+
export type WriteRefusalCode = 'CREATE_ENTITY_ERROR' | 'SAVE_ENTITY_ERROR' | 'DELETE_ENTITY_ERROR';
|
|
26
|
+
|
|
27
|
+
/** The `extensions` payload of a write-refusal `GraphQLError`. */
|
|
28
|
+
export type WriteRefusalExtensions = {
|
|
29
|
+
code: WriteRefusalCode;
|
|
30
|
+
entityName: string;
|
|
31
|
+
/**
|
|
32
|
+
* Present only when the refusal carried structured errors. Absent (not an empty array) otherwise,
|
|
33
|
+
* so a client can distinguish "validation said no" from "the database said no".
|
|
34
|
+
*/
|
|
35
|
+
validationErrors?: SerializedValidationError[];
|
|
36
|
+
/**
|
|
37
|
+
* Present (and `true`) alongside `validationErrors`: a statement by THIS producer that the
|
|
38
|
+
* error's `message` is `LatestResult.CompleteMessage`, i.e. it already renders every entry of
|
|
39
|
+
* `validationErrors`. The client sets `BaseEntityResult.MessageIncludesErrors` from this — a
|
|
40
|
+
* fact the server asserts on the wire — rather than assuming what the server put in `message`.
|
|
41
|
+
* A server that does not send it gets the client's pre-existing behaviour (the text may repeat).
|
|
42
|
+
*/
|
|
43
|
+
messageIncludesValidationErrors?: true;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Builds the `extensions` for a refused create / update / delete.
|
|
48
|
+
*
|
|
49
|
+
* @param code - Which operation was refused.
|
|
50
|
+
* @param entityName - The entity the operation targeted.
|
|
51
|
+
* @param result - The entity's `LatestResult` after the failed call, if any.
|
|
52
|
+
* @returns `{ code, entityName }`, plus `validationErrors` when `result.Errors` had anything to carry.
|
|
53
|
+
*/
|
|
54
|
+
export function RefusalExtensions(code: WriteRefusalCode, entityName: string, result: BaseEntityResult | null | undefined): WriteRefusalExtensions {
|
|
55
|
+
const validationErrors = SerializeValidationErrors(result?.Errors);
|
|
56
|
+
return validationErrors.length > 0
|
|
57
|
+
? { code, entityName, validationErrors, messageIncludesValidationErrors: true }
|
|
58
|
+
: { code, entityName };
|
|
59
|
+
}
|
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';
|
|
@@ -127,12 +131,31 @@ export {
|
|
|
127
131
|
} from './auth/index.js';
|
|
128
132
|
export * from './auth/APIKeyScopeAuth.js';
|
|
129
133
|
export * from './auth/actingContextResolver.js';
|
|
134
|
+
// The context-user ladder (#4209). Public because `auth/exampleNewUserSubClass.ts` — the template
|
|
135
|
+
// integrators are told to copy into their OWN package — resolves through it, and `package.json`
|
|
136
|
+
// publishes only "."; without this the example compiles here and cannot be reused anywhere else.
|
|
137
|
+
// `ReportedMisconfigurationCount` / `MAX_REPORTED_MISCONFIGURATIONS` are deliberately NOT here:
|
|
138
|
+
// they exist so the LRU's bound is assertable, the tests import them from the module directly, and
|
|
139
|
+
// a published export is a maintenance commitment no caller asked for.
|
|
140
|
+
export { ResolveConfiguredPrincipal, resolvePrincipalFrom } from './auth/principals.js';
|
|
141
|
+
export type { ResolvablePrincipal, PrincipalResolution, PrincipalResolutionReason } from './auth/principals.js';
|
|
130
142
|
export { CloneUserForSessionContext } from './auth/sessionUserClone.js';
|
|
131
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
|
+
|
|
132
154
|
export * from './generic/PushStatusResolver.js';
|
|
133
155
|
export * from './generic/PubSubManager.js';
|
|
134
156
|
export * from './generic/CacheInvalidationResolver.js';
|
|
135
157
|
export * from './generic/ResolverBase.js';
|
|
158
|
+
export * from './generic/refusalExtensions.js';
|
|
136
159
|
export * from './generic/RunViewResolver.js';
|
|
137
160
|
export * from './resolvers/RunTemplateResolver.js';
|
|
138
161
|
export * from './resolvers/RunAIPromptResolver.js';
|
|
@@ -1158,6 +1181,15 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1158
1181
|
startupLog.LogIf('verbose', '[OAuth] Callback route registered at /oauth/callback');
|
|
1159
1182
|
}
|
|
1160
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
|
+
|
|
1161
1193
|
// ─── eSignature webhook (unauthenticated, registered BEFORE auth) ─────
|
|
1162
1194
|
// Called by external signature providers (DocuSign Connect, etc.) without an MJ bearer token.
|
|
1163
1195
|
// The provider DRIVER verifies the payload signature/HMAC; MJ auth does not apply here.
|
|
@@ -1171,6 +1203,12 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1171
1203
|
app.use('/media', cors<cors.CorsRequest>(), createMediaStreamRouter());
|
|
1172
1204
|
startupLog.LogIf('verbose', '[Media] Streaming route registered at /media/:fileId');
|
|
1173
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
|
+
|
|
1174
1212
|
// ─── Magic-link routes (MJ-issued, app-scoped external access) ───────────
|
|
1175
1213
|
// Public router (JWKS + redeem) mounts BEFORE the auth middleware; the
|
|
1176
1214
|
// authenticated invite-creation router mounts AFTER it (see below).
|
|
@@ -1204,110 +1242,111 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1204
1242
|
startupLog.LogIf('verbose', `[Widget] Public routes registered at ${WIDGET_MOUNT_PATH}/session and ${WIDGET_MOUNT_PATH}/session/refresh`);
|
|
1205
1243
|
}
|
|
1206
1244
|
|
|
1207
|
-
// ───
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
// to the shared HTTP server. The outbound PlaceTwilioCall mutation reuses the same service.
|
|
1211
|
-
if (configInfo.telephony?.enabled && configInfo.telephony.twilio) {
|
|
1212
|
-
const twilioHandler = createTwilioTelephonyHandler(oauthPublicUrl, configInfo.telephony.twilio);
|
|
1213
|
-
app.use(TWILIO_TELEPHONY_MOUNT_PATH, cors<cors.CorsRequest>(), twilioHandler.publicRouter);
|
|
1214
|
-
twilioHandler.attachMediaStreamServer();
|
|
1215
|
-
SetTwilioTelephonyService(twilioHandler.service);
|
|
1216
|
-
startupLog.LogIf('verbose', `[Telephony] Twilio routes registered at ${TWILIO_TELEPHONY_MOUNT_PATH}/voice + Media-Streams WSS`);
|
|
1217
|
-
}
|
|
1245
|
+
// ─── Server extensions loader & shared service registry ───────────────────
|
|
1246
|
+
const extensionLoader = new ServerExtensionLoader();
|
|
1247
|
+
_currentExtensionLoader = extensionLoader;
|
|
1218
1248
|
|
|
1219
|
-
//
|
|
1220
|
-
//
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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
|
+
}
|
|
1229
1289
|
}
|
|
1230
1290
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
// as SIP INVITEs on its own SIP/TLS connection, so there is no public webhook or media WSS to mount.
|
|
1234
|
-
// start() registers the softphone fire-and-forget so SIP registration never blocks boot; the outbound
|
|
1235
|
-
// PlaceRingCentralCall mutation reuses the same service via the runtime holder.
|
|
1236
|
-
if (configInfo.telephony?.enabled && configInfo.telephony.ringcentral) {
|
|
1237
|
-
const ringCentralService = new RingCentralTelephonyService(configInfo.telephony.ringcentral);
|
|
1238
|
-
SetRingCentralTelephonyService(ringCentralService);
|
|
1239
|
-
void ringCentralService.start();
|
|
1240
|
-
startupLog.LogIf('verbose', `[Telephony] RingCentral SIP softphone starting (codec ${configInfo.telephony.ringcentral.codec ?? 'OPUS/16000'})`);
|
|
1241
|
-
}
|
|
1291
|
+
const rawHostExtensions = (configInfo.serverExtensions ?? []) as ServerExtensionConfig[];
|
|
1292
|
+
const mergedHostExtensions = mergeServerExtensionConfigs(telephonyExtensionConfigs, rawHostExtensions);
|
|
1242
1293
|
|
|
1243
|
-
//
|
|
1244
|
-
//
|
|
1245
|
-
//
|
|
1246
|
-
//
|
|
1247
|
-
//
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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)}`);
|
|
1255
1312
|
}
|
|
1256
1313
|
|
|
1257
|
-
// Install the single path-routing WebSocket-upgrade dispatcher AFTER all media WSS routes have
|
|
1258
|
-
// registered. ws 8.x has each {server}-bound WebSocketServer 400 paths it doesn't own, so the GraphQL
|
|
1259
|
-
// socket and the telephony media sockets cannot coexist as separate {server} servers — this strips the
|
|
1260
|
-
// auto-listeners and routes upgrades by path. No-op when no media routes registered (telephony off).
|
|
1261
|
-
InstallMediaUpgradeDispatcher(httpServer, webSocketServer, graphqlRootPath);
|
|
1262
|
-
|
|
1263
1314
|
// ─── Global CORS (before auth so 401 responses include CORS headers) ─────
|
|
1264
1315
|
// Without this, the browser blocks 401 responses from the auth middleware
|
|
1265
1316
|
// because they lack Access-Control-Allow-Origin headers, preventing the
|
|
1266
1317
|
// client from reading the error code and triggering token refresh.
|
|
1267
1318
|
const corsAllowed = configInfo.cors?.allowedOrigins ?? ['*'];
|
|
1319
|
+
const corsWildcard = corsAllowed.includes('*');
|
|
1320
|
+
// SECURITY: never combine credentials with a wildcard/reflect-any-origin policy. When
|
|
1321
|
+
// allowedOrigins is ['*'] the origin callback reflects the caller's Origin, and pairing that
|
|
1322
|
+
// with Access-Control-Allow-Credentials: true lets any site a signed-in user visits make
|
|
1323
|
+
// credentialed cross-origin reads. MJ's primary auth is a Bearer token (not auto-sent
|
|
1324
|
+
// cross-origin), so dropping credentials under the wildcard default is safe; deployments that
|
|
1325
|
+
// genuinely need credentialed CORS must configure an explicit allowedOrigins list.
|
|
1268
1326
|
app.use(cors<cors.CorsRequest>({
|
|
1269
1327
|
origin: (origin, callback) => {
|
|
1270
1328
|
// Allow all origins when ['*'] (default/backward-compatible),
|
|
1271
1329
|
// or when no Origin header (server-to-server calls).
|
|
1272
|
-
if (
|
|
1330
|
+
if (corsWildcard || !origin || corsAllowed.includes(origin)) {
|
|
1273
1331
|
callback(null, true);
|
|
1274
1332
|
} else {
|
|
1275
1333
|
callback(new Error(`Origin ${origin} not allowed by CORS`));
|
|
1276
1334
|
}
|
|
1277
1335
|
},
|
|
1278
|
-
credentials: configInfo.cors?.allowCredentials ?? true,
|
|
1336
|
+
credentials: corsWildcard ? false : (configInfo.cors?.allowCredentials ?? true),
|
|
1279
1337
|
maxAge: configInfo.cors?.maxAge ?? 86400,
|
|
1280
1338
|
}));
|
|
1281
1339
|
|
|
1282
|
-
// ───
|
|
1340
|
+
// ─── Pre-auth server extensions (before auth — extensions handle their own auth) ─────
|
|
1283
1341
|
// Slack uses HMAC signature verification, Teams uses Bot Framework JWT validation.
|
|
1284
1342
|
// These must be registered before the unified auth middleware so webhook
|
|
1285
1343
|
// requests aren't rejected for lacking an MJ bearer token.
|
|
1286
|
-
const extensionLoader = new ServerExtensionLoader();
|
|
1287
|
-
// Open App packages publish their extensions; host mj.config.cjs overlays by DriverClass
|
|
1288
|
-
// (and remains the only source for host-only extensions such as Slack/Teams).
|
|
1289
|
-
// extraReservedRoots is derived from the mounts registered above plus graphqlRootPath
|
|
1290
|
-
// so a new pre-auth app.use(...) in serve() must also be added to
|
|
1291
|
-
// coreReservedServerExtensionRoots() — otherwise an Open App can claim it.
|
|
1292
|
-
const extensionConfigs = prepareServerExtensionConfigs(
|
|
1293
|
-
mergeServerExtensionConfigs(
|
|
1294
|
-
options?.serverExtensions ?? [],
|
|
1295
|
-
(configInfo.serverExtensions ?? []) as ServerExtensionConfig[],
|
|
1296
|
-
),
|
|
1297
|
-
{
|
|
1298
|
-
onInvalid: (message) => LogError(message),
|
|
1299
|
-
onOverlap: (message) => LogStatus(message),
|
|
1300
|
-
extraReservedRoots: coreReservedServerExtensionRoots(graphqlRootPath),
|
|
1301
|
-
},
|
|
1302
|
-
);
|
|
1303
|
-
// These routes mount BEFORE createUnifiedAuthMiddleware. Name every one so an
|
|
1304
|
-
// operator who installed an Open App for its entities can see the pre-auth HTTP
|
|
1305
|
-
// surface and suppress it with host serverExtensions[].Enabled = false.
|
|
1306
|
-
for (const cfg of extensionConfigs) {
|
|
1307
|
-
LogStatus(`Server extension ${describeServerExtensionMount(cfg)}`);
|
|
1308
|
-
}
|
|
1309
1344
|
if (extensionConfigs.length > 0) {
|
|
1310
|
-
await extensionLoader.LoadExtensions(app, extensionConfigs
|
|
1345
|
+
await extensionLoader.LoadExtensions(app, extensionConfigs, {
|
|
1346
|
+
phase: 'pre-auth',
|
|
1347
|
+
httpServer,
|
|
1348
|
+
publicUrl: oauthPublicUrl,
|
|
1349
|
+
});
|
|
1311
1350
|
}
|
|
1312
1351
|
|
|
1313
1352
|
// Extension health endpoint (always available, returns empty array if no extensions)
|
|
@@ -1357,6 +1396,34 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1357
1396
|
startupLog.LogIf('verbose', `[Widget] Authenticated route registered at ${WIDGET_MOUNT_PATH}/resolve-identity`);
|
|
1358
1397
|
}
|
|
1359
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
|
+
|
|
1360
1427
|
// ─── REST API endpoints (auth already handled by unified middleware) ─────
|
|
1361
1428
|
const restApiConfig = {
|
|
1362
1429
|
enabled: configInfo.restApiOptions?.enabled ?? false,
|
|
@@ -1522,22 +1589,6 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1522
1589
|
.catch(err => console.warn(`[TaskGraphDispatcher] Startup failed: ${err}`));
|
|
1523
1590
|
}
|
|
1524
1591
|
|
|
1525
|
-
// Launch the calendar / scheduled-bridge loop (M2): poll agent calendars for meeting invites and
|
|
1526
|
-
// start due meeting bridges. Mirrors the SessionJanitor lifecycle (run-once + interval, timer
|
|
1527
|
-
// unref'd). Gated on Teams meetings being enabled (the provider whose scheduled-join is wired) and
|
|
1528
|
-
// reuses the SAME meetings service as the ingress; identities without configured calendar creds are
|
|
1529
|
-
// skipped, so this is a harmless no-op until a Graph-backed identity + token are configured.
|
|
1530
|
-
if (resumeUser && configInfo.telephony?.teams?.enabled) { // global-provider-ok: server-owned background poller under the server's provider + system user
|
|
1531
|
-
const teamsMeetingsService = GetTeamsMeetingsService();
|
|
1532
|
-
if (teamsMeetingsService) {
|
|
1533
|
-
StartCalendarScheduler({
|
|
1534
|
-
Provider: Metadata.Provider, // global-provider-ok: server-owned background poller under the server's single default provider + system user
|
|
1535
|
-
ContextUser: resumeUser,
|
|
1536
|
-
TeamsService: teamsMeetingsService,
|
|
1537
|
-
TeamsConfig: configInfo.telephony.teams,
|
|
1538
|
-
});
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
1592
|
|
|
1542
1593
|
// Set up graceful shutdown handlers
|
|
1543
1594
|
const gracefulShutdown = async (signal: string) => {
|
|
@@ -1547,6 +1598,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
1547
1598
|
if (extensionLoader.ExtensionCount > 0) {
|
|
1548
1599
|
try {
|
|
1549
1600
|
await extensionLoader.ShutdownAll();
|
|
1601
|
+
_currentExtensionLoader = null;
|
|
1550
1602
|
console.log('✅ Server extensions shut down');
|
|
1551
1603
|
} catch (error) {
|
|
1552
1604
|
console.error('❌ Error shutting down server extensions:', error);
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { Metadata, RunView, UserInfo, LogError, LogStatus } from '@memberjunction/core';
|
|
18
18
|
import { MJLruCache, UUIDsEqual } from '@memberjunction/global';
|
|
19
19
|
import type { MJConversationWidgetInstanceEntity, MJConversationEntity } from '@memberjunction/core-entities';
|
|
20
|
-
import {
|
|
20
|
+
import { ResolveConfiguredPrincipal } from '../auth/principals.js';
|
|
21
21
|
import { MagicLinkKeyManager } from '../auth/magicLink/MagicLinkKeys.js';
|
|
22
22
|
import { generateSessionId } from '../auth/magicLink/magicLinkCore.js';
|
|
23
23
|
import { MagicLinkService } from '../auth/magicLink/MagicLinkService.js';
|
|
@@ -694,19 +694,7 @@ export class WidgetSessionService {
|
|
|
694
694
|
|
|
695
695
|
/** Resolves the server-side context user used to READ widget config (not the guest principal). */
|
|
696
696
|
private resolveLookupUser(): UserInfo | null {
|
|
697
|
-
|
|
698
|
-
if (candidate) {
|
|
699
|
-
const byName = UserCache.Instance.UserByName(candidate);
|
|
700
|
-
if (byName) {
|
|
701
|
-
return byName;
|
|
702
|
-
}
|
|
703
|
-
LogError(`[Widget] Configured lookup user '${candidate}' not found; falling back to system/Owner.`);
|
|
704
|
-
}
|
|
705
|
-
return (
|
|
706
|
-
UserCache.Instance.GetSystemUser() ??
|
|
707
|
-
UserCache.Users.find((u) => u.IsActive && u.Type?.trim().toLowerCase() === 'owner') ??
|
|
708
|
-
null
|
|
709
|
-
);
|
|
697
|
+
return ResolveConfiguredPrincipal(this.config.contextUserForLookup, 'Widget');
|
|
710
698
|
}
|
|
711
699
|
|
|
712
700
|
/**
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* @module @memberjunction/server/widget
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { RunView, UserInfo, LogError, LogStatus, type IMetadataProvider } from '@memberjunction/core';
|
|
27
|
+
import { CompositeKey, RunView, UserInfo, LogError, LogStatus, type IMetadataProvider } from '@memberjunction/core';
|
|
28
28
|
import type { MJConversationEntity, MJAIAgentNoteEntity } from '@memberjunction/core-entities';
|
|
29
29
|
|
|
30
30
|
const CONVERSATIONS_ENTITY = 'MJ: Conversations';
|
|
@@ -60,17 +60,24 @@ export async function resolveIdentityByEmail(
|
|
|
60
60
|
}
|
|
61
61
|
const entityName = target?.entityName?.trim() || 'Users';
|
|
62
62
|
const emailField = target?.emailField?.trim() || 'Email';
|
|
63
|
-
const
|
|
64
|
-
if (!
|
|
63
|
+
const entityInfo = provider.EntityByName(entityName);
|
|
64
|
+
if (!entityInfo) {
|
|
65
65
|
LogError(`[VisitorIdentity] identity-resolution entity '${entityName}' not found in metadata.`);
|
|
66
66
|
return undefined;
|
|
67
67
|
}
|
|
68
|
+
const entityId = entityInfo.ID;
|
|
69
|
+
// The identity entity is configurable, so its key column(s) can have any name — and may be
|
|
70
|
+
// composite. Select every key column and carry the record id in the compact record-id form
|
|
71
|
+
// (the bare value for a single-column key, "F1|v1||F2|v2" for a composite one) that
|
|
72
|
+
// CompositeKey.FromURLSegment reads back, so LinkedRecordID / PrimaryScopeRecordID round-trip
|
|
73
|
+
// for any key shape instead of being truncated to the first column.
|
|
74
|
+
const pkNames = entityInfo.PrimaryKeys.map(pk => pk.Name);
|
|
68
75
|
const rv = new RunView();
|
|
69
|
-
const result = await rv.RunView<
|
|
76
|
+
const result = await rv.RunView<Record<string, unknown>>(
|
|
70
77
|
{
|
|
71
78
|
EntityName: entityName,
|
|
72
79
|
ExtraFilter: `${emailField} = '${trimmed.replace(/'/g, "''")}'`,
|
|
73
|
-
Fields:
|
|
80
|
+
Fields: pkNames,
|
|
74
81
|
MaxRows: 1,
|
|
75
82
|
ResultType: 'simple',
|
|
76
83
|
},
|
|
@@ -80,7 +87,15 @@ export async function resolveIdentityByEmail(
|
|
|
80
87
|
LogError(`[VisitorIdentity] identity lookup failed for '${entityName}.${emailField}': ${result.ErrorMessage}`);
|
|
81
88
|
return undefined;
|
|
82
89
|
}
|
|
83
|
-
const
|
|
90
|
+
const row = result.Results?.[0];
|
|
91
|
+
if (!row) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
const key = CompositeKey.FromEntityRecord(entityInfo, row);
|
|
95
|
+
if (key.KeyValuePairs.length === 0 || key.KeyValuePairs.some(kv => kv.Value == null)) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
const recordId = key.ToCompactURLSegment();
|
|
84
99
|
return recordId ? { entityId, recordId } : undefined;
|
|
85
100
|
} catch (e) {
|
|
86
101
|
LogError(`[VisitorIdentity] resolveIdentityByEmail failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -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;
|
|
@@ -14,9 +14,8 @@ export class FileCategoryResolver extends FileCategoryResolverBase {
|
|
|
14
14
|
@Arg('options___', () => DeleteOptionsInput) options: DeleteOptionsInput,
|
|
15
15
|
@Ctx() { providers, userPayload }: AppContext
|
|
16
16
|
) {
|
|
17
|
-
const key =
|
|
18
|
-
|
|
19
|
-
const p = GetReadWriteProvider(providers);
|
|
17
|
+
const key = CompositeKey.FromID(ID); // first-pk-ok: this mutation deletes from MJ: File Categories (core entity keyed by ID) — see the GetEntityObject below
|
|
18
|
+
const p = GetReadWriteProvider(providers);
|
|
20
19
|
|
|
21
20
|
if (!(await this.BeforeDelete(p, key))) {
|
|
22
21
|
return null;
|