@kubun/plugin-p2p 0.9.0 → 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 (49) 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/join.js +1 -1
  5. package/lib/context/types.d.ts +16 -2
  6. package/lib/groups/broadcast-service.d.ts +60 -1
  7. package/lib/groups/broadcast-service.js +1 -1
  8. package/lib/groups/broadcast.d.ts +50 -0
  9. package/lib/groups/broadcast.js +1 -1
  10. package/lib/groups/events.d.ts +15 -5
  11. package/lib/groups/events.js +1 -1
  12. package/lib/groups/group-handle-registry.js +1 -1
  13. package/lib/groups/invite-payload.d.ts +16 -0
  14. package/lib/groups/join-utils.d.ts +68 -2
  15. package/lib/groups/join-utils.js +1 -1
  16. package/lib/groups/manager.d.ts +6 -4
  17. package/lib/groups/manager.js +1 -1
  18. package/lib/groups/store-received-grant.d.ts +47 -0
  19. package/lib/groups/store-received-grant.js +1 -0
  20. package/lib/groups/store-received-revocation.d.ts +46 -0
  21. package/lib/groups/store-received-revocation.js +1 -0
  22. package/lib/groups/wire-frame.d.ts +32 -0
  23. package/lib/groups/wire-frame.js +1 -0
  24. package/lib/hub/forward-remote-broadcast.d.ts +3 -3
  25. package/lib/hub/forward-remote-broadcast.js +1 -1
  26. package/lib/hub/group-channel.d.ts +8 -0
  27. package/lib/hub/group-channel.js +1 -1
  28. package/lib/hub/hub-connection.js +1 -1
  29. package/lib/hub/receive-handler.js +1 -1
  30. package/lib/hub/relay-manager.d.ts +7 -0
  31. package/lib/hub/relay-manager.js +1 -1
  32. package/lib/hub/send-handler.d.ts +16 -0
  33. package/lib/hub/send-handler.js +1 -1
  34. package/lib/hub/wiring.d.ts +14 -2
  35. package/lib/hub/wiring.js +1 -1
  36. package/lib/index.d.ts +1 -2
  37. package/lib/index.js +1 -1
  38. package/lib/schema.d.ts +2 -2
  39. package/lib/schema.js +47 -3
  40. package/lib/sync/broadcast-sender.js +1 -1
  41. package/lib/sync/forwarder.d.ts +1 -0
  42. package/lib/sync/forwarder.js +1 -1
  43. package/lib/sync/handlers.d.ts +5 -1
  44. package/lib/sync/handlers.js +1 -1
  45. package/lib/sync/merkle-apply.js +1 -1
  46. package/lib/types.d.ts +92 -1
  47. package/package.json +40 -40
  48. package/lib/sync/catalog-match.d.ts +0 -13
  49. package/lib/sync/catalog-match.js +0 -1
