@peers-app/peers-sdk 0.23.1 → 0.23.4
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.
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UserContext } from "../context/user-context";
|
|
1
2
|
import { type IGroupMember } from "./group-members";
|
|
2
3
|
import { type IGroup } from "./groups";
|
|
3
4
|
/**
|
|
@@ -16,6 +17,18 @@ export interface IGroupShare {
|
|
|
16
17
|
* @throws Error if group not found or user lacks permission
|
|
17
18
|
*/
|
|
18
19
|
export declare function generateGroupShare(groupId: string): Promise<IGroupShare>;
|
|
20
|
+
/**
|
|
21
|
+
* Write a group record and missing membership rows into the group's data context.
|
|
22
|
+
*
|
|
23
|
+
* Uses the lowest data source so bootstrap can seed already-signed rows without
|
|
24
|
+
* re-running table permission checks. Existing memberships for the same user
|
|
25
|
+
* are left in place so retries can repair a partial import.
|
|
26
|
+
*
|
|
27
|
+
* @param userContext - Joiner or importer whose group context should be seeded
|
|
28
|
+
* @param groupShare - Canonical group plus Admin+ memberships
|
|
29
|
+
* @param extraMembers - Additional signed rows such as a newly approved Reader
|
|
30
|
+
*/
|
|
31
|
+
export declare function seedGroupContextFromShare(userContext: UserContext, groupShare: Pick<IGroupShare, "group" | "groupMembers">, extraMembers?: IGroupMember[]): Promise<void>;
|
|
19
32
|
/**
|
|
20
33
|
* Validates that an object has the correct GroupShare structure
|
|
21
34
|
* @param obj Object to validate
|
package/dist/data/group-share.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateGroupShare = generateGroupShare;
|
|
4
|
+
exports.seedGroupContextFromShare = seedGroupContextFromShare;
|
|
4
5
|
exports.validateGroupShare = validateGroupShare;
|
|
5
6
|
exports.estimateGroupShareSize = estimateGroupShareSize;
|
|
6
7
|
exports.checkQRCodeCompatibility = checkQRCodeCompatibility;
|
|
@@ -57,7 +58,7 @@ async function importGroupShare(groupShareJson) {
|
|
|
57
58
|
throw new Error("Invalid GroupShare structure");
|
|
58
59
|
}
|
|
59
60
|
const userContext = await (0, user_context_singleton_1.getUserContext)();
|
|
60
|
-
const { group
|
|
61
|
+
const { group } = groupShare;
|
|
61
62
|
// Check if group already exists
|
|
62
63
|
const existingGroup = await (0, groups_1.Groups)(userContext.userDataContext).get(group.groupId);
|
|
63
64
|
if (existingGroup) {
|
|
@@ -65,19 +66,42 @@ async function importGroupShare(groupShareJson) {
|
|
|
65
66
|
}
|
|
66
67
|
// Save group to user's groups table
|
|
67
68
|
await (0, groups_1.Groups)(userContext.userDataContext).save(group, { restoreIfDeleted: true });
|
|
68
|
-
|
|
69
|
+
await seedGroupContextFromShare(userContext, groupShare);
|
|
70
|
+
return group.groupId;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Write a group record and missing membership rows into the group's data context.
|
|
74
|
+
*
|
|
75
|
+
* Uses the lowest data source so bootstrap can seed already-signed rows without
|
|
76
|
+
* re-running table permission checks. Existing memberships for the same user
|
|
77
|
+
* are left in place so retries can repair a partial import.
|
|
78
|
+
*
|
|
79
|
+
* @param userContext - Joiner or importer whose group context should be seeded
|
|
80
|
+
* @param groupShare - Canonical group plus Admin+ memberships
|
|
81
|
+
* @param extraMembers - Additional signed rows such as a newly approved Reader
|
|
82
|
+
*/
|
|
83
|
+
async function seedGroupContextFromShare(userContext, groupShare, extraMembers = []) {
|
|
84
|
+
const { group, groupMembers } = groupShare;
|
|
69
85
|
const groupDataContext = userContext.getDataContext(group.groupId);
|
|
70
|
-
// Save the group data to the group's own context
|
|
71
86
|
const groupsTable = (0, groups_1.Groups)(groupDataContext);
|
|
72
|
-
|
|
73
|
-
await groupsTableDataSource.save(group);
|
|
74
|
-
// Save all admin members as seed data for device recognition
|
|
87
|
+
await getLowestDataSource(groupsTable).save(group);
|
|
75
88
|
const groupMembersTable = (0, group_members_1.GroupMembers)(groupDataContext);
|
|
76
89
|
const groupMembersDataSource = getLowestDataSource(groupMembersTable);
|
|
77
|
-
|
|
90
|
+
const seenUserIds = new Set();
|
|
91
|
+
for (const member of [...groupMembers, ...extraMembers]) {
|
|
92
|
+
if (!member?.userId || member.groupId !== group.groupId || seenUserIds.has(member.userId)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
seenUserIds.add(member.userId);
|
|
96
|
+
const existing = await groupMembersTable.findOne({
|
|
97
|
+
groupId: group.groupId,
|
|
98
|
+
userId: member.userId,
|
|
99
|
+
});
|
|
100
|
+
if (existing) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
78
103
|
await groupMembersDataSource.save(member);
|
|
79
104
|
}
|
|
80
|
-
return group.groupId;
|
|
81
105
|
}
|
|
82
106
|
/**
|
|
83
107
|
* Validates that an object has the correct GroupShare structure
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const SQLiteDB = require("better-sqlite3");
|
|
4
|
+
const user_context_1 = require("../context/user-context");
|
|
5
|
+
const utils_1 = require("../utils");
|
|
6
|
+
const group_member_roles_1 = require("./group-member-roles");
|
|
7
|
+
const group_members_1 = require("./group-members");
|
|
8
|
+
const group_share_1 = require("./group-share");
|
|
9
|
+
const groups_1 = require("./groups");
|
|
10
|
+
const sql_data_source_1 = require("./orm/sql.data-source");
|
|
11
|
+
class DBHarness {
|
|
12
|
+
_db = null;
|
|
13
|
+
get db() {
|
|
14
|
+
if (!this._db) {
|
|
15
|
+
this._db = new SQLiteDB(":memory:");
|
|
16
|
+
this._db.pragma("journal_mode = WAL");
|
|
17
|
+
}
|
|
18
|
+
return this._db;
|
|
19
|
+
}
|
|
20
|
+
async get(sql, params = []) {
|
|
21
|
+
return this.db.prepare(sql).get(params);
|
|
22
|
+
}
|
|
23
|
+
async all(sql, params = []) {
|
|
24
|
+
return this.db.prepare(sql).all(params);
|
|
25
|
+
}
|
|
26
|
+
async exec(sql, params = []) {
|
|
27
|
+
const result = this.db.prepare(sql).run(params);
|
|
28
|
+
return { changes: result.changes };
|
|
29
|
+
}
|
|
30
|
+
async close() {
|
|
31
|
+
this._db?.close();
|
|
32
|
+
this._db = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
describe("seedGroupContextFromShare", () => {
|
|
36
|
+
const dbs = new Map();
|
|
37
|
+
let userContext;
|
|
38
|
+
const userId = (0, utils_1.newid)();
|
|
39
|
+
beforeAll(async () => {
|
|
40
|
+
groups_1.GroupsTable.isPassthrough = true;
|
|
41
|
+
group_members_1.GroupMembersTable.isPassthrough = true;
|
|
42
|
+
const dataSourceFactory = (metaData, schema, groupId) => {
|
|
43
|
+
const key = groupId || userId;
|
|
44
|
+
let db = dbs.get(key);
|
|
45
|
+
if (!db) {
|
|
46
|
+
db = new DBHarness();
|
|
47
|
+
dbs.set(key, db);
|
|
48
|
+
}
|
|
49
|
+
return new sql_data_source_1.SQLDataSource(db, metaData, schema);
|
|
50
|
+
};
|
|
51
|
+
userContext = new user_context_1.UserContext(userId, dataSourceFactory, true);
|
|
52
|
+
await userContext.loadingPromise;
|
|
53
|
+
userContext.deviceId((0, utils_1.newid)());
|
|
54
|
+
});
|
|
55
|
+
afterAll(async () => {
|
|
56
|
+
groups_1.GroupsTable.isPassthrough = false;
|
|
57
|
+
group_members_1.GroupMembersTable.isPassthrough = false;
|
|
58
|
+
userContext.dispose();
|
|
59
|
+
await Promise.all([...dbs.values()].map((db) => db.close()));
|
|
60
|
+
});
|
|
61
|
+
function makeGroup() {
|
|
62
|
+
return {
|
|
63
|
+
groupId: (0, utils_1.newid)(),
|
|
64
|
+
name: "Home",
|
|
65
|
+
description: "",
|
|
66
|
+
founderUserId: (0, utils_1.newid)(),
|
|
67
|
+
signature: "group-sig",
|
|
68
|
+
publicKey: "pk",
|
|
69
|
+
publicBoxKey: "pbk",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function makeMember(groupId, memberUserId, role) {
|
|
73
|
+
return {
|
|
74
|
+
groupMemberId: (0, utils_1.newid)(),
|
|
75
|
+
groupId,
|
|
76
|
+
userId: memberUserId,
|
|
77
|
+
role,
|
|
78
|
+
signature: "member-sig",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
it("seeds the group record and missing memberships, including extra Reader rows", async () => {
|
|
82
|
+
const group = makeGroup();
|
|
83
|
+
const admin = makeMember(group.groupId, (0, utils_1.newid)(), group_member_roles_1.GroupMemberRole.Admin);
|
|
84
|
+
const joiner = makeMember(group.groupId, userId, group_member_roles_1.GroupMemberRole.Reader);
|
|
85
|
+
await (0, group_share_1.seedGroupContextFromShare)(userContext, { group, groupMembers: [admin] }, [joiner]);
|
|
86
|
+
const groupContext = userContext.getDataContext(group.groupId);
|
|
87
|
+
await expect((0, groups_1.Groups)(groupContext).get(group.groupId)).resolves.toMatchObject({
|
|
88
|
+
name: "Home",
|
|
89
|
+
});
|
|
90
|
+
await expect((0, group_members_1.GroupMembers)(groupContext).findOne({ userId: admin.userId, groupId: group.groupId })).resolves.toMatchObject({ role: group_member_roles_1.GroupMemberRole.Admin });
|
|
91
|
+
await expect((0, group_members_1.GroupMembers)(groupContext).findOne({ userId: userId, groupId: group.groupId })).resolves.toMatchObject({ role: group_member_roles_1.GroupMemberRole.Reader });
|
|
92
|
+
});
|
|
93
|
+
it("leaves existing memberships in place on a retry", async () => {
|
|
94
|
+
const group = makeGroup();
|
|
95
|
+
const admin = makeMember(group.groupId, (0, utils_1.newid)(), group_member_roles_1.GroupMemberRole.Admin);
|
|
96
|
+
await (0, group_share_1.seedGroupContextFromShare)(userContext, { group, groupMembers: [admin] });
|
|
97
|
+
const replacement = {
|
|
98
|
+
...admin,
|
|
99
|
+
groupMemberId: (0, utils_1.newid)(),
|
|
100
|
+
signature: "other-sig",
|
|
101
|
+
};
|
|
102
|
+
await (0, group_share_1.seedGroupContextFromShare)(userContext, { group, groupMembers: [replacement] });
|
|
103
|
+
const saved = await (0, group_members_1.GroupMembers)(userContext.getDataContext(group.groupId)).findOne({
|
|
104
|
+
userId: admin.userId,
|
|
105
|
+
groupId: group.groupId,
|
|
106
|
+
});
|
|
107
|
+
expect(saved?.groupMemberId).toBe(admin.groupMemberId);
|
|
108
|
+
expect(saved?.signature).toBe("member-sig");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* password. Multiple admins can listen for the same password simultaneously.
|
|
6
6
|
*/
|
|
7
7
|
import type { GroupMemberRole } from "../data/group-member-roles";
|
|
8
|
+
import type { IGroupMember } from "../data/group-members";
|
|
9
|
+
import type { IGroupShare } from "../data/group-share";
|
|
8
10
|
/**
|
|
9
11
|
* Information about a group shared during the invitation flow.
|
|
10
12
|
*/
|
|
@@ -69,16 +71,24 @@ export interface IGroupJoinApprovalInfo {
|
|
|
69
71
|
approved: boolean;
|
|
70
72
|
/** The role assigned to the new member (if approved) */
|
|
71
73
|
role?: GroupMemberRole;
|
|
72
|
-
/**
|
|
73
|
-
groupShare?:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
};
|
|
74
|
+
/** Canonical group record plus Admin+ seed memberships */
|
|
75
|
+
groupShare?: IGroupShare;
|
|
76
|
+
/** Newly signed membership for the joiner, including Reader roles */
|
|
77
|
+
newMember?: IGroupMember;
|
|
77
78
|
/** Admin's user info for adding to contacts */
|
|
78
79
|
adminInfo?: IGroupJoinRequestInfo;
|
|
79
80
|
/** Error message if rejected */
|
|
80
81
|
rejectionReason?: string;
|
|
81
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Signal that a joiner finished importing approval data and can connect to the admin.
|
|
85
|
+
*/
|
|
86
|
+
export interface IGroupInviteJoinFinished {
|
|
87
|
+
/** Group the joiner just imported */
|
|
88
|
+
groupId: string;
|
|
89
|
+
/** Approving admin device that should be contacted in that group context */
|
|
90
|
+
adminDeviceId: string;
|
|
91
|
+
}
|
|
82
92
|
/**
|
|
83
93
|
* Active invitation listener state.
|
|
84
94
|
*/
|