@memberjunction/core-entities-server 6.2.0-edge.7 → 6.2.0-edge.8

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 (32) hide show
  1. package/dist/custom/AnchoredConversationReadSecurityHook.d.ts +22 -0
  2. package/dist/custom/AnchoredConversationReadSecurityHook.d.ts.map +1 -0
  3. package/dist/custom/AnchoredConversationReadSecurityHook.js +293 -0
  4. package/dist/custom/AnchoredConversationReadSecurityHook.js.map +1 -0
  5. package/dist/custom/MJConversationDetailEntityServer.server.d.ts +20 -30
  6. package/dist/custom/MJConversationDetailEntityServer.server.d.ts.map +1 -1
  7. package/dist/custom/MJConversationDetailEntityServer.server.js +89 -35
  8. package/dist/custom/MJConversationDetailEntityServer.server.js.map +1 -1
  9. package/dist/custom/MJConversationEntityServer.server.d.ts +19 -4
  10. package/dist/custom/MJConversationEntityServer.server.d.ts.map +1 -1
  11. package/dist/custom/MJConversationEntityServer.server.js +169 -11
  12. package/dist/custom/MJConversationEntityServer.server.js.map +1 -1
  13. package/dist/custom/MJProjectEntityServer.server.d.ts +21 -0
  14. package/dist/custom/MJProjectEntityServer.server.d.ts.map +1 -0
  15. package/dist/custom/MJProjectEntityServer.server.js +106 -0
  16. package/dist/custom/MJProjectEntityServer.server.js.map +1 -0
  17. package/dist/custom/MJQueryEntityServer.server.d.ts.map +1 -1
  18. package/dist/custom/MJQueryEntityServer.server.js +14 -2
  19. package/dist/custom/MJQueryEntityServer.server.js.map +1 -1
  20. package/dist/custom/RestrictedFieldCommentClassifier.d.ts +73 -0
  21. package/dist/custom/RestrictedFieldCommentClassifier.d.ts.map +1 -0
  22. package/dist/custom/RestrictedFieldCommentClassifier.js +240 -0
  23. package/dist/custom/RestrictedFieldCommentClassifier.js.map +1 -0
  24. package/dist/engines/QueryEngineServer.d.ts +4 -4
  25. package/dist/engines/QueryEngineServer.d.ts.map +1 -1
  26. package/dist/engines/QueryEngineServer.js +6 -16
  27. package/dist/engines/QueryEngineServer.js.map +1 -1
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +3 -0
  31. package/dist/index.js.map +1 -1
  32. package/package.json +30 -30
