@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.
Files changed (261) hide show
  1. package/LICENSE +180 -4
  2. package/dist/auth/AuthProviderCatalogRouter.d.ts +31 -0
  3. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -0
  4. package/dist/auth/AuthProviderCatalogRouter.js +69 -0
  5. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -0
  6. package/dist/auth/AuthProviderEngine.d.ts +96 -0
  7. package/dist/auth/AuthProviderEngine.d.ts.map +1 -0
  8. package/dist/auth/AuthProviderEngine.js +245 -0
  9. package/dist/auth/AuthProviderEngine.js.map +1 -0
  10. package/dist/auth/exampleNewUserSubClass.js +1 -1
  11. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  12. package/dist/auth/index.d.ts +11 -0
  13. package/dist/auth/index.d.ts.map +1 -1
  14. package/dist/auth/index.js +65 -18
  15. package/dist/auth/index.js.map +1 -1
  16. package/dist/auth/initializeProviders.d.ts +64 -1
  17. package/dist/auth/initializeProviders.d.ts.map +1 -1
  18. package/dist/auth/initializeProviders.js +117 -5
  19. package/dist/auth/initializeProviders.js.map +1 -1
  20. package/dist/auth/magicLink/MagicLinkService.js +1 -1
  21. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  22. package/dist/auth/newUsers.js +1 -1
  23. package/dist/auth/newUsers.js.map +1 -1
  24. package/dist/config.d.ts +89 -0
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +52 -35
  27. package/dist/config.js.map +1 -1
  28. package/dist/context.js +2 -2
  29. package/dist/context.js.map +1 -1
  30. package/dist/generated/generated.d.ts +540 -1595
  31. package/dist/generated/generated.d.ts.map +1 -1
  32. package/dist/generated/generated.js +2755 -16857
  33. package/dist/generated/generated.js.map +1 -1
  34. package/dist/generic/ResolverBase.d.ts +20 -4
  35. package/dist/generic/ResolverBase.d.ts.map +1 -1
  36. package/dist/generic/ResolverBase.js +58 -21
  37. package/dist/generic/ResolverBase.js.map +1 -1
  38. package/dist/generic/RunViewResolver.d.ts +4 -0
  39. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  40. package/dist/generic/RunViewResolver.js +31 -0
  41. package/dist/generic/RunViewResolver.js.map +1 -1
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +183 -54
  45. package/dist/index.js.map +1 -1
  46. package/dist/integration/RSUProgressBridge.d.ts +83 -0
  47. package/dist/integration/RSUProgressBridge.d.ts.map +1 -0
  48. package/dist/integration/RSUProgressBridge.js +230 -0
  49. package/dist/integration/RSUProgressBridge.js.map +1 -0
  50. package/dist/integration/SchemaRefreshLaunch.d.ts +52 -0
  51. package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
  52. package/dist/integration/SchemaRefreshLaunch.js +50 -0
  53. package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
  54. package/dist/logging/secretRedactor.d.ts +9 -0
  55. package/dist/logging/secretRedactor.d.ts.map +1 -1
  56. package/dist/logging/secretRedactor.js +39 -0
  57. package/dist/logging/secretRedactor.js.map +1 -1
  58. package/dist/middleware/BaseServerMiddleware.d.ts +5 -0
  59. package/dist/middleware/BaseServerMiddleware.d.ts.map +1 -1
  60. package/dist/middleware/BaseServerMiddleware.js +5 -0
  61. package/dist/middleware/BaseServerMiddleware.js.map +1 -1
  62. package/dist/providerConfigUnits.d.ts +20 -0
  63. package/dist/providerConfigUnits.d.ts.map +1 -0
  64. package/dist/providerConfigUnits.js +22 -0
  65. package/dist/providerConfigUnits.js.map +1 -0
  66. package/dist/realtimeWidget/WidgetSessionService.js +1 -1
  67. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
  68. package/dist/realtimeWidget/widgetGuestElevation.js +1 -1
  69. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  70. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  71. package/dist/resolvers/ActionResolver.js +12 -0
  72. package/dist/resolvers/ActionResolver.js.map +1 -1
  73. package/dist/resolvers/ComponentRegistryResolver.js +1 -1
  74. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  75. package/dist/resolvers/EntityPermissionResolver.js +1 -1
  76. package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
  77. package/dist/resolvers/FileResolver.d.ts +34 -4
  78. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  79. package/dist/resolvers/FileResolver.js +218 -28
  80. package/dist/resolvers/FileResolver.js.map +1 -1
  81. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +85 -3
  82. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  83. package/dist/resolvers/IntegrationDiscoveryResolver.js +302 -82
  84. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  85. package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
  86. package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
  87. package/dist/resolvers/QueryResolver.d.ts +5 -4
  88. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  89. package/dist/resolvers/QueryResolver.js +29 -14
  90. package/dist/resolvers/QueryResolver.js.map +1 -1
  91. package/dist/resolvers/QuerySystemUserResolver.d.ts +12 -3
  92. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  93. package/dist/resolvers/QuerySystemUserResolver.js +33 -14
  94. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  95. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -0
  96. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  97. package/dist/resolvers/RealtimeClientSessionResolver.js +5 -0
  98. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  99. package/dist/resolvers/RunAIAgentResolver.d.ts +3 -1
  100. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  101. package/dist/resolvers/RunAIAgentResolver.js +20 -7
  102. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  103. package/dist/resolvers/SearchEntitiesResolver.js +1 -1
  104. package/dist/resolvers/SearchEntitiesResolver.js.map +1 -1
  105. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  106. package/dist/resolvers/SqlLoggingConfigResolver.js +1 -1
  107. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  108. package/dist/resolvers/SyncRolesUsersResolver.js +2 -2
  109. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  110. package/dist/resolvers/TaskGraphFrameResolver.d.ts +18 -0
  111. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
  112. package/dist/resolvers/TaskGraphFrameResolver.js +75 -0
  113. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
  114. package/dist/resolvers/UserFavoriteResolver.js +1 -1
  115. package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
  116. package/dist/resolvers/UserResolver.d.ts +19 -17
  117. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  118. package/dist/resolvers/UserResolver.js +66 -67
  119. package/dist/resolvers/UserResolver.js.map +1 -1
  120. package/dist/resolvers/currentUserRoles.d.ts +19 -0
  121. package/dist/resolvers/currentUserRoles.d.ts.map +1 -0
  122. package/dist/resolvers/currentUserRoles.js +36 -0
  123. package/dist/resolvers/currentUserRoles.js.map +1 -0
  124. package/dist/rest/MediaAccessKeys.d.ts +17 -0
  125. package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
  126. package/dist/rest/MediaAccessKeys.js +42 -1
  127. package/dist/rest/MediaAccessKeys.js.map +1 -1
  128. package/dist/rest/MediaStreamHandler.d.ts +3 -2
  129. package/dist/rest/MediaStreamHandler.d.ts.map +1 -1
  130. package/dist/rest/MediaStreamHandler.js +83 -2
  131. package/dist/rest/MediaStreamHandler.js.map +1 -1
  132. package/dist/rest/OAuthCallbackHandler.d.ts +49 -1
  133. package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
  134. package/dist/rest/OAuthCallbackHandler.js +130 -44
  135. package/dist/rest/OAuthCallbackHandler.js.map +1 -1
  136. package/dist/rest/SignatureWebhookHandler.js +1 -1
  137. package/dist/rest/SignatureWebhookHandler.js.map +1 -1
  138. package/dist/rest/UploadTokenManager.d.ts +90 -0
  139. package/dist/rest/UploadTokenManager.d.ts.map +1 -0
  140. package/dist/rest/UploadTokenManager.js +156 -0
  141. package/dist/rest/UploadTokenManager.js.map +1 -0
  142. package/dist/services/DurableEntityActionTaskSubmitter.d.ts +26 -0
  143. package/dist/services/DurableEntityActionTaskSubmitter.d.ts.map +1 -0
  144. package/dist/services/DurableEntityActionTaskSubmitter.js +55 -0
  145. package/dist/services/DurableEntityActionTaskSubmitter.js.map +1 -0
  146. package/dist/services/IntegrationSyncWorkerService.d.ts +48 -0
  147. package/dist/services/IntegrationSyncWorkerService.d.ts.map +1 -0
  148. package/dist/services/IntegrationSyncWorkerService.js +132 -0
  149. package/dist/services/IntegrationSyncWorkerService.js.map +1 -0
  150. package/dist/services/ScheduledJobsService.js +1 -1
  151. package/dist/services/ScheduledJobsService.js.map +1 -1
  152. package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
  153. package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
  154. package/dist/services/StartTaskGraphDispatcher.js +37 -2
  155. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  156. package/dist/services/TaskGraphActionRunner.d.ts +34 -0
  157. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -0
  158. package/dist/services/TaskGraphActionRunner.js +95 -0
  159. package/dist/services/TaskGraphActionRunner.js.map +1 -0
  160. package/dist/services/TaskGraphAgentRunner.d.ts +20 -0
  161. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  162. package/dist/services/TaskGraphAgentRunner.js +53 -0
  163. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  164. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  165. package/dist/services/TaskGraphContinuationDeliverer.js +12 -1
  166. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  167. package/dist/services/TaskGraphPromptRunner.d.ts +31 -0
  168. package/dist/services/TaskGraphPromptRunner.d.ts.map +1 -0
  169. package/dist/services/TaskGraphPromptRunner.js +134 -0
  170. package/dist/services/TaskGraphPromptRunner.js.map +1 -0
  171. package/dist/telephony/RingCentralTelephonyService.js +1 -1
  172. package/dist/telephony/RingCentralTelephonyService.js.map +1 -1
  173. package/dist/telephony/TwilioTelephonyRouter.js +1 -1
  174. package/dist/telephony/TwilioTelephonyRouter.js.map +1 -1
  175. package/dist/telephony/VonageTelephonyRouter.js +1 -1
  176. package/dist/telephony/VonageTelephonyRouter.js.map +1 -1
  177. package/dist/types.d.ts +6 -0
  178. package/dist/types.d.ts.map +1 -1
  179. package/dist/types.js.map +1 -1
  180. package/package.json +91 -91
  181. package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
  182. package/src/__tests__/AuthProviderEngine.test.ts +278 -0
  183. package/src/__tests__/EntityCRUDHandler.test.ts +378 -0
  184. package/src/__tests__/IntegrationSyncWorkerService.test.ts +275 -0
  185. package/src/__tests__/OAuthCallbackHandler.openRedirect.test.ts +117 -0
  186. package/src/__tests__/OAuthCallbackHandler.xss.test.ts +4 -1
  187. package/src/__tests__/RESTEndpointHandler.test.ts +672 -0
  188. package/src/__tests__/RSUProgressBridge.test.ts +250 -0
  189. package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -2
  190. package/src/__tests__/RealtimeClientSessionResolver.test.ts +1 -1
  191. package/src/__tests__/ResolverBase.frozenRecordMapping.test.ts +133 -0
  192. package/src/__tests__/ResolverBase.transportMapping.test.ts +289 -0
  193. package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
  194. package/src/__tests__/SessionManager.test.ts +1 -1
  195. package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
  196. package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
  197. package/src/__tests__/WidgetSessionService.test.ts +1 -1
  198. package/src/__tests__/config-units.test.ts +74 -0
  199. package/src/__tests__/databaseAbstraction.test.ts +1 -1
  200. package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
  201. package/src/__tests__/magicLinkService.provisionLog.test.ts +1 -1
  202. package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
  203. package/src/__tests__/newUsers.test.ts +726 -0
  204. package/src/__tests__/resolverBase.rls.test.ts +3 -0
  205. package/src/__tests__/secretRedactor.failOpen.test.ts +135 -0
  206. package/src/__tests__/uploadTokenManager.test.ts +159 -0
  207. package/src/__tests__/widgetGuestElevation.test.ts +1 -1
  208. package/src/auth/AuthProviderCatalogRouter.ts +76 -0
  209. package/src/auth/AuthProviderEngine.ts +253 -0
  210. package/src/auth/exampleNewUserSubClass.ts +1 -1
  211. package/src/auth/index.ts +69 -21
  212. package/src/auth/initializeProviders.ts +128 -7
  213. package/src/auth/magicLink/MagicLinkService.ts +1 -1
  214. package/src/auth/newUsers.ts +1 -1
  215. package/src/config.ts +55 -36
  216. package/src/context.ts +2 -2
  217. package/src/generated/generated.ts +2038 -10529
  218. package/src/generic/ResolverBase.ts +63 -21
  219. package/src/generic/RunViewResolver.ts +31 -0
  220. package/src/index.ts +199 -56
  221. package/src/integration/RSUProgressBridge.ts +248 -0
  222. package/src/integration/SchemaRefreshLaunch.ts +77 -0
  223. package/src/logging/secretRedactor.ts +43 -0
  224. package/src/middleware/BaseServerMiddleware.ts +5 -0
  225. package/src/providerConfigUnits.ts +21 -0
  226. package/src/realtimeWidget/WidgetSessionService.ts +1 -1
  227. package/src/realtimeWidget/widgetGuestElevation.ts +1 -1
  228. package/src/resolvers/ActionResolver.ts +13 -0
  229. package/src/resolvers/ComponentRegistryResolver.ts +1 -1
  230. package/src/resolvers/EntityPermissionResolver.ts +1 -1
  231. package/src/resolvers/FileResolver.ts +200 -40
  232. package/src/resolvers/IntegrationDiscoveryResolver.ts +328 -70
  233. package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -1
  234. package/src/resolvers/QueryResolver.ts +25 -11
  235. package/src/resolvers/QuerySystemUserResolver.ts +28 -13
  236. package/src/resolvers/RealtimeClientSessionResolver.ts +13 -0
  237. package/src/resolvers/RunAIAgentResolver.ts +21 -6
  238. package/src/resolvers/SearchEntitiesResolver.ts +1 -1
  239. package/src/resolvers/SqlLoggingConfigResolver.ts +1 -2
  240. package/src/resolvers/SyncRolesUsersResolver.ts +2 -2
  241. package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
  242. package/src/resolvers/UserFavoriteResolver.ts +1 -1
  243. package/src/resolvers/UserResolver.ts +99 -60
  244. package/src/resolvers/currentUserRoles.ts +47 -0
  245. package/src/rest/MediaAccessKeys.ts +47 -1
  246. package/src/rest/MediaStreamHandler.ts +98 -3
  247. package/src/rest/OAuthCallbackHandler.ts +150 -42
  248. package/src/rest/SignatureWebhookHandler.ts +1 -1
  249. package/src/rest/UploadTokenManager.ts +211 -0
  250. package/src/services/DurableEntityActionTaskSubmitter.ts +78 -0
  251. package/src/services/IntegrationSyncWorkerService.ts +141 -0
  252. package/src/services/ScheduledJobsService.ts +1 -1
  253. package/src/services/StartTaskGraphDispatcher.ts +38 -1
  254. package/src/services/TaskGraphActionRunner.ts +99 -0
  255. package/src/services/TaskGraphAgentRunner.ts +58 -1
  256. package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
  257. package/src/services/TaskGraphPromptRunner.ts +144 -0
  258. package/src/telephony/RingCentralTelephonyService.ts +1 -1
  259. package/src/telephony/TwilioTelephonyRouter.ts +1 -1
  260. package/src/telephony/VonageTelephonyRouter.ts +1 -1
  261. package/src/types.ts +6 -0
