@memberjunction/server 6.1.0-edge.1 → 6.1.0-edge.3
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/LICENSE +180 -4
- package/dist/auth/AuthProviderCatalogRouter.d.ts +31 -0
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -0
- package/dist/auth/AuthProviderCatalogRouter.js +69 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -0
- package/dist/auth/AuthProviderEngine.d.ts +96 -0
- package/dist/auth/AuthProviderEngine.d.ts.map +1 -0
- package/dist/auth/AuthProviderEngine.js +245 -0
- package/dist/auth/AuthProviderEngine.js.map +1 -0
- package/dist/auth/exampleNewUserSubClass.js +1 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/index.d.ts +11 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +65 -18
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.d.ts +64 -1
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +117 -5
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +1 -1
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/newUsers.js +1 -1
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/config.d.ts +89 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +52 -35
- package/dist/config.js.map +1 -1
- package/dist/context.js +2 -2
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +540 -1595
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +2755 -16857
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +20 -4
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +58 -21
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +31 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +183 -54
- package/dist/index.js.map +1 -1
- package/dist/integration/RSUProgressBridge.d.ts +83 -0
- package/dist/integration/RSUProgressBridge.d.ts.map +1 -0
- package/dist/integration/RSUProgressBridge.js +230 -0
- package/dist/integration/RSUProgressBridge.js.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts +52 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.js +50 -0
- package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
- package/dist/logging/secretRedactor.d.ts +9 -0
- package/dist/logging/secretRedactor.d.ts.map +1 -1
- package/dist/logging/secretRedactor.js +39 -0
- package/dist/logging/secretRedactor.js.map +1 -1
- package/dist/middleware/BaseServerMiddleware.d.ts +5 -0
- package/dist/middleware/BaseServerMiddleware.d.ts.map +1 -1
- package/dist/middleware/BaseServerMiddleware.js +5 -0
- package/dist/middleware/BaseServerMiddleware.js.map +1 -1
- package/dist/providerConfigUnits.d.ts +20 -0
- package/dist/providerConfigUnits.d.ts.map +1 -0
- package/dist/providerConfigUnits.js +22 -0
- package/dist/providerConfigUnits.js.map +1 -0
- package/dist/realtimeWidget/WidgetSessionService.js +1 -1
- package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +12 -0
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/EntityPermissionResolver.js +1 -1
- package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +34 -4
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +218 -28
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +85 -3
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +302 -82
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +5 -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 +12 -3
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +33 -14
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +5 -0
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +3 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +20 -7
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchEntitiesResolver.js +1 -1
- package/dist/resolvers/SearchEntitiesResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +2 -2
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +18 -0
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.js +75 -0
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.js +1 -1
- package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
- package/dist/resolvers/UserResolver.d.ts +19 -17
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +66 -67
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/currentUserRoles.d.ts +19 -0
- package/dist/resolvers/currentUserRoles.d.ts.map +1 -0
- package/dist/resolvers/currentUserRoles.js +36 -0
- package/dist/resolvers/currentUserRoles.js.map +1 -0
- package/dist/rest/MediaAccessKeys.d.ts +17 -0
- package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
- package/dist/rest/MediaAccessKeys.js +42 -1
- package/dist/rest/MediaAccessKeys.js.map +1 -1
- package/dist/rest/MediaStreamHandler.d.ts +3 -2
- package/dist/rest/MediaStreamHandler.d.ts.map +1 -1
- package/dist/rest/MediaStreamHandler.js +83 -2
- package/dist/rest/MediaStreamHandler.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.d.ts +49 -1
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +130 -44
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/SignatureWebhookHandler.js +1 -1
- package/dist/rest/SignatureWebhookHandler.js.map +1 -1
- package/dist/rest/UploadTokenManager.d.ts +90 -0
- package/dist/rest/UploadTokenManager.d.ts.map +1 -0
- package/dist/rest/UploadTokenManager.js +156 -0
- package/dist/rest/UploadTokenManager.js.map +1 -0
- package/dist/services/DurableEntityActionTaskSubmitter.d.ts +26 -0
- package/dist/services/DurableEntityActionTaskSubmitter.d.ts.map +1 -0
- package/dist/services/DurableEntityActionTaskSubmitter.js +55 -0
- package/dist/services/DurableEntityActionTaskSubmitter.js.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts +48 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.js +132 -0
- package/dist/services/IntegrationSyncWorkerService.js.map +1 -0
- package/dist/services/ScheduledJobsService.js +1 -1
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
- package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +37 -2
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +34 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphActionRunner.js +95 -0
- package/dist/services/TaskGraphActionRunner.js.map +1 -0
- package/dist/services/TaskGraphAgentRunner.d.ts +20 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +53 -0
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +12 -1
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/services/TaskGraphPromptRunner.d.ts +31 -0
- package/dist/services/TaskGraphPromptRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphPromptRunner.js +134 -0
- package/dist/services/TaskGraphPromptRunner.js.map +1 -0
- package/dist/telephony/RingCentralTelephonyService.js +1 -1
- package/dist/telephony/RingCentralTelephonyService.js.map +1 -1
- package/dist/telephony/TwilioTelephonyRouter.js +1 -1
- package/dist/telephony/TwilioTelephonyRouter.js.map +1 -1
- package/dist/telephony/VonageTelephonyRouter.js +1 -1
- package/dist/telephony/VonageTelephonyRouter.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +91 -91
- package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
- package/src/__tests__/AuthProviderEngine.test.ts +278 -0
- package/src/__tests__/EntityCRUDHandler.test.ts +378 -0
- package/src/__tests__/IntegrationSyncWorkerService.test.ts +275 -0
- package/src/__tests__/OAuthCallbackHandler.openRedirect.test.ts +117 -0
- package/src/__tests__/OAuthCallbackHandler.xss.test.ts +4 -1
- package/src/__tests__/RESTEndpointHandler.test.ts +672 -0
- package/src/__tests__/RSUProgressBridge.test.ts +250 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -2
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +1 -1
- package/src/__tests__/ResolverBase.frozenRecordMapping.test.ts +133 -0
- package/src/__tests__/ResolverBase.transportMapping.test.ts +289 -0
- package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
- package/src/__tests__/SessionManager.test.ts +1 -1
- package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
- package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
- package/src/__tests__/WidgetSessionService.test.ts +1 -1
- package/src/__tests__/config-units.test.ts +74 -0
- package/src/__tests__/databaseAbstraction.test.ts +1 -1
- package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +1 -1
- package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
- package/src/__tests__/newUsers.test.ts +726 -0
- package/src/__tests__/resolverBase.rls.test.ts +3 -0
- package/src/__tests__/secretRedactor.failOpen.test.ts +135 -0
- package/src/__tests__/uploadTokenManager.test.ts +159 -0
- package/src/__tests__/widgetGuestElevation.test.ts +1 -1
- package/src/auth/AuthProviderCatalogRouter.ts +76 -0
- package/src/auth/AuthProviderEngine.ts +253 -0
- package/src/auth/exampleNewUserSubClass.ts +1 -1
- package/src/auth/index.ts +69 -21
- package/src/auth/initializeProviders.ts +128 -7
- package/src/auth/magicLink/MagicLinkService.ts +1 -1
- package/src/auth/newUsers.ts +1 -1
- package/src/config.ts +55 -36
- package/src/context.ts +2 -2
- package/src/generated/generated.ts +2038 -10529
- package/src/generic/ResolverBase.ts +63 -21
- package/src/generic/RunViewResolver.ts +31 -0
- package/src/index.ts +199 -56
- package/src/integration/RSUProgressBridge.ts +248 -0
- package/src/integration/SchemaRefreshLaunch.ts +77 -0
- package/src/logging/secretRedactor.ts +43 -0
- package/src/middleware/BaseServerMiddleware.ts +5 -0
- package/src/providerConfigUnits.ts +21 -0
- package/src/realtimeWidget/WidgetSessionService.ts +1 -1
- package/src/realtimeWidget/widgetGuestElevation.ts +1 -1
- package/src/resolvers/ActionResolver.ts +13 -0
- package/src/resolvers/ComponentRegistryResolver.ts +1 -1
- package/src/resolvers/EntityPermissionResolver.ts +1 -1
- package/src/resolvers/FileResolver.ts +200 -40
- package/src/resolvers/IntegrationDiscoveryResolver.ts +328 -70
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -1
- package/src/resolvers/QueryResolver.ts +25 -11
- package/src/resolvers/QuerySystemUserResolver.ts +28 -13
- package/src/resolvers/RealtimeClientSessionResolver.ts +13 -0
- package/src/resolvers/RunAIAgentResolver.ts +21 -6
- package/src/resolvers/SearchEntitiesResolver.ts +1 -1
- package/src/resolvers/SqlLoggingConfigResolver.ts +1 -2
- package/src/resolvers/SyncRolesUsersResolver.ts +2 -2
- package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
- package/src/resolvers/UserFavoriteResolver.ts +1 -1
- package/src/resolvers/UserResolver.ts +99 -60
- package/src/resolvers/currentUserRoles.ts +47 -0
- package/src/rest/MediaAccessKeys.ts +47 -1
- package/src/rest/MediaStreamHandler.ts +98 -3
- package/src/rest/OAuthCallbackHandler.ts +150 -42
- package/src/rest/SignatureWebhookHandler.ts +1 -1
- package/src/rest/UploadTokenManager.ts +211 -0
- package/src/services/DurableEntityActionTaskSubmitter.ts +78 -0
- package/src/services/IntegrationSyncWorkerService.ts +141 -0
- package/src/services/ScheduledJobsService.ts +1 -1
- package/src/services/StartTaskGraphDispatcher.ts +38 -1
- package/src/services/TaskGraphActionRunner.ts +99 -0
- package/src/services/TaskGraphAgentRunner.ts +58 -1
- package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
- package/src/services/TaskGraphPromptRunner.ts +144 -0
- package/src/telephony/RingCentralTelephonyService.ts +1 -1
- package/src/telephony/TwilioTelephonyRouter.ts +1 -1
- package/src/telephony/VonageTelephonyRouter.ts +1 -1
- package/src/types.ts +6 -0
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import express from 'express';
|
|
16
16
|
import { LogError, LogStatus, RunView, UserInfo } from '@memberjunction/core';
|
|
17
|
-
import { UUIDsEqual } from '@memberjunction/global';
|
|
18
|
-
import { UserCache } from '@memberjunction/
|
|
17
|
+
import { IsValidUUID, UUIDsEqual } from '@memberjunction/global';
|
|
18
|
+
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
19
19
|
import { OAuthManager, MCPClientManager } from '@memberjunction/ai-mcp-client';
|
|
20
20
|
import type { MCPServerOAuthConfig } from '@memberjunction/ai-mcp-client';
|
|
21
21
|
|
|
@@ -38,6 +38,23 @@ export interface OAuthCallbackHandlerOptions {
|
|
|
38
38
|
successRedirectUrl?: string;
|
|
39
39
|
/** URL to redirect to after failed authorization */
|
|
40
40
|
errorRedirectUrl?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Origins a caller-supplied `frontendReturnUrl` is allowed to point at, so the OAuth callback
|
|
43
|
+
* cannot be turned into an open redirect from the trusted MJAPI origin. Normally wired to
|
|
44
|
+
* `cors.allowedOrigins`. Pass `['*']` to allow any origin — MJ's backward-compatible default
|
|
45
|
+
* CORS posture, and what a deployment that has not narrowed `cors.allowedOrigins` gets.
|
|
46
|
+
*
|
|
47
|
+
* REQUIRED on purpose. An optional field defaulting to allow-all is fail-open: a construction
|
|
48
|
+
* site that forgets it loses the open-redirect protection silently, with nothing to notice.
|
|
49
|
+
* Being required makes that a compile error instead. The handler is not exported from this
|
|
50
|
+
* package's barrel and the package declares no subpath exports, so no consumer outside MJServer
|
|
51
|
+
* can construct it — revisit this if it is ever added to the public API.
|
|
52
|
+
*
|
|
53
|
+
* Injected rather than read from `configInfo` directly so this module has no import-time
|
|
54
|
+
* dependency on configuration loading — importing the config module validates the whole config
|
|
55
|
+
* as a side effect, which makes this handler unimportable in any context without one.
|
|
56
|
+
*/
|
|
57
|
+
allowedFrontendOrigins: string[];
|
|
41
58
|
}
|
|
42
59
|
|
|
43
60
|
/**
|
|
@@ -46,10 +63,15 @@ export interface OAuthCallbackHandlerOptions {
|
|
|
46
63
|
* The callback endpoint is unauthenticated because it's called by external auth servers.
|
|
47
64
|
* It uses the state parameter to look up the authorization context and validate the flow.
|
|
48
65
|
*
|
|
66
|
+
* NOTE: `allowedFrontendOrigins` is what prevents the callback becoming an open redirect, and is
|
|
67
|
+
* required so it cannot be forgotten. Wire it to `configInfo.cors?.allowedOrigins`; `['*']` allows
|
|
68
|
+
* any origin, matching MJ's default CORS posture.
|
|
69
|
+
*
|
|
49
70
|
* @example
|
|
50
71
|
* ```typescript
|
|
51
72
|
* const oauthHandler = new OAuthCallbackHandler({
|
|
52
|
-
* publicUrl: 'https://api.example.com'
|
|
73
|
+
* publicUrl: 'https://api.example.com',
|
|
74
|
+
* allowedFrontendOrigins: configInfo.cors?.allowedOrigins ?? ['*']
|
|
53
75
|
* });
|
|
54
76
|
*
|
|
55
77
|
* // Mount unauthenticated callback route
|
|
@@ -348,7 +370,10 @@ export class OAuthCallbackHandler {
|
|
|
348
370
|
* @param res - Express response
|
|
349
371
|
*/
|
|
350
372
|
private async initiateFlow(req: express.Request, res: express.Response): Promise<void> {
|
|
351
|
-
|
|
373
|
+
// Coerce request-body values to strings up front. A JSON body can carry arrays/objects, and
|
|
374
|
+
// passing those downstream unvalidated turns a bad request into a confusing internal failure.
|
|
375
|
+
const connectionId: string = typeof req.body?.connectionId === 'string' ? req.body.connectionId : '';
|
|
376
|
+
const { additionalScopes, frontendReturnUrl } = req.body;
|
|
352
377
|
const contextUser = req['mjUser'] as UserInfo;
|
|
353
378
|
|
|
354
379
|
if (!contextUser) {
|
|
@@ -368,6 +393,29 @@ export class OAuthCallbackHandler {
|
|
|
368
393
|
return;
|
|
369
394
|
}
|
|
370
395
|
|
|
396
|
+
// connectionId is a record ID (UUID). Reject anything else at the boundary so it can never
|
|
397
|
+
// reach a SQL filter or downstream consumer as injectable input.
|
|
398
|
+
if (!IsValidUUID(connectionId)) {
|
|
399
|
+
res.status(400).json({
|
|
400
|
+
success: false,
|
|
401
|
+
errorCode: 'invalid_request',
|
|
402
|
+
errorMessage: 'connectionId must be a valid record identifier'
|
|
403
|
+
});
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Reject a disallowed return URL here rather than at the callback. Otherwise the caller only
|
|
408
|
+
// finds out after the whole OAuth round trip, by being silently sent to the default page.
|
|
409
|
+
// An absent value is acceptable — see isFrontendReturnUrlAcceptable.
|
|
410
|
+
if (!this.isFrontendReturnUrlAcceptable(frontendReturnUrl)) {
|
|
411
|
+
res.status(400).json({
|
|
412
|
+
success: false,
|
|
413
|
+
errorCode: 'invalid_request',
|
|
414
|
+
errorMessage: 'frontendReturnUrl is not an allowed redirect target'
|
|
415
|
+
});
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
371
419
|
try {
|
|
372
420
|
// Load connection and server config
|
|
373
421
|
const config = await this.loadConnectionConfig(connectionId, contextUser);
|
|
@@ -644,10 +692,13 @@ export class OAuthCallbackHandler {
|
|
|
644
692
|
try {
|
|
645
693
|
const rv = new RunView();
|
|
646
694
|
|
|
647
|
-
// Get connection to get server ID
|
|
695
|
+
// Get connection to get server ID.
|
|
696
|
+
// connectionId is caller-supplied (POST /oauth/initiate body). MJ ExtraFilter is a raw
|
|
697
|
+
// SQL fragment, so single quotes MUST be doubled to prevent injection — matching the
|
|
698
|
+
// escaping used for stateParameter in loadAuthorizationState().
|
|
648
699
|
const connResult = await rv.RunView<{ MCPServerID: string }>({
|
|
649
700
|
EntityName: ENTITY_MCP_SERVER_CONNECTIONS,
|
|
650
|
-
ExtraFilter: `ID='${connectionId}'`,
|
|
701
|
+
ExtraFilter: `ID='${connectionId.replace(/'/g, "''")}'`,
|
|
651
702
|
Fields: ['MCPServerID'],
|
|
652
703
|
ResultType: 'simple'
|
|
653
704
|
}, contextUser);
|
|
@@ -696,32 +747,93 @@ export class OAuthCallbackHandler {
|
|
|
696
747
|
}
|
|
697
748
|
}
|
|
698
749
|
|
|
750
|
+
/**
|
|
751
|
+
* Validates that a caller-supplied frontend return URL points at an allowed origin, so the
|
|
752
|
+
* OAuth callback cannot be turned into an open redirect from the trusted MJAPI origin.
|
|
753
|
+
*
|
|
754
|
+
* Allowed when: the allowlist is "allow all" (`['*']`, the backward-compatible default),
|
|
755
|
+
* the URL's origin is in `options.allowedFrontendOrigins`, or it matches the origin of a
|
|
756
|
+
* built-in success/error redirect URL. Anything else is rejected and the caller falls back to
|
|
757
|
+
* the default redirect page.
|
|
758
|
+
*/
|
|
759
|
+
private isFrontendReturnUrlAllowed(url: URL): boolean {
|
|
760
|
+
const allowed = this.options.allowedFrontendOrigins;
|
|
761
|
+
if (allowed.includes('*')) {
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
if (allowed.includes(url.origin)) {
|
|
765
|
+
return true;
|
|
766
|
+
}
|
|
767
|
+
for (const builtIn of [this.options.successRedirectUrl, this.options.errorRedirectUrl]) {
|
|
768
|
+
try {
|
|
769
|
+
if (builtIn && new URL(builtIn).origin === url.origin) {
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
} catch {
|
|
773
|
+
// Ignore an unparseable built-in URL and keep checking.
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Parses a caller-supplied return URL and applies {@link isFrontendReturnUrlAllowed}.
|
|
781
|
+
*
|
|
782
|
+
* @returns the parsed URL when it is well-formed AND points at an allowed origin, otherwise null.
|
|
783
|
+
*/
|
|
784
|
+
private parseAllowedFrontendReturnUrl(frontendReturnUrl: string): URL | null {
|
|
785
|
+
let url: URL;
|
|
786
|
+
try {
|
|
787
|
+
url = new URL(frontendReturnUrl);
|
|
788
|
+
} catch {
|
|
789
|
+
LogError(`[OAuth Callback] Invalid frontend return URL '${frontendReturnUrl}', falling back to default`);
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
if (!this.isFrontendReturnUrlAllowed(url)) {
|
|
793
|
+
LogError(`[OAuth Callback] frontend return URL '${frontendReturnUrl}' origin not allowed, falling back to default`);
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
return url;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Boundary check for `/oauth/initiate` — validates the return URL before it is persisted onto
|
|
801
|
+
* the authorization state, so a caller gets a 400 instead of a silent fallback later.
|
|
802
|
+
*
|
|
803
|
+
* An ABSENT value (undefined / null / empty string) is acceptable: it means "no return URL", and
|
|
804
|
+
* every downstream consumer treats it that way via a truthiness check. Only a value the caller
|
|
805
|
+
* actually supplied is validated — otherwise omitting the field, or sending an empty one, would
|
|
806
|
+
* newly fail a request that has always worked.
|
|
807
|
+
*/
|
|
808
|
+
private isFrontendReturnUrlAcceptable(frontendReturnUrl: unknown): boolean {
|
|
809
|
+
if (!frontendReturnUrl) {
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
return typeof frontendReturnUrl === 'string' && this.parseAllowedFrontendReturnUrl(frontendReturnUrl) !== null;
|
|
813
|
+
}
|
|
814
|
+
|
|
699
815
|
/**
|
|
700
816
|
* Redirects to success page with state info.
|
|
701
817
|
* If a frontend return URL is provided, redirects there instead of the default success page.
|
|
702
818
|
*/
|
|
703
819
|
private redirectToSuccess(res: express.Response, state: string, connectionId: string, frontendReturnUrl?: string): void {
|
|
704
|
-
// If frontend return URL is provided, redirect there with
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
} catch (error) {
|
|
715
|
-
LogError(`[OAuth Callback] Invalid frontend return URL '${frontendReturnUrl}', falling back to default`);
|
|
716
|
-
// Fall through to default redirect
|
|
717
|
-
}
|
|
820
|
+
// If frontend return URL is provided and points at an allowed origin, redirect there with
|
|
821
|
+
// success parameters. Anything else falls through to the built-in page.
|
|
822
|
+
const url = frontendReturnUrl ? this.parseAllowedFrontendReturnUrl(frontendReturnUrl) : null;
|
|
823
|
+
if (url) {
|
|
824
|
+
url.searchParams.set('oauth', 'success');
|
|
825
|
+
url.searchParams.set('state', state);
|
|
826
|
+
url.searchParams.set('connectionId', connectionId);
|
|
827
|
+
LogStatus(`[OAuth Callback] Redirecting to frontend URL: ${url.toString()}`);
|
|
828
|
+
res.redirect(302, url.toString());
|
|
829
|
+
return;
|
|
718
830
|
}
|
|
719
831
|
|
|
720
832
|
// Default: redirect to built-in success page
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
res.redirect(302,
|
|
833
|
+
const defaultUrl = new URL(this.options.successRedirectUrl!);
|
|
834
|
+
defaultUrl.searchParams.set('state', state);
|
|
835
|
+
defaultUrl.searchParams.set('connectionId', connectionId);
|
|
836
|
+
res.redirect(302, defaultUrl.toString());
|
|
725
837
|
}
|
|
726
838
|
|
|
727
839
|
/**
|
|
@@ -729,27 +841,23 @@ export class OAuthCallbackHandler {
|
|
|
729
841
|
* If a frontend return URL is provided, redirects there instead of the default error page.
|
|
730
842
|
*/
|
|
731
843
|
private redirectToError(res: express.Response, errorCode: string, errorMessage: string, frontendReturnUrl?: string): void {
|
|
732
|
-
// If frontend return URL is provided, redirect there with
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
} catch (error) {
|
|
743
|
-
LogError(`[OAuth Callback] Invalid frontend return URL '${frontendReturnUrl}', falling back to default`);
|
|
744
|
-
// Fall through to default redirect
|
|
745
|
-
}
|
|
844
|
+
// If frontend return URL is provided and points at an allowed origin, redirect there with
|
|
845
|
+
// error parameters. Anything else falls through to the built-in page.
|
|
846
|
+
const url = frontendReturnUrl ? this.parseAllowedFrontendReturnUrl(frontendReturnUrl) : null;
|
|
847
|
+
if (url) {
|
|
848
|
+
url.searchParams.set('oauth', 'error');
|
|
849
|
+
url.searchParams.set('error', errorCode);
|
|
850
|
+
url.searchParams.set('error_description', errorMessage);
|
|
851
|
+
LogStatus(`[OAuth Callback] Redirecting to frontend URL with error: ${url.toString()}`);
|
|
852
|
+
res.redirect(302, url.toString());
|
|
853
|
+
return;
|
|
746
854
|
}
|
|
747
855
|
|
|
748
856
|
// Default: redirect to built-in error page
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
res.redirect(302,
|
|
857
|
+
const defaultUrl = new URL(this.options.errorRedirectUrl!);
|
|
858
|
+
defaultUrl.searchParams.set('error', errorCode);
|
|
859
|
+
defaultUrl.searchParams.set('error_description', errorMessage);
|
|
860
|
+
res.redirect(302, defaultUrl.toString());
|
|
753
861
|
}
|
|
754
862
|
|
|
755
863
|
/**
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import express from 'express';
|
|
15
15
|
import BodyParser from 'body-parser';
|
|
16
16
|
import { LogError, LogStatus, UserInfo } from '@memberjunction/core';
|
|
17
|
-
import { UserCache } from '@memberjunction/
|
|
17
|
+
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
18
18
|
import { SignatureEngine } from '@memberjunction/esignature/server';
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Ephemeral in-memory staged upload token manager.
|
|
3
|
+
*
|
|
4
|
+
* Provides a secure, single-use, time-to-live (TTL) bounded staging area for raw
|
|
5
|
+
* binary upload buffers received via REST streaming before being committed through
|
|
6
|
+
* the GraphQL `UploadStorageFile` pipeline.
|
|
7
|
+
*
|
|
8
|
+
* Security properties:
|
|
9
|
+
* - Single-use nonce: Token and buffer are atomically destroyed on first consumption.
|
|
10
|
+
* - User binding: Tokens are bound to the authenticated User ID that uploaded them.
|
|
11
|
+
* - Auto-eviction: Abandoned tokens are automatically evicted from RAM after the TTL.
|
|
12
|
+
* - Memory caps: Enforces per-file max size (default 100MB) and total pool capacity (default 500MB).
|
|
13
|
+
*
|
|
14
|
+
* @module @memberjunction/server/rest/UploadTokenManager
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { BaseSingleton, UUIDsEqual } from '@memberjunction/global';
|
|
18
|
+
import { LogError } from '@memberjunction/core';
|
|
19
|
+
import { randomBytes } from 'node:crypto';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* An ephemeral in-memory staged upload record.
|
|
23
|
+
*/
|
|
24
|
+
export interface StagedUploadEntry {
|
|
25
|
+
token: string;
|
|
26
|
+
buffer: Buffer;
|
|
27
|
+
fileName: string;
|
|
28
|
+
mimeType: string;
|
|
29
|
+
contentLength: number;
|
|
30
|
+
userId: string;
|
|
31
|
+
createdAt: number;
|
|
32
|
+
expiresAt: number;
|
|
33
|
+
timer: NodeJS.Timeout;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Ephemeral In-Memory Staged Upload Token Manager.
|
|
38
|
+
*
|
|
39
|
+
* Architecture & Deployment Topology Note:
|
|
40
|
+
* This implementation maintains staged buffers in-process RAM with a 5-minute sliding TTL.
|
|
41
|
+
* In a clustered/multi-replica deployment behind a load balancer, sticky sessions (or routing
|
|
42
|
+
* the subsequent GraphQL `UploadStorageFile` commit to the same node) are recommended.
|
|
43
|
+
* For shared-memory multi-node clustering, an external ephemeral store (such as Redis)
|
|
44
|
+
* can be plugged in behind this interface.
|
|
45
|
+
*/
|
|
46
|
+
export class UploadTokenManager extends BaseSingleton<UploadTokenManager> {
|
|
47
|
+
private readonly _staged = new Map<string, StagedUploadEntry>();
|
|
48
|
+
private _totalMemoryBytes = 0;
|
|
49
|
+
|
|
50
|
+
public defaultTtlSeconds = 300; // 5 minutes
|
|
51
|
+
public maxFileSizeBytes = 100 * 1024 * 1024; // 100 MB
|
|
52
|
+
public maxUserMemoryBytes = 150 * 1024 * 1024; // 150 MB per user
|
|
53
|
+
public maxPoolMemoryBytes = 500 * 1024 * 1024; // 500 MB
|
|
54
|
+
|
|
55
|
+
// Public constructor required by BaseSingleton
|
|
56
|
+
public constructor() {
|
|
57
|
+
super();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public static get Instance(): UploadTokenManager {
|
|
61
|
+
return UploadTokenManager.getInstance<UploadTokenManager>();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Calculates total active staged bytes currently held for a given user.
|
|
66
|
+
*/
|
|
67
|
+
public GetUserMemoryBytes(userId: string): number {
|
|
68
|
+
let total = 0;
|
|
69
|
+
for (const entry of this._staged.values()) {
|
|
70
|
+
if (UUIDsEqual(entry.userId, userId)) {
|
|
71
|
+
total += entry.contentLength;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return total;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Stages a raw binary buffer in RAM, returning a single-use cryptographic token.
|
|
79
|
+
*
|
|
80
|
+
* @throws Error if buffer exceeds single file limit, per-user quota, or global pool memory capacity.
|
|
81
|
+
*/
|
|
82
|
+
public Stage(params: {
|
|
83
|
+
buffer: Buffer;
|
|
84
|
+
fileName: string;
|
|
85
|
+
mimeType: string;
|
|
86
|
+
userId: string;
|
|
87
|
+
ttlSeconds?: number;
|
|
88
|
+
}): string {
|
|
89
|
+
const size = params.buffer.length;
|
|
90
|
+
|
|
91
|
+
if (size > this.maxFileSizeBytes) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Staged upload exceeds maximum allowed file size of ${Math.round(this.maxFileSizeBytes / (1024 * 1024))}MB (received ${Math.round(size / (1024 * 1024))}MB).`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const userCurrentBytes = this.GetUserMemoryBytes(params.userId);
|
|
98
|
+
if (userCurrentBytes + size > this.maxUserMemoryBytes) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Per-user upload memory quota reached (${Math.round(userCurrentBytes / (1024 * 1024))}MB / ${Math.round(this.maxUserMemoryBytes / (1024 * 1024))}MB used). Please wait for active uploads to finalize.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (this._totalMemoryBytes + size > this.maxPoolMemoryBytes) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Staged upload memory capacity reached (${Math.round(this._totalMemoryBytes / (1024 * 1024))}MB / ${Math.round(this.maxPoolMemoryBytes / (1024 * 1024))}MB used). Please try again shortly.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const token = 'upt_' + randomBytes(32).toString('hex');
|
|
111
|
+
const ttl = params.ttlSeconds || this.defaultTtlSeconds;
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
const expiresAt = now + ttl * 1000;
|
|
114
|
+
|
|
115
|
+
const timer = setTimeout(() => {
|
|
116
|
+
this.Evict(token);
|
|
117
|
+
}, ttl * 1000);
|
|
118
|
+
|
|
119
|
+
// Unref timer so it doesn't hold open process shutdown in tests or CLI
|
|
120
|
+
if (typeof timer.unref === 'function') {
|
|
121
|
+
timer.unref();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const entry: StagedUploadEntry = {
|
|
125
|
+
token,
|
|
126
|
+
buffer: params.buffer,
|
|
127
|
+
fileName: params.fileName,
|
|
128
|
+
mimeType: params.mimeType,
|
|
129
|
+
contentLength: size,
|
|
130
|
+
userId: params.userId,
|
|
131
|
+
createdAt: now,
|
|
132
|
+
expiresAt,
|
|
133
|
+
timer,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
this._staged.set(token, entry);
|
|
137
|
+
this._totalMemoryBytes += size;
|
|
138
|
+
|
|
139
|
+
return token;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Atomically consumes and evicts a staged upload token.
|
|
144
|
+
*
|
|
145
|
+
* @param token The staged upload token.
|
|
146
|
+
* @param claimingUserId The user ID claiming this token. If provided, must match the user who uploaded it.
|
|
147
|
+
* @returns The staged buffer and metadata, or null if the token is invalid, expired, or claimed by the wrong user.
|
|
148
|
+
*/
|
|
149
|
+
public Consume(
|
|
150
|
+
token: string,
|
|
151
|
+
claimingUserId?: string
|
|
152
|
+
): { buffer: Buffer; fileName: string; mimeType: string; contentLength: number } | null {
|
|
153
|
+
if (!token || !this._staged.has(token)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const entry = this._staged.get(token)!;
|
|
158
|
+
|
|
159
|
+
// Security check: verify user ownership using UUIDsEqual for robust ID normalization
|
|
160
|
+
if (claimingUserId && entry.userId && !UUIDsEqual(entry.userId, claimingUserId)) {
|
|
161
|
+
LogError(`[UploadTokenManager] Security violation: User '${claimingUserId}' attempted to claim upload token owned by User '${entry.userId}'.`);
|
|
162
|
+
// Evict immediately to prevent further probing
|
|
163
|
+
this.Evict(token);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Atomic consumption & memory reclamation
|
|
168
|
+
this.Evict(token);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
buffer: entry.buffer,
|
|
172
|
+
fileName: entry.fileName,
|
|
173
|
+
mimeType: entry.mimeType,
|
|
174
|
+
contentLength: entry.contentLength,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Evicts a token from RAM and reclaims memory.
|
|
180
|
+
*/
|
|
181
|
+
public Evict(token: string): boolean {
|
|
182
|
+
const entry = this._staged.get(token);
|
|
183
|
+
if (!entry) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
clearTimeout(entry.timer);
|
|
188
|
+
this._staged.delete(token);
|
|
189
|
+
this._totalMemoryBytes = Math.max(0, this._totalMemoryBytes - entry.contentLength);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Total RAM in bytes currently consumed by all active staged uploads. */
|
|
194
|
+
public get TotalMemoryBytes(): number {
|
|
195
|
+
return this._totalMemoryBytes;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Count of currently active staged uploads. */
|
|
199
|
+
public get ActiveCount(): number {
|
|
200
|
+
return this._staged.size;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Clears all active staged uploads. */
|
|
204
|
+
public Clear(): void {
|
|
205
|
+
for (const [, entry] of this._staged) {
|
|
206
|
+
clearTimeout(entry.timer);
|
|
207
|
+
}
|
|
208
|
+
this._staged.clear();
|
|
209
|
+
this._totalMemoryBytes = 0;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview MJServer's implementation of the `DurableEntityActionSubmitter` seam.
|
|
3
|
+
*
|
|
4
|
+
* Turns one After* entity-action dispatch into a single-node durable task graph, which is D14's
|
|
5
|
+
* whole answer to "run this durably": not a new queue, the substrate that already has a claim
|
|
6
|
+
* protocol, restart recovery and orphan reclaim.
|
|
7
|
+
*
|
|
8
|
+
* Registered at boot beside the dispatcher, because the two are one capability — a submitter with
|
|
9
|
+
* no dispatcher writes rows nobody picks up, which is worse than staying inline.
|
|
10
|
+
*
|
|
11
|
+
* @module @memberjunction/server
|
|
12
|
+
*/
|
|
13
|
+
import type {
|
|
14
|
+
DurableEntityActionRequest,
|
|
15
|
+
DurableEntityActionSubmission,
|
|
16
|
+
DurableEntityActionSubmitter,
|
|
17
|
+
} from '@memberjunction/actions-base';
|
|
18
|
+
import { LogError } from '@memberjunction/core';
|
|
19
|
+
import { TaskNode, type TaskGraphSpec } from '@memberjunction/ai-core-plus';
|
|
20
|
+
import { MJEnvironmentEntityExtended } from '@memberjunction/core-entities';
|
|
21
|
+
import { TaskGraphService, type ProviderFactory } from '@memberjunction/task-graph';
|
|
22
|
+
|
|
23
|
+
export class DurableEntityActionTaskSubmitter implements DurableEntityActionSubmitter {
|
|
24
|
+
/**
|
|
25
|
+
* @param environmentID which environment the tasks belong to. Defaults to the platform default
|
|
26
|
+
* because a save has no environment of its own to inherit — the record's entity is global, and
|
|
27
|
+
* inventing a per-save environment would put a durable dispatch somewhere nobody looks.
|
|
28
|
+
*/
|
|
29
|
+
constructor(
|
|
30
|
+
private readonly providerFactory: ProviderFactory,
|
|
31
|
+
private readonly environmentID: string = MJEnvironmentEntityExtended.DefaultEnvironmentID,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
public async Submit(request: DurableEntityActionRequest): Promise<DurableEntityActionSubmission> {
|
|
35
|
+
try {
|
|
36
|
+
const spec: TaskGraphSpec = {
|
|
37
|
+
workflowName: `${request.ActionName} — ${request.EntityName}`,
|
|
38
|
+
reasoning:
|
|
39
|
+
`Durable dispatch of entity action binding ${request.EntityActionID} on ` +
|
|
40
|
+
`${request.EntityName} (${request.RecordID}), fired by ${request.InvocationType}.`,
|
|
41
|
+
tasks: [
|
|
42
|
+
TaskNode.Action(
|
|
43
|
+
{
|
|
44
|
+
tempId: 'action',
|
|
45
|
+
name: request.ActionName,
|
|
46
|
+
description: `Run ${request.ActionName} for ${request.EntityName} ${request.RecordID}.`,
|
|
47
|
+
dependsOn: [],
|
|
48
|
+
inputPayload: request.RedactedParams,
|
|
49
|
+
},
|
|
50
|
+
{ actionName: request.ActionName },
|
|
51
|
+
),
|
|
52
|
+
],
|
|
53
|
+
// Nothing to say to anyone: this graph exists because a record was saved, not
|
|
54
|
+
// because a person asked a question. Defaulting to 'message' would post an
|
|
55
|
+
// orphaned result into whichever conversation happened to be nearby.
|
|
56
|
+
continuation: 'none',
|
|
57
|
+
// The point of the exercise. A one-node graph constant-folds to in-run execution by
|
|
58
|
+
// default (D9), which is exactly the fire-and-forget behaviour being escaped.
|
|
59
|
+
durable: true,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const provider = await this.providerFactory.CreateProvider();
|
|
63
|
+
const result = await new TaskGraphService().Submit(spec, {
|
|
64
|
+
EnvironmentID: this.environmentID,
|
|
65
|
+
ContextUser: request.ContextUser,
|
|
66
|
+
Provider: provider,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return result.Success
|
|
70
|
+
? { Success: true, ParentTaskID: result.ParentTaskID }
|
|
71
|
+
: { Success: false, ErrorMessage: result.ErrorMessage };
|
|
72
|
+
} catch (e) {
|
|
73
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
74
|
+
LogError(`[DurableEntityActionTaskSubmitter] Submission failed for binding ${request.EntityActionID}: ${message}`);
|
|
75
|
+
return { Success: false, ErrorMessage: message };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|