@@ -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 r}from"@kubun/store-graph";import{getP2PStore as t}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 l}from"../groups/join-utils.js";import{checkPeerAccess as d,toISO as n}from"./types.js";export function createGroupContext(s,m){let p=null,u=null,g=()=>p??=t(m.stores),w=()=>u??=r(m.stores);return{create:async e=>{let r=await m.groupManager.createGroup({stores:m.stores,identity:m.identity,name:e.name,description:e.description,createdBy:s.viewerDID,hubs:e.hubs?.map(e=>({url:e.url,serverDID:e.serverDID??void 0}))}),t=await g(),a=await t.getGroup(r.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:n(a.created_at)};return await m.emitter.emit("groupJoined",i),i},list:async()=>{let e=await g();return(await e.listGroups()).map(e=>({id:e.id,name:e.name,description:e.description,createdBy:e.created_by,createdAt:n(e.created_at)}))},get:async e=>{let r=await g(),t=await r.getGroup(e);return null==t?null:{id:t.id,name:t.name,description:t.description,createdBy:t.created_by,createdAt:n(t.created_at)}},addMember:async(e,r,t)=>{let a=await g();await a.addGroupMember({group_id:e,member_did:r,role:t});let i=await a.getGroupMember(e,r);if(null==i)throw Error("Failed to add group member");let o={groupID:i.group_id,memberDID:i.member_did,role:i.role,createdAt:n(i.created_at)};return await m.emitter.emit("groupMemberJoined",{...o,groupID:o.groupID}),o},listMembers:async e=>{let r=await g();return(await r.listGroupMembers(e)).map(e=>({groupID:e.group_id,memberDID:e.member_did,role:e.role,createdAt:n(e.created_at)}))},createCircle:async e=>{let r=await m.groupManager.createCircle({stores:m.stores,groupID:e.groupID,name:e.name,description:e.description}),t=await g(),a=await t.getCircle(r.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:n(a.created_at)};return await m.emitter.emit("circleCreated",{...i,groupID:i.groupID}),m.stores.onCommit(()=>m.scheduleBroadcast(a.group_id,{type:"circle:create",circle:{id:a.id,groupID:a.group_id,name:a.name,description:a.description,catalogIDs:a.catalog_ids,hlc:a.hlc}})),i},listCircles:async e=>{let r=await g();return(await r.listCirclesByGroup(e)).map(e=>({id:e.id,groupID:e.group_id,name:e.name,description:e.description,createdAt:n(e.created_at)}))},addCircleMember:async(r,t,a)=>{let i=await g(),o=e.serialize(m.hlc.now());await i.addCircleMember({circle_id:r,member_did:t,role:a,hlc:o});let c=(await i.listCircleMembers(r)).find(e=>e.member_did===t);if(null==c)throw Error("Failed to add circle member");let l={circleID:c.circle_id,memberDID:c.member_did,role:c.role,createdAt:n(c.created_at)};await m.emitter.emit("circleMemberAdded",{...l,circleID:l.circleID});let d=await i.getCircle(r);if(null!=d)if("admin"===c.role||"member"===c.role){let e=c.role;m.stores.onCommit(()=>m.scheduleBroadcast(d.group_id,{type:"member:add",member:{circleID:c.circle_id,memberDID:c.member_did,role:e,hlc:c.hlc}}))}else m.logger.warn("addCircleMember: skipping broadcast for unrecognized role",{circleID:r,memberDID:t,role:c.role});return l},createCatalog:async r=>{let t=await w(),a=m.runtime.getRandomID(),i=e.serialize(m.hlc.now());await t.createCatalog({id:a,owner_did:s.viewerDID,name:r.name,description:r.description??"",filter_criteria:JSON.parse(r.filterCriteria),hlc:i});let o=await t.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:n(o.created_at)}},listCatalogs:async e=>{let r=await w();if(null!=e)return(await r.listCatalogs(e)).map(e=>({id:e.id,ownerDID:e.owner_did,name:e.name,description:e.description,filterCriteria:JSON.stringify(e.filter_criteria),createdAt:n(e.created_at)}));let t=await g(),a=new Set((await t.listGroups()).map(e=>e.created_by));return(await Promise.all(Array.from(a).map(e=>r.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:n(e.created_at)}))},invite:async(e,r)=>{d(s.viewerDID,m.identity.id,m.autoAcceptPeers);let t=await g(),a=m.identity,i=o(r),l=await m.groupManager.inviteToGroup({stores:m.stores,groupID:e,identity:a,recipientDID:i.did,recipientKeyPackage:i.publicPackage,permission:"member"}),n=await t.getGroup(e),p=(await t.listHubsByGroupID(e)).map(e=>null==e.server_did?{url:e.url}:{url:e.url,serverDID:e.server_did}),u=c({groupID:e,groupName:n?.name??"",suggestedHubs:p,invite:l.invite,welcomeMessage:l.welcomeMessage,ratchetTree:l.ratchetTree});return await m.emitter.emit("groupMemberJoined",{groupID:e,memberDID:i.did,role:"member",createdAt:new Date().toISOString()}),{invitePayload:u,groupID:e,groupName:n?.name??""}},join:async(e,r)=>{let t=m.identity,o=i(e),c=a(r),d={publicPackage:c.publicPackage,privatePackage:c.privatePackage,ownerDID:c.did};return await m.groupManager.joinGroup({stores:m.stores,identity:t,groupID:o.groupID,groupName:o.groupName,hubs:o.suggestedHubs,invite:o.invite,keyPackageBundle:d,welcomeMessage:o.welcomeMessage,ratchetTree:o.ratchetTree}),await l({stores:m.stores,emitter:m.emitter,invite:o})},leave:async e=>{let r=await g(),t=await r.getGroup(e),a=m.identity;return await m.groupManager.leaveGroup({stores:m.stores,groupID:e,identity:a}),null!=t&&await m.emitter.emit("groupLeft",{id:t.id,name:t.name,description:t.description,createdBy:t.created_by,createdAt:n(t.created_at),groupID:e}),{groupID:e}},removeMember:async(e,r)=>(await m.groupManager.removeGroupMember({stores:m.stores,groupID:e,memberDID:r}),await m.emitter.emit("groupMemberLeft",{groupID:e,memberDID:r,role:"",createdAt:new Date().toISOString()}),{groupID:e}),update:async e=>{await m.groupManager.updateGroup({stores:m.stores,groupID:e.groupID,update:{name:e.name,description:e.description}});let r=await g(),t=await r.getGroup(e.groupID);if(null==t)throw Error(`Group ${e.groupID} not found`);let a={id:t.id,name:t.name,description:t.description,createdBy:t.created_by,createdAt:n(t.created_at)};return await m.emitter.emit("groupDataChanged",{...a,groupID:e.groupID}),m.stores.onCommit(()=>m.scheduleBroadcast(t.id,{type:"group:update",groupID:t.id,update:{name:e.name,description:e.description,hlc:t.hlc}})),a},updateCircle:async e=>{await m.groupManager.updateCircle({stores:m.stores,circleID:e.circleID,update:{name:e.name,description:e.description,catalogIDs:e.catalogIDs}});let r=await g(),t=await r.getCircle(e.circleID);if(null==t)throw Error(`Circle ${e.circleID} not found`);let a={id:t.id,groupID:t.group_id,name:t.name,description:t.description,createdAt:n(t.created_at)};return null!=e.catalogIDs?await m.emitter.emit("circleCatalogsChanged",{...a,circleID:e.circleID}):await m.emitter.emit("circleDataChanged",{...a,circleID:e.circleID}),m.stores.onCommit(()=>m.scheduleBroadcast(t.group_id,{type:"circle:update",circleID:t.id,update:{name:e.name,description:e.description,catalogIDs:e.catalogIDs,hlc:t.hlc}})),a},updateCatalog:async r=>{let t=await w(),a={hlc:e.serialize(m.hlc.now())};null!=r.name&&(a.name=r.name),null!=r.filterCriteria&&(a.filter_criteria=JSON.parse(r.filterCriteria)),await t.updateCatalog(r.catalogID,a);let i=await t.getCatalog(r.catalogID);if(null==i)throw Error(`Catalog ${r.catalogID} not found`);return{id:i.id,ownerDID:i.owner_did,name:i.name,description:i.description,filterCriteria:JSON.stringify(i.filter_criteria),createdAt:n(i.created_at)}},deleteCatalog:async e=>{let r=await w();return await r.deleteCatalog(e),{deletedID:e}},deleteCircle:async r=>{let t=await g(),a=await t.getCircle(r);if(await m.groupManager.deleteCircle({stores:m.stores,circleID:r}),null!=a){await m.emitter.emit("circleDeleted",{id:a.id,groupID:a.group_id,name:a.name,description:a.description,createdAt:n(a.created_at)});let t=e.serialize(m.hlc.now());m.stores.onCommit(()=>m.scheduleBroadcast(a.group_id,{type:"circle:delete",circleID:r,hlc:t}))}return{deletedID:r}},removeCircleMember:async(r,t)=>{let a=await g(),[i,o]=await Promise.all([a.getCircle(r),a.getCircleMember(r,t)]);if(await m.groupManager.removeCircleMember({stores:m.stores,circleID:r,memberDID:t}),await m.emitter.emit("circleMemberRemoved",{circleID:r,memberDID:t,role:o?.role??"",createdAt:null!=o?n(o.created_at):new Date().toISOString()}),null!=i){let a=e.serialize(m.hlc.now());m.stores.onCommit(()=>m.scheduleBroadcast(i.group_id,{type:"member:remove",circleID:r,memberDID:t,hlc:a}))}return{circleID:r,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}}}
@@ -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,s){return{prepareRequest:async()=>{a(o.viewerDID,s.identity.id,s.autoAcceptPeers);let t=s.identity,r=await e(t);return s.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,s.identity.id,s.autoAcceptPeers);let i=s.pendingJoinRequests.get(o.viewerDID);if(null==i)throw Error("No pending join request found — call prepareJoinRequest first");let n=s.identity,p=t(e),u={publicPackage:i.publicPackage,privatePackage:i.privatePackage,ownerDID:n.id};await s.groupManager.joinGroup({stores:s.stores,identity:n,groupID:p.groupID,groupName:p.groupName,hubs:p.suggestedHubs,invite:p.invite,keyPackageBundle:u,welcomeMessage:p.welcomeMessage,ratchetTree:p.ratchetTree});let c=await r({stores:s.stores,emitter:s.emitter,invite:p});return s.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}}}
@@ -6,7 +6,7 @@ import type { Adapter } from '@kubun/db-adapter';
6
6
  import type { HLC } from '@kubun/hlc';
