@peers-app/peers-sdk 0.27.5 → 0.28.1

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 (91) 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/system/network-contracts.d.ts +1 -1
  8. package/dist/contracts/tool-catalog-service.js +1 -1
  9. package/dist/data/assistants.d.ts +3 -3
  10. package/dist/data/authority-write-checks.d.ts +42 -0
  11. package/dist/data/authority-write-checks.js +324 -0
  12. package/dist/data/change-tracking.d.ts +4 -4
  13. package/dist/data/create-group.js +1 -0
  14. package/dist/data/devices.d.ts +27 -4
  15. package/dist/data/devices.js +31 -0
  16. package/dist/data/group-authority.d.ts +111 -0
  17. package/dist/data/group-authority.js +281 -0
  18. package/dist/data/group-members.d.ts +8 -2
  19. package/dist/data/group-members.js +41 -18
  20. package/dist/data/group-permissions.d.ts +15 -31
  21. package/dist/data/group-permissions.js +25 -95
  22. package/dist/data/group-resign.d.ts +38 -0
  23. package/dist/data/group-resign.js +71 -0
  24. package/dist/data/groups.d.ts +11 -0
  25. package/dist/data/groups.js +31 -13
  26. package/dist/data/index.d.ts +3 -0
  27. package/dist/data/index.js +3 -0
  28. package/dist/data/invites.d.ts +14 -0
  29. package/dist/data/orm/index.d.ts +1 -0
  30. package/dist/data/orm/index.js +1 -0
  31. package/dist/data/orm/sql.data-source.d.ts +2 -2
  32. package/dist/data/orm/sql.data-source.js +24 -22
  33. package/dist/data/orm/table-container.js +20 -11
  34. package/dist/data/orm/write-checks.d.ts +89 -0
  35. package/dist/data/orm/write-checks.js +115 -0
  36. package/dist/data/package-table-identity.d.ts +21 -0
  37. package/dist/data/package-table-identity.js +87 -0
  38. package/dist/data/package-version-resolver.d.ts +15 -0
  39. package/dist/data/package-version-resolver.js +70 -3
  40. package/dist/data/package-versions.d.ts +20 -14
  41. package/dist/data/package-versions.js +15 -9
  42. package/dist/data/packages.d.ts +6 -1
  43. package/dist/data/packages.js +15 -20
  44. package/dist/data/persistent-vars.js +2 -39
  45. package/dist/data/sync-recovery-queue.d.ts +4 -4
  46. package/dist/data/sync-recovery-queue.js +7 -1
  47. package/dist/data/tool-access-approvals.d.ts +3 -3
  48. package/dist/data/user-permissions.d.ts +33 -2
  49. package/dist/data/user-permissions.js +87 -10
  50. package/dist/data/users.d.ts +49 -12
  51. package/dist/data/users.js +21 -1
  52. package/dist/data/workflows.d.ts +1 -1
  53. package/dist/define/index.d.ts +1 -1
  54. package/dist/define/index.js +2 -1
  55. package/dist/define/manifest.js +9 -1
  56. package/dist/define/table.d.ts +15 -0
  57. package/dist/device/binary-peer-connection-v2.d.ts +1 -1
  58. package/dist/device/binary-peer-connection-v2.js +1 -1
  59. package/dist/device/connection.d.ts +1 -1
  60. package/dist/device/frame-cipher.d.ts +1 -1
  61. package/dist/device/frame-cipher.js +1 -1
  62. package/dist/device/get-trust-level-fn.js +3 -6
  63. package/dist/device-pairing/types.d.ts +26 -26
  64. package/dist/identity/user-row-change.d.ts +3 -1
  65. package/dist/identity/user-row-change.js +33 -18
  66. package/dist/invites/group-approval.js +23 -24
  67. package/dist/invites/invite-messages.d.ts +250 -28
  68. package/dist/invites/invite-token.d.ts +18 -0
  69. package/dist/invites/invite-token.js +2 -0
  70. package/dist/keys.d.ts +7 -0
  71. package/dist/keys.js +27 -0
  72. package/dist/package-installer/package-installer.d.ts +1 -1
  73. package/dist/package-installer/package-installer.js +8 -3
  74. package/dist/package-installer/package-propagation.d.ts +2 -0
  75. package/dist/package-installer/package-propagation.js +20 -38
  76. package/dist/package-installer/package-remote-checker.js +15 -3
  77. package/dist/package-loader/isolated-artifact.d.ts +14 -0
  78. package/dist/package-loader/isolated-artifact.js +3 -3
  79. package/dist/package-loader/isolated-runtime.d.ts +3 -1
  80. package/dist/package-loader/isolated-table-gateway.js +4 -1
  81. package/dist/package-loader/package-loader.js +6 -0
  82. package/dist/peer-id.d.ts +19 -0
  83. package/dist/peer-id.js +41 -0
  84. package/dist/types/peer-device.d.ts +2 -0
  85. package/dist/types/workflow.d.ts +2 -2
  86. package/dist/utils.js +13 -32
  87. package/package.json +1 -1
  88. package/dist/data/package-permissions.d.ts +0 -14
  89. package/dist/data/package-permissions.js +0 -26
  90. package/dist/data/package-version-permissions.d.ts +0 -14
  91. package/dist/data/package-version-permissions.js +0 -30
