@kubun/plugin-p2p 0.8.3 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/context/delegation.d.ts +4 -0
- package/lib/context/delegation.js +1 -0
- package/lib/context/group.js +1 -1
- package/lib/context/hub.d.ts +4 -0
- package/lib/context/hub.js +1 -0
- package/lib/context/join.js +1 -1
- package/lib/context/types.d.ts +28 -2
- package/lib/groups/broadcast-service.d.ts +130 -17
- package/lib/groups/broadcast-service.js +1 -1
- package/lib/groups/broadcast.d.ts +162 -5
- package/lib/groups/broadcast.js +1 -1
- package/lib/groups/events.d.ts +26 -5
- package/lib/groups/events.js +1 -1
- package/lib/groups/group-handle-registry.d.ts +90 -0
- package/lib/groups/group-handle-registry.js +1 -0
- package/lib/groups/invite-payload.d.ts +21 -1
- package/lib/groups/join-utils.d.ts +68 -2
- package/lib/groups/join-utils.js +1 -1
- package/lib/groups/manager.d.ts +26 -18
- package/lib/groups/manager.js +1 -1
- package/lib/groups/mls-codec.d.ts +7 -0
- package/lib/groups/mls-codec.js +1 -0
- package/lib/groups/mls-encryptor.d.ts +25 -0
- package/lib/groups/mls-encryptor.js +1 -0
- package/lib/groups/store-received-grant.d.ts +47 -0
- package/lib/groups/store-received-grant.js +1 -0
- package/lib/groups/store-received-revocation.d.ts +46 -0
- package/lib/groups/store-received-revocation.js +1 -0
- package/lib/groups/wire-frame.d.ts +32 -0
- package/lib/groups/wire-frame.js +1 -0
- package/lib/hub/connection-pool.d.ts +24 -0
- package/lib/hub/connection-pool.js +1 -1
- package/lib/hub/did-observing-transport.d.ts +64 -0
- package/lib/hub/did-observing-transport.js +1 -0
- package/lib/hub/errors.d.ts +28 -0
- package/lib/hub/errors.js +1 -0
- package/lib/hub/forward-remote-broadcast.d.ts +15 -0
- package/lib/hub/forward-remote-broadcast.js +1 -0
- package/lib/hub/group-channel.d.ts +37 -21
- package/lib/hub/group-channel.js +1 -1
- package/lib/hub/http-client.d.ts +17 -0
- package/lib/hub/http-client.js +1 -0
- package/lib/hub/hub-connection.d.ts +96 -0
- package/lib/hub/hub-connection.js +1 -0
- package/lib/hub/manager.d.ts +117 -0
- package/lib/hub/manager.js +1 -0
- package/lib/hub/receive-handler.d.ts +3 -6
- package/lib/hub/receive-handler.js +1 -1
- package/lib/hub/relay-manager.d.ts +92 -2
- package/lib/hub/relay-manager.js +1 -1
- package/lib/hub/send-handler.d.ts +28 -16
- package/lib/hub/send-handler.js +1 -1
- package/lib/hub/tunnel-inbox.d.ts +20 -0
- package/lib/hub/tunnel-inbox.js +1 -0
- package/lib/hub/wait-for-gate.d.ts +14 -0
- package/lib/hub/wait-for-gate.js +1 -0
- package/lib/hub/wiring.d.ts +85 -0
- package/lib/hub/wiring.js +1 -0
- package/lib/index.d.ts +102 -7
- package/lib/index.js +1 -1
- package/lib/schema.d.ts +2 -2
- package/lib/schema.js +113 -21
- package/lib/sync/broadcast-queue.d.ts +59 -0
- package/lib/sync/broadcast-queue.js +1 -0
- package/lib/sync/broadcast-sender.d.ts +52 -0
- package/lib/sync/broadcast-sender.js +1 -0
- package/lib/sync/forwarder.d.ts +97 -0
- package/lib/sync/forwarder.js +1 -0
- package/lib/sync/handlers.d.ts +20 -1
- package/lib/sync/handlers.js +1 -1
- package/lib/sync/hub-tunnel-sync-listener.d.ts +24 -0
- package/lib/sync/hub-tunnel-sync-listener.js +1 -0
- package/lib/sync/hub-tunnel-sync-provider.d.ts +36 -0
- package/lib/sync/hub-tunnel-sync-provider.js +1 -0
- package/lib/sync/merkle-apply.d.ts +41 -0
- package/lib/sync/merkle-apply.js +1 -1
- package/lib/sync/merkle-channel.d.ts +15 -0
- package/lib/sync/merkle-channel.js +1 -1
- package/lib/sync/receive-access-gate.d.ts +36 -0
- package/lib/sync/receive-access-gate.js +1 -0
- package/lib/sync/scope-resolver.d.ts +32 -0
- package/lib/sync/scope-resolver.js +1 -0
- package/lib/sync/sync-manager.d.ts +10 -0
- package/lib/sync/sync-manager.js +1 -1
- package/lib/types.d.ts +151 -1
- package/lib/util/mutex.d.ts +4 -0
- package/lib/util/mutex.js +1 -0
- package/package.json +40 -37
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ExecutionContext } from '@kubun/engine';
|
|
2
|
+
import type { DelegationRequestContext } from '../types.js';
|
|
3
|
+
import type { ContextDeps } from './types.js';
|
|
4
|
+
export declare function createDelegationContext(_ctx: ExecutionContext, deps: ContextDeps): DelegationRequestContext;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{getGraphStore as e}from"@kubun/store-graph";import{getP2PStore as t}from"@kubun/store-p2p";import{GraphQLError as r}from"graphql";export function createDelegationContext(r,u){let a=null,i=null,d=()=>a??=t(u.stores),c=()=>i??=e(u.stores);return{listOwnTokens:async({viewerDID:e,grantor:t,covers:r})=>{let n=await d(),u=Math.floor(Date.now()/1e3),a=await n.getHeldTokens({audience:e,atTime:u}),i=null!=t?a.filter(e=>e.grantor===t):a,s=i;return null!=r&&(null!=r.docID||null!=r.modelID)&&(s=await o(i,r,c)),s.map(l)},listOwnRevokedTokens:async({viewerDID:e,grantor:t})=>{let r=await d(),o=await r.getHeldRevocations({audience:e});return(null!=t?o.filter(e=>e.grantor===t):o).map(n)}}}function n(e){return{jti:e.jti,grantor:e.grantor,audience:e.audience,revokerDID:e.revoker_did,revokedAt:e.revoked_iat,verifiedAt:e.verified_at,capExp:e.cap_exp,groupID:e.group_id}}async function o(e,t,n){let o=null,l=null;if(null!=t.docID){l=t.docID;let e=await n(),u=null;try{u=await e.getDocument(t.docID)}catch{return[]}if(null==u)return[];if(o=u.model,null!=t.modelID&&t.modelID!==o)throw new r(`Document ${l} has model ${o}, not ${t.modelID}`,{extensions:{code:"DELEGATION_TOKEN_COVERS_MISMATCH"}})}else null!=t.modelID&&(o=t.modelID);if(null==o)return e;let u=new Map,a=await n(),i=u.get(o);null==i&&(i=await a.getModelInterfaces(o),u.set(o,i));let d=new Set;for(let e of(d.add(`urn:kubun:model:${o}`),i))d.add(`urn:kubun:model:${e}`);let c=null!=l?`urn:kubun:document:${l}`:null;return e.filter(e=>!!("*"===e.resource||e.resource===`urn:kubun:user:${e.grantor}`||d.has(e.resource))||null!=c&&e.resource===c)}function l(e){return{jwt:e.token,jti:e.jti,grantor:e.grantor,audience:e.audience,resource:e.resource,exp:e.exp}}
|
package/lib/context/group.js
CHANGED
|
@@ -1 +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{finalizeJoinedGroup as n}from"../groups/join-utils.js";import{restoreMLSGroupHandle as d}from"../groups/mls-group-handle.js";import{checkPeerAccess as l,normalizeCredential as s,toISO as p}from"./types.js";export function createGroupContext(m,u){let g=null,w=null,D=()=>g??=r(u.stores),I=()=>w??=t(u.stores);return{create: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:p(a.created_at)};return await u.emitter.emit("groupJoined",i),i},list: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:p(e.created_at)}))},get: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:p(r.created_at)}},addMember: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:p(i.created_at)};return await u.emitter.emit("groupMemberJoined",{...o,groupID:o.groupID}),o},listMembers: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:p(e.created_at)}))},createCircle: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:p(a.created_at)};return await u.emitter.emit("circleCreated",{...i,groupID:i.groupID}),i},listCircles: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:p(e.created_at)}))},addCircleMember: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:p(c.created_at)};return await u.emitter.emit("circleMemberAdded",{...n,circleID:n.circleID}),n},createCatalog: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:p(o.created_at)}},listCatalogs: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:p(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:p(e.created_at)}))},invite: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:s(i.credential),epoch:i.epoch,rootCapability:i.root_capability}),p=o(t),g=await u.groupManager.inviteToGroup({stores:u.stores,groupID:e,identity:a,groupHandle:n,recipientDID:p.did,recipientKeyPackage:p.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:p.did,role:"member",createdAt:new Date().toISOString()}),{invitePayload:I,groupID:e,groupName:w?.name??""}},join: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,emitter:u.emitter,invite:o})},leave: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:p(r.created_at),groupID:e}),{groupID:e}},removeMember: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:s(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}},update: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:p(r.created_at)};return await u.emitter.emit("groupDataChanged",{...a,groupID:e.groupID}),a},updateCircle: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:p(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},updateCatalog: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:p(i.created_at)}},deleteCatalog:async e=>{let t=await I();return await t.deleteCatalog(e),{deletedID:e}},deleteCircle: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:p(r.created_at)}),{deletedID:e}},removeCircleMember: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
|
+
import{createCapability as e,createRevocationRecord as t}from"@enkaku/capability";import{stringifyToken as r}from"@enkaku/token";import{HLC as i}from"@kubun/hlc";import{getGraphStore as a}from"@kubun/store-graph";import{getP2PStore as o}from"@kubun/store-p2p";import{GraphQLError as d}from"graphql";import{decodeFullJoinRequest as l,decodeInvitePayload as c,decodeJoinRequest as n,encodeInvitePayload as s}from"../groups/invite-payload.js";import{applyInviteGrants as m,applyInviteRevocations as u,finalizeJoinedGroup as p}from"../groups/join-utils.js";import{checkPeerAccess as g,toISO as w}from"./types.js";export function createGroupContext(D,I){let y=null,_=null,h=()=>y??=o(I.stores),b=()=>_??=a(I.stores);return{create:async e=>{let t=await I.groupManager.createGroup({stores:I.stores,identity:I.identity,name:e.name,description:e.description,createdBy:D.viewerDID,hubs:e.hubs?.map(e=>({url:e.url,serverDID:e.serverDID??void 0}))}),r=await h(),i=await r.getGroup(t.groupID);if(null==i)throw Error("Failed to create group");let a={id:i.id,name:i.name,description:i.description,createdBy:i.created_by,createdAt:w(i.created_at)};return await I.emitter.emit("groupJoined",a),a},list:async()=>{let e=await h();return(await e.listGroups()).map(e=>({id:e.id,name:e.name,description:e.description,createdBy:e.created_by,createdAt:w(e.created_at)}))},get:async e=>{let t=await h(),r=await t.getGroup(e);return null==r?null:{id:r.id,name:r.name,description:r.description,createdBy:r.created_by,createdAt:w(r.created_at)}},addMember:async(e,t,r)=>{let a=await h();await a.addGroupMember({group_id:e,member_did:t,role:r,hlc:i.serialize(I.hlc.now())});let o=await a.getGroupMember(e,t);if(null==o)throw Error("Failed to add group member");let d={groupID:o.group_id,memberDID:o.member_did,role:o.role,createdAt:w(o.created_at)};return await I.emitter.emit("groupMemberJoined",{...d,groupID:d.groupID}),d},listMembers:async e=>{let t=await h();return(await t.listGroupMembers(e)).map(e=>({groupID:e.group_id,memberDID:e.member_did,role:e.role,createdAt:w(e.created_at)}))},createCircle:async e=>{let t=await I.groupManager.createCircle({stores:I.stores,groupID:e.groupID,name:e.name,description:e.description}),r=await h(),i=await r.getCircle(t.circleID);if(null==i)throw Error("Failed to create circle");let a={id:i.id,groupID:i.group_id,name:i.name,description:i.description,createdAt:w(i.created_at)};return await I.emitter.emit("circleCreated",{...a,groupID:a.groupID}),I.stores.onCommit(()=>I.scheduleBroadcast(i.group_id,{type:"circle:create",circle:{id:i.id,groupID:i.group_id,name:i.name,description:i.description,catalogIDs:i.catalog_ids,hlc:i.hlc}})),a},listCircles:async e=>{let t=await h();return(await t.listCirclesByGroup(e)).map(e=>({id:e.id,groupID:e.group_id,name:e.name,description:e.description,createdAt:w(e.created_at)}))},addCircleMember:async(e,t,r)=>{let a=await h(),o=i.serialize(I.hlc.now());await a.addCircleMember({circle_id:e,member_did:t,role:r,hlc:o});let d=(await a.listCircleMembers(e)).find(e=>e.member_did===t);if(null==d)throw Error("Failed to add circle member");let l={circleID:d.circle_id,memberDID:d.member_did,role:d.role,createdAt:w(d.created_at)};await I.emitter.emit("circleMemberAdded",{...l,circleID:l.circleID});let c=await a.getCircle(e);if(null!=c)if("admin"===d.role||"member"===d.role){let e=d.role;I.stores.onCommit(()=>I.scheduleBroadcast(c.group_id,{type:"member:add",member:{circleID:d.circle_id,memberDID:d.member_did,role:e,hlc:d.hlc}}))}else I.logger.warn("addCircleMember: skipping broadcast for unrecognized role",{circleID:e,memberDID:t,role:d.role});return l},createCatalog:async e=>{let t=await b(),r=I.runtime.getRandomID(),a=i.serialize(I.hlc.now());await t.createCatalog({id:r,owner_did:D.viewerDID,name:e.name,description:e.description??"",filter_criteria:JSON.parse(e.filterCriteria),hlc:a});let o=await t.getCatalog(r);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:w(o.created_at)}},listCatalogs:async e=>{let t=await b();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:w(e.created_at)}));let r=await h(),i=new Set((await r.listGroups()).map(e=>e.created_by));return(await Promise.all(Array.from(i).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:w(e.created_at)}))},invite:async(e,t,r,i)=>{g(D.viewerDID,I.identity.id,I.autoAcceptPeers);let a=await h(),o=I.identity,d=n(t),l=await I.groupManager.inviteToGroup({stores:I.stores,groupID:e,identity:o,recipientDID:d.did,recipientKeyPackage:d.publicPackage,permission:"member"}),c=await a.getGroup(e),m=(await a.listHubsByGroupID(e)).map(e=>null==e.server_did?{url:e.url}:{url:e.url,serverDID:e.server_did}),u=s({groupID:e,groupName:c?.name??"",suggestedHubs:m,invite:l.invite,welcomeMessage:l.welcomeMessage,ratchetTree:l.ratchetTree,...null!=r&&r.length>0?{grants:r}:{},...null!=i&&i.length>0?{revocations:i}:{}});return await I.emitter.emit("groupMemberJoined",{groupID:e,memberDID:d.did,role:"member",createdAt:new Date().toISOString()}),I.stores.onCommit(()=>I.scheduleSendCommit(e,l.commitMessage)),{invitePayload:u,groupID:e,groupName:c?.name??""}},join:async(e,t)=>{let r=I.identity,i=c(e),a=l(t),o={publicPackage:a.publicPackage,privatePackage:a.privatePackage,ownerDID:a.did};return await I.groupManager.joinGroup({stores:I.stores,identity:r,groupID:i.groupID,groupName:i.groupName,hubs:i.suggestedHubs,invite:i.invite,keyPackageBundle:o,welcomeMessage:i.welcomeMessage,ratchetTree:i.ratchetTree}),await m({stores:I.stores,selfDID:r.id,groupID:i.groupID,grants:i.grants,hlc:I.hlc,emitter:I.emitter,logger:I.logger}),await u({stores:I.stores,selfDID:r.id,groupID:i.groupID,revocations:i.revocations,hlc:I.hlc,emitter:I.emitter,logger:I.logger}),await p({stores:I.stores,emitter:I.emitter,invite:i})},leave:async e=>{let t=await h(),r=await t.getGroup(e),a=I.identity;try{await I.broadcastNow(e,{type:"group:leaveRequest",groupID:e,memberDID:a.id,hlc:i.serialize(I.hlc.now())})}catch(t){I.logger.warn("leave-request broadcast failed",{groupID:e,error:t})}return await I.groupManager.leaveGroup({stores:I.stores,groupID:e,identity:a}),null!=r&&await I.emitter.emit("groupLeft",{id:r.id,name:r.name,description:r.description,createdBy:r.created_by,createdAt:w(r.created_at),groupID:e}),{groupID:e}},removeMember:async(e,t)=>{let{commitMessage:r}=await I.groupManager.removeGroupMember({stores:I.stores,groupID:e,memberDID:t});return await I.emitter.emit("groupMemberLeft",{groupID:e,memberDID:t,role:"",createdAt:new Date().toISOString()}),I.stores.onCommit(()=>I.scheduleSendCommit(e,r)),{groupID:e}},update:async e=>{await I.groupManager.updateGroup({stores:I.stores,groupID:e.groupID,update:{name:e.name,description:e.description}});let t=await h(),r=await t.getGroup(e.groupID);if(null==r)throw Error(`Group ${e.groupID} not found`);let i={id:r.id,name:r.name,description:r.description,createdBy:r.created_by,createdAt:w(r.created_at)};return await I.emitter.emit("groupDataChanged",{...i,groupID:e.groupID}),I.stores.onCommit(()=>I.scheduleBroadcast(r.id,{type:"group:update",groupID:r.id,update:{name:e.name,description:e.description,hlc:r.hlc}})),i},updateCircle:async e=>{await I.groupManager.updateCircle({stores:I.stores,circleID:e.circleID,update:{name:e.name,description:e.description,catalogIDs:e.catalogIDs}});let t=await h(),r=await t.getCircle(e.circleID);if(null==r)throw Error(`Circle ${e.circleID} not found`);let i={id:r.id,groupID:r.group_id,name:r.name,description:r.description,createdAt:w(r.created_at)};return null!=e.catalogIDs?await I.emitter.emit("circleCatalogsChanged",{...i,circleID:e.circleID}):await I.emitter.emit("circleDataChanged",{...i,circleID:e.circleID}),I.stores.onCommit(()=>I.scheduleBroadcast(r.group_id,{type:"circle:update",circleID:r.id,update:{name:e.name,description:e.description,catalogIDs:e.catalogIDs,hlc:r.hlc}})),i},updateCatalog:async e=>{let t=await b(),r={hlc:i.serialize(I.hlc.now())};null!=e.name&&(r.name=e.name),null!=e.filterCriteria&&(r.filter_criteria=JSON.parse(e.filterCriteria)),await t.updateCatalog(e.catalogID,r);let a=await t.getCatalog(e.catalogID);if(null==a)throw Error(`Catalog ${e.catalogID} not found`);return{id:a.id,ownerDID:a.owner_did,name:a.name,description:a.description,filterCriteria:JSON.stringify(a.filter_criteria),createdAt:w(a.created_at)}},deleteCatalog:async e=>{let t=await b();return await t.deleteCatalog(e),{deletedID:e}},deleteCircle:async e=>{let t=await h(),r=await t.getCircle(e);if(await I.groupManager.deleteCircle({stores:I.stores,circleID:e}),null!=r){await I.emitter.emit("circleDeleted",{id:r.id,groupID:r.group_id,name:r.name,description:r.description,createdAt:w(r.created_at)});let t=i.serialize(I.hlc.now());I.stores.onCommit(()=>I.scheduleBroadcast(r.group_id,{type:"circle:delete",circleID:e,hlc:t}))}return{deletedID:e}},removeCircleMember:async(e,t)=>{let r=await h(),[a,o]=await Promise.all([r.getCircle(e),r.getCircleMember(e,t)]);if(await I.groupManager.removeCircleMember({stores:I.stores,circleID:e,memberDID:t}),await I.emitter.emit("circleMemberRemoved",{circleID:e,memberDID:t,role:o?.role??"",createdAt:null!=o?w(o.created_at):new Date().toISOString()}),null!=a){let r=i.serialize(I.hlc.now());I.stores.onCommit(()=>I.scheduleBroadcast(a.group_id,{type:"member:remove",circleID:e,memberDID:t,hlc:r}))}return{circleID:e,memberDID:t}},grantWriteCapability:async({to:t,res:a,groupID:o,expiresIn:d})=>{let l=await h(),c=Math.floor(Date.now()/1e3),n=c+(d??2592e3),s=I.runtime.getRandomID(),m=r(await e(I.identity,{sub:I.identity.id,aud:t,act:"document/write",res:a,iat:c,exp:n,jti:s})),u=i.serialize(I.hlc.now());return await l.addDelegationToken({jti:s,grantor:I.identity.id,audience:t,token:m,resource:a,act:"document/write",exp:n,group_id:o,hlc:u})&&await I.emitter.emit("delegationTokenAdded",{jwt:m,jti:s,grantor:I.identity.id,audience:t,resource:a,exp:n}),I.stores.onCommit(()=>I.scheduleBroadcast(o,{type:"delegation:share",token:m,groupID:o,hlc:u})),m},revokeCapability:async({jti:e})=>{let a=await h(),o=await a.getDelegationTokenByJti(e);if(null==o)throw new d(`No capability found locally for jti ${e}`,{extensions:{code:"REVOCATION_CAP_UNKNOWN"}});if(o.grantor!==D.viewerDID)throw new d(`Only the capability issuer can revoke jti ${e}`,{extensions:{code:"REVOCATION_UNAUTHORIZED"}});let l=await t(I.identity,e),c=r(await I.identity.signToken(l)),n=i.serialize(I.hlc.now()),s=await a.addRevocation({jti:e,revoker_did:I.identity.id,revoked_iat:l.iat,revocation_token:c,verified_at:l.iat,cap_exp:o.exp,group_id:o.group_id,hlc:n});return s&&await I.emitter.emit("delegationTokenRevoked",{jti:e,grantor:o.grantor,audience:o.audience,revokerDID:I.identity.id,revokedAt:l.iat,verifiedAt:l.iat,capExp:o.exp,groupID:o.group_id}),I.stores.onCommit(()=>I.scheduleBroadcast(o.group_id,{type:"delegation:revoke",token:c,groupID:o.group_id,hlc:n})),s}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{getP2PStore as e}from"@kubun/store-p2p";import{bindHubToGroup as t,createHub as r,deleteHub as s,getHub as a,listHubs as u,listHubsByGroup as i,unbindHubFromGroup as l,updateHub as n}from"../hub/manager.js";import{toISO as o}from"./types.js";function d(e){return{id:e.id,label:e.label,url:e.url,serverDID:e.serverDID,createdAt:e.createdAt.toISOString(),updatedAt:null==e.updatedAt?null:e.updatedAt.toISOString()}}export function createHubContext(c,p){return{list:async()=>(await u({stores:p.stores})).map(d),get:async e=>{let t=await a({stores:p.stores,id:e});return null==t?null:d(t)},create:async e=>d(await r({stores:p.stores,url:e.url,label:e.label,serverDID:e.serverDID})),update:async e=>d(await n({stores:p.stores,id:e.id,url:e.url,label:e.label,serverDID:e.serverDID})),delete:async(e,t)=>{let r=await s({stores:p.stores,id:e,force:t});return{success:r.success,boundGroupIDs:r.boundGroupIDs}},bind:async({hubID:e,groupID:r})=>{let s=await t({stores:p.stores,hubID:e,groupID:r});if(s){let t=await a({stores:p.stores,id:e});null!=t&&await p.emitter.emit("hubBound",{groupID:r,hubID:e,hubURL:t.url,serverDID:t.serverDID})}return s},unbind:async({hubID:e,groupID:t})=>{let r=await i({stores:p.stores,groupID:t}),s=r.find(t=>t.id===e)?.url??null,a=await l({stores:p.stores,hubID:e,groupID:t});return a&&null!=s&&await p.emitter.emit("hubUnbound",{groupID:t,hubID:e,hubURL:s}),a},listByGroup:async e=>(await i({stores:p.stores,groupID:e})).map(d),listGroups:async t=>{let r=await e(p.stores);return(await r.listGroupsByHubID(t)).map(e=>({id:e.id,name:e.name,description:e.description,createdBy:e.created_by,createdAt:o(e.created_at)}))}}}
|
package/lib/context/join.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createKeyPackageBundle as e}from"@enkaku/group";import{decodeInvitePayload as t,encodeJoinRequest as
|
|
1
|
+
import{createKeyPackageBundle as e}from"@enkaku/group";import{decodeInvitePayload as t,encodeJoinRequest as r}from"../groups/invite-payload.js";import{applyInviteGrants as i,applyInviteRevocations as o,finalizeJoinedGroup as a}from"../groups/join-utils.js";import{checkPeerAccess as s}from"./types.js";export function createJoinContext(g,n){return{prepareRequest:async()=>{s(g.viewerDID,n.identity.id,n.autoAcceptPeers);let t=n.identity,i=await e(t);return n.pendingJoinRequests.set(g.viewerDID,{publicPackage:i.publicPackage,privatePackage:i.privatePackage,did:t.id}),{joinRequest:r({did:t.id,publicPackage:i.publicPackage})}},complete:async e=>{s(g.viewerDID,n.identity.id,n.autoAcceptPeers);let r=n.pendingJoinRequests.get(g.viewerDID);if(null==r)throw Error("No pending join request found — call prepareJoinRequest first");let p=n.identity,u=t(e),c={publicPackage:r.publicPackage,privatePackage:r.privatePackage,ownerDID:p.id};await n.groupManager.joinGroup({stores:n.stores,identity:p,groupID:u.groupID,groupName:u.groupName,hubs:u.suggestedHubs,invite:u.invite,keyPackageBundle:c,welcomeMessage:u.welcomeMessage,ratchetTree:u.ratchetTree}),await i({stores:n.stores,selfDID:p.id,groupID:u.groupID,grants:u.grants,hlc:n.hlc,emitter:n.emitter,logger:n.logger}),await o({stores:n.stores,selfDID:p.id,groupID:u.groupID,revocations:u.revocations,hlc:n.hlc,emitter:n.emitter,logger:n.logger});let l=await a({stores:n.stores,emitter:n.emitter,invite:u});return n.pendingJoinRequests.delete(g.viewerDID),l}}}
|
package/lib/context/types.d.ts
CHANGED
|
@@ -4,7 +4,10 @@ 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 {
|
|
7
|
+
import type { Logger } from '@kubun/logger';
|
|
8
|
+
import type { GroupBroadcastMessage } from '../groups/broadcast.js';
|
|
9
|
+
import type { P2PEventEmitter } from '../groups/events.js';
|
|
10
|
+
import type { GroupHandleRegistry } from '../groups/group-handle-registry.js';
|
|
8
11
|
import type { GroupManager } from '../groups/manager.js';
|
|
9
12
|
import type { SyncManager } from '../sync/sync-manager.js';
|
|
10
13
|
export type PendingJoinRequest = {
|
|
@@ -15,14 +18,37 @@ export type PendingJoinRequest = {
|
|
|
15
18
|
export type ContextDeps = {
|
|
16
19
|
identity: OwnIdentity;
|
|
17
20
|
groupManager: GroupManager;
|
|
21
|
+
registry: GroupHandleRegistry;
|
|
18
22
|
syncManager: SyncManager;
|
|
19
23
|
stores: StoreProvider;
|
|
20
24
|
adapter: Adapter;
|
|
21
25
|
hlc: HLC;
|
|
22
|
-
emitter:
|
|
26
|
+
emitter: P2PEventEmitter;
|
|
23
27
|
pendingJoinRequests: Map<string, PendingJoinRequest>;
|
|
24
28
|
runtime: Runtime;
|
|
25
29
|
autoAcceptPeers?: Array<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Fire-and-forget hub broadcast trigger. Wired to `HubWiring.scheduleBroadcast`
|
|
32
|
+
* when the p2p plugin is configured with `hub`; otherwise a no-op from
|
|
33
|
+
* `DISABLED_HUB`. Handlers invoke this via `stores.onCommit(...)` so the hub
|
|
34
|
+
* send runs after the mutation transaction commits.
|
|
35
|
+
*/
|
|
36
|
+
scheduleBroadcast: (groupID: string, message: GroupBroadcastMessage) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Awaitable broadcast (encrypt + send completed on resolve). Wired to
|
|
39
|
+
* `HubWiring.broadcastNow`; a no-op resolve under `DISABLED_HUB`. Used where
|
|
40
|
+
* the send must complete before a following local op — e.g. a leave-request
|
|
41
|
+
* encrypted while the MLS handle is still live, before `leaveGroup`.
|
|
42
|
+
*/
|
|
43
|
+
broadcastNow: (groupID: string, message: GroupBroadcastMessage) => Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Fire-and-forget fan-out of an MLS handshake Commit to a group's existing
|
|
46
|
+
* members. Wired to `HubWiring.scheduleSendCommit`; a no-op under
|
|
47
|
+
* `DISABLED_HUB`. Handlers invoke it via `stores.onCommit(...)` so the send
|
|
48
|
+
* runs after the mutation transaction commits.
|
|
49
|
+
*/
|
|
50
|
+
scheduleSendCommit: (groupID: string, commitBytes: Uint8Array) => void;
|
|
51
|
+
logger: Logger;
|
|
26
52
|
};
|
|
27
53
|
export declare function toISO(timestamp: unknown): string;
|
|
28
54
|
/**
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { EventEmitter } from '@enkaku/event';
|
|
2
|
+
import type { DefaultAccessLevel, GraphInternals } from '@kubun/engine';
|
|
3
|
+
import { HLC } from '@kubun/hlc';
|
|
4
|
+
import type { Logger } from '@kubun/logger';
|
|
2
5
|
import type { GraphStoreAPI } from '@kubun/store-graph';
|
|
3
6
|
import type { P2PStoreAPI } from '@kubun/store-p2p';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
7
|
+
import type { ForwardingConfig } from '../sync/forwarder.js';
|
|
8
|
+
import type { StoreUnreadableMode } from '../types.js';
|
|
9
|
+
import { type AffectedRow, type GroupBroadcastMessage } from './broadcast.js';
|
|
10
|
+
import type { P2PEventEmitter } from './events.js';
|
|
11
|
+
import type { GroupHandleRegistry } from './group-handle-registry.js';
|
|
6
12
|
/**
|
|
7
13
|
* Thrown when a received broadcast fails to decrypt (MLS restore, decrypt,
|
|
8
14
|
* or deserialize). Callers should typically ack-and-skip these, since the
|
|
@@ -21,10 +27,32 @@ export declare class ApplyError extends Error {
|
|
|
21
27
|
readonly cause: unknown;
|
|
22
28
|
constructor(message: string, cause: unknown);
|
|
23
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Thrown when a received MLS Commit targets a future epoch the local handle
|
|
32
|
+
* has not reached yet (a gap in the in-order Commit stream). Callers should
|
|
33
|
+
* NOT ack — the hub redelivers in `sequence_id` order, so the gap-filling
|
|
34
|
+
* Commit arrives first and this one applies on a later delivery.
|
|
35
|
+
*/
|
|
36
|
+
export declare class CommitOutOfOrderError extends Error {
|
|
37
|
+
readonly messageEpoch: bigint;
|
|
38
|
+
readonly handleEpoch: bigint;
|
|
39
|
+
constructor(messageEpoch: bigint, handleEpoch: bigint);
|
|
40
|
+
}
|
|
24
41
|
export type BroadcastEvent = {
|
|
25
42
|
groupID: string;
|
|
26
43
|
message: GroupBroadcastMessage;
|
|
27
44
|
applied: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Row affected by the broadcast. After-state for create / update,
|
|
47
|
+
* before-state for delete / remove. Undefined when `applied === false`.
|
|
48
|
+
*/
|
|
49
|
+
affected?: AffectedRow;
|
|
50
|
+
/**
|
|
51
|
+
* Count of `mutation:apply` entries the local broadcast log already had.
|
|
52
|
+
* Only set on the `mutation:apply` branch; absent on control-plane
|
|
53
|
+
* variants.
|
|
54
|
+
*/
|
|
55
|
+
duplicates?: number;
|
|
28
56
|
};
|
|
29
57
|
export type BroadcastServiceEvents = {
|
|
30
58
|
broadcast: BroadcastEvent;
|
|
@@ -32,45 +60,130 @@ export type BroadcastServiceEvents = {
|
|
|
32
60
|
export type SendBroadcastParams = {
|
|
33
61
|
groupID: string;
|
|
34
62
|
message: GroupBroadcastMessage;
|
|
35
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Wire-send hook invoked with the MLS-encrypted payload INSIDE the
|
|
65
|
+
* registry's per-group lock. A throw here propagates out of `prepareSend`
|
|
66
|
+
* and the registry skips persist — so a failed send leaves the device's
|
|
67
|
+
* MLS state at its pre-encrypt epoch, preventing the wedge where local
|
|
68
|
+
* state outpaces recipients.
|
|
69
|
+
*/
|
|
70
|
+
send: (encrypted: Uint8Array) => Promise<void>;
|
|
36
71
|
};
|
|
37
|
-
export type
|
|
38
|
-
|
|
39
|
-
|
|
72
|
+
export type SendCommitParams = {
|
|
73
|
+
groupID: string;
|
|
74
|
+
/**
|
|
75
|
+
* Canonical MLS-encoded handshake Commit bytes (an encrypted
|
|
76
|
+
* `PrivateMessage`) produced by the committer's `GroupManager` call.
|
|
77
|
+
*/
|
|
78
|
+
commitBytes: Uint8Array;
|
|
79
|
+
/** Wire-send hook; receives the `mls`-framed bytes. */
|
|
80
|
+
send: (framed: Uint8Array) => Promise<void>;
|
|
40
81
|
};
|
|
41
82
|
export type ReceiveBroadcastParams = {
|
|
42
83
|
groupID: string;
|
|
43
84
|
encrypted: Uint8Array;
|
|
44
|
-
groupState: SerializedMLSGroupState;
|
|
45
85
|
};
|
|
46
|
-
export type
|
|
86
|
+
export type ReceiveAppResult = {
|
|
87
|
+
kind: 'app';
|
|
47
88
|
message: GroupBroadcastMessage;
|
|
48
89
|
applied: boolean;
|
|
49
|
-
|
|
90
|
+
affected?: AffectedRow;
|
|
91
|
+
duplicates?: number;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Result of processing a received MLS handshake Commit (`mls` wire frame).
|
|
95
|
+
* `removed`/`added` are the membership deltas derived by diffing the group
|
|
96
|
+
* roster before and after applying the Commit. `removed` DIDs are tombstoned
|
|
97
|
+
* via `markMemberRemoved` so the apply-time membership check can reject their
|
|
98
|
+
* post-removal writes.
|
|
99
|
+
*/
|
|
100
|
+
export type ReceiveCommitResult = {
|
|
101
|
+
kind: 'mls';
|
|
102
|
+
epoch: bigint;
|
|
103
|
+
removed: Array<string>;
|
|
104
|
+
added: Array<string>;
|
|
50
105
|
};
|
|
106
|
+
export type ReceiveBroadcastResult = ReceiveAppResult | ReceiveCommitResult;
|
|
51
107
|
/**
|
|
52
108
|
* BroadcastService handles MLS encrypt/decrypt for group broadcasts
|
|
53
109
|
* and applies received messages to the local DB.
|
|
54
110
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
111
|
+
* Encrypt and decrypt go through `GroupHandleRegistry.withHandle` so MLS
|
|
112
|
+
* state mutations serialize through the per-group mutex with write-through
|
|
113
|
+
* persist. The DB-apply step (`processBroadcast`) for receives runs OUTSIDE
|
|
114
|
+
* the lock so a slow apply does not block subsequent encrypt/decrypt on the
|
|
115
|
+
* same group.
|
|
58
116
|
*/
|
|
59
117
|
export type BroadcastServiceParams = {
|
|
60
118
|
p2pStore: P2PStoreAPI;
|
|
61
119
|
graphStore: GraphStoreAPI;
|
|
120
|
+
/**
|
|
121
|
+
* Engine graph internals — required so `mutation:apply` broadcasts route
|
|
122
|
+
* through {@link GraphInternals.applyVerifiedMutation} with `origin: 'peer'`.
|
|
123
|
+
*/
|
|
124
|
+
graph: GraphInternals;
|
|
125
|
+
/** Self DID for `mutation:apply` loopback detection. */
|
|
126
|
+
selfDID: string;
|
|
127
|
+
/** Single canonical access point for MLS `GroupHandle` instances. */
|
|
128
|
+
registry: GroupHandleRegistry;
|
|
129
|
+
/**
|
|
130
|
+
* Local HLC clock for stamping the `removed_at_hlc` of members dropped by a
|
|
131
|
+
* received MLS Commit (the receive side has no HLC otherwise). Defaults to a
|
|
132
|
+
* fresh clock keyed on `selfDID` when omitted.
|
|
133
|
+
*/
|
|
134
|
+
hlc?: HLC;
|
|
135
|
+
/**
|
|
136
|
+
* Receiver storage mode for incoming `mutation:apply` entries. Defaults
|
|
137
|
+
* to `'persist'`. When `'drop'`, `defaultAccessLevel` must be supplied
|
|
138
|
+
* so the gate factory can resolve server-side default access rules.
|
|
139
|
+
*/
|
|
140
|
+
storeUnreadable?: StoreUnreadableMode;
|
|
141
|
+
/** Server default access level — required when `storeUnreadable === 'drop'`. */
|
|
142
|
+
defaultAccessLevel?: DefaultAccessLevel;
|
|
143
|
+
/**
|
|
144
|
+
* Forwarding config. Threaded into `processBroadcast` per call.
|
|
145
|
+
* `false` / undefined → forwarding disabled (default).
|
|
146
|
+
*/
|
|
147
|
+
forwarding?: ForwardingConfig;
|
|
148
|
+
/**
|
|
149
|
+
* Hub forwarding hook — required when `forwarding` is set. Called once per
|
|
150
|
+
* surviving candidate group on the receive path.
|
|
151
|
+
*/
|
|
152
|
+
scheduleBroadcast?: (groupID: string, message: GroupBroadcastMessage) => void | Promise<void>;
|
|
153
|
+
/**
|
|
154
|
+
* Emitter forwarded to `processBroadcast` so `delegation:share` receives
|
|
155
|
+
* surface a `delegationTokenAdded` event to local subscribers.
|
|
156
|
+
*/
|
|
157
|
+
emitter?: P2PEventEmitter;
|
|
158
|
+
logger?: Logger;
|
|
62
159
|
};
|
|
63
160
|
export declare class BroadcastService extends EventEmitter<BroadcastServiceEvents> {
|
|
64
161
|
#private;
|
|
65
162
|
constructor(params: BroadcastServiceParams);
|
|
66
163
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
164
|
+
* Encrypt + send a broadcast for a group, then persist the advanced MLS state.
|
|
165
|
+
*
|
|
166
|
+
* Failure semantics: send happens inside the registry's per-group lock. A
|
|
167
|
+
* throw from `send` propagates out and the registry skips persist — so the
|
|
168
|
+
* device's MLS state stays at its pre-encrypt epoch and the next attempt
|
|
169
|
+
* re-derives a fresh handle from the prior generation. Wedge protection
|
|
170
|
+
* intact.
|
|
171
|
+
*/
|
|
172
|
+
prepareSend(params: SendBroadcastParams): Promise<void>;
|
|
173
|
+
/**
|
|
174
|
+
* Send a pre-produced MLS handshake Commit to the group, tagged `mls` on the
|
|
175
|
+
* wire. The Commit is already a self-protected (encrypted `PrivateMessage`)
|
|
176
|
+
* framed message produced by the committer's `GroupManager` call — it is NOT
|
|
177
|
+
* re-encrypted through the application path, so this does not touch the
|
|
178
|
+
* handle. The committer's MLS state has already advanced + persisted; the
|
|
179
|
+
* hub's durable + ordered mailbox carries the Commit to existing members and
|
|
180
|
+
* replays it on reconnect.
|
|
69
181
|
*/
|
|
70
|
-
|
|
182
|
+
prepareSendCommit(params: SendCommitParams): Promise<void>;
|
|
71
183
|
/**
|
|
72
|
-
* Process a received broadcast: MLS decrypt
|
|
73
|
-
*
|
|
184
|
+
* Process a received broadcast: MLS decrypt INSIDE the registry lock,
|
|
185
|
+
* then apply to the DB OUTSIDE the lock so a slow apply doesn't block
|
|
186
|
+
* subsequent encrypt/decrypt on the same group.
|
|
74
187
|
*/
|
|
75
188
|
processReceived(params: ReceiveBroadcastParams): Promise<ReceiveBroadcastResult>;
|
|
76
189
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{EventEmitter as r}from"@enkaku/event";import{
|
|
1
|
+
import{EventEmitter as r}from"@enkaku/event";import{readMessageEpoch as e}from"@enkaku/group";import{HLC as t}from"@kubun/hlc";import{processBroadcast as a}from"./broadcast.js";import{deserializeBroadcast as o,serializeBroadcast as s}from"./broadcast-codec.js";import{mlsDecryptFramed as i,mlsEncryptFramed as c}from"./mls-codec.js";import{frameWire as p,unframeWire as m,WireKind as d}from"./wire-frame.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 CommitOutOfOrderError extends Error{messageEpoch;handleEpoch;constructor(r,e){super(`MLS commit epoch ${r} ahead of handle epoch ${e}`),this.name="CommitOutOfOrderError",this.messageEpoch=r,this.handleEpoch=e}}export class BroadcastService extends r{#r;#e;#t;constructor(r){super(),this.#t=r.hlc??new t({nodeID:r.selfDID}),this.#r={p2pStore:r.p2pStore,graphStore:r.graphStore,graph:r.graph,selfDID:r.selfDID,storeUnreadable:r.storeUnreadable,defaultAccessLevel:r.defaultAccessLevel,forwarding:r.forwarding,scheduleBroadcast:r.scheduleBroadcast,emitter:r.emitter,logger:r.logger},this.#e=r.registry}async prepareSend(r){let{groupID:e,message:t,send:a}=r,o=s(t);await this.#e.withHandle(e,async r=>{let{framed:e}=await c(r,o);await a(p(d.app,e))})}async prepareSendCommit(r){let{commitBytes:e,send:t}=r;await t(p(d.mls,e))}async processReceived(r){let e,t,s,c,{groupID:p,encrypted:l}=r;try{({kind:e,payload:t}=m(l))}catch(r){throw new DecryptError("Malformed group wire frame",r)}if(e===d.mls)return await this.#a(p,t);try{s=await this.#e.withHandle(p,async r=>{let e=await i(r,t);return o(e)})}catch(r){throw new DecryptError("Failed to decrypt broadcast",r)}try{c=await a(this.#r,s,p)}catch(r){throw new ApplyError("Failed to apply broadcast",r)}let{applied:h,affected:n,duplicates:u}=c;return await this.emit("broadcast",{groupID:p,message:s,applied:h,affected:n,duplicates:u}),{kind:"app",message:s,applied:h,affected:n,duplicates:u}}async #a(r,a){return await this.#e.withHandle(r,async o=>{let s=e(a),i=o.epoch;if(null!=s&&s<i)return{kind:"mls",epoch:i,removed:[],added:[]};if(null!=s&&s>i)throw new CommitOutOfOrderError(s,i);let c=o.listMembers();try{await o.processMessage(a)}catch(r){throw new DecryptError("Failed to process MLS commit",r)}let p=o.listMembers(),m=new Set(c.map(r=>r.id)),d=new Set(p.map(r=>r.id)),l=c.filter(r=>!d.has(r.id)).map(r=>r.id),h=p.filter(r=>!m.has(r.id)).map(r=>r.id),n=t.serialize(this.#t.now()),{p2pStore:u}=this.#r;for(let e of l)await u.markMemberRemoved(r,e,n);for(let e of h)await u.addGroupMember({group_id:r,member_did:e,role:"member",hlc:n});return{kind:"mls",epoch:o.epoch,removed:l,added:h}})}}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
import type { DefaultAccessLevel, GraphInternals } from '@kubun/engine';
|
|
2
|
+
import type { Logger } from '@kubun/logger';
|
|
1
3
|
import type { CatalogRecord, CircleMemberRecord, CircleRecord } from '@kubun/protocol';
|
|
2
|
-
import type { GraphStoreAPI } from '@kubun/store-graph';
|
|
3
|
-
import type { P2PStoreAPI } from '@kubun/store-p2p';
|
|
4
|
+
import type { Catalog, GraphStoreAPI } from '@kubun/store-graph';
|
|
5
|
+
import type { Circle, CircleMember, Group, P2PStoreAPI } from '@kubun/store-p2p';
|
|
6
|
+
import { type ForwardingConfig } from '../sync/forwarder.js';
|
|
7
|
+
import type { StoreUnreadableMode } from '../types.js';
|
|
8
|
+
import type { P2PEventEmitter } from './events.js';
|
|
9
|
+
export type MutationApplyEntry = {
|
|
10
|
+
mutationJWT: string;
|
|
11
|
+
docID: string;
|
|
12
|
+
/** HLC string identifying this mutation's version. */
|
|
13
|
+
version: string;
|
|
14
|
+
modelID: string;
|
|
15
|
+
};
|
|
4
16
|
export type GroupBroadcastMessage = {
|
|
5
17
|
type: 'circle:create';
|
|
6
18
|
circle: CircleRecord;
|
|
@@ -41,24 +53,169 @@ export type GroupBroadcastMessage = {
|
|
|
41
53
|
type: 'catalog:delete';
|
|
42
54
|
catalogID: string;
|
|
43
55
|
hlc: string;
|
|
56
|
+
} | {
|
|
57
|
+
/**
|
|
58
|
+
* Distributes a minted `document/write` capability token to group
|
|
59
|
+
* co-members. The recipient verifies the token's signature and stores a
|
|
60
|
+
* held row only when the token's audience (`aud`) is the receiving
|
|
61
|
+
* device, so the engine's auto-attach can reuse it on later mutations.
|
|
62
|
+
*/
|
|
63
|
+
type: 'delegation:share';
|
|
64
|
+
/** Stringified capability JWT carrying the `document/write` grant. */
|
|
65
|
+
token: string;
|
|
66
|
+
/** Group whose broadcast channel carries this grant. */
|
|
67
|
+
groupID: string;
|
|
68
|
+
/** HLC stamped by the grantor; mirrors the grantor-side issued row. */
|
|
69
|
+
hlc: string;
|
|
70
|
+
} | {
|
|
71
|
+
/**
|
|
72
|
+
* Distributes a signed revocation record for a previously minted
|
|
73
|
+
* `document/write` capability. The recipient verifies the token's
|
|
74
|
+
* signature and stores the revocation; verification against the
|
|
75
|
+
* referenced cap's `iss` flips `verified_at` once the cap is known.
|
|
76
|
+
* Revocation is binary and reactive — once verified locally, any
|
|
77
|
+
* subsequent mutation under the revoked `jti` is denied regardless
|
|
78
|
+
* of `mutation.atTime`.
|
|
79
|
+
*/
|
|
80
|
+
type: 'delegation:revoke';
|
|
81
|
+
/** Stringified signed revocation JWT carrying `{ jti, iss, rev, iat }`. */
|
|
82
|
+
token: string;
|
|
83
|
+
/** Group whose broadcast channel carries this revocation. */
|
|
84
|
+
groupID: string;
|
|
85
|
+
/** HLC stamped by the revoker; mirrors the revoker-side stored row. */
|
|
86
|
+
hlc: string;
|
|
44
87
|
} | {
|
|
45
88
|
type: 'group:update';
|
|
46
89
|
groupID: string;
|
|
47
90
|
update: {
|
|
48
91
|
name?: string;
|
|
49
92
|
description?: string;
|
|
50
|
-
hubURLs?: Array<string>;
|
|
51
93
|
hlc: string;
|
|
52
94
|
};
|
|
95
|
+
} | {
|
|
96
|
+
/**
|
|
97
|
+
* A member announces a voluntary leave. MLS forbids self-removal, so this
|
|
98
|
+
* is an advisory notification (not an authoritative removal): receivers
|
|
99
|
+
* surface a `groupLeaveRequested` event for an admin to act on by issuing
|
|
100
|
+
* a real `removeMember`. Deliberately does NOT tombstone — a forged
|
|
101
|
+
* leave-request must not be able to evict an arbitrary member.
|
|
102
|
+
*/
|
|
103
|
+
type: 'group:leaveRequest';
|
|
104
|
+
groupID: string;
|
|
105
|
+
memberDID: string;
|
|
106
|
+
hlc: string;
|
|
107
|
+
} | {
|
|
108
|
+
type: 'mutation:apply';
|
|
109
|
+
entries: Array<MutationApplyEntry>;
|
|
110
|
+
/** DID of the peer that authored this broadcast — used for loopback drop. */
|
|
111
|
+
senderPeerDID: string;
|
|
112
|
+
};
|
|
113
|
+
export type AffectedRow = {
|
|
114
|
+
kind: 'circle';
|
|
115
|
+
row: Circle;
|
|
116
|
+
} | {
|
|
117
|
+
kind: 'member';
|
|
118
|
+
row: CircleMember;
|
|
119
|
+
} | {
|
|
120
|
+
kind: 'catalog';
|
|
121
|
+
row: Catalog;
|
|
122
|
+
} | {
|
|
123
|
+
kind: 'group';
|
|
124
|
+
row: Group;
|
|
125
|
+
};
|
|
126
|
+
export type ProcessBroadcastResult = {
|
|
127
|
+
applied: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Row snapshot. After-state for create / update, before-state for
|
|
130
|
+
* delete / remove. Undefined when `applied === false`.
|
|
131
|
+
*/
|
|
132
|
+
affected?: AffectedRow;
|
|
133
|
+
/**
|
|
134
|
+
* Count of `mutation:apply` entries denied by the receive-time access
|
|
135
|
+
* gate. Only set on the `mutation:apply` branch; other branches do not
|
|
136
|
+
* gate. Always `0` when `storeUnreadable === 'persist'` (the default).
|
|
137
|
+
*/
|
|
138
|
+
dropped?: number;
|
|
139
|
+
/**
|
|
140
|
+
* Count of forwarded broadcast targets across all `mutation:apply`
|
|
141
|
+
* entries. Only set on the `mutation:apply` branch when `forwarding` is
|
|
142
|
+
* configured and not `false`. `0` when forwarding is disabled or no
|
|
143
|
+
* candidate group survived the gates.
|
|
144
|
+
*/
|
|
145
|
+
forwarded?: number;
|
|
146
|
+
/**
|
|
147
|
+
* Count of `mutation:apply` entries where `recordBroadcast` returned
|
|
148
|
+
* `false` (already-recorded duplicate). Only set on the `mutation:apply`
|
|
149
|
+
* branch; other branches do not call `recordBroadcast`. Always defined
|
|
150
|
+
* (possibly `0`) on `mutation:apply`.
|
|
151
|
+
*/
|
|
152
|
+
duplicates?: number;
|
|
53
153
|
};
|
|
54
154
|
export type ProcessBroadcastParams = {
|
|
55
155
|
p2pStore: P2PStoreAPI;
|
|
56
156
|
graphStore: GraphStoreAPI;
|
|
157
|
+
/**
|
|
158
|
+
* Engine graph internals — required to apply `mutation:apply` entries via
|
|
159
|
+
* {@link GraphInternals.applyVerifiedMutation} with `origin: 'peer'`, which
|
|
160
|
+
* fires engine lifecycle events (document:saved, engine:document:*,
|
|
161
|
+
* engine:mutation:received) so GraphQL subscribers see peer-authored docs.
|
|
162
|
+
*/
|
|
163
|
+
graph: GraphInternals;
|
|
164
|
+
/** Self DID for `mutation:apply` loopback detection. */
|
|
165
|
+
selfDID: string;
|
|
166
|
+
/**
|
|
167
|
+
* Receiver storage mode for incoming `mutation:apply` entries.
|
|
168
|
+
* - `'persist'` (default) — every entry the engine accepts is persisted;
|
|
169
|
+
* access enforcement is left for query time.
|
|
170
|
+
* - `'drop'` — apply a receive-time read-access check using `selfDID` as
|
|
171
|
+
* viewer; entries the local peer cannot read are not persisted.
|
|
172
|
+
* Only consulted on the `mutation:apply` branch — control-plane variants
|
|
173
|
+
* are unaffected.
|
|
174
|
+
*/
|
|
175
|
+
storeUnreadable?: StoreUnreadableMode;
|
|
176
|
+
/**
|
|
177
|
+
* Server default access level. Required when `storeUnreadable === 'drop'`
|
|
178
|
+
* to feed the access-rule resolver. Ignored under `'persist'`.
|
|
179
|
+
*/
|
|
180
|
+
defaultAccessLevel?: DefaultAccessLevel;
|
|
181
|
+
/**
|
|
182
|
+
* Forwarding config. When omitted or `false` the forwarding pipeline is
|
|
183
|
+
* skipped entirely. When `true` or a {@link ForwardFilter}, applied
|
|
184
|
+
* entries are evaluated for re-broadcast to
|
|
185
|
+
* other MLS groups the local peer is a member of (excluding the source
|
|
186
|
+
* group), gated by access + catalog + dedup.
|
|
187
|
+
*/
|
|
188
|
+
forwarding?: ForwardingConfig;
|
|
189
|
+
/**
|
|
190
|
+
* Hub forwarding hook — required when `forwarding` is set. Called once per
|
|
191
|
+
* surviving candidate group with a single-entry `mutation:apply` broadcast
|
|
192
|
+
* carrying `senderPeerDID = selfDID`.
|
|
193
|
+
*/
|
|
194
|
+
scheduleBroadcast?: (groupID: string, message: GroupBroadcastMessage) => void | Promise<void>;
|
|
195
|
+
/**
|
|
196
|
+
* Emitter forwarded to `storeReceivedGrant` on the `delegation:share` branch
|
|
197
|
+
* so consumers of `ownDelegationTokenAdded` see broadcast-delivered grants.
|
|
198
|
+
* Absent in tests that don't exercise the subscription path.
|
|
199
|
+
*/
|
|
200
|
+
emitter?: P2PEventEmitter;
|
|
201
|
+
/** Optional logger — warns on per-entry apply failures. */
|
|
202
|
+
logger?: Logger;
|
|
57
203
|
};
|
|
58
204
|
/**
|
|
59
205
|
* Process a received broadcast message, applying it to the local database
|
|
60
206
|
* with HLC-based last-write-wins conflict resolution.
|
|
61
207
|
*
|
|
62
|
-
* Returns
|
|
208
|
+
* Returns `{ applied, affected? }`. `applied` is true when the message
|
|
209
|
+
* mutated local state. `affected` carries the row snapshot needed by
|
|
210
|
+
* downstream event mapping: after-state for create / update, before-state
|
|
211
|
+
* for delete / remove.
|
|
212
|
+
*
|
|
213
|
+
* Readback note: for create / update the post-apply row is fetched with a
|
|
214
|
+
* separate `get*` call rather than a txn-scoped read. If an unrelated
|
|
215
|
+
* concurrent write to the same row lands between apply and readback, the
|
|
216
|
+
* emitted `affected.row` reflects that later write. `BroadcastService`
|
|
217
|
+
* serializes receives, so the realistic exposure is only a same-row local
|
|
218
|
+
* mutation between apply and readback, which is acceptable for event
|
|
219
|
+
* payload fidelity.
|
|
63
220
|
*/
|
|
64
|
-
export declare function processBroadcast(params: ProcessBroadcastParams, message: GroupBroadcastMessage): Promise<
|
|
221
|
+
export declare function processBroadcast(params: ProcessBroadcastParams, message: GroupBroadcastMessage, groupID: string): Promise<ProcessBroadcastResult>;
|
package/lib/groups/broadcast.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{HLC as e}from"@kubun/hlc";function
|
|
1
|
+
import{HLC as e}from"@kubun/hlc";import{evaluateAndForward as a,rethrowIfProgrammerError as r}from"../sync/forwarder.js";import{createReceiveAccessGate as l}from"../sync/receive-access-gate.js";import{storeReceivedGrant as t}from"./store-received-grant.js";import{storeReceivedRevocation as i}from"./store-received-revocation.js";function c(a,r){return e.compare(e.parse(a),e.parse(r))>0}export async function processBroadcast(e,o,d){let{p2pStore:n,graphStore:p}=e;switch(o.type){case"circle:create":{let e=await n.getCircle(o.circle.id);if(null!=e){if(!c(o.circle.hlc,e.hlc))return{applied:!1};await n.updateCircle(o.circle.id,{name:o.circle.name,description:o.circle.description,catalog_ids:o.circle.catalogIDs,hlc:o.circle.hlc});let a=await n.getCircle(o.circle.id);return null!=a?{applied:!0,affected:{kind:"circle",row:a}}:{applied:!0}}await n.createCircle({id:o.circle.id,group_id:o.circle.groupID,name:o.circle.name,description:o.circle.description,catalog_ids:o.circle.catalogIDs,hlc:o.circle.hlc});let a=await n.getCircle(o.circle.id);return null!=a?{applied:!0,affected:{kind:"circle",row:a}}:{applied:!0}}case"circle:update":{let e=await n.getCircle(o.circleID);if(null==e||!c(o.update.hlc,e.hlc))return{applied:!1};await n.updateCircle(o.circleID,{name:o.update.name,description:o.update.description,catalog_ids:o.update.catalogIDs,hlc:o.update.hlc});let a=await n.getCircle(o.circleID);return null!=a?{applied:!0,affected:{kind:"circle",row:a}}:{applied:!0}}case"circle:delete":{let e=await n.getCircle(o.circleID);if(null==e||!c(o.hlc,e.hlc))return{applied:!1};return await n.deleteCircle(o.circleID),{applied:!0,affected:{kind:"circle",row:e}}}case"member:add":{await n.addCircleMember({circle_id:o.member.circleID,member_did:o.member.memberDID,role:o.member.role,hlc:o.member.hlc});let e=await n.getCircleMember(o.member.circleID,o.member.memberDID);return null!=e?{applied:!0,affected:{kind:"member",row:e}}:{applied:!0}}case"member:remove":{let e=await n.getCircleMember(o.circleID,o.memberDID);return await n.removeCircleMember(o.circleID,o.memberDID),null!=e?{applied:!0,affected:{kind:"member",row:e}}:{applied:!0}}case"catalog:create":{let e=await p.getCatalog(o.catalog.id);if(null!=e){if(!c(o.catalog.hlc,e.hlc))return{applied:!1};await p.updateCatalog(o.catalog.id,{name:o.catalog.name,description:o.catalog.description,filter_criteria:o.catalog.filterCriteria,hlc:o.catalog.hlc});let a=await p.getCatalog(o.catalog.id);return null!=a?{applied:!0,affected:{kind:"catalog",row:a}}:{applied:!0}}await p.createCatalog({id:o.catalog.id,owner_did:o.catalog.ownerDID,name:o.catalog.name,description:o.catalog.description,filter_criteria:o.catalog.filterCriteria,hlc:o.catalog.hlc});let a=await p.getCatalog(o.catalog.id);return null!=a?{applied:!0,affected:{kind:"catalog",row:a}}:{applied:!0}}case"catalog:update":{let e=await p.getCatalog(o.catalogID);if(null==e||!c(o.update.hlc,e.hlc))return{applied:!1};await p.updateCatalog(o.catalogID,{name:o.update.name,description:o.update.description,filter_criteria:o.update.filterCriteria,hlc:o.update.hlc});let a=await p.getCatalog(o.catalogID);return null!=a?{applied:!0,affected:{kind:"catalog",row:a}}:{applied:!0}}case"catalog:delete":{let e=await p.getCatalog(o.catalogID);if(null==e||!c(o.hlc,e.hlc))return{applied:!1};return await p.deleteCatalog(o.catalogID),{applied:!0,affected:{kind:"catalog",row:e}}}case"delegation:share":return{applied:await t({p2pStore:n,token:o.token,groupID:o.groupID,hlc:o.hlc,selfDID:e.selfDID,...null!=e.emitter?{emitter:e.emitter}:{},...null!=e.logger?{logger:e.logger}:{}})};case"delegation:revoke":return{applied:await i({p2pStore:n,token:o.token,groupID:o.groupID,hlc:o.hlc,...null!=e.selfDID?{selfDID:e.selfDID}:{},...null!=e.emitter?{emitter:e.emitter}:{},...null!=e.logger?{logger:e.logger}:{}})};case"group:update":{let e=await n.getGroup(o.groupID);if(null==e||!c(o.update.hlc,e.hlc))return{applied:!1};await n.updateGroup(o.groupID,{name:o.update.name,description:o.update.description,hlc:o.update.hlc});let a=await n.getGroup(o.groupID);return null!=a?{applied:!0,affected:{kind:"group",row:a}}:{applied:!0}}case"group:leaveRequest":return await e.emitter?.emit("groupLeaveRequested",{groupID:o.groupID,memberDID:o.memberDID,role:"",createdAt:new Date().toISOString()}),{applied:!0};case"mutation:apply":{if(o.senderPeerDID===e.selfDID)return{applied:!1};let t="drop"===(e.storeUnreadable??"persist")?l({selfDID:e.selfDID,db:{getUserModelAccessDefault:(e,a,r)=>p.getUserModelAccessDefault(e,a,r),isMemberOfAnyCircle:(e,a)=>n.isMemberOfAnyCircle(e,a),isMemberOfAnyGroup:(e,a)=>n.isMemberOfAnyGroup(e,a),getModelInterfaces:e=>p.getModelInterfaces(e)},defaultAccessLevel:e.defaultAccessLevel??{read:"only_owner",write:"only_owner"}}):void 0,i=null!=e.forwarding&&!1!==e.forwarding&&null!=e.scheduleBroadcast,c=!1,u=0,g=0,s=0;for(let l of o.entries)try{let o=await e.graph.applyVerifiedMutation({token:l.mutationJWT,origin:"peer",arrivalGroupID:d,...null!=t?{accessGate:t}:{}});if(o.dropped){u++;continue}let m=!1;try{let e=await n.recordBroadcast({docID:l.docID,version:l.version,mlsGroupID:d});m=!0,!e&&s++}catch(a){r(a),e.logger?.warn("mutation:apply recordBroadcast failed, skipping forward",{docID:l.docID,version:l.version,groupID:d,error:a})}if(c=!0,!m)continue;if(i&&null!=e.forwarding&&null!=e.scheduleBroadcast&&null!=o.document)try{let r=await a({entry:l,postState:o.document,sourceGroupID:d,selfDID:e.selfDID,p2pStore:n,graphStore:p,defaultAccessLevel:e.defaultAccessLevel??{read:"only_owner",write:"only_owner"},forwarding:e.forwarding,scheduleBroadcast:e.scheduleBroadcast,logger:e.logger});g+=r.forwarded}catch(a){r(a),e.logger?.warn("mutation:apply forwarding failed, skipping entry",{docID:l.docID,version:l.version,groupID:d,error:a})}}catch(a){r(a),e.logger?.warn("mutation:apply entry failed, skipping",{docID:l.docID,version:l.version,modelID:l.modelID,groupID:d,error:a})}let m={applied:c,dropped:u,duplicates:s};return i&&(m.forwarded=g),m}}}
|
package/lib/groups/events.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from '@enkaku/event';
|
|
2
|
-
import type { CircleData, CircleMemberData, GroupData, GroupMemberData } from '../
|
|
3
|
-
export type
|
|
2
|
+
import type { CircleData, CircleMemberData, DelegationData, GroupData, GroupMemberData, RevokedDelegationData } from '../types.js';
|
|
3
|
+
export type P2PEventMap = {
|
|
4
4
|
groupJoined: GroupData;
|
|
5
5
|
groupLeft: GroupData & {
|
|
6
6
|
groupID: string;
|
|
@@ -14,6 +14,14 @@ export type GroupEventMap = {
|
|
|
14
14
|
groupMemberLeft: GroupMemberData & {
|
|
15
15
|
groupID: string;
|
|
16
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* A member announced a voluntary leave (received `group:leaveRequest`).
|
|
19
|
+
* Advisory: MLS forbids self-removal, so the leaf lingers until an admin
|
|
20
|
+
* issues a real `removeMember`. Admin apps may act on this; not auto-evicted.
|
|
21
|
+
*/
|
|
22
|
+
groupLeaveRequested: GroupMemberData & {
|
|
23
|
+
groupID: string;
|
|
24
|
+
};
|
|
17
25
|
circleCreated: CircleData & {
|
|
18
26
|
groupID: string;
|
|
19
27
|
};
|
|
@@ -32,7 +40,20 @@ export type GroupEventMap = {
|
|
|
32
40
|
circleCatalogsChanged: CircleData & {
|
|
33
41
|
circleID: string;
|
|
34
42
|
};
|
|
43
|
+
hubBound: {
|
|
44
|
+
groupID: string;
|
|
45
|
+
hubID: string;
|
|
46
|
+
hubURL: string;
|
|
47
|
+
serverDID: string | null;
|
|
48
|
+
};
|
|
49
|
+
hubUnbound: {
|
|
50
|
+
groupID: string;
|
|
51
|
+
hubID: string;
|
|
52
|
+
hubURL: string;
|
|
53
|
+
};
|
|
54
|
+
delegationTokenAdded: DelegationData;
|
|
55
|
+
delegationTokenRevoked: RevokedDelegationData;
|
|
35
56
|
};
|
|
36
|
-
export type
|
|
37
|
-
export declare function
|
|
38
|
-
export declare function createFilteredGenerator<K extends keyof
|
|
57
|
+
export type P2PEventEmitter = EventEmitter<P2PEventMap>;
|
|
58
|
+
export declare function createP2PEventEmitter(): P2PEventEmitter;
|
|
59
|
+
export declare function createFilteredGenerator<K extends keyof P2PEventMap>(emitter: P2PEventEmitter, eventName: K, filter?: (data: P2PEventMap[K]) => boolean): AsyncGenerator<P2PEventMap[K], void, void>;
|
package/lib/groups/events.js
CHANGED
|
@@ -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 createP2PEventEmitter(){return new e}export function createFilteredGenerator(e,t,n){return r(e,t,{filter:n})}
|