7
7
  import type { Logger } from '@kubun/logger';
8
8
  import type { GroupBroadcastMessage } from '../groups/broadcast.js';
9
- import type { GroupEventEmitter } from '../groups/events.js';
9
+ import type { P2PEventEmitter } from '../groups/events.js';
10
10
  import type { GroupHandleRegistry } from '../groups/group-handle-registry.js';
11
11
  import type { GroupManager } from '../groups/manager.js';
12
12
  import type { SyncManager } from '../sync/sync-manager.js';
@@ -23,7 +23,7 @@ export type ContextDeps = {
23
23
  stores: StoreProvider;
24
24
  adapter: Adapter;
25
25
  hlc: HLC;
26
- emitter: GroupEventEmitter;
26
+ emitter: P2PEventEmitter;
27
27
  pendingJoinRequests: Map<string, PendingJoinRequest>;
28
28
  runtime: Runtime;
29
29
  autoAcceptPeers?: Array<string>;
@@ -34,6 +34,20 @@ export type ContextDeps = {
34
34
  * send runs after the mutation transaction commits.
35
35
  */
36
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;
37
51
  logger: Logger;
38
52
  };
39
53
  export declare function toISO(timestamp: unknown): string;
@@ -1,11 +1,13 @@
1
1
  import { EventEmitter } from '@enkaku/event';
2
2
  import type { DefaultAccessLevel, GraphInternals } from '@kubun/engine';
3
+ import { HLC } from '@kubun/hlc';
3
4
  import type { Logger } from '@kubun/logger';
4
5
  import type { GraphStoreAPI } from '@kubun/store-graph';
5
6
  import type { P2PStoreAPI } from '@kubun/store-p2p';
6
7
  import type { ForwardingConfig } from '../sync/forwarder.js';
7
8
  import type { StoreUnreadableMode } from '../types.js';
8
9
  import { type AffectedRow, type GroupBroadcastMessage } from './broadcast.js';
10
+ import type { P2PEventEmitter } from './events.js';
9
11
  import type { GroupHandleRegistry } from './group-handle-registry.js';
10
12
  /**
11
13
  * Thrown when a received broadcast fails to decrypt (MLS restore, decrypt,
@@ -25,6 +27,17 @@ export declare class ApplyError extends Error {
25
27
  readonly cause: unknown;
26
28
  constructor(message: string, cause: unknown);
27
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
+ }
28
41
  export type BroadcastEvent = {
29
42
  groupID: string;
30
43
  message: GroupBroadcastMessage;
@@ -56,16 +69,41 @@ export type SendBroadcastParams = {
56
69
  */
57
70
  send: (encrypted: Uint8Array) => Promise<void>;
58
71
  };
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>;
81
+ };
59
82
  export type ReceiveBroadcastParams = {
60
83
  groupID: string;
61
84
  encrypted: Uint8Array;
62
85
  };
63
- export type ReceiveBroadcastResult = {
86
+ export type ReceiveAppResult = {
87
+ kind: 'app';
64
88
  message: GroupBroadcastMessage;
65
89
  applied: boolean;
66
90
  affected?: AffectedRow;
67
91
  duplicates?: number;
68
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>;
105
+ };
106
+ export type ReceiveBroadcastResult = ReceiveAppResult | ReceiveCommitResult;
69
107
  /**
70
108
  * BroadcastService handles MLS encrypt/decrypt for group broadcasts
71
109
  * and applies received messages to the local DB.
@@ -88,6 +126,12 @@ export type BroadcastServiceParams = {
88
126
  selfDID: string;
89
127
  /** Single canonical access point for MLS `GroupHandle` instances. */
90
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;
91
135
  /**
92
136
  * Receiver storage mode for incoming `mutation:apply` entries. Defaults
93
137
  * to `'persist'`. When `'drop'`, `defaultAccessLevel` must be supplied
@@ -106,6 +150,11 @@ export type BroadcastServiceParams = {
106
150
  * surviving candidate group on the receive path.
107
151
  */
108
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;
109
158
  logger?: Logger;
110
159
  };
111
160
  export declare class BroadcastService extends EventEmitter<BroadcastServiceEvents> {
@@ -121,6 +170,16 @@ export declare class BroadcastService extends EventEmitter<BroadcastServiceEvent
121
170
  * intact.
122
171
  */
123
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.
181
+ */
182
+ prepareSendCommit(params: SendCommitParams): Promise<void>;
124
183
  /**
125
184
  * Process a received broadcast: MLS decrypt INSIDE the registry lock,
126
185
  * then apply to the DB OUTSIDE the lock so a slow apply doesn't block
@@ -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{mlsDecryptFramed as s,mlsEncryptFramed as o}from"./mls-codec.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;#e;constructor(r){super(),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,logger:r.logger},this.#e=r.registry}async prepareSend(r){let{groupID:e,message:t,send:s}=r,c=a(t);await this.#e.withHandle(e,async r=>{let{framed:e}=await o(r,c);await s(e)})}async processReceived(r){let a,o,{groupID:c,encrypted:i}=r;try{a=await this.#e.withHandle(c,async r=>{let e=await s(r,i);return t(e)})}catch(r){throw new DecryptError("Failed to decrypt broadcast",r)}try{o=await e(this.#r,a,c)}catch(r){throw new ApplyError("Failed to apply broadcast",r)}let{applied:p,affected:d,duplicates:l}=o;return await this.emit("broadcast",{groupID:c,message:a,applied:p,affected:d,duplicates:l}),{message:a,applied:p,affected:d,duplicates: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}})}}
@@ -5,6 +5,7 @@ import type { Catalog, GraphStoreAPI } from '@kubun/store-graph';
5
5
  import type { Circle, CircleMember, Group, P2PStoreAPI } from '@kubun/store-p2p';
6
6
  import { type ForwardingConfig } from '../sync/forwarder.js';
7
7
  import type { StoreUnreadableMode } from '../types.js';
8
+ import type { P2PEventEmitter } from './events.js';
8
9
  export type MutationApplyEntry = {
9
10
  mutationJWT: string;
10
11
  docID: string;
@@ -52,6 +53,37 @@ export type GroupBroadcastMessage = {
52
53
  type: 'catalog:delete';
53
54
  catalogID: string;
54
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;
55
87
  } | {
56
88
  type: 'group:update';
57
89
  groupID: string;
@@ -60,6 +92,18 @@ export type GroupBroadcastMessage = {
60
92
  description?: string;
61
93
  hlc: string;
62
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;
63
107
  } | {
64
108
  type: 'mutation:apply';
65
109
  entries: Array<MutationApplyEntry>;
@@ -148,6 +192,12 @@ export type ProcessBroadcastParams = {
148
192
  * carrying `senderPeerDID = selfDID`.
149
193
  */
150
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;
151
201
  /** Optional logger — warns on per-entry apply failures. */
152
202
  logger?: Logger;
153
203
  };
