@kubun/store-p2p 0.10.0 → 0.11.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.js CHANGED
@@ -1 +1,593 @@
1
- import{getKubunLogger as e}from"@kubun/logger";export function createP2PStore(t,r,u=e("store-p2p")){let i={async createGroup(e){await t.insertInto("kubun_p2p_groups").values({id:e.id,name:e.name,description:e.description,created_by:e.created_by,hlc:e.hlc}).execute()},getGroup:async e=>await t.selectFrom("kubun_p2p_groups").selectAll().where("id","=",e).executeTakeFirst(),async updateGroup(e,u){let i={};null!=u.name&&(i.name=u.name),null!=u.description&&(i.description=u.description),null!=u.hlc&&(i.hlc=u.hlc),i.updated_at=r.encodeTimestamp(new Date),await t.updateTable("kubun_p2p_groups").set(i).where("id","=",e).execute()},async deleteGroup(e){await t.deleteFrom("kubun_p2p_groups").where("id","=",e).execute()},listGroups:async()=>await t.selectFrom("kubun_p2p_groups").selectAll().execute(),async listGroupsOffsetPaged(e){let{limit:r,offset:u}=e,i=t.selectFrom("kubun_p2p_groups").selectAll().orderBy("name");if(null!=u&&(i=i.offset(u)),null!=r){i=i.limit(r+1);let e=await i.execute(),t=e.length>r;return{entries:t?e.slice(0,r):e,hasMore:t}}return{entries:await i.execute(),hasMore:!1}},async addGroupMember(e){await t.insertInto("kubun_p2p_group_members").values({group_id:e.group_id,member_did:e.member_did,role:e.role,hlc:e.hlc}).onConflict(e=>e.columns(["group_id","member_did"]).doUpdateSet(e=>({role:e.ref("excluded.role"),hlc:e.ref("excluded.hlc"),removed_at_hlc:null,updated_at:r.encodeTimestamp(new Date)}))).execute()},async markMemberRemoved(e,u,i){await t.updateTable("kubun_p2p_group_members").set({removed_at_hlc:i,updated_at:r.encodeTimestamp(new Date)}).where("group_id","=",e).where("member_did","=",u).where(e=>e.or([e("removed_at_hlc","is",null),e("removed_at_hlc",">",i)])).execute()},async getMemberRemoval(e,r){let u=await t.selectFrom("kubun_p2p_group_members").select("removed_at_hlc").where("group_id","=",e).where("member_did","=",r).executeTakeFirst();return null==u?void 0:{removed_at_hlc:u.removed_at_hlc}},async removeGroupMember(e,t,r){await i.markMemberRemoved(e,t,r)},getGroupMember:async(e,r)=>await t.selectFrom("kubun_p2p_group_members").selectAll().where("group_id","=",e).where("member_did","=",r).where("removed_at_hlc","is",null).executeTakeFirst(),isGroupMember:async(e,r)=>null!=await t.selectFrom("kubun_p2p_group_members").select("member_did").where("group_id","=",e).where("member_did","=",r).where("removed_at_hlc","is",null).executeTakeFirst(),isMemberOfAnyGroup:async(e,r)=>0!==r.length&&null!=await t.selectFrom("kubun_p2p_group_members").select("group_id").where("member_did","=",e).where("group_id","in",r).where("removed_at_hlc","is",null).limit(1).executeTakeFirst(),listGroupMembers:async e=>await t.selectFrom("kubun_p2p_group_members").selectAll().where("group_id","=",e).where("removed_at_hlc","is",null).execute(),getGroupsForMember:async e=>await t.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","=",e).where("kubun_p2p_group_members.removed_at_hlc","is",null).execute(),async getGroupsForMembers(e){let r=new Map;if(0===e.length)return r;for(let u of(await t.selectFrom("kubun_p2p_group_members").innerJoin("kubun_p2p_groups","kubun_p2p_groups.id","kubun_p2p_group_members.group_id").select(["kubun_p2p_group_members.member_did as member_did","kubun_p2p_groups.id as id","kubun_p2p_groups.name as name","kubun_p2p_groups.description as description","kubun_p2p_groups.created_by as created_by","kubun_p2p_groups.hlc as hlc","kubun_p2p_groups.created_at as created_at","kubun_p2p_groups.updated_at as updated_at"]).where("kubun_p2p_group_members.member_did","in",e).where("kubun_p2p_group_members.removed_at_hlc","is",null).execute())){let{member_did:e,...t}=u,i=r.get(e);null!=i?i.push(t):r.set(e,[t])}return r},async createCircle(e){await t.insertInto("kubun_p2p_circles").values({id:e.id,group_id:e.group_id,name:e.name,description:e.description,catalog_ids:r.encodeJSON(e.catalog_ids),hlc:e.hlc}).execute()},getCircle:async e=>await t.selectFrom("kubun_p2p_circles").selectAll().where("id","=",e).executeTakeFirst(),async getCircles(e){let r=new Map;if(0===e.length)return r;for(let u of(await t.selectFrom("kubun_p2p_circles").selectAll().where("id","in",e).execute()))r.set(u.id,u);return r},async updateCircle(e,u){let i={};null!=u.name&&(i.name=u.name),null!=u.description&&(i.description=u.description),null!=u.catalog_ids&&(i.catalog_ids=r.encodeJSON(u.catalog_ids)),null!=u.hlc&&(i.hlc=u.hlc),i.updated_at=r.encodeTimestamp(new Date),await t.updateTable("kubun_p2p_circles").set(i).where("id","=",e).execute()},async deleteCircle(e){await t.deleteFrom("kubun_p2p_circles").where("id","=",e).execute()},listCirclesByGroup:async e=>await t.selectFrom("kubun_p2p_circles").selectAll().where("group_id","=",e).execute(),async addCircleMember(e){await t.insertInto("kubun_p2p_circle_members").values({circle_id:e.circle_id,member_did:e.member_did,role:e.role,hlc:e.hlc}).onConflict(e=>e.columns(["circle_id","member_did"]).doUpdateSet(e=>({role:e.ref("excluded.role"),hlc:e.ref("excluded.hlc")}))).execute()},async removeCircleMember(e,r){await t.deleteFrom("kubun_p2p_circle_members").where("circle_id","=",e).where("member_did","=",r).execute()},getCircleMember:async(e,r)=>await t.selectFrom("kubun_p2p_circle_members").selectAll().where("circle_id","=",e).where("member_did","=",r).executeTakeFirst(),listCircleMembers:async e=>await t.selectFrom("kubun_p2p_circle_members").selectAll().where("circle_id","=",e).execute(),getCirclesForMember:async e=>await t.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","=",e).execute(),isMemberOfAnyCircle:async(e,r)=>0!==r.length&&null!=await t.selectFrom("kubun_p2p_circle_members").select("circle_id").where("member_did","=",e).where("circle_id","in",r).limit(1).executeTakeFirst(),async addDelegationToken(e){let a,_=await t.selectFrom("kubun_p2p_delegation_tokens").select(["jti","token","act","exp","group_id","hlc"]).where("grantor","=",e.grantor).where("audience","=",e.audience).where("resource","=",e.resource).executeTakeFirst();if(await t.insertInto("kubun_p2p_delegation_tokens").values({jti:e.jti,grantor:e.grantor,audience:e.audience,token:e.token,resource:e.resource,act:e.act,exp:e.exp,group_id:e.group_id,hlc:e.hlc}).onConflict(e=>e.columns(["grantor","audience","resource"]).doUpdateSet(e=>({jti:e.ref("excluded.jti"),token:e.ref("excluded.token"),act:e.ref("excluded.act"),exp:e.ref("excluded.exp"),group_id:e.ref("excluded.group_id"),hlc:e.ref("excluded.hlc"),updated_at:r.encodeTimestamp(new Date)})).where(e=>e("excluded.hlc",">",e.ref("kubun_p2p_delegation_tokens.hlc")))).execute(),a=null==_||!(e.hlc<=_.hlc)&&(_.jti!==e.jti||_.token!==e.token||_.act!==e.act||_.exp!==e.exp||_.group_id!==e.group_id),.01>Math.random())try{await i.purgeExpiredRevocations({graceSeconds:2592e3}),await i.purgeDeadPendingRevocations({maxAgeSeconds:604800})}catch(e){u.warn("revocation GC sweep failed during addDelegationToken: {error}",{error:e})}return a},getDelegationTokens:async e=>await t.selectFrom("kubun_p2p_delegation_tokens").selectAll().where("grantor","=",e.grantor).where("audience","=",e.audience).execute(),getHeldTokens:async e=>await t.selectFrom("kubun_p2p_delegation_tokens").leftJoin("kubun_p2p_revoked_capabilities","kubun_p2p_revoked_capabilities.jti","kubun_p2p_delegation_tokens.jti").selectAll("kubun_p2p_delegation_tokens").where("audience","=",e.audience).where("exp",">",e.atTime).where(e=>e.or([e("kubun_p2p_revoked_capabilities.jti","is",null),e("kubun_p2p_revoked_capabilities.verified_at","is",null)])).execute(),listIssuedTokens:async e=>await t.selectFrom("kubun_p2p_delegation_tokens").selectAll().where("grantor","=",e).execute(),getDelegationTokenByJti:async e=>await t.selectFrom("kubun_p2p_delegation_tokens").selectAll().where("jti","=",e).executeTakeFirst()??null,async addRevocation(e){let u=await t.selectFrom("kubun_p2p_revoked_capabilities").select(["revoker_did","revoked_iat","revocation_token","verified_at","cap_exp","group_id","hlc"]).where("jti","=",e.jti).executeTakeFirst();if(await t.insertInto("kubun_p2p_revoked_capabilities").values({jti:e.jti,revoker_did:e.revoker_did,revoked_iat:e.revoked_iat,revocation_token:e.revocation_token,verified_at:e.verified_at??null,cap_exp:e.cap_exp??null,group_id:e.group_id,hlc:e.hlc}).onConflict(e=>e.column("jti").doUpdateSet(e=>({revoker_did:e.ref("excluded.revoker_did"),revoked_iat:e.ref("excluded.revoked_iat"),revocation_token:e.ref("excluded.revocation_token"),verified_at:e.ref("excluded.verified_at"),cap_exp:e.ref("excluded.cap_exp"),group_id:e.ref("excluded.group_id"),hlc:e.ref("excluded.hlc"),updated_at:r.encodeTimestamp(new Date)})).where(e=>e("excluded.hlc",">",e.ref("kubun_p2p_revoked_capabilities.hlc")))).execute(),null==u)return!0;if(e.hlc<=u.hlc)return!1;let i=e.verified_at??null,a=e.cap_exp??null;return u.revoker_did!==e.revoker_did||u.revoked_iat!==e.revoked_iat||u.revocation_token!==e.revocation_token||u.verified_at!==i||u.cap_exp!==a||u.group_id!==e.group_id},getRevocation:async e=>await t.selectFrom("kubun_p2p_revoked_capabilities").selectAll().where("jti","=",e).executeTakeFirst()??null,isRevoked:async e=>null!=await t.selectFrom("kubun_p2p_revoked_capabilities").select("jti").where("jti","=",e).where("verified_at","is not",null).limit(1).executeTakeFirst(),getHeldRevocations:async e=>(await t.selectFrom("kubun_p2p_revoked_capabilities").innerJoin("kubun_p2p_delegation_tokens","kubun_p2p_delegation_tokens.jti","kubun_p2p_revoked_capabilities.jti").select(["kubun_p2p_revoked_capabilities.jti as jti","kubun_p2p_delegation_tokens.grantor as grantor","kubun_p2p_delegation_tokens.audience as audience","kubun_p2p_revoked_capabilities.revoker_did as revoker_did","kubun_p2p_revoked_capabilities.revoked_iat as revoked_iat","kubun_p2p_revoked_capabilities.verified_at as verified_at","kubun_p2p_revoked_capabilities.cap_exp as cap_exp","kubun_p2p_revoked_capabilities.group_id as group_id"]).where("kubun_p2p_revoked_capabilities.verified_at","is not",null).where("kubun_p2p_delegation_tokens.audience","=",e.audience).execute()).map(e=>({jti:e.jti,grantor:e.grantor,audience:e.audience,revoker_did:e.revoker_did,revoked_iat:e.revoked_iat,verified_at:e.verified_at,cap_exp:e.cap_exp,group_id:e.group_id})),async markRevocationVerified(e,u){let i=u.verified_at??Math.floor(Date.now()/1e3);return((await t.updateTable("kubun_p2p_revoked_capabilities").set({verified_at:i,cap_exp:u.cap_exp,updated_at:r.encodeTimestamp(new Date)}).where("jti","=",e).where("verified_at","is",null).executeTakeFirst()).numUpdatedRows??0n)>0n},getPendingRevocationByJti:async e=>await t.selectFrom("kubun_p2p_revoked_capabilities").selectAll().where("jti","=",e).where("verified_at","is",null).executeTakeFirst()??null,deletePendingRevocation:async e=>((await t.deleteFrom("kubun_p2p_revoked_capabilities").where("jti","=",e).where("verified_at","is",null).executeTakeFirst()).numDeletedRows??0n)>0n,async purgeExpiredRevocations(e){let r=Math.floor(Date.now()/1e3)-e.graceSeconds;return Number((await t.deleteFrom("kubun_p2p_revoked_capabilities").where("verified_at","is not",null).where("cap_exp","is not",null).where("cap_exp","<",r).executeTakeFirst()).numDeletedRows??0n)},async purgeDeadPendingRevocations(e){let u=r.encodeTimestamp(new Date(Date.now()-1e3*e.maxAgeSeconds));return Number((await t.deleteFrom("kubun_p2p_revoked_capabilities").where("verified_at","is",null).where("created_at","<",u).executeTakeFirst()).numDeletedRows??0n)},async saveMLSState(e){await t.insertInto("kubun_p2p_group_mls_state").values({group_id:e.group_id,device_id:e.device_id,mls_state:r.encodeBinary(e.mls_state),credential:e.credential,epoch:e.epoch,root_capability:e.root_capability}).onConflict(e=>e.columns(["group_id","device_id"]).doUpdateSet(e=>({mls_state:e.ref("excluded.mls_state"),credential:e.ref("excluded.credential"),epoch:e.ref("excluded.epoch"),root_capability:e.ref("excluded.root_capability"),updated_at:r.encodeTimestamp(new Date)}))).execute()},getMLSState:async(e,r)=>await t.selectFrom("kubun_p2p_group_mls_state").selectAll().where("group_id","=",e).where("device_id","=",r).executeTakeFirst(),async deleteMLSState(e,r){await t.deleteFrom("kubun_p2p_group_mls_state").where("group_id","=",e).where("device_id","=",r).execute()},listMLSStates:async e=>await t.selectFrom("kubun_p2p_group_mls_state").selectAll().where("group_id","=",e).execute(),async deleteGroupMLSStates(e){await t.deleteFrom("kubun_p2p_group_mls_state").where("group_id","=",e).execute()},async addSyncPeer(e){let u=r.encodeJSON(e.allowed_users),i=r.encodeJSON(e.config);await t.insertInto("kubun_p2p_sync_peers").values({...e,allowed_users:u,config:i}).onConflict(t=>t.column("peer_did").doUpdateSet({endpoint:e.endpoint,mode:e.mode,allowed_users:u,priority:e.priority,trust_level:e.trust_level,config:i,updated_at:e.updated_at})).execute()},getSyncPeer:async e=>await t.selectFrom("kubun_p2p_sync_peers").selectAll().where("peer_did","=",e).executeTakeFirst(),listSyncPeers:async()=>await t.selectFrom("kubun_p2p_sync_peers").selectAll().execute(),async updateSyncPeer(e,u){let i={};null!=u.endpoint&&(i.endpoint=u.endpoint),null!=u.mode&&(i.mode=u.mode),null!=u.allowed_users&&(i.allowed_users=r.encodeJSON(u.allowed_users)),null!=u.priority&&(i.priority=u.priority),null!=u.trust_level&&(i.trust_level=u.trust_level),null!=u.config&&(i.config=r.encodeJSON(u.config)),i.updated_at=Date.now(),await t.updateTable("kubun_p2p_sync_peers").set(i).where("peer_did","=",e).execute()},async removeSyncPeer(e){await t.deleteFrom("kubun_p2p_sync_peers").where("peer_did","=",e).execute()},recordBroadcast:async e=>((await t.insertInto("kubun_p2p_broadcast_log").values({doc_id:e.docID,version:e.version,mls_group_id:e.mlsGroupID}).onConflict(e=>e.columns(["doc_id","version","mls_group_id"]).doNothing()).executeTakeFirst()).numInsertedOrUpdatedRows??0n)>0n,hasBroadcast:async e=>null!=await t.selectFrom("kubun_p2p_broadcast_log").select("doc_id").where("doc_id","=",e.docID).where("version","=",e.version).where("mls_group_id","=",e.mlsGroupID).limit(1).executeTakeFirst(),async insertHub(e){await t.insertInto("kubun_p2p_hubs").values({id:e.id,label:e.label,url:e.url,server_did:e.server_did}).execute()},getHub:async e=>await t.selectFrom("kubun_p2p_hubs").selectAll().where("id","=",e).executeTakeFirst(),getHubByURL:async e=>await t.selectFrom("kubun_p2p_hubs").selectAll().where("url","=",e).executeTakeFirst(),async updateHub(e,u){let i={};void 0!==u.label&&(i.label=u.label),void 0!==u.url&&(i.url=u.url),void 0!==u.server_did&&(i.server_did=u.server_did),i.updated_at=r.encodeTimestamp(new Date),await t.updateTable("kubun_p2p_hubs").set(i).where("id","=",e).execute()},async deleteHub(e){await t.deleteFrom("kubun_p2p_hubs").where("id","=",e).execute()},listHubs:async()=>await t.selectFrom("kubun_p2p_hubs").selectAll().execute(),insertGroupHub:async e=>((await t.insertInto("kubun_p2p_group_hubs").values({group_id:e.group_id,hub_id:e.hub_id}).onConflict(e=>e.columns(["group_id","hub_id"]).doNothing()).executeTakeFirst()).numInsertedOrUpdatedRows??0n)>0n,deleteGroupHub:async e=>((await t.deleteFrom("kubun_p2p_group_hubs").where("group_id","=",e.groupID).where("hub_id","=",e.hubID).executeTakeFirst()).numDeletedRows??0n)>0n,listGroupIDsByHubID:async e=>(await t.selectFrom("kubun_p2p_group_hubs").select("group_id").where("hub_id","=",e).execute()).map(e=>e.group_id),listHubsByGroupID:async e=>await t.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","=",e).execute(),listGroupsByHubID:async e=>await t.selectFrom("kubun_p2p_group_hubs").innerJoin("kubun_p2p_groups","kubun_p2p_groups.id","kubun_p2p_group_hubs.group_id").select(["kubun_p2p_groups.id as id","kubun_p2p_groups.name as name","kubun_p2p_groups.description as description","kubun_p2p_groups.created_by as created_by","kubun_p2p_groups.hlc as hlc","kubun_p2p_groups.created_at as created_at","kubun_p2p_groups.updated_at as updated_at"]).where("kubun_p2p_group_hubs.hub_id","=",e).execute(),async deleteGroupHubsByHubID(e){await t.deleteFrom("kubun_p2p_group_hubs").where("hub_id","=",e).execute()}};return i}
1
+ import { normalizeDID } from '@kokuin/token';
2
+ // --- Factory function ---
3
+ export function createP2PStore(db, adapter) {
4
+ const api = {
5
+ // --- Group operations ---
6
+ async createGroup (group) {
7
+ await db.insertInto('kubun_p2p_groups').values({
8
+ id: group.id,
9
+ name: group.name,
10
+ description: group.description,
11
+ created_by: group.created_by,
12
+ hlc: group.hlc
13
+ }).execute();
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
+ }
591
+ };
592
+ return api;
593
+ }
package/lib/definition.js CHANGED
@@ -1 +1,7 @@
1
- import{createP2PStore as i}from"./api.js";import{getP2PMigrations as o}from"./migrations.js";export const p2pStoreDefinition={name:"p2p",migrations:o,createAPI:i};
1
+ import { createP2PStore } from './api.js';
2
+ import { getP2PMigrations } from './migrations.js';
3
+ export const p2pStoreDefinition = {
4
+ name: 'p2p',
5
+ migrations: getP2PMigrations,
6
+ createAPI: createP2PStore
7
+ };