@kubun/plugin-p2p 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/lib/context/group.d.ts +4 -0
  2. package/lib/context/group.js +1 -0
  3. package/lib/context/join.js +1 -1
  4. package/lib/context/types.d.ts +5 -5
  5. package/lib/groups/broadcast-codec.d.ts +3 -0
  6. package/lib/{spaces → groups}/broadcast-service.d.ts +33 -15
  7. package/lib/groups/broadcast-service.js +1 -0
  8. package/lib/{spaces → groups}/broadcast.d.ts +6 -6
  9. package/lib/groups/broadcast.js +1 -0
  10. package/lib/groups/events.d.ts +38 -0
  11. package/lib/{spaces → groups}/events.js +1 -1
  12. package/lib/{spaces → groups}/invite-payload.d.ts +2 -2
  13. package/lib/groups/join-utils.d.ts +19 -0
  14. package/lib/groups/join-utils.js +1 -0
  15. package/lib/{spaces → groups}/manager.d.ts +41 -39
  16. package/lib/groups/manager.js +1 -0
  17. package/lib/groups/mls-group-handle.d.ts +4 -0
  18. package/lib/groups/mls-group-handle.js +1 -0
  19. package/lib/groups/mls-state.d.ts +42 -0
  20. package/lib/groups/mls-state.js +1 -0
  21. package/lib/hub/connection-pool.d.ts +19 -0
  22. package/lib/hub/connection-pool.js +1 -0
  23. package/lib/hub/group-channel.d.ts +51 -0
  24. package/lib/hub/group-channel.js +1 -0
  25. package/lib/hub/receive-handler.d.ts +36 -0
  26. package/lib/hub/receive-handler.js +1 -0
  27. package/lib/hub/relay-manager.d.ts +37 -0
  28. package/lib/hub/relay-manager.js +1 -0
  29. package/lib/hub/send-handler.d.ts +28 -0
  30. package/lib/hub/send-handler.js +1 -0
  31. package/lib/index.d.ts +12 -10
  32. package/lib/index.js +1 -1
  33. package/lib/schema.d.ts +2 -2
  34. package/lib/schema.js +42 -42
  35. package/lib/sync/catalog-scope.js +1 -1
  36. package/lib/sync/peer-registry.js +1 -1
  37. package/lib/types.d.ts +39 -39
  38. package/package.json +17 -15
  39. package/lib/context/space.d.ts +0 -4
  40. package/lib/context/space.js +0 -1
  41. package/lib/spaces/broadcast-codec.d.ts +0 -3
  42. package/lib/spaces/broadcast-service.js +0 -1
  43. package/lib/spaces/broadcast.js +0 -1
  44. package/lib/spaces/events.d.ts +0 -38
  45. package/lib/spaces/group-handle.d.ts +0 -3
  46. package/lib/spaces/group-handle.js +0 -1
  47. package/lib/spaces/join-utils.d.ts +0 -19
  48. package/lib/spaces/join-utils.js +0 -1
  49. package/lib/spaces/manager.js +0 -1
  50. package/lib/spaces/mls-state.d.ts +0 -27
  51. package/lib/spaces/mls-state.js +0 -1
  52. /package/lib/{spaces → groups}/broadcast-codec.js +0 -0
  53. /package/lib/{spaces → groups}/invite-payload.js +0 -0
  54. /package/lib/{spaces → groups}/mls-json.d.ts +0 -0
  55. /package/lib/{spaces → groups}/mls-json.js +0 -0
