@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.
Files changed (88) hide show
  1. package/lib/context/delegation.d.ts +4 -0
  2. package/lib/context/delegation.js +1 -0
  3. package/lib/context/group.js +1 -1
  4. package/lib/context/hub.d.ts +4 -0
  5. package/lib/context/hub.js +1 -0
  6. package/lib/context/join.js +1 -1
  7. package/lib/context/types.d.ts +28 -2
  8. package/lib/groups/broadcast-service.d.ts +130 -17
  9. package/lib/groups/broadcast-service.js +1 -1
  10. package/lib/groups/broadcast.d.ts +162 -5
  11. package/lib/groups/broadcast.js +1 -1
  12. package/lib/groups/events.d.ts +26 -5
  13. package/lib/groups/events.js +1 -1
  14. package/lib/groups/group-handle-registry.d.ts +90 -0
  15. package/lib/groups/group-handle-registry.js +1 -0
  16. package/lib/groups/invite-payload.d.ts +21 -1
  17. package/lib/groups/join-utils.d.ts +68 -2
  18. package/lib/groups/join-utils.js +1 -1
  19. package/lib/groups/manager.d.ts +26 -18
  20. package/lib/groups/manager.js +1 -1
  21. package/lib/groups/mls-codec.d.ts +7 -0
  22. package/lib/groups/mls-codec.js +1 -0
  23. package/lib/groups/mls-encryptor.d.ts +25 -0
  24. package/lib/groups/mls-encryptor.js +1 -0
  25. package/lib/groups/store-received-grant.d.ts +47 -0
  26. package/lib/groups/store-received-grant.js +1 -0
  27. package/lib/groups/store-received-revocation.d.ts +46 -0
  28. package/lib/groups/store-received-revocation.js +1 -0
  29. package/lib/groups/wire-frame.d.ts +32 -0
  30. package/lib/groups/wire-frame.js +1 -0
  31. package/lib/hub/connection-pool.d.ts +24 -0
  32. package/lib/hub/connection-pool.js +1 -1
  33. package/lib/hub/did-observing-transport.d.ts +64 -0
  34. package/lib/hub/did-observing-transport.js +1 -0
  35. package/lib/hub/errors.d.ts +28 -0
  36. package/lib/hub/errors.js +1 -0
  37. package/lib/hub/forward-remote-broadcast.d.ts +15 -0
  38. package/lib/hub/forward-remote-broadcast.js +1 -0
  39. package/lib/hub/group-channel.d.ts +37 -21
  40. package/lib/hub/group-channel.js +1 -1
  41. package/lib/hub/http-client.d.ts +17 -0
  42. package/lib/hub/http-client.js +1 -0
  43. package/lib/hub/hub-connection.d.ts +96 -0
  44. package/lib/hub/hub-connection.js +1 -0
  45. package/lib/hub/manager.d.ts +117 -0
  46. package/lib/hub/manager.js +1 -0
  47. package/lib/hub/receive-handler.d.ts +3 -6
  48. package/lib/hub/receive-handler.js +1 -1
  49. package/lib/hub/relay-manager.d.ts +92 -2
  50. package/lib/hub/relay-manager.js +1 -1
  51. package/lib/hub/send-handler.d.ts +28 -16
  52. package/lib/hub/send-handler.js +1 -1
  53. package/lib/hub/tunnel-inbox.d.ts +20 -0
  54. package/lib/hub/tunnel-inbox.js +1 -0
  55. package/lib/hub/wait-for-gate.d.ts +14 -0
  56. package/lib/hub/wait-for-gate.js +1 -0
  57. package/lib/hub/wiring.d.ts +85 -0
  58. package/lib/hub/wiring.js +1 -0
  59. package/lib/index.d.ts +102 -7
  60. package/lib/index.js +1 -1
  61. package/lib/schema.d.ts +2 -2
  62. package/lib/schema.js +113 -21
  63. package/lib/sync/broadcast-queue.d.ts +59 -0
  64. package/lib/sync/broadcast-queue.js +1 -0
  65. package/lib/sync/broadcast-sender.d.ts +52 -0
  66. package/lib/sync/broadcast-sender.js +1 -0
  67. package/lib/sync/forwarder.d.ts +97 -0
  68. package/lib/sync/forwarder.js +1 -0
  69. package/lib/sync/handlers.d.ts +20 -1
  70. package/lib/sync/handlers.js +1 -1
  71. package/lib/sync/hub-tunnel-sync-listener.d.ts +24 -0
  72. package/lib/sync/hub-tunnel-sync-listener.js +1 -0
  73. package/lib/sync/hub-tunnel-sync-provider.d.ts +36 -0
  74. package/lib/sync/hub-tunnel-sync-provider.js +1 -0
  75. package/lib/sync/merkle-apply.d.ts +41 -0
  76. package/lib/sync/merkle-apply.js +1 -1
  77. package/lib/sync/merkle-channel.d.ts +15 -0
  78. package/lib/sync/merkle-channel.js +1 -1
  79. package/lib/sync/receive-access-gate.d.ts +36 -0
  80. package/lib/sync/receive-access-gate.js +1 -0
  81. package/lib/sync/scope-resolver.d.ts +32 -0
  82. package/lib/sync/scope-resolver.js +1 -0
  83. package/lib/sync/sync-manager.d.ts +10 -0
  84. package/lib/sync/sync-manager.js +1 -1
  85. package/lib/types.d.ts +151 -1
  86. package/lib/util/mutex.d.ts +4 -0
  87. package/lib/util/mutex.js +1 -0
  88. 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}}