@@ -15,6 +15,7 @@ export declare const deviceSchema: z.ZodObject<{
15
15
  reportedName: z.ZodOptional<z.ZodString>;
16
16
  serverUrl: z.ZodOptional<z.ZodString>;
17
17
  trustLevel: z.ZodNativeEnum<typeof TrustLevel>;
18
+ ownerSignature: z.ZodOptional<z.ZodString>;
18
19
  }, "strip", z.ZodTypeAny, {
19
20
  userId: string;
20
21
  deviceId: string;
@@ -24,6 +25,7 @@ export declare const deviceSchema: z.ZodObject<{
24
25
  name?: string | undefined;
25
26
  reportedName?: string | undefined;
26
27
  serverUrl?: string | undefined;
28
+ ownerSignature?: string | undefined;
27
29
  }, {
28
30
  userId: string;
29
31
  deviceId: string;
@@ -33,9 +35,29 @@ export declare const deviceSchema: z.ZodObject<{
33
35
  name?: string | undefined;
34
36
  reportedName?: string | undefined;
35
37
  serverUrl?: string | undefined;
38
+ ownerSignature?: string | undefined;
36
39
  }>;
37
40
  /** A context-scoped device record containing local state and owner-reported metadata. */
38
41
  export type IDevice = z.infer<typeof deviceSchema>;
42
+ /**
43
+ * What a device's owner signs to claim it: the contents of `ownerSignature`. Each device signs
44
+ * its own claim with its user's key and writes it to its row in every group it belongs to.
45
+ *
46
+ * @param device - The device id and the user it belongs to.
47
+ */
48
+ export declare function deviceOwnerStatement(device: Pick<IDevice, "deviceId" | "userId">): {
49
+ purpose: string;
50
+ deviceId: string;
51
+ userId: string;
52
+ };
53
+ /**
54
+ * Whether `device.ownerSignature` is `ownerPublicKey`'s signature over
55
+ * {@link deviceOwnerStatement}: the owner, not whoever wrote the row, says the device is theirs.
56
+ *
57
+ * @param device - A Devices row.
58
+ * @param ownerPublicKey - The signing key of `device.userId`, as this device knows it.
59
+ */
60
+ export declare function isDeviceOwnerSigned(device: Pick<IDevice, "deviceId" | "userId" | "ownerSignature">, ownerPublicKey: string | undefined): boolean;
39
61
  /** Schema for the signed identity and bounded presentation hints exchanged by devices. */
40
62
  export declare const deviceInfoSchema: z.ZodObject<{
41
63
  userId: z.ZodEffects<z.ZodString, string, string>;
@@ -47,15 +69,15 @@ export declare const deviceInfoSchema: z.ZodObject<{
47
69
  }, "strip", z.ZodTypeAny, {
48
70
  publicKey: string;
49
71
  userId: string;
50
- deviceId: string;
51
72
  publicBoxKey: string;
73
+ deviceId: string;
52
74
  userName?: string | undefined;
53
75
  deviceName?: string | undefined;
54
76
  }, {
55
77
  publicKey: string;
56
78
  userId: string;
57
- deviceId: string;
58
79
  publicBoxKey: string;
80
+ deviceId: string;
59
81
  userName?: string | undefined;
60
82
  deviceName?: string | undefined;
61
83
  }>;
@@ -155,9 +177,9 @@ export declare const deviceHandshakeSchema: z.ZodObject<{
155
177
  timestamp: number;
156
178
  publicKey: string;
157
179
  userId: string;
180
+ publicBoxKey: string;
158
181
  connectionId: string;
159
182
  deviceId: string;
160
- publicBoxKey: string;
161
183
  serverAddress: string;
162
184
  userName?: string | undefined;
163
185
  deviceName?: string | undefined;
@@ -180,9 +202,9 @@ export declare const deviceHandshakeSchema: z.ZodObject<{
180
202
  timestamp: number;
181
203
  publicKey: string;
182
204
  userId: string;
205
+ publicBoxKey: string;
183
206
  connectionId: string;
184
207
  deviceId: string;
185
- publicBoxKey: string;
186
208
  serverAddress: string;
187
209
  userName?: string | undefined;
188
210
  deviceName?: string | undefined;
@@ -214,6 +236,7 @@ export declare function Devices(dataContext?: DataContext): import("..").Table<{
214
236
  name?: string | undefined;
215
237
  reportedName?: string | undefined;
216
238
  serverUrl?: string | undefined;
239
+ ownerSignature?: string | undefined;
217
240
  }>;
218
241
  /** Relay server URLs trusted by the active group. */
219
242
  export declare const trustedServers: import("./persistent-vars").PersistentVar<string[]>;
@@ -1,11 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.thisDeviceId = exports.trustedServers = exports.deviceHandshakeSchema = exports.handshakeFrameCryptoSchema = exports.deviceInfoSchema = exports.deviceSchema = exports.displayNameSchema = exports.DISPLAY_NAME_MAX_LENGTH = void 0;
4
+ exports.deviceOwnerStatement = deviceOwnerStatement;
5
+ exports.isDeviceOwnerSigned = isDeviceOwnerSigned;
4
6
  exports.Devices = Devices;
5
7
  const zod_1 = require("zod");
6
8
  const user_context_singleton_1 = require("../context/user-context-singleton");
7
9
  const socket_type_1 = require("../device/socket.type");
8
10
  const key_contest_1 = require("../identity/key-contest");
11
+ const keys_1 = require("../keys");
9
12
  const zod_types_1 = require("../types/zod-types");
10
13
  const table_definitions_system_1 = require("./orm/table-definitions.system");
11
14
  const types_1 = require("./orm/types");
@@ -24,7 +27,35 @@ exports.deviceSchema = zod_1.z.object({
24
27
  reportedName: exports.displayNameSchema.optional(),
25
28
  serverUrl: zod_1.z.string().optional(),
26
29
  trustLevel: zod_1.z.nativeEnum(socket_type_1.TrustLevel),
30
+ ownerSignature: zod_1.z
31
+ .string()
32
+ .optional()
33
+ .describe("The owner's signature claiming this device for userId (see deviceOwnerStatement). In a group, only a row with a valid one says who a device belongs to, and it stays theirs"),
27
34
  });
35
+ /**
36
+ * What a device's owner signs to claim it: the contents of `ownerSignature`. Each device signs
37
+ * its own claim with its user's key and writes it to its row in every group it belongs to.
38
+ *
39
+ * @param device - The device id and the user it belongs to.
40
+ */
41
+ function deviceOwnerStatement(device) {
42
+ return { purpose: "peers-device-owner", deviceId: device.deviceId, userId: device.userId };
43
+ }
44
+ /**
45
+ * Whether `device.ownerSignature` is `ownerPublicKey`'s signature over
46
+ * {@link deviceOwnerStatement}: the owner, not whoever wrote the row, says the device is theirs.
47
+ *
48
+ * @param device - A Devices row.
49
+ * @param ownerPublicKey - The signing key of `device.userId`, as this device knows it.
50
+ */
51
+ function isDeviceOwnerSigned(device, ownerPublicKey) {
52
+ if (!device.ownerSignature || !ownerPublicKey)
53
+ return false;
54
+ return ((0, keys_1.signedByKey)({
55
+ ...deviceOwnerStatement(device),
56
+ signature: `${ownerPublicKey}:${device.ownerSignature}`,
57
+ }) === ownerPublicKey);
58
+ }
28
59
  /** Schema for the signed identity and bounded presentation hints exchanged by devices. */
29
60
  exports.deviceInfoSchema = zod_1.z.object({
30
61
  userId: zod_types_1.zodPeerId,
@@ -0,0 +1,111 @@
1
+ import type { UserContext } from "../context/user-context";
2
+ import { GroupMemberRole } from "./group-member-roles";
3
+ import type { IGroupMember } from "./group-members";
4
+ import type { IGroup } from "./groups";
5
+ import type { IUser } from "./users";
6
+ /**
7
+ * Who holds which role in a group, counting only rows whose signatures check out against the
8
+ * group's own chain of authority.
9
+ *
10
+ * Sync writes rows without running the tables' checks, and any member's device can serve a
11
+ * row, so a role or setting is believed only when its signer could have granted it. Built by
12
+ * {@link computeGroupAuthority}; read through {@link getGroupAuthority}.
13
+ */
14
+ export interface IGroupAuthority {
15
+ groupId: string;
16
+ /** The founder: the root every role is traced back to. Undefined when nothing names one. */
17
+ founderUserId?: string;
18
+ /** Role anyone without a membership gets, from the group row when that row counts. */
19
+ publicRole: GroupMemberRole;
20
+ /** Role of each member whose membership counts. The founder is {@link GroupMemberRole.Founder}. */
21
+ roles: ReadonlyMap<string, GroupMemberRole>;
22
+ /**
23
+ * The signing key each counted role belongs to, by user id. A device presenting a member's
24
+ * user id with another key doesn't get the member's role (see {@link roleInGroupAuthority}).
25
+ * Absent for a founder without a verified profile row and for members with role None.
26
+ */
27
+ keys: ReadonlyMap<string, string>;
28
+ /** The group-context Groups row, when it counts (signed by the founder or a counted Admin). */
29
+ group?: IGroup;
30
+ /** Each user's verified profile row (personal rows first), by user id. */
31
+ users: ReadonlyMap<string, IUser>;
32
+ }
33
+ /** The rows {@link computeGroupAuthority} works from. */
34
+ export interface IGroupAuthorityInput {
35
+ groupId: string;
36
+ /**
37
+ * The user's own copy of the group row (personal context), written when they created or
38
+ * joined the group. Only their own devices write it, so its founder is the root of trust.
39
+ */
40
+ pinnedGroup?: IGroup;
41
+ /** The group's row in the group context, which any member's device can serve. */
42
+ group?: IGroup;
43
+ /** Every GroupMembers row in the group context. */
44
+ members: readonly IGroupMember[];
45
+ /**
46
+ * Users rows that map signing keys to users, most trusted first: the first row pins the
47
+ * keys, and a matching self-signed row verifies them. Pass personal rows before group rows.
48
+ */
49
+ users: readonly IUser[];
50
+ }
51
+ /**
52
+ * Build a group's {@link IGroupAuthority} from its rows. Pure and deterministic: the same rows
53
+ * give the same roles on every device, whatever order they arrived in.
54
+ *
55
+ * - **Profiles:** `users` maps each user to the profile row that settles their keys: the first
56
+ * row for a user pins the keys (an unsigned contact stub included) and a self-signed row with
57
+ * the same keys verifies them.
58
+ * - **Founder:** from `pinnedGroup`. Without a personal pin no authority counts. A group row
59
+ * naming a different founder counts only if the pinned founder signed it.
60
+ * - **Members:** starting from the founder, a membership counts when its signature is valid
61
+ * and its signer already counts as at least Admin and at least the role granted. Repeated
62
+ * until nothing changes, so a row can't justify itself or another row that justifies it.
63
+ * Deleted rows don't count.
64
+ * - **Keys:** a role belongs to a key, not only a user id. The founder's key is their verified
65
+ * profile row's. A membership names the member's key: it counts for that key, or for the
66
+ * key the member's verified profile row has if that row shows the named key handing over to
67
+ * it ({@link keySucceeds}). A profile row with any other key (a namesake a member forged on a
68
+ * device that had never seen the real one) stops the grant counting rather than inheriting
69
+ * it. Signatures count only by these keys.
70
+ * - **Group settings:** the group row's `publicRole` counts only when that row is signed by the
71
+ * founder or a counted Admin; otherwise no public access is granted.
72
+ *
73
+ * @param input - The group's rows.
74
+ */
75
+ export declare function computeGroupAuthority(input: IGroupAuthorityInput): IGroupAuthority;
76
+ /**
77
+ * A user's role under `authority`: their counted membership, else the group's public role.
78
+ *
79
+ * Pass `publicKey` whenever the user is a remote caller: the role then counts only if it
80
+ * belongs to the key the caller authenticated with ({@link IGroupAuthority.keys}). Otherwise a
81
+ * device that proved a namesake's key somewhere else (another group's copy of a forged profile)
82
+ * would get the member's role here. A membership with role None applies whatever the key.
83
+ *
84
+ * @param authority - From {@link computeGroupAuthority} or {@link getGroupAuthority}.
85
+ * @param userId - User whose role is requested.
86
+ * @param publicKey - The signing key the caller authenticated with, when it is remote.
87
+ */
88
+ export declare function roleInGroupAuthority(authority: IGroupAuthority, userId: string, publicKey?: string): GroupMemberRole;
89
+ /**
90
+ * The counted member who signed `row` and their role: its signature verifies with the key
91
+ * their role belongs to under `authority`. Undefined for anyone else, whatever their profile says.
92
+ *
93
+ * @param authority - The group's verified authority.
94
+ * @param row - A signed row.
95
+ */
96
+ export declare function groupSigner(authority: Pick<IGroupAuthority, "keys" | "roles">, row: {
97
+ signature?: string;
98
+ }): {
99
+ signer: string;
100
+ signerRole: GroupMemberRole;
101
+ } | undefined;
102
+ /**
103
+ * The verified authority for `groupId`, cached per user context and rebuilt after any change
104
+ * to that group's Groups, GroupMembers or Users rows, or to the user's own Groups or Users rows.
105
+ *
106
+ * @param userContext - Context that owns the group.
107
+ * @param groupId - Group id (never the personal context).
108
+ */
109
+ export declare function getGroupAuthority(userContext: UserContext, groupId: string): Promise<IGroupAuthority>;
110
+ /** Drop cached authority for one group, or every group of `userContext`. */
111
+ export declare function invalidateGroupAuthority(userContext: UserContext, groupId?: string): void;
@@ -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.