@memberjunction/server 5.51.0 → 6.1.0-edge.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/dist/agentSessions/SessionManager.d.ts +22 -1
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +13 -3
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/auth/actingContextResolver.d.ts +44 -0
- package/dist/auth/actingContextResolver.d.ts.map +1 -0
- package/dist/auth/actingContextResolver.js +18 -0
- package/dist/auth/actingContextResolver.js.map +1 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/sessionUserClone.d.ts +32 -0
- package/dist/auth/sessionUserClone.d.ts.map +1 -0
- package/dist/auth/sessionUserClone.js +50 -0
- package/dist/auth/sessionUserClone.js.map +1 -0
- package/dist/context.d.ts +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +43 -6
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +71 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +383 -3
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts +5 -0
- package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.js +5 -4
- package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
- package/dist/generic/PushStatusResolver.d.ts +58 -0
- package/dist/generic/PushStatusResolver.d.ts.map +1 -1
- package/dist/generic/PushStatusResolver.js +42 -3
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +9 -0
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +31 -13
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/multiTenancy/index.d.ts +19 -0
- package/dist/multiTenancy/index.d.ts.map +1 -1
- package/dist/multiTenancy/index.js +82 -5
- package/dist/multiTenancy/index.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +12 -5
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +13 -16
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +15 -14
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +19 -26
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/ReportResolver.d.ts.map +1 -1
- package/dist/resolvers/ReportResolver.js +11 -3
- package/dist/resolvers/ReportResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +30 -40
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTestResolver.js +90 -112
- package/dist/resolvers/RunTestResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +3 -3
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.js +2 -2
- package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
- package/dist/resolvers/VersionHistoryResolver.d.ts.map +1 -1
- package/dist/resolvers/VersionHistoryResolver.js +8 -12
- package/dist/resolvers/VersionHistoryResolver.js.map +1 -1
- package/dist/services/TaskOrchestrator.d.ts.map +1 -1
- package/dist/services/TaskOrchestrator.js +17 -15
- package/dist/services/TaskOrchestrator.js.map +1 -1
- package/package.json +100 -100
- package/src/__tests__/RealtimeBridgeResolver.test.ts +6 -0
- package/src/__tests__/SessionManager.test.ts +44 -0
- package/src/__tests__/multiTenancy.security.test.ts +254 -22
- package/src/__tests__/multiTenancy.test.ts +22 -0
- package/src/__tests__/pushStatusResolver.filter.test.ts +56 -0
- package/src/__tests__/resolverBase.rls.test.ts +4 -0
- package/src/__tests__/search-knowledge-tags.test.ts +74 -6
- package/src/__tests__/sessionUserClone.test.ts +161 -0
- package/src/agentSessions/SessionManager.ts +26 -2
- package/src/auth/actingContextResolver.ts +56 -0
- package/src/auth/index.ts +1 -0
- package/src/auth/sessionUserClone.ts +50 -0
- package/src/context.ts +48 -6
- package/src/generated/generated.ts +276 -3
- package/src/generic/FireAndForgetHeartbeat.ts +10 -4
- package/src/generic/PushStatusResolver.ts +79 -4
- package/src/generic/ResolverBase.ts +33 -13
- package/src/index.ts +4 -2
- package/src/multiTenancy/index.ts +92 -5
- package/src/resolvers/ActionResolver.ts +15 -7
- package/src/resolvers/MCPResolver.ts +13 -15
- package/src/resolvers/RealtimeClientSessionResolver.ts +15 -13
- package/src/resolvers/RemoteBrowserActionResolver.ts +19 -25
- package/src/resolvers/ReportResolver.ts +11 -3
- package/src/resolvers/RunAIAgentResolver.ts +30 -40
- package/src/resolvers/RunTestResolver.ts +90 -112
- package/src/resolvers/SearchKnowledgeResolver.ts +3 -3
- package/src/resolvers/SearchKnowledgeStreamResolver.ts +2 -2
- package/src/resolvers/VersionHistoryResolver.ts +10 -13
- package/src/resolvers/__tests__/ReportResolver.test.ts +279 -0
- package/src/services/TaskOrchestrator.ts +17 -17
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ReportResolverExtended.CreateReportFromConversationDetailID.
|
|
3
|
+
*
|
|
4
|
+
* Regression coverage for a SQL injection fix: ConversationDetailID (a plain GraphQL
|
|
5
|
+
* String arg) used to be interpolated directly into the WHERE clause. It is now bound
|
|
6
|
+
* via mssql's parameterized `request.input(...)`, so a hostile value must never appear
|
|
7
|
+
* spliced into the executed SQL text, and query structure must stay identical regardless
|
|
8
|
+
* of what the value contains.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
11
|
+
|
|
12
|
+
// ─── Hoisted mocks ────────────────────────────────────────────────────────
|
|
13
|
+
const { mockUserCacheUsers, mssqlState } = vi.hoisted(() => ({
|
|
14
|
+
mockUserCacheUsers: [] as Array<{ Email: string; ID: string }>,
|
|
15
|
+
mssqlState: {
|
|
16
|
+
inputCalls: [] as Array<{ name: string; type: unknown; value: unknown }>,
|
|
17
|
+
queryCalls: [] as string[],
|
|
18
|
+
poolArgs: [] as unknown[],
|
|
19
|
+
recordset: [] as Array<Record<string, unknown>>,
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Stub external deps before imports (mirrors resolverBase.rls.test.ts)
|
|
24
|
+
vi.mock('@memberjunction/sqlserver-dataprovider', () => ({
|
|
25
|
+
SQLServerDataProvider: class {},
|
|
26
|
+
UserCache: {
|
|
27
|
+
get Users() { return mockUserCacheUsers; },
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock('cloudevents', () => ({
|
|
32
|
+
CloudEvent: class {},
|
|
33
|
+
httpTransport: () => () => undefined,
|
|
34
|
+
emitterFor: () => () => undefined,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
vi.mock('type-graphql', () => ({
|
|
38
|
+
Resolver: () => () => undefined,
|
|
39
|
+
Mutation: () => () => undefined,
|
|
40
|
+
Query: () => () => undefined,
|
|
41
|
+
Subscription: () => () => undefined,
|
|
42
|
+
Ctx: () => () => undefined,
|
|
43
|
+
Arg: () => () => undefined,
|
|
44
|
+
PubSub: () => () => undefined,
|
|
45
|
+
Root: () => () => undefined,
|
|
46
|
+
ObjectType: () => () => undefined,
|
|
47
|
+
InputType: () => () => undefined,
|
|
48
|
+
Field: () => () => undefined,
|
|
49
|
+
FieldResolver: () => () => undefined,
|
|
50
|
+
Int: () => undefined,
|
|
51
|
+
Float: () => undefined,
|
|
52
|
+
registerEnumType: () => undefined,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
vi.mock('graphql', () => ({
|
|
56
|
+
GraphQLError: class extends Error {
|
|
57
|
+
constructor(msg: string) { super(msg); }
|
|
58
|
+
},
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
vi.mock('mssql', () => {
|
|
62
|
+
const UniqueIdentifier = { __marker: 'UniqueIdentifier' };
|
|
63
|
+
class Request {
|
|
64
|
+
constructor(pool: unknown) {
|
|
65
|
+
mssqlState.poolArgs.push(pool);
|
|
66
|
+
}
|
|
67
|
+
input(name: string, type: unknown, value: unknown) {
|
|
68
|
+
mssqlState.inputCalls.push({ name, type, value });
|
|
69
|
+
}
|
|
70
|
+
async query(sql: string) {
|
|
71
|
+
mssqlState.queryCalls.push(sql);
|
|
72
|
+
return { recordset: mssqlState.recordset };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { default: { Request, UniqueIdentifier }, Request, UniqueIdentifier };
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
vi.mock('@memberjunction/data-context', () => {
|
|
79
|
+
class DataContext {
|
|
80
|
+
LoadMetadata = vi.fn(async () => true);
|
|
81
|
+
}
|
|
82
|
+
(DataContext as unknown as { Clone: unknown }).Clone = vi.fn(async () => ({ ID: 'dctx-clone-1' }));
|
|
83
|
+
return { DataContext };
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
vi.mock('@memberjunction/api-keys', () => ({
|
|
87
|
+
GetAPIKeyEngine: vi.fn(),
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
vi.mock('@memberjunction/encryption', () => ({
|
|
91
|
+
EncryptionEngine: { Instance: {} },
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
vi.mock('@memberjunction/graphql-dataprovider', () => ({
|
|
95
|
+
FieldMapper: class { static Instance = { MapFieldsFromCodeNamesToDBNames: vi.fn() }; },
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
vi.mock('../../generic/PubSubManager.js', () => ({
|
|
99
|
+
PubSubManager: class { static Instance = { publish: vi.fn() }; },
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
vi.mock('../../generic/PushStatusResolver.js', () => ({
|
|
103
|
+
PUSH_STATUS_UPDATES_TOPIC: 'test-push-topic',
|
|
104
|
+
PushStatusNotification: class {},
|
|
105
|
+
PushStatusResolver: class {},
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
vi.mock('../../generic/CacheInvalidationResolver.js', () => ({
|
|
109
|
+
CACHE_INVALIDATION_TOPIC: 'test-cache-topic',
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
vi.mock('../../generic/RunViewResolver.js', () => ({
|
|
113
|
+
RunViewByIDInput: class {},
|
|
114
|
+
RunViewByNameInput: class {},
|
|
115
|
+
RunDynamicViewInput: class {},
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
vi.mock('../../generic/DeleteOptionsInput.js', () => ({
|
|
119
|
+
DeleteOptionsInput: class {},
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
vi.mock('../../types.js', () => ({
|
|
123
|
+
RunViewGenericParams: class {},
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
vi.mock('@memberjunction/core', async () => {
|
|
127
|
+
const actual = await vi.importActual<typeof import('@memberjunction/core')>('@memberjunction/core');
|
|
128
|
+
return {
|
|
129
|
+
...actual,
|
|
130
|
+
LogError: vi.fn(),
|
|
131
|
+
LogStatus: vi.fn(),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
vi.mock('@memberjunction/core-entities', () => ({}));
|
|
136
|
+
|
|
137
|
+
// ─── Import after mocks ──────────────────────────────────────────────────
|
|
138
|
+
import { ReportResolverExtended } from '../ReportResolver';
|
|
139
|
+
|
|
140
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
function makeReportEntity() {
|
|
143
|
+
return {
|
|
144
|
+
ID: 'report-1',
|
|
145
|
+
Name: '',
|
|
146
|
+
Description: '',
|
|
147
|
+
ConversationID: '',
|
|
148
|
+
ConversationDetailID: '',
|
|
149
|
+
DataContextID: '',
|
|
150
|
+
Configuration: '',
|
|
151
|
+
SharingScope: '',
|
|
152
|
+
UserID: '',
|
|
153
|
+
NewRecord: vi.fn(),
|
|
154
|
+
Save: vi.fn(async () => true),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function makeContext(
|
|
159
|
+
getEntityObject: () => ReturnType<typeof makeReportEntity>,
|
|
160
|
+
userPayload: Record<string, unknown> = { email: 'test@example.com' } // no apiKeyHash -> scope check no-ops
|
|
161
|
+
) {
|
|
162
|
+
const getEntityObjectSpy = vi.fn(async (_name: string, _contextUser: unknown) => getEntityObject());
|
|
163
|
+
const md = {
|
|
164
|
+
Entities: [
|
|
165
|
+
{ Name: 'MJ: Conversation Details', SchemaName: 'dbo', BaseView: 'vwConversationDetails' },
|
|
166
|
+
{ Name: 'MJ: Conversations', SchemaName: 'dbo', BaseView: 'vwConversations' },
|
|
167
|
+
],
|
|
168
|
+
GetEntityObject: getEntityObjectSpy,
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
context: {
|
|
172
|
+
dataSource: { __fakePool: true },
|
|
173
|
+
userPayload,
|
|
174
|
+
providers: [{ type: 'Read-Write', provider: md }],
|
|
175
|
+
} as unknown as Parameters<ReportResolverExtended['CreateReportFromConversationDetailID']>[1],
|
|
176
|
+
getEntityObjectSpy,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
describe('ReportResolverExtended.CreateReportFromConversationDetailID', () => {
|
|
181
|
+
let resolver: ReportResolverExtended;
|
|
182
|
+
|
|
183
|
+
beforeEach(() => {
|
|
184
|
+
resolver = new ReportResolverExtended();
|
|
185
|
+
mssqlState.inputCalls.length = 0;
|
|
186
|
+
mssqlState.queryCalls.length = 0;
|
|
187
|
+
mssqlState.poolArgs.length = 0;
|
|
188
|
+
mssqlState.recordset.length = 0;
|
|
189
|
+
mssqlState.recordset.push({
|
|
190
|
+
Message: JSON.stringify({ title: 'Test Report', userExplanation: 'exp' }),
|
|
191
|
+
ConversationID: 'conv-1',
|
|
192
|
+
DataContextID: 'dctx-1',
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
mockUserCacheUsers.length = 0;
|
|
196
|
+
mockUserCacheUsers.push({ Email: 'test@example.com', ID: 'user-1' });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('binds ConversationDetailID as a query parameter rather than splicing it into the SQL text', async () => {
|
|
200
|
+
const maliciousID = "1'; DROP TABLE MJ_Reports; --";
|
|
201
|
+
const { context } = makeContext(makeReportEntity);
|
|
202
|
+
|
|
203
|
+
const result = await resolver.CreateReportFromConversationDetailID(maliciousID, context);
|
|
204
|
+
|
|
205
|
+
expect(result.Success).toBe(true);
|
|
206
|
+
|
|
207
|
+
// The query text must use a bound parameter, never the raw value.
|
|
208
|
+
expect(mssqlState.queryCalls).toHaveLength(1);
|
|
209
|
+
expect(mssqlState.queryCalls[0]).toContain('@ConversationDetailID');
|
|
210
|
+
expect(mssqlState.queryCalls[0]).not.toContain(maliciousID);
|
|
211
|
+
expect(mssqlState.queryCalls[0]).not.toContain('DROP TABLE');
|
|
212
|
+
|
|
213
|
+
// The value must be bound through request.input, not string concatenation.
|
|
214
|
+
expect(mssqlState.inputCalls).toHaveLength(1);
|
|
215
|
+
expect(mssqlState.inputCalls[0].name).toBe('ConversationDetailID');
|
|
216
|
+
expect(mssqlState.inputCalls[0].value).toBe(maliciousID);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('does not alter query structure for values containing quotes, --, or OR 1=1', async () => {
|
|
220
|
+
const hostileID = "abc' OR '1'='1' --";
|
|
221
|
+
const { context } = makeContext(makeReportEntity);
|
|
222
|
+
|
|
223
|
+
await resolver.CreateReportFromConversationDetailID(hostileID, context);
|
|
224
|
+
|
|
225
|
+
expect(mssqlState.queryCalls[0]).toMatch(/WHERE\s+cd\.ID=@ConversationDetailID\s*$/);
|
|
226
|
+
expect(mssqlState.inputCalls[0].value).toBe(hostileID);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('still succeeds end to end for a normal GUID-shaped value', async () => {
|
|
230
|
+
const normalID = '12345678-1234-1234-1234-123456789012';
|
|
231
|
+
const { context } = makeContext(makeReportEntity);
|
|
232
|
+
|
|
233
|
+
const result = await resolver.CreateReportFromConversationDetailID(normalID, context);
|
|
234
|
+
|
|
235
|
+
expect(result.Success).toBe(true);
|
|
236
|
+
expect(result.ReportName).toBe('Test Report');
|
|
237
|
+
expect(mssqlState.inputCalls[0].value).toBe(normalID);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// §5.6 row 11 — the operating user must come from the stamped payload user
|
|
241
|
+
// (GetUserFromPayload), never a fresh UserCache lookup by email. A fresh lookup would
|
|
242
|
+
// silently drop any per-request API-key row-filter binding (APIKeyRowFilters /
|
|
243
|
+
// APIKeyActingContext) already stamped onto userPayload.userRecord, so a filtered key's
|
|
244
|
+
// report-creation work would run unfiltered.
|
|
245
|
+
describe('operating user resolution (§5.6 row 11)', () => {
|
|
246
|
+
it('uses userPayload.userRecord (with its stamped bindings) when present, not a fresh UserCache lookup', async () => {
|
|
247
|
+
const stampedUser = {
|
|
248
|
+
ID: 'user-1',
|
|
249
|
+
Email: 'test@example.com',
|
|
250
|
+
APIKeyRowFilters: [{ EntityID: 'e1', PermissionType: 'Create', FilterID: 'f1' }],
|
|
251
|
+
};
|
|
252
|
+
// A DIFFERENT UserCache entry for the same email — if the resolver fell back to a
|
|
253
|
+
// UserCache lookup, it would get THIS unstamped object instead.
|
|
254
|
+
mockUserCacheUsers.length = 0;
|
|
255
|
+
mockUserCacheUsers.push({ Email: 'test@example.com', ID: 'user-1' });
|
|
256
|
+
|
|
257
|
+
const { context, getEntityObjectSpy } = makeContext(makeReportEntity, {
|
|
258
|
+
email: 'test@example.com',
|
|
259
|
+
userRecord: stampedUser,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const result = await resolver.CreateReportFromConversationDetailID('12345678-1234-1234-1234-123456789012', context);
|
|
263
|
+
|
|
264
|
+
expect(result.Success).toBe(true);
|
|
265
|
+
// GetEntityObject's contextUser must be the SAME stamped object reference — proving
|
|
266
|
+
// the row-filter binding flows through to the entity work, not a fresh, unstamped one.
|
|
267
|
+
expect(getEntityObjectSpy.mock.calls[0][1]).toBe(stampedUser);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('falls back to a UserCache lookup by email when no payload user is present', async () => {
|
|
271
|
+
const { context, getEntityObjectSpy } = makeContext(makeReportEntity, { email: 'test@example.com' });
|
|
272
|
+
|
|
273
|
+
const result = await resolver.CreateReportFromConversationDetailID('12345678-1234-1234-1234-123456789012', context);
|
|
274
|
+
|
|
275
|
+
expect(result.Success).toBe(true);
|
|
276
|
+
expect((getEntityObjectSpy.mock.calls[0][1] as { ID: string }).ID).toBe('user-1');
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -4,7 +4,7 @@ import { AgentRunner } from '@memberjunction/ai-agents';
|
|
|
4
4
|
import { ChatMessageRole } from '@memberjunction/ai';
|
|
5
5
|
import { PubSubEngine } from 'type-graphql';
|
|
6
6
|
import { UserPayload } from '../types.js';
|
|
7
|
-
import {
|
|
7
|
+
import { publishStatusUpdate } from '../generic/PushStatusResolver.js';
|
|
8
8
|
import { MJAIAgentEntityExtended } from '@memberjunction/ai-core-plus';
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -226,7 +226,10 @@ export class TaskOrchestrator {
|
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
LogStatus(`📡 Publishing task progress: ${taskName} - ${message} (${percentComplete}%) to session ${this.userPayload.sessionId}`);
|
|
230
|
+
publishStatusUpdate(this.pubSub, {
|
|
231
|
+
sessionId: this.userPayload.sessionId,
|
|
232
|
+
ownerUserId: this.userPayload.userRecord.ID,
|
|
230
233
|
message: JSON.stringify({
|
|
231
234
|
resolver: 'TaskOrchestrator',
|
|
232
235
|
type: 'TaskProgress',
|
|
@@ -238,12 +241,8 @@ export class TaskOrchestrator {
|
|
|
238
241
|
timestamp: new Date(),
|
|
239
242
|
conversationDetailId: this.conversationDetailId
|
|
240
243
|
}
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
LogStatus(`📡 Publishing task progress: ${taskName} - ${message} (${percentComplete}%) to session ${this.userPayload.sessionId}`);
|
|
246
|
-
this.pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, payload);
|
|
244
|
+
})
|
|
245
|
+
});
|
|
247
246
|
|
|
248
247
|
LogStatus(`[Task: ${taskName}] ${message} (${percentComplete}%)`);
|
|
249
248
|
}
|
|
@@ -257,7 +256,10 @@ export class TaskOrchestrator {
|
|
|
257
256
|
return;
|
|
258
257
|
}
|
|
259
258
|
|
|
260
|
-
|
|
259
|
+
LogStatus(`📡 Publishing agent progress: ${taskName} → ${agentStep} to session ${this.userPayload.sessionId}`);
|
|
260
|
+
publishStatusUpdate(this.pubSub, {
|
|
261
|
+
sessionId: this.userPayload.sessionId,
|
|
262
|
+
ownerUserId: this.userPayload.userRecord.ID,
|
|
261
263
|
message: JSON.stringify({
|
|
262
264
|
resolver: 'TaskOrchestrator',
|
|
263
265
|
type: 'AgentProgress',
|
|
@@ -269,12 +271,8 @@ export class TaskOrchestrator {
|
|
|
269
271
|
timestamp: new Date(),
|
|
270
272
|
conversationDetailId: this.conversationDetailId
|
|
271
273
|
}
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
LogStatus(`📡 Publishing agent progress: ${taskName} → ${agentStep} to session ${this.userPayload.sessionId}`);
|
|
277
|
-
this.pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, payload);
|
|
274
|
+
})
|
|
275
|
+
});
|
|
278
276
|
|
|
279
277
|
LogStatus(`[Task: ${taskName}] → ${agentStep}: ${agentMessage}`);
|
|
280
278
|
}
|
|
@@ -851,8 +849,10 @@ export class TaskOrchestrator {
|
|
|
851
849
|
|
|
852
850
|
// Publish real-time event if pubSub available
|
|
853
851
|
if (this.pubSub && this.userPayload) {
|
|
854
|
-
|
|
855
|
-
|
|
852
|
+
// NOTE (B49): normalized from a malformed no-sessionId payload
|
|
853
|
+
publishStatusUpdate(this.pubSub, {
|
|
854
|
+
sessionId: this.userPayload.sessionId,
|
|
855
|
+
ownerUserId: this.userPayload.userRecord.ID,
|
|
856
856
|
message: JSON.stringify({
|
|
857
857
|
type: 'notification',
|
|
858
858
|
notificationId: notification.ID,
|