@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
@@ -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
- }
@@ -0,0 +1,38 @@
1
+ import type { DataContext } from "../context/data-context";
2
+ /** What {@link resignGroupAuthority} did, per table. */
3
+ export interface IResignGroupAuthorityResult {
4
+ /** Rows re-signed. */
5
+ resigned: Record<"Groups" | "GroupMembers" | "Packages" | "PackageVersions", number>;
6
+ /** Rows left as they were because their table refused this user's signature, with why. */
7
+ refused: {
8
+ table: string;
9
+ recordId: string;
10
+ error: string;
11
+ }[];
12
+ }
13
+ /** Which rows {@link resignGroupAuthority} re-signs. */
14
+ export interface IResignGroupAuthorityOptions {
15
+ /** Only rows validly signed with this public key (a key the user just rotated away from). */
16
+ signedByKey?: string;
17
+ /**
18
+ * Sign with this secret key instead of the tables' registered signer. Key rotation passes the
19
+ * new key, since the running host still signs with the old one until it restarts. Host code
20
+ * only; never accepted from a client.
21
+ */
22
+ secretKey?: string;
23
+ }
24
+ /**
25
+ * Re-sign the rows that carry a group's authority (the group row, memberships, packages and
26
+ * package versions) with this device's current key, so they count under the group's verified
27
+ * authority again: after the hard cutover that started checking synced rows, and after a key
28
+ * rotation (`signedByKey`), since roles are traced through signatures by current keys.
29
+ *
30
+ * Each row goes through its table's `signAndSave`, so its table's rules still apply: the founder
31
+ * can re-sign everything; an Admin only what an Admin may sign. Rows a table refuses are left
32
+ * as they were and reported. Re-signing vouches for each row as it is now, so only run it on a
33
+ * group whose current rows you trust.
34
+ *
35
+ * @param dataContext - The group's context (not the personal context).
36
+ * @param opts - Limit to rows signed by one key.
37
+ */
38
+ export declare function resignGroupAuthority(dataContext: DataContext, opts?: IResignGroupAuthorityOptions): Promise<IResignGroupAuthorityResult>;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resignGroupAuthority = resignGroupAuthority;
4
+ const keys_1 = require("../keys");
5
+ const write_checks_1 = require("./orm/write-checks");
6
+ /**
7
+ * Re-sign the rows that carry a group's authority (the group row, memberships, packages and
8
+ * package versions) with this device's current key, so they count under the group's verified
9
+ * authority again: after the hard cutover that started checking synced rows, and after a key
10
+ * rotation (`signedByKey`), since roles are traced through signatures by current keys.
11
+ *
12
+ * Each row goes through its table's `signAndSave`, so its table's rules still apply: the founder
13
+ * can re-sign everything; an Admin only what an Admin may sign. Rows a table refuses are left
14
+ * as they were and reported. Re-signing vouches for each row as it is now, so only run it on a
15
+ * group whose current rows you trust.
16
+ *
17
+ * @param dataContext - The group's context (not the personal context).
18
+ * @param opts - Limit to rows signed by one key.
19
+ */
20
+ async function resignGroupAuthority(dataContext, opts = {}) {
21
+ const groupId = dataContext.groupId;
22
+ if (!groupId) {
23
+ throw new Error("resignGroupAuthority needs a group context");
24
+ }
25
+ const { Groups } = await Promise.resolve().then(() => require("./groups"));
26
+ const { GroupMembers } = await Promise.resolve().then(() => require("./group-members"));
27
+ const { Packages } = await Promise.resolve().then(() => require("./packages"));
28
+ const { PackageVersions } = await Promise.resolve().then(() => require("./package-versions"));
29
+ const result = {
30
+ resigned: { Groups: 0, GroupMembers: 0, Packages: 0, PackageVersions: 0 },
31
+ refused: [],
32
+ };
33
+ const snapshot = { saveAsSnapshot: true, restoreIfDeleted: true };
34
+ // Through each table's own save, so its rules still apply to the new signature.
35
+ const resign = async (table, rows, prepare = (row) => row) => {
36
+ const tableName = table.tableName;
37
+ // Only rows that key really signed: a row can name any key in its signature field.
38
+ for (const row of rows.filter((row) => !opts.signedByKey || (0, keys_1.signedByKey)(row) === opts.signedByKey)) {
39
+ try {
40
+ if (opts.secretKey) {
41
+ const stamp = await (0, write_checks_1.signingStamp)(table, row, row);
42
+ await table.save((0, keys_1.addSignatureToObject)({ ...row, ...stamp }, opts.secretKey), snapshot);
43
+ }
44
+ else {
45
+ await table.signAndSave(prepare(row), snapshot);
46
+ }
47
+ result.resigned[tableName]++;
48
+ }
49
+ catch (error) {
50
+ result.refused.push({
51
+ table: tableName,
52
+ recordId: row[table.primaryKeyName],
53
+ error: String(error),
54
+ });
55
+ }
56
+ }
57
+ };
58
+ const groups = Groups(dataContext);
59
+ const group = await groups.get(groupId);
60
+ await resign(groups, group ? [group] : []);
61
+ const members = GroupMembers(dataContext);
62
+ await resign(members, await members.list({ groupId }),
63
+ // A rotation re-signs the grant as it is; otherwise bind it to the member's key as this
64
+ // device knows it (a grant from before keys were named, or to a key since replaced).
65
+ ({ publicKey: _named, ...rest }) => rest);
66
+ const packages = Packages(dataContext);
67
+ await resign(packages, await packages.list());
68
+ const versions = PackageVersions(dataContext);
69
+ await resign(versions, await versions.list());
70
+ return result;
71
+ }
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import type { DataContext } from "../context/data-context";
3
3
  import { GroupMemberRole } from "./group-member-roles";