@@ -0,0 +1,4 @@
1
+ import type { ExecutionContext } from '@kubun/engine';
2
+ import type { GroupRequestContext } from '../types.js';
3
+ import type { ContextDeps } from './types.js';
4
+ export declare function createGroupContext(ctx: ExecutionContext, deps: ContextDeps): GroupRequestContext;
@@ -0,0 +1 @@
1
+ import{HLC as e}from"@kubun/hlc";import{getGraphStore as t}from"@kubun/store-graph";import{getP2PStore as r}from"@kubun/store-p2p";import{decodeFullJoinRequest as a,decodeInvitePayload as i,decodeJoinRequest as o,encodeInvitePayload as c}from"../groups/invite-payload.js";import{createGroupFromJoin as n}from"../groups/join-utils.js";import{restoreMLSGroupHandle as d}from"../groups/mls-group-handle.js";import{checkPeerAccess as l,normalizeCredential as p,toISO as s}from"./types.js";export function createGroupContext(m,u){let g=null,w=null,D=()=>g??=r(u.stores),I=()=>w??=t(u.stores);return{p2pCreateGroup:async e=>{let t=await u.groupManager.createGroup({stores:u.stores,identity:u.identity,name:e.name,description:e.description,createdBy:m.viewerDID}),r=await D(),a=await r.getGroup(t.groupID);if(null==a)throw Error("Failed to create group");let i={id:a.id,name:a.name,description:a.description,createdBy:a.created_by,createdAt:s(a.created_at)};return await u.emitter.emit("groupJoined",i),i},p2pGetGroups:async()=>{let e=await D();return(await e.listGroups()).map(e=>({id:e.id,name:e.name,description:e.description,createdBy:e.created_by,createdAt:s(e.created_at)}))},p2pGetGroup:async e=>{let t=await D(),r=await t.getGroup(e);return null==r?null:{id:r.id,name:r.name,description:r.description,createdBy:r.created_by,createdAt:s(r.created_at)}},p2pAddGroupMember:async(e,t,r)=>{let a=await D();await a.addGroupMember({group_id:e,member_did:t,role:r});let i=await a.getGroupMember(e,t);if(null==i)throw Error("Failed to add group member");let o={groupID:i.group_id,memberDID:i.member_did,role:i.role,createdAt:s(i.created_at)};return await u.emitter.emit("groupMemberJoined",{...o,groupID:o.groupID}),o},p2pGetGroupMembers:async e=>{let t=await D();return(await t.listGroupMembers(e)).map(e=>({groupID:e.group_id,memberDID:e.member_did,role:e.role,createdAt:s(e.created_at)}))},p2pCreateCircle:async e=>{let t=await u.groupManager.createCircle({stores:u.stores,groupID:e.groupID,name:e.name,description:e.description}),r=await D(),a=await r.getCircle(t.circleID);if(null==a)throw Error("Failed to create circle");let i={id:a.id,groupID:a.group_id,name:a.name,description:a.description,createdAt:s(a.created_at)};return await u.emitter.emit("circleCreated",{...i,groupID:i.groupID}),i},p2pGetCircles:async e=>{let t=await D();return(await t.listCirclesByGroup(e)).map(e=>({id:e.id,groupID:e.group_id,name:e.name,description:e.description,createdAt:s(e.created_at)}))},p2pAddCircleMember:async(t,r,a)=>{let i=await D(),o=e.serialize(u.hlc.now());await i.addCircleMember({circle_id:t,member_did:r,role:a,hlc:o});let c=(await i.listCircleMembers(t)).find(e=>e.member_did===r);if(null==c)throw Error("Failed to add circle member");let n={circleID:c.circle_id,memberDID:c.member_did,role:c.role,createdAt:s(c.created_at)};return await u.emitter.emit("circleMemberAdded",{...n,circleID:n.circleID}),n},p2pCreateCatalog:async t=>{let r=await I(),a=u.runtime.getRandomID(),i=e.serialize(u.hlc.now());await r.createCatalog({id:a,owner_did:m.viewerDID,name:t.name,description:t.description??"",filter_criteria:JSON.parse(t.filterCriteria),hlc:i});let o=await r.getCatalog(a);if(null==o)throw Error("Failed to create catalog");return{id:o.id,ownerDID:o.owner_did,name:o.name,description:o.description,filterCriteria:JSON.stringify(o.filter_criteria),createdAt:s(o.created_at)}},p2pGetCatalogs:async e=>{let t=await I();if(null!=e)return(await t.listCatalogs(e)).map(e=>({id:e.id,ownerDID:e.owner_did,name:e.name,description:e.description,filterCriteria:JSON.stringify(e.filter_criteria),createdAt:s(e.created_at)}));let r=await D(),a=new Set((await r.listGroups()).map(e=>e.created_by));return(await Promise.all(Array.from(a).map(e=>t.listCatalogs(e)))).flat().map(e=>({id:e.id,ownerDID:e.owner_did,name:e.name,description:e.description,filterCriteria:JSON.stringify(e.filter_criteria),createdAt:s(e.created_at)}))},p2pInviteToGroup:async(e,t)=>{l(m.viewerDID,u.identity.id,u.autoAcceptPeers);let r=await D(),a=u.identity,i=await r.getMLSState(e,a.id);if(null==i)throw Error(`No MLS state for group ${e}`);let n=await d({mlsState:i.mls_state,credential:p(i.credential),epoch:i.epoch,rootCapability:i.root_capability}),s=o(t),g=await u.groupManager.inviteToGroup({stores:u.stores,groupID:e,identity:a,groupHandle:n,recipientDID:s.did,recipientKeyPackage:s.publicPackage,permission:"member"}),w=await r.getGroup(e),I=c({groupID:e,groupName:w?.name??"",hubURLs:w?.hub_urls??[],invite:g.invite,welcomeMessage:g.welcomeMessage,ratchetTree:g.updatedGroupHandle.state.ratchetTree});return await u.emitter.emit("groupMemberJoined",{groupID:e,memberDID:s.did,role:"member",createdAt:new Date().toISOString()}),{invitePayload:I,groupID:e,groupName:w?.name??""}},p2pJoinGroup:async(e,t)=>{let r=u.identity,o=i(e),c=a(t),d={publicPackage:c.publicPackage,privatePackage:c.privatePackage,ownerDID:c.did};return await u.groupManager.joinGroup({stores:u.stores,identity:r,groupID:o.groupID,groupName:o.groupName,hubURLs:o.hubURLs,invite:o.invite,keyPackageBundle:d,welcomeMessage:o.welcomeMessage,ratchetTree:o.ratchetTree}),await n({stores:u.stores,hlc:u.hlc,emitter:u.emitter,identity:r,invite:o})},p2pLeaveGroup:async e=>{let t=await D(),r=await t.getGroup(e),a=u.identity;return await u.groupManager.leaveGroup({stores:u.stores,groupID:e,identity:a}),null!=r&&await u.emitter.emit("groupLeft",{id:r.id,name:r.name,description:r.description,createdBy:r.created_by,createdAt:s(r.created_at),groupID:e}),{groupID:e}},p2pRemoveGroupMember:async(e,t)=>{let r=await D(),a=u.identity,i=await r.getMLSState(e,a.id);if(null==i)throw Error(`No MLS state for group ${e}`);let o=await d({mlsState:i.mls_state,credential:p(i.credential),epoch:i.epoch,rootCapability:i.root_capability});return await u.groupManager.removeGroupMember({stores:u.stores,groupID:e,groupHandle:o,memberDID:t}),await u.emitter.emit("groupMemberLeft",{groupID:e,memberDID:t,role:"",createdAt:new Date().toISOString()}),{groupID:e}},p2pUpdateGroup:async e=>{await u.groupManager.updateGroup({stores:u.stores,groupID:e.groupID,update:{name:e.name,description:e.description}});let t=await D(),r=await t.getGroup(e.groupID);if(null==r)throw Error(`Group ${e.groupID} not found`);let a={id:r.id,name:r.name,description:r.description,createdBy:r.created_by,createdAt:s(r.created_at)};return await u.emitter.emit("groupDataChanged",{...a,groupID:e.groupID}),a},p2pUpdateCircle:async e=>{await u.groupManager.updateCircle({stores:u.stores,circleID:e.circleID,update:{name:e.name,description:e.description,catalogIDs:e.catalogIDs}});let t=await D(),r=await t.getCircle(e.circleID);if(null==r)throw Error(`Circle ${e.circleID} not found`);let a={id:r.id,groupID:r.group_id,name:r.name,description:r.description,createdAt:s(r.created_at)};return null!=e.catalogIDs?await u.emitter.emit("circleCatalogsChanged",{...a,circleID:e.circleID}):await u.emitter.emit("circleDataChanged",{...a,circleID:e.circleID}),a},p2pUpdateCatalog:async t=>{let r=await I(),a={hlc:e.serialize(u.hlc.now())};null!=t.name&&(a.name=t.name),null!=t.filterCriteria&&(a.filter_criteria=JSON.parse(t.filterCriteria)),await r.updateCatalog(t.catalogID,a);let i=await r.getCatalog(t.catalogID);if(null==i)throw Error(`Catalog ${t.catalogID} not found`);return{id:i.id,ownerDID:i.owner_did,name:i.name,description:i.description,filterCriteria:JSON.stringify(i.filter_criteria),createdAt:s(i.created_at)}},p2pDeleteCatalog:async e=>{let t=await I();return await t.deleteCatalog(e),{deletedID:e}},p2pDeleteCircle:async e=>{let t=await D(),r=await t.getCircle(e);return await u.groupManager.deleteCircle({stores:u.stores,circleID:e}),null!=r&&await u.emitter.emit("circleDeleted",{id:r.id,groupID:r.group_id,name:r.name,description:r.description,createdAt:s(r.created_at)}),{deletedID:e}},p2pRemoveCircleMember:async(e,t)=>(await u.groupManager.removeCircleMember({stores:u.stores,circleID:e,memberDID:t}),await u.emitter.emit("circleMemberRemoved",{circleID:e,memberDID:t,role:"",createdAt:new Date().toISOString()}),{circleID:e,memberDID:t})}}
@@ -1 +1 @@
1
- import{createKeyPackageBundle as e}from"@enkaku/group";import{decodeInvitePayload as t,encodeJoinRequest as i}from"../spaces/invite-payload.js";import{createSpaceFromJoin as a}from"../spaces/join-utils.js";import{checkPeerAccess as r}from"./types.js";export function createJoinContext(n,s){return{p2pPrepareJoinRequest:async()=>{r(n.viewerDID,s.identity.id,s.autoAcceptPeers);let t=s.identity,a=await e(t);return s.pendingJoinRequests.set(n.viewerDID,{publicPackage:a.publicPackage,privatePackage:a.privatePackage,did:t.id}),{joinRequest:i({did:t.id,publicPackage:a.publicPackage})}},p2pCompleteJoin:async e=>{r(n.viewerDID,s.identity.id,s.autoAcceptPeers);let i=s.pendingJoinRequests.get(n.viewerDID);if(null==i)throw Error("No pending join request found — call prepareJoinRequest first");let o=s.identity,p=t(e),c={publicPackage:i.publicPackage,privatePackage:i.privatePackage,ownerDID:o.id};await s.spaceManager.joinSpace({stores:s.stores,identity:o,spaceID:p.spaceID,invite:p.invite,keyPackageBundle:c,welcomeMessage:p.welcomeMessage,ratchetTree:p.ratchetTree});let u=await a({stores:s.stores,hlc:s.hlc,emitter:s.emitter,identity:o,invite:p});return s.pendingJoinRequests.delete(n.viewerDID),u}}}
1
+ import{createKeyPackageBundle as e}from"@enkaku/group";import{decodeInvitePayload as t,encodeJoinRequest as i}from"../groups/invite-payload.js";import{createGroupFromJoin as r}from"../groups/join-utils.js";import{checkPeerAccess as a}from"./types.js";export function createJoinContext(o,n){return{p2pPrepareJoinRequest:async()=>{a(o.viewerDID,n.identity.id,n.autoAcceptPeers);let t=n.identity,r=await e(t);return n.pendingJoinRequests.set(o.viewerDID,{publicPackage:r.publicPackage,privatePackage:r.privatePackage,did:t.id}),{joinRequest:i({did:t.id,publicPackage:r.publicPackage})}},p2pCompleteJoin:async e=>{a(o.viewerDID,n.identity.id,n.autoAcceptPeers);let i=n.pendingJoinRequests.get(o.viewerDID);if(null==i)throw Error("No pending join request found — call prepareJoinRequest first");let p=n.identity,s=t(e),u={publicPackage:i.publicPackage,privatePackage:i.privatePackage,ownerDID:p.id};await n.groupManager.joinGroup({stores:n.stores,identity:p,groupID:s.groupID,groupName:s.groupName,hubURLs:s.hubURLs,invite:s.invite,keyPackageBundle:u,welcomeMessage:s.welcomeMessage,ratchetTree:s.ratchetTree});let c=await r({stores:n.stores,hlc:n.hlc,emitter:n.emitter,identity:p,invite:s});return n.pendingJoinRequests.delete(o.viewerDID),c}}}
@@ -4,8 +4,8 @@ import type { OwnIdentity } from '@enkaku/token';
4
4
  import type { StoreProvider } from '@kubun/db';
