@peers-app/peers-sdk 0.23.14 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/contracts/index.d.ts +1 -0
  2. package/dist/contracts/index.js +4 -1
  3. package/dist/contracts/system/index.d.ts +2 -0
  4. package/dist/contracts/system/index.js +5 -1
  5. package/dist/contracts/system/invites-contract.d.ts +121 -0
  6. package/dist/contracts/system/invites-contract.js +150 -0
  7. package/dist/data/create-group.d.ts +24 -0
  8. package/dist/data/create-group.js +61 -0
  9. package/dist/data/group-secret.d.ts +10 -0
  10. package/dist/data/group-secret.js +26 -0
  11. package/dist/data/group-share.d.ts +0 -20
  12. package/dist/data/group-share.js +0 -35
  13. package/dist/data/index.d.ts +2 -0
  14. package/dist/data/index.js +2 -0
  15. package/dist/data/invites.d.ts +350 -0
  16. package/dist/data/invites.js +99 -0
  17. package/dist/data/users.js +4 -0
  18. package/dist/device-pairing/constants.d.ts +27 -9
  19. package/dist/device-pairing/constants.js +28 -10
  20. package/dist/device-pairing/cpace.d.ts +46 -0
  21. package/dist/device-pairing/cpace.js +173 -0
  22. package/dist/device-pairing/cpace.test.js +116 -0
  23. package/dist/device-pairing/crypto.d.ts +40 -15
  24. package/dist/device-pairing/crypto.js +74 -33
  25. package/dist/device-pairing/device-pairing.test.js +156 -119
  26. package/dist/device-pairing/index.d.ts +3 -1
  27. package/dist/device-pairing/index.js +3 -1
  28. package/dist/device-pairing/pairing-code.d.ts +27 -0
  29. package/dist/device-pairing/pairing-code.js +84 -0
  30. package/dist/device-pairing/pgp-word-list.d.ts +14 -0
  31. package/dist/device-pairing/pgp-word-list.js +531 -0
  32. package/dist/device-pairing/signaling.d.ts +17 -6
  33. package/dist/device-pairing/signaling.js +31 -8
  34. package/dist/device-pairing/transcript.d.ts +3 -3
  35. package/dist/device-pairing/transcript.js +6 -13
  36. package/dist/device-pairing/types.d.ts +870 -224
  37. package/dist/device-pairing/types.js +186 -108
  38. package/dist/index.d.ts +1 -2
  39. package/dist/index.js +1 -2
  40. package/dist/invites/data-box-schema.d.ts +15 -0
  41. package/dist/invites/data-box-schema.js +15 -0
  42. package/dist/invites/group-approval.d.ts +43 -0
  43. package/dist/invites/group-approval.js +155 -0
  44. package/dist/invites/group-approval.test.js +221 -0
  45. package/dist/invites/index.d.ts +5 -0
  46. package/dist/{group-invite → invites}/index.js +5 -3
  47. package/dist/invites/invite-epoch.d.ts +18 -0
  48. package/dist/invites/invite-epoch.js +35 -0
  49. package/dist/invites/invite-messages.d.ts +1354 -0
  50. package/dist/invites/invite-messages.js +145 -0
  51. package/dist/invites/invite-token.d.ts +334 -0
  52. package/dist/invites/invite-token.js +178 -0
  53. package/dist/invites/invite-token.test.d.ts +1 -0
  54. package/dist/invites/invite-token.test.js +123 -0
  55. package/dist/rpc-types.d.ts +5 -4
  56. package/dist/rpc-types.js +2 -1
  57. package/dist/system-ids.d.ts +22 -0
  58. package/dist/system-ids.js +23 -1
  59. package/package.json +1 -1
  60. package/dist/device-pairing/invitation.d.ts +0 -13
  61. package/dist/device-pairing/invitation.js +0 -85
  62. package/dist/group-invite/group-invite.d.ts +0 -53
  63. package/dist/group-invite/group-invite.js +0 -231
  64. package/dist/group-invite/group-invite.pvars.d.ts +0 -94
  65. package/dist/group-invite/group-invite.pvars.js +0 -139
  66. package/dist/group-invite/group-invite.types.d.ts +0 -124
  67. package/dist/group-invite/group-invite.types.js +0 -8
  68. package/dist/group-invite/index.d.ts +0 -3
  69. package/dist/user-connect/connection-code.d.ts +0 -67
  70. package/dist/user-connect/connection-code.js +0 -176
  71. package/dist/user-connect/connection-code.test.js +0 -213
  72. package/dist/user-connect/index.d.ts +0 -3
  73. package/dist/user-connect/index.js +0 -19
  74. package/dist/user-connect/user-connect.pvars.d.ts +0 -20
  75. package/dist/user-connect/user-connect.pvars.js +0 -63
  76. package/dist/user-connect/user-connect.pvars.test.js +0 -46
  77. package/dist/user-connect/user-connect.types.d.ts +0 -94
  78. package/dist/user-connect/user-connect.types.js +0 -68
  79. /package/dist/{user-connect/connection-code.test.d.ts → device-pairing/cpace.test.d.ts} +0 -0
  80. /package/dist/{user-connect/user-connect.pvars.test.d.ts → invites/group-approval.test.d.ts} +0 -0
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildGroupApproval = buildGroupApproval;
4
+ exports.importGroupApproval = importGroupApproval;
5
+ const data_1 = require("../data");
6
+ const keys_1 = require("../keys");
7
+ const utils_1 = require("../utils");
8
+ /**
9
+ * Builds the approval payload for a joiner: signs their membership row, gathers Admin+
10
+ * seed rows plus the approver's own Users/Devices records, and boxes the group secret to
11
+ * the joiner's box key.
12
+ *
13
+ * The approver must hold Admin+ in the group. The membership row is signed and saved in
14
+ * the approver's group context here; it syncs to the joiner when they connect and is also
15
+ * carried in the payload so the joiner can seed their copy immediately.
16
+ *
17
+ * @throws when the group is unknown, the approver is not Admin+, or the secret is unavailable
18
+ */
19
+ async function buildGroupApproval(userContext, input) {
20
+ const group = await (0, data_1.Groups)(userContext.userDataContext).get(input.groupId);
21
+ if (!group) {
22
+ throw new Error("Group not found");
23
+ }
24
+ const groupDataContext = userContext.getDataContext(input.groupId);
25
+ const groupMembersTable = (0, data_1.GroupMembers)(groupDataContext);
26
+ let approvingMember = await groupMembersTable.findOne({
27
+ groupId: input.groupId,
28
+ userId: userContext.userId,
29
+ });
30
+ if (!approvingMember && group.founderUserId === userContext.userId) {
31
+ // Groups created before founders got an explicit membership row: the founder role is
32
+ // implicit (see `getUserRole`). Write the row now so it seeds to joiners like any admin.
33
+ approvingMember = await groupMembersTable.signAndSave({
34
+ groupMemberId: (0, utils_1.newid)(),
35
+ groupId: input.groupId,
36
+ userId: userContext.userId,
37
+ role: data_1.GroupMemberRole.Founder,
38
+ signature: "",
39
+ });
40
+ }
41
+ if (!approvingMember || approvingMember.deleted || approvingMember.role < data_1.GroupMemberRole.Admin) {
42
+ throw new Error("Only Admin or higher members can approve group joins");
43
+ }
44
+ if (input.role > approvingMember.role) {
45
+ throw new Error("Cannot grant a role higher than your own");
46
+ }
47
+ const groupSecretKey = await (0, data_1.readGroupSecretKey)(userContext, input.groupId, input.approverKeys.secretKey);
48
+ const joinerUser = {
49
+ userId: input.joiner.userId,
50
+ name: input.joiner.displayName || input.joiner.userId,
51
+ publicKey: input.joiner.publicKey,
52
+ publicBoxKey: input.joiner.publicBoxKey,
53
+ };
54
+ const saveOptions = { restoreIfDeleted: true, weakInsert: true };
55
+ await (0, data_1.Users)(userContext.userDataContext).save(joinerUser, saveOptions);
56
+ await (0, data_1.Users)(groupDataContext).save(joinerUser, saveOptions);
57
+ const existingMember = await groupMembersTable.findOne({
58
+ groupId: input.groupId,
59
+ userId: input.joiner.userId,
60
+ });
61
+ let membership;
62
+ if (existingMember) {
63
+ existingMember.role = input.role;
64
+ delete existingMember.deleted;
65
+ membership = await groupMembersTable.signAndSave(existingMember);
66
+ }
67
+ else {
68
+ membership = await groupMembersTable.signAndSave({
69
+ groupMemberId: (0, utils_1.newid)(),
70
+ groupId: input.groupId,
71
+ userId: input.joiner.userId,
72
+ role: input.role,
73
+ signature: "",
74
+ });
75
+ }
76
+ let adminMembers = await groupMembersTable.list({ deleted: { $ne: true }, role: { $gte: data_1.GroupMemberRole.Admin } }, { pageSize: 5, sortBy: ["-groupMemberId"] });
77
+ if (!adminMembers.some((member) => member.groupMemberId === approvingMember.groupMemberId)) {
78
+ adminMembers = [approvingMember, ...adminMembers];
79
+ }
80
+ const me = await (0, data_1.Users)(userContext.userDataContext).get(userContext.userId);
81
+ if (!me) {
82
+ throw new Error("My user object was not found in the DB. Is the user setup?");
83
+ }
84
+ const myDevice = await (0, data_1.Devices)(userContext.userDataContext).get(input.approverDeviceId);
85
+ const seedDevices = [
86
+ {
87
+ userId: me.userId,
88
+ deviceId: input.approverDeviceId,
89
+ publicKey: input.approverKeys.publicKey,
90
+ publicBoxKey: input.approverKeys.publicBoxKey,
91
+ ...(myDevice?.name ? { deviceName: myDevice.name } : {}),
92
+ },
93
+ ];
94
+ return {
95
+ kind: "group-approval",
96
+ ...(input.inviteNonce ? { inviteNonce: input.inviteNonce } : {}),
97
+ group,
98
+ membership,
99
+ seed: {
100
+ groupMembers: adminMembers,
101
+ users: [me],
102
+ devices: seedDevices,
103
+ },
104
+ approver: {
105
+ userId: me.userId,
106
+ publicKey: me.publicKey,
107
+ publicBoxKey: me.publicBoxKey,
108
+ displayName: me.name,
109
+ },
110
+ approverDeviceId: input.approverDeviceId,
111
+ boxedSecret: (0, keys_1.boxDataWithKeys)(groupSecretKey, input.joiner.publicBoxKey, input.approverKeys.secretKey),
112
+ };
113
+ }
114
+ /**
115
+ * Imports a {@link GroupApproval} on the joiner's device: records the group, seeds the group
116
+ * context with the signed rows, saves the approver as a contact, and stores the unboxed
117
+ * group secret in the personal context after validating it against the group's keys.
118
+ *
119
+ * Personal `Groups` is added only when missing so a local leave stays authoritative.
120
+ * `Devices` rows for the approver are left to the caller (device layer).
121
+ */
122
+ async function importGroupApproval(userContext, approval, joinerKeys) {
123
+ if (approval.membership.userId !== userContext.userId) {
124
+ throw new Error("Group approval is for a different user");
125
+ }
126
+ if (approval.membership.groupId !== approval.group.groupId) {
127
+ throw new Error("Group approval membership does not match the group");
128
+ }
129
+ if (approval.boxedSecret.fromPublicKey !== approval.approver.publicBoxKey ||
130
+ !approval.seed.users.some((user) => user.userId === approval.approver.userId)) {
131
+ throw new Error("Group approval approver identity is inconsistent");
132
+ }
133
+ const groupSecretKey = (0, keys_1.openBoxWithSecretKey)(approval.boxedSecret, joinerKeys.secretKey);
134
+ if (typeof groupSecretKey !== "string") {
135
+ throw new Error("Group approval did not contain a usable group secret");
136
+ }
137
+ const { group } = approval;
138
+ const existingGroup = await (0, data_1.Groups)(userContext.userDataContext).get(group.groupId);
139
+ if (!existingGroup) {
140
+ await (0, data_1.Groups)(userContext.userDataContext).save(group, { restoreIfDeleted: true });
141
+ }
142
+ await (0, data_1.seedGroupContextFromShare)(userContext, { group, groupMembers: approval.seed.groupMembers }, [approval.membership]);
143
+ const groupDataContext = userContext.getDataContext(group.groupId);
144
+ const saveOptions = { restoreIfDeleted: true, weakInsert: true };
145
+ for (const user of approval.seed.users) {
146
+ await (0, data_1.Users)(userContext.userDataContext).save(user, saveOptions);
147
+ await (0, data_1.Users)(groupDataContext).save(user, saveOptions);
148
+ }
149
+ await (0, data_1.saveGroupSecretKey)(userContext.userDataContext, group.groupId, groupSecretKey, group);
150
+ return {
151
+ groupId: group.groupId,
152
+ approverUserId: approval.approver.userId,
153
+ approverDeviceId: approval.approverDeviceId,
154
+ };
155
+ }
@@ -0,0 +1,221 @@
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 devices_1 = require("../data/devices");
6
+ const group_member_roles_1 = require("../data/group-member-roles");
7
+ const group_members_1 = require("../data/group-members");
8
+ const group_secret_1 = require("../data/group-secret");
9
+ const groups_1 = require("../data/groups");
10
+ const sql_data_source_1 = require("../data/orm/sql.data-source");
11
+ const persistent_vars_1 = require("../data/persistent-vars");
12
+ const users_1 = require("../data/users");
13
+ const keys_1 = require("../keys");
14
+ const rpc_types_1 = require("../rpc-types");
15
+ const utils_1 = require("../utils");
16
+ const group_approval_1 = require("./group-approval");
17
+ const invite_messages_1 = require("./invite-messages");
18
+ class DBHarness {
19
+ _db = null;
20
+ get db() {
21
+ if (!this._db) {
22
+ this._db = new SQLiteDB(":memory:");
23
+ this._db.pragma("journal_mode = WAL");
24
+ }
25
+ return this._db;
26
+ }
27
+ async get(sql, params = []) {
28
+ return this.db.prepare(sql).get(params);
29
+ }
30
+ async all(sql, params = []) {
31
+ return this.db.prepare(sql).all(params);
32
+ }
33
+ async exec(sql, params = []) {
34
+ const result = this.db.prepare(sql).run(params);
35
+ return { changes: result.changes };
36
+ }
37
+ async close() {
38
+ this._db?.close();
39
+ this._db = null;
40
+ }
41
+ }
42
+ /** One in-memory user with its own personal and group databases. */
43
+ async function makeUser(name) {
44
+ const dbs = new Map();
45
+ const keys = (0, keys_1.newKeys)();
46
+ const userId = (0, utils_1.newid)();
47
+ const dataSourceFactory = (metaData, schema, groupId) => {
48
+ const key = groupId || userId;
49
+ let db = dbs.get(key);
50
+ if (!db) {
51
+ db = new DBHarness();
52
+ dbs.set(key, db);
53
+ }
54
+ return new sql_data_source_1.SQLDataSource(db, metaData, schema);
55
+ };
56
+ const userContext = new user_context_1.UserContext(userId, dataSourceFactory, true);
57
+ await userContext.loadingPromise;
58
+ const deviceId = (0, utils_1.newid)();
59
+ userContext.deviceId(deviceId);
60
+ await (0, users_1.Users)(userContext.userDataContext).save({
61
+ userId,
62
+ name,
63
+ publicKey: keys.publicKey,
64
+ publicBoxKey: keys.publicBoxKey,
65
+ });
66
+ return {
67
+ userId,
68
+ deviceId,
69
+ keys,
70
+ userContext,
71
+ dispose: async () => {
72
+ userContext.dispose();
73
+ await Promise.all([...dbs.values()].map((db) => db.close()));
74
+ },
75
+ };
76
+ }
77
+ describe("group approval payloads", () => {
78
+ let admin;
79
+ let joiner;
80
+ let group;
81
+ let groupKeys;
82
+ const originalEncrypt = rpc_types_1.rpcServerCalls.encryptData;
83
+ beforeAll(async () => {
84
+ groups_1.GroupsTable.isPassthrough = true;
85
+ group_members_1.GroupMembersTable.isPassthrough = true;
86
+ users_1.UsersTable.isPassthrough = true;
87
+ admin = await makeUser("Alice");
88
+ joiner = await makeUser("Bob");
89
+ group_members_1.GroupMembersTable.enableGroupMemberSigning((member) => (0, keys_1.addSignatureToObject)(member, admin.keys.secretKey));
90
+ groupKeys = (0, keys_1.newKeys)();
91
+ group = {
92
+ groupId: (0, utils_1.newid)(),
93
+ name: "Family",
94
+ description: "",
95
+ founderUserId: admin.userId,
96
+ signature: "group-sig",
97
+ publicKey: groupKeys.publicKey,
98
+ publicBoxKey: groupKeys.publicBoxKey,
99
+ };
100
+ await (0, groups_1.Groups)(admin.userContext.userDataContext).save(group);
101
+ await (0, group_members_1.GroupMembers)(admin.userContext.getDataContext(group.groupId)).signAndSave({
102
+ groupMemberId: (0, utils_1.newid)(),
103
+ groupId: group.groupId,
104
+ userId: admin.userId,
105
+ role: group_member_roles_1.GroupMemberRole.Owner,
106
+ signature: "",
107
+ });
108
+ // The secret pvar is stored encrypted with the owning user's secret key.
109
+ await (0, persistent_vars_1.PersistentVars)(admin.userContext.userDataContext).save({
110
+ persistentVarId: "",
111
+ name: (0, group_secret_1.groupSecretKeyPvarName)(group.groupId),
112
+ scope: "groupUser",
113
+ isSecret: false,
114
+ value: { value: (0, keys_1.encryptData)(groupKeys.secretKey, admin.keys.secretKey) },
115
+ });
116
+ await (0, devices_1.Devices)(admin.userContext.userDataContext).save({
117
+ deviceId: admin.deviceId,
118
+ userId: admin.userId,
119
+ name: "Alice laptop",
120
+ firstSeen: new Date(),
121
+ lastSeen: new Date(),
122
+ trustLevel: 100,
123
+ });
124
+ });
125
+ afterAll(async () => {
126
+ groups_1.GroupsTable.isPassthrough = false;
127
+ group_members_1.GroupMembersTable.isPassthrough = false;
128
+ users_1.UsersTable.isPassthrough = false;
129
+ rpc_types_1.rpcServerCalls.encryptData = originalEncrypt;
130
+ await admin.dispose();
131
+ await joiner.dispose();
132
+ });
133
+ it("builds an approval the joiner can import, including the boxed group secret", async () => {
134
+ const approval = await (0, group_approval_1.buildGroupApproval)(admin.userContext, {
135
+ groupId: group.groupId,
136
+ joiner: {
137
+ userId: joiner.userId,
138
+ publicKey: joiner.keys.publicKey,
139
+ publicBoxKey: joiner.keys.publicBoxKey,
140
+ displayName: "Bob",
141
+ },
142
+ role: group_member_roles_1.GroupMemberRole.Writer,
143
+ approverKeys: admin.keys,
144
+ approverDeviceId: admin.deviceId,
145
+ });
146
+ expect(() => invite_messages_1.groupApprovalSchema.parse(approval)).not.toThrow();
147
+ expect(approval.membership).toMatchObject({
148
+ userId: joiner.userId,
149
+ role: group_member_roles_1.GroupMemberRole.Writer,
150
+ });
151
+ expect(approval.membership.signature).toContain(":");
152
+ expect(approval.seed.groupMembers.map((member) => member.userId)).toContain(admin.userId);
153
+ expect(approval.seed.devices[0]).toMatchObject({
154
+ deviceId: admin.deviceId,
155
+ deviceName: "Alice laptop",
156
+ });
157
+ expect(JSON.stringify(approval)).not.toContain(groupKeys.secretKey);
158
+ // The joiner side re-encrypts the secret with its own key when saving the pvar.
159
+ rpc_types_1.rpcServerCalls.encryptData = async (value) => (0, keys_1.encryptData)(value, joiner.keys.secretKey);
160
+ const result = await (0, group_approval_1.importGroupApproval)(joiner.userContext, approval, joiner.keys);
161
+ expect(result).toEqual({
162
+ groupId: group.groupId,
163
+ approverUserId: admin.userId,
164
+ approverDeviceId: admin.deviceId,
165
+ });
166
+ await expect((0, groups_1.Groups)(joiner.userContext.userDataContext).get(group.groupId)).resolves.toMatchObject({ name: "Family" });
167
+ const joinerGroupContext = joiner.userContext.getDataContext(group.groupId);
168
+ await expect((0, group_members_1.GroupMembers)(joinerGroupContext).findOne({ userId: joiner.userId, groupId: group.groupId })).resolves.toMatchObject({ role: group_member_roles_1.GroupMemberRole.Writer });
169
+ await expect((0, group_members_1.GroupMembers)(joinerGroupContext).findOne({ userId: admin.userId, groupId: group.groupId })).resolves.toMatchObject({ role: group_member_roles_1.GroupMemberRole.Owner });
170
+ await expect((0, users_1.Users)(joiner.userContext.userDataContext).get(admin.userId)).resolves.toMatchObject({ name: "Alice" });
171
+ await expect((0, group_secret_1.readGroupSecretKey)(joiner.userContext, group.groupId, joiner.keys.secretKey)).resolves.toBe(groupKeys.secretKey);
172
+ });
173
+ it("rejects approvals addressed to someone else or with a mismatched box sender", async () => {
174
+ const approval = await (0, group_approval_1.buildGroupApproval)(admin.userContext, {
175
+ groupId: group.groupId,
176
+ joiner: {
177
+ userId: joiner.userId,
178
+ publicKey: joiner.keys.publicKey,
179
+ publicBoxKey: joiner.keys.publicBoxKey,
180
+ },
181
+ role: group_member_roles_1.GroupMemberRole.Reader,
182
+ approverKeys: admin.keys,
183
+ approverDeviceId: admin.deviceId,
184
+ });
185
+ const stranger = await makeUser("Carol");
186
+ try {
187
+ await expect((0, group_approval_1.importGroupApproval)(stranger.userContext, approval, stranger.keys)).rejects.toThrow("different user");
188
+ }
189
+ finally {
190
+ await stranger.dispose();
191
+ }
192
+ await expect((0, group_approval_1.importGroupApproval)(joiner.userContext, {
193
+ ...approval,
194
+ boxedSecret: { ...approval.boxedSecret, fromPublicKey: (0, keys_1.newKeys)().publicBoxKey },
195
+ }, joiner.keys)).rejects.toThrow("inconsistent");
196
+ });
197
+ it("refuses to approve from a non-admin or to grant above the approver's role", async () => {
198
+ await expect((0, group_approval_1.buildGroupApproval)(joiner.userContext, {
199
+ groupId: group.groupId,
200
+ joiner: {
201
+ userId: (0, utils_1.newid)(),
202
+ publicKey: (0, keys_1.newKeys)().publicKey,
203
+ publicBoxKey: (0, keys_1.newKeys)().publicBoxKey,
204
+ },
205
+ role: group_member_roles_1.GroupMemberRole.Reader,
206
+ approverKeys: joiner.keys,
207
+ approverDeviceId: joiner.deviceId,
208
+ })).rejects.toThrow();
209
+ await expect((0, group_approval_1.buildGroupApproval)(admin.userContext, {
210
+ groupId: group.groupId,
211
+ joiner: {
212
+ userId: (0, utils_1.newid)(),
213
+ publicKey: (0, keys_1.newKeys)().publicKey,
214
+ publicBoxKey: (0, keys_1.newKeys)().publicBoxKey,
215
+ },
216
+ role: group_member_roles_1.GroupMemberRole.Founder,
217
+ approverKeys: admin.keys,
218
+ approverDeviceId: admin.deviceId,
219
+ })).rejects.toThrow("higher than your own");
220
+ });
221
+ });
@@ -0,0 +1,5 @@
1
+ export * from "./data-box-schema";
2
+ export * from "./group-approval";
3
+ export * from "./invite-epoch";
4
+ export * from "./invite-messages";
5
+ export * from "./invite-token";
@@ -14,6 +14,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("./group-invite"), exports);
18
- __exportStar(require("./group-invite.pvars"), exports);
19
- __exportStar(require("./group-invite.types"), exports);
17
+ __exportStar(require("./data-box-schema"), exports);
18
+ __exportStar(require("./group-approval"), exports);
19
+ __exportStar(require("./invite-epoch"), exports);
20
+ __exportStar(require("./invite-messages"), exports);
21
+ __exportStar(require("./invite-token"), exports);
@@ -0,0 +1,18 @@
1
+ import type { UserContext } from "../context/user-context";
2
+ import { type PersistentVar } from "../data/persistent-vars";
3
+ /** Name of the synced personal-context variable that holds the invite epoch. */
4
+ export declare const INVITE_EPOCH_VAR_NAME = "inviteEpoch";
5
+ /**
6
+ * The user's current invite epoch, synced across their devices.
7
+ *
8
+ * Every issued token records the epoch at issue time. Bumping the epoch revokes every
9
+ * outstanding token at once (including the profile QR); devices ignore replies and join
10
+ * requests whose token epoch is older than the inviter's current epoch.
11
+ *
12
+ * @param userContext - Bind to a specific user context; defaults to the process singleton.
13
+ */
14
+ export declare function inviteEpochVar(userContext?: UserContext): PersistentVar<number>;
15
+ /** Increments the invite epoch, revoking all outstanding tokens; returns the new epoch. */
16
+ export declare function bumpInviteEpoch(userContext?: UserContext): Promise<number>;
17
+ /** Returns the current epoch after the variable has loaded. */
18
+ export declare function currentInviteEpoch(userContext?: UserContext): Promise<number>;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.INVITE_EPOCH_VAR_NAME = void 0;
4
+ exports.inviteEpochVar = inviteEpochVar;
5
+ exports.bumpInviteEpoch = bumpInviteEpoch;
6
+ exports.currentInviteEpoch = currentInviteEpoch;
7
+ const persistent_vars_1 = require("../data/persistent-vars");
8
+ /** Name of the synced personal-context variable that holds the invite epoch. */
9
+ exports.INVITE_EPOCH_VAR_NAME = "inviteEpoch";
10
+ /**
11
+ * The user's current invite epoch, synced across their devices.
12
+ *
13
+ * Every issued token records the epoch at issue time. Bumping the epoch revokes every
14
+ * outstanding token at once (including the profile QR); devices ignore replies and join
15
+ * requests whose token epoch is older than the inviter's current epoch.
16
+ *
17
+ * @param userContext - Bind to a specific user context; defaults to the process singleton.
18
+ */
19
+ function inviteEpochVar(userContext) {
20
+ return (0, persistent_vars_1.userVar)(exports.INVITE_EPOCH_VAR_NAME, { defaultValue: 0, userContext });
21
+ }
22
+ /** Increments the invite epoch, revoking all outstanding tokens; returns the new epoch. */
23
+ async function bumpInviteEpoch(userContext) {
24
+ const epoch = inviteEpochVar(userContext);
25
+ await epoch.loadingPromise;
26
+ const next = (epoch() ?? 0) + 1;
27
+ epoch(next);
28
+ return next;
29
+ }
30
+ /** Returns the current epoch after the variable has loaded. */
31
+ async function currentInviteEpoch(userContext) {
32
+ const epoch = inviteEpochVar(userContext);
33
+ await epoch.loadingPromise;
34
+ return epoch() ?? 0;
35
+ }