@peers-app/peers-sdk 0.27.4 → 0.28.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/dist/context/user-context.d.ts +7 -1
- package/dist/context/user-context.js +4 -9
- package/dist/contracts/contract-proxy.d.ts +5 -0
- package/dist/contracts/contract-proxy.js +2 -2
- package/dist/contracts/contract-resolvers.d.ts +2 -2
- package/dist/contracts/contract-resolvers.js +14 -15
- package/dist/contracts/contract-transport.d.ts +2 -1
- package/dist/contracts/contract-transport.js +24 -2
- package/dist/contracts/system/network-contracts.d.ts +1 -1
- package/dist/contracts/tool-catalog-service.js +1 -1
- package/dist/contracts/tool-catalog.js +6 -2
- package/dist/data/assistants.d.ts +3 -3
- package/dist/data/authority-write-checks.d.ts +42 -0
- package/dist/data/authority-write-checks.js +324 -0
- package/dist/data/change-tracking.d.ts +4 -4
- package/dist/data/create-group.js +1 -0
- package/dist/data/devices.d.ts +27 -4
- package/dist/data/devices.js +31 -0
- package/dist/data/group-authority.d.ts +111 -0
- package/dist/data/group-authority.js +281 -0
- package/dist/data/group-members.d.ts +8 -2
- package/dist/data/group-members.js +41 -18
- package/dist/data/group-permissions.d.ts +15 -31
- package/dist/data/group-permissions.js +25 -95
- package/dist/data/group-resign.d.ts +38 -0
- package/dist/data/group-resign.js +71 -0
- package/dist/data/groups.d.ts +11 -0
- package/dist/data/groups.js +31 -13
- package/dist/data/index.d.ts +3 -0
- package/dist/data/index.js +3 -0
- package/dist/data/invites.d.ts +14 -0
- package/dist/data/orm/index.d.ts +1 -0
- package/dist/data/orm/index.js +1 -0
- package/dist/data/orm/sql.data-source.d.ts +2 -2
- package/dist/data/orm/sql.data-source.js +8 -9
- package/dist/data/orm/table-container.js +14 -8
- package/dist/data/orm/write-checks.d.ts +89 -0
- package/dist/data/orm/write-checks.js +115 -0
- package/dist/data/package-table-identity.d.ts +6 -0
- package/dist/data/package-table-identity.js +51 -0
- package/dist/data/package-version-resolver.d.ts +15 -0
- package/dist/data/package-version-resolver.js +58 -3
- package/dist/data/package-versions.d.ts +20 -14
- package/dist/data/package-versions.js +15 -9
- package/dist/data/packages.d.ts +6 -1
- package/dist/data/packages.js +15 -20
- package/dist/data/persistent-vars.js +2 -39
- package/dist/data/sync-recovery-queue.d.ts +4 -4
- package/dist/data/sync-recovery-queue.js +7 -1
- package/dist/data/tool-access-approvals.d.ts +3 -3
- package/dist/data/user-permissions.d.ts +33 -2
- package/dist/data/user-permissions.js +87 -10
- package/dist/data/users.d.ts +49 -12
- package/dist/data/users.js +21 -1
- package/dist/data/workflows.d.ts +1 -1
- package/dist/device/binary-peer-connection-v2.d.ts +8 -16
- package/dist/device/binary-peer-connection-v2.js +15 -31
- package/dist/device/connection.d.ts +35 -44
- package/dist/device/connection.js +188 -186
- package/dist/device/device.d.ts +1 -8
- package/dist/device/device.js +1 -30
- package/dist/device/frame-cipher.d.ts +1 -1
- package/dist/device/frame-cipher.js +1 -1
- package/dist/device/get-trust-level-fn.js +3 -6
- package/dist/device/socket.type.d.ts +6 -12
- package/dist/device-pairing/types.d.ts +26 -26
- package/dist/identity/user-row-change.d.ts +3 -1
- package/dist/identity/user-row-change.js +33 -18
- package/dist/invites/group-approval.js +23 -24
- package/dist/invites/invite-messages.d.ts +250 -28
- package/dist/invites/invite-token.d.ts +18 -0
- package/dist/invites/invite-token.js +2 -0
- package/dist/keys.d.ts +7 -0
- package/dist/keys.js +27 -0
- package/dist/package-installer/package-installer.d.ts +1 -1
- package/dist/package-installer/package-installer.js +8 -3
- package/dist/package-installer/package-propagation.d.ts +2 -0
- package/dist/package-installer/package-propagation.js +20 -38
- package/dist/package-installer/package-remote-checker.js +15 -3
- package/dist/package-loader/isolated-artifact.js +0 -2
- package/dist/package-loader/package-loader.js +6 -0
- package/dist/types/peer-device.d.ts +2 -0
- package/dist/types/workflow.d.ts +2 -2
- package/package.json +1 -1
- package/dist/data/package-permissions.d.ts +0 -14
- package/dist/data/package-permissions.js +0 -26
- package/dist/data/package-version-permissions.d.ts +0 -14
- package/dist/data/package-version-permissions.js +0 -30
- package/dist/device/binary-peer-connection.d.ts +0 -58
- package/dist/device/binary-peer-connection.js +0 -516
- package/dist/device/streamed-socket.d.ts +0 -23
- package/dist/device/streamed-socket.js +0 -255
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeGroupAuthority = computeGroupAuthority;
|
|
4
|
+
exports.roleInGroupAuthority = roleInGroupAuthority;
|
|
5
|
+
exports.groupSigner = groupSigner;
|
|
6
|
+
exports.getGroupAuthority = getGroupAuthority;
|
|
7
|
+
exports.invalidateGroupAuthority = invalidateGroupAuthority;
|
|
8
|
+
const keys_1 = require("../keys");
|
|
9
|
+
const group_member_roles_1 = require("./group-member-roles");
|
|
10
|
+
const user_permissions_1 = require("./user-permissions");
|
|
11
|
+
/**
|
|
12
|
+
* Build a group's {@link IGroupAuthority} from its rows. Pure and deterministic: the same rows
|
|
13
|
+
* give the same roles on every device, whatever order they arrived in.
|
|
14
|
+
*
|
|
15
|
+
* - **Profiles:** `users` maps each user to the profile row that settles their keys: the first
|
|
16
|
+
* row for a user pins the keys (an unsigned contact stub included) and a self-signed row with
|
|
17
|
+
* the same keys verifies them.
|
|
18
|
+
* - **Founder:** from `pinnedGroup`. Without a personal pin no authority counts. A group row
|
|
19
|
+
* naming a different founder counts only if the pinned founder signed it.
|
|
20
|
+
* - **Members:** starting from the founder, a membership counts when its signature is valid
|
|
21
|
+
* and its signer already counts as at least Admin and at least the role granted. Repeated
|
|
22
|
+
* until nothing changes, so a row can't justify itself or another row that justifies it.
|
|
23
|
+
* Deleted rows don't count.
|
|
24
|
+
* - **Keys:** a role belongs to a key, not only a user id. The founder's key is their verified
|
|
25
|
+
* profile row's. A membership names the member's key: it counts for that key, or for the
|
|
26
|
+
* key the member's verified profile row has if that row shows the named key handing over to
|
|
27
|
+
* it ({@link keySucceeds}). A profile row with any other key (a namesake a member forged on a
|
|
28
|
+
* device that had never seen the real one) stops the grant counting rather than inheriting
|
|
29
|
+
* it. Signatures count only by these keys.
|
|
30
|
+
* - **Group settings:** the group row's `publicRole` counts only when that row is signed by the
|
|
31
|
+
* founder or a counted Admin; otherwise no public access is granted.
|
|
32
|
+
*
|
|
33
|
+
* @param input - The group's rows.
|
|
34
|
+
*/
|
|
35
|
+
function computeGroupAuthority(input) {
|
|
36
|
+
const { groupId, pinnedGroup, group } = input;
|
|
37
|
+
const users = verifiedUsers(input.users);
|
|
38
|
+
const keys = new Map();
|
|
39
|
+
const pinnedFounder = pinnedGroup?.groupId === groupId ? pinnedGroup.founderUserId : undefined;
|
|
40
|
+
const groupSigningKey = group && group.groupId === groupId ? (0, keys_1.signedByKey)(group) : undefined;
|
|
41
|
+
const signedByPinnedFounder = !!pinnedFounder && !!groupSigningKey && groupSigningKey === users.get(pinnedFounder)?.publicKey;
|
|
42
|
+
let founderUserId = pinnedFounder;
|
|
43
|
+
if (group && founderUserId && group.founderUserId !== founderUserId && signedByPinnedFounder) {
|
|
44
|
+
founderUserId = group.founderUserId;
|
|
45
|
+
}
|
|
46
|
+
const founderKey = founderUserId && users.get(founderUserId)?.publicKey;
|
|
47
|
+
if (founderUserId && founderKey)
|
|
48
|
+
keys.set(founderUserId, founderKey);
|
|
49
|
+
const roles = new Map();
|
|
50
|
+
if (founderUserId)
|
|
51
|
+
roles.set(founderUserId, group_member_roles_1.GroupMemberRole.Founder);
|
|
52
|
+
const pending = input.members
|
|
53
|
+
.filter((member) => member.groupId === groupId && !member.deleted)
|
|
54
|
+
.filter((member) => member.userId !== founderUserId)
|
|
55
|
+
.sort((a, b) => a.role - b.role ||
|
|
56
|
+
(a.groupMemberId < b.groupMemberId ? -1 : a.groupMemberId > b.groupMemberId ? 1 : 0));
|
|
57
|
+
const memberKey = (member) => {
|
|
58
|
+
if (!member.publicKey)
|
|
59
|
+
return undefined;
|
|
60
|
+
const profile = users.get(member.userId);
|
|
61
|
+
if (!profile)
|
|
62
|
+
return member.publicKey;
|
|
63
|
+
return (0, user_permissions_1.keySucceeds)(profile, member.publicKey) ? profile.publicKey : undefined;
|
|
64
|
+
};
|
|
65
|
+
let changed = true;
|
|
66
|
+
while (changed) {
|
|
67
|
+
changed = false;
|
|
68
|
+
for (const member of pending) {
|
|
69
|
+
if (roles.has(member.userId))
|
|
70
|
+
continue;
|
|
71
|
+
const signerRole = groupSigner({ keys, roles }, member)?.signerRole;
|
|
72
|
+
if (signerRole === undefined ||
|
|
73
|
+
signerRole < group_member_roles_1.GroupMemberRole.Admin ||
|
|
74
|
+
signerRole < member.role) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const key = memberKey(member);
|
|
78
|
+
if (member.role > group_member_roles_1.GroupMemberRole.None) {
|
|
79
|
+
if (!key)
|
|
80
|
+
continue;
|
|
81
|
+
keys.set(member.userId, key);
|
|
82
|
+
}
|
|
83
|
+
roles.set(member.userId, member.role);
|
|
84
|
+
changed = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const groupSignerRole = group ? groupSigner({ keys, roles }, group)?.signerRole : undefined;
|
|
88
|
+
const groupRowAccepted = !!group &&
|
|
89
|
+
group.groupId === groupId &&
|
|
90
|
+
group.founderUserId === founderUserId &&
|
|
91
|
+
(signedByPinnedFounder ||
|
|
92
|
+
(groupSignerRole !== undefined &&
|
|
93
|
+
groupSignerRole >= group_member_roles_1.GroupMemberRole.Admin &&
|
|
94
|
+
groupSignerRole >= (group.publicRole ?? group_member_roles_1.GroupMemberRole.None)));
|
|
95
|
+
return {
|
|
96
|
+
groupId,
|
|
97
|
+
founderUserId,
|
|
98
|
+
publicRole: groupRowAccepted
|
|
99
|
+
? (group.publicRole ?? group_member_roles_1.GroupMemberRole.None)
|
|
100
|
+
: group_member_roles_1.GroupMemberRole.None,
|
|
101
|
+
roles,
|
|
102
|
+
keys,
|
|
103
|
+
group: groupRowAccepted ? group : undefined,
|
|
104
|
+
users,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A user's role under `authority`: their counted membership, else the group's public role.
|
|
109
|
+
*
|
|
110
|
+
* Pass `publicKey` whenever the user is a remote caller: the role then counts only if it
|
|
111
|
+
* belongs to the key the caller authenticated with ({@link IGroupAuthority.keys}). Otherwise a
|
|
112
|
+
* device that proved a namesake's key somewhere else (another group's copy of a forged profile)
|
|
113
|
+
* would get the member's role here. A membership with role None applies whatever the key.
|
|
114
|
+
*
|
|
115
|
+
* @param authority - From {@link computeGroupAuthority} or {@link getGroupAuthority}.
|
|
116
|
+
* @param userId - User whose role is requested.
|
|
117
|
+
* @param publicKey - The signing key the caller authenticated with, when it is remote.
|
|
118
|
+
*/
|
|
119
|
+
function roleInGroupAuthority(authority, userId, publicKey) {
|
|
120
|
+
const role = authority.roles.get(userId);
|
|
121
|
+
if (role === undefined)
|
|
122
|
+
return authority.publicRole;
|
|
123
|
+
if (role > group_member_roles_1.GroupMemberRole.None &&
|
|
124
|
+
publicKey !== undefined &&
|
|
125
|
+
authority.keys.get(userId) !== publicKey) {
|
|
126
|
+
return authority.publicRole;
|
|
127
|
+
}
|
|
128
|
+
return role;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Each user's profile row that settles their keys, from self-signed Users rows. The first row
|
|
132
|
+
* for a user pins the keys, including an unsigned contact stub; a self-signed row with the same
|
|
133
|
+
* keys verifies them.
|
|
134
|
+
*
|
|
135
|
+
* @param users - Users rows, most trusted first.
|
|
136
|
+
*/
|
|
137
|
+
function verifiedUsers(users) {
|
|
138
|
+
const verified = new Map();
|
|
139
|
+
const pins = new Map();
|
|
140
|
+
for (const user of users) {
|
|
141
|
+
if (!user.publicKey)
|
|
142
|
+
continue;
|
|
143
|
+
const pin = pins.get(user.userId) ?? user;
|
|
144
|
+
pins.set(user.userId, pin);
|
|
145
|
+
if (!(0, user_permissions_1.sameUserIdentity)(user, pin))
|
|
146
|
+
continue;
|
|
147
|
+
if (verified.has(user.userId) || !(0, user_permissions_1.isSelfSignedUser)(user))
|
|
148
|
+
continue;
|
|
149
|
+
verified.set(user.userId, user);
|
|
150
|
+
}
|
|
151
|
+
return verified;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The counted member who signed `row` and their role: its signature verifies with the key
|
|
155
|
+
* their role belongs to under `authority`. Undefined for anyone else, whatever their profile says.
|
|
156
|
+
*
|
|
157
|
+
* @param authority - The group's verified authority.
|
|
158
|
+
* @param row - A signed row.
|
|
159
|
+
*/
|
|
160
|
+
function groupSigner(authority, row) {
|
|
161
|
+
const key = (0, keys_1.signedByKey)(row);
|
|
162
|
+
if (!key)
|
|
163
|
+
return undefined;
|
|
164
|
+
for (const [userId, memberKey] of authority.keys) {
|
|
165
|
+
if (memberKey === key) {
|
|
166
|
+
return { signer: userId, signerRole: authority.roles.get(userId) ?? group_member_roles_1.GroupMemberRole.None };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
const caches = new WeakMap();
|
|
172
|
+
const subscribed = new WeakMap();
|
|
173
|
+
/**
|
|
174
|
+
* The verified authority for `groupId`, cached per user context and rebuilt after any change
|
|
175
|
+
* to that group's Groups, GroupMembers or Users rows, or to the user's own Groups or Users rows.
|
|
176
|
+
*
|
|
177
|
+
* @param userContext - Context that owns the group.
|
|
178
|
+
* @param groupId - Group id (never the personal context).
|
|
179
|
+
*/
|
|
180
|
+
async function getGroupAuthority(userContext, groupId) {
|
|
181
|
+
let byGroup = caches.get(userContext);
|
|
182
|
+
if (!byGroup) {
|
|
183
|
+
byGroup = new Map();
|
|
184
|
+
caches.set(userContext, byGroup);
|
|
185
|
+
}
|
|
186
|
+
let entry = byGroup.get(groupId);
|
|
187
|
+
if (!entry) {
|
|
188
|
+
entry = { generation: 0 };
|
|
189
|
+
byGroup.set(groupId, entry);
|
|
190
|
+
}
|
|
191
|
+
const dataContext = userContext.getDataContext(groupId);
|
|
192
|
+
if (entry.dataContext !== dataContext) {
|
|
193
|
+
invalidateGroupAuthority(userContext, groupId);
|
|
194
|
+
entry.dataContext = dataContext;
|
|
195
|
+
}
|
|
196
|
+
if (entry.loaded)
|
|
197
|
+
return entry.loaded;
|
|
198
|
+
if (entry.loading)
|
|
199
|
+
return entry.loading;
|
|
200
|
+
const cache = entry;
|
|
201
|
+
const generation = cache.generation;
|
|
202
|
+
const loading = (async () => {
|
|
203
|
+
await subscribeToAuthorityRows(userContext, groupId);
|
|
204
|
+
const loaded = await readGroupAuthority(userContext, groupId);
|
|
205
|
+
// Invalidation during the asynchronous reads also invalidates this caller's result.
|
|
206
|
+
if (cache.generation !== generation)
|
|
207
|
+
return getGroupAuthority(userContext, groupId);
|
|
208
|
+
cache.loaded = loaded;
|
|
209
|
+
return loaded;
|
|
210
|
+
})();
|
|
211
|
+
cache.loading = loading;
|
|
212
|
+
try {
|
|
213
|
+
return await loading;
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
if (cache.loading === loading)
|
|
217
|
+
cache.loading = undefined;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** Drop cached authority for one group, or every group of `userContext`. */
|
|
221
|
+
function invalidateGroupAuthority(userContext, groupId) {
|
|
222
|
+
const byGroup = caches.get(userContext);
|
|
223
|
+
if (!byGroup)
|
|
224
|
+
return;
|
|
225
|
+
const entries = groupId ? [byGroup.get(groupId)] : [...byGroup.values()];
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
if (!entry)
|
|
228
|
+
continue;
|
|
229
|
+
entry.generation++;
|
|
230
|
+
entry.loaded = undefined;
|
|
231
|
+
entry.loading = undefined;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function readGroupAuthority(userContext, groupId) {
|
|
235
|
+
const { Groups } = await Promise.resolve().then(() => require("./groups"));
|
|
236
|
+
const { GroupMembers } = await Promise.resolve().then(() => require("./group-members"));
|
|
237
|
+
const { Users } = await Promise.resolve().then(() => require("./users"));
|
|
238
|
+
const groupContext = userContext.getDataContext(groupId);
|
|
239
|
+
const [pinnedGroup, group, members, personalUsers, groupUsers] = await Promise.all([
|
|
240
|
+
Groups(userContext.userDataContext).get(groupId),
|
|
241
|
+
Groups(groupContext).get(groupId),
|
|
242
|
+
GroupMembers(groupContext).list({ groupId }),
|
|
243
|
+
Users(userContext.userDataContext).list(),
|
|
244
|
+
Users(groupContext).list(),
|
|
245
|
+
]);
|
|
246
|
+
return computeGroupAuthority({
|
|
247
|
+
groupId,
|
|
248
|
+
pinnedGroup,
|
|
249
|
+
group,
|
|
250
|
+
members,
|
|
251
|
+
users: [...personalUsers, ...groupUsers],
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async function subscribeToAuthorityRows(userContext, groupId) {
|
|
255
|
+
let groups = subscribed.get(userContext);
|
|
256
|
+
if (!groups) {
|
|
257
|
+
groups = new Map();
|
|
258
|
+
subscribed.set(userContext, groups);
|
|
259
|
+
}
|
|
260
|
+
const { Groups } = await Promise.resolve().then(() => require("./groups"));
|
|
261
|
+
const { GroupMembers } = await Promise.resolve().then(() => require("./group-members"));
|
|
262
|
+
const { Users } = await Promise.resolve().then(() => require("./users"));
|
|
263
|
+
for (const [key, dataContext] of [
|
|
264
|
+
["", userContext.userDataContext],
|
|
265
|
+
[groupId, userContext.getDataContext(groupId)],
|
|
266
|
+
]) {
|
|
267
|
+
const previous = groups.get(key);
|
|
268
|
+
if (previous?.dataContext === dataContext)
|
|
269
|
+
continue;
|
|
270
|
+
for (const subscription of previous?.subscriptions ?? [])
|
|
271
|
+
subscription.unsubscribe();
|
|
272
|
+
const invalidate = () => invalidateGroupAuthority(userContext, key || undefined);
|
|
273
|
+
const tables = key
|
|
274
|
+
? [Groups(dataContext), GroupMembers(dataContext), Users(dataContext)]
|
|
275
|
+
: [Groups(dataContext), Users(dataContext)];
|
|
276
|
+
groups.set(key, {
|
|
277
|
+
dataContext,
|
|
278
|
+
subscriptions: tables.map((table) => table.dataChanged.subscribe(invalidate)),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -9,7 +9,9 @@ export declare const groupMemberSchema: z.ZodObject<{
|
|
|
9
9
|
groupId: z.ZodEffects<z.ZodString, string, string>;
|
|
10
10
|
userId: z.ZodEffects<z.ZodString, string, string>;
|
|
11
11
|
role: z.ZodNativeEnum<typeof GroupMemberRole>;
|
|
12
|
+
publicKey: z.ZodOptional<z.ZodString>;
|
|
12
13
|
signature: z.ZodString;
|
|
14
|
+
signedAt: z.ZodOptional<z.ZodNumber>;
|
|
13
15
|
deleted: z.ZodOptional<z.ZodBoolean>;
|
|
14
16
|
}, "strip", z.ZodTypeAny, {
|
|
15
17
|
role: GroupMemberRole;
|
|
@@ -18,6 +20,8 @@ export declare const groupMemberSchema: z.ZodObject<{
|
|
|
18
20
|
groupId: string;
|
|
19
21
|
groupMemberId: string;
|
|
20
22
|
deleted?: boolean | undefined;
|
|
23
|
+
publicKey?: string | undefined;
|
|
24
|
+
signedAt?: number | undefined;
|
|
21
25
|
}, {
|
|
22
26
|
role: GroupMemberRole;
|
|
23
27
|
signature: string;
|
|
@@ -25,6 +29,8 @@ export declare const groupMemberSchema: z.ZodObject<{
|
|
|
25
29
|
groupId: string;
|
|
26
30
|
groupMemberId: string;
|
|
27
31
|
deleted?: boolean | undefined;
|
|
32
|
+
publicKey?: string | undefined;
|
|
33
|
+
signedAt?: number | undefined;
|
|
28
34
|
}>;
|
|
29
35
|
/** A signed association between a user and a group with an assigned role. */
|
|
30
36
|
export type IGroupMember = z.infer<typeof groupMemberSchema>;
|
|
@@ -40,8 +46,8 @@ export declare class GroupMembersTable extends Table<IGroupMember> {
|
|
|
40
46
|
* Settle two membership rows for the same user in the same group: devices that approved the
|
|
41
47
|
* same join at once could each write one, under a different id.
|
|
42
48
|
*
|
|
43
|
-
* Merged only when both rows grant the same access (role and deleted state), so nothing
|
|
44
|
-
* the member's access changes. The id {@link deterministicGroupMemberId} gives wins if either
|
|
49
|
+
* Merged only when both rows grant the same access (role, key and deleted state), so nothing
|
|
50
|
+
* about the member's access changes. The id {@link deterministicGroupMemberId} gives wins if either
|
|
45
51
|
* row has it, otherwise the lexicographically smaller id; the winner is kept as it is, signature
|
|
46
52
|
* included. Rows that disagree stay queued for a person. A pure function of the two rows, so
|
|
47
53
|
* every device keeps the same one.
|
|
@@ -41,12 +41,12 @@ const user_context_singleton_1 = require("../context/user-context-singleton");
|
|
|
41
41
|
const decorators_1 = require("../data/orm/decorators");
|
|
42
42
|
const zod_types_1 = require("../types/zod-types");
|
|
43
43
|
const utils_1 = require("../utils");
|
|
44
|
+
const authority_write_checks_1 = require("./authority-write-checks");
|
|
44
45
|
const group_member_roles_1 = require("./group-member-roles");
|
|
45
|
-
const group_permissions_1 = require("./group-permissions");
|
|
46
46
|
const table_1 = require("./orm/table");
|
|
47
47
|
const table_definitions_system_1 = require("./orm/table-definitions.system");
|
|
48
48
|
const types_1 = require("./orm/types");
|
|
49
|
-
const
|
|
49
|
+
const write_checks_1 = require("./orm/write-checks");
|
|
50
50
|
/** Schema for a signed group membership, including its role and soft deletion state. */
|
|
51
51
|
exports.groupMemberSchema = zod_1.z.object({
|
|
52
52
|
groupMemberId: zod_types_1.zodPeerId,
|
|
@@ -55,7 +55,15 @@ exports.groupMemberSchema = zod_1.z.object({
|
|
|
55
55
|
role: zod_1.z
|
|
56
56
|
.nativeEnum(group_member_roles_1.GroupMemberRole)
|
|
57
57
|
.describe('The role of the user in the group. Use "none" to explicitly block a user'),
|
|
58
|
+
publicKey: zod_1.z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("The member's signing key this grant is for. The role counts only for this key, or a key it handed over to (the member's keySuccession)"),
|
|
58
62
|
signature: zod_1.z.string().describe("The signed hash of this data excluding the signature itself"),
|
|
63
|
+
signedAt: zod_1.z
|
|
64
|
+
.number()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("When the signer signed this row (ms since epoch), under the signature: a copy of an earlier row can't replace a later one"),
|
|
59
67
|
deleted: zod_1.z
|
|
60
68
|
.boolean()
|
|
61
69
|
.optional()
|
|
@@ -70,6 +78,12 @@ const metaData = {
|
|
|
70
78
|
fields: (0, types_1.schemaToFields)(exports.groupMemberSchema),
|
|
71
79
|
indexes: [{ fields: ["groupId", "userId"], unique: true }],
|
|
72
80
|
};
|
|
81
|
+
/** A member's key as this device knows it: from their verified profile row. */
|
|
82
|
+
async function knownKeyOf(groupMember) {
|
|
83
|
+
const { getGroupAuthority } = await Promise.resolve().then(() => require("./group-authority"));
|
|
84
|
+
const { users } = await getGroupAuthority(await (0, user_context_singleton_1.getUserContext)(), groupMember.groupId);
|
|
85
|
+
return users.get(groupMember.userId)?.publicKey;
|
|
86
|
+
}
|
|
73
87
|
// Symbol.for keeps signer registration shared by separately installed SDK copies.
|
|
74
88
|
const signingKey = Symbol.for("@peers-app/peers-sdk/group-member-signing");
|
|
75
89
|
const signingGlobals = globalThis;
|
|
@@ -87,27 +101,35 @@ let GroupMembersTable = (() => {
|
|
|
87
101
|
}
|
|
88
102
|
static isPassthrough = false;
|
|
89
103
|
async save(groupMember, opts) {
|
|
90
|
-
if (GroupMembersTable.isPassthrough) {
|
|
91
|
-
|
|
104
|
+
if (!GroupMembersTable.isPassthrough) {
|
|
105
|
+
if (groupMember.groupId === (await (0, user_context_singleton_1.getUserContext)()).userId) {
|
|
106
|
+
throw new Error("Group members cannot be added to personal user contexts");
|
|
107
|
+
}
|
|
108
|
+
await (0, write_checks_1.checkLocalSave)(this, groupMember, authority_write_checks_1.checkGroupMember);
|
|
92
109
|
}
|
|
93
|
-
|
|
94
|
-
if (groupMember.groupId === me.userId) {
|
|
95
|
-
throw new Error("Group members cannot be added to personal user contexts");
|
|
96
|
-
}
|
|
97
|
-
const oldGroupMember = groupMember.groupMemberId
|
|
98
|
-
? await this.get(groupMember.groupMemberId)
|
|
99
|
-
: undefined;
|
|
100
|
-
if (await (0, group_permissions_1.isGroupMemberSignatureValid)(groupMember.groupId, groupMember, oldGroupMember)) {
|
|
101
|
-
return super.save(groupMember, opts);
|
|
102
|
-
}
|
|
103
|
-
throw new Error("Group member signature is not valid");
|
|
110
|
+
return super.save(groupMember, opts);
|
|
104
111
|
}
|
|
105
112
|
async signAndSave(groupMember, opts) {
|
|
106
113
|
const sign = signingGlobals[signingKey];
|
|
107
114
|
if (!sign) {
|
|
108
115
|
throw new Error("Group member signing must be enabled to sign and save group members. Call GroupMembersTable.enableGroupMemberSigning(fn) to enable it.");
|
|
109
116
|
}
|
|
110
|
-
|
|
117
|
+
const current = (groupMember.groupMemberId ? await this.get(groupMember.groupMemberId) : undefined) ??
|
|
118
|
+
(await this.findOne({ groupId: groupMember.groupId, userId: groupMember.userId }));
|
|
119
|
+
// A key the caller names, else the member's key as this device knows it, else the stored
|
|
120
|
+
// one. A block (role None) applies whatever the key.
|
|
121
|
+
const granted = groupMember.role > group_member_roles_1.GroupMemberRole.None;
|
|
122
|
+
const publicKey = groupMember.publicKey ??
|
|
123
|
+
(granted ? await knownKeyOf(groupMember) : undefined) ??
|
|
124
|
+
current?.publicKey;
|
|
125
|
+
if (!publicKey && granted) {
|
|
126
|
+
throw new Error(`No signed profile for user ${groupMember.userId} on this device: pass the member's publicKey`);
|
|
127
|
+
}
|
|
128
|
+
groupMember = sign.call(GroupMembersTable, {
|
|
129
|
+
...groupMember,
|
|
130
|
+
...(publicKey ? { publicKey } : {}),
|
|
131
|
+
...(await (0, write_checks_1.signingStamp)(this, groupMember, current)),
|
|
132
|
+
});
|
|
111
133
|
return this.save(groupMember, {
|
|
112
134
|
saveAsSnapshot: true,
|
|
113
135
|
restoreIfDeleted: true,
|
|
@@ -130,8 +152,8 @@ let GroupMembersTable = (() => {
|
|
|
130
152
|
* Settle two membership rows for the same user in the same group: devices that approved the
|
|
131
153
|
* same join at once could each write one, under a different id.
|
|
132
154
|
*
|
|
133
|
-
* Merged only when both rows grant the same access (role and deleted state), so nothing
|
|
134
|
-
* the member's access changes. The id {@link deterministicGroupMemberId} gives wins if either
|
|
155
|
+
* Merged only when both rows grant the same access (role, key and deleted state), so nothing
|
|
156
|
+
* about the member's access changes. The id {@link deterministicGroupMemberId} gives wins if either
|
|
135
157
|
* row has it, otherwise the lexicographically smaller id; the winner is kept as it is, signature
|
|
136
158
|
* included. Rows that disagree stay queued for a person. A pure function of the two rows, so
|
|
137
159
|
* every device keeps the same one.
|
|
@@ -144,6 +166,7 @@ let GroupMembersTable = (() => {
|
|
|
144
166
|
return undefined;
|
|
145
167
|
}
|
|
146
168
|
if (incoming.role !== existing.role ||
|
|
169
|
+
incoming.publicKey !== existing.publicKey ||
|
|
147
170
|
Boolean(incoming.deleted) !== Boolean(existing.deleted)) {
|
|
148
171
|
return undefined;
|
|
149
172
|
}
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import type { DataContext } from "../context/data-context";
|
|
2
2
|
import type { UserContext } from "../context/user-context";
|
|
3
3
|
import { GroupMemberRole } from "./group-member-roles";
|
|
4
|
-
import type { IGroupMember } from "./group-members";
|
|
5
|
-
import type { IGroup } from "./groups";
|
|
6
4
|
export { GroupMemberRole };
|
|
7
5
|
/**
|
|
8
6
|
* Return a user's {@link GroupMemberRole} in `groupId`.
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* In a group, roles come from the group's verified authority ({@link getGroupAuthority}): only
|
|
9
|
+
* memberships and settings whose signers could grant them count, so a row synced from a member
|
|
10
|
+
* who changed their own copy of Peers can't raise anyone's role. A group this device doesn't
|
|
11
|
+
* have gives {@link GroupMemberRole.None}.
|
|
12
|
+
*
|
|
13
|
+
* In the personal context (`groupId` is the user's own id), the user is
|
|
14
|
+
* {@link GroupMemberRole.Founder}, and so is a user they trust as Self; anyone else is None.
|
|
15
|
+
*
|
|
16
|
+
* Pass `publicKey` for a remote caller: a role belongs to the key it was granted to, so a
|
|
17
|
+
* device that authenticated with any other key gets the group's public role (see
|
|
18
|
+
* `roleInGroupAuthority`). Local lookups leave it out.
|
|
12
19
|
*
|
|
13
20
|
* Prefer passing `userContext` from the provider that owns the data. The
|
|
14
21
|
* process singleton is only a fallback and is not awaited — multi-device
|
|
@@ -17,18 +24,10 @@ export { GroupMemberRole };
|
|
|
17
24
|
* @param groupId - Group id, or the user's id for their personal context.
|
|
18
25
|
* @param userId - User whose role is requested.
|
|
19
26
|
* @param userContext - Provider-owned context. Falls back to {@link peekUserContext}.
|
|
27
|
+
* @param publicKey - Signing key a remote caller authenticated with.
|
|
20
28
|
* @throws If neither an explicit context nor the process singleton is available.
|
|
21
29
|
*/
|
|
22
|
-
export declare function getUserRole(groupId: string, userId: string, userContext?: UserContext): Promise<GroupMemberRole>;
|
|
23
|
-
/**
|
|
24
|
-
* Resolve a group role from the signing public key of a Users record.
|
|
25
|
-
*
|
|
26
|
-
* Uses {@link peekUserContext}; throws if the process singleton is unset.
|
|
27
|
-
*
|
|
28
|
-
* @param groupId - Group or personal context id.
|
|
29
|
-
* @param publicKey - User public key that signed a record.
|
|
30
|
-
*/
|
|
31
|
-
export declare function getUserRoleFromPublicKey(groupId: string, publicKey: string): Promise<GroupMemberRole>;
|
|
30
|
+
export declare function getUserRole(groupId: string, userId: string, userContext?: UserContext, publicKey?: string): Promise<GroupMemberRole>;
|
|
32
31
|
/**
|
|
33
32
|
* Return the current user's access level in `dataContext`.
|
|
34
33
|
*
|
|
@@ -42,21 +41,6 @@ export declare function getCurrentUserAccessLevel(dataContext: DataContext): Pro
|
|
|
42
41
|
*
|
|
43
42
|
* @param dataContext - Context whose group/personal role is evaluated.
|
|
44
43
|
* @param userId - User to resolve.
|
|
44
|
+
* @param publicKey - Signing key a remote caller authenticated with (see {@link getUserRole}).
|
|
45
45
|
*/
|
|
46
|
-
export declare function getUserAccessLevel(dataContext: DataContext, userId: string): Promise<number>;
|
|
47
|
-
/**
|
|
48
|
-
* Verify that a group record was signed by an Admin+ member (Founder to change founder).
|
|
49
|
-
*
|
|
50
|
-
* @param group - Proposed group record.
|
|
51
|
-
* @param oldGroup - Previous record when this is an update.
|
|
52
|
-
*/
|
|
53
|
-
export declare function verifyGroupSignature(group: IGroup, oldGroup?: IGroup): Promise<void>;
|
|
54
|
-
/**
|
|
55
|
-
* Return whether a group-member write is signed by someone allowed to make it.
|
|
56
|
-
*
|
|
57
|
-
* @param groupId - Group the membership belongs to.
|
|
58
|
-
* @param newGroupMember - Proposed membership row.
|
|
59
|
-
* @param oldGroupMember - Previous row when this is an update.
|
|
60
|
-
* @param signerRole - Optional precomputed signer role.
|
|
61
|
-
*/
|
|
62
|
-
export declare function isGroupMemberSignatureValid(groupId: string, newGroupMember: IGroupMember, oldGroupMember?: IGroupMember, signerRole?: GroupMemberRole): Promise<boolean>;
|
|
46
|
+
export declare function getUserAccessLevel(dataContext: DataContext, userId: string, publicKey?: string): Promise<number>;
|
|
@@ -2,21 +2,28 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GroupMemberRole = void 0;
|
|
4
4
|
exports.getUserRole = getUserRole;
|
|
5
|
-
exports.getUserRoleFromPublicKey = getUserRoleFromPublicKey;
|
|
6
5
|
exports.getCurrentUserAccessLevel = getCurrentUserAccessLevel;
|
|
7
6
|
exports.getUserAccessLevel = getUserAccessLevel;
|
|
8
|
-
exports.verifyGroupSignature = verifyGroupSignature;
|
|
9
|
-
exports.isGroupMemberSignatureValid = isGroupMemberSignatureValid;
|
|
10
7
|
const context_1 = require("../context");
|
|
11
|
-
const
|
|
8
|
+
const socket_type_1 = require("../device/socket.type");
|
|
12
9
|
const access_level_1 = require("./access-level");
|
|
10
|
+
const group_authority_1 = require("./group-authority");
|
|
13
11
|
const group_member_roles_1 = require("./group-member-roles");
|
|
14
12
|
Object.defineProperty(exports, "GroupMemberRole", { enumerable: true, get: function () { return group_member_roles_1.GroupMemberRole; } });
|
|
15
13
|
/**
|
|
16
14
|
* Return a user's {@link GroupMemberRole} in `groupId`.
|
|
17
15
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* In a group, roles come from the group's verified authority ({@link getGroupAuthority}): only
|
|
17
|
+
* memberships and settings whose signers could grant them count, so a row synced from a member
|
|
18
|
+
* who changed their own copy of Peers can't raise anyone's role. A group this device doesn't
|
|
19
|
+
* have gives {@link GroupMemberRole.None}.
|
|
20
|
+
*
|
|
21
|
+
* In the personal context (`groupId` is the user's own id), the user is
|
|
22
|
+
* {@link GroupMemberRole.Founder}, and so is a user they trust as Self; anyone else is None.
|
|
23
|
+
*
|
|
24
|
+
* Pass `publicKey` for a remote caller: a role belongs to the key it was granted to, so a
|
|
25
|
+
* device that authenticated with any other key gets the group's public role (see
|
|
26
|
+
* `roleInGroupAuthority`). Local lookups leave it out.
|
|
20
27
|
*
|
|
21
28
|
* Prefer passing `userContext` from the provider that owns the data. The
|
|
22
29
|
* process singleton is only a fallback and is not awaited — multi-device
|
|
@@ -25,51 +32,23 @@ Object.defineProperty(exports, "GroupMemberRole", { enumerable: true, get: funct
|
|
|
25
32
|
* @param groupId - Group id, or the user's id for their personal context.
|
|
26
33
|
* @param userId - User whose role is requested.
|
|
27
34
|
* @param userContext - Provider-owned context. Falls back to {@link peekUserContext}.
|
|
35
|
+
* @param publicKey - Signing key a remote caller authenticated with.
|
|
28
36
|
* @throws If neither an explicit context nor the process singleton is available.
|
|
29
37
|
*/
|
|
30
|
-
async function getUserRole(groupId, userId, userContext) {
|
|
31
|
-
const { Groups } = await Promise.resolve().then(() => require("./groups"));
|
|
32
|
-
const { GroupMembers } = await Promise.resolve().then(() => require("./group-members"));
|
|
38
|
+
async function getUserRole(groupId, userId, userContext, publicKey) {
|
|
33
39
|
const ctx = userContext ?? (0, context_1.peekUserContext)();
|
|
34
40
|
if (!ctx) {
|
|
35
41
|
throw new Error("User context is not available");
|
|
36
42
|
}
|
|
37
43
|
await ctx.loadingPromise;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return group_member_roles_1.GroupMemberRole.Founder;
|
|
44
|
+
if (groupId === ctx.userId) {
|
|
45
|
+
if (userId === ctx.userId)
|
|
46
|
+
return group_member_roles_1.GroupMemberRole.Founder;
|
|
47
|
+
const { getUserTrustLevel } = await Promise.resolve().then(() => require("./user-trust-levels"));
|
|
48
|
+
const trust = await getUserTrustLevel(ctx.userDataContext, userId);
|
|
49
|
+
return trust === socket_type_1.TrustLevel.Self ? group_member_roles_1.GroupMemberRole.Founder : group_member_roles_1.GroupMemberRole.None;
|
|
44
50
|
}
|
|
45
|
-
|
|
46
|
-
if (groupMembership) {
|
|
47
|
-
return groupMembership.role;
|
|
48
|
-
}
|
|
49
|
-
return group.publicRole || group_member_roles_1.GroupMemberRole.None;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Resolve a group role from the signing public key of a Users record.
|
|
53
|
-
*
|
|
54
|
-
* Uses {@link peekUserContext}; throws if the process singleton is unset.
|
|
55
|
-
*
|
|
56
|
-
* @param groupId - Group or personal context id.
|
|
57
|
-
* @param publicKey - User public key that signed a record.
|
|
58
|
-
*/
|
|
59
|
-
async function getUserRoleFromPublicKey(groupId, publicKey) {
|
|
60
|
-
const { Users } = await Promise.resolve().then(() => require("./users"));
|
|
61
|
-
const userContext = (0, context_1.peekUserContext)();
|
|
62
|
-
if (!userContext) {
|
|
63
|
-
throw new Error("User context is not available");
|
|
64
|
-
}
|
|
65
|
-
await userContext.loadingPromise;
|
|
66
|
-
const groupDataContext = userContext.getDataContext(groupId);
|
|
67
|
-
const user = (await Users(groupDataContext).findOne({ publicKey })) ||
|
|
68
|
-
(await Users(userContext.userDataContext).findOne({ publicKey }));
|
|
69
|
-
if (user) {
|
|
70
|
-
return getUserRole(groupId, user?.userId, userContext);
|
|
71
|
-
}
|
|
72
|
-
return group_member_roles_1.GroupMemberRole.None;
|
|
51
|
+
return (0, group_authority_1.roleInGroupAuthority)(await (0, group_authority_1.getGroupAuthority)(ctx, groupId), userId, publicKey);
|
|
73
52
|
}
|
|
74
53
|
/**
|
|
75
54
|
* Return the current user's access level in `dataContext`.
|
|
@@ -87,59 +66,10 @@ async function getCurrentUserAccessLevel(dataContext) {
|
|
|
87
66
|
*
|
|
88
67
|
* @param dataContext - Context whose group/personal role is evaluated.
|
|
89
68
|
* @param userId - User to resolve.
|
|
69
|
+
* @param publicKey - Signing key a remote caller authenticated with (see {@link getUserRole}).
|
|
90
70
|
*/
|
|
91
|
-
async function getUserAccessLevel(dataContext, userId) {
|
|
71
|
+
async function getUserAccessLevel(dataContext, userId, publicKey) {
|
|
92
72
|
const contextKey = dataContext.groupId ?? dataContext.userContext.userId;
|
|
93
|
-
const role = await getUserRole(contextKey, userId, dataContext.userContext);
|
|
73
|
+
const role = await getUserRole(contextKey, userId, dataContext.userContext, publicKey);
|
|
94
74
|
return (0, access_level_1.accessLevelFromGroupRole)(role);
|
|
95
75
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Verify that a group record was signed by an Admin+ member (Founder to change founder).
|
|
98
|
-
*
|
|
99
|
-
* @param group - Proposed group record.
|
|
100
|
-
* @param oldGroup - Previous record when this is an update.
|
|
101
|
-
*/
|
|
102
|
-
async function verifyGroupSignature(group, oldGroup) {
|
|
103
|
-
(0, keys_1.verifyObjectSignature)(group);
|
|
104
|
-
const publicKey = (0, keys_1.getPublicKeyFromObjectSignature)(group) ?? "";
|
|
105
|
-
const signerRole = await getUserRoleFromPublicKey(group.groupId, publicKey);
|
|
106
|
-
if (oldGroup &&
|
|
107
|
-
oldGroup.founderUserId !== group.founderUserId &&
|
|
108
|
-
signerRole < group_member_roles_1.GroupMemberRole.Founder) {
|
|
109
|
-
throw new Error("Only group founders can change the founder of the group");
|
|
110
|
-
}
|
|
111
|
-
if (signerRole < group_member_roles_1.GroupMemberRole.Admin) {
|
|
112
|
-
throw new Error("Only group admins can modify group settings");
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Return whether a group-member write is signed by someone allowed to make it.
|
|
117
|
-
*
|
|
118
|
-
* @param groupId - Group the membership belongs to.
|
|
119
|
-
* @param newGroupMember - Proposed membership row.
|
|
120
|
-
* @param oldGroupMember - Previous row when this is an update.
|
|
121
|
-
* @param signerRole - Optional precomputed signer role.
|
|
122
|
-
*/
|
|
123
|
-
async function isGroupMemberSignatureValid(groupId, newGroupMember, oldGroupMember, signerRole) {
|
|
124
|
-
oldGroupMember ??= newGroupMember;
|
|
125
|
-
// Basic signature verification
|
|
126
|
-
if (!(0, keys_1.isObjectSignatureValid)(newGroupMember)) {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
// Get the public key that signed this group member
|
|
130
|
-
const signerPublicKey = (0, keys_1.getPublicKeyFromObjectSignature)(newGroupMember);
|
|
131
|
-
if (!signerPublicKey) {
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
// Determine signer's role in the group using the user-based permission system
|
|
135
|
-
signerRole ??= await getUserRoleFromPublicKey(groupId, signerPublicKey);
|
|
136
|
-
// Validate permissions for group member modifications
|
|
137
|
-
if (signerRole < group_member_roles_1.GroupMemberRole.Admin) {
|
|
138
|
-
return false; // Only group admins or above can modify group membership
|
|
139
|
-
}
|
|
140
|
-
// admins or above can only modify roles up to their own level
|
|
141
|
-
if (signerRole < newGroupMember.role || signerRole < oldGroupMember.role) {
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
return true;
|
|
145
|
-
}
|