@kubun/store-p2p 0.19.0 → 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.
@@ -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 `kubun_p2p_circle_members`
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('kubun_p2p_circle_members').select('kubun_p2p_circle_members.circle_id').whereRef('kubun_p2p_circle_members.circle_id', '=', 'kubun_p2p_circles.id').where('kubun_p2p_circle_members.member_did', '=', normalizedDID).where('kubun_p2p_circle_members.removed_at_hlc', 'is', null)),
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('kubun_p2p_circles.open', '=', 1),
29
- eb.exists(eb.selectFrom('kubun_p2p_group_members').select('kubun_p2p_group_members.member_did').whereRef('kubun_p2p_group_members.group_id', '=', 'kubun_p2p_circles.group_id').where('kubun_p2p_group_members.member_did', '=', normalizedDID).where('kubun_p2p_group_members.removed_at_hlc', 'is', null))
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('kubun_p2p_circles').values({
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('kubun_p2p_circles').selectAll().where('id', '=', id);
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('kubun_p2p_circles').selectAll().where('id', 'in', chunk).execute();
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('kubun_p2p_circles').set(values).where('id', '=', id).execute();
72
+ await db.updateTable('p2p_circles').set(values).where('id', '=', id).execute();
73
73
  },
74
74
  async deleteCircle (id) {
75
- await db.deleteFrom('kubun_p2p_circles').where('id', '=', id).execute();
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('kubun_p2p_circles').set({
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('kubun_p2p_circles').selectAll().where('group_id', '=', groupID);
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('kubun_p2p_circle_members').values({
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('kubun_p2p_circle_members').set({
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('kubun_p2p_circle_members').selectAll().where('circle_id', '=', circleID).where('member_did', '=', normalizeDID(memberDID)).executeTakeFirst();
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('kubun_p2p_circle_members').selectAll().where('circle_id', '=', circleID);
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('kubun_p2p_circles').selectAll('kubun_p2p_circles').where('kubun_p2p_circles.removed_at_hlc', 'is', null).where((eb)=>circleAccessPredicate(eb, normalized)).execute();
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('kubun_p2p_circles').select('kubun_p2p_circles.id').where('kubun_p2p_circles.id', 'in', circleIDs).where('kubun_p2p_circles.removed_at_hlc', 'is', null).where((eb)=>circleAccessPredicate(eb, normalized)).limit(1).executeTakeFirst();
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('kubun_p2p_circles').select([
159
- 'kubun_p2p_circles.id',
160
- 'kubun_p2p_circles.group_id'
161
- ]).where('kubun_p2p_circles.id', 'in', circleIDs).where('kubun_p2p_circles.removed_at_hlc', 'is', null).where((eb)=>circleAccessPredicate(eb, normalized)).execute();
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('kubun_p2p_circle_members').select('member_did').where('circle_id', '=', circleID).where('removed_at_hlc', 'is', null).execute(),
176
- db.selectFrom('kubun_p2p_circles').select([
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('kubun_p2p_group_members').select('member_did').where('group_id', '=', circle.group_id).where('removed_at_hlc', 'is', null).execute();
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('kubun_p2p_circle_members').select('member_did').where('circle_id', 'in', circleIDs).where('removed_at_hlc', 'is', null).execute(),
200
- db.selectFrom('kubun_p2p_circles').select('group_id').where('id', 'in', circleIDs).where('removed_at_hlc', 'is', null).where('open', '=', 1).execute()
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('kubun_p2p_group_members').select('member_did').where('group_id', 'in', groupIDs).where('removed_at_hlc', 'is', null).execute();
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('kubun_p2p_circles').set({
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('kubun_p2p_circles').set({
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('kubun_p2p_circles').values({
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('kubun_p2p_circles', column))))).execute();
262
+ ].map((column)=>differs('p2p_circles', column))))).execute();
263
263
  },
264
264
  async reconcileCircleMemberProjection (params) {
265
- await db.insertInto('kubun_p2p_circle_members').values({
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('kubun_p2p_circle_members', column))))).execute();
283
+ ].map((column)=>differs('p2p_circle_members', column))))).execute();
284
284
  }
285
285
  };
286
286
  }
