@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.
- package/dist/context/user-context.d.ts +7 -1
- package/dist/context/user-context.js +4 -9
- package/dist/contracts/contract-proxy.d.ts +5 -0
- package/dist/contracts/contract-proxy.js +2 -2
- package/dist/contracts/contract-resolvers.d.ts +2 -2
- package/dist/contracts/contract-resolvers.js +14 -15
- package/dist/contracts/system/network-contracts.d.ts +1 -1
- package/dist/contracts/tool-catalog-service.js +1 -1
- package/dist/data/assistants.d.ts +3 -3
- package/dist/data/authority-write-checks.d.ts +42 -0
- package/dist/data/authority-write-checks.js +324 -0
- package/dist/data/change-tracking.d.ts +4 -4
- package/dist/data/create-group.js +1 -0
- package/dist/data/devices.d.ts +27 -4
- package/dist/data/devices.js +31 -0
- package/dist/data/group-authority.d.ts +111 -0
- package/dist/data/group-authority.js +281 -0
- package/dist/data/group-members.d.ts +8 -2
- package/dist/data/group-members.js +41 -18
- package/dist/data/group-permissions.d.ts +15 -31
- package/dist/data/group-permissions.js +25 -95
- package/dist/data/group-resign.d.ts +38 -0
- package/dist/data/group-resign.js +71 -0
- package/dist/data/groups.d.ts +11 -0
- package/dist/data/groups.js +31 -13
- package/dist/data/index.d.ts +3 -0
- package/dist/data/index.js +3 -0
- package/dist/data/invites.d.ts +14 -0
- package/dist/data/orm/index.d.ts +1 -0
- package/dist/data/orm/index.js +1 -0
- package/dist/data/orm/sql.data-source.d.ts +2 -2
- package/dist/data/orm/sql.data-source.js +24 -22
- package/dist/data/orm/table-container.js +20 -11
- package/dist/data/orm/write-checks.d.ts +89 -0
- package/dist/data/orm/write-checks.js +115 -0
- package/dist/data/package-table-identity.d.ts +21 -0
- package/dist/data/package-table-identity.js +87 -0
- package/dist/data/package-version-resolver.d.ts +15 -0
- package/dist/data/package-version-resolver.js +70 -3
- package/dist/data/package-versions.d.ts +20 -14
- package/dist/data/package-versions.js +15 -9
- package/dist/data/packages.d.ts +6 -1
- package/dist/data/packages.js +15 -20
- package/dist/data/persistent-vars.js +2 -39
- package/dist/data/sync-recovery-queue.d.ts +4 -4
- package/dist/data/sync-recovery-queue.js +7 -1
- package/dist/data/tool-access-approvals.d.ts +3 -3
- package/dist/data/user-permissions.d.ts +33 -2
- package/dist/data/user-permissions.js +87 -10
- package/dist/data/users.d.ts +49 -12
- package/dist/data/users.js +21 -1
- package/dist/data/workflows.d.ts +1 -1
- package/dist/define/index.d.ts +1 -1
- package/dist/define/index.js +2 -1
- package/dist/define/manifest.js +9 -1
- package/dist/define/table.d.ts +15 -0
- package/dist/device/binary-peer-connection-v2.d.ts +1 -1
- package/dist/device/binary-peer-connection-v2.js +1 -1
- package/dist/device/connection.d.ts +1 -1
- package/dist/device/frame-cipher.d.ts +1 -1
- package/dist/device/frame-cipher.js +1 -1
- package/dist/device/get-trust-level-fn.js +3 -6
- package/dist/device-pairing/types.d.ts +26 -26
- package/dist/identity/user-row-change.d.ts +3 -1
- package/dist/identity/user-row-change.js +33 -18
- package/dist/invites/group-approval.js +23 -24
- package/dist/invites/invite-messages.d.ts +250 -28
- package/dist/invites/invite-token.d.ts +18 -0
- package/dist/invites/invite-token.js +2 -0
- package/dist/keys.d.ts +7 -0
- package/dist/keys.js +27 -0
- package/dist/package-installer/package-installer.d.ts +1 -1
- package/dist/package-installer/package-installer.js +8 -3
- package/dist/package-installer/package-propagation.d.ts +2 -0
- package/dist/package-installer/package-propagation.js +20 -38
- package/dist/package-installer/package-remote-checker.js +15 -3
- package/dist/package-loader/isolated-artifact.d.ts +14 -0
- package/dist/package-loader/isolated-artifact.js +3 -3
- package/dist/package-loader/isolated-runtime.d.ts +3 -1
- package/dist/package-loader/isolated-table-gateway.js +4 -1
- package/dist/package-loader/package-loader.js +6 -0
- package/dist/peer-id.d.ts +19 -0
- package/dist/peer-id.js +41 -0
- package/dist/types/peer-device.d.ts +2 -0
- package/dist/types/workflow.d.ts +2 -2
- package/dist/utils.js +13 -32
- package/package.json +1 -1
- package/dist/data/package-permissions.d.ts +0 -14
- package/dist/data/package-permissions.js +0 -26
- package/dist/data/package-version-permissions.d.ts +0 -14
- package/dist/data/package-version-permissions.js +0 -30
|
@@ -41,12 +41,12 @@ const user_context_singleton_1 = require("../context/user-context-singleton");
|
|
|
41
41
|
const decorators_1 = require("../data/orm/decorators");
|
|
42
42
|
const zod_types_1 = require("../types/zod-types");
|
|
43
43
|
const utils_1 = require("../utils");
|
|
44
|
+
const authority_write_checks_1 = require("./authority-write-checks");
|
|
44
45
|
const group_member_roles_1 = require("./group-member-roles");
|
|
45
|
-
const group_permissions_1 = require("./group-permissions");
|
|
46
46
|
const table_1 = require("./orm/table");
|
|
47
47
|
const table_definitions_system_1 = require("./orm/table-definitions.system");
|
|
48
48
|
const types_1 = require("./orm/types");
|
|
49
|
-
const
|
|
49
|
+
const write_checks_1 = require("./orm/write-checks");
|
|
50
50
|
/** Schema for a signed group membership, including its role and soft deletion state. */
|
|
51
51
|
exports.groupMemberSchema = zod_1.z.object({
|
|
52
52
|
groupMemberId: zod_types_1.zodPeerId,
|
|
@@ -55,7 +55,15 @@ exports.groupMemberSchema = zod_1.z.object({
|
|
|
55
55
|
role: zod_1.z
|
|
56
56
|
.nativeEnum(group_member_roles_1.GroupMemberRole)
|
|
57
57
|
.describe('The role of the user in the group. Use "none" to explicitly block a user'),
|
|
58
|
+
publicKey: zod_1.z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("The member's signing key this grant is for. The role counts only for this key, or a key it handed over to (the member's keySuccession)"),
|
|
58
62
|
signature: zod_1.z.string().describe("The signed hash of this data excluding the signature itself"),
|
|
63
|
+
signedAt: zod_1.z
|
|
64
|
+
.number()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("When the signer signed this row (ms since epoch), under the signature: a copy of an earlier row can't replace a later one"),
|
|
59
67
|
deleted: zod_1.z
|
|
60
68
|
.boolean()
|
|
61
69
|
.optional()
|
|
@@ -70,6 +78,12 @@ const metaData = {
|
|
|
70
78
|
fields: (0, types_1.schemaToFields)(exports.groupMemberSchema),
|
|
71
79
|
indexes: [{ fields: ["groupId", "userId"], unique: true }],
|
|
72
80
|
};
|
|
81
|
+
/** A member's key as this device knows it: from their verified profile row. */
|
|
82
|
+
async function knownKeyOf(groupMember) {
|
|
83
|
+
const { getGroupAuthority } = await Promise.resolve().then(() => require("./group-authority"));
|
|
84
|
+
const { users } = await getGroupAuthority(await (0, user_context_singleton_1.getUserContext)(), groupMember.groupId);
|
|
85
|
+
return users.get(groupMember.userId)?.publicKey;
|
|
86
|
+
}
|
|
73
87
|
// Symbol.for keeps signer registration shared by separately installed SDK copies.
|
|
74
88
|
const signingKey = Symbol.for("@peers-app/peers-sdk/group-member-signing");
|
|
75
89
|
const signingGlobals = globalThis;
|
|
@@ -87,27 +101,35 @@ let GroupMembersTable = (() => {
|
|
|
87
101
|
}
|
|
88
102
|
static isPassthrough = false;
|
|
89
103
|
async save(groupMember, opts) {
|
|
90
|
-
if (GroupMembersTable.isPassthrough) {
|
|
91
|
-
|
|
104
|
+
if (!GroupMembersTable.isPassthrough) {
|
|
105
|
+
if (groupMember.groupId === (await (0, user_context_singleton_1.getUserContext)()).userId) {
|
|
106
|
+
throw new Error("Group members cannot be added to personal user contexts");
|
|
107
|
+
}
|
|
108
|
+
await (0, write_checks_1.checkLocalSave)(this, groupMember, authority_write_checks_1.checkGroupMember);
|
|
92
109
|
}
|
|
93
|
-
|
|
94
|
-
if (groupMember.groupId === me.userId) {
|
|
95
|
-
throw new Error("Group members cannot be added to personal user contexts");
|
|
96
|
-
}
|
|
97
|
-
const oldGroupMember = groupMember.groupMemberId
|
|
98
|
-
? await this.get(groupMember.groupMemberId)
|
|
99
|
-
: undefined;
|
|
100
|
-
if (await (0, group_permissions_1.isGroupMemberSignatureValid)(groupMember.groupId, groupMember, oldGroupMember)) {
|
|
101
|
-
return super.save(groupMember, opts);
|
|
102
|
-
}
|
|
103
|
-
throw new Error("Group member signature is not valid");
|
|
110
|
+
return super.save(groupMember, opts);
|
|
104
111
|
}
|
|
105
112
|
async signAndSave(groupMember, opts) {
|
|
106
113
|
const sign = signingGlobals[signingKey];
|
|
107
114
|
if (!sign) {
|
|
108
115
|
throw new Error("Group member signing must be enabled to sign and save group members. Call GroupMembersTable.enableGroupMemberSigning(fn) to enable it.");
|
|
109
116
|
}
|
|
110
|
-
|
|
117
|
+
const current = (groupMember.groupMemberId ? await this.get(groupMember.groupMemberId) : undefined) ??
|
|
118
|
+
(await this.findOne({ groupId: groupMember.groupId, userId: groupMember.userId }));
|
|
119
|
+
// A key the caller names, else the member's key as this device knows it, else the stored
|
|
120
|
+
// one. A block (role None) applies whatever the key.
|
|
121
|
+
const granted = groupMember.role > group_member_roles_1.GroupMemberRole.None;
|
|
122
|
+
const publicKey = groupMember.publicKey ??
|
|
123
|
+
(granted ? await knownKeyOf(groupMember) : undefined) ??
|
|
124
|
+
current?.publicKey;
|
|
125
|
+
if (!publicKey && granted) {
|
|
126
|
+
throw new Error(`No signed profile for user ${groupMember.userId} on this device: pass the member's publicKey`);
|
|
127
|
+
}
|
|
128
|
+
groupMember = sign.call(GroupMembersTable, {
|
|
129
|
+
...groupMember,
|
|
130
|
+
...(publicKey ? { publicKey } : {}),
|
|
131
|
+
...(await (0, write_checks_1.signingStamp)(this, groupMember, current)),
|
|
132
|
+
});
|
|
111
133
|
return this.save(groupMember, {
|
|
112
134
|
saveAsSnapshot: true,
|
|
113
135
|
restoreIfDeleted: true,
|
|
@@ -130,8 +152,8 @@ let GroupMembersTable = (() => {
|
|
|
130
152
|
* Settle two membership rows for the same user in the same group: devices that approved the
|
|
131
153
|
* same join at once could each write one, under a different id.
|
|
132
154
|
*
|
|
133
|
-
* Merged only when both rows grant the same access (role and deleted state), so nothing
|
|
134
|
-
* the member's access changes. The id {@link deterministicGroupMemberId} gives wins if either
|
|
155
|
+
* Merged only when both rows grant the same access (role, key and deleted state), so nothing
|
|
156
|
+
* about the member's access changes. The id {@link deterministicGroupMemberId} gives wins if either
|
|
135
157
|
* row has it, otherwise the lexicographically smaller id; the winner is kept as it is, signature
|
|
136
158
|
* included. Rows that disagree stay queued for a person. A pure function of the two rows, so
|
|
137
159
|
* every device keeps the same one.
|
|
@@ -144,6 +166,7 @@ let GroupMembersTable = (() => {
|
|
|
144
166
|
return undefined;
|
|
145
167
|
}
|
|
146
168
|
if (incoming.role !== existing.role ||
|
|
169
|
+
incoming.publicKey !== existing.publicKey ||
|
|
147
170
|
Boolean(incoming.deleted) !== Boolean(existing.deleted)) {
|
|
148
171
|
return undefined;
|
|
149
172
|
}
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import type { DataContext } from "../context/data-context";
|
|
2
2
|
import type { UserContext } from "../context/user-context";
|
|
3
3
|
import { GroupMemberRole } from "./group-member-roles";
|
|
4
|
-
import type { IGroupMember } from "./group-members";
|
|
5
|
-
import type { IGroup } from "./groups";
|
|
6
4
|
export { GroupMemberRole };
|
|
7
5
|
/**
|
|
8
6
|
* Return a user's {@link GroupMemberRole} in `groupId`.
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* In a group, roles come from the group's verified authority ({@link getGroupAuthority}): only
|
|
9
|
+
* memberships and settings whose signers could grant them count, so a row synced from a member
|
|
10
|
+
* who changed their own copy of Peers can't raise anyone's role. A group this device doesn't
|
|
11
|
+
* have gives {@link GroupMemberRole.None}.
|
|
12
|
+
*
|
|
13
|
+
* In the personal context (`groupId` is the user's own id), the user is
|
|
14
|
+
* {@link GroupMemberRole.Founder}, and so is a user they trust as Self; anyone else is None.
|
|
15
|
+
*
|
|
16
|
+
* Pass `publicKey` for a remote caller: a role belongs to the key it was granted to, so a
|
|
17
|
+
* device that authenticated with any other key gets the group's public role (see
|
|
18
|
+
* `roleInGroupAuthority`). Local lookups leave it out.
|
|
12
19
|
*
|
|
13
20
|
* Prefer passing `userContext` from the provider that owns the data. The
|
|
14
21
|
* process singleton is only a fallback and is not awaited — multi-device
|
|
@@ -17,18 +24,10 @@ export { GroupMemberRole };
|
|
|
17
24
|
* @param groupId - Group id, or the user's id for their personal context.
|
|
18
25
|
* @param userId - User whose role is requested.
|
|
19
26
|
* @param userContext - Provider-owned context. Falls back to {@link peekUserContext}.
|
|
27
|
+
* @param publicKey - Signing key a remote caller authenticated with.
|
|
20
28
|
* @throws If neither an explicit context nor the process singleton is available.
|
|
21
29
|
*/
|
|
22
|
-
export declare function getUserRole(groupId: string, userId: string, userContext?: UserContext): Promise<GroupMemberRole>;
|
|
23
|
-
/**
|
|
24
|
-
* Resolve a group role from the signing public key of a Users record.
|
|
25
|
-
*
|
|
26
|
-
* Uses {@link peekUserContext}; throws if the process singleton is unset.
|
|
27
|
-
*
|
|
28
|
-
* @param groupId - Group or personal context id.
|
|
29
|
-
* @param publicKey - User public key that signed a record.
|
|
30
|
-
*/
|
|
31
|
-
export declare function getUserRoleFromPublicKey(groupId: string, publicKey: string): Promise<GroupMemberRole>;
|
|
30
|
+
export declare function getUserRole(groupId: string, userId: string, userContext?: UserContext, publicKey?: string): Promise<GroupMemberRole>;
|
|
32
31
|
/**
|
|
33
32
|
* Return the current user's access level in `dataContext`.
|
|
34
33
|
*
|
|
@@ -42,21 +41,6 @@ export declare function getCurrentUserAccessLevel(dataContext: DataContext): Pro
|
|
|
42
41
|
*
|
|
43
42
|
* @param dataContext - Context whose group/personal role is evaluated.
|
|
44
43
|
* @param userId - User to resolve.
|
|
44
|
+
* @param publicKey - Signing key a remote caller authenticated with (see {@link getUserRole}).
|
|
45
45
|
*/
|
|
46
|
-
export declare function getUserAccessLevel(dataContext: DataContext, userId: string): Promise<number>;
|
|
47
|
-
/**
|
|
48
|
-
* Verify that a group record was signed by an Admin+ member (Founder to change founder).
|
|
49
|
-
*
|
|
50
|
-
* @param group - Proposed group record.
|
|
51
|
-
* @param oldGroup - Previous record when this is an update.
|
|
52
|
-
*/
|
|
53
|
-
export declare function verifyGroupSignature(group: IGroup, oldGroup?: IGroup): Promise<void>;
|
|
54
|
-
/**
|
|
55
|
-
* Return whether a group-member write is signed by someone allowed to make it.
|
|
56
|
-
*
|
|
57
|
-
* @param groupId - Group the membership belongs to.
|
|
58
|
-
* @param newGroupMember - Proposed membership row.
|
|
59
|
-
* @param oldGroupMember - Previous row when this is an update.
|
|
60
|
-
* @param signerRole - Optional precomputed signer role.
|
|
61
|
-
*/
|
|
62
|
-
export declare function isGroupMemberSignatureValid(groupId: string, newGroupMember: IGroupMember, oldGroupMember?: IGroupMember, signerRole?: GroupMemberRole): Promise<boolean>;
|
|
46
|
+
export declare function getUserAccessLevel(dataContext: DataContext, userId: string, publicKey?: string): Promise<number>;
|
|
@@ -2,21 +2,28 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GroupMemberRole = void 0;
|
|
4
4
|
exports.getUserRole = getUserRole;
|
|
5
|
-
exports.getUserRoleFromPublicKey = getUserRoleFromPublicKey;
|
|
6
5
|
exports.getCurrentUserAccessLevel = getCurrentUserAccessLevel;
|
|
7
6
|
exports.getUserAccessLevel = getUserAccessLevel;
|
|
8
|
-
exports.verifyGroupSignature = verifyGroupSignature;
|
|
9
|
-
exports.isGroupMemberSignatureValid = isGroupMemberSignatureValid;
|
|
10
7
|
const context_1 = require("../context");
|
|
11
|
-
const
|
|
8
|
+
const socket_type_1 = require("../device/socket.type");
|
|
12
9
|
const access_level_1 = require("./access-level");
|
|
10
|
+
const group_authority_1 = require("./group-authority");
|
|
13
11
|
const group_member_roles_1 = require("./group-member-roles");
|
|
14
12
|
Object.defineProperty(exports, "GroupMemberRole", { enumerable: true, get: function () { return group_member_roles_1.GroupMemberRole; } });
|
|
15
13
|
/**
|
|
16
14
|
* Return a user's {@link GroupMemberRole} in `groupId`.
|
|
17
15
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* In a group, roles come from the group's verified authority ({@link getGroupAuthority}): only
|
|
17
|
+
* memberships and settings whose signers could grant them count, so a row synced from a member
|
|
18
|
+
* who changed their own copy of Peers can't raise anyone's role. A group this device doesn't
|
|
19
|
+
* have gives {@link GroupMemberRole.None}.
|
|
20
|
+
*
|
|
21
|
+
* In the personal context (`groupId` is the user's own id), the user is
|
|
22
|
+
* {@link GroupMemberRole.Founder}, and so is a user they trust as Self; anyone else is None.
|
|
23
|
+
*
|
|
24
|
+
* Pass `publicKey` for a remote caller: a role belongs to the key it was granted to, so a
|
|
25
|
+
* device that authenticated with any other key gets the group's public role (see
|
|
26
|
+
* `roleInGroupAuthority`). Local lookups leave it out.
|
|
20
27
|
*
|
|
21
28
|
* Prefer passing `userContext` from the provider that owns the data. The
|
|
22
29
|
* process singleton is only a fallback and is not awaited — multi-device
|
|
@@ -25,51 +32,23 @@ Object.defineProperty(exports, "GroupMemberRole", { enumerable: true, get: funct
|
|
|
25
32
|
* @param groupId - Group id, or the user's id for their personal context.
|
|
26
33
|
* @param userId - User whose role is requested.
|
|
27
34
|
* @param userContext - Provider-owned context. Falls back to {@link peekUserContext}.
|
|
35
|
+
* @param publicKey - Signing key a remote caller authenticated with.
|
|
28
36
|
* @throws If neither an explicit context nor the process singleton is available.
|
|
29
37
|
*/
|
|
30
|
-
async function getUserRole(groupId, userId, userContext) {
|
|
31
|
-
const { Groups } = await Promise.resolve().then(() => require("./groups"));
|
|
32
|
-
const { GroupMembers } = await Promise.resolve().then(() => require("./group-members"));
|
|
38
|
+
async function getUserRole(groupId, userId, userContext, publicKey) {
|
|
33
39
|
const ctx = userContext ?? (0, context_1.peekUserContext)();
|
|
34
40
|
if (!ctx) {
|
|
35
41
|
throw new Error("User context is not available");
|
|
36
42
|
}
|
|
37
43
|
await ctx.loadingPromise;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return group_member_roles_1.GroupMemberRole.Founder;
|
|
44
|
+
if (groupId === ctx.userId) {
|
|
45
|
+
if (userId === ctx.userId)
|
|
46
|
+
return group_member_roles_1.GroupMemberRole.Founder;
|
|
47
|
+
const { getUserTrustLevel } = await Promise.resolve().then(() => require("./user-trust-levels"));
|
|
48
|
+
const trust = await getUserTrustLevel(ctx.userDataContext, userId);
|
|
49
|
+
return trust === socket_type_1.TrustLevel.Self ? group_member_roles_1.GroupMemberRole.Founder : group_member_roles_1.GroupMemberRole.None;
|
|
44
50
|
}
|
|
45
|
-
|
|
46
|
-
if (groupMembership) {
|
|
47
|
-
return groupMembership.role;
|
|
48
|
-
}
|
|
49
|
-
return group.publicRole || group_member_roles_1.GroupMemberRole.None;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Resolve a group role from the signing public key of a Users record.
|
|
53
|
-
*
|
|
54
|
-
* Uses {@link peekUserContext}; throws if the process singleton is unset.
|
|
55
|
-
*
|
|
56
|
-
* @param groupId - Group or personal context id.
|
|
57
|
-
* @param publicKey - User public key that signed a record.
|
|
58
|
-
*/
|
|
59
|
-
async function getUserRoleFromPublicKey(groupId, publicKey) {
|
|
60
|
-
const { Users } = await Promise.resolve().then(() => require("./users"));
|
|
61
|
-
const userContext = (0, context_1.peekUserContext)();
|
|
62
|
-
if (!userContext) {
|
|
63
|
-
throw new Error("User context is not available");
|
|
64
|
-
}
|
|
65
|
-
await userContext.loadingPromise;
|
|
66
|
-
const groupDataContext = userContext.getDataContext(groupId);
|
|
67
|
-
const user = (await Users(groupDataContext).findOne({ publicKey })) ||
|
|
68
|
-
(await Users(userContext.userDataContext).findOne({ publicKey }));
|
|
69
|
-
if (user) {
|
|
70
|
-
return getUserRole(groupId, user?.userId, userContext);
|
|
71
|
-
}
|
|
72
|
-
return group_member_roles_1.GroupMemberRole.None;
|
|
51
|
+
return (0, group_authority_1.roleInGroupAuthority)(await (0, group_authority_1.getGroupAuthority)(ctx, groupId), userId, publicKey);
|
|
73
52
|
}
|
|
74
53
|
/**
|
|
75
54
|
* Return the current user's access level in `dataContext`.
|
|
@@ -87,59 +66,10 @@ async function getCurrentUserAccessLevel(dataContext) {
|
|
|
87
66
|
*
|
|
88
67
|
* @param dataContext - Context whose group/personal role is evaluated.
|
|
89
68
|
* @param userId - User to resolve.
|
|
69
|
+
* @param publicKey - Signing key a remote caller authenticated with (see {@link getUserRole}).
|
|
90
70
|
*/
|
|
91
|
-
async function getUserAccessLevel(dataContext, userId) {
|
|
71
|
+
async function getUserAccessLevel(dataContext, userId, publicKey) {
|
|
92
72
|
const contextKey = dataContext.groupId ?? dataContext.userContext.userId;
|
|
93
|
-
const role = await getUserRole(contextKey, userId, dataContext.userContext);
|
|
73
|
+
const role = await getUserRole(contextKey, userId, dataContext.userContext, publicKey);
|
|
94
74
|
return (0, access_level_1.accessLevelFromGroupRole)(role);
|
|
95
75
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Verify that a group record was signed by an Admin+ member (Founder to change founder).
|
|
98
|
-
*
|
|
99
|
-
* @param group - Proposed group record.
|
|
100
|
-
* @param oldGroup - Previous record when this is an update.
|
|
101
|
-
*/
|
|
102
|
-
async function verifyGroupSignature(group, oldGroup) {
|
|
103
|
-
(0, keys_1.verifyObjectSignature)(group);
|
|
104
|
-
const publicKey = (0, keys_1.getPublicKeyFromObjectSignature)(group) ?? "";
|
|
105
|
-
const signerRole = await getUserRoleFromPublicKey(group.groupId, publicKey);
|
|
106
|
-
if (oldGroup &&
|
|
107
|
-
oldGroup.founderUserId !== group.founderUserId &&
|
|
108
|
-
signerRole < group_member_roles_1.GroupMemberRole.Founder) {
|
|
109
|
-
throw new Error("Only group founders can change the founder of the group");
|
|
110
|
-
}
|
|
111
|
-
if (signerRole < group_member_roles_1.GroupMemberRole.Admin) {
|
|
112
|
-
throw new Error("Only group admins can modify group settings");
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Return whether a group-member write is signed by someone allowed to make it.
|
|
117
|
-
*
|
|
118
|
-
* @param groupId - Group the membership belongs to.
|
|
119
|
-
* @param newGroupMember - Proposed membership row.
|
|
120
|
-
* @param oldGroupMember - Previous row when this is an update.
|
|
121
|
-
* @param signerRole - Optional precomputed signer role.
|
|
122
|
-
*/
|
|
123
|
-
async function isGroupMemberSignatureValid(groupId, newGroupMember, oldGroupMember, signerRole) {
|
|
124
|
-
oldGroupMember ??= newGroupMember;
|
|
125
|
-
// Basic signature verification
|
|
126
|
-
if (!(0, keys_1.isObjectSignatureValid)(newGroupMember)) {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
// Get the public key that signed this group member
|
|
130
|
-
const signerPublicKey = (0, keys_1.getPublicKeyFromObjectSignature)(newGroupMember);
|
|
131
|
-
if (!signerPublicKey) {
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
// Determine signer's role in the group using the user-based permission system
|
|
135
|
-
signerRole ??= await getUserRoleFromPublicKey(groupId, signerPublicKey);
|
|
136
|
-
// Validate permissions for group member modifications
|
|
137
|
-
if (signerRole < group_member_roles_1.GroupMemberRole.Admin) {
|
|
138
|
-
return false; // Only group admins or above can modify group membership
|
|
139
|
-
}
|
|
140
|
-
// admins or above can only modify roles up to their own level
|
|
141
|
-
if (signerRole < newGroupMember.role || signerRole < oldGroupMember.role) {
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
@@ -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
|
+
}
|
package/dist/data/groups.d.ts
CHANGED
|
@@ -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;
|
package/dist/data/groups.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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);
|
package/dist/data/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/data/index.js
CHANGED
|
@@ -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);
|