@memberjunction/lists 0.0.1 → 5.37.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/README.md +292 -28
- package/dist/AudienceResolver.d.ts +22 -0
- package/dist/AudienceResolver.d.ts.map +1 -0
- package/dist/AudienceResolver.js +25 -0
- package/dist/AudienceResolver.js.map +1 -0
- package/dist/ListOperations.d.ts +215 -0
- package/dist/ListOperations.d.ts.map +1 -0
- package/dist/ListOperations.js +829 -0
- package/dist/ListOperations.js.map +1 -0
- package/dist/ListSharing.d.ts +157 -0
- package/dist/ListSharing.d.ts.map +1 -0
- package/dist/ListSharing.js +568 -0
- package/dist/ListSharing.js.map +1 -0
- package/dist/deltaToken.d.ts +47 -0
- package/dist/deltaToken.d.ts.map +1 -0
- package/dist/deltaToken.js +123 -0
- package/dist/deltaToken.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -7
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import { LogError, Metadata, RunView, } from '@memberjunction/core';
|
|
2
|
+
import { AuditLogTypeEngine, CreateShareNotification, ResourceTypeEngine, } from '@memberjunction/core-entities';
|
|
3
|
+
/**
|
|
4
|
+
* @memberjunction/lists — sharing surface.
|
|
5
|
+
*
|
|
6
|
+
* Sharing has two parallel paths:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Direct share** — the recipient already has an MJ user account.
|
|
9
|
+
* We write a row to `MJResourcePermission` immediately, scoped to
|
|
10
|
+
* the List resource type. Either a user (`Type='User'`) or a role
|
|
11
|
+
* (`Type='Role'`) target.
|
|
12
|
+
*
|
|
13
|
+
* 2. **Email invitation** — the recipient may or may not have an
|
|
14
|
+
* account yet. We write a row to `MJ: List Invitations` with a
|
|
15
|
+
* signed token + expiry; `AcceptInvitation` later promotes that
|
|
16
|
+
* pending invite into a real `MJResourcePermission`.
|
|
17
|
+
*
|
|
18
|
+
* Every mutation also emits an `MJ: Audit Logs` entry — type names are
|
|
19
|
+
* resolved through `AuditLogTypeEngine.ByName(...)` at runtime so the
|
|
20
|
+
* seed data is the source of truth, not hardcoded UUIDs. Same pattern
|
|
21
|
+
* (`ResourceTypeEngine.ByName(...)`) for the `MJ: Resource Types` row
|
|
22
|
+
* that scopes `MJResourcePermission` records to the List resource type.
|
|
23
|
+
*/
|
|
24
|
+
/** Default invitation TTL: 7 days. The plan calls for tunable expiry but
|
|
25
|
+
* keeps the spec simple — overridable at call time. */
|
|
26
|
+
export const DEFAULT_INVITATION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
27
|
+
/**
|
|
28
|
+
* Name of the row in `MJ: Resource Types` that scopes List shares.
|
|
29
|
+
* Centralized so a rename in the seed data is a one-line fix here, not
|
|
30
|
+
* a grep across the codebase.
|
|
31
|
+
*/
|
|
32
|
+
const LIST_RESOURCE_TYPE_NAME = 'Lists';
|
|
33
|
+
/** Names of the rows in `MJ: Audit Log Types` for the seven list-sharing events. */
|
|
34
|
+
const LIST_AUDIT_LOG_TYPE_NAMES = {
|
|
35
|
+
Shared: 'List Shared',
|
|
36
|
+
Unshared: 'List Unshared',
|
|
37
|
+
InvitationSent: 'List Invitation Sent',
|
|
38
|
+
InvitationAccepted: 'List Invitation Accepted',
|
|
39
|
+
InvitationRevoked: 'List Invitation Revoked',
|
|
40
|
+
};
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// ListSharing service
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
/**
|
|
45
|
+
* Sharing engine. Public methods follow MJ PascalCase convention; private
|
|
46
|
+
* helpers are camelCase. Mirrors the structure of `ListOperations`.
|
|
47
|
+
*/
|
|
48
|
+
export class ListSharing {
|
|
49
|
+
constructor(contextUser, provider) {
|
|
50
|
+
this.contextUser = contextUser;
|
|
51
|
+
this.provider = provider;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Grant a direct share on a list to a user or role. Idempotent at the
|
|
55
|
+
* (list, target) level — re-sharing the same target updates the
|
|
56
|
+
* existing permission row's level rather than creating duplicates.
|
|
57
|
+
*/
|
|
58
|
+
async Share(args) {
|
|
59
|
+
try {
|
|
60
|
+
const md = this.metadata();
|
|
61
|
+
const existing = await this.findExistingPermission(args.ListID, args.Target);
|
|
62
|
+
const perm = existing ??
|
|
63
|
+
(await md.GetEntityObject('MJ: Resource Permissions', this.contextUser));
|
|
64
|
+
if (!existing) {
|
|
65
|
+
perm.NewRecord();
|
|
66
|
+
perm.ResourceTypeID = await this.getListResourceTypeId();
|
|
67
|
+
perm.ResourceRecordID = args.ListID;
|
|
68
|
+
perm.Type = args.Target.kind === 'user' ? 'User' : 'Role';
|
|
69
|
+
if (args.Target.kind === 'user')
|
|
70
|
+
perm.UserID = args.Target.userId;
|
|
71
|
+
else
|
|
72
|
+
perm.RoleID = args.Target.roleId;
|
|
73
|
+
perm.SharedByUserID = this.contextUser.ID;
|
|
74
|
+
}
|
|
75
|
+
perm.PermissionLevel = args.PermissionLevel;
|
|
76
|
+
perm.Status = 'Approved';
|
|
77
|
+
const ok = await perm.Save();
|
|
78
|
+
if (!ok) {
|
|
79
|
+
return this.shareFailure('UNEXPECTED_ERROR', `Failed to grant permission: ${perm.LatestResult?.CompleteMessage ?? 'unknown'}`);
|
|
80
|
+
}
|
|
81
|
+
await this.emitAuditLog({
|
|
82
|
+
Type: LIST_AUDIT_LOG_TYPE_NAMES.Shared,
|
|
83
|
+
ListID: args.ListID,
|
|
84
|
+
Description: `${this.contextUser.Name ?? this.contextUser.Email} shared list with ${args.Target.kind} ${this.targetId(args.Target)} as ${args.PermissionLevel}`,
|
|
85
|
+
Details: { TargetKind: args.Target.kind, TargetID: this.targetId(args.Target), Level: args.PermissionLevel },
|
|
86
|
+
});
|
|
87
|
+
// Direct user shares route through the platform-wide share-notification
|
|
88
|
+
// dispatcher — `@memberjunction/notifications` auto-registers a handler
|
|
89
|
+
// that fans this out across the user's notification preferences (in-app
|
|
90
|
+
// + email + SMS). Role-target shares are fan-out at the role level by
|
|
91
|
+
// a separate worker; we don't try to enumerate every role member here.
|
|
92
|
+
if (args.Target.kind === 'user' && args.Target.userId !== this.contextUser.ID) {
|
|
93
|
+
await this.dispatchShareNotification(args.ListID, args.Target.userId, args.PermissionLevel);
|
|
94
|
+
}
|
|
95
|
+
return { Success: true, ResultCode: 'SUCCESS', Message: 'Permission granted', PermissionID: perm.ID };
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
99
|
+
LogError(`ListSharing.Share failed: ${message}`);
|
|
100
|
+
return this.shareFailure('UNEXPECTED_ERROR', message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Revoke a previously-granted permission. We soft-revoke (Status =
|
|
105
|
+
* 'Revoked') rather than hard-delete so the audit trail stays
|
|
106
|
+
* meaningful — `GetSharesForList` filters out revoked rows by default.
|
|
107
|
+
*/
|
|
108
|
+
async Unshare(permissionId) {
|
|
109
|
+
try {
|
|
110
|
+
const md = this.metadata();
|
|
111
|
+
const perm = await md.GetEntityObject('MJ: Resource Permissions', this.contextUser);
|
|
112
|
+
const loaded = await perm.Load(permissionId);
|
|
113
|
+
if (!loaded)
|
|
114
|
+
return this.shareFailure('PERMISSION_NOT_FOUND', `Permission ${permissionId} not found`);
|
|
115
|
+
const listId = perm.ResourceRecordID;
|
|
116
|
+
const targetSnapshot = this.snapshotTarget(perm);
|
|
117
|
+
perm.Status = 'Revoked';
|
|
118
|
+
const ok = await perm.Save();
|
|
119
|
+
if (!ok) {
|
|
120
|
+
return this.shareFailure('UNEXPECTED_ERROR', `Failed to revoke: ${perm.LatestResult?.CompleteMessage ?? 'unknown'}`);
|
|
121
|
+
}
|
|
122
|
+
await this.emitAuditLog({
|
|
123
|
+
Type: LIST_AUDIT_LOG_TYPE_NAMES.Unshared,
|
|
124
|
+
ListID: listId,
|
|
125
|
+
Description: `${this.contextUser.Name ?? this.contextUser.Email} revoked permission ${permissionId}`,
|
|
126
|
+
Details: targetSnapshot,
|
|
127
|
+
});
|
|
128
|
+
return { Success: true, ResultCode: 'SUCCESS', Message: 'Permission revoked', PermissionID: perm.ID };
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
132
|
+
LogError(`ListSharing.Unshare failed: ${message}`);
|
|
133
|
+
return this.shareFailure('UNEXPECTED_ERROR', message);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Send an email invitation to share a list with someone — who may or
|
|
138
|
+
* may not already have an MJ user account. We persist the invitation
|
|
139
|
+
* with a random token + expiry; the recipient later calls
|
|
140
|
+
* `AcceptInvitation(token)` to promote it into a real
|
|
141
|
+
* `MJResourcePermission`.
|
|
142
|
+
*/
|
|
143
|
+
async Invite(args) {
|
|
144
|
+
if (!args.Email || !args.Email.includes('@')) {
|
|
145
|
+
return { Success: false, ResultCode: 'INVALID_PARAMETER', Message: 'Email is required' };
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const md = this.metadata();
|
|
149
|
+
const inv = await md.GetEntityObject('MJ: List Invitations', this.contextUser);
|
|
150
|
+
inv.NewRecord();
|
|
151
|
+
inv.ListID = args.ListID;
|
|
152
|
+
inv.Email = args.Email;
|
|
153
|
+
inv.Role = args.Role;
|
|
154
|
+
inv.Token = this.generateInvitationToken();
|
|
155
|
+
inv.ExpiresAt = new Date(Date.now() + (args.TtlMs ?? DEFAULT_INVITATION_TTL_MS));
|
|
156
|
+
inv.CreatedByUserID = this.contextUser.ID;
|
|
157
|
+
inv.Status = 'Pending';
|
|
158
|
+
const ok = await inv.Save();
|
|
159
|
+
if (!ok) {
|
|
160
|
+
return {
|
|
161
|
+
Success: false,
|
|
162
|
+
ResultCode: 'UNEXPECTED_ERROR',
|
|
163
|
+
Message: `Failed to create invitation: ${inv.LatestResult?.CompleteMessage ?? 'unknown'}`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
await this.emitAuditLog({
|
|
167
|
+
Type: LIST_AUDIT_LOG_TYPE_NAMES.InvitationSent,
|
|
168
|
+
ListID: args.ListID,
|
|
169
|
+
Description: `${this.contextUser.Name ?? this.contextUser.Email} invited ${args.Email} (${args.Role})`,
|
|
170
|
+
Details: { Email: args.Email, Role: args.Role, InvitationID: inv.ID },
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
Success: true,
|
|
174
|
+
ResultCode: 'SUCCESS',
|
|
175
|
+
Message: `Invitation sent to ${args.Email}`,
|
|
176
|
+
InvitationID: inv.ID,
|
|
177
|
+
Token: inv.Token,
|
|
178
|
+
ExpiresAt: inv.ExpiresAt,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
183
|
+
LogError(`ListSharing.Invite failed: ${message}`);
|
|
184
|
+
return { Success: false, ResultCode: 'UNEXPECTED_ERROR', Message: message };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Accept an invitation by its token. The caller becomes the recipient
|
|
189
|
+
* (we read `contextUser`); the invitation's email must match the
|
|
190
|
+
* caller's email — protects against token interception by a different
|
|
191
|
+
* MJ account.
|
|
192
|
+
*
|
|
193
|
+
* On success: creates the `MJResourcePermission` and marks the
|
|
194
|
+
* invitation `Accepted`.
|
|
195
|
+
*/
|
|
196
|
+
async AcceptInvitation(token) {
|
|
197
|
+
try {
|
|
198
|
+
const md = this.metadata();
|
|
199
|
+
const rv = this.runView();
|
|
200
|
+
const lookup = await rv.RunView({
|
|
201
|
+
EntityName: 'MJ: List Invitations',
|
|
202
|
+
ExtraFilter: `Token='${this.escape(token)}'`,
|
|
203
|
+
ResultType: 'entity_object',
|
|
204
|
+
}, this.contextUser);
|
|
205
|
+
if (!lookup.Success || !lookup.Results || lookup.Results.length === 0) {
|
|
206
|
+
return { Success: false, ResultCode: 'INVITATION_NOT_FOUND', Message: 'Invitation not found' };
|
|
207
|
+
}
|
|
208
|
+
const inv = lookup.Results[0];
|
|
209
|
+
if (inv.Status === 'Revoked') {
|
|
210
|
+
return { Success: false, ResultCode: 'INVITATION_REVOKED', Message: 'Invitation has been revoked' };
|
|
211
|
+
}
|
|
212
|
+
if (inv.Status === 'Accepted') {
|
|
213
|
+
return { Success: false, ResultCode: 'INVITATION_ALREADY_USED', Message: 'Invitation has already been accepted' };
|
|
214
|
+
}
|
|
215
|
+
if (inv.ExpiresAt.getTime() < Date.now()) {
|
|
216
|
+
// Mark expired so subsequent reads see consistent state.
|
|
217
|
+
inv.Status = 'Expired';
|
|
218
|
+
await inv.Save();
|
|
219
|
+
return { Success: false, ResultCode: 'INVITATION_EXPIRED', Message: 'Invitation has expired' };
|
|
220
|
+
}
|
|
221
|
+
// Email-match check — best-effort: if context user has no email we
|
|
222
|
+
// skip and rely on the token itself as the auth gate.
|
|
223
|
+
const callerEmail = this.contextUser.Email?.toLowerCase();
|
|
224
|
+
if (callerEmail && inv.Email.toLowerCase() !== callerEmail) {
|
|
225
|
+
return {
|
|
226
|
+
Success: false,
|
|
227
|
+
ResultCode: 'EMAIL_RECIPIENT_NOT_FOUND',
|
|
228
|
+
Message: `Invitation was for ${inv.Email} but caller is ${this.contextUser.Email}`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const share = await this.Share({
|
|
232
|
+
ListID: inv.ListID,
|
|
233
|
+
Target: { kind: 'user', userId: this.contextUser.ID },
|
|
234
|
+
PermissionLevel: inv.Role === 'Editor' ? 'Edit' : 'View',
|
|
235
|
+
});
|
|
236
|
+
if (!share.Success) {
|
|
237
|
+
return { Success: false, ResultCode: share.ResultCode, Message: share.Message };
|
|
238
|
+
}
|
|
239
|
+
inv.Status = 'Accepted';
|
|
240
|
+
await inv.Save();
|
|
241
|
+
await this.emitAuditLog({
|
|
242
|
+
Type: LIST_AUDIT_LOG_TYPE_NAMES.InvitationAccepted,
|
|
243
|
+
ListID: inv.ListID,
|
|
244
|
+
Description: `${this.contextUser.Name ?? this.contextUser.Email} accepted invitation ${inv.ID}`,
|
|
245
|
+
Details: { InvitationID: inv.ID, GrantedPermissionID: share.PermissionID },
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
Success: true,
|
|
249
|
+
ResultCode: 'SUCCESS',
|
|
250
|
+
Message: 'Invitation accepted',
|
|
251
|
+
PermissionID: share.PermissionID,
|
|
252
|
+
ListID: inv.ListID,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
257
|
+
LogError(`ListSharing.AcceptInvitation failed: ${message}`);
|
|
258
|
+
return { Success: false, ResultCode: 'UNEXPECTED_ERROR', Message: message };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Revoke a pending invitation. No-op if already accepted (the resulting
|
|
263
|
+
* permission can still be `Unshare`d separately).
|
|
264
|
+
*/
|
|
265
|
+
async RevokeInvitation(invitationId) {
|
|
266
|
+
try {
|
|
267
|
+
const md = this.metadata();
|
|
268
|
+
const inv = await md.GetEntityObject('MJ: List Invitations', this.contextUser);
|
|
269
|
+
const loaded = await inv.Load(invitationId);
|
|
270
|
+
if (!loaded)
|
|
271
|
+
return this.shareFailure('INVITATION_NOT_FOUND', `Invitation ${invitationId} not found`);
|
|
272
|
+
if (inv.Status === 'Accepted') {
|
|
273
|
+
return this.shareFailure('INVITATION_ALREADY_USED', 'Invitation already accepted — Unshare the resulting permission instead');
|
|
274
|
+
}
|
|
275
|
+
inv.Status = 'Revoked';
|
|
276
|
+
const ok = await inv.Save();
|
|
277
|
+
if (!ok) {
|
|
278
|
+
return this.shareFailure('UNEXPECTED_ERROR', `Failed to revoke: ${inv.LatestResult?.CompleteMessage ?? 'unknown'}`);
|
|
279
|
+
}
|
|
280
|
+
await this.emitAuditLog({
|
|
281
|
+
Type: LIST_AUDIT_LOG_TYPE_NAMES.InvitationRevoked,
|
|
282
|
+
ListID: inv.ListID,
|
|
283
|
+
Description: `${this.contextUser.Name ?? this.contextUser.Email} revoked invitation ${inv.ID}`,
|
|
284
|
+
Details: { InvitationID: inv.ID, Email: inv.Email },
|
|
285
|
+
});
|
|
286
|
+
return { Success: true, ResultCode: 'SUCCESS', Message: 'Invitation revoked' };
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
290
|
+
LogError(`ListSharing.RevokeInvitation failed: ${message}`);
|
|
291
|
+
return this.shareFailure('UNEXPECTED_ERROR', message);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Return the current (non-revoked) shares for a list — both user and
|
|
296
|
+
* role grants, mapped into the canonical `ListShareSummary` shape.
|
|
297
|
+
*/
|
|
298
|
+
async GetSharesForList(listId) {
|
|
299
|
+
const resourceTypeId = await this.getListResourceTypeId();
|
|
300
|
+
const rv = this.runView();
|
|
301
|
+
const result = await rv.RunView({
|
|
302
|
+
EntityName: 'MJ: Resource Permissions',
|
|
303
|
+
ExtraFilter: `ResourceTypeID='${resourceTypeId}' AND ResourceRecordID='${this.escape(listId)}' AND Status<>'Revoked'`,
|
|
304
|
+
OrderBy: '__mj_CreatedAt DESC',
|
|
305
|
+
ResultType: 'entity_object',
|
|
306
|
+
}, this.contextUser);
|
|
307
|
+
if (!result.Success || !result.Results)
|
|
308
|
+
return [];
|
|
309
|
+
return result.Results.map((p) => this.toShareSummary(p));
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Resolve the effective permission level the context user has on a
|
|
313
|
+
* list. Returns `'Owner'` if they created the list (UserID match);
|
|
314
|
+
* otherwise checks for a direct user share. Role-based shares are
|
|
315
|
+
* resolved by `ResourcePermissionEngine` server-side — we return null
|
|
316
|
+
* here to indicate "no direct share" and let the caller fall back if
|
|
317
|
+
* needed. Phase 2 ships with direct user-shares only.
|
|
318
|
+
*/
|
|
319
|
+
async ResolveEffectivePermission(listId) {
|
|
320
|
+
try {
|
|
321
|
+
const md = this.metadata();
|
|
322
|
+
const list = await md.GetEntityObject('MJ: Lists', this.contextUser);
|
|
323
|
+
const loaded = await list.Load(listId);
|
|
324
|
+
if (!loaded)
|
|
325
|
+
return null;
|
|
326
|
+
if (list.UserID === this.contextUser.ID)
|
|
327
|
+
return 'Owner';
|
|
328
|
+
const resourceTypeId = await this.getListResourceTypeId();
|
|
329
|
+
const rv = this.runView();
|
|
330
|
+
const result = await rv.RunView({
|
|
331
|
+
EntityName: 'MJ: Resource Permissions',
|
|
332
|
+
ExtraFilter: `ResourceTypeID='${resourceTypeId}' AND ResourceRecordID='${this.escape(listId)}' AND Type='User' AND UserID='${this.contextUser.ID}' AND Status='Approved'`,
|
|
333
|
+
ResultType: 'entity_object',
|
|
334
|
+
}, this.contextUser);
|
|
335
|
+
if (!result.Success || !result.Results || result.Results.length === 0)
|
|
336
|
+
return null;
|
|
337
|
+
return (result.Results[0].PermissionLevel ?? null);
|
|
338
|
+
}
|
|
339
|
+
catch (e) {
|
|
340
|
+
LogError(`ResolveEffectivePermission failed for list ${listId}: ${e instanceof Error ? e.message : String(e)}`);
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Return all lists currently shared with the context user. Role-based
|
|
346
|
+
* shares are deferred to a future iteration — Phase 2 ships with
|
|
347
|
+
* direct user-level shares only.
|
|
348
|
+
*/
|
|
349
|
+
async GetListsSharedWithUser() {
|
|
350
|
+
const resourceTypeId = await this.getListResourceTypeId();
|
|
351
|
+
const rv = this.runView();
|
|
352
|
+
const result = await rv.RunView({
|
|
353
|
+
EntityName: 'MJ: Resource Permissions',
|
|
354
|
+
ExtraFilter: `ResourceTypeID='${resourceTypeId}' AND Type='User' AND UserID='${this.contextUser.ID}' AND Status='Approved'`,
|
|
355
|
+
OrderBy: '__mj_CreatedAt DESC',
|
|
356
|
+
ResultType: 'entity_object',
|
|
357
|
+
}, this.contextUser);
|
|
358
|
+
if (!result.Success || !result.Results)
|
|
359
|
+
return [];
|
|
360
|
+
// Resolve list names in one pass via a single RunView. Avoids an N+1.
|
|
361
|
+
const listIds = result.Results.map((p) => p.ResourceRecordID);
|
|
362
|
+
if (listIds.length === 0)
|
|
363
|
+
return [];
|
|
364
|
+
const filter = listIds.map((id) => `'${this.escape(id)}'`).join(',');
|
|
365
|
+
const listLookup = await rv.RunView({
|
|
366
|
+
EntityName: 'MJ: Lists',
|
|
367
|
+
ExtraFilter: `ID IN (${filter})`,
|
|
368
|
+
Fields: ['ID', 'Name'],
|
|
369
|
+
ResultType: 'simple',
|
|
370
|
+
}, this.contextUser);
|
|
371
|
+
const nameById = new Map();
|
|
372
|
+
if (listLookup.Success && listLookup.Results) {
|
|
373
|
+
for (const row of listLookup.Results) {
|
|
374
|
+
nameById.set(String(row.ID), String(row.Name));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return result.Results.map((p) => ({
|
|
378
|
+
ListID: p.ResourceRecordID,
|
|
379
|
+
ListName: nameById.get(p.ResourceRecordID) ?? '(unknown list)',
|
|
380
|
+
PermissionLevel: (p.PermissionLevel ?? 'View'),
|
|
381
|
+
SharedByUserID: p.SharedByUserID,
|
|
382
|
+
SharedAt: p.__mj_CreatedAt,
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
// ---------------------------------------------------------------------
|
|
386
|
+
// Private helpers
|
|
387
|
+
// ---------------------------------------------------------------------
|
|
388
|
+
async findExistingPermission(listId, target) {
|
|
389
|
+
const rv = this.runView();
|
|
390
|
+
const targetClause = target.kind === 'user'
|
|
391
|
+
? `Type='User' AND UserID='${this.escape(target.userId)}'`
|
|
392
|
+
: `Type='Role' AND RoleID='${this.escape(target.roleId)}'`;
|
|
393
|
+
const resourceTypeId = await this.getListResourceTypeId();
|
|
394
|
+
const result = await rv.RunView({
|
|
395
|
+
EntityName: 'MJ: Resource Permissions',
|
|
396
|
+
ExtraFilter: `ResourceTypeID='${resourceTypeId}' AND ResourceRecordID='${this.escape(listId)}' AND ${targetClause}`,
|
|
397
|
+
ResultType: 'entity_object',
|
|
398
|
+
}, this.contextUser);
|
|
399
|
+
if (!result.Success || !result.Results || result.Results.length === 0)
|
|
400
|
+
return null;
|
|
401
|
+
return result.Results[0];
|
|
402
|
+
}
|
|
403
|
+
toShareSummary(p) {
|
|
404
|
+
const target = p.Type === 'User'
|
|
405
|
+
? { kind: 'user', userId: p.UserID }
|
|
406
|
+
: { kind: 'role', roleId: p.RoleID };
|
|
407
|
+
return {
|
|
408
|
+
PermissionID: p.ID,
|
|
409
|
+
ListID: p.ResourceRecordID,
|
|
410
|
+
Target: target,
|
|
411
|
+
PermissionLevel: (p.PermissionLevel ?? 'View'),
|
|
412
|
+
Status: p.Status,
|
|
413
|
+
SharedByUserID: p.SharedByUserID,
|
|
414
|
+
CreatedAt: p.__mj_CreatedAt,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
snapshotTarget(p) {
|
|
418
|
+
return {
|
|
419
|
+
TargetKind: p.Type === 'User' ? 'user' : 'role',
|
|
420
|
+
TargetID: p.Type === 'User' ? p.UserID : p.RoleID,
|
|
421
|
+
Level: p.PermissionLevel,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
targetId(target) {
|
|
425
|
+
return target.kind === 'user' ? target.userId : target.roleId;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Generate a 32-byte URL-safe random token for invitations. 256 bits of
|
|
429
|
+
* entropy is overkill for a 7-day TTL but cheap, and keeps the token
|
|
430
|
+
* unguessable even if the database leaks Statuses/Emails separately.
|
|
431
|
+
*
|
|
432
|
+
* Uses the Web Crypto API (`globalThis.crypto.getRandomValues`) so the
|
|
433
|
+
* function works identically in Node 19+ and browser bundles — without
|
|
434
|
+
* the `node:crypto` import that breaks browser-side bundling.
|
|
435
|
+
*/
|
|
436
|
+
generateInvitationToken() {
|
|
437
|
+
const bytes = new Uint8Array(32);
|
|
438
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
439
|
+
// Base64-URL: standard base64 then swap `+` `/` and strip `=` padding.
|
|
440
|
+
let binary = '';
|
|
441
|
+
for (let i = 0; i < bytes.length; i++)
|
|
442
|
+
binary += String.fromCharCode(bytes[i]);
|
|
443
|
+
const b64 = (typeof btoa === 'function' ? btoa(binary) : Buffer.from(bytes).toString('base64'));
|
|
444
|
+
return b64.replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Resolve the `MJ: Resource Types.ID` for the row named "Lists" via the
|
|
448
|
+
* shared `ResourceTypeEngine`. Throws if missing — that indicates a
|
|
449
|
+
* misconfigured environment (seed data never pushed) and we'd rather
|
|
450
|
+
* fail loud than write `MJResourcePermission` rows with an undefined
|
|
451
|
+
* `ResourceTypeID`. Engine `Config()` is idempotent.
|
|
452
|
+
*/
|
|
453
|
+
async getListResourceTypeId() {
|
|
454
|
+
await ResourceTypeEngine.Instance.Config(false, this.contextUser, this.provider);
|
|
455
|
+
const rt = ResourceTypeEngine.Instance.ByName(LIST_RESOURCE_TYPE_NAME);
|
|
456
|
+
if (!rt) {
|
|
457
|
+
throw new Error(`MJ: Resource Types row "${LIST_RESOURCE_TYPE_NAME}" not found. ` +
|
|
458
|
+
`Seed metadata/resource-types/ via 'mj sync push' before using ListSharing.`);
|
|
459
|
+
}
|
|
460
|
+
return rt.ID;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Resolve an `MJ: Audit Log Types.ID` by row name via the shared
|
|
464
|
+
* `AuditLogTypeEngine`. Same fail-loud contract as
|
|
465
|
+
* {@link getListResourceTypeId}.
|
|
466
|
+
*/
|
|
467
|
+
async getAuditLogTypeId(name) {
|
|
468
|
+
await AuditLogTypeEngine.Instance.Config(false, this.contextUser, this.provider);
|
|
469
|
+
const t = AuditLogTypeEngine.Instance.ByName(name);
|
|
470
|
+
if (!t) {
|
|
471
|
+
throw new Error(`MJ: Audit Log Types row "${name}" not found. ` +
|
|
472
|
+
`Seed metadata/audit-log-types/ via 'mj sync push' before using ListSharing.`);
|
|
473
|
+
}
|
|
474
|
+
return t.ID;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Best-effort audit-log emit. We never fail the parent operation on
|
|
478
|
+
* an audit-log failure; instead we LogError so the discrepancy is
|
|
479
|
+
* observable. Cross-cutting reliability concern.
|
|
480
|
+
*
|
|
481
|
+
* Takes the audit-log-type by NAME so call sites are grep-able and
|
|
482
|
+
* resilient to seed-UUID changes. Name → ID resolution goes through the
|
|
483
|
+
* `AuditLogTypeEngine`.
|
|
484
|
+
*/
|
|
485
|
+
async emitAuditLog(args) {
|
|
486
|
+
try {
|
|
487
|
+
const typeId = await this.getAuditLogTypeId(args.Type);
|
|
488
|
+
const md = this.metadata();
|
|
489
|
+
const log = await md.GetEntityObject('MJ: Audit Logs', this.contextUser);
|
|
490
|
+
log.NewRecord();
|
|
491
|
+
log.UserID = this.contextUser.ID;
|
|
492
|
+
log.AuditLogTypeID = typeId;
|
|
493
|
+
log.Status = 'Success';
|
|
494
|
+
log.Description = args.Description;
|
|
495
|
+
log.Details = JSON.stringify(args.Details);
|
|
496
|
+
// EntityID for MJ: Lists via the O(1) by-name map. Entities.find
|
|
497
|
+
// is an O(N) scan and the documented anti-pattern.
|
|
498
|
+
const listEntity = md.EntityByName('MJ: Lists');
|
|
499
|
+
if (listEntity)
|
|
500
|
+
log.EntityID = listEntity.ID;
|
|
501
|
+
log.RecordID = args.ListID;
|
|
502
|
+
const ok = await log.Save();
|
|
503
|
+
if (!ok) {
|
|
504
|
+
LogError(`Audit log save failed: ${log.LatestResult?.CompleteMessage ?? 'unknown'}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
catch (e) {
|
|
508
|
+
LogError(`emitAuditLog threw: ${e instanceof Error ? e.message : String(e)}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Fire-and-forget share notification via the platform-wide dispatcher.
|
|
513
|
+
* If the recipient has notification preferences for email, MJServer's
|
|
514
|
+
* registered handler will send an email; if they prefer in-app only,
|
|
515
|
+
* a `MJ: User Notifications` row is written instead. Either way the
|
|
516
|
+
* share itself has already succeeded — a notification failure is logged
|
|
517
|
+
* (inside `CreateShareNotification`) but does not propagate.
|
|
518
|
+
*
|
|
519
|
+
* Pre-condition: this is only called for `Target.kind === 'user'` and
|
|
520
|
+
* never for self-shares (the caller guards both).
|
|
521
|
+
*/
|
|
522
|
+
async dispatchShareNotification(listId, granteeUserId, level) {
|
|
523
|
+
try {
|
|
524
|
+
const md = this.metadata();
|
|
525
|
+
// Resolve the list name for the notification copy. Best-effort — if
|
|
526
|
+
// it fails, the dispatcher falls back to a generic "shared a list".
|
|
527
|
+
let listName;
|
|
528
|
+
try {
|
|
529
|
+
const list = await md.GetEntityObject('MJ: Lists', this.contextUser);
|
|
530
|
+
const loaded = await list.Load(listId);
|
|
531
|
+
if (loaded)
|
|
532
|
+
listName = list.Name;
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
// ignore — listName stays undefined
|
|
536
|
+
}
|
|
537
|
+
await CreateShareNotification({
|
|
538
|
+
Provider: this.metadata(),
|
|
539
|
+
ContextUser: this.contextUser,
|
|
540
|
+
GrantorUserID: this.contextUser.ID,
|
|
541
|
+
GranteeUserID: granteeUserId,
|
|
542
|
+
ResourceTypeLabel: 'List',
|
|
543
|
+
ResourceTypeName: 'Lists',
|
|
544
|
+
ResourceName: listName,
|
|
545
|
+
ResourceRecordID: listId,
|
|
546
|
+
ActionsSummary: `granted ${level} access`,
|
|
547
|
+
ExtraConfiguration: { PermissionLevel: level },
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
catch (e) {
|
|
551
|
+
LogError(`dispatchShareNotification failed for list ${listId}: ${e instanceof Error ? e.message : String(e)}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
shareFailure(code, message) {
|
|
555
|
+
return { Success: false, ResultCode: code, Message: message };
|
|
556
|
+
}
|
|
557
|
+
/** Escape a value for inline-literal use in `ExtraFilter`. */
|
|
558
|
+
escape(value) {
|
|
559
|
+
return value.replace(/'/g, "''");
|
|
560
|
+
}
|
|
561
|
+
metadata() {
|
|
562
|
+
return this.provider ?? new Metadata();
|
|
563
|
+
}
|
|
564
|
+
runView() {
|
|
565
|
+
return this.provider ? RunView.FromMetadataProvider(this.provider) : new RunView();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
//# sourceMappingURL=ListSharing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListSharing.js","sourceRoot":"","sources":["../src/ListSharing.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-signed token attached to every `ListDelta`. The token is the only
|
|
3
|
+
* thing `ApplyDelta` will accept as proof that:
|
|
4
|
+
* - the preview was produced server-side (not forged by a client),
|
|
5
|
+
* - the preview describes a known target + source pairing,
|
|
6
|
+
* - the preview is fresh (within `TOKEN_TTL_MS`).
|
|
7
|
+
*
|
|
8
|
+
* Token format is `<base64url(payload-json)>.<base64url(hmac-sha256)>`. We
|
|
9
|
+
* deliberately avoid a JWT dependency — the payload shape is fixed, the
|
|
10
|
+
* algorithm is fixed, and we own both ends.
|
|
11
|
+
*
|
|
12
|
+
* Uses Node's built-in `node:crypto`. This module is server-only — clients
|
|
13
|
+
* never sign or verify tokens (they receive them inside a `ListDelta` and
|
|
14
|
+
* hand them back unchanged to `ApplyDelta`).
|
|
15
|
+
*
|
|
16
|
+
* Public functions remain async for backwards compatibility with callers
|
|
17
|
+
* that already `await` them; the underlying node:crypto APIs are synchronous,
|
|
18
|
+
* which is fine — HMAC on a tiny payload is sub-millisecond.
|
|
19
|
+
*/
|
|
20
|
+
import { type DeltaTokenError, type DeltaTokenPayload } from '@memberjunction/lists-base';
|
|
21
|
+
export declare class DeltaTokenVerificationError extends Error {
|
|
22
|
+
readonly Code: DeltaTokenError;
|
|
23
|
+
constructor(code: DeltaTokenError, message: string);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Produce a deterministic fingerprint of a record-ID set. Used as the `ssig`
|
|
27
|
+
* field on the token payload — server-side `ApplyDelta` recomputes the
|
|
28
|
+
* fingerprint of the freshly resolved source and compares against the token
|
|
29
|
+
* to detect `STALE_DELTA`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function ComputeSourceSignature(recordIds: readonly string[]): Promise<string>;
|
|
32
|
+
export declare function SetDeltaTokenSecret(secret: string | undefined): void;
|
|
33
|
+
/**
|
|
34
|
+
* Sign a payload. Callers should pass `iat: Date.now()` and an `ssig`
|
|
35
|
+
* computed via `ComputeSourceSignature`. Returns the wire form.
|
|
36
|
+
*/
|
|
37
|
+
export declare function SignDeltaToken(payload: DeltaTokenPayload): Promise<string>;
|
|
38
|
+
/**
|
|
39
|
+
* Verify a token: checks structural shape, HMAC, and TTL. Returns the parsed
|
|
40
|
+
* payload on success; throws `DeltaTokenVerificationError` otherwise.
|
|
41
|
+
*
|
|
42
|
+
* `timingSafeEqual` compares in time independent of the mismatch position,
|
|
43
|
+
* so a token-fuzzing attacker can't learn the secret from comparison-time
|
|
44
|
+
* side channels.
|
|
45
|
+
*/
|
|
46
|
+
export declare function VerifyDeltaToken(token: string, now?: number): Promise<DeltaTokenPayload>;
|
|
47
|
+
//# sourceMappingURL=deltaToken.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deltaToken.d.ts","sourceRoot":"","sources":["../src/deltaToken.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACvB,MAAM,4BAA4B,CAAC;AAEpC,qBAAa,2BAA4B,SAAQ,KAAK;IACpD,SAAgB,IAAI,EAAE,eAAe,CAAC;gBAC1B,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM;CAInD;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAG1F;AASD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAEpE;AA8BD;;;GAGG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAKhF;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAsC1G"}
|