@@ -0,0 +1,22 @@
1
+ import { PostRunViewHook } from '@memberjunction/core';
2
+ /**
3
+ * Server-side `PostRunView` data hook enforcing Row-Level Security (RLS) and Field-Level Security (FLS)
4
+ * on anchored conversations (`MJ: Conversations` where `AnchorType IS NOT NULL`) and their comment
5
+ * messages (`MJ: Conversation Details`).
6
+ *
7
+ * Guarantees that even a raw `RunView` call over GraphQL, REST, or server code cannot return:
8
+ * - Anchored conversation threads (`MJ: Conversations`) attached to a linked record that `contextUser`
9
+ * cannot read (entity permissions + RLS), or anchored/stamped with restricted fields (`AnchorKey` /
10
+ * `AnchorMetadata.restrictedFields`) that `contextUser` is denied Read access to (FLS).
11
+ * - Comment messages (`MJ: Conversation Details`) belonging to any such anchored conversation thread.
12
+ *
13
+ * Never mutates `results.Results` in place (safe with frozen cache entries); returns a replacement
14
+ * `RunViewResult` whenever rows are filtered out.
15
+ */
16
+ export declare const AnchoredConversationPostRunViewHook: PostRunViewHook;
17
+ /**
18
+ * Ensures {@link AnchoredConversationPostRunViewHook} is registered in the global `PostRunView`
19
+ * data hooks store. Safe to call multiple times (idempotent).
20
+ */
21
+ export declare function EnsureAnchoredConversationReadHookRegistered(): void;
22
+ //# sourceMappingURL=AnchoredConversationReadSecurityHook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnchoredConversationReadSecurityHook.d.ts","sourceRoot":"","sources":["../../src/custom/AnchoredConversationReadSecurityHook.ts"],"names":[],"mappings":"AAAA,OAAO,EAMH,eAAe,EAOlB,MAAM,sBAAsB,CAAC;AAmE9B;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mCAAmC,EAAE,eA0CjD,CAAC;AA4SF;;;GAGG;AACH,wBAAgB,4CAA4C,IAAI,IAAI,CAKnE"}
@@ -0,0 +1,293 @@
1
+ import { BaseEntity, GetDataHooks, LogError, Metadata, RegisterDataHook, RunView, WellKnownUserSource } from '@memberjunction/core';
2
+ import { EscapeSQLString, NormalizeUUID } from '@memberjunction/global';
3
+ import { FilterAnchoredConversationsForUser } from '@memberjunction/core-entities';
4
+ const INTERNAL_SECURITY_QUERY_PARAMS = new WeakSet();
5
+ /**
6
+ * Resolves the entity name targeted by `params` without additional I/O.
7
+ */
8
+ function resolveTargetEntityName(params) {
9
+ if (typeof params.EntityName === 'string' && params.EntityName.trim().length > 0) {
10
+ return params.EntityName.trim();
11
+ }
12
+ const viewEntityName = params.ViewEntity?.Get('Entity');
13
+ if (typeof viewEntityName === 'string' && viewEntityName.trim().length > 0) {
14
+ return viewEntityName.trim();
15
+ }
16
+ return '';
17
+ }
18
+ function resolveProviderForUser(contextUser) {
19
+ return (contextUser.OwningProvider ??
20
+ Metadata.Provider ?? // global-provider-ok: fallback when UserInfo was constructed without an OwningProvider
21
+ null);
22
+ }
23
+ function readRowField(row, fieldName) {
24
+ if (!row || typeof row !== 'object') {
25
+ return undefined;
26
+ }
27
+ if (row instanceof BaseEntity) {
28
+ return row.Get(fieldName);
29
+ }
30
+ return row[fieldName];
31
+ }
32
+ function hasOwnOrEntityField(row, fieldName) {
33
+ if (!row || typeof row !== 'object') {
34
+ return false;
35
+ }
36
+ if (row instanceof BaseEntity) {
37
+ return true;
38
+ }
39
+ return Object.prototype.hasOwnProperty.call(row, fieldName);
40
+ }
41
+ /**
42
+ * Server-side `PostRunView` data hook enforcing Row-Level Security (RLS) and Field-Level Security (FLS)
43
+ * on anchored conversations (`MJ: Conversations` where `AnchorType IS NOT NULL`) and their comment
44
+ * messages (`MJ: Conversation Details`).
45
+ *
46
+ * Guarantees that even a raw `RunView` call over GraphQL, REST, or server code cannot return:
47
+ * - Anchored conversation threads (`MJ: Conversations`) attached to a linked record that `contextUser`
48
+ * cannot read (entity permissions + RLS), or anchored/stamped with restricted fields (`AnchorKey` /
49
+ * `AnchorMetadata.restrictedFields`) that `contextUser` is denied Read access to (FLS).
50
+ * - Comment messages (`MJ: Conversation Details`) belonging to any such anchored conversation thread.
51
+ *
52
+ * Never mutates `results.Results` in place (safe with frozen cache entries); returns a replacement
53
+ * `RunViewResult` whenever rows are filtered out.
54
+ */
55
+ export const AnchoredConversationPostRunViewHook = async (params, results, contextUser) => {
56
+ if (!results?.Success ||
57
+ !Array.isArray(results.Results) ||
58
+ results.Results.length === 0 ||
59
+ !contextUser ||
60
+ INTERNAL_SECURITY_QUERY_PARAMS.has(params)) {
61
+ return results;
62
+ }
63
+ const entityName = resolveTargetEntityName(params);
64
+ if (entityName !== 'MJ: Conversations' && entityName !== 'MJ: Conversation Details') {
65
+ return results;
66
+ }
67
+ const provider = resolveProviderForUser(contextUser);
68
+ if (!provider || provider.ProviderType !== 'Database') {
69
+ return results;
70
+ }
71
+ if (WellKnownUserSource.Instance.IsSystemUser(contextUser)) {
72
+ return results;
73
+ }
74
+ try {
75
+ if (entityName === 'MJ: Conversations') {
76
+ return await filterConversationsRunViewResult(provider, contextUser, results);
77
+ }
78
+ return await filterConversationDetailsRunViewResult(provider, contextUser, results);
79
+ }
80
+ catch (error) {
81
+ LogError(`AnchoredConversationPostRunViewHook failed on ${entityName}: ${error instanceof Error ? error.message : String(error)}`);
82
+ return results;
83
+ }
84
+ };
85
+ async function filterConversationsRunViewResult(provider, contextUser, results) {
86
+ const rows = results.Results;
87
+ // Check if any row is missing security columns due to a narrow `Fields` projection
88
+ const missingMetadataIds = [];
89
+ for (const row of rows) {
90
+ const hasSecurityCols = hasOwnOrEntityField(row, 'AnchorType') &&
91
+ hasOwnOrEntityField(row, 'LinkedEntityID') &&
92
+ hasOwnOrEntityField(row, 'LinkedRecordID') &&
93
+ hasOwnOrEntityField(row, 'AnchorKey') &&
94
+ hasOwnOrEntityField(row, 'AnchorMetadata');
95
+ if (!hasSecurityCols) {
96
+ const id = String(readRowField(row, 'ID') ?? '').trim();
97
+ if (id) {
98
+ missingMetadataIds.push(id);
99
+ }
100
+ }
101
+ }
102
+ const hydratedById = new Map();
103
+ if (missingMetadataIds.length > 0) {
104
+ const fetched = await fetchConversationsSecurityMetadataByIds(provider, contextUser, missingMetadataIds, false);
105
+ for (const item of fetched) {
106
+ if (item.ID) {
107
+ hydratedById.set(NormalizeUUID(item.ID), item);
108
+ }
109
+ }
110
+ }
111
+ // Collect anchored candidates
112
+ const anchoredCandidates = [];
113
+ for (let i = 0; i < rows.length; i++) {
114
+ const row = rows[i];
115
+ const rowId = String(readRowField(row, 'ID') ?? '').trim();
116
+ const hydrated = rowId ? hydratedById.get(NormalizeUUID(rowId)) : undefined;
117
+ const anchorType = (hydrated ? hydrated.AnchorType : readRowField(row, 'AnchorType'));
118
+ const linkedEntityId = (hydrated ? hydrated.LinkedEntityID : readRowField(row, 'LinkedEntityID'));
119
+ const linkedRecordId = (hydrated ? hydrated.LinkedRecordID : readRowField(row, 'LinkedRecordID'));
120
+ if (!anchorType && (!linkedEntityId || !linkedRecordId)) {
121
+ continue;
122
+ }
123
+ // Only filter anchored comment threads (or conversations linked to a record that carry an anchor)
124
+ if (!anchorType) {
125
+ continue;
126
+ }
127
+ const anchorKey = (hydrated ? hydrated.AnchorKey : readRowField(row, 'AnchorKey'));
128
+ const anchorMetadata = (hydrated ? hydrated.AnchorMetadata : readRowField(row, 'AnchorMetadata'));
129
+ const anchorMetadataObject = (hydrated ? hydrated.AnchorMetadataObject : readRowField(row, 'AnchorMetadataObject'));
130
+ anchoredCandidates.push({
131
+ __rowIndex: i,
132
+ ID: rowId || undefined,
133
+ LinkedEntityID: linkedEntityId ?? null,
134
+ LinkedRecordID: linkedRecordId ?? null,
135
+ AnchorType: anchorType,
136
+ AnchorKey: anchorKey ?? null,
137
+ AnchorMetadata: anchorMetadata ?? null,
138
+ AnchorMetadataObject: anchorMetadataObject ?? null
139
+ });
140
+ }
141
+ if (anchoredCandidates.length === 0) {
142
+ return results;
143
+ }
144
+ const allowedAnchored = await FilterAnchoredConversationsForUser(provider, contextUser, anchoredCandidates);
145
+ if (allowedAnchored.length === anchoredCandidates.length) {
146
+ return results;
147
+ }
148
+ const allowedRowIndexes = new Set(allowedAnchored.map((c) => c.__rowIndex));
149
+ const anchoredRowIndexes = new Set(anchoredCandidates.map((c) => c.__rowIndex));
150
+ const filteredRows = rows.filter((_row, index) => !anchoredRowIndexes.has(index) || allowedRowIndexes.has(index));
151
+ const removedCount = rows.length - filteredRows.length;
152
+ return {
153
+ ...results,
154
+ Results: filteredRows,
155
+ RowCount: filteredRows.length,
156
+ TotalRowCount: Math.max(filteredRows.length, (results.TotalRowCount ?? results.RowCount ?? rows.length) - removedCount)
157
+ };
158
+ }
159
+ async function filterConversationDetailsRunViewResult(provider, contextUser, results) {
160
+ const rows = results.Results;
161
+ const missingConvIdDetailIds = [];
162
+ for (const row of rows) {
163
+ if (!hasOwnOrEntityField(row, 'ConversationID')) {
164
+ const detailId = String(readRowField(row, 'ID') ?? '').trim();
165
+ if (detailId) {
166
+ missingConvIdDetailIds.push(detailId);
167
+ }
168
+ }
169
+ }
170
+ const convIdByDetailId = new Map();
171
+ if (missingConvIdDetailIds.length > 0) {
172
+ const fetchedDetails = await fetchDetailConversationIdsByIds(provider, contextUser, missingConvIdDetailIds);
173
+ for (const d of fetchedDetails) {
174
+ if (d.ID && d.ConversationID) {
175
+ convIdByDetailId.set(NormalizeUUID(d.ID), d.ConversationID);
176
+ }
177
+ }
178
+ }
179
+ const conversationIdsByNorm = new Map();
180
+ const rowConvNormByIndex = new Map();
181
+ for (let i = 0; i < rows.length; i++) {
182
+ const row = rows[i];
183
+ let convId = String(readRowField(row, 'ConversationID') ?? '').trim();
184
+ if (!convId) {
185
+ const detailId = String(readRowField(row, 'ID') ?? '').trim();
186
+ if (detailId) {
187
+ convId = convIdByDetailId.get(NormalizeUUID(detailId)) ?? '';
188
+ }
189
+ }
190
+ if (convId) {
191
+ const norm = NormalizeUUID(convId);
192
+ conversationIdsByNorm.set(norm, convId);
193
+ rowConvNormByIndex.set(i, norm);
194
+ }
195
+ }
196
+ if (conversationIdsByNorm.size === 0) {
197
+ return results;
198
+ }
199
+ // Fetch only parent conversations that are anchored (`AnchorType IS NOT NULL`)
200
+ const anchoredParents = await fetchConversationsSecurityMetadataByIds(provider, contextUser, [...conversationIdsByNorm.values()], true);
201
+ if (anchoredParents.length === 0) {
202
+ return results;
203
+ }
204
+ const allowedParents = await FilterAnchoredConversationsForUser(provider, contextUser, anchoredParents);
205
+ if (allowedParents.length === anchoredParents.length) {
206
+ return results;
207
+ }
208
+ const allowedNormIds = new Set(allowedParents.map((c) => NormalizeUUID(String(c.ID ?? ''))));
209
+ const deniedNormIds = new Set();
210
+ for (const parent of anchoredParents) {
211
+ const norm = NormalizeUUID(String(parent.ID ?? ''));
212
+ if (norm && !allowedNormIds.has(norm)) {
213
+ deniedNormIds.add(norm);
214
+ }
215
+ }
216
+ if (deniedNormIds.size === 0) {
217
+ return results;
218
+ }
219
+ const filteredRows = rows.filter((_row, index) => {
220
+ const convNorm = rowConvNormByIndex.get(index);
221
+ return !convNorm || !deniedNormIds.has(convNorm);
222
+ });
223
+ const removedCount = rows.length - filteredRows.length;
224
+ return {
225
+ ...results,
226
+ Results: filteredRows,
227
+ RowCount: filteredRows.length,
228
+ TotalRowCount: Math.max(filteredRows.length, (results.TotalRowCount ?? results.RowCount ?? rows.length) - removedCount)
229
+ };
230
+ }
231
+ async function fetchConversationsSecurityMetadataByIds(provider, contextUser, conversationIds, anchoredOnly) {
232
+ const uniqueEscaped = Array.from(new Set(conversationIds
233
+ .map((id) => id.trim())
234
+ .filter((id) => id.length > 0)
235
+ .map((id) => `'${EscapeSQLString(id)}'`)));
236
+ if (uniqueEscaped.length === 0) {
237
+ return [];
238
+ }
239
+ const systemUser = (await WellKnownUserSource.Instance.GetSystemUser(provider)) ?? contextUser;
240
+ const rv = RunView.FromMetadataProvider(provider);
241
+ const filterClauses = [`ID IN (${uniqueEscaped.join(',')})`];
242
+ if (anchoredOnly) {
243
+ filterClauses.push(`AnchorType IS NOT NULL`);
244
+ }
245
+ const params = {
246
+ EntityName: 'MJ: Conversations',
247
+ ExtraFilter: filterClauses.join(' AND '),
248
+ Fields: ['ID', 'LinkedEntityID', 'LinkedRecordID', 'AnchorType', 'AnchorKey', 'AnchorMetadata'],
249
+ ResultType: 'simple'
250
+ };
251
+ INTERNAL_SECURITY_QUERY_PARAMS.add(params);
252
+ const res = await rv.RunView(params, systemUser);
253
+ if (!res?.Success || !Array.isArray(res.Results)) {
254
+ return [];
255
+ }
256
+ return res.Results;
257
+ }
258
+ async function fetchDetailConversationIdsByIds(provider, contextUser, detailIds) {
259
+ const uniqueEscaped = Array.from(new Set(detailIds
260
+ .map((id) => id.trim())
261
+ .filter((id) => id.length > 0)
262
+ .map((id) => `'${EscapeSQLString(id)}'`)));
263
+ if (uniqueEscaped.length === 0) {
264
+ return [];
265
+ }
266
+ const systemUser = (await WellKnownUserSource.Instance.GetSystemUser(provider)) ?? contextUser;
267
+ const rv = RunView.FromMetadataProvider(provider);
268
+ const params = {
269
+ EntityName: 'MJ: Conversation Details',
270
+ ExtraFilter: `ID IN (${uniqueEscaped.join(',')})`,
271
+ Fields: ['ID', 'ConversationID'],
272
+ ResultType: 'simple'
273
+ };
274
+ INTERNAL_SECURITY_QUERY_PARAMS.add(params);
275
+ const res = await rv.RunView(params, systemUser);
276
+ if (!res?.Success || !Array.isArray(res.Results)) {
277
+ return [];
278
+ }
279
+ return res.Results;
280
+ }
281
+ /**
282
+ * Ensures {@link AnchoredConversationPostRunViewHook} is registered in the global `PostRunView`
283
+ * data hooks store. Safe to call multiple times (idempotent).
284
+ */
285
+ export function EnsureAnchoredConversationReadHookRegistered() {
286
+ const existing = GetDataHooks('PostRunView');
287
+ if (!existing.includes(AnchoredConversationPostRunViewHook)) {
288
+ RegisterDataHook('PostRunView', AnchoredConversationPostRunViewHook);
289
+ }
290
+ }
291
+ // Auto-register when @memberjunction/core-entities-server is loaded
292
+ EnsureAnchoredConversationReadHookRegistered();
293
+ //# sourceMappingURL=AnchoredConversationReadSecurityHook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnchoredConversationReadSecurityHook.js","sourceRoot":"","sources":["../../src/custom/AnchoredConversationReadSecurityHook.ts"],"names":[],"mappings":"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"}
@@ -1,48 +1,38 @@
1
1
  import { EntitySaveOptions } from "@memberjunction/core";
