@kubun/store-p2p 0.11.0 → 0.12.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.
- package/lib/api.d.ts +25 -251
- package/lib/api.js +19 -590
- package/lib/index.d.ts +2 -2
- package/lib/migrations.d.ts +21 -0
- package/lib/migrations.js +106 -26
- package/lib/ops/app-lane.d.ts +36 -0
- package/lib/ops/app-lane.js +38 -0
- package/lib/ops/circles.d.ts +153 -0
- package/lib/ops/circles.js +269 -0
- package/lib/ops/control.d.ts +118 -0
- package/lib/ops/control.js +110 -0
- package/lib/ops/delegations.d.ts +25 -0
- package/lib/ops/delegations.js +27 -0
- package/lib/ops/discovery.d.ts +92 -0
- package/lib/ops/discovery.js +90 -0
- package/lib/ops/groups.d.ts +132 -0
- package/lib/ops/groups.js +259 -0
- package/lib/ops/hubs.d.ts +56 -0
- package/lib/ops/hubs.js +222 -0
- package/lib/ops/mls.d.ts +21 -0
- package/lib/ops/mls.js +50 -0
- package/lib/ops/sync.d.ts +22 -0
- package/lib/ops/sync.js +50 -0
- package/lib/tables.d.ts +106 -26
- package/package.json +18 -18
package/lib/api.js
CHANGED
|
@@ -1,593 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { createAppLaneOps } from './ops/app-lane.js';
|
|
2
|
+
import { createCircleOps } from './ops/circles.js';
|
|
3
|
+
import { createControlOps } from './ops/control.js';
|
|
4
|
+
import { createDelegationOps } from './ops/delegations.js';
|
|
5
|
+
import { createDiscoveryOps } from './ops/discovery.js';
|
|
6
|
+
import { createGroupOps } from './ops/groups.js';
|
|
7
|
+
import { createHubOps } from './ops/hubs.js';
|
|
8
|
+
import { createMLSOps } from './ops/mls.js';
|
|
9
|
+
import { createSyncOps } from './ops/sync.js';
|
|
3
10
|
export function createP2PStore(db, adapter) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
},
|
|
15
|
-
async getGroup (id) {
|
|
16
|
-
return await db.selectFrom('kubun_p2p_groups').selectAll().where('id', '=', id).executeTakeFirst();
|
|
17
|
-
},
|
|
18
|
-
async updateGroup (id, update) {
|
|
19
|
-
const values = {};
|
|
20
|
-
if (update.name != null) values.name = update.name;
|
|
21
|
-
if (update.description != null) values.description = update.description;
|
|
22
|
-
if (update.hlc != null) values.hlc = update.hlc;
|
|
23
|
-
values.updated_at = adapter.encodeTimestamp(new Date());
|
|
24
|
-
await db.updateTable('kubun_p2p_groups').set(values).where('id', '=', id).execute();
|
|
25
|
-
},
|
|
26
|
-
async deleteGroup (id) {
|
|
27
|
-
await db.deleteFrom('kubun_p2p_groups').where('id', '=', id).execute();
|
|
28
|
-
},
|
|
29
|
-
async listGroups () {
|
|
30
|
-
return await db.selectFrom('kubun_p2p_groups').selectAll().execute();
|
|
31
|
-
},
|
|
32
|
-
async listGroupsOffsetPaged (params) {
|
|
33
|
-
const { limit, offset } = params;
|
|
34
|
-
let query = db.selectFrom('kubun_p2p_groups').selectAll().orderBy('name');
|
|
35
|
-
if (offset != null) {
|
|
36
|
-
query = query.offset(offset);
|
|
37
|
-
}
|
|
38
|
-
if (limit != null) {
|
|
39
|
-
query = query.limit(limit + 1);
|
|
40
|
-
const rows = await query.execute();
|
|
41
|
-
const hasMore = rows.length > limit;
|
|
42
|
-
return {
|
|
43
|
-
entries: hasMore ? rows.slice(0, limit) : rows,
|
|
44
|
-
hasMore
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
const rows = await query.execute();
|
|
48
|
-
return {
|
|
49
|
-
entries: rows,
|
|
50
|
-
hasMore: false
|
|
51
|
-
};
|
|
52
|
-
},
|
|
53
|
-
// --- Group member operations ---
|
|
54
|
-
async addGroupMember (member) {
|
|
55
|
-
// Re-adding a previously-removed member clears the tombstone and refreshes
|
|
56
|
-
// the row's HLC, so a member that left and rejoined is active again.
|
|
57
|
-
await db.insertInto('kubun_p2p_group_members').values({
|
|
58
|
-
group_id: member.group_id,
|
|
59
|
-
member_did: normalizeDID(member.member_did),
|
|
60
|
-
role: member.role,
|
|
61
|
-
hlc: member.hlc
|
|
62
|
-
}).onConflict((oc)=>oc.columns([
|
|
63
|
-
'group_id',
|
|
64
|
-
'member_did'
|
|
65
|
-
]).doUpdateSet((eb)=>({
|
|
66
|
-
role: eb.ref('excluded.role'),
|
|
67
|
-
hlc: eb.ref('excluded.hlc'),
|
|
68
|
-
removed_at_hlc: null,
|
|
69
|
-
updated_at: adapter.encodeTimestamp(new Date())
|
|
70
|
-
}))).execute();
|
|
71
|
-
},
|
|
72
|
-
async addGroupMembers (members) {
|
|
73
|
-
if (members.length === 0) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
await db.insertInto('kubun_p2p_group_members').values(members.map((member)=>({
|
|
77
|
-
group_id: member.group_id,
|
|
78
|
-
member_did: normalizeDID(member.member_did),
|
|
79
|
-
role: member.role,
|
|
80
|
-
hlc: member.hlc
|
|
81
|
-
}))).onConflict((oc)=>oc.columns([
|
|
82
|
-
'group_id',
|
|
83
|
-
'member_did'
|
|
84
|
-
]).doUpdateSet((eb)=>({
|
|
85
|
-
role: eb.ref('excluded.role'),
|
|
86
|
-
hlc: eb.ref('excluded.hlc'),
|
|
87
|
-
removed_at_hlc: null,
|
|
88
|
-
updated_at: adapter.encodeTimestamp(new Date())
|
|
89
|
-
}))).execute();
|
|
90
|
-
},
|
|
91
|
-
// Earliest-wins LWW on `removed_at_hlc`: the update lands only when the row
|
|
92
|
-
// exists AND it is either not yet tombstoned or the incoming `hlc` precedes
|
|
93
|
-
// the stored removal HLC. A removal for a `(groupID, memberDID)` with no
|
|
94
|
-
// local row is ignored — there is no member to tombstone, and creating a
|
|
95
|
-
// row would leak a phantom member. The string `<` compare mirrors the SQL
|
|
96
|
-
// LWW used by the delegation-token paths: the serialized HLC form is
|
|
97
|
-
// lexicographically ordered, so SQL ordering matches HLC ordering.
|
|
98
|
-
async markMemberRemoved (groupID, memberDID, hlc) {
|
|
99
|
-
await db.updateTable('kubun_p2p_group_members').set({
|
|
100
|
-
removed_at_hlc: hlc,
|
|
101
|
-
updated_at: adapter.encodeTimestamp(new Date())
|
|
102
|
-
}).where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).where((eb)=>eb.or([
|
|
103
|
-
eb('removed_at_hlc', 'is', null),
|
|
104
|
-
eb('removed_at_hlc', '>', hlc)
|
|
105
|
-
])).execute();
|
|
106
|
-
},
|
|
107
|
-
async getMemberRemoval (groupID, memberDID) {
|
|
108
|
-
const row = await db.selectFrom('kubun_p2p_group_members').select('removed_at_hlc').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).executeTakeFirst();
|
|
109
|
-
return row == null ? undefined : {
|
|
110
|
-
removed_at_hlc: row.removed_at_hlc
|
|
111
|
-
};
|
|
112
|
-
},
|
|
113
|
-
async removeGroupMember (groupID, memberDID, hlc) {
|
|
114
|
-
await api.markMemberRemoved(groupID, memberDID, hlc);
|
|
115
|
-
},
|
|
116
|
-
async getGroupMember (groupID, memberDID) {
|
|
117
|
-
return await db.selectFrom('kubun_p2p_group_members').selectAll().where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).where('removed_at_hlc', 'is', null).executeTakeFirst();
|
|
118
|
-
},
|
|
119
|
-
async isGroupMember (groupID, memberDID) {
|
|
120
|
-
const row = await db.selectFrom('kubun_p2p_group_members').select('member_did').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).where('removed_at_hlc', 'is', null).executeTakeFirst();
|
|
121
|
-
return row != null;
|
|
122
|
-
},
|
|
123
|
-
async isGroupMemberAtHLC (groupID, did, atHLC) {
|
|
124
|
-
let query = db.selectFrom('kubun_p2p_group_members').select('member_did').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(did));
|
|
125
|
-
if (atHLC == null) {
|
|
126
|
-
query = query.where('removed_at_hlc', 'is', null);
|
|
127
|
-
} else {
|
|
128
|
-
query = query.where((eb)=>eb.or([
|
|
129
|
-
eb('removed_at_hlc', 'is', null),
|
|
130
|
-
eb('removed_at_hlc', '>', atHLC)
|
|
131
|
-
]));
|
|
132
|
-
}
|
|
133
|
-
const row = await query.limit(1).executeTakeFirst();
|
|
134
|
-
return row != null;
|
|
135
|
-
},
|
|
136
|
-
async isGroupAdmin (groupID, did, atHLC) {
|
|
137
|
-
let query = db.selectFrom('kubun_p2p_group_members').select('member_did').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(did)).where('role', '=', 'admin');
|
|
138
|
-
if (atHLC == null) {
|
|
139
|
-
query = query.where('removed_at_hlc', 'is', null);
|
|
140
|
-
} else {
|
|
141
|
-
// Admit a write whose HLC strictly precedes the removal HLC; deny
|
|
142
|
-
// at-or-after. Complements `checkMembership`'s `removed_at_hlc <= hlc`
|
|
143
|
-
// denial so both agree on the remove-boundary instant.
|
|
144
|
-
query = query.where((eb)=>eb.or([
|
|
145
|
-
eb('removed_at_hlc', 'is', null),
|
|
146
|
-
eb('removed_at_hlc', '>', atHLC)
|
|
147
|
-
]));
|
|
148
|
-
}
|
|
149
|
-
const row = await query.limit(1).executeTakeFirst();
|
|
150
|
-
return row != null;
|
|
151
|
-
},
|
|
152
|
-
async setGroupMemberRole (groupID, memberDID, role) {
|
|
153
|
-
// Update-only by design: a missing row leaves the table untouched, so a
|
|
154
|
-
// role grant can never conjure a phantom member. hlc/removed_at_hlc are
|
|
155
|
-
// left alone to keep membership LWW and tombstones intact.
|
|
156
|
-
await db.updateTable('kubun_p2p_group_members').set({
|
|
157
|
-
role,
|
|
158
|
-
updated_at: adapter.encodeTimestamp(new Date())
|
|
159
|
-
}).where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).execute();
|
|
160
|
-
},
|
|
161
|
-
async isMemberOfAnyGroup (viewerDID, groupIDs) {
|
|
162
|
-
if (groupIDs.length === 0) return false;
|
|
163
|
-
const row = await db.selectFrom('kubun_p2p_group_members').select('group_id').where('member_did', '=', normalizeDID(viewerDID)).where('group_id', 'in', groupIDs).where('removed_at_hlc', 'is', null).limit(1).executeTakeFirst();
|
|
164
|
-
return row != null;
|
|
165
|
-
},
|
|
166
|
-
async listGroupMembers (groupID) {
|
|
167
|
-
return await db.selectFrom('kubun_p2p_group_members').selectAll().where('group_id', '=', groupID).where('removed_at_hlc', 'is', null).execute();
|
|
168
|
-
},
|
|
169
|
-
async getGroupsForMember (memberDID) {
|
|
170
|
-
return await db.selectFrom('kubun_p2p_group_members').innerJoin('kubun_p2p_groups', 'kubun_p2p_groups.id', 'kubun_p2p_group_members.group_id').selectAll('kubun_p2p_groups').where('kubun_p2p_group_members.member_did', '=', normalizeDID(memberDID)).where('kubun_p2p_group_members.removed_at_hlc', 'is', null).execute();
|
|
171
|
-
},
|
|
172
|
-
// Result Map is keyed by the normalized stored `member_did`; callers must
|
|
173
|
-
// normalize before looking up by key (existing callers iterate `.values()`).
|
|
174
|
-
async getGroupsForMembers (memberDIDs) {
|
|
175
|
-
const result = new Map();
|
|
176
|
-
if (memberDIDs.length === 0) return result;
|
|
177
|
-
const normalizedDIDs = memberDIDs.map(normalizeDID);
|
|
178
|
-
const rows = await db.selectFrom('kubun_p2p_group_members').innerJoin('kubun_p2p_groups', 'kubun_p2p_groups.id', 'kubun_p2p_group_members.group_id').select([
|
|
179
|
-
'kubun_p2p_group_members.member_did as member_did',
|
|
180
|
-
'kubun_p2p_groups.id as id',
|
|
181
|
-
'kubun_p2p_groups.name as name',
|
|
182
|
-
'kubun_p2p_groups.description as description',
|
|
183
|
-
'kubun_p2p_groups.created_by as created_by',
|
|
184
|
-
'kubun_p2p_groups.hlc as hlc',
|
|
185
|
-
'kubun_p2p_groups.created_at as created_at',
|
|
186
|
-
'kubun_p2p_groups.updated_at as updated_at'
|
|
187
|
-
]).where('kubun_p2p_group_members.member_did', 'in', normalizedDIDs).where('kubun_p2p_group_members.removed_at_hlc', 'is', null).execute();
|
|
188
|
-
for (const row of rows){
|
|
189
|
-
const { member_did, ...group } = row;
|
|
190
|
-
const list = result.get(member_did);
|
|
191
|
-
if (list != null) {
|
|
192
|
-
list.push(group);
|
|
193
|
-
} else {
|
|
194
|
-
result.set(member_did, [
|
|
195
|
-
group
|
|
196
|
-
]);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return result;
|
|
200
|
-
},
|
|
201
|
-
// --- Circle operations ---
|
|
202
|
-
async createCircle (circle) {
|
|
203
|
-
await db.insertInto('kubun_p2p_circles').values({
|
|
204
|
-
id: circle.id,
|
|
205
|
-
group_id: circle.group_id,
|
|
206
|
-
name: circle.name,
|
|
207
|
-
description: circle.description,
|
|
208
|
-
catalog_ids: adapter.encodeJSON(circle.catalog_ids),
|
|
209
|
-
hlc: circle.hlc
|
|
210
|
-
}).execute();
|
|
211
|
-
},
|
|
212
|
-
async getCircle (id, options) {
|
|
213
|
-
let query = db.selectFrom('kubun_p2p_circles').selectAll().where('id', '=', id);
|
|
214
|
-
if (!options?.includeRemoved) {
|
|
215
|
-
query = query.where('removed_at_hlc', 'is', null);
|
|
216
|
-
}
|
|
217
|
-
return await query.executeTakeFirst();
|
|
218
|
-
},
|
|
219
|
-
async getCircles (ids) {
|
|
220
|
-
const result = new Map();
|
|
221
|
-
if (ids.length === 0) return result;
|
|
222
|
-
const rows = await db.selectFrom('kubun_p2p_circles').selectAll().where('id', 'in', ids).execute();
|
|
223
|
-
for (const row of rows){
|
|
224
|
-
result.set(row.id, row);
|
|
225
|
-
}
|
|
226
|
-
return result;
|
|
227
|
-
},
|
|
228
|
-
async updateCircle (id, update) {
|
|
229
|
-
const values = {};
|
|
230
|
-
if (update.name != null) values.name = update.name;
|
|
231
|
-
if (update.description != null) values.description = update.description;
|
|
232
|
-
if (update.catalog_ids != null) {
|
|
233
|
-
values.catalog_ids = adapter.encodeJSON(update.catalog_ids);
|
|
234
|
-
}
|
|
235
|
-
if (update.hlc != null) {
|
|
236
|
-
values.hlc = update.hlc;
|
|
237
|
-
// A create/update that wins the apply-layer HLC check resurrects a
|
|
238
|
-
// tombstoned circle: clear the removal alongside the HLC refresh.
|
|
239
|
-
values.removed_at_hlc = null;
|
|
240
|
-
}
|
|
241
|
-
values.updated_at = adapter.encodeTimestamp(new Date());
|
|
242
|
-
await db.updateTable('kubun_p2p_circles').set(values).where('id', '=', id).execute();
|
|
243
|
-
},
|
|
244
|
-
async deleteCircle (id) {
|
|
245
|
-
await db.deleteFrom('kubun_p2p_circles').where('id', '=', id).execute();
|
|
246
|
-
},
|
|
247
|
-
// Earliest-wins LWW on `removed_at_hlc`: the update lands only when the row
|
|
248
|
-
// exists AND it is either not yet tombstoned or the incoming `hlc` precedes
|
|
249
|
-
// the stored removal HLC. A removal for a `circleID` with no local row is
|
|
250
|
-
// ignored — there is no circle to tombstone, and creating a row would leak
|
|
251
|
-
// a phantom circle.
|
|
252
|
-
async markCircleRemoved (circleID, hlc) {
|
|
253
|
-
await db.updateTable('kubun_p2p_circles').set({
|
|
254
|
-
removed_at_hlc: hlc,
|
|
255
|
-
updated_at: adapter.encodeTimestamp(new Date())
|
|
256
|
-
}).where('id', '=', circleID).where((eb)=>eb.or([
|
|
257
|
-
eb('removed_at_hlc', 'is', null),
|
|
258
|
-
eb('removed_at_hlc', '>', hlc)
|
|
259
|
-
])).execute();
|
|
260
|
-
},
|
|
261
|
-
async listCirclesByGroup (groupID, options) {
|
|
262
|
-
let query = db.selectFrom('kubun_p2p_circles').selectAll().where('group_id', '=', groupID);
|
|
263
|
-
if (!options?.includeRemoved) {
|
|
264
|
-
query = query.where('removed_at_hlc', 'is', null);
|
|
265
|
-
}
|
|
266
|
-
return await query.execute();
|
|
267
|
-
},
|
|
268
|
-
async addCircleMember (member) {
|
|
269
|
-
// Re-adding a previously-removed member clears the tombstone and refreshes
|
|
270
|
-
// the row's HLC, so a member that left and rejoined is active again.
|
|
271
|
-
await db.insertInto('kubun_p2p_circle_members').values({
|
|
272
|
-
circle_id: member.circle_id,
|
|
273
|
-
member_did: normalizeDID(member.member_did),
|
|
274
|
-
role: member.role,
|
|
275
|
-
hlc: member.hlc
|
|
276
|
-
}).onConflict((oc)=>oc.columns([
|
|
277
|
-
'circle_id',
|
|
278
|
-
'member_did'
|
|
279
|
-
]).doUpdateSet((eb)=>({
|
|
280
|
-
role: eb.ref('excluded.role'),
|
|
281
|
-
hlc: eb.ref('excluded.hlc'),
|
|
282
|
-
removed_at_hlc: null,
|
|
283
|
-
updated_at: adapter.encodeTimestamp(new Date())
|
|
284
|
-
}))).execute();
|
|
285
|
-
},
|
|
286
|
-
// Earliest-wins LWW on `removed_at_hlc`, mirroring `markMemberRemoved`: the
|
|
287
|
-
// update lands only when the row exists AND it is either not yet tombstoned
|
|
288
|
-
// or the incoming `hlc` precedes the stored removal HLC. A removal for a
|
|
289
|
-
// `(circleID, memberDID)` with no local row is ignored — no phantom row is
|
|
290
|
-
// created.
|
|
291
|
-
async removeCircleMember (circleID, memberDID, hlc) {
|
|
292
|
-
await db.updateTable('kubun_p2p_circle_members').set({
|
|
293
|
-
removed_at_hlc: hlc,
|
|
294
|
-
updated_at: adapter.encodeTimestamp(new Date())
|
|
295
|
-
}).where('circle_id', '=', circleID).where('member_did', '=', normalizeDID(memberDID)).where((eb)=>eb.or([
|
|
296
|
-
eb('removed_at_hlc', 'is', null),
|
|
297
|
-
eb('removed_at_hlc', '>', hlc)
|
|
298
|
-
])).execute();
|
|
299
|
-
},
|
|
300
|
-
async getCircleMember (circleID, memberDID) {
|
|
301
|
-
return await db.selectFrom('kubun_p2p_circle_members').selectAll().where('circle_id', '=', circleID).where('member_did', '=', normalizeDID(memberDID)).executeTakeFirst();
|
|
302
|
-
},
|
|
303
|
-
async listCircleMembers (circleID, options) {
|
|
304
|
-
let query = db.selectFrom('kubun_p2p_circle_members').selectAll().where('circle_id', '=', circleID);
|
|
305
|
-
if (!options?.includeRemoved) {
|
|
306
|
-
query = query.where('removed_at_hlc', 'is', null);
|
|
307
|
-
}
|
|
308
|
-
return await query.execute();
|
|
309
|
-
},
|
|
310
|
-
// Read-ACL / sync-scope source: a tombstoned circle or a tombstoned
|
|
311
|
-
// membership grants no read access, so gate both sides on `removed_at_hlc`.
|
|
312
|
-
async getCirclesForMember (memberDID) {
|
|
313
|
-
return await db.selectFrom('kubun_p2p_circle_members').innerJoin('kubun_p2p_circles', 'kubun_p2p_circles.id', 'kubun_p2p_circle_members.circle_id').selectAll('kubun_p2p_circles').where('kubun_p2p_circle_members.member_did', '=', normalizeDID(memberDID)).where('kubun_p2p_circle_members.removed_at_hlc', 'is', null).where('kubun_p2p_circles.removed_at_hlc', 'is', null).execute();
|
|
314
|
-
},
|
|
315
|
-
// Read-ACL / sync-scope source: a tombstoned circle or a tombstoned
|
|
316
|
-
// membership grants no read access, so gate both sides on `removed_at_hlc`.
|
|
317
|
-
async isMemberOfAnyCircle (memberDID, circleIDs) {
|
|
318
|
-
if (circleIDs.length === 0) return false;
|
|
319
|
-
const row = await db.selectFrom('kubun_p2p_circle_members').innerJoin('kubun_p2p_circles', 'kubun_p2p_circles.id', 'kubun_p2p_circle_members.circle_id').select('kubun_p2p_circle_members.circle_id').where('kubun_p2p_circle_members.member_did', '=', normalizeDID(memberDID)).where('kubun_p2p_circle_members.circle_id', 'in', circleIDs).where('kubun_p2p_circle_members.removed_at_hlc', 'is', null).where('kubun_p2p_circles.removed_at_hlc', 'is', null).limit(1).executeTakeFirst();
|
|
320
|
-
return row != null;
|
|
321
|
-
},
|
|
322
|
-
// Unconditional fold-authoritative upsert: every projected column is set
|
|
323
|
-
// from the supplied derived state, including `removed_at_hlc` (a value
|
|
324
|
-
// tombstones, null marks active). No HLC guard — the reproject already
|
|
325
|
-
// resolved the winner via the ledger fold, so the store write must not
|
|
326
|
-
// re-arbitrate.
|
|
327
|
-
async reconcileCircleProjection (params) {
|
|
328
|
-
await db.insertInto('kubun_p2p_circles').values({
|
|
329
|
-
id: params.id,
|
|
330
|
-
group_id: params.groupID,
|
|
331
|
-
name: params.name,
|
|
332
|
-
description: params.description,
|
|
333
|
-
catalog_ids: adapter.encodeJSON(params.catalogIDs),
|
|
334
|
-
hlc: params.hlc,
|
|
335
|
-
removed_at_hlc: params.removedAtHLC
|
|
336
|
-
}).onConflict((oc)=>oc.column('id').doUpdateSet((eb)=>({
|
|
337
|
-
group_id: eb.ref('excluded.group_id'),
|
|
338
|
-
name: eb.ref('excluded.name'),
|
|
339
|
-
description: eb.ref('excluded.description'),
|
|
340
|
-
catalog_ids: eb.ref('excluded.catalog_ids'),
|
|
341
|
-
hlc: eb.ref('excluded.hlc'),
|
|
342
|
-
removed_at_hlc: eb.ref('excluded.removed_at_hlc'),
|
|
343
|
-
updated_at: adapter.encodeTimestamp(new Date())
|
|
344
|
-
}))).execute();
|
|
345
|
-
},
|
|
346
|
-
async reconcileCircleMemberProjection (params) {
|
|
347
|
-
await db.insertInto('kubun_p2p_circle_members').values({
|
|
348
|
-
circle_id: params.circleID,
|
|
349
|
-
member_did: normalizeDID(params.memberDID),
|
|
350
|
-
role: params.role,
|
|
351
|
-
hlc: params.hlc,
|
|
352
|
-
removed_at_hlc: params.removedAtHLC
|
|
353
|
-
}).onConflict((oc)=>oc.columns([
|
|
354
|
-
'circle_id',
|
|
355
|
-
'member_did'
|
|
356
|
-
]).doUpdateSet((eb)=>({
|
|
357
|
-
role: eb.ref('excluded.role'),
|
|
358
|
-
hlc: eb.ref('excluded.hlc'),
|
|
359
|
-
removed_at_hlc: eb.ref('excluded.removed_at_hlc'),
|
|
360
|
-
updated_at: adapter.encodeTimestamp(new Date())
|
|
361
|
-
}))).execute();
|
|
362
|
-
},
|
|
363
|
-
// --- Group-delegation edge operations ---
|
|
364
|
-
async addGroupDelegation (edge) {
|
|
365
|
-
await db.insertInto('kubun_p2p_group_delegation').values({
|
|
366
|
-
group_id: edge.group_id,
|
|
367
|
-
jti: edge.jti
|
|
368
|
-
}).onConflict((oc)=>oc.column('jti').doNothing()).execute();
|
|
369
|
-
},
|
|
370
|
-
async getGroupIDByJTI (jti) {
|
|
371
|
-
const row = await db.selectFrom('kubun_p2p_group_delegation').select('group_id').where('jti', '=', jti).executeTakeFirst();
|
|
372
|
-
return row?.group_id ?? null;
|
|
373
|
-
},
|
|
374
|
-
async getGroupIDsByJTIs (jtis) {
|
|
375
|
-
if (jtis.length === 0) {
|
|
376
|
-
return new Map();
|
|
377
|
-
}
|
|
378
|
-
const rows = await db.selectFrom('kubun_p2p_group_delegation').select([
|
|
379
|
-
'jti',
|
|
380
|
-
'group_id'
|
|
381
|
-
]).where('jti', 'in', jtis).execute();
|
|
382
|
-
return new Map(rows.map((row)=>[
|
|
383
|
-
row.jti,
|
|
384
|
-
row.group_id
|
|
385
|
-
]));
|
|
386
|
-
},
|
|
387
|
-
// --- MLS state operations ---
|
|
388
|
-
async saveMLSState (state) {
|
|
389
|
-
await db.insertInto('kubun_p2p_group_mls_state').values({
|
|
390
|
-
group_id: state.group_id,
|
|
391
|
-
device_id: state.device_id,
|
|
392
|
-
mls_state: adapter.encodeBinary(state.mls_state),
|
|
393
|
-
credential: state.credential,
|
|
394
|
-
epoch: state.epoch,
|
|
395
|
-
root_capability: state.root_capability
|
|
396
|
-
}).onConflict((oc)=>oc.columns([
|
|
397
|
-
'group_id',
|
|
398
|
-
'device_id'
|
|
399
|
-
]).doUpdateSet((eb)=>({
|
|
400
|
-
mls_state: eb.ref('excluded.mls_state'),
|
|
401
|
-
credential: eb.ref('excluded.credential'),
|
|
402
|
-
epoch: eb.ref('excluded.epoch'),
|
|
403
|
-
root_capability: eb.ref('excluded.root_capability'),
|
|
404
|
-
updated_at: adapter.encodeTimestamp(new Date())
|
|
405
|
-
}))// Monotonic-epoch guard: only overwrite when the incoming epoch is
|
|
406
|
-
// at least the stored one. The MLS epoch never moves backwards, so a
|
|
407
|
-
// write carrying a lower epoch is a stale handle re-persisting an
|
|
408
|
-
// already-superseded ratchet — apply it and we roll the ratchet back.
|
|
409
|
-
// When this predicate is false the existing row is left untouched
|
|
410
|
-
// with no error (a silent no-op); a fresh insert has no conflict and
|
|
411
|
-
// is unaffected. The bound is `<=`, not `<`: decrypting an
|
|
412
|
-
// application message advances the secret-tree generation WITHIN an
|
|
413
|
-
// epoch while `epoch` stays constant, so a legitimate same-epoch
|
|
414
|
-
// re-persist carries a changed `mls_state` blob at the same epoch and
|
|
415
|
-
// must still win — `<` would reject it and wedge decryption.
|
|
416
|
-
.where((eb)=>eb('kubun_p2p_group_mls_state.epoch', '<=', eb.ref('excluded.epoch')))).execute();
|
|
417
|
-
},
|
|
418
|
-
async getMLSState (groupID, deviceID) {
|
|
419
|
-
return await db.selectFrom('kubun_p2p_group_mls_state').selectAll().where('group_id', '=', groupID).where('device_id', '=', deviceID).executeTakeFirst();
|
|
420
|
-
},
|
|
421
|
-
async deleteMLSState (groupID, deviceID) {
|
|
422
|
-
await db.deleteFrom('kubun_p2p_group_mls_state').where('group_id', '=', groupID).where('device_id', '=', deviceID).execute();
|
|
423
|
-
},
|
|
424
|
-
async listMLSStates (groupID) {
|
|
425
|
-
return await db.selectFrom('kubun_p2p_group_mls_state').selectAll().where('group_id', '=', groupID).execute();
|
|
426
|
-
},
|
|
427
|
-
async deleteGroupMLSStates (groupID) {
|
|
428
|
-
await db.deleteFrom('kubun_p2p_group_mls_state').where('group_id', '=', groupID).execute();
|
|
429
|
-
},
|
|
430
|
-
// --- Sync peer operations ---
|
|
431
|
-
async addSyncPeer (peer) {
|
|
432
|
-
const allowedUsers = adapter.encodeJSON(peer.allowed_users);
|
|
433
|
-
const config = adapter.encodeJSON(peer.config);
|
|
434
|
-
await db.insertInto('kubun_p2p_sync_peers').values({
|
|
435
|
-
...peer,
|
|
436
|
-
allowed_users: allowedUsers,
|
|
437
|
-
config
|
|
438
|
-
}).onConflict((oc)=>oc.column('peer_did').doUpdateSet({
|
|
439
|
-
endpoint: peer.endpoint,
|
|
440
|
-
mode: peer.mode,
|
|
441
|
-
allowed_users: allowedUsers,
|
|
442
|
-
priority: peer.priority,
|
|
443
|
-
trust_level: peer.trust_level,
|
|
444
|
-
config,
|
|
445
|
-
updated_at: peer.updated_at
|
|
446
|
-
})).execute();
|
|
447
|
-
},
|
|
448
|
-
async getSyncPeer (peerDID) {
|
|
449
|
-
return await db.selectFrom('kubun_p2p_sync_peers').selectAll().where('peer_did', '=', peerDID).executeTakeFirst();
|
|
450
|
-
},
|
|
451
|
-
async listSyncPeers () {
|
|
452
|
-
return await db.selectFrom('kubun_p2p_sync_peers').selectAll().execute();
|
|
453
|
-
},
|
|
454
|
-
async updateSyncPeer (peerDID, update) {
|
|
455
|
-
const values = {};
|
|
456
|
-
if (update.endpoint != null) values.endpoint = update.endpoint;
|
|
457
|
-
if (update.mode != null) values.mode = update.mode;
|
|
458
|
-
if (update.allowed_users != null) values.allowed_users = adapter.encodeJSON(update.allowed_users);
|
|
459
|
-
if (update.priority != null) values.priority = update.priority;
|
|
460
|
-
if (update.trust_level != null) values.trust_level = update.trust_level;
|
|
461
|
-
if (update.config != null) values.config = adapter.encodeJSON(update.config);
|
|
462
|
-
values.updated_at = Date.now();
|
|
463
|
-
await db.updateTable('kubun_p2p_sync_peers').set(values).where('peer_did', '=', peerDID).execute();
|
|
464
|
-
},
|
|
465
|
-
async removeSyncPeer (peerDID) {
|
|
466
|
-
await db.deleteFrom('kubun_p2p_sync_peers').where('peer_did', '=', peerDID).execute();
|
|
467
|
-
},
|
|
468
|
-
// --- Broadcast log operations ---
|
|
469
|
-
async recordBroadcast (entry) {
|
|
470
|
-
const result = await db.insertInto('kubun_p2p_broadcast_log').values({
|
|
471
|
-
doc_id: entry.docID,
|
|
472
|
-
version: entry.version,
|
|
473
|
-
mls_group_id: entry.mlsGroupID
|
|
474
|
-
}).onConflict((oc)=>oc.columns([
|
|
475
|
-
'doc_id',
|
|
476
|
-
'version',
|
|
477
|
-
'mls_group_id'
|
|
478
|
-
]).doNothing()).executeTakeFirst();
|
|
479
|
-
return (result.numInsertedOrUpdatedRows ?? 0n) > 0n;
|
|
480
|
-
},
|
|
481
|
-
async hasBroadcast (entry) {
|
|
482
|
-
const row = await db.selectFrom('kubun_p2p_broadcast_log').select('doc_id').where('doc_id', '=', entry.docID).where('version', '=', entry.version).where('mls_group_id', '=', entry.mlsGroupID).limit(1).executeTakeFirst();
|
|
483
|
-
return row != null;
|
|
484
|
-
},
|
|
485
|
-
// --- Control ledger operations ---
|
|
486
|
-
async appendLedgerEntry (entry) {
|
|
487
|
-
const result = await db.insertInto('kubun_p2p_control_ledger').values({
|
|
488
|
-
group_id: entry.group_id,
|
|
489
|
-
entry_id: entry.entry_id,
|
|
490
|
-
type: entry.type,
|
|
491
|
-
subject: entry.subject,
|
|
492
|
-
value: adapter.encodeJSON(entry.value),
|
|
493
|
-
hlc: entry.hlc,
|
|
494
|
-
issuer_did: normalizeDID(entry.issuer_did),
|
|
495
|
-
signed_token: entry.signed_token
|
|
496
|
-
}).onConflict((oc)=>oc.column('entry_id').doNothing()).executeTakeFirst();
|
|
497
|
-
return (result.numInsertedOrUpdatedRows ?? 0n) > 0n;
|
|
498
|
-
},
|
|
499
|
-
async getLedgerEntries (groupID) {
|
|
500
|
-
return await db.selectFrom('kubun_p2p_control_ledger').selectAll().where('group_id', '=', groupID).orderBy('hlc').orderBy('entry_id').execute();
|
|
501
|
-
},
|
|
502
|
-
async getLedgerEntriesByType (groupID, type) {
|
|
503
|
-
return await db.selectFrom('kubun_p2p_control_ledger').selectAll().where('group_id', '=', groupID).where('type', '=', type).orderBy('hlc').orderBy('entry_id').execute();
|
|
504
|
-
},
|
|
505
|
-
// --- Projection state operations ---
|
|
506
|
-
async getProjectionWatermark (groupID, projection) {
|
|
507
|
-
const row = await db.selectFrom('kubun_p2p_projection_state').select('projected_through_hlc').where('group_id', '=', groupID).where('projection', '=', projection).executeTakeFirst();
|
|
508
|
-
return row?.projected_through_hlc ?? null;
|
|
509
|
-
},
|
|
510
|
-
// Overwrite-on-conflict: callers only ever advance the watermark, so there
|
|
511
|
-
// is no need for a SQL max/guard here.
|
|
512
|
-
async setProjectionWatermark (groupID, projection, hlc) {
|
|
513
|
-
await db.insertInto('kubun_p2p_projection_state').values({
|
|
514
|
-
group_id: groupID,
|
|
515
|
-
projection,
|
|
516
|
-
projected_through_hlc: hlc
|
|
517
|
-
}).onConflict((oc)=>oc.columns([
|
|
518
|
-
'group_id',
|
|
519
|
-
'projection'
|
|
520
|
-
]).doUpdateSet((eb)=>({
|
|
521
|
-
projected_through_hlc: eb.ref('excluded.projected_through_hlc'),
|
|
522
|
-
updated_at: adapter.encodeTimestamp(new Date())
|
|
523
|
-
}))).execute();
|
|
524
|
-
},
|
|
525
|
-
// --- Hub operations ---
|
|
526
|
-
async insertHub (hub) {
|
|
527
|
-
await db.insertInto('kubun_p2p_hubs').values({
|
|
528
|
-
id: hub.id,
|
|
529
|
-
label: hub.label,
|
|
530
|
-
url: hub.url,
|
|
531
|
-
server_did: hub.server_did
|
|
532
|
-
}).execute();
|
|
533
|
-
},
|
|
534
|
-
async getHub (id) {
|
|
535
|
-
return await db.selectFrom('kubun_p2p_hubs').selectAll().where('id', '=', id).executeTakeFirst();
|
|
536
|
-
},
|
|
537
|
-
async getHubByURL (url) {
|
|
538
|
-
return await db.selectFrom('kubun_p2p_hubs').selectAll().where('url', '=', url).executeTakeFirst();
|
|
539
|
-
},
|
|
540
|
-
async updateHub (id, update) {
|
|
541
|
-
const values = {};
|
|
542
|
-
if (update.label !== undefined) values.label = update.label;
|
|
543
|
-
if (update.url !== undefined) values.url = update.url;
|
|
544
|
-
if (update.server_did !== undefined) values.server_did = update.server_did;
|
|
545
|
-
values.updated_at = adapter.encodeTimestamp(new Date());
|
|
546
|
-
await db.updateTable('kubun_p2p_hubs').set(values).where('id', '=', id).execute();
|
|
547
|
-
},
|
|
548
|
-
async deleteHub (id) {
|
|
549
|
-
await db.deleteFrom('kubun_p2p_hubs').where('id', '=', id).execute();
|
|
550
|
-
},
|
|
551
|
-
async listHubs () {
|
|
552
|
-
return await db.selectFrom('kubun_p2p_hubs').selectAll().execute();
|
|
553
|
-
},
|
|
554
|
-
// --- Group-hub binding operations ---
|
|
555
|
-
async insertGroupHub (binding) {
|
|
556
|
-
const result = await db.insertInto('kubun_p2p_group_hubs').values({
|
|
557
|
-
group_id: binding.group_id,
|
|
558
|
-
hub_id: binding.hub_id
|
|
559
|
-
}).onConflict((oc)=>oc.columns([
|
|
560
|
-
'group_id',
|
|
561
|
-
'hub_id'
|
|
562
|
-
]).doNothing()).executeTakeFirst();
|
|
563
|
-
return (result.numInsertedOrUpdatedRows ?? 0n) > 0n;
|
|
564
|
-
},
|
|
565
|
-
async deleteGroupHub (params) {
|
|
566
|
-
const result = await db.deleteFrom('kubun_p2p_group_hubs').where('group_id', '=', params.groupID).where('hub_id', '=', params.hubID).executeTakeFirst();
|
|
567
|
-
return (result.numDeletedRows ?? 0n) > 0n;
|
|
568
|
-
},
|
|
569
|
-
async listGroupIDsByHubID (hubID) {
|
|
570
|
-
const rows = await db.selectFrom('kubun_p2p_group_hubs').select('group_id').where('hub_id', '=', hubID).execute();
|
|
571
|
-
return rows.map((r)=>r.group_id);
|
|
572
|
-
},
|
|
573
|
-
async listHubsByGroupID (groupID) {
|
|
574
|
-
return await db.selectFrom('kubun_p2p_group_hubs').innerJoin('kubun_p2p_hubs', 'kubun_p2p_hubs.id', 'kubun_p2p_group_hubs.hub_id').selectAll('kubun_p2p_hubs').where('kubun_p2p_group_hubs.group_id', '=', groupID).execute();
|
|
575
|
-
},
|
|
576
|
-
async listGroupsByHubID (hubID) {
|
|
577
|
-
const rows = await db.selectFrom('kubun_p2p_group_hubs').innerJoin('kubun_p2p_groups', 'kubun_p2p_groups.id', 'kubun_p2p_group_hubs.group_id').select([
|
|
578
|
-
'kubun_p2p_groups.id as id',
|
|
579
|
-
'kubun_p2p_groups.name as name',
|
|
580
|
-
'kubun_p2p_groups.description as description',
|
|
581
|
-
'kubun_p2p_groups.created_by as created_by',
|
|
582
|
-
'kubun_p2p_groups.hlc as hlc',
|
|
583
|
-
'kubun_p2p_groups.created_at as created_at',
|
|
584
|
-
'kubun_p2p_groups.updated_at as updated_at'
|
|
585
|
-
]).where('kubun_p2p_group_hubs.hub_id', '=', hubID).execute();
|
|
586
|
-
return rows;
|
|
587
|
-
},
|
|
588
|
-
async deleteGroupHubsByHubID (hubID) {
|
|
589
|
-
await db.deleteFrom('kubun_p2p_group_hubs').where('hub_id', '=', hubID).execute();
|
|
590
|
-
}
|
|
11
|
+
return {
|
|
12
|
+
...createGroupOps(db, adapter),
|
|
13
|
+
...createCircleOps(db, adapter),
|
|
14
|
+
...createDelegationOps(db),
|
|
15
|
+
...createMLSOps(db, adapter),
|
|
16
|
+
...createSyncOps(db, adapter),
|
|
17
|
+
...createControlOps(db, adapter),
|
|
18
|
+
...createAppLaneOps(db, adapter),
|
|
19
|
+
...createHubOps(db, adapter),
|
|
20
|
+
...createDiscoveryOps(db, adapter)
|
|
591
21
|
};
|
|
592
|
-
return api;
|
|
593
22
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { StoreProvider } from '@kubun/db';
|
|
2
2
|
import type { P2PStoreAPI } from './api.js';
|
|
3
|
-
export type { P2PStoreAPI } from './api.js';
|
|
3
|
+
export type { ControlRequestKind, ControlRequestOutcome, P2PStoreAPI, PutPendingJoinRequestParams, SetLocalPeerProfileParams, UpsertPeerAnnouncementParams, } from './api.js';
|
|
4
4
|
export { createP2PStore } from './api.js';
|
|
5
5
|
export { p2pStoreDefinition } from './definition.js';
|
|
6
6
|
export declare const P2P_STORE: "p2p";
|
|
7
7
|
export declare function getP2PStore(provider: StoreProvider): Promise<P2PStoreAPI>;
|
|
8
|
-
export type { Circle, CircleMember, CircleMemberTable, CircleTable,
|
|
8
|
+
export type { AppCursor, AppCursorTable, Circle, CircleMember, CircleMemberTable, CircleTable, ControlRequest, ControlRequestTable, Group, GroupAnchor, GroupAnchorTable, GroupDelegation, GroupDelegationTable, GroupHub, GroupHubTable, GroupMember, GroupMemberTable, GroupMLSState, GroupMLSStateTable, GroupStatus, GroupTable, Hub, HubTable, InsertCircle, InsertCircleMember, InsertGroup, InsertGroupDelegation, InsertGroupHub, InsertGroupMember, InsertGroupMLSState, InsertHub, InsertSyncCheckpoint, InsertSyncPeer, LocalPeerProfile, LocalPeerProfileTable, P2PStoreTables, PaginatedResult, PaginationParams, PeerAnnouncement, PeerAnnouncementTable, PeerCapabilityRecord, PendingJoinRequestRow, PendingJoinRequestTable, SyncCheckpoint, SyncCheckpointTable, SyncPeer, SyncPeerTable, UpdateGroupMLSState, } from './tables.js';
|
package/lib/migrations.d.ts
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
1
|
import type { MigrationContext } from '@kubun/db';
|
|
2
2
|
import type { Migration } from 'kysely/migration';
|
|
3
|
+
/**
|
|
4
|
+
* Partial unique index keeping one commit-hub designation per group. Named here
|
|
5
|
+
* because the store recognises a lost first-bind race by this name appearing in
|
|
6
|
+
* the driver's constraint-violation error.
|
|
7
|
+
*/
|
|
8
|
+
export declare const COMMIT_HUB_UNIQUE_INDEX = "uq_p2p_group_hubs_commit";
|
|
9
|
+
/**
|
|
10
|
+
* A shipped migration is immutable: once a database has run it, editing it
|
|
11
|
+
* changes nothing on that database and silently diverges the schema from the
|
|
12
|
+
* code. Every schema change after a release goes in a NEW migration.
|
|
13
|
+
*
|
|
14
|
+
* That rule is suspended for this release, deliberately. The eight migrations
|
|
15
|
+
* this store had accumulated are folded into `0-init`, which is now the whole
|
|
16
|
+
* schema in one readable place. It is a BREAKING change — a database created
|
|
17
|
+
* before it will not pick the new shape up and must be recreated — and it costs
|
|
18
|
+
* nothing here only because the release already requires recreating local
|
|
19
|
+
* databases for unrelated reasons.
|
|
20
|
+
*
|
|
21
|
+
* Reinstate the rule the moment this ships: the next schema change is a new
|
|
22
|
+
* migration, and `0-init` is immutable again.
|
|
23
|
+
*/
|
|
3
24
|
export declare function getP2PMigrations(ctx: MigrationContext): Record<string, Migration>;
|