@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.
- package/lib/context/group.d.ts +4 -0
- package/lib/context/group.js +1 -0
- package/lib/context/join.js +1 -1
- package/lib/context/types.d.ts +5 -5
- package/lib/groups/broadcast-codec.d.ts +3 -0
- package/lib/{spaces → groups}/broadcast-service.d.ts +33 -15
- package/lib/groups/broadcast-service.js +1 -0
- package/lib/{spaces → groups}/broadcast.d.ts +6 -6
- package/lib/groups/broadcast.js +1 -0
- package/lib/groups/events.d.ts +38 -0
- package/lib/{spaces → groups}/events.js +1 -1
- package/lib/{spaces → groups}/invite-payload.d.ts +2 -2
- package/lib/groups/join-utils.d.ts +19 -0
- package/lib/groups/join-utils.js +1 -0
- package/lib/{spaces → groups}/manager.d.ts +41 -39
- package/lib/groups/manager.js +1 -0
- package/lib/groups/mls-group-handle.d.ts +4 -0
- package/lib/groups/mls-group-handle.js +1 -0
- package/lib/groups/mls-state.d.ts +42 -0
- package/lib/groups/mls-state.js +1 -0
- package/lib/hub/connection-pool.d.ts +19 -0
- package/lib/hub/connection-pool.js +1 -0
- package/lib/hub/group-channel.d.ts +51 -0
- package/lib/hub/group-channel.js +1 -0
- package/lib/hub/receive-handler.d.ts +36 -0
- package/lib/hub/receive-handler.js +1 -0
- package/lib/hub/relay-manager.d.ts +37 -0
- package/lib/hub/relay-manager.js +1 -0
- package/lib/hub/send-handler.d.ts +28 -0
- package/lib/hub/send-handler.js +1 -0
- package/lib/index.d.ts +12 -10
- package/lib/index.js +1 -1
- package/lib/schema.d.ts +2 -2
- package/lib/schema.js +42 -42
- package/lib/sync/catalog-scope.js +1 -1
- package/lib/sync/peer-registry.js +1 -1
- package/lib/types.d.ts +39 -39
- package/package.json +17 -15
- package/lib/context/space.d.ts +0 -4
- package/lib/context/space.js +0 -1
- package/lib/spaces/broadcast-codec.d.ts +0 -3
- package/lib/spaces/broadcast-service.js +0 -1
- package/lib/spaces/broadcast.js +0 -1
- package/lib/spaces/events.d.ts +0 -38
- package/lib/spaces/group-handle.d.ts +0 -3
- package/lib/spaces/group-handle.js +0 -1
- package/lib/spaces/join-utils.d.ts +0 -19
- package/lib/spaces/join-utils.js +0 -1
- package/lib/spaces/manager.js +0 -1
- package/lib/spaces/mls-state.d.ts +0 -27
- package/lib/spaces/mls-state.js +0 -1
- /package/lib/{spaces → groups}/broadcast-codec.js +0 -0
- /package/lib/{spaces → groups}/invite-payload.js +0 -0
- /package/lib/{spaces → groups}/mls-json.d.ts +0 -0
- /package/lib/{spaces → groups}/mls-json.js +0 -0
|
@@ -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})}}
|
package/lib/context/join.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createKeyPackageBundle as e}from"@enkaku/group";import{decodeInvitePayload as t,encodeJoinRequest as i}from"../
|
|
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}}}
|
package/lib/context/types.d.ts
CHANGED
|
@@ -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 {
|
|
8
|
-
import type {
|
|
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
|
-
|
|
17
|
+
groupManager: GroupManager;
|
|
18
18
|
syncManager: SyncManager;
|
|
19
19
|
stores: StoreProvider;
|
|
20
20
|
adapter: Adapter;
|
|
21
21
|
hlc: HLC;
|
|
22
|
-
emitter:
|
|
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.
|
|
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;
|
|
@@ -1,37 +1,55 @@
|
|
|
1
1
|
import { EventEmitter } from '@enkaku/event';
|
|
2
2
|
import type { GraphStoreAPI } from '@kubun/store-graph';
|
|
3
|
-
import type {
|
|
4
|
-
import { type
|
|
5
|
-
import { type
|
|
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
|
-
|
|
8
|
-
message:
|
|
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
|
-
|
|
16
|
-
message:
|
|
17
|
-
groupState:
|
|
33
|
+
groupID: string;
|
|
34
|
+
message: GroupBroadcastMessage;
|
|
35
|
+
groupState: SerializedMLSGroupState;
|
|
18
36
|
};
|
|
19
37
|
export type SendBroadcastResult = {
|
|
20
38
|
encrypted: Uint8Array;
|
|
21
|
-
updatedGroupState:
|
|
39
|
+
updatedGroupState: SerializedMLSGroupState;
|
|
22
40
|
};
|
|
23
41
|
export type ReceiveBroadcastParams = {
|
|
24
|
-
|
|
42
|
+
groupID: string;
|
|
25
43
|
encrypted: Uint8Array;
|
|
26
|
-
groupState:
|
|
44
|
+
groupState: SerializedMLSGroupState;
|
|
27
45
|
};
|
|
28
46
|
export type ReceiveBroadcastResult = {
|
|
29
|
-
message:
|
|
47
|
+
message: GroupBroadcastMessage;
|
|
30
48
|
applied: boolean;
|
|
31
|
-
updatedGroupState:
|
|
49
|
+
updatedGroupState: SerializedMLSGroupState;
|
|
32
50
|
};
|
|
33
51
|
/**
|
|
34
|
-
* BroadcastService handles MLS encrypt/decrypt for
|
|
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
|
-
|
|
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 {
|
|
4
|
-
export type
|
|
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: '
|
|
46
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
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 {
|
|
5
|
-
export type
|
|
4
|
+
import type { GroupBroadcastMessage } from './broadcast.js';
|
|
5
|
+
export type GroupManagerParams = {
|
|
6
6
|
identity: Identity;
|
|
7
7
|
getRandomID: () => string;
|
|
8
8
|
};
|
|
9
|
-
export type
|
|
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
|
|
18
|
-
|
|
17
|
+
export type CreateGroupResult = {
|
|
18
|
+
groupID: string;
|
|
19
19
|
groupHandle: GroupHandle;
|
|
20
20
|
};
|
|
21
|
-
export type
|
|
21
|
+
export type UpdateGroupParams = {
|
|
22
22
|
stores: StoreProvider;
|
|
23
|
-
|
|
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
|
|
31
|
-
broadcast:
|
|
30
|
+
export type UpdateGroupResult = {
|
|
31
|
+
broadcast: GroupBroadcastMessage;
|
|
32
32
|
};
|
|
33
|
-
export type
|
|
33
|
+
export type InviteToGroupParams = {
|
|
34
34
|
stores: StoreProvider;
|
|
35
|
-
|
|
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
|
|
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
|
|
48
|
+
export type JoinGroupParams = {
|
|
49
49
|
stores: StoreProvider;
|
|
50
50
|
identity: OwnIdentity;
|
|
51
|
-
|
|
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
|
|
59
|
+
export type JoinGroupResult = {
|
|
58
60
|
groupHandle: GroupHandle;
|
|
59
61
|
};
|
|
60
62
|
export type RemoveMemberParams = {
|
|
61
63
|
stores: StoreProvider;
|
|
62
|
-
|
|
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
|
|
73
|
+
export type RemoveGroupMemberParams = {
|
|
72
74
|
stores: StoreProvider;
|
|
73
|
-
|
|
75
|
+
groupID: string;
|
|
74
76
|
groupHandle: GroupHandle;
|
|
75
77
|
memberDID: string;
|
|
76
78
|
};
|
|
77
|
-
export type
|
|
79
|
+
export type RemoveGroupMemberResult = {
|
|
78
80
|
commitMessage: unknown;
|
|
79
81
|
updatedGroupHandle: GroupHandle;
|
|
80
82
|
};
|
|
81
|
-
export type
|
|
83
|
+
export type LeaveGroupParams = {
|
|
82
84
|
stores: StoreProvider;
|
|
83
|
-
|
|
85
|
+
groupID: string;
|
|
84
86
|
identity: OwnIdentity;
|
|
85
87
|
};
|
|
86
|
-
export type
|
|
88
|
+
export type LeaveGroupResult = undefined;
|
|
87
89
|
export type CreateCircleParams = {
|
|
88
90
|
stores: StoreProvider;
|
|
89
|
-
|
|
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:
|
|
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:
|
|
109
|
-
broadcasts: Array<
|
|
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:
|
|
118
|
+
broadcast: GroupBroadcastMessage;
|
|
117
119
|
};
|
|
118
120
|
export type AddCircleMemberParams = {
|
|
119
121
|
stores: StoreProvider;
|
|
120
|
-
|
|
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:
|
|
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:
|
|
136
|
+
broadcast: GroupBroadcastMessage;
|
|
135
137
|
};
|
|
136
|
-
export declare class
|
|
138
|
+
export declare class GroupManager {
|
|
137
139
|
#private;
|
|
138
|
-
constructor(params:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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 @@
|
|
|
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`))}}
|