@memberjunction/server 6.1.0-edge.1 → 6.1.0-edge.2

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.
Files changed (136) hide show
  1. package/dist/auth/exampleNewUserSubClass.js +1 -1
  2. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  3. package/dist/auth/index.d.ts.map +1 -1
  4. package/dist/auth/index.js +9 -5
  5. package/dist/auth/index.js.map +1 -1
  6. package/dist/auth/magicLink/MagicLinkService.js +1 -1
  7. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  8. package/dist/auth/newUsers.js +1 -1
  9. package/dist/auth/newUsers.js.map +1 -1
  10. package/dist/context.js +2 -2
  11. package/dist/context.js.map +1 -1
  12. package/dist/generated/generated.d.ts +45 -4
  13. package/dist/generated/generated.d.ts.map +1 -1
  14. package/dist/generated/generated.js +229 -21
  15. package/dist/generated/generated.js.map +1 -1
  16. package/dist/generic/ResolverBase.d.ts +19 -3
  17. package/dist/generic/ResolverBase.d.ts.map +1 -1
  18. package/dist/generic/ResolverBase.js +51 -17
  19. package/dist/generic/ResolverBase.js.map +1 -1
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +85 -41
  23. package/dist/index.js.map +1 -1
  24. package/dist/logging/secretRedactor.d.ts +9 -0
  25. package/dist/logging/secretRedactor.d.ts.map +1 -1
  26. package/dist/logging/secretRedactor.js +39 -0
  27. package/dist/logging/secretRedactor.js.map +1 -1
  28. package/dist/middleware/BaseServerMiddleware.d.ts +5 -0
  29. package/dist/middleware/BaseServerMiddleware.d.ts.map +1 -1
  30. package/dist/middleware/BaseServerMiddleware.js +5 -0
  31. package/dist/middleware/BaseServerMiddleware.js.map +1 -1
  32. package/dist/realtimeWidget/WidgetSessionService.js +1 -1
  33. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
  34. package/dist/realtimeWidget/widgetGuestElevation.js +1 -1
  35. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  36. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  37. package/dist/resolvers/ActionResolver.js +12 -0
  38. package/dist/resolvers/ActionResolver.js.map +1 -1
  39. package/dist/resolvers/ComponentRegistryResolver.js +1 -1
  40. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  41. package/dist/resolvers/EntityPermissionResolver.js +1 -1
  42. package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
  43. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +9 -0
  44. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  45. package/dist/resolvers/IntegrationDiscoveryResolver.js +3 -1
  46. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  47. package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
  48. package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
  49. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -0
  50. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  51. package/dist/resolvers/RealtimeClientSessionResolver.js +5 -0
  52. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  53. package/dist/resolvers/SearchEntitiesResolver.js +1 -1
  54. package/dist/resolvers/SearchEntitiesResolver.js.map +1 -1
  55. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  56. package/dist/resolvers/SqlLoggingConfigResolver.js +1 -1
  57. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  58. package/dist/resolvers/SyncRolesUsersResolver.js +2 -2
  59. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  60. package/dist/resolvers/UserFavoriteResolver.js +1 -1
  61. package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
  62. package/dist/rest/OAuthCallbackHandler.js +1 -1
  63. package/dist/rest/OAuthCallbackHandler.js.map +1 -1
  64. package/dist/rest/SignatureWebhookHandler.js +1 -1
  65. package/dist/rest/SignatureWebhookHandler.js.map +1 -1
  66. package/dist/services/DurableEntityActionTaskSubmitter.d.ts +26 -0
  67. package/dist/services/DurableEntityActionTaskSubmitter.d.ts.map +1 -0
  68. package/dist/services/DurableEntityActionTaskSubmitter.js +55 -0
  69. package/dist/services/DurableEntityActionTaskSubmitter.js.map +1 -0
  70. package/dist/services/ScheduledJobsService.js +1 -1
  71. package/dist/services/ScheduledJobsService.js.map +1 -1
  72. package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
  73. package/dist/services/StartTaskGraphDispatcher.js +16 -1
  74. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  75. package/dist/services/TaskGraphActionRunner.d.ts +34 -0
  76. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -0
  77. package/dist/services/TaskGraphActionRunner.js +95 -0
  78. package/dist/services/TaskGraphActionRunner.js.map +1 -0
  79. package/dist/services/TaskGraphAgentRunner.d.ts +13 -0
  80. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  81. package/dist/services/TaskGraphAgentRunner.js +30 -0
  82. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  83. package/dist/services/TaskGraphPromptRunner.d.ts +31 -0
  84. package/dist/services/TaskGraphPromptRunner.d.ts.map +1 -0
  85. package/dist/services/TaskGraphPromptRunner.js +134 -0
  86. package/dist/services/TaskGraphPromptRunner.js.map +1 -0
  87. package/dist/telephony/RingCentralTelephonyService.js +1 -1
  88. package/dist/telephony/RingCentralTelephonyService.js.map +1 -1
  89. package/dist/telephony/TwilioTelephonyRouter.js +1 -1
  90. package/dist/telephony/TwilioTelephonyRouter.js.map +1 -1
  91. package/dist/telephony/VonageTelephonyRouter.js +1 -1
  92. package/dist/telephony/VonageTelephonyRouter.js.map +1 -1
  93. package/package.json +90 -90
  94. package/src/__tests__/RealtimeClientSessionResolver.test.ts +1 -1
  95. package/src/__tests__/ResolverBase.frozenRecordMapping.test.ts +133 -0
  96. package/src/__tests__/ResolverBase.transportMapping.test.ts +289 -0
  97. package/src/__tests__/SessionManager.test.ts +1 -1
  98. package/src/__tests__/WidgetSessionService.test.ts +1 -1
  99. package/src/__tests__/databaseAbstraction.test.ts +1 -1
  100. package/src/__tests__/magicLinkService.provisionLog.test.ts +1 -1
  101. package/src/__tests__/resolverBase.rls.test.ts +3 -0
  102. package/src/__tests__/secretRedactor.failOpen.test.ts +135 -0
  103. package/src/__tests__/widgetGuestElevation.test.ts +1 -1
  104. package/src/auth/exampleNewUserSubClass.ts +1 -1
  105. package/src/auth/index.ts +10 -6
  106. package/src/auth/magicLink/MagicLinkService.ts +1 -1
  107. package/src/auth/newUsers.ts +1 -1
  108. package/src/context.ts +2 -2
  109. package/src/generated/generated.ts +166 -17
  110. package/src/generic/ResolverBase.ts +52 -17
  111. package/src/index.ts +92 -42
  112. package/src/logging/secretRedactor.ts +43 -0
  113. package/src/middleware/BaseServerMiddleware.ts +5 -0
  114. package/src/realtimeWidget/WidgetSessionService.ts +1 -1
  115. package/src/realtimeWidget/widgetGuestElevation.ts +1 -1
  116. package/src/resolvers/ActionResolver.ts +13 -0
  117. package/src/resolvers/ComponentRegistryResolver.ts +1 -1
  118. package/src/resolvers/EntityPermissionResolver.ts +1 -1
  119. package/src/resolvers/IntegrationDiscoveryResolver.ts +12 -2
  120. package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -1
  121. package/src/resolvers/RealtimeClientSessionResolver.ts +13 -0
  122. package/src/resolvers/SearchEntitiesResolver.ts +1 -1
  123. package/src/resolvers/SqlLoggingConfigResolver.ts +1 -2
  124. package/src/resolvers/SyncRolesUsersResolver.ts +2 -2
  125. package/src/resolvers/UserFavoriteResolver.ts +1 -1
  126. package/src/rest/OAuthCallbackHandler.ts +1 -1
  127. package/src/rest/SignatureWebhookHandler.ts +1 -1
  128. package/src/services/DurableEntityActionTaskSubmitter.ts +78 -0
  129. package/src/services/ScheduledJobsService.ts +1 -1
  130. package/src/services/StartTaskGraphDispatcher.ts +17 -0
  131. package/src/services/TaskGraphActionRunner.ts +99 -0
  132. package/src/services/TaskGraphAgentRunner.ts +35 -1
  133. package/src/services/TaskGraphPromptRunner.ts +144 -0
  134. package/src/telephony/RingCentralTelephonyService.ts +1 -1
  135. package/src/telephony/TwilioTelephonyRouter.ts +1 -1
  136. package/src/telephony/VonageTelephonyRouter.ts +1 -1