@@ -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,4 @@
1
+ import type { ExecutionContext } from '@kubun/engine';
2
+ import type { HubRequestContext } from '../types.js';
3
+ import type { ContextDeps } from './types.js';
4
+ export declare function createHubContext(_ctx: ExecutionContext, deps: ContextDeps): HubRequestContext;
@@ -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)}))}}}
@@ -1 +1 @@
1
- import{createKeyPackageBundle as e}from"@enkaku/group";import{decodeInvitePayload as t,encodeJoinRequest as i}from"../groups/invite-payload.js";import{finalizeJoinedGroup as r}from"../groups/join-utils.js";import{checkPeerAccess as a}from"./types.js";export function createJoinContext(o,n){return{prepareRequest: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})}},complete: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,emitter:n.emitter,invite:s});return n.pendingJoinRequests.delete(o.viewerDID),c}}}
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}}}
@@ -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 { GroupEventEmitter } from '../groups/events.js';
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: GroupEventEmitter;
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 { type GroupBroadcastMessage } from './broadcast.js';
5
- import { type SerializedMLSGroupState } from './mls-state.js';
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
- groupState: SerializedMLSGroupState;
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 SendBroadcastResult = {
38
- encrypted: Uint8Array;
39
- updatedGroupState: SerializedMLSGroupState;
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 ReceiveBroadcastResult = {
86
+ export type ReceiveAppResult = {
87
+ kind: 'app';
47
88
  message: GroupBroadcastMessage;
48
89
  applied: boolean;
49
- updatedGroupState: SerializedMLSGroupState;
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
- * It does NOT manage hub transport directly -- callers are responsible
56
- * for sending encrypted bytes via hub and delivering received bytes
57
- * from hub. This keeps the service testable without hub infrastructure.
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
- * Prepare a broadcast for sending: serialize + MLS encrypt.
68
- * Returns encrypted bytes to send via hub and updated MLS state to persist.
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
- prepareSend(params: SendBroadcastParams): Promise<SendBroadcastResult>;
182
+ prepareSendCommit(params: SendCommitParams): Promise<void>;
71
183
  /**
72
- * Process a received broadcast: MLS decrypt + deserialize + apply to DB.
73
- * Returns the decoded message, whether it was applied, and updated MLS state.
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{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
+ 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 true if the message was applied, false if it was skipped (stale HLC or missing target).
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<boolean>;
221
+ export declare function processBroadcast(params: ProcessBroadcastParams, message: GroupBroadcastMessage, groupID: string): Promise<ProcessBroadcastResult>;
@@ -1 +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}}}
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}}}
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from '@enkaku/event';
2
- import type { CircleData, CircleMemberData, GroupData, GroupMemberData } from '../index.js';
3
- export type GroupEventMap = {
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 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>;
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>;
@@ -1 +1 @@
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})}
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})}