@@ -1,4 +1,4 @@
1
- import type { Adapter } from '@kubun/db-adapter';
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';
@@ -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('kubun_p2p_control_requests').values({
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('kubun_p2p_control_requests').selectAll().where('id', '=', id).executeTakeFirst();
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('kubun_p2p_control_requests').selectAll().orderBy('enqueued_at').orderBy('id');
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('kubun_p2p_control_requests').set(update)// The terminal-state guard, in the WHERE so it is decided by the same
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('kubun_p2p_control_requests').select('result').where('id', '=', id).executeTakeFirst();
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('kubun_p2p_control_requests').where('id', '=', id).where('status', '!=', 'pending').execute();
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('kubun_p2p_commit_journal').values(values)// Single slot per group: a fresh `put` REPLACES the whole row,
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('kubun_p2p_commit_journal').selectAll().where('group_id', '=', groupID).executeTakeFirst();
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('kubun_p2p_commit_journal').set({
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('kubun_p2p_commit_journal').where('group_id', '=', params.groupID).where('publish_id', '=', params.publishID).execute();
118
+ await db.deleteFrom('p2p_commit_journal').where('group_id', '=', params.groupID).where('publish_id', '=', params.publishID).execute();
117
119
  }
118
120
  };
119
121
  }
@@ -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>): DelegationOps;
57
+ export declare function createDelegationOps(db: Kysely<P2PStoreTables>, context: StoreAPIContext): DelegationOps;
@@ -1,21 +1,20 @@
1
1
  import { encodeMultibase, multihashSHA256 } from '@kokuin/token';
2
- export function createDelegationOps(db) {
2
+ import { withAppOutboxLock } from './app-outbox.js';
3
+ export function createDelegationOps(db, context) {
3
4
  const lockJoinedGroup = async (tx, groupID)=>{
4
- await tx.insertInto('kubun_p2p_app_outbox_state').values({
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').doUpdateSet({
7
- group_id: groupID
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
- if (db.isTransaction) return await lockJoinedGroup(db, groupID);
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('kubun_p2p_group_delegation').values({
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('kubun_p2p_group_delegation').set(params.kind === 'grant' ? {
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
- if (db.isTransaction) await mark(db);
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('kubun_p2p_group_delegation as delegation').innerJoin('kubun_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();
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('kubun_p2p_group_delegation as delegation').innerJoin('kubun_p2p_groups as group', 'group.id', 'delegation.group_id').select([
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('kubun_p2p_group_delegation').set(params.kind === 'grant' ? {
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
- if (db.isTransaction) await clear(db);
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('kubun_p2p_group_delegation').set({
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('kubun_p2p_group_delegation').select('group_id').where('jti', '=', jti).executeTakeFirst();
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('kubun_p2p_group_delegation').select([
80
+ const rows = await db.selectFrom('p2p_group_delegation').select([
89
81
  'jti',
90
82
  'group_id'
91
83
  ]).where('jti', 'in', jtis).execute();
@@ -1,4 +1,4 @@
1
- import type { Adapter } from '@kubun/db-adapter';
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 = {
@@ -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('kubun_p2p_peer_announcements').values(values).onConflict((oc)=>oc.columns([
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
- return await db.selectFrom('kubun_p2p_peer_announcements').selectAll().where('group_id', '=', params.groupID).where('peer_did', '=', normalizeDID(params.peerDID)).executeTakeFirst();
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
- return await db.selectFrom('kubun_p2p_peer_announcements').selectAll().where('group_id', '=', groupID).orderBy('peer_did').execute();
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('kubun_p2p_peer_announcements').where('group_id', '=', params.groupID).where('peer_did', 'in', params.peerDIDs.map(normalizeDID)).execute();
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('kubun_p2p_local_peer_profile').values(values).onConflict((oc)=>oc.column('peer_did').doUpdateSet((eb)=>({
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
- return await db.selectFrom('kubun_p2p_local_peer_profile').selectAll().where('peer_did', '=', normalizeDID(peerDID)).executeTakeFirst();
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('kubun_p2p_pending_join_requests').where('created_at', '<', expiredBefore(adapter)).execute();
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('kubun_p2p_pending_join_requests').values(values).onConflict((oc)=>oc.column('did').doUpdateSet((eb)=>({
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('kubun_p2p_pending_join_requests').selectAll().where('did', '=', normalizeDID(did)).where('created_at', '>=', expiredBefore(adapter)).executeTakeFirst();
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('kubun_p2p_pending_join_requests').where('did', '=', normalizeDID(did)).execute();
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
- 'kubun_p2p_document_repair_answers',
6
- 'kubun_p2p_document_repairs',
7
- 'kubun_p2p_observation_repair_peers',
8
- 'kubun_p2p_observation_repairs'
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('kubun_p2p_broadcast_log').where('doc_id', '=', documentID).execute();
13
+ await db.deleteFrom('p2p_broadcast_log').where('doc_id', '=', documentID).execute();
14
14
  }
15
15
  };
16
16
  }
@@ -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 `kubun_p2p_groups` and so
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;