@@ -1 +1 @@
1
- import{HLC as e}from"@kubun/hlc";import{evaluateAndForward as a}from"../sync/forwarder.js";import{createReceiveAccessGate as l}from"../sync/receive-access-gate.js";function r(a,l){return e.compare(e.parse(a),e.parse(l))>0}export async function processBroadcast(e,t,i){let{p2pStore:c,graphStore:d}=e;switch(t.type){case"circle:create":{let e=await c.getCircle(t.circle.id);if(null!=e){if(!r(t.circle.hlc,e.hlc))return{applied:!1};await c.updateCircle(t.circle.id,{name:t.circle.name,description:t.circle.description,catalog_ids:t.circle.catalogIDs,hlc:t.circle.hlc});let a=await c.getCircle(t.circle.id);return null!=a?{applied:!0,affected:{kind:"circle",row:a}}:{applied:!0}}await c.createCircle({id:t.circle.id,group_id:t.circle.groupID,name:t.circle.name,description:t.circle.description,catalog_ids:t.circle.catalogIDs,hlc:t.circle.hlc});let a=await c.getCircle(t.circle.id);return null!=a?{applied:!0,affected:{kind:"circle",row:a}}:{applied:!0}}case"circle:update":{let e=await c.getCircle(t.circleID);if(null==e||!r(t.update.hlc,e.hlc))return{applied:!1};await c.updateCircle(t.circleID,{name:t.update.name,description:t.update.description,catalog_ids:t.update.catalogIDs,hlc:t.update.hlc});let a=await c.getCircle(t.circleID);return null!=a?{applied:!0,affected:{kind:"circle",row:a}}:{applied:!0}}case"circle:delete":{let e=await c.getCircle(t.circleID);if(null==e||!r(t.hlc,e.hlc))return{applied:!1};return await c.deleteCircle(t.circleID),{applied:!0,affected:{kind:"circle",row:e}}}case"member:add":{await c.addCircleMember({circle_id:t.member.circleID,member_did:t.member.memberDID,role:t.member.role,hlc:t.member.hlc});let e=await c.getCircleMember(t.member.circleID,t.member.memberDID);return null!=e?{applied:!0,affected:{kind:"member",row:e}}:{applied:!0}}case"member:remove":{let e=await c.getCircleMember(t.circleID,t.memberDID);return await c.removeCircleMember(t.circleID,t.memberDID),null!=e?{applied:!0,affected:{kind:"member",row:e}}:{applied:!0}}case"catalog:create":{let e=await d.getCatalog(t.catalog.id);if(null!=e){if(!r(t.catalog.hlc,e.hlc))return{applied:!1};await d.updateCatalog(t.catalog.id,{name:t.catalog.name,description:t.catalog.description,filter_criteria:t.catalog.filterCriteria,hlc:t.catalog.hlc});let a=await d.getCatalog(t.catalog.id);return null!=a?{applied:!0,affected:{kind:"catalog",row:a}}:{applied:!0}}await d.createCatalog({id:t.catalog.id,owner_did:t.catalog.ownerDID,name:t.catalog.name,description:t.catalog.description,filter_criteria:t.catalog.filterCriteria,hlc:t.catalog.hlc});let a=await d.getCatalog(t.catalog.id);return null!=a?{applied:!0,affected:{kind:"catalog",row:a}}:{applied:!0}}case"catalog:update":{let e=await d.getCatalog(t.catalogID);if(null==e||!r(t.update.hlc,e.hlc))return{applied:!1};await d.updateCatalog(t.catalogID,{name:t.update.name,description:t.update.description,filter_criteria:t.update.filterCriteria,hlc:t.update.hlc});let a=await d.getCatalog(t.catalogID);return null!=a?{applied:!0,affected:{kind:"catalog",row:a}}:{applied:!0}}case"catalog:delete":{let e=await d.getCatalog(t.catalogID);if(null==e||!r(t.hlc,e.hlc))return{applied:!1};return await d.deleteCatalog(t.catalogID),{applied:!0,affected:{kind:"catalog",row:e}}}case"group:update":{let e=await c.getGroup(t.groupID);if(null==e||!r(t.update.hlc,e.hlc))return{applied:!1};await c.updateGroup(t.groupID,{name:t.update.name,description:t.update.description,hlc:t.update.hlc});let a=await c.getGroup(t.groupID);return null!=a?{applied:!0,affected:{kind:"group",row:a}}:{applied:!0}}case"mutation:apply":{if(t.senderPeerDID===e.selfDID)return{applied:!1};let r="drop"===(e.storeUnreadable??"persist")?l({selfDID:e.selfDID,db:{getUserModelAccessDefault:(e,a,l)=>d.getUserModelAccessDefault(e,a,l),isMemberOfAnyCircle:(e,a)=>c.isMemberOfAnyCircle(e,a),isMemberOfAnyGroup:(e,a)=>c.isMemberOfAnyGroup(e,a)},defaultAccessLevel:e.defaultAccessLevel??{read:"only_owner",write:"only_owner"}}):void 0,o=null!=e.forwarding&&!1!==e.forwarding&&null!=e.scheduleBroadcast,n=!1,p=0,u=0,s=0;for(let l of t.entries)try{let t=await e.graph.applyVerifiedMutation({token:l.mutationJWT,origin:"peer",...null!=r?{accessGate:r}:{}});if(t.dropped){p++;continue}let g=!1;try{let e=await c.recordBroadcast({docID:l.docID,version:l.version,mlsGroupID:i});g=!0,!e&&s++}catch(a){e.logger?.warn("mutation:apply recordBroadcast failed, skipping forward",{docID:l.docID,version:l.version,groupID:i,error:a})}if(n=!0,!g)continue;if(o&&null!=e.forwarding&&null!=e.scheduleBroadcast&&null!=t.document)try{let r=await a({entry:l,postState:t.document,sourceGroupID:i,selfDID:e.selfDID,p2pStore:c,graphStore:d,defaultAccessLevel:e.defaultAccessLevel??{read:"only_owner",write:"only_owner"},forwarding:e.forwarding,scheduleBroadcast:e.scheduleBroadcast,logger:e.logger});u+=r.forwarded}catch(a){e.logger?.warn("mutation:apply forwarding failed, skipping entry",{docID:l.docID,version:l.version,groupID:i,error:a})}}catch(a){e.logger?.warn("mutation:apply entry failed, skipping",{docID:l.docID,version:l.version,modelID:l.modelID,groupID:i,error:a})}let g={applied:n,dropped:p,duplicates:s};return o&&(g.forwarded=u),g}}}
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
  };