5
5
  import type { Adapter } from '@kubun/db-adapter';
6
6
  import type { HLC } from '@kubun/hlc';
7
- import type { SpaceEventEmitter } from '../spaces/events.js';
8
- import type { SpaceManager } from '../spaces/manager.js';
7
+ import type { GroupEventEmitter } from '../groups/events.js';
8
+ import type { GroupManager } from '../groups/manager.js';
9
9
  import type { SyncManager } from '../sync/sync-manager.js';
10
10
  export type PendingJoinRequest = {
11
11
  publicPackage: KeyPackageBundle['publicPackage'];
@@ -14,12 +14,12 @@ export type PendingJoinRequest = {
14
14
  };
15
15
  export type ContextDeps = {
16
16
  identity: OwnIdentity;
17
- spaceManager: SpaceManager;
17
+ groupManager: GroupManager;
18
18
  syncManager: SyncManager;
19
19
  stores: StoreProvider;
20
20
  adapter: Adapter;
21
21
  hlc: HLC;
22
- emitter: SpaceEventEmitter;
22
+ emitter: GroupEventEmitter;
23
23
  pendingJoinRequests: Map<string, PendingJoinRequest>;
24
24
  runtime: Runtime;
25
25
  autoAcceptPeers?: Array<string>;
@@ -28,7 +28,7 @@ export declare function toISO(timestamp: unknown): string;
28
28
  /**
29
29
  * Normalize a credential value from the DB.
30
30
  * Kysely's ParseJSONResultsPlugin may have already parsed the JSON string
31
- * into an object. SerializedGroupState expects a JSON string.
31
+ * into an object. SerializedMLSGroupState expects a JSON string.
32
32
  */
33
33
  export declare function normalizeCredential(value: unknown): string;
34
34
  export declare function checkPeerAccess(viewerDID: string, identityID: string, autoAcceptPeers?: Array<string>): void;
@@ -0,0 +1,3 @@
1
+ import type { GroupBroadcastMessage } from './broadcast.js';
2
+ export declare function serializeBroadcast(message: GroupBroadcastMessage): Uint8Array;
3
+ export declare function deserializeBroadcast(bytes: Uint8Array): GroupBroadcastMessage;
@@ -1,37 +1,55 @@
1
1
  import { EventEmitter } from '@enkaku/event';
2
2
  import type { GraphStoreAPI } from '@kubun/store-graph';
3
- import type { SpaceStoreAPI } from '@kubun/store-space';
4
- import { type SpaceBroadcastMessage } from './broadcast.js';
5
- import { type SerializedGroupState } from './mls-state.js';
3
+ import type { P2PStoreAPI } from '@kubun/store-p2p';
4
+ import { type GroupBroadcastMessage } from './broadcast.js';
5
+ import { type SerializedMLSGroupState } from './mls-state.js';
6
+ /**
7
+ * Thrown when a received broadcast fails to decrypt (MLS restore, decrypt,
8
+ * or deserialize). Callers should typically ack-and-skip these, since the
9
+ * message is unrecoverable and redelivery will not help.
10
+ */
11
+ export declare class DecryptError extends Error {
12
+ readonly cause: unknown;
13
+ constructor(message: string, cause: unknown);
14
+ }
15
+ /**
16
+ * Thrown when a successfully decrypted broadcast fails to apply to the local
17
+ * DB. Callers should typically NOT ack these, so the message can be
18
+ * redelivered after the transient failure clears.
19
+ */
20
+ export declare class ApplyError extends Error {
21
+ readonly cause: unknown;
22
+ constructor(message: string, cause: unknown);
23
+ }
6
24
  export type BroadcastEvent = {
7
- spaceID: string;
8
- message: SpaceBroadcastMessage;
25
+ groupID: string;
26
+ message: GroupBroadcastMessage;
9
27
  applied: boolean;
10
28
  };
11
29
  export type BroadcastServiceEvents = {
12
30
  broadcast: BroadcastEvent;
13
31
  };
14
32
  export type SendBroadcastParams = {
15
- spaceID: string;
16
- message: SpaceBroadcastMessage;
17
- groupState: SerializedGroupState;
33
+ groupID: string;
34
+ message: GroupBroadcastMessage;
35
+ groupState: SerializedMLSGroupState;
18
36
  };
19
37
  export type SendBroadcastResult = {
20
38
  encrypted: Uint8Array;
21
- updatedGroupState: SerializedGroupState;
39
+ updatedGroupState: SerializedMLSGroupState;
22
40
  };
23
41
  export type ReceiveBroadcastParams = {
24
- spaceID: string;
42
+ groupID: string;
25
43
  encrypted: Uint8Array;
26
- groupState: SerializedGroupState;
44
+ groupState: SerializedMLSGroupState;
27
45
  };
28
46
  export type ReceiveBroadcastResult = {
29
- message: SpaceBroadcastMessage;
47
+ message: GroupBroadcastMessage;
30
48
  applied: boolean;
31
- updatedGroupState: SerializedGroupState;
49
+ updatedGroupState: SerializedMLSGroupState;
32
50
  };
33
51
  /**
34
- * BroadcastService handles MLS encrypt/decrypt for space broadcasts
52
+ * BroadcastService handles MLS encrypt/decrypt for group broadcasts
35
53
  * and applies received messages to the local DB.
36
54
  *
37
55
  * It does NOT manage hub transport directly -- callers are responsible
@@ -39,7 +57,7 @@ export type ReceiveBroadcastResult = {
39
57
  * from hub. This keeps the service testable without hub infrastructure.
40
58
  */
41
59
  export type BroadcastServiceParams = {
42
- spaceStore: SpaceStoreAPI;
60
+ p2pStore: P2PStoreAPI;
43
61
  graphStore: GraphStoreAPI;
44
62
  };
45
63
  export declare class BroadcastService extends EventEmitter<BroadcastServiceEvents> {
@@ -0,0 +1 @@
1
+ import{EventEmitter as r}from"@enkaku/event";import{processBroadcast as e}from"./broadcast.js";import{deserializeBroadcast as t,serializeBroadcast as a}from"./broadcast-codec.js";import{restoreMLSGroupHandle as o}from"./mls-group-handle.js";import{replacer as s,reviver as c}from"./mls-json.js";import{serializeMLSGroupState as p}from"./mls-state.js";export class DecryptError extends Error{cause;constructor(r,e){super(r),this.name="DecryptError",this.cause=e}}export class ApplyError extends Error{cause;constructor(r,e){super(r),this.name="ApplyError",this.cause=e}}export class BroadcastService extends r{#r;constructor(r){super(),this.#r={p2pStore:r.p2pStore,graphStore:r.graphStore}}async prepareSend(r){let{message:e,groupState:t}=r,c=a(e),d=await o(t),{message:i}=await d.encrypt(c),n=p(d);return{encrypted:new TextEncoder().encode(JSON.stringify(i,s)),updatedGroupState:n}}async processReceived(r){let a,s,d,{groupID:i,encrypted:n,groupState:m}=r;try{a=await o(m);let r=JSON.parse(new TextDecoder().decode(n),c),e=await a.decrypt(r);s=t(e)}catch(r){throw new DecryptError("Failed to decrypt broadcast",r)}try{d=await e(this.#r,s)}catch(r){throw new ApplyError("Failed to apply broadcast",r)}let l=p(a);return await this.emit("broadcast",{groupID:i,message:s,applied:d}),{message:s,applied:d,updatedGroupState:l}}}
@@ -1,7 +1,7 @@
1
1
  import type { CatalogRecord, CircleMemberRecord, CircleRecord } from '@kubun/protocol';
2
2
  import type { GraphStoreAPI } from '@kubun/store-graph';
3
- import type { SpaceStoreAPI } from '@kubun/store-space';
4
- export type SpaceBroadcastMessage = {
3
+ import type { P2PStoreAPI } from '@kubun/store-p2p';
4
+ export type GroupBroadcastMessage = {
5
5
  type: 'circle:create';
6
6
  circle: CircleRecord;
7
7
  } | {
@@ -42,8 +42,8 @@ export type SpaceBroadcastMessage = {
42
42
  catalogID: string;
43
43
  hlc: string;
44
44
  } | {
45
- type: 'space:update';
46
- spaceID: string;
45
+ type: 'group:update';
46
+ groupID: string;
47
47
  update: {
48
48
  name?: string;
49
49
  description?: string;
@@ -52,7 +52,7 @@ export type SpaceBroadcastMessage = {
52
52
  };
53
53
  };
54
54
  export type ProcessBroadcastParams = {
55
- spaceStore: SpaceStoreAPI;
55
+ p2pStore: P2PStoreAPI;
56
56
  graphStore: GraphStoreAPI;
57
57
  };
58
58
  /**
@@ -61,4 +61,4 @@ export type ProcessBroadcastParams = {
61
61
  *
62
62
  * Returns true if the message was applied, false if it was skipped (stale HLC or missing target).
63
63
  */
64
- export declare function processBroadcast(params: ProcessBroadcastParams, message: SpaceBroadcastMessage): Promise<boolean>;
64
+ export declare function processBroadcast(params: ProcessBroadcastParams, message: GroupBroadcastMessage): Promise<boolean>;
@@ -0,0 +1 @@
1
+ import{HLC as e}from"@kubun/hlc";function a(a,c){return e.compare(e.parse(a),e.parse(c))>0}export async function processBroadcast(e,c){let{p2pStore:t,graphStore:r}=e;switch(c.type){case"circle:create":{let e=await t.getCircle(c.circle.id);if(null!=e){if(!a(c.circle.hlc,e.hlc))return!1;return await t.updateCircle(c.circle.id,{name:c.circle.name,description:c.circle.description,catalog_ids:c.circle.catalogIDs,hlc:c.circle.hlc}),!0}return await t.createCircle({id:c.circle.id,group_id:c.circle.groupID,name:c.circle.name,description:c.circle.description,catalog_ids:c.circle.catalogIDs,hlc:c.circle.hlc}),!0}case"circle:update":{let e=await t.getCircle(c.circleID);if(null==e||!a(c.update.hlc,e.hlc))return!1;return await t.updateCircle(c.circleID,{name:c.update.name,description:c.update.description,catalog_ids:c.update.catalogIDs,hlc:c.update.hlc}),!0}case"circle:delete":{let e=await t.getCircle(c.circleID);if(null==e||!a(c.hlc,e.hlc))return!1;return await t.deleteCircle(c.circleID),!0}case"member:add":return await t.addCircleMember({circle_id:c.member.circleID,member_did:c.member.memberDID,role:c.member.role,hlc:c.member.hlc}),!0;case"member:remove":return await t.removeCircleMember(c.circleID,c.memberDID),!0;case"catalog:create":{let e=await r.getCatalog(c.catalog.id);if(null!=e){if(!a(c.catalog.hlc,e.hlc))return!1;return await r.updateCatalog(c.catalog.id,{name:c.catalog.name,description:c.catalog.description,filter_criteria:c.catalog.filterCriteria,hlc:c.catalog.hlc}),!0}return await r.createCatalog({id:c.catalog.id,owner_did:c.catalog.ownerDID,name:c.catalog.name,description:c.catalog.description,filter_criteria:c.catalog.filterCriteria,hlc:c.catalog.hlc}),!0}case"catalog:update":{let e=await r.getCatalog(c.catalogID);if(null==e||!a(c.update.hlc,e.hlc))return!1;return await r.updateCatalog(c.catalogID,{name:c.update.name,description:c.update.description,filter_criteria:c.update.filterCriteria,hlc:c.update.hlc}),!0}case"catalog:delete":{let e=await r.getCatalog(c.catalogID);if(null==e||!a(c.hlc,e.hlc))return!1;return await r.deleteCatalog(c.catalogID),!0}case"group:update":{let e=await t.getGroup(c.groupID);if(null==e||!a(c.update.hlc,e.hlc))return!1;return await t.updateGroup(c.groupID,{name:c.update.name,description:c.update.description,hub_urls:c.update.hubURLs,hlc:c.update.hlc}),!0}}}
@@ -0,0 +1,38 @@
1
+ import { EventEmitter } from '@enkaku/event';
2
+ import type { CircleData, CircleMemberData, GroupData, GroupMemberData } from '../index.js';
3
+ export type GroupEventMap = {
4
+ groupJoined: GroupData;
5
+ groupLeft: GroupData & {
6
+ groupID: string;
7
+ };
8
+ groupDataChanged: GroupData & {
9
+ groupID: string;
10
+ };
11
+ groupMemberJoined: GroupMemberData & {
12
+ groupID: string;
13
+ };
14
+ groupMemberLeft: GroupMemberData & {
15
+ groupID: string;
16
+ };
17
+ circleCreated: CircleData & {
18
+ groupID: string;
19
+ };
20
+ circleDeleted: CircleData & {
21
+ groupID: string;
22
+ };
23
+ circleDataChanged: CircleData & {
24
+ circleID: string;
25
+ };
26
+ circleMemberAdded: CircleMemberData & {
27
+ circleID: string;
28
+ };
29
+ circleMemberRemoved: CircleMemberData & {
30
+ circleID: string;
31
+ };
32
+ circleCatalogsChanged: CircleData & {
33
+ circleID: string;
34
+ };
35
+ };
36
+ export type GroupEventEmitter = EventEmitter<GroupEventMap>;
37
+ export declare function createGroupEventEmitter(): GroupEventEmitter;
38
+ export declare function createFilteredGenerator<K extends keyof GroupEventMap>(emitter: GroupEventEmitter, eventName: K, filter?: (data: GroupEventMap[K]) => boolean): AsyncGenerator<GroupEventMap[K], void, void>;
@@ -1 +1 @@
1
- import{EventEmitter as e}from"@enkaku/event";import{fromEmitter as r}from"@enkaku/generator";export function createSpaceEventEmitter(){return new e}export function createFilteredGenerator(e,t,n){return r(e,t,{filter:n})}
1
+ import{EventEmitter as e}from"@enkaku/event";import{fromEmitter as r}from"@enkaku/generator";export function createGroupEventEmitter(){return new e}export function createFilteredGenerator(e,t,n){return r(e,t,{filter:n})}
@@ -9,8 +9,8 @@ export type FullJoinRequestPayload = {
9
9
  privatePackage: KeyPackageBundle['privatePackage'];
10
10
  };
11
11
  export type InvitePayload = {
12
- spaceID: string;
13
- spaceName: string;
12
+ groupID: string;
13
+ groupName: string;
14
14
  hubURLs: Array<string>;
15
15
  invite: Invite;
16
16
  welcomeMessage: unknown;
@@ -0,0 +1,19 @@
1
+ import type { OwnIdentity } from '@enkaku/token';
2
+ import type { StoreProvider } from '@kubun/db';
3
+ import { HLC } from '@kubun/hlc';
4
+ import type { GroupData } from '../types.js';
5
+ import type { GroupEventEmitter } from './events.js';
6
+ export type CreateGroupFromJoinParams = {
7
+ stores: StoreProvider;
8
+ hlc: HLC;
9
+ emitter: GroupEventEmitter;
10
+ identity: OwnIdentity;
11
+ invite: {
12
+ groupID: string;
13
+ groupName: string;
14
+ hubURLs: Array<string>;
15
+ };
16
+ };
17
+ export declare function createGroupFromJoin(params: CreateGroupFromJoinParams): Promise<{
18
+ group: GroupData | null;
19
+ }>;
@@ -0,0 +1 @@
1
+ import{HLC as e}from"@kubun/hlc";import{getP2PStore as r}from"@kubun/store-p2p";import{toISO as t}from"../context/types.js";export async function createGroupFromJoin(o){let{stores:a,hlc:i,emitter:u,identity:n,invite:p}=o,c=await r(a);null==await c.getGroup(p.groupID)&&await c.createGroup({id:p.groupID,name:p.groupName,description:"",created_by:n.id,hub_urls:p.hubURLs,hlc:e.serialize(i.now())});let l=await c.getGroup(p.groupID),d=null!=l?{id:l.id,name:l.name,description:l.description,createdBy:l.created_by,createdAt:t(l.created_at)}:null;return null!=d&&await u.emit("groupJoined",d),{group:d}}
@@ -1,12 +1,12 @@
1
1
  import { type GroupHandle, type GroupPermission, type Invite, type KeyPackageBundle } from '@enkaku/group';
2
2
  import type { Identity, OwnIdentity } from '@enkaku/token';
3
3
  import type { StoreProvider } from '@kubun/db';
4
- import type { SpaceBroadcastMessage } from './broadcast.js';
5
- export type SpaceManagerParams = {
4
+ import type { GroupBroadcastMessage } from './broadcast.js';
5
+ export type GroupManagerParams = {
6
6
  identity: Identity;
7
7
  getRandomID: () => string;
8
8
  };
9
- export type CreateSpaceParams = {
9
+ export type CreateGroupParams = {
10
10
  stores: StoreProvider;
11
11
  identity: OwnIdentity;
12
12
  name: string;
@@ -14,52 +14,54 @@ export type CreateSpaceParams = {
14
14
  hubURLs?: Array<string>;
15
15
  createdBy?: string;
16
16
  };
17
- export type CreateSpaceResult = {
18
- spaceID: string;
17
+ export type CreateGroupResult = {
18
+ groupID: string;
19
19
  groupHandle: GroupHandle;
20
20
  };
21
- export type UpdateSpaceParams = {
21
+ export type UpdateGroupParams = {
22
22
  stores: StoreProvider;
23
- spaceID: string;
23
+ groupID: string;
24
24
  update: {
25
25
  name?: string;
26
26
  description?: string;
27
27
  hubURLs?: Array<string>;
28
28
  };
29
29
  };
30
- export type UpdateSpaceResult = {
31
- broadcast: SpaceBroadcastMessage;
30
+ export type UpdateGroupResult = {
31
+ broadcast: GroupBroadcastMessage;
32
32
  };
33
- export type InviteToSpaceParams = {
33
+ export type InviteToGroupParams = {
34
34
  stores: StoreProvider;
35
- spaceID: string;
35
+ groupID: string;
36
36
  identity: OwnIdentity;
37
37
  groupHandle: GroupHandle;
38
38
  recipientDID: string;
39
39
  recipientKeyPackage: KeyPackageBundle['publicPackage'];
40
40
  permission: GroupPermission;
41
41
  };
42
- export type InviteToSpaceResult = {
42
+ export type InviteToGroupResult = {
43
43
  invite: Invite;
44
44
  welcomeMessage: unknown;
45
45
  commitMessage: unknown;
46
46
  updatedGroupHandle: GroupHandle;
47
47
  };
48
- export type JoinSpaceParams = {
48
+ export type JoinGroupParams = {
49
49
  stores: StoreProvider;
50
50
  identity: OwnIdentity;
51
- spaceID: string;
51
+ groupID: string;
52
+ groupName: string;
53
+ hubURLs: Array<string>;
52
54
  invite: Invite;
53
55
  keyPackageBundle: KeyPackageBundle;
54
56
  welcomeMessage: unknown;
55
57
  ratchetTree: unknown;
56
58
  };
57
- export type JoinSpaceResult = {
59
+ export type JoinGroupResult = {
58
60
  groupHandle: GroupHandle;
59
61
  };
60
62
  export type RemoveMemberParams = {
61
63
  stores: StoreProvider;
62
- spaceID: string;
64
+ groupID: string;
63
65
  groupHandle: GroupHandle;
64
66
  leafIndex: number;
65
67
  memberDID: string;
@@ -68,32 +70,32 @@ export type RemoveMemberResult = {
68
70
  commitMessage: unknown;
69
71
  updatedGroupHandle: GroupHandle;
70
72
  };
71
- export type RemoveSpaceMemberParams = {
73
+ export type RemoveGroupMemberParams = {
72
74
  stores: StoreProvider;
73
- spaceID: string;
75
+ groupID: string;
74
76
  groupHandle: GroupHandle;
75
77
  memberDID: string;
76
78
  };
77
- export type RemoveSpaceMemberResult = {
79
+ export type RemoveGroupMemberResult = {
78
80
  commitMessage: unknown;
79
81
  updatedGroupHandle: GroupHandle;
80
82
  };
81
- export type LeaveSpaceParams = {
83
+ export type LeaveGroupParams = {
82
84
  stores: StoreProvider;
83
- spaceID: string;
85
+ groupID: string;
84
86
  identity: OwnIdentity;
85
87
  };
86
- export type LeaveSpaceResult = undefined;
88
+ export type LeaveGroupResult = undefined;
87
89
  export type CreateCircleParams = {
88
90
  stores: StoreProvider;
89
- spaceID: string;
91
+ groupID: string;
90
92
  name: string;
91
93
  description?: string;
92
94
  catalogIDs?: Array<string>;
93
95
  };
94
96
  export type CreateCircleResult = {
95
97
  circleID: string;
96
- broadcast: SpaceBroadcastMessage;
98
+ broadcast: GroupBroadcastMessage;
97
99
  };
98
100
  export type UpdateCircleParams = {
99
101
  stores: StoreProvider;
@@ -105,25 +107,25 @@ export type UpdateCircleParams = {
105
107
  };
106
108
  };
107
109
  export type UpdateCircleResult = {
108
- broadcast: SpaceBroadcastMessage;
109
- broadcasts: Array<SpaceBroadcastMessage>;
110
+ broadcast: GroupBroadcastMessage;
111
+ broadcasts: Array<GroupBroadcastMessage>;
110
112
  };
111
113
  export type DeleteCircleParams = {
112
114
  stores: StoreProvider;
113
115
  circleID: string;
114
116
  };
115
117
  export type DeleteCircleResult = {
116
- broadcast: SpaceBroadcastMessage;
118
+ broadcast: GroupBroadcastMessage;
117
119
  };
118
120
  export type AddCircleMemberParams = {
119
121
  stores: StoreProvider;
120
- spaceID: string;
122
+ groupID: string;
121
123
  circleID: string;
122
124
  memberDID: string;
123
125
  role: 'admin' | 'member';
124
126
  };
125
127
  export type AddCircleMemberResult = {
126
- broadcast: SpaceBroadcastMessage;
128
+ broadcast: GroupBroadcastMessage;
127
129
  };
128
130
  export type RemoveCircleMemberParams = {
129
131
  stores: StoreProvider;
@@ -131,19 +133,19 @@ export type RemoveCircleMemberParams = {
131
133
  memberDID: string;
132
134
  };
133
135
  export type RemoveCircleMemberResult = {
134
- broadcast: SpaceBroadcastMessage;
136
+ broadcast: GroupBroadcastMessage;
135
137
  };
136
- export declare class SpaceManager {
138
+ export declare class GroupManager {
137
139
  #private;
138
- constructor(params: SpaceManagerParams);
139
- createSpace(params: CreateSpaceParams): Promise<CreateSpaceResult>;
140
- updateSpace(params: UpdateSpaceParams): Promise<UpdateSpaceResult>;
141
- inviteToSpace(params: InviteToSpaceParams): Promise<InviteToSpaceResult>;
142
- joinSpace(params: JoinSpaceParams): Promise<JoinSpaceResult>;
140
+ constructor(params: GroupManagerParams);
141
+ createGroup(params: CreateGroupParams): Promise<CreateGroupResult>;
142
+ updateGroup(params: UpdateGroupParams): Promise<UpdateGroupResult>;
143
+ inviteToGroup(params: InviteToGroupParams): Promise<InviteToGroupResult>;
144
+ joinGroup(params: JoinGroupParams): Promise<JoinGroupResult>;
143
145
  removeMember(params: RemoveMemberParams): Promise<RemoveMemberResult>;
144
- removeSpaceMember(params: RemoveSpaceMemberParams): Promise<RemoveSpaceMemberResult>;
145
- leaveSpace(params: LeaveSpaceParams): Promise<LeaveSpaceResult>;
146
- loadSpace(stores: StoreProvider, spaceID: string): Promise<{
146
+ removeGroupMember(params: RemoveGroupMemberParams): Promise<RemoveGroupMemberResult>;
147
+ leaveGroup(params: LeaveGroupParams): Promise<LeaveGroupResult>;
148
+ loadGroup(stores: StoreProvider, groupID: string): Promise<{
147
149
  epoch: number;
148
150
  credential: string;
149
151
  } | undefined>;
@@ -0,0 +1 @@
1
+ import{commitInvite as e,createGroup as t,createInvite as r,processWelcome as a,removeMember as i}from"@enkaku/group";import{HLC as o}from"@kubun/hlc";import{getGraphStore as s}from"@kubun/store-graph";import{getP2PStore as c}from"@kubun/store-p2p";import{serializeMLSGroupState as d}from"./mls-state.js";export class GroupManager{#e;#t;#r;constructor(e){this.#e=e.identity.id,this.#t=e.getRandomID,this.#r=new o({nodeID:e.identity.id})}async #a(e,t,r){let a=d(r),i=await c(e);await i.saveMLSState({group_id:t,device_id:this.#e,mls_state:a.mlsState,credential:a.credential,epoch:a.epoch,root_capability:a.rootCapability})}async createGroup(e){let r=this.#t(),{group:a}=await t(e.identity,r),i=await c(e.stores);return await i.createGroup({id:r,name:e.name,description:e.description??"",created_by:e.createdBy??e.identity.id,hub_urls:e.hubURLs??[],hlc:o.serialize(this.#r.now())}),await this.#a(e.stores,r,a),await i.addGroupMember({group_id:r,member_did:e.identity.id,role:"admin"}),{groupID:r,groupHandle:a}}async updateGroup(e){let t=o.serialize(this.#r.now()),r=await c(e.stores);return await r.updateGroup(e.groupID,{name:e.update.name,description:e.update.description,hub_urls:e.update.hubURLs,hlc:t}),{broadcast:{type:"group:update",groupID:e.groupID,update:{...e.update,hlc:t}}}}async inviteToGroup(t){let{invite:a}=await r({group:t.groupHandle,identity:t.identity,recipientDID:t.recipientDID,permission:t.permission}),{commitMessage:i,welcomeMessage:o,newGroup:s}=await e(t.groupHandle,t.recipientKeyPackage);await this.#a(t.stores,t.groupID,s);let d=await c(t.stores);return await d.addGroupMember({group_id:t.groupID,member_did:t.recipientDID,role:"admin"===t.permission?"admin":"member"}),{invite:a,welcomeMessage:o,commitMessage:i,updatedGroupHandle:s}}async joinGroup(e){let{group:t}=await a({identity:e.identity,invite:e.invite,welcome:e.welcomeMessage,keyPackageBundle:e.keyPackageBundle,ratchetTree:e.ratchetTree});await this.#a(e.stores,e.groupID,t);let r=await c(e.stores);return null==await r.getGroup(e.groupID)&&await r.createGroup({id:e.groupID,name:e.groupName,description:"",created_by:e.identity.id,hub_urls:e.hubURLs,hlc:o.serialize(this.#r.now())}),await r.addGroupMember({group_id:e.groupID,member_did:e.identity.id,role:"member"}),{groupHandle:t}}async removeMember(e){let{commitMessage:t,newGroup:r}=await i(e.groupHandle,e.leafIndex);await this.#a(e.stores,e.groupID,r);let a=await c(e.stores);return await a.removeGroupMember(e.groupID,e.memberDID),{commitMessage:t,updatedGroupHandle:r}}async removeGroupMember(e){let t=e.groupHandle.findMemberLeafIndex(e.memberDID);if(null==t)throw Error(`Member ${e.memberDID} not found in MLS group`);let{commitMessage:r,newGroup:a}=await i(e.groupHandle,t);await this.#a(e.stores,e.groupID,a);let o=await c(e.stores);return await o.removeGroupMember(e.groupID,e.memberDID),{commitMessage:r,updatedGroupHandle:a}}async leaveGroup(e){let t=await c(e.stores);await t.deleteMLSState(e.groupID,this.#e),await t.removeGroupMember(e.groupID,e.identity.id)}async loadGroup(e,t){let r=await c(e),a=await r.getMLSState(t,this.#e);if(null!=a)return{epoch:a.epoch,credential:a.credential}}async createCircle(e){let t=this.#t(),r=o.serialize(this.#r.now()),a=e.catalogIDs??[],i=await c(e.stores);return await i.createCircle({id:t,group_id:e.groupID,name:e.name,description:e.description??"",catalog_ids:a,hlc:r}),{circleID:t,broadcast:{type:"circle:create",circle:{id:t,groupID:e.groupID,name:e.name,description:e.description??"",catalogIDs:a,hlc:r}}}}async updateCircle(e){let t=o.serialize(this.#r.now()),r=[],a=await c(e.stores);if(null!=e.update.catalogIDs){let t=await a.getCircle(e.circleID),i=new Set(null!=t?t.catalog_ids:[]),o=e.update.catalogIDs.filter(e=>!i.has(e)),c=await s(e.stores);for(let e of(await Promise.all(o.map(e=>c.getCatalog(e)))))null!=e&&r.push({type:"catalog:create",catalog:{id:e.id,ownerDID:e.owner_did,name:e.name,description:e.description,filterCriteria:e.filter_criteria,hlc:e.hlc}})}await a.updateCircle(e.circleID,{name:e.update.name,description:e.update.description,catalog_ids:e.update.catalogIDs,hlc:t});let i={type:"circle:update",circleID:e.circleID,update:{...e.update,hlc:t}};return{broadcast:i,broadcasts:[i,...r]}}async deleteCircle(e){let t=o.serialize(this.#r.now()),r=await c(e.stores);return await r.deleteCircle(e.circleID),{broadcast:{type:"circle:delete",circleID:e.circleID,hlc:t}}}async addCircleMember(e){let t=await c(e.stores);if(!await t.isGroupMember(e.groupID,e.memberDID))throw Error(`${e.memberDID} is not a member of group ${e.groupID}`);let r=o.serialize(this.#r.now());return await t.addCircleMember({circle_id:e.circleID,member_did:e.memberDID,role:e.role,hlc:r}),{broadcast:{type:"member:add",member:{circleID:e.circleID,memberDID:e.memberDID,role:e.role,hlc:r}}}}async removeCircleMember(e){let t=o.serialize(this.#r.now()),r=await c(e.stores);return await r.removeCircleMember(e.circleID,e.memberDID),{broadcast:{type:"member:remove",circleID:e.circleID,memberDID:e.memberDID,hlc:t}}}}
@@ -0,0 +1,4 @@
1
+ import { type GroupHandle } from '@enkaku/group';
2
+ import { type SerializedMLSGroupState } from './mls-state.js';
3
+ export type MLSGroupHandle = GroupHandle;
4
+ export declare function restoreMLSGroupHandle(serialized: SerializedMLSGroupState): Promise<MLSGroupHandle>;
@@ -0,0 +1 @@
1
+ import{restoreGroup as r}from"@enkaku/group";import{deserializeMLSGroupState as o}from"./mls-state.js";export async function restoreMLSGroupHandle(t){let{state:e,credential:n,rootCapability:m}=o(t);return r({state:e,credential:n,rootCapability:m})}
@@ -0,0 +1,42 @@
1
+ import { type ClientState, type GroupHandle, type MemberCredential } from '@enkaku/group';
2
+ import type { GroupMLSState, InsertGroupMLSState } from '@kubun/store-p2p';
3
+ /**
4
+ * Serialized form of a GroupHandle for DB persistence.
5
+ * The MLS ClientState is opaque binary; the credential is JSON.
6
+ */
7
+ export type SerializedMLSGroupState = {
8
+ mlsState: Uint8Array;
9
+ credential: string;
10
+ epoch: number;
11
+ rootCapability: string;
12
+ };
13
+ /**
14
+ * Serialize a GroupHandle's state for persistence.
15
+ * The implementer must verify the serialization approach works
16
+ * with the installed version of ts-mls.
17
+ */
18
+ export declare function serializeMLSGroupState(group: GroupHandle): SerializedMLSGroupState;
19
+ export type MLSGroupState = {
20
+ state: ClientState;
21
+ credential: MemberCredential;
22
+ rootCapability: string;
23
+ };
24
+ /**
25
+ * Deserialize persisted state back into GroupHandle constructor params.
26
+ * Returns the params needed to reconstruct a GroupHandle.
27
+ */
28
+ export declare function deserializeMLSGroupState(serialized: SerializedMLSGroupState): MLSGroupState;
29
+ /**
30
+ * Map a persisted `GroupMLSState` DB row to the `SerializedMLSGroupState` shape
31
+ * expected by `BroadcastService.prepareSend` / `processReceived`.
32
+ *
33
+ * Credential is stored as TEXT but Kysely's `ParseJSONResultsPlugin` may have
34
+ * already parsed it into an object on read. `SerializedMLSGroupState.credential`
35
+ * is a JSON string, so re-stringify when needed.
36
+ */
37
+ export declare function fromMLSStateRow(row: GroupMLSState): SerializedMLSGroupState;
38
+ /**
39
+ * Map a `SerializedMLSGroupState` plus scope (group + device) to an
40
+ * `InsertGroupMLSState` row suitable for `P2PStoreAPI.saveMLSState`.
41
+ */
42
+ export declare function toMLSStateInsert(serialized: SerializedMLSGroupState, groupID: string, deviceID: string): InsertGroupMLSState;
@@ -0,0 +1 @@
1
+ import{decodeClientState as t,encodeClientState as e}from"@enkaku/group";import{normalizeCredential as o}from"../context/types.js";export function serializeMLSGroupState(t){return{mlsState:e(t.state),credential:JSON.stringify(t.credential),epoch:Number(t.epoch),rootCapability:t.rootCapability}}export function deserializeMLSGroupState(e){let o=t(e.mlsState);if(null==o)throw Error("Could not decode ClientState");return{state:o,credential:JSON.parse(e.credential),rootCapability:e.rootCapability}}export function fromMLSStateRow(t){return{mlsState:t.mls_state,credential:o(t.credential),epoch:t.epoch,rootCapability:t.root_capability}}export function toMLSStateInsert(t,e,o){return{group_id:e,device_id:o,mls_state:t.mlsState,credential:t.credential,epoch:t.epoch,root_capability:t.rootCapability}}
@@ -0,0 +1,19 @@
1
+ import type { Client } from '@enkaku/client';
2
+ import type { HubProtocol } from '@enkaku/hub-protocol';
3
+ import type { Logger } from '@kubun/logger';
4
+ import type { CreateHubClient } from './relay-manager.js';
5
+ export type HubClientRef = {
6
+ client: Client<HubProtocol>;
7
+ refID: string;
8
+ };
9
+ export type HubConnectionPoolParams = {
10
+ createHubClient: CreateHubClient;
11
+ logger?: Logger;
12
+ };
13
+ export declare class HubConnectionPool {
14
+ #private;
15
+ constructor(params: HubConnectionPoolParams);
16
+ acquire(hubURL: string): Promise<HubClientRef>;
17
+ release(hubURL: string, refID: string): Promise<void>;
18
+ disposeAll(): Promise<void>;
19
+ }
@@ -0,0 +1 @@
1
+ export class HubConnectionPool{#e;#t;#i=new Map;#s=0;constructor(e){this.#e=e.createHubClient,this.#t=e.logger}async acquire(e){let t=this.#i.get(e);if(null!=t&&null!=t.disposing&&(await t.disposing,this.#i.get(e)===t&&this.#i.delete(e),t=void 0),null==t){let i=await this.#e(e),s={client:i,refs:new Set};t=s,this.#i.set(e,s),i.disposed.then(()=>{this.#i.get(e)===s&&(this.#t?.debug("hub client disposed externally, evicting pool entry",{hubURL:e,outstandingRefs:[...s.refs]}),this.#i.delete(e))}).catch(()=>{})}let i=String(++this.#s);return t.refs.add(i),{client:t.client,refID:i}}async release(e,t){let i=this.#i.get(e);if(null==i)return void this.#t?.debug("HubConnectionPool.release: unknown hubURL",{hubURL:e,refID:t});if(!i.refs.has(t))return void this.#t?.debug("HubConnectionPool.release: unknown refID",{hubURL:e,refID:t});if(i.refs.delete(t),i.refs.size>0)return;let s=i.client.dispose();i.disposing=s;try{await s}finally{this.#i.get(e)===i&&this.#i.delete(e)}}async disposeAll(){let e=Array.from(this.#i.entries());await Promise.all(e.map(async([e,t])=>{t.refs.size>0&&this.#t?.warn("Disposing hub client with outstanding refs",{hubURL:e,refs:Array.from(t.refs)}),await t.client.dispose()})),this.#i.clear()}}
@@ -0,0 +1,51 @@
1
+ import type { Logger } from '@kubun/logger';
2
+ import type { P2PStoreAPI } from '@kubun/store-p2p';
3
+ import type { GroupBroadcastMessage } from '../groups/broadcast.js';
4
+ import type { BroadcastService } from '../groups/broadcast-service.js';
5
+ import type { HubConnectionPool } from './connection-pool.js';
6
+ export type GroupChannelParams = {
7
+ groupID: string;
8
+ hubURL: string;
9
+ deviceID: string;
10
+ pool: HubConnectionPool;
11
+ broadcastService: BroadcastService;
12
+ p2pStore: P2PStoreAPI;
13
+ logger?: Logger;
14
+ ackFlushMs: number;
15
+ ackFlushMax: number;
16
+ backoffBaseMs: number;
17
+ backoffMaxMs: number;
18
+ backoffJitter: number;
19
+ };
20
+ export type Mutex = {
21
+ run<T>(fn: () => Promise<T>): Promise<T>;
22
+ };
23
+ export declare function createMutex(): Mutex;
24
+ export type AckBatchParams = {
25
+ flushMs: number;
26
+ flushMax: number;
27
+ send: (ids: Array<string>) => Promise<void>;
28
+ logger?: Logger;
29
+ };
30
+ export declare class AckBatch {
31
+ #private;
32
+ constructor(params: AckBatchParams);
33
+ add(id: string): void;
34
+ /**
35
+ * Drain the pending id set and send it via the configured sender. Never
36
+ * rejects: send errors are logged and the ids are dropped silently (the hub
37
+ * will redeliver unacknowledged messages, so the next receive will produce a
38
+ * fresh ack).
39
+ */
40
+ flush(): Promise<void>;
41
+ dispose(): void;
42
+ }
43
+ export declare function computeBackoff(attempt: number, base: number, max: number, jitter: number): number;
44
+ export declare function abortableSleep(ms: number, signal: AbortSignal): Promise<void>;
45
+ export declare class GroupChannel {
46
+ #private;
47
+ constructor(params: GroupChannelParams);
48
+ open(): Promise<void>;
49
+ close(): Promise<void>;
50
+ broadcast(message: GroupBroadcastMessage): Promise<void>;
51
+ }
@@ -0,0 +1 @@
1
+ import{handleReceivedMessage as e}from"./receive-handler.js";import{handleSendBroadcast as t}from"./send-handler.js";export function createMutex(){let e=Promise.resolve();return{run(t){let r=e.then(()=>t());return e=r.catch(()=>{}),r}}}export class AckBatch{#e;#t;#r;#i;#s=new Set;#o=null;constructor(e){this.#e=e.flushMs,this.#t=e.flushMax,this.#r=e.send,this.#i=e.logger}add(e){let t=0===this.#s.size;if(this.#s.add(e),this.#s.size>=this.#t){null!==this.#o&&(clearTimeout(this.#o),this.#o=null),this.flush();return}t&&null===this.#o&&(this.#o=setTimeout(()=>{this.#o=null,this.flush()},this.#e))}async flush(){if(null!==this.#o&&(clearTimeout(this.#o),this.#o=null),0===this.#s.size)return;let e=Array.from(this.#s);this.#s.clear();try{await this.#r(e)}catch(t){this.#i?.error("ack batch send failed",{error:t,count:e.length})}}dispose(){null!==this.#o&&(clearTimeout(this.#o),this.#o=null),this.#s.clear()}}export function computeBackoff(e,t,r,i){let s=Math.min(t*2**e*(1+i*(2*Math.random()-1)),r);return s<0?0:s}export function abortableSleep(e,t){return new Promise((r,i)=>{if(t.aborted)return void i(t.reason??Error("aborted"));let s=setTimeout(()=>{t.removeEventListener("abort",o),r()},e),o=()=>{clearTimeout(s),t.removeEventListener("abort",o),i(t.reason??Error("aborted"))};t.addEventListener("abort",o)})}let r=Symbol("disposed");export class GroupChannel{#a;#h;#l;#n;#c;#u;#i;#p;#g;#d;#f;#b;#I;#D=new AbortController;#m=null;#k=null;#v=null;#M=null;#y=null;#w=0;#x=!1;#C=!1;constructor(e){this.#a=e.groupID,this.#h=e.hubURL,this.#l=e.deviceID,this.#n=e.pool,this.#c=e.broadcastService,this.#u=e.p2pStore,this.#i=e.logger,this.#p=e.ackFlushMs,this.#g=e.ackFlushMax,this.#d=e.backoffBaseMs,this.#f=e.backoffMaxMs,this.#b=e.backoffJitter,this.#I=createMutex()}async open(){if(this.#x)throw Error("GroupChannel already opened");try{await this.#S(),this.#x=!0}catch(e){if(null!=this.#k){try{await this.#n.release(this.#h,this.#k)}catch(e){this.#i?.debug("pool release after open failure failed",{groupID:this.#a,error:e})}this.#k=null}throw this.#m=null,e}}async #S(){if(null!=this.#k){let e=this.#k;this.#k=null,this.#m=null;try{await this.#n.release(this.#h,e)}catch(e){this.#i?.debug("pool release before reconnect failed",{groupID:this.#a,error:e})}}let{client:e,refID:t}=await this.#n.acquire(this.#h);this.#m=e,this.#k=t;let r=await this.#u.getMLSState(this.#a,this.#l);if(null==r)throw Error(`MLS state missing for group ${this.#a}; cannot join hub group`);await e.request("hub/group/join",{param:{groupID:this.#a,credential:r.credential}});let i=e.createChannel("hub/receive",{param:{groupIDs:[this.#a]},signal:this.#D.signal});this.#v=i,i.catch(()=>{}),this.#M=new AckBatch({flushMs:this.#p,flushMax:this.#g,send:async e=>{await i.send({ack:e})},logger:this.#i}),this.#y=this.#L(e).catch(e=>{this.#i?.debug("receive task tail error",{groupID:this.#a,error:e})}),this.#C=!0}async #L(t){let i=this.#v;if(null==i)return;let s=i.readable.getReader();try{for(;!this.#D.signal.aborted;){let i=s.read(),o=t.disposed.then(()=>r),a=await Promise.race([i,o]);if("symbol"==typeof a){this.#i?.debug("receive loop observed hub client disposal",{groupID:this.#a});break}let{done:h,value:l}=a;if(h)break;(await this.#I.run(()=>e({groupID:this.#a,deviceID:this.#l,message:l,p2pStore:this.#u,broadcastService:this.#c,logger:this.#i}))).ack&&this.#M?.add(l.sequenceID),this.#w=0}}catch(e){this.#i?.warn("hub receive loop error",{groupID:this.#a,error:e})}finally{try{s.releaseLock()}catch{}}this.#C=!1,this.#D.signal.aborted||this.#B()}#B(){this.#C=!1,this.#y=this.#R().catch(e=>{this.#i?.debug("reconnect task tail error",{groupID:this.#a,error:e})})}async #R(){for(;!this.#D.signal.aborted;){this.#C=!1;let e=computeBackoff(this.#w++,this.#d,this.#f,this.#b);this.#i?.debug("reconnect scheduled",{groupID:this.#a,delay:e,attempt:this.#w});try{await abortableSleep(e,this.#D.signal)}catch{return}try{this.#M?.dispose(),this.#M=null,this.#v=null,await this.#S(),this.#w=0;return}catch(e){this.#i?.warn("reconnect attempt failed",{groupID:this.#a,attempt:this.#w,error:e})}}}async close(){if(!this.#x)return;this.#C=!1;let e=this.#M;this.#M=null;try{await e?.flush()}catch(e){this.#i?.debug("ack flush during close failed",{groupID:this.#a,error:e})}e?.dispose();try{await this.#m?.request("hub/group/leave",{param:{groupID:this.#a}})}catch(e){this.#i?.warn("hub/group/leave failed",{groupID:this.#a,error:e})}try{this.#v?.close()}catch{}if(!this.#D.signal.aborted)try{this.#D.abort()}catch{}try{await this.#y}catch{}if(null!=this.#k){try{await this.#n.release(this.#h,this.#k)}catch(e){this.#i?.debug("pool release during close failed",{groupID:this.#a,error:e})}this.#k=null}this.#m=null,this.#v=null,this.#y=null,this.#x=!1}broadcast(e){return this.#x&&this.#C&&null!=this.#m?this.#I.run(async()=>{if(!this.#x||!this.#C||null==this.#m)throw Error(`GroupChannel for ${this.#a} is not ready for broadcast`);let r=this.#m;return t({groupID:this.#a,deviceID:this.#l,message:e,p2pStore:this.#u,broadcastService:this.#c,hubClient:r,logger:this.#i})}):Promise.reject(Error(`GroupChannel for ${this.#a} is not ready for broadcast`))}}