package/src/index.ts CHANGED
@@ -5,9 +5,9 @@ dotenv.config({ quiet: true });
5
5
  import { expressMiddleware } from '@as-integrations/express5';
6
6
  import { mergeSchemas } from '@graphql-tools/schema';
7
7
  import { Metadata, DatabasePlatform, SetProvider, StartupManager as StartupManagerImport, BaseEntity, BaseEntityEvent, RunView, DatabaseProviderBase, ResolveStartupMode } from '@memberjunction/core';
8
- import { resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provider';
8
+ import { UserCache, resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provider';
9
9
  import { MJGlobal, MJEventType, UUIDsEqual, ShutdownRegistry } from '@memberjunction/global';
10
- import { setupSQLServerClient, SQLServerDataProvider, SQLServerProviderConfigData, UserCache } from '@memberjunction/sqlserver-dataprovider';
10
+ import { setupSQLServerClient, SQLServerDataProvider, SQLServerProviderConfigData } from '@memberjunction/sqlserver-dataprovider';
11
11
  import { extendConnectionPoolWithQuery } from './util.js';
12
12
  import { registerIntegrationCustomColumnPromoter } from './integration/CustomColumnPromoter.js';
13
13
  import { DisableUnselectedEntityMaps, ReenableFieldMapsForEntityMap } from './integration/EntityMapLifecycle.js';
@@ -19,11 +19,12 @@ import { default as fg } from 'fast-glob';
19
19
  import { useServer } from 'graphql-ws/lib/use/ws';
20
20
  import { createServer } from 'node:http';
21
21
  import { fileURLToPath } from 'node:url';
22
- import { readFileSync } from 'node:fs';
22
+ import { readFileSync, writeFileSync } from 'node:fs';
23
+ import { Session as InspectorSession } from 'node:inspector';
23
24
  import { sep } from 'node:path';
24
25
  import 'reflect-metadata';
25
26
  import { ReplaySubject } from 'rxjs';
26
- import { BuildSchemaOptions, buildSchemaSync, GraphQLTimestamp, PubSubEngine } from 'type-graphql';
27
+ import { BuildSchemaOptions, buildSchemaSync, getMetadataStorage, GraphQLTimestamp, PubSubEngine } from 'type-graphql';
27
28
  import { PubSub } from 'graphql-subscriptions';
28
29
  import sql from 'mssql';
29
30
  import { WebSocketServer } from 'ws';
@@ -153,6 +154,7 @@ export * from './resolvers/QueryResolver.js';
153
154
  export * from './services/TaskGraphProviderFactory.js';
154
155
  export * from './services/TaskGraphAgentRunner.js';
155
156
  export * from './services/StartTaskGraphDispatcher.js';
157
+ export * from './services/TaskGraphPromptRunner.js';
156
158
  export * from './services/TaskGraphContinuationDeliverer.js';
157
159
  export * from './resolvers/TaskGraphFrameResolver.js';
158
160
  export * from './resolvers/TestQuerySQLResolver.js';
@@ -328,8 +330,8 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
328
330
  await provider.Config(pgConfigData);
329
331
  SetProvider(provider);
330
332
 
331
- // Refresh user cache using PostgreSQL
332
- await refreshUserCacheFromPG(pgPool, mj_core_schema);
333
+ // Warm the user cache dialect-neutral, same call the SQL Server path makes
334
+ await UserCache.Instance.Refresh(provider);
333
335
 
334
336
  // Run startup actions — same 'full' entry-point default as the SQL Server path
335
337
  const sysUser = UserCache.Instance.GetSystemUser();
@@ -858,23 +860,73 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
858
860
  lap('Resolver + Middleware Discovery', tServe);
859
861
  tServe = startupLog.BeginPhase('Building GraphQL schema');
860
862
 
863
+ // ── Scale-runtime instrumentation (fix #1 profiling) ─────────────────────────
864
+ // Split the previously-monolithic "Schema Build" phase into its three real
865
+ // sub-steps so we can see which one dominates at high entity counts, and capture
866
+ // the type-graphql metadata cardinality (the O(entities×fields) signal). Guarded
867
+ // by MJ_SCHEMA_PROFILE, and emits ONE explicit console line (independent of the
868
+ // startup logger's level, and NOT via EndPhase, so the "schema" summary token and
869
+ // its total math stay clean). No-op in normal deployments.
870
+ const schemaProfile = process.env.MJ_SCHEMA_PROFILE === '1';
871
+ let objectTypeCount = -1, fieldCount = -1;
872
+ if (schemaProfile) {
873
+ try {
874
+ const meta = getMetadataStorage();
875
+ // Read BEFORE buildSchemaSync — buildSchemaSync's internal build() resets these arrays.
876
+ objectTypeCount = meta.objectTypes.length;
877
+ fieldCount = meta.fields.length;
878
+ } catch {
879
+ /* metadata cardinality is best-effort; leave sentinels */
880
+ }
881
+ }
882
+
883
+ // Surgical CPU profile of ONLY buildSchemaSync (behind MJ_SCHEMA_CPUPROF=1), so we can see
884
+ // whether the ~66s at scale is uniform per-field work (cache is the only fix) or a patchable
885
+ // hotspot in type-graphql's generator. Deterministic flush (writes before serve continues) —
886
+ // no dependency on --cpu-prof surviving a process kill.
887
+ const cpuProf = process.env.MJ_SCHEMA_CPUPROF === '1';
888
+ let profSession: InspectorSession | undefined;
889
+ if (cpuProf) {
890
+ profSession = new InspectorSession();
891
+ profSession.connect();
892
+ await new Promise<void>((res, rej) => profSession!.post('Profiler.enable', (e) => (e ? rej(e) : res())));
893
+ await new Promise<void>((res, rej) => profSession!.post('Profiler.start', (e) => (e ? rej(e) : res())));
894
+ }
895
+
896
+ const tBuild = performance.now();
897
+ const builtSchema = buildSchemaSync({
898
+ resolvers: allResolvers,
899
+ validate: false,
900
+ scalarsMap: [{ type: Date, scalar: GraphQLTimestamp }],
901
+ emitSchemaFile: websiteRunFromPackage !== 1,
902
+ pubSub,
903
+ globalMiddlewares: [variablesLoggingMiddleware],
904
+ });
905
+ const buildMs = performance.now() - tBuild;
906
+
907
+ if (cpuProf && profSession) {
908
+ const profile = await new Promise<unknown>((res, rej) =>
909
+ profSession!.post('Profiler.stop', (e, r) => (e ? rej(e) : res((r as { profile: unknown }).profile)))
910
+ );
911
+ const outPath = `/tmp/mjapi_schema_build_${process.pid}.cpuprofile`;
912
+ writeFileSync(outPath, JSON.stringify(profile));
913
+ profSession.disconnect();
914
+ // eslint-disable-next-line no-console
915
+ console.log(`[SCHEMA-CPUPROF] wrote ${outPath} (buildSchemaSync=${buildMs.toFixed(0)}ms)`);
916
+ }
917
+
918
+ const tMerge = performance.now();
861
919
  let schema = mergeSchemas({
862
- schemas: [
863
- buildSchemaSync({
864
- resolvers: allResolvers,
865
- validate: false,
866
- scalarsMap: [{ type: Date, scalar: GraphQLTimestamp }],
867
- emitSchemaFile: websiteRunFromPackage !== 1,
868
- pubSub,
869
- globalMiddlewares: [variablesLoggingMiddleware],
870
- }),
871
- ],
920
+ schemas: [builtSchema],
872
921
  typeDefs: [requireSystemUserDirective.typeDefs, publicDirective.typeDefs],
873
922
  });
923
+ const mergeMs = performance.now() - tMerge;
874
924
 
875
925
  // Verbose-mode-only diagnostic: name custom-resolver args that aren't metadata-bound
876
926
  // and aren't @NoLog-marked. No-op in default config (logVariables=false).
877
927
  auditResolversForUndecoratedArgs();
928
+
929
+ const tTransform = performance.now();
878
930
  schema = requireSystemUserDirective.transformer(schema);
879
931
  schema = publicDirective.transformer(schema);
880
932
 
@@ -882,6 +934,17 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
882
934
  for (const transformer of mwSchemaTransformers) {
883
935
  schema = transformer(schema);
884
936
  }
937
+ const transformMs = performance.now() - tTransform;
938
+
939
+ if (schemaProfile) {
940
+ const typeMapSize = Object.keys(schema.getTypeMap()).length;
941
+ // eslint-disable-next-line no-console
942
+ console.log(
943
+ `[SCHEMA-PROFILE] resolvers=${allResolvers.length} objectTypes=${objectTypeCount} fields=${fieldCount} ` +
944
+ `typeMap=${typeMapSize} | buildSchemaSync=${buildMs.toFixed(0)}ms mergeSchemas=${mergeMs.toFixed(0)}ms ` +
945
+ `transformers=${transformMs.toFixed(0)}ms`
946
+ );
947
+ }
885
948
 
886
949
  lap('Schema Build', tServe);
887
950
  tServe = startupLog.BeginPhase('Starting HTTP server');
@@ -1350,8 +1413,20 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
1350
1413
  // submitted graph — submission would be durable and inert, which is strictly worse than the old
1351
1414
  // client-driven path it replaced. Gated on SQL Server because the provider factory mints
1352
1415
  // SQLServerDataProvider; the PG branch lands with PG parity. Self-registers with ShutdownRegistry.
1416
+ //
1417
+ // `MJ_DISABLE_TASK_GRAPH_DISPATCHER=1` suppresses it, for the one case where a second dispatcher
1418
+ // is actively harmful: the integration suite's task-graph bundle drives its OWN dispatcher against
1419
+ // a stub runner and asserts exactly-once execution. A dispatcher claims from the whole table, not
1420
+ // from "its own" graphs, so a server sharing that database races the suite for every claim and
1421
+ // executes the suite's tasks with the real agent runner. The bundle then reports tasks that never
1422
+ // ran and graphs that settled to the wrong status — symptoms that read as engine defects and cost
1423
+ // a release cycle to trace back to here. The suite still needs MJAPI up for its client-transport
1424
+ // members, so "stop the server" is not the remedy; this is.
1353
1425
  const taskGraphPool = dataSources[0]?.dataSource;
1354
- if (resumeUser && taskGraphPool instanceof sql.ConnectionPool) {
1426
+ const taskGraphDispatcherDisabled = process.env.MJ_DISABLE_TASK_GRAPH_DISPATCHER === '1';
1427
+ if (taskGraphDispatcherDisabled) {
1428
+ LogStatus('[TaskGraphDispatcher] Disabled by MJ_DISABLE_TASK_GRAPH_DISPATCHER=1 — submitted graphs will not be executed by this process.');
1429
+ } else if (resumeUser && taskGraphPool instanceof sql.ConnectionPool) {
1355
1430
  StartTaskGraphDispatcher(taskGraphPool, resumeUser)
1356
1431
  .catch(err => console.warn(`[TaskGraphDispatcher] Startup failed: ${err}`));
1357
1432
  }
@@ -1745,31 +1820,6 @@ function createMSSQLCompatPool(pgPool: import('pg').Pool): sql.ConnectionPool {
1745
1820
  return wrapper as unknown as sql.ConnectionPool;
1746
1821
  }
1747
1822
 
1748
- /**
1749
- * Refreshes the UserCache using PostgreSQL queries instead of MSSQL.
1750
- * This mirrors the logic in UserCache.Refresh() but uses pg.Pool.
1751
- */
1752
- async function refreshUserCacheFromPG(pgPool: import('pg').Pool, coreSchema: string): Promise<void> {
1753
- const { UserInfo } = await import('@memberjunction/core');
1754
- const uResult = await pgPool.query(`SELECT * FROM ${coreSchema}."vwUsers"`);
1755
- const rResult = await pgPool.query(`SELECT * FROM ${coreSchema}."vwUserRoles"`);
1756
- const users = uResult.rows;
1757
- const roles = rResult.rows;
1758
-
1759
- if (users) {
1760
- const userInfos = users.map((user: Record<string, unknown>) => {
1761
- const userWithRoles = {
1762
- ...user,
1763
- UserRoles: roles.filter((role: Record<string, unknown>) => UUIDsEqual(role.UserID as string, user.ID as string)),
1764
- };
1765
- return new UserInfo(Metadata.Provider, userWithRoles); // global-provider-ok: bootstrap (UserCache initialization)
1766
- });
1767
- // Access the UserCache internals to set users
1768
- const cache = UserCache.Instance;
1769
- (cache as unknown as Record<string, unknown>)['_users'] = userInfos;
1770
- }
1771
- }
1772
-
1773
1823
  /**
1774
1824
  * Translates SQL Server bracket-quoted identifiers to PostgreSQL double-quoted identifiers.
1775
1825
  * Converts [schema].[table] to "schema"."table" and handles common T-SQL patterns.
@@ -51,6 +51,20 @@ export function redactArg(ctx: RedactionContext): unknown {
51
51
  const entity = ctx.provider.Entities.find((e) => e.ClassName === match.groups!.name);
52
52
  if (entity) {
53
53
  encryptedFieldNames = new Set(entity.EncryptedFields.map((f) => f.Name));
54
+ } else {
55
+ // The name matched the CRUD convention but resolves to no entity — so the
56
+ // metadata half of the redactor contributes nothing and, absent any @NoLog
57
+ // field, this arg falls through to shortenForLog with its values intact.
58
+ //
59
+ // This is the silent case: the boot audit tests only the NAME pattern, so an
60
+ // input like `CreateConnectionInput` is classified as metadata-bound and never
61
+ // warned about, while the lookup here quietly fails. Every hand-written
62
+ // `Create*Input` in the resolvers hits this — codegen inputs embed the entity
63
+ // ClassName (`CreateMJCredentialInput`), hand-written ones do not.
64
+ //
65
+ // Report it once per input type so the operator who enabled variables logging
66
+ // learns which args are unprotected, rather than discovering it in a log file.
67
+ WarnUnboundCrudInput(ctx.inputTypeName);
54
68
  }
55
69
  }
56
70
 
@@ -80,3 +94,32 @@ export function redactArg(ctx: RedactionContext): unknown {
80
94
  }
81
95
 
82
96
  const EMPTY_SET: ReadonlySet<string> = new Set<string>();
97
+
98
+ /**
99
+ * Input type names already reported by {@link WarnUnboundCrudInput}. Warning once
100
+ * per type keeps a hot mutation from flooding the log with the same diagnostic.
101
+ */
102
+ const warnedUnboundInputTypes = new Set<string>();
103
+
104
+ /**
105
+ * Reports, once per input type, that an arg named like a CRUD input resolves to no
106
+ * entity and is therefore logged unredacted unless its fields carry `@NoLog`.
107
+ *
108
+ * Exported for tests; not part of the redaction path's contract.
109
+ */
110
+ export function WarnUnboundCrudInput(inputTypeName: string): void {
111
+ if (warnedUnboundInputTypes.has(inputTypeName)) {
112
+ return;
113
+ }
114
+ warnedUnboundInputTypes.add(inputTypeName);
115
+ console.warn(
116
+ `[MJServer] Variables logging: '${inputTypeName}' matches the Create/Update/Delete input ` +
117
+ `naming convention but maps to no entity, so encrypted-field redaction cannot apply to it. ` +
118
+ `Its values are being logged. Mark any sensitive @Field() on this input with @NoLog.`,
119
+ );
120
+ }
121
+
122
+ /** Test seam — clears the once-per-type warning state. */
123
+ export function ResetUnboundCrudInputWarnings(): void {
124
+ warnedUnboundInputTypes.clear();
125
+ }
@@ -126,6 +126,11 @@ export abstract class BaseServerMiddleware {
126
126
  /**
127
127
  * Hook that runs after a RunView operation completes. Can modify the result
128
128
  * (e.g., filtering or augmenting data) before it is returned to the caller.
129
+ *
130
+ * ⚠️ `results.Results` and its rows may be FROZEN shared cache state (LocalCacheManager
131
+ * freeze-on-write) — writing into a row throws `TypeError`. To modify, build copies and
132
+ * either reassign `results.Results = results.Results.map(r => ({ ...r, ... }))` or return
133
+ * a new result; never mutate rows in place.
129
134
  */
130
135
  PostRunView(params: RunViewParams, results: RunViewResult, contextUser: UserInfo | undefined): RunViewResult | Promise<RunViewResult> {
131
136
  return results;
@@ -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 { UserCache } from '@memberjunction/sqlserver-dataprovider';
20
+ import { UserCache } from '@memberjunction/generic-database-provider';
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';
@@ -15,7 +15,7 @@
15
15
 
16
16
  import { RunView, LogError, type UserInfo, type DatabaseProviderBase } from '@memberjunction/core';
17
17
  import type { MJConversationWidgetInstanceEntity } from '@memberjunction/core-entities';
18
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
18
+ import { UserCache } from '@memberjunction/generic-database-provider';
19
19
  import type { UserPayload } from '../types.js';
20
20
 
21
21
  const WIDGET_ENTITY = 'MJ: Conversation Widget Instances';
@@ -366,6 +366,19 @@ export class ActionResolver extends ResolverBase {
366
366
  // Run the entity action
367
367
  const result = await EntityActionEngineServer.Instance.RunEntityAction(params);
368
368
 
369
+ if (!result) {
370
+ // The binding is scoped (ScopeEntityID/ScopeRecordID) and this record falls outside it, so
371
+ // the action never ran. That is a legitimate answer, not a failure — dereferencing the null
372
+ // here turned "this binding does not apply to that record" into a server error for the
373
+ // caller. Reported as a successful no-op with an explicit message, so a client can tell it
374
+ // apart from an action that ran and did nothing.
375
+ return {
376
+ Success: true,
377
+ Message: 'The entity action did not run: its binding is scoped to different records.',
378
+ ResultData: JSON.stringify({ Ran: false, Reason: 'OutOfScope' })
379
+ };
380
+ }
381
+
369
382
  // Return the result
370
383
  return {
371
384
  Success: result.Success,
@@ -1,7 +1,7 @@
1
1
  import { Arg, Ctx, Field, InputType, ObjectType, Query, Mutation, Resolver } from 'type-graphql';
2
2
  import { UserInfo, LogError, LogStatus } from '@memberjunction/core';
3
3
  import { UUIDsEqual, MJLruCache } from '@memberjunction/global';
4
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
4
+ import { UserCache } from '@memberjunction/generic-database-provider';
5
5
  import { MJComponentRegistryEntity, ComponentMetadataEngine } from '@memberjunction/core-entities';
6
6
  import { CredentialEngine } from '@memberjunction/credentials';
7
7
  import { ComponentSpec } from '@memberjunction/interactive-component-types';
@@ -3,7 +3,7 @@ import { LogError } from '@memberjunction/core';
3
3
  import { AppContext } from '../types.js';
4
4
  import { ResolverBase } from '../generic/ResolverBase.js';
5
5
  import { GetReadOnlyProvider } from '../util.js';
6
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
6
+ import { UserCache } from '@memberjunction/generic-database-provider';
7
7
 
8
8
  @ObjectType()
9
9
  class EntityPermissionResult {
@@ -1,6 +1,7 @@
1
1
  import { Resolver, Query, Mutation, Arg, Ctx, ObjectType, Field, InputType, Int, Float } from "type-graphql";
2
2
  import { CompositeKey, DatabaseProviderBase, EntityInfo, LocalCacheManager, Metadata, RunView, UserInfo, LogError, LogStatus, IMetadataProvider, TransactionGroupBase } from "@memberjunction/core";
3
3
  import { GetReadOnlyProvider, GetReadWriteProvider } from "../util.js";
4
+ import { NoLog } from "../logging/NoLog.js";
4
5
  import { UUIDsEqual } from "@memberjunction/global";
5
6
  import { CronExpressionHelper } from "@memberjunction/scheduling-engine";
6
7
  import {
@@ -56,7 +57,7 @@ import { IntegrationCustomColumnPromoter } from "../integration/CustomColumnProm
56
57
  import { ComputeCascadeRemovalSet, ComputeRemovedDependencyWarnings, DisableUnselectedEntityMaps, ReenableFieldMapsForEntityMap, ResetPullWatermarks, SetEntityMapEnabled } from "../integration/EntityMapLifecycle.js";
57
58
  import { AppContext } from "../types.js";
58
59
  import { RequireSystemUser } from "../directives/RequireSystemUser.js";
59
- import { UserCache } from "@memberjunction/sqlserver-dataprovider";
60
+ import { UserCache } from "@memberjunction/generic-database-provider";
60
61
 
61
62
  // ─── RSU Pipeline Output Types ──────────────────────────────────────────────
62
63
 
@@ -551,7 +552,16 @@ class CreateConnectionInput {
551
552
  @Field() CompanyID: string;
552
553
  @Field() CredentialTypeID: string;
553
554
  @Field() CredentialName: string;
554
- @Field() CredentialValues: string;
555
+ /**
556
+ * The credential payload being connected, as a JSON string.
557
+ *
558
+ * `@NoLog` is load-bearing here, not decorative. This field does NOT map to an
559
+ * entity column — the resolver assigns it onto `MJ: Credentials`.`Values`
560
+ * (which IS `Encrypt=true`) in procedural code further down. The metadata-driven
561
+ * half of the redactor keys off entity columns, so it cannot see this field, and
562
+ * without the mark the variables log emits the credential in plaintext.
563
+ */
564
+ @Field() @NoLog CredentialValues: string;
555
565
  @Field({ nullable: true }) ExternalSystemID?: string;
556
566
  @Field({ nullable: true }) Configuration?: string;
557
567
  }
@@ -8,7 +8,7 @@ import {
8
8
  PotentialDuplicateResult,
9
9
  } from '@memberjunction/core';
10
10
  import { AppContext } from '../types.js';
11
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
11
+ import { UserCache } from '@memberjunction/generic-database-provider';
12
12
 
13
13
  import { CompositeKeyInputType, CompositeKeyOutputType, KeyValuePairOutputType } from '../generic/KeyInputOutputTypes.js';
14
14
  import { GetReadOnlyProvider } from '../util.js';
@@ -227,6 +227,18 @@ export class StartRealtimeClientSessionResult {
227
227
  @Field(() => String, { nullable: true })
228
228
  ModelName?: string;
229
229
 
230
+ /**
231
+ * `DriverClass` of the vendor that actually runs the session (e.g. `OpenAIRealtime`,
232
+ * `ElevenLabsRealtime`). Null when unknown.
233
+ *
234
+ * Surfaced to the BROWSER because on the default-model path the framework picks the vendor itself:
235
+ * without this the caller cannot tell which vendor spoke, and so cannot diagnose a voice that did
236
+ * not land (issue #3530). `ModelName` alone is not enough — one model can resolve to different
237
+ * vendors depending on which API keys a deployment configures.
238
+ */
239
+ @Field(() => String, { nullable: true })
240
+ DriverClass?: string;
241
+
230
242
  /**
231
243
  * DB-driven progress-narration instruction template (contains a `{{ progressMessage }}`
232
244
  * placeholder). Null when the narration prompt is not present in this deployment's metadata —
@@ -1677,6 +1689,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1677
1689
  ExpiresAt: cfg.ExpiresAt,
1678
1690
  SessionConfigJson: JSON.stringify(cfg.SessionConfig),
1679
1691
  ModelName: prep.ModelName,
1692
+ DriverClass: prep.DriverClass,
1680
1693
  NarrationInstructionsTemplate: prep.NarrationInstructionsTemplate,
1681
1694
  NarrationPaceMs: prep.NarrationPaceMs,
1682
1695
  EffectiveConfigJson: prep.EffectiveConfig ? JSON.stringify(prep.EffectiveConfig) : undefined,
@@ -1,7 +1,7 @@
1
1
  import { Arg, Ctx, Field, Float, InputType, Int, ObjectType, Query, Resolver } from 'type-graphql';
2
2
  import { EntitySearchResult, SearchEntityParams } from '@memberjunction/core';
3
3
  import { AppContext } from '../types.js';
4
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
4
+ import { UserCache } from '@memberjunction/generic-database-provider';
5
5
  import { GetReadOnlyProvider } from '../util.js';
6
6
 
7
7
  /**
@@ -2,14 +2,13 @@ import { Arg, Ctx, Field, InputType, Mutation, ObjectType, Query, Int, Resolver
2
2
  import { AppContext } from '../types.js';
3
3
  import { Metadata, UserInfo } from '@memberjunction/core';
4
4
  import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
5
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
6
5
  import { v4 as uuidv4 } from 'uuid';
7
6
  import * as path from 'path';
8
7
  import * as fs from 'fs/promises';
9
8
  import { loadConfig } from '../config.js';
10
9
  import { ResolverBase } from '../generic/ResolverBase.js';
11
10
  import { GetReadOnlyProvider } from '../util.js';
12
- import { SqlLoggingOptions as ProviderSqlLoggingOptions } from '@memberjunction/generic-database-provider';
11
+ import { UserCache, SqlLoggingOptions as ProviderSqlLoggingOptions } from '@memberjunction/generic-database-provider';
13
12
 
14
13
  /**
15
14
  * Configuration options for SQL logging sessions.
@@ -4,7 +4,7 @@ import { DatabaseProviderBase, EntityDeleteOptions, EntitySaveOptions, IMetadata
4
4
  import { UUIDsEqual } from '@memberjunction/global';
5
5
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
6
6
  import { MJRoleEntity, MJUserEntity, MJUserRoleEntity } from '@memberjunction/core-entities';
7
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
7
+ import { UserCache } from '@memberjunction/generic-database-provider';
8
8
  import { GetReadWriteProvider } from '../util.js';
9
9
 
10
10
  @ObjectType()
@@ -112,7 +112,7 @@ export class SyncRolesAndUsersResolver {
112
112
  await provider.CommitTransaction();
113
113
 
114
114
  // refresh the user cache — one-time, since the normal auto-refresh is already scheduled
115
- await UserCache.Instance.Refresh(context.dataSource);
115
+ await UserCache.Instance.Refresh(provider);
116
116
  return usersResult;
117
117
  } catch (txErr) {
118
118
  await provider.RollbackTransaction();
@@ -14,7 +14,7 @@ import {
14
14
  Query,
15
15
  Resolver,
16
16
  } from '@memberjunction/server';
17
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
17
+ import { UserCache } from '@memberjunction/generic-database-provider';
18
18
 
19
19
  import { MJUserFavorite_, MJUserFavoriteResolverBase } from '../generated/generated.js';
20
20
  import { GetReadOnlyProvider } from '../util.js';
@@ -15,7 +15,7 @@
15
15
  import express from 'express';
16
16
  import { LogError, LogStatus, RunView, UserInfo } from '@memberjunction/core';
17
17
  import { UUIDsEqual } from '@memberjunction/global';
18
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
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
 
@@ -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/sqlserver-dataprovider';
17
+ import { UserCache } from '@memberjunction/generic-database-provider';
18
18
  import { SignatureEngine } from '@memberjunction/esignature/server';
19
19
 
20
20
  /**
@@ -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
+ }
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { LogError, LogStatus, LogStatusEx, UserInfo } from '@memberjunction/core';
7
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
7
+ import { UserCache } from '@memberjunction/generic-database-provider';
8
8
  import { SchedulingEngine } from '@memberjunction/scheduling-engine';
9
9
  import { ScheduledJobsConfig } from '../config.js';
10
10
 
@@ -18,9 +18,13 @@
18
18
  */
19
19
  import { UserInfo } from '@memberjunction/core';
20
20
  import { LoadTaskGraphOperations, TaskGraphDispatcher } from '@memberjunction/task-graph';
21
+ import { TaskGraphPromptRunner } from './TaskGraphPromptRunner.js';
21
22
  import sql from 'mssql';
23
+ import { DurableEntityActionRegistry } from '@memberjunction/actions-base';
22
24
  import { CreateTaskGraphProviderFactory } from './TaskGraphProviderFactory.js';
25
+ import { TaskGraphActionRunner } from './TaskGraphActionRunner.js';
23
26
  import { TaskGraphAgentRunner } from './TaskGraphAgentRunner.js';
27
+ import { DurableEntityActionTaskSubmitter } from './DurableEntityActionTaskSubmitter.js';
24
28
  import { TaskGraphContinuationDeliverer } from './TaskGraphContinuationDeliverer.js';
25
29
  import { PubSubManager } from '../generic/PubSubManager.js';
26
30
  import { TaskGraphFrameBroadcaster } from '../resolvers/TaskGraphFrameResolver.js';
@@ -55,7 +59,20 @@ export async function StartTaskGraphDispatcher(
55
59
  // Live signal. Supplied only when a PubSub engine exists — in a worker or a test there is
56
60
  // nobody to publish to, and the dispatcher runs identically without one.
57
61
  pubSub ? new TaskGraphFrameBroadcaster(pubSub) : undefined,
62
+ // Action nodes — the third execution shape, beside agents and people. Durable After* entity
63
+ // actions land as these.
64
+ new TaskGraphActionRunner(),
65
+ // Prompt nodes. Without this a Flow agent with a Prompt step submits and then
66
+ // stalls on a task nothing can execute — which is why the shipped User Onboarding
67
+ // Flow Agent could not run at all before this seam existed.
68
+ new TaskGraphPromptRunner(),
58
69
  );
59
70
  await dispatcher.Start();
71
+
72
+ // Registered only AFTER the dispatcher is running, and only here. A submitter without a
73
+ // dispatcher writes Task rows nobody picks up — strictly worse than staying inline, because the
74
+ // work is now both undone and invisible in the place it used to be logged.
75
+ DurableEntityActionRegistry.Instance.Register(new DurableEntityActionTaskSubmitter(providerFactory));
76
+
60
77
  return dispatcher;
61
78
  }