@@ -43,7 +51,9 @@ export type GroupEventMap = {
43
51
  hubID: string;
44
52
  hubURL: string;
45
53
  };
54
+ delegationTokenAdded: DelegationData;
55
+ delegationTokenRevoked: RevokedDelegationData;
46
56
  };
47
- export type GroupEventEmitter = EventEmitter<GroupEventMap>;
48
- export declare function createGroupEventEmitter(): GroupEventEmitter;
49
- 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})}
@@ -1 +1 @@
1
- import{getKubunLogger as e}from"@kubun/logger";import{getP2PStore as t}from"@kubun/store-p2p";import{createMutex as r}from"../util/mutex.js";import{restoreMLSGroupHandle as s}from"./mls-group-handle.js";import{fromMLSStateRow as a,serializeMLSGroupState as i,toMLSStateInsert as l}from"./mls-state.js";export class GroupHandleRegistry{#e;#t;#r;#s=new Map;constructor(t){this.#e=t.stores,this.#t=t.deviceID,this.#r=t.logger??e("plugin-p2p:group-handle-registry")}withHandle(e,t,r){let s=r?.stores??this.#e,a=this.#a(e);return a.mutex.run(async()=>{null==a.handle&&(a.handle=await this.#i(s,e));let r=await t(a.handle);return await this.#l(s,e,a.handle),r})}withHandleReplacing(e,t,r){let s=r?.stores??this.#e,a=this.#a(e);return a.mutex.run(async()=>{null==a.handle&&(a.handle=await this.#i(s,e));let{result:r,updated:i}=await t(a.handle);return a.handle=i,await this.#l(s,e,i),r})}invalidate(e){this.#s.delete(e)}async seed(e){let{groupID:r,handle:s,stores:a}=e,n=this.#s.get(r);if(n?.handle!=null)throw Error(`GroupHandleRegistry.seed: handle already cached for group ${r}`);let o=i(s),h=await t(a);await h.saveMLSState(l(o,r,this.#t)),a.onCommit(()=>{let e=this.#a(r);null==e.handle&&(e.handle=s)})}#a(e){let t=this.#s.get(e);return null==t&&(t={handle:null,mutex:r()},this.#s.set(e,t)),t}async #i(e,r){let i=await t(e),l=await i.getMLSState(r,this.#t);if(null==l)throw Error(`No MLS state for group ${r}`);return await s(a(l))}async #l(e,r,s){let a=i(s),n=await t(e);await n.saveMLSState(l(a,r,this.#t))}}
1
+ import{getKubunLogger as e}from"@kubun/logger";import{getP2PStore as t}from"@kubun/store-p2p";import{createMutex as r}from"../util/mutex.js";import{restoreMLSGroupHandle as s}from"./mls-group-handle.js";import{fromMLSStateRow as i,serializeMLSGroupState as a,toMLSStateInsert as l}from"./mls-state.js";export class GroupHandleRegistry{#e;#t;#r;#s=new Map;constructor(t){this.#e=t.stores,this.#t=t.deviceID,this.#r=t.logger??e("plugin-p2p:group-handle-registry")}withHandle(e,t,r){return this.#i(e,r,async(r,s)=>{let i=await t(r.handle);return await this.#a(s,e,r.handle),i})}withHandleReplacing(e,t,r){return this.#i(e,r,async(r,s)=>{let{result:i,updated:a}=await t(r.handle);return r.handle=a,await this.#a(s,e,a),i})}#i(e,t,r){let s=t?.stores??this.#e,i=this.#l(e);return i.mutex.run(async()=>(null==i.handle&&(i.handle=await this.#n(s,e)),r(i,s)))}invalidate(e){this.#s.delete(e)}async seed(e){let{groupID:r,handle:s,stores:i}=e,n=this.#s.get(r);if(n?.handle!=null)throw Error(`GroupHandleRegistry.seed: handle already cached for group ${r}`);let o=a(s),h=await t(i);await h.saveMLSState(l(o,r,this.#t)),i.onCommit(()=>{let e=this.#l(r);null==e.handle&&(e.handle=s)})}#l(e){let t=this.#s.get(e);return null==t&&(t={handle:null,mutex:r()},this.#s.set(e,t)),t}async #n(e,r){let a=await t(e),l=await a.getMLSState(r,this.#t);if(null==l)throw this.#r.debug("restore: no MLS state",{groupID:r,deviceID:this.#t}),Error(`No MLS state for group ${r}`);return this.#r.debug("restore: MLS state",{groupID:r,deviceID:this.#t,epoch:l.epoch}),await s(i(l))}async #a(e,r,s){let i=a(s),n=await t(e);await n.saveMLSState(l(i,r,this.#t)),this.#r.debug("persist: MLS state",{groupID:r,deviceID:this.#t,epoch:i.epoch})}}
@@ -19,6 +19,22 @@ export type InvitePayload = {
19
19
  invite: Invite;
20
20
  welcomeMessage: unknown;
21
21
  ratchetTree: unknown;
22
+ /**
23
+ * Optional initial write-grant capability tokens (stringified JWTs) the
24
+ * inviter wants the joiner to hold immediately, before it starts receiving
25
+ * group broadcasts. Travels alongside the MLS invite as plain app-level data
26
+ * and is applied after the join completes — tokens addressed to the joiner
27
+ * are stored, others ignored.
28
+ */
29
+ grants?: Array<string>;
30
+ /**
31
+ * Optional initial signed-revocation tokens the inviter wants the joiner to
32
+ * apply immediately, before it starts receiving group broadcasts. Pending
33
+ * revocations (cap not yet known by the inviter) do NOT travel — only
34
+ * verified ones — though the receiving joiner re-cross-checks them anyway
35
+ * against any caps it already holds.
36
+ */
37
+ revocations?: Array<string>;
22
38
  };
23
39
  export declare function encodeJoinRequest(payload: JoinRequestPayload): string;
24
40
  export declare function decodeJoinRequest(encoded: string): JoinRequestPayload;
@@ -1,9 +1,11 @@
1
1
  import type { StoreProvider } from '@kubun/db';
2
+ import type { HLC } from '@kubun/hlc';
3
+ import type { Logger } from '@kubun/logger';
2
4
  import type { GroupData } from '../types.js';
3
- import type { GroupEventEmitter } from './events.js';
5
+ import type { P2PEventEmitter } from './events.js';
4
6
  export type FinalizeJoinedGroupParams = {
5
7
  stores: StoreProvider;
6
- emitter: GroupEventEmitter;
8
+ emitter: P2PEventEmitter;
7
9
  invite: {
8
10
  groupID: string;
9
11
  };
@@ -11,3 +13,67 @@ export type FinalizeJoinedGroupParams = {
11
13
  export declare function finalizeJoinedGroup(params: FinalizeJoinedGroupParams): Promise<{
12
14
  group: GroupData | null;
13
15
  }>;
16
+ export type ApplyInviteGrantsParams = {
17
+ stores: StoreProvider;
18
+ /** DID of the joining device — only grants addressed to it are stored. */
19
+ selfDID: string;
20
+ /** Group the joined invite belongs to. */
21
+ groupID: string;
22
+ /** Stringified capability JWTs carried by the invite. */
23
+ grants?: Array<string>;
24
+ /** Joiner's HLC, used to stamp each stored held row. */
25
+ hlc: HLC;
26
+ /**
27
+ * Optional emitter notified after each held row is written. Lets the joiner's
28
+ * `ownDelegationTokenAdded` subscription surface invite-bootstrap grants
29
+ * without waiting for a subsequent broadcast.
30
+ */
31
+ emitter?: P2PEventEmitter;
32
+ logger?: Logger;
33
+ };
34
+ /**
35
+ * Apply invite-bootstrap grants after a join completes. Each token is verified
36
+ * and, when addressed to the joining device, stored as a held row so the
37
+ * engine's auto-attach can use it on the very first mutation — before any group
38
+ * broadcast arrives.
39
+ *
40
+ * No grantor HLC travels in the invite (it carries plain tokens, not the
41
+ * grantor's issued rows), so the joiner stamps a fresh HLC of its own at apply
42
+ * time. This is consistent with how every other join-time row the joiner
43
+ * materializes is locally stamped.
44
+ *
45
+ * Returns the number of grants that resulted in a stored held row.
46
+ */
47
+ export declare function applyInviteGrants(params: ApplyInviteGrantsParams): Promise<number>;
48
+ export type ApplyInviteRevocationsParams = {
49
+ stores: StoreProvider;
50
+ /** Group the joined invite belongs to. */
51
+ groupID: string;
52
+ /** Signed revocation JWTs carried by the invite. */
53
+ revocations?: Array<string>;
54
+ /** Joiner's HLC, used to stamp each stored revocation row. */
55
+ hlc: HLC;
56
+ /**
57
+ * DID of the joining device. Forwarded to `storeReceivedRevocation` for
58
+ * symmetry; revocation storage is identity-agnostic but the helper accepts
59
+ * the param.
60
+ */
61
+ selfDID: string;
62
+ /** Optional emitter forwarded to `storeReceivedRevocation`. */
63
+ emitter?: P2PEventEmitter;
64
+ logger?: Logger;
65
+ };
66
+ /**
67
+ * Apply invite-bootstrap revocations after a join completes. Each token is
68
+ * verified and stored — cap-known locally with matching `iss` yields a
69
+ * verified row, cap-unknown yields a pending row that the joiner will
70
+ * cross-check when the cap later arrives.
71
+ *
72
+ * The inviter's caller is responsible for filtering to verified revocations
73
+ * only; pending revocations do not travel via the invite envelope. The joiner
74
+ * stamps a fresh HLC since the inviter's HLC for these rows is not in the
75
+ * payload.
76
+ *
77
+ * Returns the number of revocations that resulted in a stored row.
78
+ */
79
+ export declare function applyInviteRevocations(params: ApplyInviteRevocationsParams): Promise<number>;
@@ -1 +1 @@
1
- import{getP2PStore as e}from"@kubun/store-p2p";import{toISO as t}from"../context/types.js";export async function finalizeJoinedGroup(r){let{stores:i,emitter:o,invite:n}=r,a=await e(i),p=await a.getGroup(n.groupID),u=null!=p?{id:p.id,name:p.name,description:p.description,createdBy:p.created_by,createdAt:t(p.created_at)}:null;return null!=u&&await o.emit("groupJoined",u),{group:u}}
1
+ import{HLC as e}from"@kubun/hlc";import{getP2PStore as t}from"@kubun/store-p2p";import{toISO as r}from"../context/types.js";import{storeReceivedGrant as n}from"./store-received-grant.js";import{storeReceivedRevocation as i}from"./store-received-revocation.js";export async function finalizeJoinedGroup(e){let{stores:n,emitter:i,invite:o}=e,a=await t(n),l=await a.getGroup(o.groupID),p=null!=l?{id:l.id,name:l.name,description:l.description,createdBy:l.created_by,createdAt:r(l.created_at)}:null;return null!=p&&await i.emit("groupJoined",p),{group:p}}export async function applyInviteGrants(r){let{stores:i,selfDID:o,groupID:a,grants:l,hlc:p,emitter:u,logger:c}=r;if(null==l||0===l.length)return 0;let s=await t(i);return(await Promise.all(l.map(t=>n({p2pStore:s,token:t,groupID:a,hlc:e.serialize(p.now()),selfDID:o,...null!=u?{emitter:u}:{},...null!=c?{logger:c}:{}})))).reduce((e,t)=>e+ +!!t,0)}export async function applyInviteRevocations(r){let{stores:n,groupID:o,revocations:a,hlc:l,selfDID:p,emitter:u,logger:c}=r;if(null==a||0===a.length)return 0;let s=await t(n);return(await Promise.all(a.map(t=>i({p2pStore:s,token:t,groupID:o,hlc:e.serialize(l.now()),selfDID:p,...null!=u?{emitter:u}:{},...null!=c?{logger:c}:{}})))).reduce((e,t)=>e+ +!!t,0)}
@@ -46,8 +46,10 @@ export type InviteToGroupParams = {
46
46
  };
47
47
  export type InviteToGroupResult = {
48
48
  invite: Invite;
49
- welcomeMessage: unknown;
50
- commitMessage: unknown;
49
+ /** Framed MLSMessage(Welcome) bytes for the invitee (`@enkaku/group@0.16.1`). */
50
+ welcomeMessage: Uint8Array;
51
+ /** Framed MLSMessage(Commit) bytes to fan out to existing members. */
52
+ commitMessage: Uint8Array;
51
53
  /**
52
54
  * Ratchet tree from the post-commit handle. Required by `joinGroup`
53
55
  * callers that build an invite payload — they no longer hold a
@@ -76,7 +78,7 @@ export type RemoveMemberParams = {
76
78
  memberDID: string;
77
79
  };
78
80
  export type RemoveMemberResult = {
79
- commitMessage: unknown;
81
+ commitMessage: Uint8Array;
80
82
  };
81
83
  export type RemoveGroupMemberParams = {
82
84
  stores: StoreProvider;
@@ -84,7 +86,7 @@ export type RemoveGroupMemberParams = {
84
86
  memberDID: string;
85
87
  };
86
88
  export type RemoveGroupMemberResult = {
87
- commitMessage: unknown;
89
+ commitMessage: Uint8Array;
88
90
  };
89
91
  export type LeaveGroupParams = {
90
92
  stores: StoreProvider;
@@ -1 +1 @@
1
- import{commitInvite as e,createGroup as r,createInvite as t,processWelcome as i,removeMember as a}from"@enkaku/group";import{HLC as o}from"@kubun/hlc";import{getGraphStore as s}from"@kubun/store-graph";import{getP2PStore as c}from"@kubun/store-p2p";import{bindHubToGroup as l,upsertHub as n}from"../hub/manager.js";export class GroupManager{#e;#r;#t;#i;constructor(e){this.#e=e.identity.id,this.#r=e.getRandomID,this.#t=new o({nodeID:e.identity.id}),this.#i=e.registry}async createGroup(e){let t=this.#r(),{group:i}=await r(e.identity,t),a=e.hubs??[];return await e.stores.withTransaction(async r=>{let s=await c(r);for(let c of(await s.createGroup({id:t,name:e.name,description:e.description??"",created_by:e.createdBy??e.identity.id,hlc:o.serialize(this.#t.now())}),await this.#i.seed({groupID:t,handle:i,stores:r}),await s.addGroupMember({group_id:t,member_did:e.identity.id,role:"admin"}),a)){let e=await n({stores:r,url:c.url,serverDID:c.serverDID??null});await l({stores:r,hubID:e.id,groupID:t})}}),{groupID:t}}async updateGroup(e){let r=o.serialize(this.#t.now()),t=await c(e.stores);return await t.updateGroup(e.groupID,{name:e.update.name,description:e.update.description,hlc:r}),{broadcast:{type:"group:update",groupID:e.groupID,update:{...e.update,hlc:r}}}}async inviteToGroup(r){let i=await this.#i.withHandleReplacing(r.groupID,async i=>{let{invite:a}=await t({group:i,identity:r.identity,recipientDID:r.recipientDID,permission:r.permission}),{commitMessage:o,welcomeMessage:s,newGroup:c}=await e(i,r.recipientKeyPackage);return{result:{invite:a,commitMessage:o,welcomeMessage:s,ratchetTree:c.state.ratchetTree},updated:c}},{stores:r.stores}),a=await c(r.stores);return await a.addGroupMember({group_id:r.groupID,member_did:r.recipientDID,role:"admin"===r.permission?"admin":"member"}),i}async joinGroup(e){let{group:r}=await i({identity:e.identity,invite:e.invite,welcome:e.welcomeMessage,keyPackageBundle:e.keyPackageBundle,ratchetTree:e.ratchetTree}),t=e.hubs??[];await e.stores.withTransaction(async i=>{await this.#i.seed({groupID:e.groupID,handle:r,stores:i});let a=await c(i);for(let r of(null==await a.getGroup(e.groupID)&&await a.createGroup({id:e.groupID,name:e.groupName,description:"",created_by:e.identity.id,hlc:o.serialize(this.#t.now())}),await a.addGroupMember({group_id:e.groupID,member_did:e.identity.id,role:"member"}),t)){let t=await n({stores:i,url:r.url,serverDID:r.serverDID??null});await l({stores:i,hubID:t.id,groupID:e.groupID})}})}async removeMember(e){let{commitMessage:r}=await this.#i.withHandleReplacing(e.groupID,async r=>{let{commitMessage:t,newGroup:i}=await a(r,e.leafIndex);return{result:{commitMessage:t},updated:i}},{stores:e.stores}),t=await c(e.stores);return await t.removeGroupMember(e.groupID,e.memberDID),{commitMessage:r}}async removeGroupMember(e){let{commitMessage:r}=await this.#i.withHandleReplacing(e.groupID,async r=>{let t=r.findMemberLeafIndex(e.memberDID);if(null==t)throw Error(`Member ${e.memberDID} not found in MLS group`);let{commitMessage:i,newGroup:o}=await a(r,t);return{result:{commitMessage:i},updated:o}},{stores:e.stores}),t=await c(e.stores);return await t.removeGroupMember(e.groupID,e.memberDID),{commitMessage:r}}async leaveGroup(e){let r=await c(e.stores);await r.deleteMLSState(e.groupID,this.#e),await r.removeGroupMember(e.groupID,e.identity.id),this.#i.invalidate(e.groupID)}async loadGroup(e,r){let t=await c(e),i=await t.getMLSState(r,this.#e);if(null!=i)return{epoch:i.epoch,credential:i.credential}}async createCircle(e){let r=this.#r(),t=o.serialize(this.#t.now()),i=e.catalogIDs??[],a=await c(e.stores);return await a.createCircle({id:r,group_id:e.groupID,name:e.name,description:e.description??"",catalog_ids:i,hlc:t}),{circleID:r,broadcast:{type:"circle:create",circle:{id:r,groupID:e.groupID,name:e.name,description:e.description??"",catalogIDs:i,hlc:t}}}}async updateCircle(e){let r=o.serialize(this.#t.now()),t=[],i=await c(e.stores);if(null!=e.update.catalogIDs){let r=await i.getCircle(e.circleID),a=new Set(null!=r?r.catalog_ids:[]),o=e.update.catalogIDs.filter(e=>!a.has(e)),c=await s(e.stores);for(let e of(await Promise.all(o.map(e=>c.getCatalog(e)))))null!=e&&t.push({type:"catalog:create",catalog:{id:e.id,ownerDID:e.owner_did,name:e.name,description:e.description,filterCriteria:e.filter_criteria,hlc:e.hlc}})}await i.updateCircle(e.circleID,{name:e.update.name,description:e.update.description,catalog_ids:e.update.catalogIDs,hlc:r});let a={type:"circle:update",circleID:e.circleID,update:{...e.update,hlc:r}};return{broadcast:a,broadcasts:[a,...t]}}async deleteCircle(e){let r=o.serialize(this.#t.now()),t=await c(e.stores);return await t.deleteCircle(e.circleID),{broadcast:{type:"circle:delete",circleID:e.circleID,hlc:r}}}async addCircleMember(e){let r=await c(e.stores);if(!await r.isGroupMember(e.groupID,e.memberDID))throw Error(`${e.memberDID} is not a member of group ${e.groupID}`);let t=o.serialize(this.#t.now());return await r.addCircleMember({circle_id:e.circleID,member_did:e.memberDID,role:e.role,hlc:t}),{broadcast:{type:"member:add",member:{circleID:e.circleID,memberDID:e.memberDID,role:e.role,hlc:t}}}}async removeCircleMember(e){let r=o.serialize(this.#t.now()),t=await c(e.stores);return await t.removeCircleMember(e.circleID,e.memberDID),{broadcast:{type:"member:remove",circleID:e.circleID,memberDID:e.memberDID,hlc:r}}}}
1
+ import{commitInvite as e,createGroup as r,createInvite as t,processWelcome as i,removeMember as a}from"@enkaku/group";import{HLC as o}from"@kubun/hlc";import{getGraphStore as s}from"@kubun/store-graph";import{getP2PStore as l}from"@kubun/store-p2p";import{bindHubToGroup as c,upsertHub as n}from"../hub/manager.js";export class GroupManager{#e;#r;#t;#i;constructor(e){this.#e=e.identity.id,this.#r=e.getRandomID,this.#t=new o({nodeID:e.identity.id}),this.#i=e.registry}async createGroup(e){let t=this.#r(),{group:i}=await r(e.identity,t),a=e.hubs??[];return await e.stores.withTransaction(async r=>{let s=await l(r);for(let l of(await s.createGroup({id:t,name:e.name,description:e.description??"",created_by:e.createdBy??e.identity.id,hlc:o.serialize(this.#t.now())}),await this.#i.seed({groupID:t,handle:i,stores:r}),await s.addGroupMember({group_id:t,member_did:e.identity.id,role:"admin",hlc:o.serialize(this.#t.now())}),a)){let e=await n({stores:r,url:l.url,serverDID:l.serverDID??null});await c({stores:r,hubID:e.id,groupID:t})}}),{groupID:t}}async updateGroup(e){let r=o.serialize(this.#t.now()),t=await l(e.stores);return await t.updateGroup(e.groupID,{name:e.update.name,description:e.update.description,hlc:r}),{broadcast:{type:"group:update",groupID:e.groupID,update:{...e.update,hlc:r}}}}async inviteToGroup(r){let i=await this.#i.withHandleReplacing(r.groupID,async i=>{let{invite:a}=await t({group:i,identity:r.identity,recipientDID:r.recipientDID,permission:r.permission}),{commitMessage:o,welcomeMessage:s,newGroup:l}=await e(i,r.recipientKeyPackage);return{result:{invite:a,commitMessage:o,welcomeMessage:s,ratchetTree:l.state.ratchetTree},updated:l}},{stores:r.stores}),a=await l(r.stores);return await a.addGroupMember({group_id:r.groupID,member_did:r.recipientDID,role:"admin"===r.permission?"admin":"member",hlc:o.serialize(this.#t.now())}),i}async joinGroup(e){let{group:r}=await i({identity:e.identity,invite:e.invite,welcome:e.welcomeMessage,keyPackageBundle:e.keyPackageBundle,ratchetTree:e.ratchetTree}),t=e.hubs??[];await e.stores.withTransaction(async i=>{await this.#i.seed({groupID:e.groupID,handle:r,stores:i});let a=await l(i);for(let r of(null==await a.getGroup(e.groupID)&&await a.createGroup({id:e.groupID,name:e.groupName,description:"",created_by:e.identity.id,hlc:o.serialize(this.#t.now())}),await a.addGroupMember({group_id:e.groupID,member_did:e.identity.id,role:"member",hlc:o.serialize(this.#t.now())}),t)){let t=await n({stores:i,url:r.url,serverDID:r.serverDID??null});await c({stores:i,hubID:t.id,groupID:e.groupID})}})}async removeMember(e){let{commitMessage:r}=await this.#i.withHandleReplacing(e.groupID,async r=>{let{commitMessage:t,newGroup:i}=await a(r,e.leafIndex);return{result:{commitMessage:t},updated:i}},{stores:e.stores}),t=await l(e.stores);return await t.removeGroupMember(e.groupID,e.memberDID,o.serialize(this.#t.now())),{commitMessage:r}}async removeGroupMember(e){let{commitMessage:r}=await this.#i.withHandleReplacing(e.groupID,async r=>{let t=r.findMemberLeafIndex(e.memberDID);if(null==t)throw Error(`Member ${e.memberDID} not found in MLS group`);let{commitMessage:i,newGroup:o}=await a(r,t);return{result:{commitMessage:i},updated:o}},{stores:e.stores}),t=await l(e.stores);return await t.removeGroupMember(e.groupID,e.memberDID,o.serialize(this.#t.now())),{commitMessage:r}}async leaveGroup(e){let r=await l(e.stores);await r.deleteMLSState(e.groupID,this.#e),await r.removeGroupMember(e.groupID,e.identity.id,o.serialize(this.#t.now())),e.stores.onCommit(()=>this.#i.invalidate(e.groupID))}async loadGroup(e,r){let t=await l(e),i=await t.getMLSState(r,this.#e);if(null!=i)return{epoch:i.epoch,credential:i.credential}}async createCircle(e){let r=this.#r(),t=o.serialize(this.#t.now()),i=e.catalogIDs??[],a=await l(e.stores);return await a.createCircle({id:r,group_id:e.groupID,name:e.name,description:e.description??"",catalog_ids:i,hlc:t}),{circleID:r,broadcast:{type:"circle:create",circle:{id:r,groupID:e.groupID,name:e.name,description:e.description??"",catalogIDs:i,hlc:t}}}}async updateCircle(e){let r=o.serialize(this.#t.now()),t=[],i=await l(e.stores);if(null!=e.update.catalogIDs){let r=await i.getCircle(e.circleID),a=new Set(null!=r?r.catalog_ids:[]),o=e.update.catalogIDs.filter(e=>!a.has(e)),l=await s(e.stores);for(let e of(await Promise.all(o.map(e=>l.getCatalog(e)))))null!=e&&t.push({type:"catalog:create",catalog:{id:e.id,ownerDID:e.owner_did,name:e.name,description:e.description,filterCriteria:e.filter_criteria,hlc:e.hlc}})}await i.updateCircle(e.circleID,{name:e.update.name,description:e.update.description,catalog_ids:e.update.catalogIDs,hlc:r});let a={type:"circle:update",circleID:e.circleID,update:{...e.update,hlc:r}};return{broadcast:a,broadcasts:[a,...t]}}async deleteCircle(e){let r=o.serialize(this.#t.now()),t=await l(e.stores);return await t.deleteCircle(e.circleID),{broadcast:{type:"circle:delete",circleID:e.circleID,hlc:r}}}async addCircleMember(e){let r=await l(e.stores);if(!await r.isGroupMember(e.groupID,e.memberDID))throw Error(`${e.memberDID} is not a member of group ${e.groupID}`);let t=o.serialize(this.#t.now());return await r.addCircleMember({circle_id:e.circleID,member_did:e.memberDID,role:e.role,hlc:t}),{broadcast:{type:"member:add",member:{circleID:e.circleID,memberDID:e.memberDID,role:e.role,hlc:t}}}}async removeCircleMember(e){let r=o.serialize(this.#t.now()),t=await l(e.stores);return await t.removeCircleMember(e.circleID,e.memberDID),{broadcast:{type:"member:remove",circleID:e.circleID,memberDID:e.memberDID,hlc:r}}}}