2
2
  import { MJConversationDetailEntityExtended } from "@memberjunction/core-entities";
3
3
  /**
4
- * Server-side subclass of MJConversationDetailEntity that automatically tracks
5
- * when the original message content has been modified, and checks the fork of
6
- * each new message.
7
- *
8
- * When a user edits their message after initial creation, the OriginalMessageChanged
9
- * flag is set to true, allowing the UI to display an "(Edited)" indicator. This is
10
- * also the edit signal for cross-turn conversation compaction
11
- * (plans/agent-conversation-compaction.md §6): a row edited after it was folded into
12
- * a persisted summary keeps the flag, Record Changes already holds the diff, and the
13
- * UI can derive "edited after summarization" from the flag plus the row's Sequence
14
- * being below the current summary boundary — no summary regeneration required (the
15
- * summary is explicitly lossy; agents page in exact rows via the retrieval tools).
16
- *
17
- * Extends {@link MJConversationDetailEntityExtended} rather than the generated entity
18
- * directly. Both classes register for `BaseEntity` under the key
19
- * `'MJ: Conversation Details'`, and `@RegisterClass` passes `priority = 0`, which routes
20
- * to the auto-increment branch — so whichever registers LAST wins outright. On the server
21
- * this package loads after `@memberjunction/core-entities`, which meant this class replaced
22
- * the Extended one and its `Save`/`Delete` permission gate never ran. That gate is the check
23
- * that only a conversation's owner may set `UserRating` / `UserFeedback`, and that a
24
- * non-owner without a resource grant cannot write at all — and it is explicitly designed to
25
- * run server-side (`ProviderType === 'Database'`), which is exactly where it was being
26
- * shadowed out. Inheriting composes the two behaviors instead: this class flags the edit,
27
- * then delegates to the permission gate via `super.Save`.
28
- *
29
- * A new row keeps the `BranchID` its writer set (null is Main). On create, a non-null `BranchID`
30
- * must name a fork of the same conversation; otherwise `Save` returns `false` with a result
31
- * entry and nothing is written.
4
+ * Server-side subclass of MJConversationDetailEntity that:
5
+ * 1. Automatically tracks when the original message content has been modified (`OriginalMessageChanged`).
6
+ * 2. Checks the fork (`BranchID`) of each new message on create.
7
+ * 3. Inspects comments posted on anchored conversations (`LinkedEntityID` + `LinkedRecordID`)
8
+ * when the target entity has `FieldLevelSecurityApplies === true`:
9
+ * - Runs two-stage restricted-field detection (deterministic field/label/value matching + semantic LLM classifier).
10
+ * - Refuses the save if the authoring user is denied Read access on a restricted field referenced by their comment.
11
+ * - Stamps any detected restricted field names into the parent conversation's
12
+ * `AnchorMetadataObject.restrictedFields` so all subsequent reads filter deterministically in 0ms.
13
+ * 4. Delegates to {@link MJConversationDetailEntityExtended} for write-access, authorship,
14
+ * server-authored reply validation, and `@mention` notification dispatch.
32
15
  */
