@memberjunction/server 6.1.0-edge.0 → 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.
- package/dist/auth/exampleNewUserSubClass.js +1 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +9 -5
- package/dist/auth/index.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/context.js +2 -2
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +208 -646
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +1123 -3773
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +19 -3
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +51 -17
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +100 -42
- package/dist/index.js.map +1 -1
- 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/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/IntegrationDiscoveryResolver.d.ts +9 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +3 -1
- 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/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/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 +85 -0
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -0
- package/dist/resolvers/TaskGraphFrameResolver.js +163 -0
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -0
- package/dist/resolvers/UserFavoriteResolver.js +1 -1
- package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +1 -1
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/RESTEndpointHandler.d.ts +0 -4
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
- package/dist/rest/RESTEndpointHandler.js +1 -15
- package/dist/rest/RESTEndpointHandler.js.map +1 -1
- package/dist/rest/SignatureWebhookHandler.js +1 -1
- package/dist/rest/SignatureWebhookHandler.js.map +1 -1
- 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/ScheduledJobsService.js +1 -1
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.d.ts +33 -0
- package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -0
- package/dist/services/StartTaskGraphDispatcher.js +46 -0
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -0
- 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 +28 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphAgentRunner.js +104 -0
- package/dist/services/TaskGraphAgentRunner.js.map +1 -0
- package/dist/services/TaskGraphContinuationDeliverer.d.ts +70 -0
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -0
- package/dist/services/TaskGraphContinuationDeliverer.js +162 -0
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -0
- 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/services/TaskGraphProviderFactory.d.ts +39 -0
- package/dist/services/TaskGraphProviderFactory.d.ts.map +1 -0
- package/dist/services/TaskGraphProviderFactory.js +28 -0
- package/dist/services/TaskGraphProviderFactory.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/package.json +92 -89
- 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__/SessionManager.test.ts +1 -1
- package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +249 -0
- package/src/__tests__/TaskGraphFrameResolver.test.ts +136 -0
- package/src/__tests__/WidgetSessionService.test.ts +1 -1
- package/src/__tests__/databaseAbstraction.test.ts +1 -1
- package/src/__tests__/magicLinkService.provisionLog.test.ts +1 -1
- package/src/__tests__/resolverBase.rls.test.ts +3 -0
- package/src/__tests__/secretRedactor.failOpen.test.ts +135 -0
- package/src/__tests__/widgetGuestElevation.test.ts +1 -1
- package/src/auth/exampleNewUserSubClass.ts +1 -1
- package/src/auth/index.ts +10 -6
- package/src/auth/magicLink/MagicLinkService.ts +1 -1
- package/src/auth/newUsers.ts +1 -1
- package/src/context.ts +2 -2
- package/src/generated/generated.ts +955 -2735
- package/src/generic/ResolverBase.ts +52 -17
- package/src/index.ts +108 -43
- package/src/logging/secretRedactor.ts +43 -0
- package/src/middleware/BaseServerMiddleware.ts +5 -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/IntegrationDiscoveryResolver.ts +12 -2
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -1
- package/src/resolvers/RealtimeClientSessionResolver.ts +13 -0
- 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 +160 -0
- package/src/resolvers/UserFavoriteResolver.ts +1 -1
- package/src/rest/OAuthCallbackHandler.ts +1 -1
- package/src/rest/RESTEndpointHandler.ts +1 -15
- package/src/rest/SignatureWebhookHandler.ts +1 -1
- package/src/services/DurableEntityActionTaskSubmitter.ts +78 -0
- package/src/services/ScheduledJobsService.ts +1 -1
- package/src/services/StartTaskGraphDispatcher.ts +78 -0
- package/src/services/TaskGraphActionRunner.ts +99 -0
- package/src/services/TaskGraphAgentRunner.ts +115 -0
- package/src/services/TaskGraphContinuationDeliverer.ts +181 -0
- package/src/services/TaskGraphPromptRunner.ts +144 -0
- package/src/services/TaskGraphProviderFactory.ts +53 -0
- package/src/telephony/RingCentralTelephonyService.ts +1 -1
- package/src/telephony/TwilioTelephonyRouter.ts +1 -1
- package/src/telephony/VonageTelephonyRouter.ts +1 -1
- package/dist/resolvers/ReportResolver.d.ts +0 -24
- package/dist/resolvers/ReportResolver.d.ts.map +0 -1
- package/dist/resolvers/ReportResolver.js +0 -198
- package/dist/resolvers/ReportResolver.js.map +0 -1
- package/dist/resolvers/TaskResolver.d.ts +0 -31
- package/dist/resolvers/TaskResolver.d.ts.map +0 -1
- package/dist/resolvers/TaskResolver.js +0 -157
- package/dist/resolvers/TaskResolver.js.map +0 -1
- package/dist/services/TaskOrchestrator.d.ts +0 -130
- package/dist/services/TaskOrchestrator.d.ts.map +0 -1
- package/dist/services/TaskOrchestrator.js +0 -692
- package/dist/services/TaskOrchestrator.js.map +0 -1
- package/src/resolvers/ReportResolver.ts +0 -165
- package/src/resolvers/TaskResolver.ts +0 -148
- package/src/resolvers/__tests__/ReportResolver.test.ts +0 -279
- package/src/services/TaskOrchestrator.ts +0 -873
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `TaskGraphContinuationDeliverer`.
|
|
3
|
+
*
|
|
4
|
+
* The gap this closes: the dispatcher was constructed with no deliverer at all, so a graph that
|
|
5
|
+
* finished logged its outcome, marked itself delivered, and said nothing to the conversation that
|
|
6
|
+
* asked for it — durable execution nobody hears about.
|
|
7
|
+
*
|
|
8
|
+
* The contract that matters most is that it **never throws**. The dispatcher calls this inside the
|
|
9
|
+
* compare-and-swap that marks a completion delivered; an escaping error would either abort that
|
|
10
|
+
* guard or leave the graph looking undelivered and re-notifying on every later sweep.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
13
|
+
|
|
14
|
+
vi.mock('@memberjunction/core', () => ({
|
|
15
|
+
LogError: vi.fn(),
|
|
16
|
+
LogStatus: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock('@memberjunction/core-entities', () => ({
|
|
20
|
+
MJConversationDetailEntity: class {},
|
|
21
|
+
MJAIAgentRunEntity: class {},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('@memberjunction/ai-core-plus', () => ({ MJAIAgentEntityExtended: class {} }));
|
|
25
|
+
vi.mock('@memberjunction/ai', () => ({ ChatMessageRole: { user: 'user' } }));
|
|
26
|
+
|
|
27
|
+
const runAgent = vi.fn().mockResolvedValue({ success: true });
|
|
28
|
+
vi.mock('@memberjunction/ai-agents', () => ({
|
|
29
|
+
AgentRunner: class { RunAgent = runAgent; },
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
import { TaskGraphContinuationDeliverer } from '../services/TaskGraphContinuationDeliverer';
|
|
33
|
+
import type { TaskContinuationParams } from '@memberjunction/task-graph';
|
|
34
|
+
import type { UserInfo } from '@memberjunction/core';
|
|
35
|
+
|
|
36
|
+
/** A conversation-detail row that can be loaded (the source) and saved (the reply). */
|
|
37
|
+
function detailRow(over: Partial<{ loads: boolean; saves: boolean; conversationID: string }> = {}) {
|
|
38
|
+
const { loads = true, saves = true, conversationID = 'conv-1' } = over;
|
|
39
|
+
return {
|
|
40
|
+
ConversationID: conversationID,
|
|
41
|
+
Role: '', Status: '', HiddenToUser: true, Message: '',
|
|
42
|
+
NewRecord: vi.fn(),
|
|
43
|
+
Load: vi.fn().mockResolvedValue(loads),
|
|
44
|
+
Save: vi.fn().mockResolvedValue(saves),
|
|
45
|
+
LatestResult: { CompleteMessage: 'FK violation' },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function harness(over: Parameters<typeof detailRow>[0] = {}) {
|
|
50
|
+
const rows: ReturnType<typeof detailRow>[] = [];
|
|
51
|
+
const provider = {
|
|
52
|
+
GetEntityObject: vi.fn().mockImplementation(async () => {
|
|
53
|
+
const row = detailRow(over);
|
|
54
|
+
rows.push(row);
|
|
55
|
+
return row;
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
const providerFactory = { CreateProvider: vi.fn().mockResolvedValue(provider) };
|
|
59
|
+
const deliverer = new TaskGraphContinuationDeliverer(
|
|
60
|
+
providerFactory as never,
|
|
61
|
+
{ ID: 'user-1' } as UserInfo,
|
|
62
|
+
);
|
|
63
|
+
// First row is the source detail (Load), second is the reply (Save).
|
|
64
|
+
return { deliverer, providerFactory, provider, rows, reply: () => rows[1] };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const params = (over: Partial<TaskContinuationParams> = {}): TaskContinuationParams => ({
|
|
68
|
+
ParentTaskID: 'parent-1',
|
|
69
|
+
WorkflowName: 'Weekly digest',
|
|
70
|
+
ConversationDetailID: 'detail-1',
|
|
71
|
+
SubmittedByAgentRunID: 'run-1',
|
|
72
|
+
ReinvokeDepth: 0,
|
|
73
|
+
Tasks: [
|
|
74
|
+
{ TaskID: 't1', Name: 'Gather', Status: 'Complete', Summary: '412 rows' },
|
|
75
|
+
{ TaskID: 't2', Name: 'Summarize', Status: 'Complete' },
|
|
76
|
+
],
|
|
77
|
+
Summary: '2 of 2 steps completed.',
|
|
78
|
+
...over,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
beforeEach(() => vi.clearAllMocks());
|
|
82
|
+
|
|
83
|
+
describe('posting the outcome', () => {
|
|
84
|
+
it('writes an AI-role message into the graph\'s conversation', async () => {
|
|
85
|
+
const h = harness();
|
|
86
|
+
await h.deliverer.PostMessage(params());
|
|
87
|
+
|
|
88
|
+
const reply = h.reply();
|
|
89
|
+
expect(reply.Save).toHaveBeenCalled();
|
|
90
|
+
expect(reply.ConversationID).toBe('conv-1');
|
|
91
|
+
expect(reply.Role).toBe('AI');
|
|
92
|
+
expect(reply.Status).toBe('Complete');
|
|
93
|
+
expect(reply.HiddenToUser).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('names the workflow and carries the roll-up', async () => {
|
|
97
|
+
const h = harness();
|
|
98
|
+
await h.deliverer.PostMessage(params());
|
|
99
|
+
expect(h.reply().Message).toContain('Weekly digest');
|
|
100
|
+
expect(h.reply().Message).toContain('2 of 2 steps completed.');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('lists each step, not just an aggregate status', async () => {
|
|
104
|
+
// A graph where nine of ten steps succeeded is a materially different message from one that
|
|
105
|
+
// failed outright, and the roll-up alone cannot say which step went wrong.
|
|
106
|
+
const h = harness();
|
|
107
|
+
await h.deliverer.PostMessage(params({
|
|
108
|
+
Tasks: [
|
|
109
|
+
{ TaskID: 't1', Name: 'Gather', Status: 'Complete', Summary: '412 rows' },
|
|
110
|
+
{ TaskID: 't2', Name: 'Summarize', Status: 'Failed', ErrorMessage: 'model timed out' },
|
|
111
|
+
],
|
|
112
|
+
}));
|
|
113
|
+
const message = h.reply().Message;
|
|
114
|
+
expect(message).toContain('Gather');
|
|
115
|
+
expect(message).toContain('412 rows');
|
|
116
|
+
expect(message).toContain('Summarize');
|
|
117
|
+
expect(message).toContain('model timed out');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('truncates a long graph rather than burying the conversation', async () => {
|
|
121
|
+
const h = harness();
|
|
122
|
+
const many = Array.from({ length: 50 }, (_, i) => ({ TaskID: `t${i}`, Name: `Step ${i}`, Status: 'Complete' }));
|
|
123
|
+
await h.deliverer.PostMessage(params({ Tasks: many }));
|
|
124
|
+
|
|
125
|
+
const message = h.reply().Message;
|
|
126
|
+
expect(message).toContain('Step 0');
|
|
127
|
+
expect(message).not.toContain('Step 49');
|
|
128
|
+
// The count is stated rather than silently dropped — the task rows remain the full record.
|
|
129
|
+
expect(message).toContain('30 more');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('mints a fresh provider per delivery', async () => {
|
|
133
|
+
// Deliveries run outside any request and concurrently with task execution; a shared provider
|
|
134
|
+
// would share one transaction scope across unrelated work.
|
|
135
|
+
const h = harness();
|
|
136
|
+
await h.deliverer.PostMessage(params());
|
|
137
|
+
expect(h.providerFactory.CreateProvider).toHaveBeenCalledTimes(1);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('it never throws — the dispatcher marks delivery inside a CAS guard', () => {
|
|
142
|
+
it('does nothing for a headless graph, without treating it as an error', async () => {
|
|
143
|
+
// A graph submitted by a schedule, an entity-change trigger or an API call has no
|
|
144
|
+
// conversation to answer. Most workflows are in this shape.
|
|
145
|
+
const h = harness();
|
|
146
|
+
await expect(h.deliverer.PostMessage(params({ ConversationDetailID: null }))).resolves.toBeUndefined();
|
|
147
|
+
expect(h.providerFactory.CreateProvider).not.toHaveBeenCalled();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('survives a conversation detail that will not load', async () => {
|
|
151
|
+
const h = harness({ loads: false });
|
|
152
|
+
await expect(h.deliverer.PostMessage(params())).resolves.toBeUndefined();
|
|
153
|
+
expect(h.rows.length).toBe(1); // never got as far as building a reply
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('survives a failed save', async () => {
|
|
157
|
+
const h = harness({ saves: false });
|
|
158
|
+
await expect(h.deliverer.PostMessage(params())).resolves.toBeUndefined();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('survives a provider that throws outright', async () => {
|
|
162
|
+
const deliverer = new TaskGraphContinuationDeliverer(
|
|
163
|
+
{ CreateProvider: vi.fn().mockRejectedValue(new Error('pool exhausted')) } as never,
|
|
164
|
+
{ ID: 'user-1' } as UserInfo,
|
|
165
|
+
);
|
|
166
|
+
await expect(deliverer.PostMessage(params())).resolves.toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('Reinvoke — restarting the submitting agent', () => {
|
|
171
|
+
/** A provider that hands out loadable agent-run and agent rows, then the reply detail. */
|
|
172
|
+
function reinvokeHarness(over: { runLoads?: boolean; agentLoads?: boolean } = {}) {
|
|
173
|
+
const { runLoads = true, agentLoads = true } = over;
|
|
174
|
+
const rows: Array<Record<string, unknown>> = [];
|
|
175
|
+
let call = 0;
|
|
176
|
+
const provider = {
|
|
177
|
+
GetEntityObject: vi.fn().mockImplementation(async () => {
|
|
178
|
+
call++;
|
|
179
|
+
if (call === 1) {
|
|
180
|
+
const run = { AgentID: 'agent-1', Load: vi.fn().mockResolvedValue(runLoads) };
|
|
181
|
+
rows.push(run);
|
|
182
|
+
return run;
|
|
183
|
+
}
|
|
184
|
+
if (call === 2) {
|
|
185
|
+
const agent = { ID: 'agent-1', Load: vi.fn().mockResolvedValue(agentLoads) };
|
|
186
|
+
rows.push(agent);
|
|
187
|
+
return agent;
|
|
188
|
+
}
|
|
189
|
+
const detail = detailRow();
|
|
190
|
+
rows.push(detail as unknown as Record<string, unknown>);
|
|
191
|
+
return detail;
|
|
192
|
+
}),
|
|
193
|
+
};
|
|
194
|
+
const deliverer = new TaskGraphContinuationDeliverer(
|
|
195
|
+
{ CreateProvider: vi.fn().mockResolvedValue(provider) } as never,
|
|
196
|
+
{ ID: 'user-1' } as UserInfo,
|
|
197
|
+
);
|
|
198
|
+
return { deliverer, provider, rows };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
it('starts the submitting agent a fresh turn carrying the outcome', async () => {
|
|
202
|
+
const h = reinvokeHarness();
|
|
203
|
+
await h.deliverer.Reinvoke(params());
|
|
204
|
+
|
|
205
|
+
expect(runAgent).toHaveBeenCalledTimes(1);
|
|
206
|
+
const call = runAgent.mock.calls[0][0];
|
|
207
|
+
expect(call.agent.ID).toBe('agent-1');
|
|
208
|
+
expect(call.conversationMessages[0].content).toContain('Weekly digest');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('stamps depth + 1 — the value that makes MAX_REINVOKE_DEPTH real', async () => {
|
|
212
|
+
// Without this the next graph the restarted run submits begins the chain at zero again, and
|
|
213
|
+
// the cap can never fire. It is the whole reason ContinuationDepth exists as a column.
|
|
214
|
+
const h = reinvokeHarness();
|
|
215
|
+
await h.deliverer.Reinvoke(params({ ReinvokeDepth: 3 }));
|
|
216
|
+
expect(runAgent.mock.calls[0][0].continuationDepth).toBe(4);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('starts a chain at 1, not 0 — depth 0 is "not a continuation"', async () => {
|
|
220
|
+
const h = reinvokeHarness();
|
|
221
|
+
await h.deliverer.Reinvoke(params({ ReinvokeDepth: 0 }));
|
|
222
|
+
expect(runAgent.mock.calls[0][0].continuationDepth).toBe(1);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('falls back to posting when the graph records no submitting run', async () => {
|
|
226
|
+
// A schedule- or trigger-started graph has no turn to continue.
|
|
227
|
+
const h = reinvokeHarness();
|
|
228
|
+
await h.deliverer.Reinvoke(params({ SubmittedByAgentRunID: null }));
|
|
229
|
+
expect(runAgent).not.toHaveBeenCalled();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('falls back to posting when the submitting run cannot be loaded', async () => {
|
|
233
|
+
const h = reinvokeHarness({ runLoads: false });
|
|
234
|
+
await h.deliverer.Reinvoke(params());
|
|
235
|
+
expect(runAgent).not.toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('falls back to posting when the agent cannot be loaded', async () => {
|
|
239
|
+
const h = reinvokeHarness({ agentLoads: false });
|
|
240
|
+
await h.deliverer.Reinvoke(params());
|
|
241
|
+
expect(runAgent).not.toHaveBeenCalled();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('never throws — the dispatcher marks delivery inside a CAS guard', async () => {
|
|
245
|
+
runAgent.mockRejectedValueOnce(new Error('agent exploded'));
|
|
246
|
+
const h = reinvokeHarness();
|
|
247
|
+
await expect(h.deliverer.Reinvoke(params())).resolves.toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the task-graph live signal.
|
|
3
|
+
*
|
|
4
|
+
* `taskGraphFrameFilter` is the security-critical gate and is tested in isolation for the same
|
|
5
|
+
* reason `statusUpdatesFilter` is: a `parentTaskId` is a discoverable value, so without an identity
|
|
6
|
+
* check anyone holding one could watch another user's workflow — including its per-step error
|
|
7
|
+
* messages. It must fail **closed**.
|
|
8
|
+
*
|
|
9
|
+
* The broadcaster's contract is that it can never disturb the work it is describing: a frame is
|
|
10
|
+
* commentary on execution, never a step of it.
|
|
11
|
+
*/
|
|
12
|
+
// type-graphql's decorators evaluate at module load, and they need the metadata polyfill. Without
|
|
13
|
+
// this the file fails to COLLECT — which vitest reports as zero tests rather than as a failure.
|
|
14
|
+
import 'reflect-metadata';
|
|
15
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
16
|
+
|
|
17
|
+
vi.mock('@memberjunction/core', () => ({ LogError: vi.fn() }));
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
TASK_GRAPH_FRAMES_TOPIC,
|
|
21
|
+
TaskGraphFrameBroadcaster,
|
|
22
|
+
taskGraphFrameFilter,
|
|
23
|
+
type TaskGraphFramePayload,
|
|
24
|
+
} from '../resolvers/TaskGraphFrameResolver';
|
|
25
|
+
import type { TaskGraphFrame } from '@memberjunction/task-graph';
|
|
26
|
+
import type { PubSubEngine } from 'type-graphql';
|
|
27
|
+
|
|
28
|
+
const OWNER = '11111111-1111-1111-1111-111111111111';
|
|
29
|
+
const OTHER = '22222222-2222-2222-2222-222222222222';
|
|
30
|
+
const GRAPH = '33333333-3333-3333-3333-333333333333';
|
|
31
|
+
|
|
32
|
+
const payload = (over: Partial<TaskGraphFramePayload> = {}): TaskGraphFramePayload => ({
|
|
33
|
+
Kind: 'TaskCompleted',
|
|
34
|
+
ParentTaskID: GRAPH,
|
|
35
|
+
TaskID: 'task-1',
|
|
36
|
+
TaskName: 'Summarize',
|
|
37
|
+
Status: 'Complete',
|
|
38
|
+
ownerUserId: OWNER,
|
|
39
|
+
...over,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// `null` rather than `undefined` for "no identity": passing `undefined` explicitly triggers a
|
|
43
|
+
// default parameter, so the case would silently test the opposite of what it claims.
|
|
44
|
+
const context = (userId: string | null) => (userId ? { userPayload: { userRecord: { ID: userId } } } : {});
|
|
45
|
+
|
|
46
|
+
const check = (over: Partial<TaskGraphFramePayload> = {}, args = GRAPH, userId: string | null = OWNER) =>
|
|
47
|
+
taskGraphFrameFilter({ payload: payload(over), args: { parentTaskId: args }, context: context(userId) });
|
|
48
|
+
|
|
49
|
+
describe('taskGraphFrameFilter — the delivery gate', () => {
|
|
50
|
+
it('delivers a frame for the watched graph to its owner', () => {
|
|
51
|
+
expect(check()).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('matches graph IDs case-insensitively — UUID casing varies by source', () => {
|
|
55
|
+
expect(check({}, GRAPH.toUpperCase())).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does NOT deliver frames from another graph', () => {
|
|
59
|
+
expect(check({ ParentTaskID: '44444444-4444-4444-4444-444444444444' })).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('does NOT deliver another user\'s workflow to a subscriber holding its ID', () => {
|
|
63
|
+
// The whole reason identity is checked: parentTaskId is discoverable, and the frames carry
|
|
64
|
+
// per-step error messages.
|
|
65
|
+
expect(check({}, GRAPH, OTHER)).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('fails closed when the connection has no identity', () => {
|
|
69
|
+
expect(check({}, GRAPH, null)).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('fails closed when the frame has no owner', () => {
|
|
73
|
+
expect(check({ ownerUserId: '' as unknown as string })).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('fails closed on a missing graph ID on either side', () => {
|
|
77
|
+
expect(check({ ParentTaskID: '' })).toBe(false);
|
|
78
|
+
expect(check({}, '')).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('TaskGraphFrameBroadcaster', () => {
|
|
83
|
+
let published: Array<[string, unknown]>;
|
|
84
|
+
let pubSub: PubSubEngine;
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
published = [];
|
|
88
|
+
pubSub = {
|
|
89
|
+
publish: vi.fn().mockImplementation(async (topic: string, p: unknown) => { published.push([topic, p]); }),
|
|
90
|
+
} as unknown as PubSubEngine;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const frame = (over: Partial<TaskGraphFrame> = {}): TaskGraphFrame => ({
|
|
94
|
+
Kind: 'TaskStarted', ParentTaskID: GRAPH, OwnerUserID: OWNER, TaskID: 'task-1', TaskName: 'Gather', ...over,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('publishes the frame with the owner attached as the filter key', () => {
|
|
98
|
+
new TaskGraphFrameBroadcaster(pubSub).OnFrame(frame());
|
|
99
|
+
|
|
100
|
+
expect(published).toHaveLength(1);
|
|
101
|
+
const [topic, p] = published[0];
|
|
102
|
+
expect(topic).toBe(TASK_GRAPH_FRAMES_TOPIC);
|
|
103
|
+
expect((p as TaskGraphFramePayload).ownerUserId).toBe(OWNER);
|
|
104
|
+
expect((p as TaskGraphFramePayload).Kind).toBe('TaskStarted');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('drops a frame with no owner rather than publishing one the filter would refuse', () => {
|
|
108
|
+
// Reachable for graphs submitted before ownership was recorded on the parent.
|
|
109
|
+
new TaskGraphFrameBroadcaster(pubSub).OnFrame(frame({ OwnerUserID: null }));
|
|
110
|
+
expect(published).toHaveLength(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('never throws when publishing fails — a frame must not disturb the work it describes', () => {
|
|
114
|
+
const exploding = { publish: vi.fn(() => { throw new Error('pubsub down'); }) } as unknown as PubSubEngine;
|
|
115
|
+
expect(() => new TaskGraphFrameBroadcaster(exploding).OnFrame(frame())).not.toThrow();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('carries the whole frame through, so a viewer can render without re-reading rows', () => {
|
|
119
|
+
new TaskGraphFrameBroadcaster(pubSub).OnFrame(
|
|
120
|
+
frame({ Kind: 'TaskFailed', Status: 'Failed', ErrorMessage: 'model timed out' }),
|
|
121
|
+
);
|
|
122
|
+
const p = published[0][1] as TaskGraphFramePayload;
|
|
123
|
+
expect(p.Status).toBe('Failed');
|
|
124
|
+
expect(p.ErrorMessage).toBe('model timed out');
|
|
125
|
+
expect(p.TaskName).toBe('Gather');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('carries graph-level progress on a settled frame', () => {
|
|
129
|
+
new TaskGraphFrameBroadcaster(pubSub).OnFrame(
|
|
130
|
+
frame({ Kind: 'GraphSettled', TaskID: undefined, Status: 'Complete', CompletedCount: 7, TotalCount: 7 }),
|
|
131
|
+
);
|
|
132
|
+
const p = published[0][1] as TaskGraphFramePayload;
|
|
133
|
+
expect(p.CompletedCount).toBe(7);
|
|
134
|
+
expect(p.TotalCount).toBe(7);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -8,7 +8,7 @@ import { describe, it, expect, vi } from 'vitest';
|
|
|
8
8
|
vi.mock('../config.js', () => ({
|
|
9
9
|
configInfo: {},
|
|
10
10
|
}));
|
|
11
|
-
vi.mock('@memberjunction/
|
|
11
|
+
vi.mock('@memberjunction/generic-database-provider', () => ({
|
|
12
12
|
UserCache: {
|
|
13
13
|
Instance: { Users: [], UserByName: () => undefined, GetSystemUser: () => undefined },
|
|
14
14
|
Users: [],
|
|
@@ -100,6 +100,7 @@ vi.mock('@memberjunction/core', () => ({
|
|
|
100
100
|
// Inline the actual implementation here so the env-var-handling tests below
|
|
101
101
|
// exercise the genuine behavior (they're testing this function).
|
|
102
102
|
vi.mock('@memberjunction/generic-database-provider', () => ({
|
|
103
|
+
UserCache: { Instance: { Users: [], GetSystemUser: vi.fn() } },
|
|
103
104
|
resolveDbPlatformFromEnv: (envVarName: string = 'DB_PLATFORM'): 'sqlserver' | 'postgresql' | undefined => {
|
|
104
105
|
const raw = process.env[envVarName];
|
|
105
106
|
if (raw === undefined) return undefined;
|
|
@@ -117,7 +118,6 @@ vi.mock('@memberjunction/sqlserver-dataprovider', () => ({
|
|
|
117
118
|
SQLServerDataProvider: vi.fn(),
|
|
118
119
|
SQLServerProviderConfigData: vi.fn(),
|
|
119
120
|
setupSQLServerClient: vi.fn(),
|
|
120
|
-
UserCache: { Instance: { Users: [], GetSystemUser: vi.fn() } },
|
|
121
121
|
}));
|
|
122
122
|
|
|
123
123
|
vi.mock('@memberjunction/api-keys', () => ({
|
|
@@ -4,7 +4,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
4
4
|
// data-provider and communication stacks in at module load. Stub those out;
|
|
5
5
|
// this suite only exercises provisioning's console-log behavior, so
|
|
6
6
|
// @memberjunction/core (the logging implementation) stays real.
|
|
7
|
-
vi.mock('@memberjunction/
|
|
7
|
+
vi.mock('@memberjunction/generic-database-provider', () => ({
|
|
8
8
|
UserCache: { Instance: { Users: [] } },
|
|
9
9
|
}));
|
|
10
10
|
vi.mock('@memberjunction/communication-engine', () => ({
|
|
@@ -16,6 +16,9 @@ const { mockUserCacheUsers } = vi.hoisted(() => ({
|
|
|
16
16
|
// Stub external deps before imports
|
|
17
17
|
vi.mock('@memberjunction/sqlserver-dataprovider', () => ({
|
|
18
18
|
SQLServerDataProvider: class {},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock('@memberjunction/generic-database-provider', () => ({
|
|
19
22
|
UserCache: {
|
|
20
23
|
get Users() { return mockUserCacheUsers; },
|
|
21
24
|
},
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import type { IMetadataProvider } from '@memberjunction/core';
|
|
3
|
+
import { redactArg, ResetUnboundCrudInputWarnings } from '../logging/secretRedactor.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `redactArg` binds a GraphQL input type to an entity by NAME:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const match = /^(Create|Update|Delete)(?<name>.+)Input$/.exec(inputTypeName);
|
|
10
|
+
* const entity = provider.Entities.find((e) => e.ClassName === match.groups.name);
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Codegen inputs embed the entity ClassName (`CreateMJCredentialInput` → `MJCredential`)
|
|
14
|
+
* so the lookup succeeds. Hand-written resolver inputs do not (`CreateConnectionInput`
|
|
15
|
+
* → `Connection`, which is no entity), so the lookup fails, no encrypted-field names
|
|
16
|
+
* are contributed, and the arg falls through to `shortenForLog` with values intact.
|
|
17
|
+
*
|
|
18
|
+
* The boot audit tests only the NAME pattern, so it classifies these as metadata-bound
|
|
19
|
+
* and stays silent — the reason the gap went unnoticed. These tests pin both the
|
|
20
|
+
* fail-open behaviour and the warning that now surfaces it.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const SECRET = 'sk-live-CREDENTIAL-PAYLOAD';
|
|
24
|
+
|
|
25
|
+
/** Minimal provider exposing one entity with one encrypted column. */
|
|
26
|
+
function providerWith(entities: { ClassName: string; EncryptedFields: { Name: string }[] }[]): IMetadataProvider {
|
|
27
|
+
return { Entities: entities } as unknown as IMetadataProvider;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const CREDENTIAL_ENTITY = providerWith([
|
|
31
|
+
{ ClassName: 'MJCredential', EncryptedFields: [{ Name: 'Values' }] },
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
ResetUnboundCrudInputWarnings();
|
|
36
|
+
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
vi.restoreAllMocks();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('redactArg — entity-bound inputs (the codegen case)', () => {
|
|
44
|
+
it('redacts an encrypted column, keeping the rest', () => {
|
|
45
|
+
const out = redactArg({
|
|
46
|
+
inputTypeName: 'CreateMJCredentialInput',
|
|
47
|
+
rawValue: { Name: 'Stripe', Values: SECRET },
|
|
48
|
+
provider: CREDENTIAL_ENTITY,
|
|
49
|
+
noLogParameter: false,
|
|
50
|
+
noLogFields: new Set(),
|
|
51
|
+
}) as Record<string, unknown>;
|
|
52
|
+
|
|
53
|
+
expect(out.Values).toBe('<redacted>');
|
|
54
|
+
expect(out.Name).toBe('Stripe');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('emits no warning, because the binding resolved', () => {
|
|
58
|
+
redactArg({
|
|
59
|
+
inputTypeName: 'CreateMJCredentialInput',
|
|
60
|
+
rawValue: { Values: SECRET },
|
|
61
|
+
provider: CREDENTIAL_ENTITY,
|
|
62
|
+
noLogParameter: false,
|
|
63
|
+
noLogFields: new Set(),
|
|
64
|
+
});
|
|
65
|
+
expect(console.warn).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('redactArg — name matches the convention but no entity exists', () => {
|
|
70
|
+
const unbound = {
|
|
71
|
+
inputTypeName: 'CreateConnectionInput',
|
|
72
|
+
rawValue: { CredentialName: 'acme', CredentialValues: SECRET },
|
|
73
|
+
provider: CREDENTIAL_ENTITY,
|
|
74
|
+
noLogParameter: false,
|
|
75
|
+
noLogFields: new Set<string>(),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
it('falls open — values are logged, which is the defect being surfaced', () => {
|
|
79
|
+
const out = redactArg({ ...unbound });
|
|
80
|
+
// Documents current behaviour: no entity binding and no @NoLog means no
|
|
81
|
+
// redaction source, so the value survives into the log.
|
|
82
|
+
expect(JSON.stringify(out)).toContain(SECRET);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('now warns, naming the input type and what to do about it', () => {
|
|
86
|
+
redactArg({ ...unbound });
|
|
87
|
+
|
|
88
|
+
expect(console.warn).toHaveBeenCalledTimes(1);
|
|
89
|
+
const message = vi.mocked(console.warn).mock.calls[0][0] as string;
|
|
90
|
+
expect(message).toContain('CreateConnectionInput');
|
|
91
|
+
expect(message).toContain('@NoLog');
|
|
92
|
+
// The warning itself must not become a second leak.
|
|
93
|
+
expect(message).not.toContain(SECRET);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('warns only once per input type, however hot the mutation is', () => {
|
|
97
|
+
redactArg({ ...unbound });
|
|
98
|
+
redactArg({ ...unbound });
|
|
99
|
+
redactArg({ ...unbound });
|
|
100
|
+
expect(console.warn).toHaveBeenCalledTimes(1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('warns separately for a different unbound input type', () => {
|
|
104
|
+
redactArg({ ...unbound });
|
|
105
|
+
redactArg({ ...unbound, inputTypeName: 'CreateScheduleInput' });
|
|
106
|
+
expect(console.warn).toHaveBeenCalledTimes(2);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('redactArg — @NoLog closes the gap the metadata cannot', () => {
|
|
111
|
+
it('redacts a marked field on an input with no entity binding', () => {
|
|
112
|
+
const out = redactArg({
|
|
113
|
+
inputTypeName: 'CreateConnectionInput',
|
|
114
|
+
rawValue: { CredentialName: 'acme', CredentialValues: SECRET },
|
|
115
|
+
provider: CREDENTIAL_ENTITY,
|
|
116
|
+
noLogParameter: false,
|
|
117
|
+
noLogFields: new Set(['CredentialValues']),
|
|
118
|
+
}) as Record<string, unknown>;
|
|
119
|
+
|
|
120
|
+
expect(out.CredentialValues).toBe('<redacted>');
|
|
121
|
+
expect(out.CredentialName).toBe('acme');
|
|
122
|
+
expect(JSON.stringify(out)).not.toContain(SECRET);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('a parameter-level mark redacts the whole arg regardless of binding', () => {
|
|
126
|
+
const out = redactArg({
|
|
127
|
+
inputTypeName: 'CreateConnectionInput',
|
|
128
|
+
rawValue: { CredentialValues: SECRET },
|
|
129
|
+
provider: CREDENTIAL_ENTITY,
|
|
130
|
+
noLogParameter: true,
|
|
131
|
+
noLogFields: new Set(),
|
|
132
|
+
});
|
|
133
|
+
expect(out).toBe('<redacted>');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -4,7 +4,7 @@ import type { DatabaseProviderBase, UserInfo } from '@memberjunction/core';
|
|
|
4
4
|
// Controls what UserCache.Instance.GetSystemUser() serves — undefined simulates an unpopulated
|
|
5
5
|
// cache (no system user available), the fail-closed path.
|
|
6
6
|
const getSystemUserMock = vi.fn<[], UserInfo | undefined>();
|
|
7
|
-
vi.mock('@memberjunction/
|
|
7
|
+
vi.mock('@memberjunction/generic-database-provider', () => ({
|
|
8
8
|
UserCache: {
|
|
9
9
|
get Instance() {
|
|
10
10
|
return { GetSystemUser: getSystemUserMock };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RegisterClass } from '@memberjunction/global';
|
|
2
2
|
import { Metadata, RunView, LogError, EntitySaveOptions } from '@memberjunction/core';
|
|
3
3
|
import { NewUserBase } from './newUsers.js';
|
|
4
|
-
import { UserCache } from '@memberjunction/
|
|
4
|
+
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
5
5
|
import { configInfo } from '../config.js';
|
|
6
6
|
import { MJUserEntity } from '@memberjunction/core-entities';
|
|
7
7
|
|
package/src/auth/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { JwtHeader, SigningKeyCallback, JwtPayload } from 'jsonwebtoken';
|
|
2
2
|
import { configInfo } from '../config.js';
|
|
3
|
-
import { UserCache } from '@memberjunction/
|
|
3
|
+
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
4
4
|
import sql from 'mssql';
|
|
5
|
-
import { Metadata, RoleInfo, UserInfo } from '@memberjunction/core';
|
|
5
|
+
import { DatabaseProviderBase, Metadata, RoleInfo, UserInfo } from '@memberjunction/core';
|
|
6
6
|
import { NewUserBase } from './newUsers.js';
|
|
7
7
|
import { MJGlobal } from '@memberjunction/global';
|
|
8
8
|
import { MJUserEntity, MJUserEntityType } from '@memberjunction/core-entities';
|
|
@@ -21,9 +21,13 @@ class MissingAuthError extends Error {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const refreshUserCache = async (
|
|
24
|
+
const refreshUserCache = async () => {
|
|
25
25
|
const startTime: number = Date.now();
|
|
26
|
-
|
|
26
|
+
// UserCache is process-global in this phase, and so is the provider MJServer configures at
|
|
27
|
+
// bootstrap — the callers below only hold a ConnectionPool, which they use purely as a
|
|
28
|
+
// "the database is reachable" gate before asking for a refresh.
|
|
29
|
+
const provider = Metadata.Provider as unknown as DatabaseProviderBase; // global-provider-ok: server auth path; runs under the server's single default provider
|
|
30
|
+
await UserCache.Instance.Refresh(provider);
|
|
27
31
|
const endTime: number = Date.now();
|
|
28
32
|
const elapsed: number = endTime - startTime;
|
|
29
33
|
|
|
@@ -179,7 +183,7 @@ export const getSystemUser = async (dataSource?: sql.ConnectionPool, attemptCach
|
|
|
179
183
|
if (dataSource && attemptCacheUpdateIfNeeded) {
|
|
180
184
|
console.warn(`System user not found in cache. Updating cache in attempt to find the user...`);
|
|
181
185
|
|
|
182
|
-
await refreshUserCache(
|
|
186
|
+
await refreshUserCache();
|
|
183
187
|
return getSystemUser(dataSource, false); // try one more time but do not update cache next time if not found
|
|
184
188
|
}
|
|
185
189
|
throw new Error(`System user ID '${UserCache.Instance.SYSTEM_USER_ID}' not found in database`);
|
|
@@ -260,7 +264,7 @@ export const verifyUserRecord = async (
|
|
|
260
264
|
// if we get here that means in the above, if we were attempting to create a new user, it did not work, or it wasn't attempted and we have a config that asks us to auto update the cache
|
|
261
265
|
console.warn(`User ${email} not found in cache. Updating cache in attempt to find the user...`);
|
|
262
266
|
|
|
263
|
-
await refreshUserCache(
|
|
267
|
+
await refreshUserCache();
|
|
264
268
|
|
|
265
269
|
return verifyUserRecord(email, firstName, lastName, requestDomain, dataSource, false); // try one more time but do not update cache next time if not found
|
|
266
270
|
}
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
type MJUserEntityType,
|
|
30
30
|
type MJApplicationRoleEntity,
|
|
31
31
|
} from '@memberjunction/core-entities';
|
|
32
|
-
import { UserCache } from '@memberjunction/
|
|
32
|
+
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
33
33
|
import { CommunicationEngine } from '@memberjunction/communication-engine';
|
|
34
34
|
import { Message } from '@memberjunction/communication-types';
|
|
35
35
|
import { configInfo, type MagicLinkConfig } from '../../config.js';
|
package/src/auth/newUsers.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ApplicationInfo, DatabaseProviderBase, EntitySaveOptions, LogError, LogStatus, Metadata, RunView, RunViewResult, UserInfo } from "@memberjunction/core";
|
|
2
2
|
import { RegisterClass } from "@memberjunction/global";
|
|
3
|
-
import { UserCache } from "@memberjunction/
|
|
3
|
+
import { UserCache } from "@memberjunction/generic-database-provider";
|
|
4
4
|
import { configInfo } from "../config.js";
|
|
5
5
|
import { MJUserEntity, MJUserRoleEntity, MJUserApplicationEntity, MJUserApplicationEntityEntity, MJApplicationEntityType, MJApplicationEntityEntityType, UserInfoEngine } from "@memberjunction/core-entities";
|
|
6
6
|
|
package/src/context.ts
CHANGED
|
@@ -19,10 +19,10 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
19
19
|
import e from 'express';
|
|
20
20
|
import type { RequestHandler, Request, Response, NextFunction } from 'express';
|
|
21
21
|
import { DatabaseProviderBase, UserInfo, type MagicLinkScope, type ReturningVisitorContext, type WidgetGuestContext } from '@memberjunction/core';
|
|
22
|
-
import { SQLServerDataProvider, SQLServerProviderConfigData
|
|
22
|
+
import { SQLServerDataProvider, SQLServerProviderConfigData } from '@memberjunction/sqlserver-dataprovider';
|
|
23
23
|
import { Metadata } from '@memberjunction/core';
|
|
24
24
|
import { UUIDsEqual } from '@memberjunction/global';
|
|
25
|
-
import { resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provider';
|
|
25
|
+
import { UserCache, resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provider';
|
|
26
26
|
import { GetAPIKeyEngine } from '@memberjunction/api-keys';
|
|
27
27
|
|
|
28
28
|
// ── Session / login audit (Phase 3) ───────────────────────────────────────────
|