@kubun/store-p2p 0.18.1 → 0.20.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 +5 -2
- package/lib/api.js +10 -6
- package/lib/definition.d.ts +6 -1
- package/lib/definition.js +37 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/migrations.js +90 -90
- package/lib/ops/app-lane.d.ts +1 -1
- package/lib/ops/app-lane.js +18 -12
- package/lib/ops/app-outbox.d.ts +4 -2
- package/lib/ops/app-outbox.js +33 -33
- package/lib/ops/catalog-offers.d.ts +1 -1
- package/lib/ops/catalog-offers.js +9 -10
- package/lib/ops/catalog-routes.js +9 -9
- package/lib/ops/catalog-sync.d.ts +2 -1
- package/lib/ops/catalog-sync.js +39 -42
- package/lib/ops/circles.js +33 -33
- package/lib/ops/control.d.ts +1 -1
- package/lib/ops/control.js +12 -10
- package/lib/ops/delegations.d.ts +2 -1
- package/lib/ops/delegations.js +17 -25
- package/lib/ops/discovery.d.ts +1 -1
- package/lib/ops/discovery.js +23 -10
- package/lib/ops/document-sweep.js +5 -5
- package/lib/ops/groups.d.ts +3 -2
- package/lib/ops/groups.js +60 -62
- package/lib/ops/hubs.js +38 -57
- package/lib/ops/log-handler-completion.js +3 -3
- package/lib/ops/mls.d.ts +1 -1
- package/lib/ops/mls.js +36 -26
- package/lib/ops/own-agents.d.ts +3 -3
- package/lib/ops/own-agents.js +128 -72
- package/lib/ops/sync.d.ts +1 -1
- package/lib/ops/sync.js +17 -8
- package/lib/scope.d.ts +8 -0
- package/lib/scope.js +8 -0
- package/lib/tables.d.ts +52 -42
- package/package.json +14 -13
package/lib/ops/circles.js
CHANGED
|
@@ -18,22 +18,22 @@ import { sql } from 'kysely';
|
|
|
18
18
|
* at current state, so closing the circle revokes the open arm immediately.
|
|
19
19
|
*
|
|
20
20
|
* Every circle reader shares this one predicate: an open circle carries no
|
|
21
|
-
* membership rows, so a reader resolving access from `
|
|
21
|
+
* membership rows, so a reader resolving access from `p2p_circle_members`
|
|
22
22
|
* alone would see an empty set for a member whose access is purely implicit.
|
|
23
23
|
* Callers must still filter tombstoned circles themselves.
|
|
24
24
|
*/ function circleAccessPredicate(eb, normalizedDID) {
|
|
25
25
|
return eb.or([
|
|
26
|
-
eb.exists(eb.selectFrom('
|
|
26
|
+
eb.exists(eb.selectFrom('p2p_circle_members').select('p2p_circle_members.circle_id').whereRef('p2p_circle_members.circle_id', '=', 'p2p_circles.id').where('p2p_circle_members.member_did', '=', normalizedDID).where('p2p_circle_members.removed_at_hlc', 'is', null)),
|
|
27
27
|
eb.and([
|
|
28
|
-
eb('
|
|
29
|
-
eb.exists(eb.selectFrom('
|
|
28
|
+
eb('p2p_circles.open', '=', 1),
|
|
29
|
+
eb.exists(eb.selectFrom('p2p_group_members').select('p2p_group_members.member_did').whereRef('p2p_group_members.group_id', '=', 'p2p_circles.group_id').where('p2p_group_members.member_did', '=', normalizedDID).where('p2p_group_members.removed_at_hlc', 'is', null))
|
|
30
30
|
])
|
|
31
31
|
]);
|
|
32
32
|
}
|
|
33
33
|
export function createCircleOps(db, adapter) {
|
|
34
34
|
return {
|
|
35
35
|
async createCircle (circle) {
|
|
36
|
-
await db.insertInto('
|
|
36
|
+
await db.insertInto('p2p_circles').values({
|
|
37
37
|
id: circle.id,
|
|
38
38
|
group_id: circle.group_id,
|
|
39
39
|
name: circle.name,
|
|
@@ -42,7 +42,7 @@ export function createCircleOps(db, adapter) {
|
|
|
42
42
|
}).execute();
|
|
43
43
|
},
|
|
44
44
|
async getCircle (id, options) {
|
|
45
|
-
let query = db.selectFrom('
|
|
45
|
+
let query = db.selectFrom('p2p_circles').selectAll().where('id', '=', id);
|
|
46
46
|
if (!options?.includeRemoved) {
|
|
47
47
|
query = query.where('removed_at_hlc', 'is', null);
|
|
48
48
|
}
|
|
@@ -51,7 +51,7 @@ export function createCircleOps(db, adapter) {
|
|
|
51
51
|
async getCircles (ids) {
|
|
52
52
|
const result = new Map();
|
|
53
53
|
for (const chunk of chunked(ids)){
|
|
54
|
-
const rows = await db.selectFrom('
|
|
54
|
+
const rows = await db.selectFrom('p2p_circles').selectAll().where('id', 'in', chunk).execute();
|
|
55
55
|
for (const row of rows){
|
|
56
56
|
result.set(row.id, row);
|
|
57
57
|
}
|
|
@@ -69,10 +69,10 @@ export function createCircleOps(db, adapter) {
|
|
|
69
69
|
values.removed_at_hlc = null;
|
|
70
70
|
}
|
|
71
71
|
values.updated_at = adapter.encodeTimestamp(new Date());
|
|
72
|
-
await db.updateTable('
|
|
72
|
+
await db.updateTable('p2p_circles').set(values).where('id', '=', id).execute();
|
|
73
73
|
},
|
|
74
74
|
async deleteCircle (id) {
|
|
75
|
-
await db.deleteFrom('
|
|
75
|
+
await db.deleteFrom('p2p_circles').where('id', '=', id).execute();
|
|
76
76
|
},
|
|
77
77
|
// Earliest-wins LWW on `removed_at_hlc`: the update lands only when the row
|
|
78
78
|
// exists AND it is either not yet tombstoned or the incoming `hlc` precedes
|
|
@@ -80,7 +80,7 @@ export function createCircleOps(db, adapter) {
|
|
|
80
80
|
// ignored — there is no circle to tombstone, and creating a row would leak
|
|
81
81
|
// a phantom circle.
|
|
82
82
|
async markCircleRemoved (circleID, hlc) {
|
|
83
|
-
await db.updateTable('
|
|
83
|
+
await db.updateTable('p2p_circles').set({
|
|
84
84
|
removed_at_hlc: hlc,
|
|
85
85
|
updated_at: adapter.encodeTimestamp(new Date())
|
|
86
86
|
}).where('id', '=', circleID).where((eb)=>eb.or([
|
|
@@ -89,7 +89,7 @@ export function createCircleOps(db, adapter) {
|
|
|
89
89
|
])).execute();
|
|
90
90
|
},
|
|
91
91
|
async listCirclesByGroup (groupID, options) {
|
|
92
|
-
let query = db.selectFrom('
|
|
92
|
+
let query = db.selectFrom('p2p_circles').selectAll().where('group_id', '=', groupID);
|
|
93
93
|
if (!options?.includeRemoved) {
|
|
94
94
|
query = query.where('removed_at_hlc', 'is', null);
|
|
95
95
|
}
|
|
@@ -98,7 +98,7 @@ export function createCircleOps(db, adapter) {
|
|
|
98
98
|
async addCircleMember (member) {
|
|
99
99
|
// Re-adding a previously-removed member clears the tombstone and refreshes
|
|
100
100
|
// the row's HLC, so a member that left and rejoined is active again.
|
|
101
|
-
await db.insertInto('
|
|
101
|
+
await db.insertInto('p2p_circle_members').values({
|
|
102
102
|
circle_id: member.circle_id,
|
|
103
103
|
member_did: normalizeDID(member.member_did),
|
|
104
104
|
role: member.role,
|
|
@@ -119,7 +119,7 @@ export function createCircleOps(db, adapter) {
|
|
|
119
119
|
// `(circleID, memberDID)` with no local row is ignored — no phantom row is
|
|
120
120
|
// created.
|
|
121
121
|
async removeCircleMember (circleID, memberDID, hlc) {
|
|
122
|
-
await db.updateTable('
|
|
122
|
+
await db.updateTable('p2p_circle_members').set({
|
|
123
123
|
removed_at_hlc: hlc,
|
|
124
124
|
updated_at: adapter.encodeTimestamp(new Date())
|
|
125
125
|
}).where('circle_id', '=', circleID).where('member_did', '=', normalizeDID(memberDID)).where((eb)=>eb.or([
|
|
@@ -128,10 +128,10 @@ export function createCircleOps(db, adapter) {
|
|
|
128
128
|
])).execute();
|
|
129
129
|
},
|
|
130
130
|
async getCircleMember (circleID, memberDID) {
|
|
131
|
-
return await db.selectFrom('
|
|
131
|
+
return await db.selectFrom('p2p_circle_members').selectAll().where('circle_id', '=', circleID).where('member_did', '=', normalizeDID(memberDID)).executeTakeFirst();
|
|
132
132
|
},
|
|
133
133
|
async listCircleMembers (circleID, options) {
|
|
134
|
-
let query = db.selectFrom('
|
|
134
|
+
let query = db.selectFrom('p2p_circle_members').selectAll().where('circle_id', '=', circleID);
|
|
135
135
|
if (!options?.includeRemoved) {
|
|
136
136
|
query = query.where('removed_at_hlc', 'is', null);
|
|
137
137
|
}
|
|
@@ -141,13 +141,13 @@ export function createCircleOps(db, adapter) {
|
|
|
141
141
|
// member can reach, explicitly or through the open arm.
|
|
142
142
|
async getCirclesForMember (memberDID) {
|
|
143
143
|
const normalized = normalizeDID(memberDID);
|
|
144
|
-
return await db.selectFrom('
|
|
144
|
+
return await db.selectFrom('p2p_circles').selectAll('p2p_circles').where('p2p_circles.removed_at_hlc', 'is', null).where((eb)=>circleAccessPredicate(eb, normalized)).execute();
|
|
145
145
|
},
|
|
146
146
|
// Read-ACL / sync-scope source: does the member reach any of these circles?
|
|
147
147
|
async isMemberOfAnyCircle (memberDID, circleIDs) {
|
|
148
148
|
if (circleIDs.length === 0) return false;
|
|
149
149
|
const normalized = normalizeDID(memberDID);
|
|
150
|
-
const row = await db.selectFrom('
|
|
150
|
+
const row = await db.selectFrom('p2p_circles').select('p2p_circles.id').where('p2p_circles.id', 'in', circleIDs).where('p2p_circles.removed_at_hlc', 'is', null).where((eb)=>circleAccessPredicate(eb, normalized)).limit(1).executeTakeFirst();
|
|
151
151
|
return row != null;
|
|
152
152
|
},
|
|
153
153
|
// Same predicate as `isMemberOfAnyCircle`, projecting the matching rows
|
|
@@ -155,10 +155,10 @@ export function createCircleOps(db, adapter) {
|
|
|
155
155
|
async listAccessibleCircles (memberDID, circleIDs) {
|
|
156
156
|
if (circleIDs.length === 0) return [];
|
|
157
157
|
const normalized = normalizeDID(memberDID);
|
|
158
|
-
const rows = await db.selectFrom('
|
|
159
|
-
'
|
|
160
|
-
'
|
|
161
|
-
]).where('
|
|
158
|
+
const rows = await db.selectFrom('p2p_circles').select([
|
|
159
|
+
'p2p_circles.id',
|
|
160
|
+
'p2p_circles.group_id'
|
|
161
|
+
]).where('p2p_circles.id', 'in', circleIDs).where('p2p_circles.removed_at_hlc', 'is', null).where((eb)=>circleAccessPredicate(eb, normalized)).execute();
|
|
162
162
|
return rows.map((row)=>({
|
|
163
163
|
id: row.id,
|
|
164
164
|
groupID: row.group_id
|
|
@@ -172,15 +172,15 @@ export function createCircleOps(db, adapter) {
|
|
|
172
172
|
// The explicit rows and the circle row are independent, so both go out at
|
|
173
173
|
// once; only the group read depends on what the circle says.
|
|
174
174
|
const [explicit, circle] = await Promise.all([
|
|
175
|
-
db.selectFrom('
|
|
176
|
-
db.selectFrom('
|
|
175
|
+
db.selectFrom('p2p_circle_members').select('member_did').where('circle_id', '=', circleID).where('removed_at_hlc', 'is', null).execute(),
|
|
176
|
+
db.selectFrom('p2p_circles').select([
|
|
177
177
|
'group_id',
|
|
178
178
|
'open'
|
|
179
179
|
]).where('id', '=', circleID).where('removed_at_hlc', 'is', null).executeTakeFirst()
|
|
180
180
|
]);
|
|
181
181
|
const owners = new Set(explicit.map((row)=>row.member_did));
|
|
182
182
|
if (circle != null && circle.open === 1) {
|
|
183
|
-
const groupMembers = await db.selectFrom('
|
|
183
|
+
const groupMembers = await db.selectFrom('p2p_group_members').select('member_did').where('group_id', '=', circle.group_id).where('removed_at_hlc', 'is', null).execute();
|
|
184
184
|
for (const row of groupMembers){
|
|
185
185
|
owners.add(row.member_did);
|
|
186
186
|
}
|
|
@@ -196,13 +196,13 @@ export function createCircleOps(db, adapter) {
|
|
|
196
196
|
// First two are independent and go out together; the third needs the open
|
|
197
197
|
// circles' groups, so it waits.
|
|
198
198
|
const [explicit, openCircles] = await Promise.all([
|
|
199
|
-
db.selectFrom('
|
|
200
|
-
db.selectFrom('
|
|
199
|
+
db.selectFrom('p2p_circle_members').select('member_did').where('circle_id', 'in', circleIDs).where('removed_at_hlc', 'is', null).execute(),
|
|
200
|
+
db.selectFrom('p2p_circles').select('group_id').where('id', 'in', circleIDs).where('removed_at_hlc', 'is', null).where('open', '=', 1).execute()
|
|
201
201
|
]);
|
|
202
202
|
const owners = new Set(explicit.map((row)=>row.member_did));
|
|
203
203
|
const groupIDs = Array.from(new Set(openCircles.map((row)=>row.group_id)));
|
|
204
204
|
if (groupIDs.length > 0) {
|
|
205
|
-
const groupMembers = await db.selectFrom('
|
|
205
|
+
const groupMembers = await db.selectFrom('p2p_group_members').select('member_did').where('group_id', 'in', groupIDs).where('removed_at_hlc', 'is', null).execute();
|
|
206
206
|
for (const row of groupMembers){
|
|
207
207
|
owners.add(row.member_did);
|
|
208
208
|
}
|
|
@@ -216,12 +216,12 @@ export function createCircleOps(db, adapter) {
|
|
|
216
216
|
async reconcileCircleOpenFlags (groupID, openCircleIDs) {
|
|
217
217
|
const now = adapter.encodeTimestamp(new Date());
|
|
218
218
|
if (openCircleIDs.length > 0) {
|
|
219
|
-
await db.updateTable('
|
|
219
|
+
await db.updateTable('p2p_circles').set({
|
|
220
220
|
open: 1,
|
|
221
221
|
updated_at: now
|
|
222
222
|
}).where('group_id', '=', groupID).where('id', 'in', openCircleIDs).where('open', '<>', 1).execute();
|
|
223
223
|
}
|
|
224
|
-
let closeQuery = db.updateTable('
|
|
224
|
+
let closeQuery = db.updateTable('p2p_circles').set({
|
|
225
225
|
open: 0,
|
|
226
226
|
updated_at: now
|
|
227
227
|
}).where('group_id', '=', groupID).where('open', '<>', 0);
|
|
@@ -236,7 +236,7 @@ export function createCircleOps(db, adapter) {
|
|
|
236
236
|
// resolved the winner via the ledger fold, so the store write must not
|
|
237
237
|
// re-arbitrate.
|
|
238
238
|
async reconcileCircleProjection (params) {
|
|
239
|
-
await db.insertInto('
|
|
239
|
+
await db.insertInto('p2p_circles').values({
|
|
240
240
|
id: params.id,
|
|
241
241
|
group_id: params.groupID,
|
|
242
242
|
name: params.name,
|
|
@@ -259,10 +259,10 @@ export function createCircleOps(db, adapter) {
|
|
|
259
259
|
'hlc',
|
|
260
260
|
'removed_at_hlc',
|
|
261
261
|
'open'
|
|
262
|
-
].map((column)=>differs('
|
|
262
|
+
].map((column)=>differs('p2p_circles', column))))).execute();
|
|
263
263
|
},
|
|
264
264
|
async reconcileCircleMemberProjection (params) {
|
|
265
|
-
await db.insertInto('
|
|
265
|
+
await db.insertInto('p2p_circle_members').values({
|
|
266
266
|
circle_id: params.circleID,
|
|
267
267
|
member_did: normalizeDID(params.memberDID),
|
|
268
268
|
role: params.role,
|
|
@@ -280,7 +280,7 @@ export function createCircleOps(db, adapter) {
|
|
|
280
280
|
'role',
|
|
281
281
|
'hlc',
|
|
282
282
|
'removed_at_hlc'
|
|
283
|
-
].map((column)=>differs('
|
|
283
|
+
].map((column)=>differs('p2p_circle_members', column))))).execute();
|
|
284
284
|
}
|
|
285
285
|
};
|
|
286
286
|
}
|
package/lib/ops/control.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Adapter } from '@kubun/db-adapter';
|
|
2
2
|
import type { Kysely } from 'kysely';
|
|
3
3
|
import type { CommitJournalEntry, ControlRequest, P2PStoreTables } from '../tables.js';
|
|
4
4
|
export type ControlRequestKind = 'ledger' | 'invite' | 'remove';
|
package/lib/ops/control.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { decodeJSON } from '@kubun/db-adapter';
|
|
1
2
|
export function createControlOps(db, adapter) {
|
|
2
3
|
return {
|
|
3
4
|
async enqueueControlRequest (params) {
|
|
4
|
-
await db.insertInto('
|
|
5
|
+
await db.insertInto('p2p_control_requests').values({
|
|
5
6
|
id: params.id,
|
|
6
7
|
group_id: params.groupID,
|
|
7
8
|
kind: params.kind,
|
|
@@ -9,7 +10,7 @@ export function createControlOps(db, adapter) {
|
|
|
9
10
|
}).execute();
|
|
10
11
|
},
|
|
11
12
|
async getControlRequest (id) {
|
|
12
|
-
return await db.selectFrom('
|
|
13
|
+
return await db.selectFrom('p2p_control_requests').selectAll().where('id', '=', id).executeTakeFirst();
|
|
13
14
|
},
|
|
14
15
|
// Every row still in the table is either `pending` or a settled row the
|
|
15
16
|
// client has not acked yet — no status filter is needed beyond the optional
|
|
@@ -18,7 +19,7 @@ export function createControlOps(db, adapter) {
|
|
|
18
19
|
// `enqueued_at` is a caller-supplied string, so equal stamps are routine —
|
|
19
20
|
// two requests enqueued in one transaction share one. The id breaks the tie
|
|
20
21
|
// so a page is a stable order rather than the storage engine's whim.
|
|
21
|
-
let query = db.selectFrom('
|
|
22
|
+
let query = db.selectFrom('p2p_control_requests').selectAll().orderBy('enqueued_at').orderBy('id');
|
|
22
23
|
if (groupID != null) {
|
|
23
24
|
query = query.where('group_id', '=', groupID);
|
|
24
25
|
}
|
|
@@ -47,7 +48,7 @@ export function createControlOps(db, adapter) {
|
|
|
47
48
|
if (params.result !== undefined) {
|
|
48
49
|
update.result = params.result;
|
|
49
50
|
}
|
|
50
|
-
await db.updateTable('
|
|
51
|
+
await db.updateTable('p2p_control_requests').set(update)// The terminal-state guard, in the WHERE so it is decided by the same
|
|
51
52
|
// statement that writes: only a `pending` row, or a re-settle to the
|
|
52
53
|
// outcome already stored, matches. A different outcome over a terminal
|
|
53
54
|
// row matches nothing and the settle is silently dropped — the stored
|
|
@@ -59,13 +60,13 @@ export function createControlOps(db, adapter) {
|
|
|
59
60
|
])).execute();
|
|
60
61
|
},
|
|
61
62
|
async getControlRequestResult (id) {
|
|
62
|
-
const row = await db.selectFrom('
|
|
63
|
+
const row = await db.selectFrom('p2p_control_requests').select('result').where('id', '=', id).executeTakeFirst();
|
|
63
64
|
return row?.result ?? null;
|
|
64
65
|
},
|
|
65
66
|
// Only a settled row is deleted; a `pending` row (or an already-absent id)
|
|
66
67
|
// matches no row and the delete is a silent no-op.
|
|
67
68
|
async ackControlRequest (id) {
|
|
68
|
-
await db.deleteFrom('
|
|
69
|
+
await db.deleteFrom('p2p_control_requests').where('id', '=', id).where('status', '!=', 'pending').execute();
|
|
69
70
|
},
|
|
70
71
|
// --- Commit journal operations ---
|
|
71
72
|
async putCommitJournalEntry (params) {
|
|
@@ -81,7 +82,7 @@ export function createControlOps(db, adapter) {
|
|
|
81
82
|
journal: adapter.encodeBinary(params.journal),
|
|
82
83
|
holds_log_sends: params.holdsLogSends === true ? 1 : 0
|
|
83
84
|
};
|
|
84
|
-
await db.insertInto('
|
|
85
|
+
await db.insertInto('p2p_commit_journal').values(values)// Single slot per group: a fresh `put` REPLACES the whole row,
|
|
85
86
|
// whatever it held (including a stale `accepted_as` from a prior
|
|
86
87
|
// commit's acceptance) — never a merge.
|
|
87
88
|
.onConflict((oc)=>oc.column('group_id').doUpdateSet((eb)=>({
|
|
@@ -97,23 +98,24 @@ export function createControlOps(db, adapter) {
|
|
|
97
98
|
}))).execute();
|
|
98
99
|
},
|
|
99
100
|
async getCommitJournalEntry (groupID) {
|
|
100
|
-
const row = await db.selectFrom('
|
|
101
|
+
const row = await db.selectFrom('p2p_commit_journal').selectAll().where('group_id', '=', groupID).executeTakeFirst();
|
|
101
102
|
return row == null ? undefined : {
|
|
102
103
|
...row,
|
|
104
|
+
bodies: decodeJSON(adapter.kind, row.bodies),
|
|
103
105
|
holds_log_sends: row.holds_log_sends === 1
|
|
104
106
|
};
|
|
105
107
|
},
|
|
106
108
|
// A no-op if the slot no longer holds `publishID` (already cleared, or
|
|
107
109
|
// replaced by a later `put`) — the `WHERE` simply matches no row.
|
|
108
110
|
async markCommitJournalAccepted (params) {
|
|
109
|
-
await db.updateTable('
|
|
111
|
+
await db.updateTable('p2p_commit_journal').set({
|
|
110
112
|
accepted_as: params.sequenceID
|
|
111
113
|
}).where('group_id', '=', params.groupID).where('publish_id', '=', params.publishID).execute();
|
|
112
114
|
},
|
|
113
115
|
// A no-op if the slot no longer holds `publishID` — never deletes a
|
|
114
116
|
// different pending commit.
|
|
115
117
|
async clearCommitJournalEntry (params) {
|
|
116
|
-
await db.deleteFrom('
|
|
118
|
+
await db.deleteFrom('p2p_commit_journal').where('group_id', '=', params.groupID).where('publish_id', '=', params.publishID).execute();
|
|
117
119
|
}
|
|
118
120
|
};
|
|
119
121
|
}
|
package/lib/ops/delegations.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { StoreAPIContext } from '@kubun/db';
|
|
1
2
|
import type { Kysely } from 'kysely';
|
|
2
3
|
import type { InsertGroupDelegation, P2PStoreTables } from '../tables.js';
|
|
3
4
|
/** The `jti` -> group edges the p2p layer keeps for delegation tokens. */
|
|
@@ -53,4 +54,4 @@ export type DelegationOps = {
|
|
|
53
54
|
*/
|
|
54
55
|
getGroupIDsByJTIs(jtis: Array<string>): Promise<Map<string, string>>;
|
|
55
56
|
};
|
|
56
|
-
export declare function createDelegationOps(db: Kysely<P2PStoreTables
|
|
57
|
+
export declare function createDelegationOps(db: Kysely<P2PStoreTables>, context: StoreAPIContext): DelegationOps;
|
package/lib/ops/delegations.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import { encodeMultibase, multihashSHA256 } from '@kokuin/token';
|
|
2
|
-
|
|
2
|
+
import { withAppOutboxLock } from './app-outbox.js';
|
|
3
|
+
export function createDelegationOps(db, context) {
|
|
3
4
|
const lockJoinedGroup = async (tx, groupID)=>{
|
|
4
|
-
|
|
5
|
+
// Pending admission retains a state row before an encryption key exists.
|
|
6
|
+
await tx.insertInto('p2p_app_outbox_state').values({
|
|
5
7
|
group_id: groupID
|
|
6
|
-
}).onConflict((oc)=>oc.column('group_id').
|
|
7
|
-
|
|
8
|
-
})).execute();
|
|
9
|
-
const group = await tx.selectFrom('kubun_p2p_groups').select('status').where('id', '=', groupID).executeTakeFirst();
|
|
8
|
+
}).onConflict((oc)=>oc.column('group_id').doNothing()).execute();
|
|
9
|
+
const group = await tx.selectFrom('p2p_groups').select('status').where('id', '=', groupID).executeTakeFirst();
|
|
10
10
|
return group?.status === 'joined';
|
|
11
11
|
};
|
|
12
12
|
return {
|
|
13
13
|
async lockJoinedGroupForDelegation (groupID) {
|
|
14
|
-
|
|
15
|
-
return await db.transaction().execute((tx)=>lockJoinedGroup(tx, groupID));
|
|
14
|
+
return await withAppOutboxLock(context, groupID, (tx)=>lockJoinedGroup(tx, groupID));
|
|
16
15
|
},
|
|
17
16
|
async addGroupDelegation (edge) {
|
|
18
|
-
await db.insertInto('
|
|
17
|
+
await db.insertInto('p2p_group_delegation').values({
|
|
19
18
|
group_id: edge.group_id,
|
|
20
19
|
jti: edge.jti
|
|
21
20
|
}).onConflict((oc)=>oc.column('jti').doNothing()).execute();
|
|
@@ -25,7 +24,7 @@ export function createDelegationOps(db) {
|
|
|
25
24
|
// Read membership under the same lock as replacement and departure.
|
|
26
25
|
if (!await lockJoinedGroup(tx, params.groupID)) return;
|
|
27
26
|
const digest = encodeMultibase(multihashSHA256(new TextEncoder().encode(params.token)));
|
|
28
|
-
await tx.updateTable('
|
|
27
|
+
await tx.updateTable('p2p_group_delegation').set(params.kind === 'grant' ? {
|
|
29
28
|
pending_grant_hlc: params.hlc,
|
|
30
29
|
pending_grant_digest: digest
|
|
31
30
|
} : {
|
|
@@ -33,32 +32,26 @@ export function createDelegationOps(db) {
|
|
|
33
32
|
pending_revocation_digest: digest
|
|
34
33
|
}).where('group_id', '=', params.groupID).where('jti', '=', params.jti).execute();
|
|
35
34
|
};
|
|
36
|
-
|
|
37
|
-
else await db.transaction().execute(mark);
|
|
35
|
+
await withAppOutboxLock(context, params.groupID, mark);
|
|
38
36
|
},
|
|
39
37
|
async isDelegationPending (params) {
|
|
40
38
|
const hlcColumn = params.kind === 'grant' ? 'pending_grant_hlc' : 'pending_revocation_hlc';
|
|
41
39
|
const digestColumn = params.kind === 'grant' ? 'pending_grant_digest' : 'pending_revocation_digest';
|
|
42
40
|
const digest = encodeMultibase(multihashSHA256(new TextEncoder().encode(params.token)));
|
|
43
|
-
const row = await db.selectFrom('
|
|
41
|
+
const row = await db.selectFrom('p2p_group_delegation as delegation').innerJoin('p2p_groups as group', 'group.id', 'delegation.group_id').select('delegation.jti').where('delegation.group_id', '=', params.groupID).where('delegation.jti', '=', params.jti).where('group.status', '=', 'joined').where(`delegation.${hlcColumn}`, '=', params.hlc).where(`delegation.${digestColumn}`, '=', digest).executeTakeFirst();
|
|
44
42
|
return row != null;
|
|
45
43
|
},
|
|
46
44
|
async listPendingDelegations (groupID, kind) {
|
|
47
45
|
const column = kind === 'grant' ? 'pending_grant_hlc' : 'pending_revocation_hlc';
|
|
48
|
-
return await db.selectFrom('
|
|
46
|
+
return await db.selectFrom('p2p_group_delegation as delegation').innerJoin('p2p_groups as group', 'group.id', 'delegation.group_id').select([
|
|
49
47
|
'delegation.jti',
|
|
50
48
|
`delegation.${column} as hlc`
|
|
51
49
|
]).where('delegation.group_id', '=', groupID).where('group.status', '=', 'joined').where(`delegation.${column}`, 'is not', null).orderBy(`delegation.${column}`).orderBy('delegation.jti').$narrowType().execute();
|
|
52
50
|
},
|
|
53
51
|
async clearPendingDelegation (params) {
|
|
54
52
|
const clear = async (tx)=>{
|
|
55
|
-
await tx.insertInto('kubun_p2p_app_outbox_state').values({
|
|
56
|
-
group_id: params.groupID
|
|
57
|
-
}).onConflict((oc)=>oc.column('group_id').doUpdateSet({
|
|
58
|
-
group_id: params.groupID
|
|
59
|
-
})).execute();
|
|
60
53
|
const hlcColumn = params.kind === 'grant' ? 'pending_grant_hlc' : 'pending_revocation_hlc';
|
|
61
|
-
await tx.updateTable('
|
|
54
|
+
await tx.updateTable('p2p_group_delegation').set(params.kind === 'grant' ? {
|
|
62
55
|
pending_grant_hlc: null,
|
|
63
56
|
pending_grant_digest: null
|
|
64
57
|
} : {
|
|
@@ -66,11 +59,10 @@ export function createDelegationOps(db) {
|
|
|
66
59
|
pending_revocation_digest: null
|
|
67
60
|
}).where('group_id', '=', params.groupID).where('jti', '=', params.jti).where(hlcColumn, '=', params.hlc).execute();
|
|
68
61
|
};
|
|
69
|
-
|
|
70
|
-
else await db.transaction().execute(clear);
|
|
62
|
+
await withAppOutboxLock(context, params.groupID, clear);
|
|
71
63
|
},
|
|
72
64
|
async clearPendingDelegations (groupID) {
|
|
73
|
-
await db.updateTable('
|
|
65
|
+
await db.updateTable('p2p_group_delegation').set({
|
|
74
66
|
pending_grant_hlc: null,
|
|
75
67
|
pending_grant_digest: null,
|
|
76
68
|
pending_revocation_hlc: null,
|
|
@@ -78,14 +70,14 @@ export function createDelegationOps(db) {
|
|
|
78
70
|
}).where('group_id', '=', groupID).execute();
|
|
79
71
|
},
|
|
80
72
|
async getGroupIDByJTI (jti) {
|
|
81
|
-
const row = await db.selectFrom('
|
|
73
|
+
const row = await db.selectFrom('p2p_group_delegation').select('group_id').where('jti', '=', jti).executeTakeFirst();
|
|
82
74
|
return row?.group_id ?? null;
|
|
83
75
|
},
|
|
84
76
|
async getGroupIDsByJTIs (jtis) {
|
|
85
77
|
if (jtis.length === 0) {
|
|
86
78
|
return new Map();
|
|
87
79
|
}
|
|
88
|
-
const rows = await db.selectFrom('
|
|
80
|
+
const rows = await db.selectFrom('p2p_group_delegation').select([
|
|
89
81
|
'jti',
|
|
90
82
|
'group_id'
|
|
91
83
|
]).where('jti', 'in', jtis).execute();
|
package/lib/ops/discovery.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Adapter } from '@kubun/db-adapter';
|
|
2
2
|
import type { Kysely } from 'kysely';
|
|
3
3
|
import type { LocalPeerProfile, P2PStoreTables, PeerAnnouncement, PeerCapabilityRecord, PendingJoinRequestRow } from '../tables.js';
|
|
4
4
|
export type PutPendingJoinRequestParams = {
|
package/lib/ops/discovery.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeDID } from '@kokuin/token';
|
|
2
|
+
import { decodeJSON } from '@kubun/db-adapter';
|
|
2
3
|
/**
|
|
3
4
|
* How long a prepared join request stays usable. The exchange it belongs to is a
|
|
4
5
|
* human round-trip — a payload pasted into a message, answered whenever the other
|
|
@@ -26,7 +27,7 @@ export function createDiscoveryOps(db, adapter) {
|
|
|
26
27
|
announced_at: adapter.encodeTimestamp(new Date()),
|
|
27
28
|
announced_epoch: params.epoch
|
|
28
29
|
};
|
|
29
|
-
await db.insertInto('
|
|
30
|
+
await db.insertInto('p2p_peer_announcements').values(values).onConflict((oc)=>oc.columns([
|
|
30
31
|
'group_id',
|
|
31
32
|
'peer_did'
|
|
32
33
|
]).doUpdateSet((eb)=>({
|
|
@@ -39,16 +40,24 @@ export function createDiscoveryOps(db, adapter) {
|
|
|
39
40
|
}))).execute();
|
|
40
41
|
},
|
|
41
42
|
async getPeerAnnouncement (params) {
|
|
42
|
-
|
|
43
|
+
const row = await db.selectFrom('p2p_peer_announcements').selectAll().where('group_id', '=', params.groupID).where('peer_did', '=', normalizeDID(params.peerDID)).executeTakeFirst();
|
|
44
|
+
return row == null ? undefined : {
|
|
45
|
+
...row,
|
|
46
|
+
capabilities: decodeJSON(adapter.kind, row.capabilities)
|
|
47
|
+
};
|
|
43
48
|
},
|
|
44
49
|
async listPeerAnnouncements (groupID) {
|
|
45
|
-
|
|
50
|
+
const rows = await db.selectFrom('p2p_peer_announcements').selectAll().where('group_id', '=', groupID).orderBy('peer_did').execute();
|
|
51
|
+
return rows.map((row)=>({
|
|
52
|
+
...row,
|
|
53
|
+
capabilities: decodeJSON(adapter.kind, row.capabilities)
|
|
54
|
+
}));
|
|
46
55
|
},
|
|
47
56
|
async deletePeerAnnouncements (params) {
|
|
48
57
|
if (params.peerDIDs.length === 0) {
|
|
49
58
|
return;
|
|
50
59
|
}
|
|
51
|
-
await db.deleteFrom('
|
|
60
|
+
await db.deleteFrom('p2p_peer_announcements').where('group_id', '=', params.groupID).where('peer_did', 'in', params.peerDIDs.map(normalizeDID)).execute();
|
|
52
61
|
},
|
|
53
62
|
async setLocalPeerProfile (params) {
|
|
54
63
|
const values = {
|
|
@@ -58,7 +67,7 @@ export function createDiscoveryOps(db, adapter) {
|
|
|
58
67
|
capabilities: adapter.encodeJSON(params.capabilities),
|
|
59
68
|
updated_at: adapter.encodeTimestamp(new Date())
|
|
60
69
|
};
|
|
61
|
-
await db.insertInto('
|
|
70
|
+
await db.insertInto('p2p_local_peer_profile').values(values).onConflict((oc)=>oc.column('peer_did').doUpdateSet((eb)=>({
|
|
62
71
|
label: eb.ref('excluded.label'),
|
|
63
72
|
availability: eb.ref('excluded.availability'),
|
|
64
73
|
capabilities: eb.ref('excluded.capabilities'),
|
|
@@ -66,25 +75,29 @@ export function createDiscoveryOps(db, adapter) {
|
|
|
66
75
|
}))).execute();
|
|
67
76
|
},
|
|
68
77
|
async getLocalPeerProfile (peerDID) {
|
|
69
|
-
|
|
78
|
+
const row = await db.selectFrom('p2p_local_peer_profile').selectAll().where('peer_did', '=', normalizeDID(peerDID)).executeTakeFirst();
|
|
79
|
+
return row == null ? undefined : {
|
|
80
|
+
...row,
|
|
81
|
+
capabilities: decodeJSON(adapter.kind, row.capabilities)
|
|
82
|
+
};
|
|
70
83
|
},
|
|
71
84
|
async putPendingJoinRequest (params) {
|
|
72
85
|
// Swept on write rather than on a timer: preparing a request is the only
|
|
73
86
|
// moment this table grows, so it is the only moment it needs collecting.
|
|
74
|
-
await db.deleteFrom('
|
|
87
|
+
await db.deleteFrom('p2p_pending_join_requests').where('created_at', '<', expiredBefore(adapter)).execute();
|
|
75
88
|
const values = {
|
|
76
89
|
did: normalizeDID(params.did),
|
|
77
90
|
payload: params.payload
|
|
78
91
|
};
|
|
79
|
-
await db.insertInto('
|
|
92
|
+
await db.insertInto('p2p_pending_join_requests').values(values).onConflict((oc)=>oc.column('did').doUpdateSet((eb)=>({
|
|
80
93
|
payload: eb.ref('excluded.payload')
|
|
81
94
|
}))).execute();
|
|
82
95
|
},
|
|
83
96
|
async getPendingJoinRequest (did) {
|
|
84
|
-
return await db.selectFrom('
|
|
97
|
+
return await db.selectFrom('p2p_pending_join_requests').selectAll().where('did', '=', normalizeDID(did)).where('created_at', '>=', expiredBefore(adapter)).executeTakeFirst();
|
|
85
98
|
},
|
|
86
99
|
async deletePendingJoinRequest (did) {
|
|
87
|
-
await db.deleteFrom('
|
|
100
|
+
await db.deleteFrom('p2p_pending_join_requests').where('did', '=', normalizeDID(did)).execute();
|
|
88
101
|
}
|
|
89
102
|
};
|
|
90
103
|
}
|
|
@@ -2,15 +2,15 @@ export function createDocumentSweepOps(db) {
|
|
|
2
2
|
return {
|
|
3
3
|
async sweepDocumentRows (documentID) {
|
|
4
4
|
for (const table of [
|
|
5
|
-
'
|
|
6
|
-
'
|
|
7
|
-
'
|
|
8
|
-
'
|
|
5
|
+
'p2p_document_repair_answers',
|
|
6
|
+
'p2p_document_repairs',
|
|
7
|
+
'p2p_observation_repair_peers',
|
|
8
|
+
'p2p_observation_repairs'
|
|
9
9
|
]){
|
|
10
10
|
await db.deleteFrom(table).where('document_id', '=', documentID).execute();
|
|
11
11
|
}
|
|
12
12
|
// Dedups sends only: a version sent again after a restore is a no-op where it was applied.
|
|
13
|
-
await db.deleteFrom('
|
|
13
|
+
await db.deleteFrom('p2p_broadcast_log').where('doc_id', '=', documentID).execute();
|
|
14
14
|
}
|
|
15
15
|
};
|
|
16
16
|
}
|
package/lib/ops/groups.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { StoreAPIContext } from '@kubun/db';
|
|
1
2
|
import { type Adapter } from '@kubun/db-adapter';
|
|
2
3
|
import type { Kysely } from 'kysely';
|
|
3
4
|
import type { Group, GroupMember, GroupStatus, InsertGroup, InsertGroupMember, P2PStoreTables, PaginatedResult, PaginationParams } from '../tables.js';
|
|
@@ -159,7 +160,7 @@ export type GroupOps = {
|
|
|
159
160
|
* "any".
|
|
160
161
|
*
|
|
161
162
|
* Reads the membership rows ONLY. Not interchangeable with
|
|
162
|
-
* {@link getGroupsForMember}, which inner-joins `
|
|
163
|
+
* {@link getGroupsForMember}, which inner-joins `p2p_groups` and so
|
|
163
164
|
* answers false for a membership whose group row this device does not hold —
|
|
164
165
|
* an authorization gate must not deny on that.
|
|
165
166
|
*/
|
|
@@ -177,4 +178,4 @@ export type GroupOps = {
|
|
|
177
178
|
*/
|
|
178
179
|
getGroupsForMembers(memberDIDs: Array<string>): Promise<Map<string, Array<Group>>>;
|
|
179
180
|
};
|
|
180
|
-
export declare function createGroupOps(db: Kysely<P2PStoreTables>, adapter: Adapter): GroupOps;
|
|
181
|
+
export declare function createGroupOps(db: Kysely<P2PStoreTables>, adapter: Adapter, context: StoreAPIContext): GroupOps;
|