33
16
  export declare class MJConversationDetailEntityServer extends MJConversationDetailEntityExtended {
34
17
  /**
35
18
  * Sets the OriginalMessageChanged flag on a genuine message edit, checks a new row's fork,
36
- * then calls super.Save(). Returns `false` without saving when the fork check fails.
19
+ * runs FLS restricted-field inspection for anchored conversations, then calls super.Save().
37
20
  */
38
21
  Save(options?: EntitySaveOptions): Promise<boolean>;
22
+ /**
23
+ * When saving a message in an anchored conversation (`LinkedEntityID` + `LinkedRecordID`) whose
24
+ * target entity has `FieldLevelSecurityApplies === true`, runs {@link InspectAndStampRestrictedFieldsForComment}
25
+ * to block restricted-field probing by unauthorized authors and stamp `restrictedFields` onto the
26
+ * parent conversation's `AnchorMetadataObject`.
27
+ */
28
+ CheckAndStampAnchoredRestrictedFields(): Promise<string | null>;
39
29
  /**
40
30
  * On create, a non-null BranchID must name a fork of the same conversation. Returns why the save
41
31
  * must fail, or null when it may go on. An existing row, a Main row (null BranchID), a row with no
42
32
  * ConversationID and a save with no context user are not checked.
43
33
  */
44
34
  CheckBranchOnCreate(): Promise<string | null>;
45
- /** Records a failed create in the result history, so callers see why nothing was written. */
35
+ /** Records a failed create/save in the result history, so callers see why nothing was written. */
46
36
  private recordCreateFailure;
47
37
  /**
48
38
  * True when this save is a genuine Message EDIT on an existing record — not one of
@@ -1 +1 @@
1
- {"version":3,"file":"MJConversationDetailEntityServer.server.d.ts","sourceRoot":"","sources":["../../src/custom/MJConversationDetailEntityServer.server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,iBAAiB,EAAwC,MAAM,sBAAsB,CAAC;AAE7H,OAAO,EAAE,kCAAkC,EAAE,MAAM,+BAA+B,CAAC;AAEnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBACa,gCAAiC,SAAQ,kCAAkC;IACpF;;;OAGG;IACY,IAAI,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC;IAYlE;;;;OAIG;IACU,mBAAmB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAgC1D,6FAA6F;IAC7F,OAAO,CAAC,mBAAmB;IAU3B;;;;;;;;;;;OAWG;IACI,gCAAgC,IAAI,OAAO;CAiBrD"}
1
+ {"version":3,"file":"MJConversationDetailEntityServer.server.d.ts","sourceRoot":"","sources":["../../src/custom/MJConversationDetailEntityServer.server.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,iBAAiB,EAKpB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,kCAAkC,EAAwB,MAAM,+BAA+B,CAAC;AAGzG;;;;;;;;;;;;GAYG;AACH,qBACa,gCAAiC,SAAQ,kCAAkC;IACpF;;;OAGG;IACY,IAAI,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBlE;;;;;OAKG;IACU,qCAAqC,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAsE5E;;;;OAIG;IACU,mBAAmB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAgC1D,kGAAkG;IAClG,OAAO,CAAC,mBAAmB;IAU3B;;;;;;;;;;;OAWG;IACI,gCAAgC,IAAI,OAAO;CAiBrD"}
@@ -4,43 +4,28 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { BaseEntity, BaseEntityResult, LogError, RunView } from "@memberjunction/core";
7
+ var MJConversationDetailEntityServer_1;
8
+ import { BaseEntity, BaseEntityResult, LogError, RunView, WellKnownUserSource } from "@memberjunction/core";
8
9
  import { EscapeSQLString, RegisterClass, UUIDsEqual } from "@memberjunction/global";
9
10
  import { MJConversationDetailEntityExtended } from "@memberjunction/core-entities";
11
+ import { InspectAndStampRestrictedFieldsForComment } from "./RestrictedFieldCommentClassifier.js";
10
12
  /**
11
- * Server-side subclass of MJConversationDetailEntity that automatically tracks
12
- * when the original message content has been modified, and checks the fork of
13
- * each new message.
14
- *
15
- * When a user edits their message after initial creation, the OriginalMessageChanged
16
- * flag is set to true, allowing the UI to display an "(Edited)" indicator. This is
17
- * also the edit signal for cross-turn conversation compaction
18
- * (plans/agent-conversation-compaction.md §6): a row edited after it was folded into
19
- * a persisted summary keeps the flag, Record Changes already holds the diff, and the
20
- * UI can derive "edited after summarization" from the flag plus the row's Sequence
21
- * being below the current summary boundary — no summary regeneration required (the
22
- * summary is explicitly lossy; agents page in exact rows via the retrieval tools).
23
- *
24
- * Extends {@link MJConversationDetailEntityExtended} rather than the generated entity
25
- * directly. Both classes register for `BaseEntity` under the key
26
- * `'MJ: Conversation Details'`, and `@RegisterClass` passes `priority = 0`, which routes
27
- * to the auto-increment branch — so whichever registers LAST wins outright. On the server
28
- * this package loads after `@memberjunction/core-entities`, which meant this class replaced
29
- * the Extended one and its `Save`/`Delete` permission gate never ran. That gate is the check
30
- * that only a conversation's owner may set `UserRating` / `UserFeedback`, and that a
31
- * non-owner without a resource grant cannot write at all — and it is explicitly designed to
32
- * run server-side (`ProviderType === 'Database'`), which is exactly where it was being
33
- * shadowed out. Inheriting composes the two behaviors instead: this class flags the edit,
34
- * then delegates to the permission gate via `super.Save`.
35
- *
36
- * A new row keeps the `BranchID` its writer set (null is Main). On create, a non-null `BranchID`
37
- * must name a fork of the same conversation; otherwise `Save` returns `false` with a result
38
- * entry and nothing is written.
13
+ * Server-side subclass of MJConversationDetailEntity that:
14
+ * 1. Automatically tracks when the original message content has been modified (`OriginalMessageChanged`).
15
+ * 2. Checks the fork (`BranchID`) of each new message on create.
16
+ * 3. Inspects comments posted on anchored conversations (`LinkedEntityID` + `LinkedRecordID`)
17
+ * when the target entity has `FieldLevelSecurityApplies === true`:
18
+ * - Runs two-stage restricted-field detection (deterministic field/label/value matching + semantic LLM classifier).
19
+ * - Refuses the save if the authoring user is denied Read access on a restricted field referenced by their comment.
20
+ * - Stamps any detected restricted field names into the parent conversation's
21
+ * `AnchorMetadataObject.restrictedFields` so all subsequent reads filter deterministically in 0ms.
22
+ * 4. Delegates to {@link MJConversationDetailEntityExtended} for write-access, authorship,
23
+ * server-authored reply validation, and `@mention` notification dispatch.
39
24
  */
40
- let MJConversationDetailEntityServer = class MJConversationDetailEntityServer extends MJConversationDetailEntityExtended {
25
+ let MJConversationDetailEntityServer = MJConversationDetailEntityServer_1 = class MJConversationDetailEntityServer extends MJConversationDetailEntityExtended {
41
26
  /**
42
27
  * Sets the OriginalMessageChanged flag on a genuine message edit, checks a new row's fork,
43
- * then calls super.Save(). Returns `false` without saving when the fork check fails.
28
+ * runs FLS restricted-field inspection for anchored conversations, then calls super.Save().
44
29
  */
45
30
  async Save(options) {
46
31
  if (this.ShouldFlagOriginalMessageChanged()) {
@@ -51,8 +36,77 @@ let MJConversationDetailEntityServer = class MJConversationDetailEntityServer ex
51
36
  this.recordCreateFailure(problem);
52
37
  return false;
53
38
  }
39
+ const checkFls = this.CheckAndStampAnchoredRestrictedFields ??
40
+ MJConversationDetailEntityServer_1.prototype.CheckAndStampAnchoredRestrictedFields;
41
+ const flsRefusal = await checkFls.call(this);
42
+ if (flsRefusal) {
43
+ this.recordCreateFailure(flsRefusal, this.IsSaved ? 'update' : 'create');
44
+ return false;
45
+ }
54
46
  return super.Save(options);
55
47
  }
48
+ /**
49
+ * When saving a message in an anchored conversation (`LinkedEntityID` + `LinkedRecordID`) whose
50
+ * target entity has `FieldLevelSecurityApplies === true`, runs {@link InspectAndStampRestrictedFieldsForComment}
51
+ * to block restricted-field probing by unauthorized authors and stamp `restrictedFields` onto the
52
+ * parent conversation's `AnchorMetadataObject`.
53
+ */
54
+ async CheckAndStampAnchoredRestrictedFields() {
55
+ const provider = this.ProviderToUse;
56
+ if (provider?.ProviderType !== 'Database') {
57
+ return null;
58
+ }
59
+ const user = this.ContextCurrentUser;
60
+ if (!user || !this.ConversationID) {
61
+ return null;
62
+ }
63
+ // Only inspect on new messages or when Message text is being modified
64
+ if (this.IsSaved && !this.GetFieldByName('Message')?.Dirty) {
65
+ return null;
66
+ }
67
+ try {
68
+ const systemUser = (await WellKnownUserSource.Instance.GetSystemUser(provider)) ?? user;
69
+ const conversation = await provider.GetEntityObject('MJ: Conversations', systemUser);
70
+ const loaded = await conversation.Load(this.ConversationID);
71
+ if (!loaded || !conversation.AnchorType || !conversation.LinkedEntityID || !conversation.LinkedRecordID) {
72
+ return null;
73
+ }
74
+ const entityInfo = (typeof provider.EntityByID === 'function'
75
+ ? provider.EntityByID(conversation.LinkedEntityID)
76
+ : undefined) ??
77
+ provider.Entities?.find((e) => UUIDsEqual(e.ID, conversation.LinkedEntityID)) ??
78
+ null;
79
+ if (!entityInfo) {
80
+ return null;
81
+ }
82
+ if (!WellKnownUserSource.Instance.IsSystemUser(user)) {
83
+ if (entityInfo.AllowRecordComments === false) {
84
+ return `Record commenting is disabled for ${entityInfo.DisplayNameOrName}.`;
85
+ }
86
+ if (typeof entityInfo.CanUserComment === 'function' && !entityInfo.CanUserComment(user)) {
87
+ return `You do not have permission to post comments on ${entityInfo.DisplayNameOrName} records.`;
88
+ }
89
+ }
90
+ if (!entityInfo.FieldLevelSecurityApplies) {
91
+ return null;
92
+ }
93
+ const inspection = await InspectAndStampRestrictedFieldsForComment({
94
+ Provider: provider,
95
+ ContextUser: user,
96
+ Conversation: conversation,
97
+ EntityInfo: entityInfo,
98
+ MessageText: this.Message
99
+ });
100
+ if (!inspection.Allowed) {
101
+ return inspection.Reason ?? 'You do not have permission to comment on this restricted field.';
102
+ }
103
+ return null;
104
+ }
105
+ catch (error) {
106
+ LogError(`MJConversationDetailEntityServer.CheckAndStampAnchoredRestrictedFields failed on conversation ${this.ConversationID}: ${error instanceof Error ? error.message : String(error)}`);
107
+ return null;
108
+ }
109
+ }
56
110
  /**
57
111
  * On create, a non-null BranchID must name a fork of the same conversation. Returns why the save
58
112
  * must fail, or null when it may go on. An existing row, a Main row (null BranchID), a row with no
@@ -90,11 +144,11 @@ let MJConversationDetailEntityServer = class MJConversationDetailEntityServer ex
90
144
  return 'Unable to check the fork of this message.';
91
145
  }
92
146
  }
93
- /** Records a failed create in the result history, so callers see why nothing was written. */
94
- recordCreateFailure(message) {
147
+ /** Records a failed create/save in the result history, so callers see why nothing was written. */
148
+ recordCreateFailure(message, type = 'create') {
95
149
  const result = new BaseEntityResult();
96
150
  result.Success = false;
97
- result.Type = 'create';
151
+ result.Type = type;
98
152
  result.Message = message;
99
153
  result.StartedAt = new Date();
100
154
  result.EndedAt = new Date();
@@ -130,7 +184,7 @@ let MJConversationDetailEntityServer = class MJConversationDetailEntityServer ex
130
184
  return true;
131
185
  }
132
186
  };
133
- MJConversationDetailEntityServer = __decorate([
187
+ MJConversationDetailEntityServer = MJConversationDetailEntityServer_1 = __decorate([
134
188
  RegisterClass(BaseEntity, "MJ: Conversation Details")
135
189
  ], MJConversationDetailEntityServer);
136
190
  export { MJConversationDetailEntityServer };
@@ -1 +1 @@
1
- {"version":3,"file":"MJConversationDetailEntityServer.server.js","sourceRoot":"","sources":["../../src/custom/MJConversationDetailEntityServer.server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"MJConversationDetailEntityServer.server.js","sourceRoot":"","sources":["../../src/custom/MJConversationDetailEntityServer.server.ts"],"names":[],"mappings":"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"}
@@ -1,13 +1,28 @@
1
- import { ValidationResult } from '@memberjunction/core';
1
+ import { EntitySaveOptions, ValidationResult } from '@memberjunction/core';
2
2
  import { MJConversationEntity } from '@memberjunction/core-entities';
3
3
  /**
4
4
  * Server-side `MJ: Conversations` entity.
5
5
  *
6
- * Keeps `ExternalID` — the scope identity an anonymous visitor's realtime session is owned through — out
7
- * of an anonymous visitor's reach once the conversation exists. See {@link CheckAnonymousOwnershipKeyChange}.
8
- * Uses the synchronous `Validate()` so the rule runs on every save, like `MJAIAgentSessionEntityServer`.
6
+ * 1. Keeps `ExternalID` — the scope identity an anonymous visitor's realtime session is owned through — out
7
+ * of an anonymous visitor's reach once the conversation exists. See {@link CheckAnonymousOwnershipKeyChange}.
8
+ * 2. Keeps who a conversation belongs to and who it is shared with in its owner's hands: see
9
+ * {@link checkOwnerOnlyChange}. Uses the synchronous `Validate()` so the rules run on every save, like
10
+ * `MJAIAgentSessionEntityServer`.
11
+ * 3. Saves `AdditionalData` as compact JSON when it holds an object, so the stored text always uses the spelling
12
+ * the conversation list's SQL filter matches (`INHERIT_FOLDER_ACCESS_SQL_CLAUSE`), and refuses a value the filter
13
+ * and the parser would still read differently: see {@link checkInheritanceReadingsAgree}.
14
+ * 4. Enforces Row-Level Security (RLS) and Field-Level Security (FLS) on anchored conversations
15
+ * (`LinkedEntityID` + `LinkedRecordID` / `AnchorType`):
16
+ * - Verifies the saving user can read the linked record (`UserCanReadLinkedRecord`).
17
+ * - Verifies the saving user is not denied Read access on the anchored field or `restrictedFields`
18
+ * (`IsConversationHiddenByFieldSecurity`).
19
+ * - When `AnchorType === 'Field'` targets an FLS-governed field (`HasFieldPermissions === true`),
20
+ * automatically stamps that canonical field name into `AnchorMetadataObject.restrictedFields`.
9
21
  */
10
22
  export declare class MJConversationEntityServer extends MJConversationEntity {
11
23
  Validate(): ValidationResult;
24
+ Save(options?: EntitySaveOptions): Promise<boolean>;
25
+ private checkAnchoredConversationAccessAndStampMetadata;
26
+ private recordSaveDenied;
12
27
  }
13
28
  //# sourceMappingURL=MJConversationEntityServer.server.d.ts.map