4
+ import type { IResignGroupAuthorityOptions, IResignGroupAuthorityResult } from "./group-resign";
4
5
  import type { ISaveOptions } from "./orm/data-query";
5
6
  import { Table } from "./orm/table";
6
7
  /** Schema for a signed group record, including membership policy and group public keys. */
@@ -13,6 +14,7 @@ export declare const groupSchema: z.ZodObject<{
13
14
  disabled: z.ZodOptional<z.ZodBoolean>;
14
15
  founderUserId: z.ZodEffects<z.ZodString, string, string>;
15
16
  signature: z.ZodString;
17
+ signedAt: z.ZodOptional<z.ZodNumber>;
16
18
  publicKey: z.ZodString;
17
19
  publicBoxKey: z.ZodString;
18
20
  }, "strip", z.ZodTypeAny, {
@@ -25,6 +27,7 @@ export declare const groupSchema: z.ZodObject<{
25
27
  founderUserId: string;
26
28
  disabled?: boolean | undefined;
27
29
  iconClassName?: string | undefined;
30
+ signedAt?: number | undefined;
28
31
  publicRole?: GroupMemberRole | undefined;
29
32
  }, {
30
33
  name: string;
@@ -36,6 +39,7 @@ export declare const groupSchema: z.ZodObject<{
36
39
  founderUserId: string;
37
40
  disabled?: boolean | undefined;
38
41
  iconClassName?: string | undefined;
42
+ signedAt?: number | undefined;
39
43
  publicRole?: GroupMemberRole | undefined;
40
44
  }>;
41
45
  /** A signed group record shared between personal and group contexts. */
@@ -47,6 +51,13 @@ export declare class GroupsTable extends Table<IGroup> {
47
51
  static isPassthrough: boolean;
48
52
  save(group: IGroup, opts?: ISaveOptions): Promise<IGroup>;
49
53
  signAndSave(group: IGroup, opts?: ISaveOptions): Promise<IGroup>;
54
+ /**
55
+ * Re-sign this group's authority rows (group row, memberships, packages, package versions)
56
+ * with this device's key. See {@link resignGroupAuthority}. Only on a group context's table.
57
+ *
58
+ * @param opts - Limit to rows signed by one key.
59
+ */
60
+ resignAuthority(opts?: IResignGroupAuthorityOptions): Promise<IResignGroupAuthorityResult>;
50
61
  delete(groupId: string): Promise<void>;
51
62
  /** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
52
63
  static enableGroupSigning(fn: (group: IGroup) => IGroup): void;
@@ -39,12 +39,13 @@ exports.Groups = Groups;
39
39
  const zod_1 = require("zod");
40
40
  const user_context_singleton_1 = require("../context/user-context-singleton");
41
41
  const zod_types_1 = require("../types/zod-types");
42
+ const authority_write_checks_1 = require("./authority-write-checks");
42
43
  const group_member_roles_1 = require("./group-member-roles");
43
- const group_permissions_1 = require("./group-permissions");
44
44
  const decorators_1 = require("./orm/decorators");
45
45
  const table_1 = require("./orm/table");
46
46
  const table_definitions_system_1 = require("./orm/table-definitions.system");
47
47
  const types_1 = require("./orm/types");
48
+ const write_checks_1 = require("./orm/write-checks");
48
49
  /** Schema for a signed group record, including membership policy and group public keys. */
49
50
  exports.groupSchema = zod_1.z.object({
50
51
  groupId: zod_types_1.zodPeerId,
@@ -61,6 +62,10 @@ exports.groupSchema = zod_1.z.object({
61
62
  disabled: zod_1.z.boolean().optional().describe("Whether the group is currently disabled"),
62
63
  founderUserId: zod_types_1.zodPeerId.describe("The user ID of the group founder"),
63
64
  signature: zod_1.z.string().describe("The signed hash of this data excluding the signature itself"),
65
+ signedAt: zod_1.z
66
+ .number()
67
+ .optional()
68
+ .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"),
64
69
  publicKey: zod_1.z
65
70
  .string()
66
71
  .describe("The public key of the group used to verify a data has come from a group member with write permissions or higher"),
@@ -86,26 +91,20 @@ let GroupsTable = (() => {
86
91
  let _classSuper = table_1.Table;
87
92
  let _instanceExtraInitializers = [];
88
93
  let _signAndSave_decorators;
94
+ let _resignAuthority_decorators;
89
95
  return class GroupsTable extends _classSuper {
90
96
  static {
91
97
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
92
98
  _signAndSave_decorators = [(0, decorators_1.ProxyClientTableMethodCalls)()];
99
+ _resignAuthority_decorators = [(0, decorators_1.ProxyClientTableMethodCalls)()];
93
100
  __esDecorate(this, null, _signAndSave_decorators, { kind: "method", name: "signAndSave", static: false, private: false, access: { has: obj => "signAndSave" in obj, get: obj => obj.signAndSave }, metadata: _metadata }, null, _instanceExtraInitializers);
101
+ __esDecorate(this, null, _resignAuthority_decorators, { kind: "method", name: "resignAuthority", static: false, private: false, access: { has: obj => "resignAuthority" in obj, get: obj => obj.resignAuthority }, metadata: _metadata }, null, _instanceExtraInitializers);
94
102
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
95
103
  }
96
104
  static isPassthrough = false;
97
105
  async save(group, opts) {
98
- if (GroupsTable.isPassthrough) {
99
- return super.save(group, opts);
100
- }
101
- const userContext = await (0, user_context_singleton_1.getUserContext)();
102
- const userContextGroups = Groups(userContext.userDataContext);
103
- if (userContextGroups === this) {
104
- // users can do whatever they want to groups in their personal space
105
- return super.save(group, opts);
106
- }
107
- const oldGroup = await this.get(group.groupId);
108
- await (0, group_permissions_1.verifyGroupSignature)(group, oldGroup);
106
+ if (!GroupsTable.isPassthrough)
107
+ await (0, write_checks_1.checkLocalSave)(this, group, authority_write_checks_1.checkGroup);
109
108
  return super.save(group, opts);
110
109
  }
111
110
  async signAndSave(group, opts) {
@@ -113,9 +112,28 @@ let GroupsTable = (() => {
113
112
  if (!sign) {
114
113
  throw new Error("Group signing must be enabled to sign and save groups. Call GroupsTable.enableGroupSigning(fn) to enable it.");
115
114
  }
116
- group = sign.call(GroupsTable, group);
115
+ group = sign.call(GroupsTable, {
116
+ ...group,
117
+ ...(await (0, write_checks_1.signingStamp)(this, group)),
118
+ });
117
119
  return this.save(group, opts);
118
120
  }
121
+ /**
122
+ * Re-sign this group's authority rows (group row, memberships, packages, package versions)
123
+ * with this device's key. See {@link resignGroupAuthority}. Only on a group context's table.
124
+ *
125
+ * @param opts - Limit to rows signed by one key.
126
+ */
127
+ async resignAuthority(opts) {
128
+ const { resignGroupAuthority } = await Promise.resolve().then(() => require("./group-resign"));
129
+ if (!this.groupId) {
130
+ throw new Error("Re-sign a group from its own context, not the personal context");
131
+ }
132
+ // Signs with the registered signer only: a client never supplies a key.
133
+ return resignGroupAuthority((await (0, user_context_singleton_1.getUserContext)()).getDataContext(this.groupId), {
134
+ signedByKey: opts?.signedByKey,
135
+ });
136
+ }
119
137
  async delete(groupId) {
120
138
  const userContext = await (0, user_context_singleton_1.getUserContext)();
121
139
  const userContextGroups = Groups(userContext.userDataContext);
@@ -1,5 +1,6 @@
1
1
  export * from "./access-level";
2
2
  export * from "./assistants";
3
+ export * from "./authority-write-checks";
3
4
  export * from "./change-tracking";
4
5
  export * from "./channels";
5
6
  export * from "./create-group";
@@ -7,9 +8,11 @@ export * from "./device-sync-info";
7
8
  export * from "./devices";
8
9
  export * from "./embeddings";
9
10
  export * from "./files";
11
+ export * from "./group-authority";
10
12
  export * from "./group-member-roles";
11
13
  export * from "./group-members";
12
14
  export * from "./group-permissions";
15
+ export * from "./group-resign";
13
16
  export * from "./group-secret";
14
17
  export * from "./group-share";
15
18
  export * from "./groups";
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./access-level"), exports);
18
18
  __exportStar(require("./assistants"), exports);
19
+ __exportStar(require("./authority-write-checks"), exports);
19
20
  __exportStar(require("./change-tracking"), exports);
20
21
  __exportStar(require("./channels"), exports);
21
22
  __exportStar(require("./create-group"), exports);
@@ -23,9 +24,11 @@ __exportStar(require("./device-sync-info"), exports);
23
24
  __exportStar(require("./devices"), exports);
24
25
  __exportStar(require("./embeddings"), exports);
25
26
  __exportStar(require("./files"), exports);
27
+ __exportStar(require("./group-authority"), exports);
26
28
  __exportStar(require("./group-member-roles"), exports);
27
29
  __exportStar(require("./group-members"), exports);
28
30
  __exportStar(require("./group-permissions"), exports);
31
+ __exportStar(require("./group-resign"), exports);
29
32
  __exportStar(require("./group-secret"), exports);
30
33
  __exportStar(require("./group-share"), exports);
31
34
  __exportStar(require("./groups"), exports);