@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.
Files changed (92) hide show
  1. package/dist/context/user-context.d.ts +7 -1
  2. package/dist/context/user-context.js +4 -9
  3. package/dist/contracts/contract-proxy.d.ts +5 -0
  4. package/dist/contracts/contract-proxy.js +2 -2
  5. package/dist/contracts/contract-resolvers.d.ts +2 -2
  6. package/dist/contracts/contract-resolvers.js +14 -15
  7. package/dist/contracts/contract-transport.d.ts +2 -1
  8. package/dist/contracts/contract-transport.js +24 -2
  9. package/dist/contracts/system/network-contracts.d.ts +1 -1
  10. package/dist/contracts/tool-catalog-service.js +1 -1
  11. package/dist/contracts/tool-catalog.js +6 -2
  12. package/dist/data/assistants.d.ts +3 -3
  13. package/dist/data/authority-write-checks.d.ts +42 -0
  14. package/dist/data/authority-write-checks.js +324 -0
  15. package/dist/data/change-tracking.d.ts +4 -4
  16. package/dist/data/create-group.js +1 -0
  17. package/dist/data/devices.d.ts +27 -4
  18. package/dist/data/devices.js +31 -0
  19. package/dist/data/group-authority.d.ts +111 -0
  20. package/dist/data/group-authority.js +281 -0
  21. package/dist/data/group-members.d.ts +8 -2
  22. package/dist/data/group-members.js +41 -18
  23. package/dist/data/group-permissions.d.ts +15 -31
  24. package/dist/data/group-permissions.js +25 -95
  25. package/dist/data/group-resign.d.ts +38 -0
  26. package/dist/data/group-resign.js +71 -0
  27. package/dist/data/groups.d.ts +11 -0
  28. package/dist/data/groups.js +31 -13
  29. package/dist/data/index.d.ts +3 -0
  30. package/dist/data/index.js +3 -0
  31. package/dist/data/invites.d.ts +14 -0
  32. package/dist/data/orm/index.d.ts +1 -0
  33. package/dist/data/orm/index.js +1 -0
  34. package/dist/data/orm/sql.data-source.d.ts +2 -2
  35. package/dist/data/orm/sql.data-source.js +8 -9
  36. package/dist/data/orm/table-container.js +14 -8
  37. package/dist/data/orm/write-checks.d.ts +89 -0
  38. package/dist/data/orm/write-checks.js +115 -0
  39. package/dist/data/package-table-identity.d.ts +6 -0
  40. package/dist/data/package-table-identity.js +51 -0
  41. package/dist/data/package-version-resolver.d.ts +15 -0
  42. package/dist/data/package-version-resolver.js +58 -3
  43. package/dist/data/package-versions.d.ts +20 -14
  44. package/dist/data/package-versions.js +15 -9
  45. package/dist/data/packages.d.ts +6 -1
  46. package/dist/data/packages.js +15 -20
  47. package/dist/data/persistent-vars.js +2 -39
  48. package/dist/data/sync-recovery-queue.d.ts +4 -4
  49. package/dist/data/sync-recovery-queue.js +7 -1
  50. package/dist/data/tool-access-approvals.d.ts +3 -3
  51. package/dist/data/user-permissions.d.ts +33 -2
  52. package/dist/data/user-permissions.js +87 -10
  53. package/dist/data/users.d.ts +49 -12
  54. package/dist/data/users.js +21 -1
  55. package/dist/data/workflows.d.ts +1 -1
  56. package/dist/device/binary-peer-connection-v2.d.ts +8 -16
  57. package/dist/device/binary-peer-connection-v2.js +15 -31
  58. package/dist/device/connection.d.ts +35 -44
  59. package/dist/device/connection.js +188 -186
  60. package/dist/device/device.d.ts +1 -8
  61. package/dist/device/device.js +1 -30
  62. package/dist/device/frame-cipher.d.ts +1 -1
  63. package/dist/device/frame-cipher.js +1 -1
  64. package/dist/device/get-trust-level-fn.js +3 -6
  65. package/dist/device/socket.type.d.ts +6 -12
  66. package/dist/device-pairing/types.d.ts +26 -26
  67. package/dist/identity/user-row-change.d.ts +3 -1
  68. package/dist/identity/user-row-change.js +33 -18
  69. package/dist/invites/group-approval.js +23 -24
  70. package/dist/invites/invite-messages.d.ts +250 -28
  71. package/dist/invites/invite-token.d.ts +18 -0
  72. package/dist/invites/invite-token.js +2 -0
  73. package/dist/keys.d.ts +7 -0
  74. package/dist/keys.js +27 -0
  75. package/dist/package-installer/package-installer.d.ts +1 -1
  76. package/dist/package-installer/package-installer.js +8 -3
  77. package/dist/package-installer/package-propagation.d.ts +2 -0
  78. package/dist/package-installer/package-propagation.js +20 -38
  79. package/dist/package-installer/package-remote-checker.js +15 -3
  80. package/dist/package-loader/isolated-artifact.js +0 -2
  81. package/dist/package-loader/package-loader.js +6 -0
  82. package/dist/types/peer-device.d.ts +2 -0
  83. package/dist/types/workflow.d.ts +2 -2
  84. package/package.json +1 -1
  85. package/dist/data/package-permissions.d.ts +0 -14
  86. package/dist/data/package-permissions.js +0 -26
  87. package/dist/data/package-version-permissions.d.ts +0 -14
  88. package/dist/data/package-version-permissions.js +0 -30
  89. package/dist/device/binary-peer-connection.d.ts +0 -58
  90. package/dist/device/binary-peer-connection.js +0 -516
  91. package/dist/device/streamed-socket.d.ts +0 -23
  92. 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 about
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 users_1 = require("./users");
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
- return super.save(groupMember, opts);
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
- const me = await (0, users_1.getMe)();
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
- groupMember = sign.call(GroupMembersTable, groupMember);
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 about
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
- * Missing groups and the group's founder resolve to {@link GroupMemberRole.Founder},
11
- * which is how a personal context (no Groups row) becomes access 100.
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 keys_1 = require("../keys");
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
- * Missing groups and the group's founder resolve to {@link GroupMemberRole.Founder},
19
- * which is how a personal context (no Groups row) becomes access 100.
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
- const groupDataContext = ctx.getDataContext(groupId);
39
- const group = (await Groups(groupDataContext).get(groupId)) ||
40
- (await Groups(ctx.userDataContext).get(groupId));
41
- // If the group doesn't exist or the current user is explicitly the founder, return Founder role
42
- if (!group || group.founderUserId === userId) {
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
- const groupMembership = await GroupMembers(groupDataContext).findOne({ userId });
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
- }