@@ -0,0 +1,248 @@
1
+ /**
2
+ * @fileoverview Bridges RSU pipeline lifecycle events onto the integration progress artifact
3
+ * stream, so a Runtime Schema Update is observable the same way a sync or a connector build is.
4
+ *
5
+ * The gap this closes: `IntegrationRunKind` has always included `'RSU'` and
6
+ * `RUN_KIND_TO_TOPIC` has always mapped it to an `'RSU'` subscription channel — but nothing in
7
+ * production ever constructed an emitter with that kind, so the channel carried no traffic and
8
+ * `IntegrationTailRunEvents` had no RSU runs to tail. The only live signal was polling
9
+ * `RuntimeSchemaUpdateStatus`, which reports the CURRENT step and nothing else: no history, no
10
+ * durable record, and nothing at all to read after a mid-run API restart — which the RSU pipeline
11
+ * does to itself, by design, as one of its own steps.
12
+ *
13
+ * Layering: `@memberjunction/schema-engine` must not depend on the progress artifacts package, so
14
+ * it publishes plain {@link RSUObserverEvent}s and this module — one layer up, where both are
15
+ * already dependencies — owns the emitter lifecycle.
16
+ */
17
+ import { IntegrationProgressEmitter } from '@memberjunction/integration-progress-artifacts';
18
+ import type { EmitterOptions } from '@memberjunction/integration-progress-artifacts';
19
+ import { RuntimeSchemaManager } from '@memberjunction/schema-engine';
20
+ import type { RSUObserverEvent } from '@memberjunction/schema-engine';
21
+ import { LogError } from '@memberjunction/core';
22
+ import { BaseSingleton } from '@memberjunction/global';
23
+
24
+ /** The emitter stage name used for run-level (non-step) events. */
25
+ const RUN_STAGE = 'RSUPipeline';
26
+
27
+ /**
28
+ * How often an in-flight step reports that it is still alive.
29
+ *
30
+ * RSU's expensive steps — CodeGen, TypeScript compile, npm install — run for minutes with no
31
+ * intermediate observer event, so without this the stream goes silent for the entire step and a
32
+ * watcher cannot tell "compiling" from "hung". 30s matches the interval the agent notification
33
+ * conventions ask for on long phases.
34
+ */
35
+ const HEARTBEAT_INTERVAL_MS = 30_000;
36
+
37
+ /**
38
+ * Owns one emitter per RSU pipeline run and translates observer events into progress events.
39
+ *
40
+ * A {@link BaseSingleton} because the state here is inherently process-wide AND has to be readable
41
+ * from elsewhere. `RuntimeSchemaManager` is itself a singleton and serializes its runs behind the
42
+ * pipeline lock, so at most one RSU run is ever in flight and one current-emitter slot is sufficient
43
+ * — but the reason for the singleton is {@link CurrentRunID}: a resolver that has just triggered an
44
+ * RSU needs to hand its client a run to tail, and it can only do that if it can reach this instance.
45
+ * Constructing the bridge and keeping the only reference inside the observer closure made that
46
+ * accessor unreachable from any caller.
47
+ *
48
+ * NOT a `BaseEngine`: there is no metadata to load and nothing to `Config()`. The distinction is
49
+ * "single shared instance" (this) versus "cached metadata with a load lifecycle" (that).
50
+ */
51
+ export class RSUProgressBridge extends BaseSingleton<RSUProgressBridge> {
52
+ private emitter: IntegrationProgressEmitter | null = null;
53
+ private currentRunID: string | null = null;
54
+ /** Liveness timer for the step currently in flight — see {@link HEARTBEAT_INTERVAL_MS}. */
55
+ private heartbeat: ReturnType<typeof setInterval> | null = null;
56
+ private emitterOptions: EmitterOptions = {};
57
+
58
+ /** The one bridge for this process. */
59
+ public static get Instance(): RSUProgressBridge {
60
+ return super.getInstance<RSUProgressBridge>();
61
+ }
62
+
63
+ /**
64
+ * Options passed through to each run's emitter. Set once, before the first run.
65
+ *
66
+ * The only field that matters in practice is `rootDir` — left unset in production so RSU runs
67
+ * land in the same `logs/integration-runs/` tree as every other run kind and
68
+ * `IntegrationTailRunEvents` finds them without configuration. Tests point it at a temp dir.
69
+ *
70
+ * A settable property rather than a constructor argument because `BaseSingleton.getInstance`
71
+ * requires a zero-argument constructor.
72
+ */
73
+ public Configure(emitterOptions: EmitterOptions): void {
74
+ this.emitterOptions = emitterOptions;
75
+ }
76
+
77
+ /**
78
+ * The run ID of the in-flight RSU run, or null when idle. Lets a caller that triggered an RSU
79
+ * hand a client something to tail via `IntegrationTailRunEvents` / the `RSU` subscription
80
+ * channel.
81
+ */
82
+ public get CurrentRunID(): string | null {
83
+ return this.currentRunID;
84
+ }
85
+
86
+ /**
87
+ * Abandons any in-flight run state and stops its heartbeat, without emitting a terminal event.
88
+ *
89
+ * Needed because singleton state outlives a single caller: a test (or a server tearing down)
90
+ * must be able to return the bridge to idle without the next run inheriting a stale emitter or
91
+ * leaking an interval that keeps the process alive.
92
+ */
93
+ public Reset(): void {
94
+ this.stopHeartbeat();
95
+ this.emitter = null;
96
+ this.currentRunID = null;
97
+ }
98
+
99
+ /** The observer to hand to {@link RuntimeSchemaManager.PipelineObserver}. */
100
+ public readonly Observe = (event: RSUObserverEvent): void => {
101
+ switch (event.Kind) {
102
+ case 'run.start':
103
+ return this.onRunStart(event);
104
+ case 'step.start':
105
+ return this.onStepStart(event);
106
+ case 'step.end':
107
+ return this.onStepEnd(event);
108
+ case 'run.end':
109
+ return this.onRunEnd(event);
110
+ }
111
+ };
112
+
113
+ private onRunStart(event: Extract<RSUObserverEvent, { Kind: 'run.start' }>): void {
114
+ // A retry re-enters RunPipelineBatch, so it legitimately starts a NEW run. If a previous
115
+ // emitter is still open (a throw escaped before run.end), close it rather than leak it.
116
+ if (this.emitter) this.closeCurrent('Superseded by a new RSU run');
117
+
118
+ const runID = IntegrationProgressEmitter.newRunID('rsu');
119
+ this.currentRunID = runID;
120
+ this.emitter = new IntegrationProgressEmitter(
121
+ {
122
+ runID,
123
+ runKind: 'RSU',
124
+ triggerType: 'Pipeline',
125
+ startedAt: new Date().toISOString(),
126
+ context: {
127
+ itemCount: event.ItemCount,
128
+ descriptions: event.Descriptions,
129
+ affectedTables: event.AffectedTables,
130
+ stepTotal: event.StepTotal,
131
+ },
132
+ },
133
+ this.emitterOptions,
134
+ );
135
+ this.emitter.runStart(
136
+ `RSU pipeline started — ${event.ItemCount} migration(s), ${event.StepTotal} expected step(s): ` +
137
+ event.Descriptions.join('; ')
138
+ );
139
+ }
140
+
141
+ private onStepStart(event: Extract<RSUObserverEvent, { Kind: 'step.start' }>): void {
142
+ if (!this.emitter) return;
143
+ this.emitter.stageStart(event.Name, this.positionLabel(event.StepIndex, event.StepTotal));
144
+ this.startHeartbeat(event.Name);
145
+ }
146
+
147
+ private onStepEnd(event: Extract<RSUObserverEvent, { Kind: 'step.end' }>): void {
148
+ this.stopHeartbeat();
149
+ if (!this.emitter) return;
150
+ // A failed step is a stage error, not a stage completion — the run may still continue (a
151
+ // per-item migration failure does not abort the batch), so this is deliberately not fatal.
152
+ if (event.Status === 'failed') {
153
+ this.emitter.stageError(event.Name, event.Message, { durationMs: event.DurationMs, code: 'RSU_STEP_FAILED' });
154
+ return;
155
+ }
156
+ // Deliberately no `counts` here. Counts roll up into the run aggregate, and the countable
157
+ // unit of an RSU run is MIGRATIONS, not steps — reporting "processed: 12" for a 12-step
158
+ // single-migration run would read as 12 migrations. The run-level quartet is emitted once,
159
+ // in onRunEnd. Step position lives in the stage name + the step.start message.
160
+ this.emitter.stageComplete(event.Name);
161
+ }
162
+
163
+ private onRunEnd(event: Extract<RSUObserverEvent, { Kind: 'run.end' }>): void {
164
+ this.stopHeartbeat();
165
+ const emitter = this.emitter;
166
+ if (!emitter) return;
167
+ this.emitter = null;
168
+ this.currentRunID = null;
169
+ const summary = `${event.SuccessCount}/${event.TotalCount} migration(s) succeeded`;
170
+ // The one authoritative applied quartet for the run, in migrations — this is what feeds
171
+ // the reader's aggregateCounts.
172
+ emitter.stageComplete(RUN_STAGE, {
173
+ processed: event.TotalCount,
174
+ succeeded: event.SuccessCount,
175
+ failed: event.FailureCount,
176
+ totalKnown: event.TotalCount,
177
+ });
178
+ // Terminal writes are async (they flush the JSONL + write result.json). The observer
179
+ // contract is synchronous, so this is intentionally fire-and-forget with a logged catch —
180
+ // a progress-artifact write must never surface as a pipeline failure.
181
+ const terminal = event.Success
182
+ ? emitter.complete(`RSU pipeline complete — ${summary}`)
183
+ : emitter.fail(
184
+ `RSU pipeline failed at '${event.ErrorStep ?? 'unknown step'}' — ${summary}: ` +
185
+ `${event.ErrorMessage ?? 'no error message'}`,
186
+ 'rsu-pipeline-failed'
187
+ );
188
+ void terminal.catch(err => LogError(`RSUProgressBridge: terminal write failed — ${err}`));
189
+ }
190
+
191
+ /**
192
+ * Starts reporting liveness for the step that just opened. Any previous timer is cleared first,
193
+ * so a missed `step.end` can never leave two timers running.
194
+ */
195
+ private startHeartbeat(stage: string): void {
196
+ this.stopHeartbeat();
197
+ const startedAt = Date.now();
198
+ this.heartbeat = setInterval(() => {
199
+ const elapsedSec = Math.round((Date.now() - startedAt) / 1000);
200
+ this.emitter?.heartbeat(stage, `${stage} still running — ${elapsedSec}s elapsed`);
201
+ }, HEARTBEAT_INTERVAL_MS);
202
+ // Never hold the process open on a progress timer.
203
+ this.heartbeat.unref?.();
204
+ }
205
+
206
+ /** Stops the liveness timer. Safe to call when none is running. */
207
+ private stopHeartbeat(): void {
208
+ if (!this.heartbeat) return;
209
+ clearInterval(this.heartbeat);
210
+ this.heartbeat = null;
211
+ }
212
+
213
+ /** Closes an orphaned emitter so a leaked run does not stay in-flight forever. */
214
+ private closeCurrent(reason: string): void {
215
+ this.stopHeartbeat();
216
+ const emitter = this.emitter;
217
+ this.emitter = null;
218
+ this.currentRunID = null;
219
+ void emitter?.fail(reason, 'rsu-run-abandoned')
220
+ .catch(err => LogError(`RSUProgressBridge: abandon write failed — ${err}`));
221
+ }
222
+
223
+ /**
224
+ * "step 4 of 12" — omitted entirely when the determinate counter isn't armed.
225
+ *
226
+ * Explicit null/undefined checks rather than truthiness: a 0 index is falsy, and silently
227
+ * dropping the label for the first step of a 0-based counter would be a bug that only appears
228
+ * if the pipeline's numbering ever changes.
229
+ */
230
+ private positionLabel(stepIndex?: number, stepTotal?: number): string | undefined {
231
+ if (stepIndex == null || stepTotal == null) return undefined;
232
+ return `step ${stepIndex} of ${stepTotal}`;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Attaches a fresh {@link RSUProgressBridge} to the RuntimeSchemaManager singleton. Call once at
238
+ * server startup, after the progress publish hook is registered so the very first RSU event also
239
+ * reaches live subscribers.
240
+ *
241
+ * @returns the bridge, so callers can read {@link RSUProgressBridge.CurrentRunID}.
242
+ */
243
+ export function RegisterRSUProgressBridge(emitterOptions: EmitterOptions = {}): RSUProgressBridge {
244
+ const bridge = RSUProgressBridge.Instance;
245
+ bridge.Configure(emitterOptions);
246
+ RuntimeSchemaManager.Instance.PipelineObserver = bridge.Observe;
247
+ return bridge;
248
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @fileoverview Pure reporting helpers for the connector schema-refresh pipeline launch.
3
+ *
4
+ * The refresh is a live vendor introspect and can run for minutes on a large catalog, so
5
+ * IntegrationCreateConnection / IntegrationUpdateConnection can launch it *detached* and hand the
6
+ * caller a tailable run ID instead of blocking. That split creates one easy-to-get-wrong reporting
7
+ * case: a detached summary's counts are placeholder zeros, and describing them as results ("0
8
+ * created, 0 updated") reads to an operator as "the refresh found nothing" — the opposite of "the
9
+ * refresh has not finished yet". This module owns that decision so it can be pinned by a test
10
+ * without standing up a resolver.
11
+ */
12
+
13
+ /**
14
+ * The subset of the pipeline summary that message construction reads. Structural so both the
15
+ * GraphQL `@ObjectType` and a test fixture satisfy it without either depending on the other.
16
+ */
17
+ export interface SchemaRefreshSummaryLike {
18
+ /** Tailable run ID — the only meaningful field while `InProgress` is true. */
19
+ RunID: string;
20
+ /** True ⇒ launched detached and still running; every count below is a placeholder zero. */
21
+ InProgress: boolean;
22
+ /**
23
+ * Whether the pipeline itself succeeded. A pipeline that fails at ConnectionTest RETURNS rather
24
+ * than throwing, with every count at zero — so counts alone read identically to a clean
25
+ * no-op refresh. Reporting that as "0 created, 0 updated" tells an operator the vendor had
26
+ * nothing new when in fact the credentials were never accepted.
27
+ */
28
+ Succeeded: boolean;
29
+ /** The pipeline's own failure reason, when `Succeeded` is false. */
30
+ FailureMessage?: string;
31
+ ObjectsCreated: number;
32
+ ObjectsUpdated: number;
33
+ UnresolvedObjects: string[];
34
+ }
35
+
36
+ /** The counts clause, or an explicit failure clause when the pipeline did not succeed. */
37
+ function describeFinishedRefresh(summary: SchemaRefreshSummaryLike): string {
38
+ if (!summary.Succeeded) {
39
+ return `schema refresh FAILED (${summary.FailureMessage ?? 'no reason reported'}) — see run ${summary.RunID}`;
40
+ }
41
+ return `schema refresh: ${summary.ObjectsCreated} created, ` +
42
+ `${summary.ObjectsUpdated} updated, ${summary.UnresolvedObjects.length} PK-unresolved`;
43
+ }
44
+
45
+ /**
46
+ * Builds the human-facing `Message` for IntegrationCreateConnection.
47
+ *
48
+ * @param testConnection whether the caller asked for (and got) a live connection test
49
+ * @param summary the refresh summary, or undefined when no refresh was requested
50
+ */
51
+ export function BuildCreateConnectionMessage(
52
+ testConnection: boolean,
53
+ summary: SchemaRefreshSummaryLike | undefined
54
+ ): string {
55
+ if (!summary) return 'Connection created and test passed';
56
+ const created = `Connection created${testConnection ? ', test passed' : ''}`;
57
+ // Never report a detached run's placeholder counts as findings — point at the run stream instead.
58
+ if (summary.InProgress) return `${created}, schema refresh running — tail run ${summary.RunID}`;
59
+ return `${created}, ${describeFinishedRefresh(summary)}`;
60
+ }
61
+
62
+ /**
63
+ * Builds the human-facing `Message` for IntegrationUpdateConnection's blocking-refresh path.
64
+ * Shares {@link describeFinishedRefresh} with create so a failed refresh can never be reported as
65
+ * a clean zero-count run on one path and honestly on the other.
66
+ */
67
+ export function BuildUpdateConnectionMessage(summary: SchemaRefreshSummaryLike): string {
68
+ return `Updated, ${describeFinishedRefresh(summary)}`;
69
+ }
70
+
71
+ /**
72
+ * Builds the human-facing `Message` for IntegrationUpdateConnection's detached-refresh path.
73
+ * Separate from {@link BuildCreateConnectionMessage} because update has no connection-test clause.
74
+ */
75
+ export function BuildDetachedRefreshMessage(runID: string): string {
76
+ return `Updated, schema refresh running — tail run ${runID}`;
77
+ }
@@ -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;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Unit-conversion seam between MJServer's configuration and SQLServerDataProvider.
3
+ *
4
+ * MJServer configures `databaseSettings.metadataCacheRefreshInterval` in **milliseconds**
5
+ * (default 180000 = 3 minutes, overridable via the METADATA_CACHE_REFRESH_INTERVAL env var).
6
+ * `SQLServerProviderConfigData`'s third constructor parameter is `checkRefreshIntervalSeconds`
7
+ * — declared in **seconds**.
8
+ *
9
+ * Commit 645c5a5e8 fixed a bug where the raw millisecond value was handed straight to the
10
+ * provider, so the "3 minute" default became a 180000-second (~50 hour) metadata cache
11
+ * refresh cadence. This function is the single place that conversion happens; both provider
12
+ * bootstrap call sites in `index.ts` (the main pool and the CodeGen-credentialed pool) go
13
+ * through it, and `config-units.test.ts` pins the contract so the units can never silently
14
+ * regress again.
15
+ *
16
+ * @param metadataCacheRefreshIntervalMs - the configured refresh interval, in milliseconds
17
+ * @returns the same interval expressed in seconds, the unit `checkRefreshIntervalSeconds` declares
18
+ */
19
+ export function MetadataCacheRefreshIntervalSeconds(metadataCacheRefreshIntervalMs: number): number {
20
+ return metadataCacheRefreshIntervalMs / 1000;
21
+ }
@@ -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 {