@kubun/plugin-p2p 0.10.1 → 0.12.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 (201) hide show
  1. package/lib/context/delegation.js +130 -1
  2. package/lib/context/group.js +1646 -1
  3. package/lib/context/hub.js +151 -1
  4. package/lib/context/join.js +115 -1
  5. package/lib/context/peer.d.ts +92 -0
  6. package/lib/context/peer.js +1216 -0
  7. package/lib/context/require-admin.d.ts +57 -0
  8. package/lib/context/require-admin.js +57 -0
  9. package/lib/context/sync.d.ts +12 -0
  10. package/lib/context/sync.js +252 -1
  11. package/lib/context/types.d.ts +58 -22
  12. package/lib/context/types.js +48 -1
  13. package/lib/groups/access-default-token.d.ts +44 -0
  14. package/lib/groups/access-default-token.js +103 -0
  15. package/lib/groups/admin-roster.d.ts +55 -0
  16. package/lib/groups/admin-roster.js +69 -0
  17. package/lib/groups/anchor-store.d.ts +15 -0
  18. package/lib/groups/anchor-store.js +37 -0
  19. package/lib/groups/app-cursor-store.d.ts +17 -0
  20. package/lib/groups/app-cursor-store.js +34 -0
  21. package/lib/groups/broadcast-codec.js +6 -1
  22. package/lib/groups/broadcast.d.ts +238 -106
  23. package/lib/groups/broadcast.js +702 -1
  24. package/lib/groups/catalog-token.d.ts +50 -0
  25. package/lib/groups/catalog-token.js +96 -0
  26. package/lib/groups/circle-projection.d.ts +90 -0
  27. package/lib/groups/circle-projection.js +202 -0
  28. package/lib/groups/circle-reducers.d.ts +139 -0
  29. package/lib/groups/circle-reducers.js +128 -0
  30. package/lib/groups/commit-adoption.d.ts +77 -0
  31. package/lib/groups/commit-adoption.js +108 -0
  32. package/lib/groups/commit-journal.d.ts +14 -0
  33. package/lib/groups/commit-journal.js +65 -0
  34. package/lib/groups/control-request.d.ts +66 -0
  35. package/lib/groups/control-request.js +101 -0
  36. package/lib/groups/events.d.ts +19 -2
  37. package/lib/groups/events.js +10 -1
  38. package/lib/groups/group-anchor.d.ts +24 -0
  39. package/lib/groups/group-anchor.js +22 -0
  40. package/lib/groups/group-crypto.d.ts +42 -0
  41. package/lib/groups/group-crypto.js +159 -0
  42. package/lib/groups/group-handle-registry.d.ts +138 -9
  43. package/lib/groups/group-handle-registry.js +533 -1
  44. package/lib/groups/group-handlers.d.ts +30 -0
  45. package/lib/groups/group-handlers.js +218 -0
  46. package/lib/groups/group-health-monitor.d.ts +11 -0
  47. package/lib/groups/group-health-monitor.js +139 -1
  48. package/lib/groups/group-mls.d.ts +66 -0
  49. package/lib/groups/group-mls.js +696 -0
  50. package/lib/groups/group-peer-manager.d.ts +301 -0
  51. package/lib/groups/group-peer-manager.js +1336 -0
  52. package/lib/groups/group-protocols.d.ts +754 -0
  53. package/lib/groups/group-protocols.js +448 -0
  54. package/lib/groups/invite-payload.d.ts +49 -1
  55. package/lib/groups/invite-payload.js +26 -1
  56. package/lib/groups/join-request-identity.d.ts +21 -0
  57. package/lib/groups/join-request-identity.js +43 -0
  58. package/lib/groups/join-utils.d.ts +61 -0
  59. package/lib/groups/join-utils.js +366 -1
  60. package/lib/groups/ledger-affected-events.d.ts +20 -0
  61. package/lib/groups/ledger-affected-events.js +85 -0
  62. package/lib/groups/ledger-commit-fold.d.ts +107 -0
  63. package/lib/groups/ledger-commit-fold.js +167 -0
  64. package/lib/groups/ledger-ingest.d.ts +124 -0
  65. package/lib/groups/ledger-ingest.js +212 -0
  66. package/lib/groups/ledger.d.ts +44 -0
  67. package/lib/groups/ledger.js +30 -0
  68. package/lib/groups/manager.d.ts +217 -85
  69. package/lib/groups/manager.js +798 -1
  70. package/lib/groups/mls-codec.d.ts +28 -7
  71. package/lib/groups/mls-codec.js +33 -1
  72. package/lib/groups/mls-encryptor.d.ts +2 -2
  73. package/lib/groups/mls-encryptor.js +33 -1
  74. package/lib/groups/mls-group-handle.d.ts +37 -2
  75. package/lib/groups/mls-group-handle.js +18 -1
  76. package/lib/groups/mls-json.js +17 -1
  77. package/lib/groups/mls-receive-errors.d.ts +27 -0
  78. package/lib/groups/mls-receive-errors.js +39 -0
  79. package/lib/groups/mls-state.d.ts +6 -4
  80. package/lib/groups/mls-state.js +79 -1
  81. package/lib/groups/peer-presence.d.ts +90 -0
  82. package/lib/groups/peer-presence.js +295 -0
  83. package/lib/groups/peer-selection.d.ts +37 -0
  84. package/lib/groups/peer-selection.js +72 -0
  85. package/lib/groups/roster-projection.d.ts +53 -0
  86. package/lib/groups/roster-projection.js +65 -0
  87. package/lib/groups/store-received-grant.d.ts +7 -3
  88. package/lib/groups/store-received-grant.js +161 -1
  89. package/lib/groups/store-received-revocation.d.ts +19 -16
  90. package/lib/groups/store-received-revocation.js +83 -1
  91. package/lib/hub/ack-backoff.d.ts +22 -0
  92. package/lib/hub/ack-backoff.js +88 -0
  93. package/lib/hub/http-client.d.ts +18 -10
  94. package/lib/hub/http-client.js +15 -1
  95. package/lib/hub/hub-like.d.ts +75 -0
  96. package/lib/hub/hub-like.js +1022 -0
  97. package/lib/hub/loopback-log-hub.d.ts +20 -0
  98. package/lib/hub/loopback-log-hub.js +178 -0
  99. package/lib/hub/manager.d.ts +25 -40
  100. package/lib/hub/manager.js +155 -1
  101. package/lib/hub/peer-scoped-hub-view.d.ts +35 -0
  102. package/lib/hub/peer-scoped-hub-view.js +92 -0
  103. package/lib/hub/sender-scoped-hub-view.d.ts +41 -0
  104. package/lib/hub/sender-scoped-hub-view.js +59 -0
  105. package/lib/hub/server-did.d.ts +67 -0
  106. package/lib/hub/server-did.js +131 -0
  107. package/lib/hub/wiring.d.ts +77 -51
  108. package/lib/hub/wiring.js +189 -1
  109. package/lib/index.d.ts +58 -22
  110. package/lib/index.js +563 -1
  111. package/lib/peer/connection-registry.d.ts +45 -0
  112. package/lib/peer/connection-registry.js +52 -0
  113. package/lib/peer/handlers.d.ts +69 -0
  114. package/lib/peer/handlers.js +110 -0
  115. package/lib/protocol.d.ts +214 -1
  116. package/lib/protocol.js +519 -1
  117. package/lib/schema.d.ts +3 -1
  118. package/lib/schema.js +1150 -39
  119. package/lib/sync/access-default-sender.d.ts +23 -0
  120. package/lib/sync/access-default-sender.js +80 -0
  121. package/lib/sync/authorize.d.ts +58 -4
  122. package/lib/sync/authorize.js +114 -1
  123. package/lib/sync/broadcast-queue.d.ts +3 -4
  124. package/lib/sync/broadcast-queue.js +182 -1
  125. package/lib/sync/broadcast-sender.js +179 -1
  126. package/lib/sync/catalog-scope.d.ts +8 -7
  127. package/lib/sync/catalog-scope.js +84 -1
  128. package/lib/sync/errors.d.ts +12 -2
  129. package/lib/sync/errors.js +29 -1
  130. package/lib/sync/forwarder.d.ts +13 -19
  131. package/lib/sync/forwarder.js +271 -1
  132. package/lib/sync/handlers.js +200 -1
  133. package/lib/sync/hub-tunnel-sync-listener.d.ts +19 -5
  134. package/lib/sync/hub-tunnel-sync-listener.js +261 -1
  135. package/lib/sync/hub-tunnel-sync-provider.d.ts +34 -14
  136. package/lib/sync/hub-tunnel-sync-provider.js +113 -1
  137. package/lib/sync/merkle-apply.d.ts +5 -0
  138. package/lib/sync/merkle-apply.js +194 -1
  139. package/lib/sync/merkle-channel.d.ts +11 -3
  140. package/lib/sync/merkle-channel.js +0 -0
  141. package/lib/sync/merkle-tree.js +120 -1
  142. package/lib/sync/peer-registry.d.ts +15 -11
  143. package/lib/sync/peer-registry.js +82 -1
  144. package/lib/sync/receive-access-gate.js +24 -1
  145. package/lib/sync/scope-resolver.js +41 -1
  146. package/lib/sync/sync-client.d.ts +14 -4
  147. package/lib/sync/sync-client.js +254 -1
  148. package/lib/sync/sync-manager.d.ts +24 -6
  149. package/lib/sync/sync-manager.js +301 -1
  150. package/lib/sync/tunnel-listeners.d.ts +52 -0
  151. package/lib/sync/tunnel-listeners.js +175 -0
  152. package/lib/sync/tunnel-observability.d.ts +25 -0
  153. package/lib/sync/tunnel-observability.js +63 -0
  154. package/lib/sync/tunnel-topics.d.ts +20 -0
  155. package/lib/sync/tunnel-topics.js +28 -0
  156. package/lib/types.d.ts +704 -22
  157. package/lib/types.js +1 -1
  158. package/lib/util/handler-error.d.ts +16 -0
  159. package/lib/util/handler-error.js +64 -0
  160. package/lib/util/mutex.d.ts +14 -1
  161. package/lib/util/mutex.js +44 -1
  162. package/lib/utils.js +21 -1
  163. package/package.json +58 -47
  164. package/lib/groups/broadcast-service.d.ts +0 -263
  165. package/lib/groups/broadcast-service.js +0 -1
  166. package/lib/groups/rejoin-codec.d.ts +0 -14
  167. package/lib/groups/rejoin-codec.js +0 -1
  168. package/lib/groups/wire-frame.d.ts +0 -34
  169. package/lib/groups/wire-frame.js +0 -1
  170. package/lib/hub/circle-catchup-requester.d.ts +0 -58
  171. package/lib/hub/circle-catchup-requester.js +0 -1
  172. package/lib/hub/circle-catchup-responder.d.ts +0 -50
  173. package/lib/hub/circle-catchup-responder.js +0 -1
  174. package/lib/hub/connection-pool.d.ts +0 -43
  175. package/lib/hub/connection-pool.js +0 -1
  176. package/lib/hub/did-observing-transport.d.ts +0 -64
  177. package/lib/hub/did-observing-transport.js +0 -1
  178. package/lib/hub/epoch-stale-detector.d.ts +0 -18
  179. package/lib/hub/epoch-stale-detector.js +0 -1
  180. package/lib/hub/errors.d.ts +0 -30
  181. package/lib/hub/errors.js +0 -1
  182. package/lib/hub/forward-remote-broadcast.d.ts +0 -15
  183. package/lib/hub/forward-remote-broadcast.js +0 -1
  184. package/lib/hub/group-channel.d.ts +0 -89
  185. package/lib/hub/group-channel.js +0 -1
  186. package/lib/hub/hub-connection.d.ts +0 -96
  187. package/lib/hub/hub-connection.js +0 -1
  188. package/lib/hub/receive-handler.d.ts +0 -51
  189. package/lib/hub/receive-handler.js +0 -1
  190. package/lib/hub/rejoin-manager.d.ts +0 -78
  191. package/lib/hub/rejoin-manager.js +0 -1
  192. package/lib/hub/rejoin-responder.d.ts +0 -32
  193. package/lib/hub/rejoin-responder.js +0 -1
  194. package/lib/hub/relay-manager.d.ts +0 -142
  195. package/lib/hub/relay-manager.js +0 -1
  196. package/lib/hub/send-handler.d.ts +0 -40
  197. package/lib/hub/send-handler.js +0 -1
  198. package/lib/hub/tunnel-inbox.d.ts +0 -20
  199. package/lib/hub/tunnel-inbox.js +0 -1
  200. package/lib/hub/wait-for-gate.d.ts +0 -14
  201. package/lib/hub/wait-for-gate.js +0 -1
package/lib/index.js CHANGED
@@ -1 +1,563 @@
1
- import{ServerTransport as e}from"@enkaku/http-server-transport";import{Server as r}from"@enkaku/server";import{isFullIdentity as t}from"@enkaku/token";import{DirectTransports as o}from"@enkaku/transport";import{p2pStoreDefinition as s}from"@kubun/store-p2p";import{createDelegationContext as n}from"./context/delegation.js";import{createGroupContext as a}from"./context/group.js";import{createHubContext as i}from"./context/hub.js";import{createJoinContext as c}from"./context/join.js";import{createSyncContext as p}from"./context/sync.js";import{createP2PEventEmitter as d}from"./groups/events.js";import{GroupHandleRegistry as u}from"./groups/group-handle-registry.js";import{GroupHealthMonitor as l}from"./groups/group-health-monitor.js";import{GroupManager as g}from"./groups/manager.js";import{forwardRemoteBroadcast as m}from"./hub/forward-remote-broadcast.js";import{createHTTPHubClient as y}from"./hub/http-client.js";import{captureServerDID as b}from"./hub/manager.js";import{DISABLED_HUB as f,setupHubRelay as h}from"./hub/wiring.js";import{createP2PSchemaExtension as j}from"./schema.js";import{createBroadcastQueue as v,DEFAULT_BROADCAST_BATCH_CONFIG as x}from"./sync/broadcast-queue.js";import{DEFAULT_PUSH_SYNC_CONFIG as w,wireBroadcastSender as S}from"./sync/broadcast-sender.js";import{createSyncHandlers as D}from"./sync/handlers.js";import{SyncManager as R}from"./sync/sync-manager.js";export{processBroadcast}from"./groups/broadcast.js";export{deserializeBroadcast,serializeBroadcast}from"./groups/broadcast-codec.js";export{ApplyError,BroadcastService,DecryptError}from"./groups/broadcast-service.js";export{createFilteredGenerator,createP2PEventEmitter}from"./groups/events.js";export{GroupHandleRegistry}from"./groups/group-handle-registry.js";export{decodeFullJoinRequest,decodeInvitePayload,decodeJoinRequest,encodeFullJoinRequest,encodeInvitePayload,encodeJoinRequest}from"./groups/invite-payload.js";export{GroupManager}from"./groups/manager.js";export{MLSEncryptor}from"./groups/mls-encryptor.js";export{restoreMLSGroupHandle}from"./groups/mls-group-handle.js";export{replacer as mlsJSONReplacer,reviver as mlsJSONReviver}from"./groups/mls-json.js";export{deserializeMLSGroupState,serializeMLSGroupState}from"./groups/mls-state.js";export{createP2PSchemaExtension}from"./schema.js";export{createBroadcastQueue,DEFAULT_BROADCAST_BATCH_CONFIG}from"./sync/broadcast-queue.js";export{DEFAULT_PUSH_SYNC_CONFIG,wireBroadcastSender}from"./sync/broadcast-sender.js";export{resolveCatalogSyncScopes}from"./sync/catalog-scope.js";export{SyncAccessDeniedError}from"./sync/errors.js";export{evaluateAndForward}from"./sync/forwarder.js";export{checkSyncDelegation}from"./sync/handlers.js";export{HubTunnelSyncListener}from"./sync/hub-tunnel-sync-listener.js";export{HubTunnelSyncProvider}from"./sync/hub-tunnel-sync-provider.js";export{negotiateDirection,PeerRegistry}from"./sync/peer-registry.js";export{SyncClient}from"./sync/sync-client.js";export{SyncManager}from"./sync/sync-manager.js";export{DIDObservingTransport}from"./hub/did-observing-transport.js";export{ReconnectingError}from"./hub/errors.js";export{createHTTPHubClient}from"./hub/http-client.js";export{bindHubToGroup,captureServerDID,createHub,deleteHub,getHub,HubServerDIDConflictError,listGroupsByHub,listHubs,listHubsByGroup,unbindHubFromGroup,updateHub,upsertHub}from"./hub/manager.js";export{HubRelayManager}from"./hub/relay-manager.js";export{DISABLED_HUB,setupHubRelay}from"./hub/wiring.js";export const DEFAULT_RECEIVE_CONFIG={storeUnreadable:"persist"};export function createP2PPlugin(P){return I=>{var E;let H,L,B,C;I.db.register(s);let G=I.db.adapter;if(!(t(E=I.identity)&&"privateKey"in E))throw Error("createP2PPlugin requires an OwnIdentity (full identity with privateKey) — received a narrower Identity");let A=I.identity,F=I.hlc,T={...DEFAULT_RECEIVE_CONFIG,...P?.receive},U=P?.defaultAccessLevel??{read:"only_owner",write:"only_owner"},_=new u({stores:I.db,deviceID:A.id,logger:I.getLogger("group-handle-registry")}),k=new g({identity:A,getRandomID:I.runtime.getRandomID,registry:_,hlc:F}),O=d(),q=new l({emitter:O}),N=new R({deployClusters:async e=>{let r=Object.values(e);await I.engine.deployGraph({clusters:r})},graph:I.graph,runtime:I.runtime,identity:I.identity,logger:I.getLogger("sync"),storeUnreadable:T.storeUnreadable,defaultAccessLevel:U}),M=D({db:I.db,graph:I.graph,logger:I.getLogger("sync-handlers"),peerRegistry:N.peerRegistry,selfDID:A.id,storeUnreadable:T.storeUnreadable,defaultAccessLevel:U}),J=new Map,z=[],K=I.getLogger("hub-relay"),V=f,W={...w,...P?.pushSync};if(P?.hub){let e="object"==typeof P.hub&&null!=P.hub.createHubClient?P.hub.createHubClient:y({identity:A,fetch:I.runtime.fetch});V=h({identity:A,getRandomID:I.runtime.getRandomID,db:I.db,hlc:F,graph:I.graph,emitter:O,createHubClient:e,registry:_,monitor:q,onRemoteBroadcast:e=>{m(O,e,K).catch(e=>{K.error("forwardRemoteBroadcast failed",{error:e})})},logger:K,storeUnreadable:T.storeUnreadable,defaultAccessLevel:U,forwarding:P.forwarding,onServerDIDObserved:({hubURL:e,serverDID:r})=>b({stores:I.db,hubURL:e,serverDID:r}).then(()=>void 0)}),W.enabled&&(L=v({config:{...x,...P.batch},scheduleBroadcast:V.scheduleBroadcast,logger:I.getLogger("broadcast-queue")}),H=S({db:I.db,eventBus:I.eventBus,selfDID:A.id,queue:L,logger:I.getLogger("broadcast-sender")}))}let Q={hubReady:V.ready,addPeer:e=>N.addPeer({config:e,stores:I.db}),removePeer:e=>N.removePeer({peerDID:e,stores:I.db}),listPeers:()=>N.listPeers({stores:I.db}),syncWithPeer:e=>N.merkleSyncWithPeer({...e,stores:I.db}),getStatus:e=>N.getStatus({peerDID:e,stores:I.db}),onSyncEvent:e=>N.onSyncEvent(e),createSyncTransport:function(e){let t=new o({signal:e}),s=new r({getRandomID:I.runtime.getRandomID,handlers:M,identity:A,accessRules:{"sync/*":{allow:!0}},logger:I.getLogger("sync-server"),transports:[t.server],signal:e});return z.push(s),t.client}};if(P?.http){let t=P.http,o=("string"==typeof t?t:"object"==typeof t?t.path??"sync":"sync").replace(/^\//,""),s="object"==typeof t?t.allowedOrigin??["*"]:["*"];Q.syncReady=(async()=>{let t=await I.engine.getAPI("http");await t.listening,B=new e({allowedOrigin:s}),C=new r({getRandomID:I.runtime.getRandomID,handlers:M,identity:A,accessRules:{"sync/*":{allow:!0}},logger:I.getLogger("sync-http-server"),transports:[B]}),z.push(C),t.registerProtocol(o,B.fetch.bind(B))})()}return{name:"p2p",schemaExtension:e=>j(O),api:Q,createContextFactory:()=>(e,r)=>{let t={identity:A,groupManager:k,registry:_,syncManager:N,stores:r,adapter:G,hlc:F,emitter:O,pendingJoinRequests:J,runtime:I.runtime,autoAcceptPeers:P?.autoAcceptPeers,scheduleBroadcast:V.scheduleBroadcast,broadcastNow:V.broadcastNow,scheduleSendCommit:V.scheduleSendCommit,monitor:q,rejoinGroup:V.rejoinGroup,logger:I.getLogger("context")};return{group:a(e,t),sync:p(e,t),join:c(e,t),hub:i(e,t),delegation:n(e,t)}},dispose:async()=>{if(null!=H)try{H()}catch{}if(null!=L)try{L.dispose()}catch{}let e=(async()=>{for(let e of z)await e.dispose();null!=B&&await B.dispose()})(),r=I.getLogger("dispose"),t=await Promise.allSettled([e,N.dispose(),V.dispose()]),o=["sync-http","sync-manager","hub"];t.forEach((e,t)=>{"rejected"===e.status&&r.error("dispose branch failed",{branch:o[t],error:e.reason})})}}}}
1
+ import { ServerTransport } from '@enkaku/http-serve';
2
+ import { Server } from '@enkaku/server';
3
+ import { DirectTransports } from '@enkaku/transport';
4
+ import { isFullIdentity } from '@kokuin/token';
5
+ import { resolveAllowedOrigin } from '@kubun/http-util';
6
+ import { getGraphStore } from '@kubun/store-graph';
7
+ import { p2pStoreDefinition } from '@kubun/store-p2p';
8
+ import { createDelegationContext } from './context/delegation.js';
9
+ import { createGroupContext } from './context/group.js';
10
+ import { createHubContext } from './context/hub.js';
11
+ import { createJoinContext } from './context/join.js';
12
+ import { createPeerContext, serveGroupInvite, servePushControl } from './context/peer.js';
13
+ import { createSyncContext } from './context/sync.js';
14
+ import { createP2PEventEmitter } from './groups/events.js';
15
+ import { GroupHandleRegistry } from './groups/group-handle-registry.js';
16
+ import { GroupHealthMonitor } from './groups/group-health-monitor.js';
17
+ import { GroupManager } from './groups/manager.js';
18
+ import { createHTTPHubClient } from './hub/http-client.js';
19
+ import { setupHubRelay } from './hub/wiring.js';
20
+ import { PeerConnectionRegistry } from './peer/connection-registry.js';
21
+ import { createPeerHandlers } from './peer/handlers.js';
22
+ import { createP2PSchemaExtension } from './schema.js';
23
+ import { wireAccessDefaultSender } from './sync/access-default-sender.js';
24
+ import { createBroadcastQueue, DEFAULT_BROADCAST_BATCH_CONFIG } from './sync/broadcast-queue.js';
25
+ import { DEFAULT_PUSH_SYNC_CONFIG, wireBroadcastSender } from './sync/broadcast-sender.js';
26
+ import { createSyncHandlers } from './sync/handlers.js';
27
+ import { SyncManager } from './sync/sync-manager.js';
28
+ export { GROUP_CONTROL_DENIED, LAST_GROUP_ADMIN, NOT_GROUP_ADMIN, requireGroupAdmin } from './context/require-admin.js';
29
+ export { ADMIN_ROLE_ENTRY_TYPE, foldAdminRoster } from './groups/admin-roster.js';
30
+ export { processBroadcast } from './groups/broadcast.js';
31
+ export { deserializeBroadcast, serializeBroadcast } from './groups/broadcast-codec.js';
32
+ export { createFilteredGenerator, createP2PEventEmitter } from './groups/events.js';
33
+ export { buildKubunGroupAnchorExtension, readRecoverySecret } from './groups/group-anchor.js';
34
+ export { GroupHandleRegistry } from './groups/group-handle-registry.js';
35
+ export { decodeFullJoinRequest, decodeInvitePayload, decodeJoinRequest, encodeFullJoinRequest, encodeInvitePayload, encodeJoinRequest } from './groups/invite-payload.js';
36
+ export { resolveJoinRequestDID } from './groups/join-request-identity.js';
37
+ export { entryOrd, foldLedgerEntries, ledgerEntryDigest, signLedgerEntry, verifyLedgerEntry } from './groups/ledger.js';
38
+ export { MLSEncryptor } from './groups/mls-encryptor.js';
39
+ export { restoreMLSGroupHandle } from './groups/mls-group-handle.js';
40
+ export { replacer as mlsJSONReplacer, reviver as mlsJSONReviver } from './groups/mls-json.js';
41
+ export { deserializeMLSGroupState, serializeMLSGroupState } from './groups/mls-state.js';
42
+ export { MERKLE_SYNC_PROTOCOL } from './groups/peer-selection.js';
43
+ export { createHTTPHubClient } from './hub/http-client.js';
44
+ export { bindHubToGroup, createHub, deleteHub, getHub, listGroupsByHub, listHubs, listHubsByGroup, unbindHubFromGroup, updateHub, upsertHub } from './hub/manager.js';
45
+ export { createHubServerDIDResolver, fetchHubInfo } from './hub/server-did.js';
46
+ export { setupHubRelay } from './hub/wiring.js';
47
+ export { PeerConnectionRegistry } from './peer/connection-registry.js';
48
+ export { createPeerHandlers } from './peer/handlers.js';
49
+ export { peerProtocol } from './protocol.js';
50
+ export { createP2PSchemaExtension } from './schema.js';
51
+ export { createBroadcastQueue, DEFAULT_BROADCAST_BATCH_CONFIG } from './sync/broadcast-queue.js';
52
+ export { DEFAULT_PUSH_SYNC_CONFIG, wireBroadcastSender } from './sync/broadcast-sender.js';
53
+ export { resolveCatalogSyncScopes } from './sync/catalog-scope.js';
54
+ export { SyncAccessDeniedError } from './sync/errors.js';
55
+ export { evaluateAndForward } from './sync/forwarder.js';
56
+ export { checkSyncDelegation } from './sync/handlers.js';
57
+ export { HubTunnelSyncListener } from './sync/hub-tunnel-sync-listener.js';
58
+ export { HubTunnelSyncProvider } from './sync/hub-tunnel-sync-provider.js';
59
+ export { negotiateDirection, PeerRegistry } from './sync/peer-registry.js';
60
+ export { SyncClient } from './sync/sync-client.js';
61
+ export { SyncManager } from './sync/sync-manager.js';
62
+ /**
63
+ * Default receive config. Under `'persist'` no gate is built and no extra
64
+ * DB lookups occur during receive.
65
+ */ export const DEFAULT_RECEIVE_CONFIG = {
66
+ storeUnreadable: 'persist'
67
+ };
68
+ function isOwnIdentity(identity) {
69
+ return isFullIdentity(identity) && 'privateKey' in identity;
70
+ }
71
+ export function createP2PPlugin(options) {
72
+ return (params)=>{
73
+ params.db.register(p2pStoreDefinition);
74
+ const adapter = params.db.adapter;
75
+ if (!isOwnIdentity(params.identity)) {
76
+ throw new Error('createP2PPlugin requires an OwnIdentity (full identity with privateKey) — received a narrower Identity');
77
+ }
78
+ const identity = params.identity;
79
+ // The engine's single device-wide clock. Sharing it across the p2p group/
80
+ // circle/member metadata paths (context, GroupManager, BroadcastService)
81
+ // keeps every write from this device on one monotonic counter, so two
82
+ // writes in the same millisecond mint strictly-increasing timestamps rather
83
+ // than identical ones (the second of which would silently lose under
84
+ // last-writer-wins).
85
+ const hlc = params.hlc;
86
+ // Computed up front so SyncManager + sync handlers + the hub-relay
87
+ // BroadcastService all see the same receive policy. Hot-path invariant:
88
+ // when `storeUnreadable === 'persist'` (default), no gate is built and
89
+ // no extra DB lookups occur.
90
+ const receiveConfig = {
91
+ ...DEFAULT_RECEIVE_CONFIG,
92
+ ...options?.receive
93
+ };
94
+ const defaultAccessLevel = options?.defaultAccessLevel ?? {
95
+ read: 'only_owner',
96
+ write: 'only_owner'
97
+ };
98
+ // Single canonical access point for the device's MLS GroupHandle instances.
99
+ // Threaded through GroupManager + BroadcastService + the tunnel
100
+ // listener/provider so all five mutation paths serialize through one
101
+ // per-group mutex with write-through persist.
102
+ const registry = new GroupHandleRegistry({
103
+ stores: params.db,
104
+ deviceID: identity.id,
105
+ logger: params.getLogger('group-handle-registry')
106
+ });
107
+ const emitter = createP2PEventEmitter();
108
+ // `stores` + `emitter` are the lane-side wiring: a commit's `onAccepted`
109
+ // fires decoupled from the resolver that started it, so it folds this
110
+ // device's own committed entries on the device-wide connection and emits the
111
+ // resulting domain events itself.
112
+ const groupManager = new GroupManager({
113
+ identity,
114
+ runtime: params.runtime,
115
+ registry,
116
+ hlc,
117
+ stores: params.db,
118
+ emitter,
119
+ logger: params.getLogger('group-manager')
120
+ });
121
+ const groupHealthMonitor = new GroupHealthMonitor({
122
+ emitter,
123
+ logger: params.getLogger('group-health-monitor')
124
+ });
125
+ const syncManager = new SyncManager({
126
+ deployClusters: async (clusters)=>{
127
+ // Values are ClusterModel objects serialized through the protocol —
128
+ // cast through unknown since SyncManager doesn't depend on protocol types.
129
+ const clusterModels = Object.values(clusters);
130
+ // Into the graphs this device already syncs on, never a fresh one. A
131
+ // received model deployed under a new id gets its tables and nothing
132
+ // else: the document lands and the device's own graph — the one its app
133
+ // queries — has no schema for it, so the model reads as absent while
134
+ // being present. Deployed WITHOUT `plugins`, which leaves each graph's
135
+ // existing extension SDL and plugin config untouched.
136
+ const graphStore = await getGraphStore(params.db);
137
+ const targets = (await graphStore.listGraphs()).filter((graph)=>graph.plugin_config != null && 'p2p' in graph.plugin_config);
138
+ if (targets.length === 0) {
139
+ params.getLogger('sync').warn('Received clusters but no p2p-enabled graph to deploy them into');
140
+ return;
141
+ }
142
+ for (const graph of targets){
143
+ await params.engine.deployGraph({
144
+ id: graph.id,
145
+ clusters: clusterModels
146
+ });
147
+ }
148
+ },
149
+ graph: params.graph,
150
+ runtime: params.runtime,
151
+ identity: params.identity,
152
+ logger: params.getLogger('sync'),
153
+ storeUnreadable: receiveConfig.storeUnreadable,
154
+ defaultAccessLevel
155
+ });
156
+ const syncHandlers = createSyncHandlers({
157
+ db: params.db,
158
+ graph: params.graph,
159
+ logger: params.getLogger('sync-handlers'),
160
+ peerRegistry: syncManager.peerRegistry,
161
+ selfDID: identity.id,
162
+ storeUnreadable: receiveConfig.storeUnreadable,
163
+ defaultAccessLevel
164
+ });
165
+ // In-memory registry of live peer connections, keyed by remote DID. Written
166
+ // by `connectPeer`; nothing persisted. Plugin-scoped so a connection
167
+ // outlives a single GraphQL request.
168
+ const peerConnections = new PeerConnectionRegistry({
169
+ logger: params.getLogger('peer-connections')
170
+ });
171
+ // The p2p plugin manages its own Enkaku server for sync handlers,
172
+ // separate from plugin-rpc's graph server.
173
+ const syncServers = [];
174
+ function createSyncTransport(signal) {
175
+ const directTransports = new DirectTransports({
176
+ signal
177
+ });
178
+ const server = new Server({
179
+ runtime: params.runtime,
180
+ handlers: syncHandlers,
181
+ identity,
182
+ // Open at the procedure layer by design: the sync protocol's
183
+ // authorization is enforced downstream, per document (read-ACL and
184
+ // write-delegation tokens), not by a coarse caller allow-list here.
185
+ accessRules: {
186
+ 'sync/*': {
187
+ allow: true
188
+ }
189
+ },
190
+ logger: params.getLogger('sync-server'),
191
+ transports: [
192
+ directTransports.server
193
+ ],
194
+ signal
195
+ });
196
+ syncServers.push(server);
197
+ return directTransports.client;
198
+ }
199
+ const hubRelayLogger = params.getLogger('hub-relay');
200
+ let unwireBroadcastSender;
201
+ let unwireAccessDefaultSender;
202
+ let broadcastQueue;
203
+ const pushSyncConfig = {
204
+ ...DEFAULT_PUSH_SYNC_CONFIG,
205
+ ...options?.pushSync
206
+ };
207
+ // The relay always runs: it is what gives every joined group a commit lane,
208
+ // including a group with no hub bound (which commits against an in-process
209
+ // loopback log). Hub clients are only ever spawned for a group that has a
210
+ // hub row, so a device configured without hubs opens no connection.
211
+ const createHubClient = typeof options?.hub === 'object' && options.hub.createHubClient != null ? options.hub.createHubClient : createHTTPHubClient({
212
+ identity,
213
+ fetch: params.runtime.fetch
214
+ });
215
+ const hubReconnectBackoff = typeof options?.hub === 'object' ? options.hub.reconnectBackoff : undefined;
216
+ const hub = setupHubRelay({
217
+ identity,
218
+ runtime: params.runtime,
219
+ db: params.db,
220
+ hlc,
221
+ graph: params.graph,
222
+ emitter,
223
+ createHubClient,
224
+ registry,
225
+ // Re-drive a lost `ledger` commit's surviving tokens through the group
226
+ // manager's own ledger write path, so the seam stays a consumer of it.
227
+ buildLedgerRedrive: (groupID, tokens, requestID)=>groupManager.buildEnactLedgerCommit({
228
+ groupID,
229
+ tokens,
230
+ requestID
231
+ }),
232
+ logger: hubRelayLogger,
233
+ storeUnreadable: receiveConfig.storeUnreadable,
234
+ defaultAccessLevel,
235
+ forwarding: options?.forwarding,
236
+ ...hubReconnectBackoff != null ? {
237
+ hubReconnectBackoff
238
+ } : {},
239
+ ...options?.tunnelIdleTimeoutMs != null ? {
240
+ tunnelIdleTimeoutMs: options.tunnelIdleTimeoutMs
241
+ } : {},
242
+ // The same handlers the direct and HTTP transports serve — a tunnel is a
243
+ // route, so it must not reach a different sync implementation.
244
+ syncHandlers: syncHandlers
245
+ });
246
+ // Turn a `tunnel://<groupID>/<peerDID>` route into a live session over the
247
+ // group's hub. Until this existed the scheme parsed and then failed at
248
+ // "Server resolver not configured", so every device could only be reached by
249
+ // URL — the topology this design retires.
250
+ //
251
+ // Route resolution only. A resolver hands back a transport; what the far end
252
+ // serves over it is decided per document there, so nothing here can widen
253
+ // what a peer may read.
254
+ syncManager.setServerResolver((serverID, route)=>{
255
+ if (route == null) {
256
+ // `direct://` — in-process peers, which only a test wires by hand.
257
+ return undefined;
258
+ }
259
+ // Built lazily: the hub relay binds asynchronously, and a resolver that
260
+ // waited for it here would have to be async, which this seam is not.
261
+ return {
262
+ createSyncTransport: async (signal)=>{
263
+ const provider = await hub.syncTransportTo(route.groupID, serverID);
264
+ if (provider == null) {
265
+ throw new Error(`No hub bound for group ${route.groupID}: cannot open a tunnel`);
266
+ }
267
+ return await provider.createSyncTransport(signal);
268
+ }
269
+ };
270
+ });
271
+ if (options?.hub) {
272
+ // Replicate this peer's own model access-defaults to co-members.
273
+ // Control-plane, not document push-sync: always wired when a hub exists so
274
+ // an owner's sharing policy reaches members that must evaluate its docs.
275
+ unwireAccessDefaultSender = wireAccessDefaultSender({
276
+ db: params.db,
277
+ eventBus: params.eventBus,
278
+ identity,
279
+ scheduleBroadcast: hub.scheduleBroadcast,
280
+ logger: params.getLogger('access-default-sender')
281
+ });
282
+ // Subscribe to locally-authored mutations and fan out per-scope
283
+ // `mutation:apply` broadcasts through the hub. Gated independently from
284
+ // the hub itself: the hub is needed for receive + pull regardless, but
285
+ // push-sync is opt-in via `pushSync.enabled`. Without the gate an
286
+ // unconfigured device would still accrue broadcast_log rows for sends that
287
+ // reach no hub-backed peer.
288
+ if (pushSyncConfig.enabled) {
289
+ const batchConfig = {
290
+ ...DEFAULT_BROADCAST_BATCH_CONFIG,
291
+ ...options.batch
292
+ };
293
+ broadcastQueue = createBroadcastQueue({
294
+ config: batchConfig,
295
+ scheduleBroadcast: hub.scheduleBroadcast,
296
+ logger: params.getLogger('broadcast-queue')
297
+ });
298
+ unwireBroadcastSender = wireBroadcastSender({
299
+ db: params.db,
300
+ eventBus: params.eventBus,
301
+ selfDID: identity.id,
302
+ queue: broadcastQueue,
303
+ logger: params.getLogger('broadcast-sender')
304
+ });
305
+ }
306
+ }
307
+ // Single builder for the per-request ContextDeps, shared by the GraphQL
308
+ // context factory and the peer-procedure runners. Reads `hub` at call time
309
+ // so the hub wiring assigned above is always current.
310
+ function buildContextDeps(stores) {
311
+ return {
312
+ identity,
313
+ groupManager,
314
+ registry,
315
+ syncManager,
316
+ peerConnections,
317
+ stores,
318
+ deviceStores: params.db,
319
+ adapter,
320
+ hlc,
321
+ emitter,
322
+ runtime: params.runtime,
323
+ autoAcceptPeers: options?.autoAcceptPeers,
324
+ ...options?.inviteCommitTimeoutMs != null ? {
325
+ inviteCommitTimeoutMs: options.inviteCommitTimeoutMs
326
+ } : {},
327
+ scheduleBroadcast: hub.scheduleBroadcast,
328
+ broadcastNow: hub.broadcastNow,
329
+ commitToGroup: hub.commitToGroup,
330
+ monitor: groupHealthMonitor,
331
+ rejoinGroup: hub.rejoinGroup,
332
+ requestLedgerCatchup: hub.requestLedgerCatchup,
333
+ logger: params.getLogger('context')
334
+ };
335
+ }
336
+ // The dance runners reuse the SAME join context ops the GraphQL mutations
337
+ // use, executed on behalf of the verified caller against the plugin's own
338
+ // DB (peer procedures run outside an engine transaction; `joinGroup` manages
339
+ // its own inner transaction). No MLS logic is reimplemented here.
340
+ const peerHandlers = createPeerHandlers({
341
+ db: params.db,
342
+ identity,
343
+ logger: params.getLogger('peer-handlers'),
344
+ autoAcceptPeers: options?.autoAcceptPeers,
345
+ runPrepareJoin: (callerDID)=>createJoinContext({
346
+ viewerDID: callerDID
347
+ }, buildContextDeps(params.db)).prepareRequest(),
348
+ runCompleteJoin: (callerDID, invitePayload)=>createJoinContext({
349
+ viewerDID: callerDID
350
+ }, buildContextDeps(params.db)).complete(invitePayload),
351
+ runInvite: (callerDID, groupID, joinRequest)=>serveGroupInvite(buildContextDeps(params.db), {
352
+ callerDID,
353
+ groupID,
354
+ joinRequest
355
+ }),
356
+ runPushControl: (callerDID, param)=>servePushControl(buildContextDeps(params.db), {
357
+ callerDID,
358
+ ...param
359
+ })
360
+ });
361
+ const pluginAPI = {
362
+ hubReady: hub.ready,
363
+ addPeer: (config)=>syncManager.addPeer({
364
+ config,
365
+ stores: params.db
366
+ }),
367
+ removePeer: (peerDID)=>syncManager.removePeer({
368
+ peerDID,
369
+ stores: params.db
370
+ }),
371
+ listPeers: ()=>syncManager.listPeers({
372
+ stores: params.db
373
+ }),
374
+ syncWithPeer: (syncParams)=>syncManager.merkleSyncWithPeer({
375
+ ...syncParams,
376
+ stores: params.db
377
+ }),
378
+ getStatus: (peerDID)=>syncManager.getStatus({
379
+ peerDID,
380
+ stores: params.db
381
+ }),
382
+ // Through the same context the GraphQL surface uses, as the peer-procedure
383
+ // runners do: the scopes an automatic catch-up asks for cannot drift from
384
+ // the ones an explicit one asks for if there is only one implementation.
385
+ catchUpWithBestPeer: (groupID)=>createSyncContext(null, buildContextDeps(params.db)).catchUpWithBestPeer(groupID),
386
+ listPeerDevices: (groupID)=>createSyncContext(null, buildContextDeps(params.db)).listPeerDevices(groupID),
387
+ onSyncEvent: (callback)=>syncManager.onSyncEvent(callback),
388
+ onHubServerDIDChanged: (listener)=>emitter.on('hubServerDIDChanged', listener),
389
+ createSyncTransport,
390
+ requestLedgerCatchup: (groupID, options)=>hub.requestLedgerCatchup(groupID, options),
391
+ setLocalPeerProfile: async (profile)=>{
392
+ await (await hub.presence()).setProfile(profile);
393
+ },
394
+ getLocalPeerProfile: async ()=>await (await hub.presence()).getProfile(),
395
+ gatherPeers: async (groupID, options)=>await (await hub.presence()).gather(groupID, options),
396
+ refreshPeerPresence: async (groupID, options)=>await (await hub.presence()).refresh(groupID, options),
397
+ retryHubConnection: async ()=>await hub.retryHubs(),
398
+ getPeerConnection: (peerDID)=>peerConnections.get(peerDID)
399
+ };
400
+ let httpSyncTransport;
401
+ let httpSyncServer;
402
+ let httpPeerTransport;
403
+ let httpPeerServer;
404
+ if (options?.http) {
405
+ // Capture in the outer scope so the union is narrowed before the async
406
+ // closure below (re-reading options.http inside the IIFE would widen it).
407
+ const httpOption = options.http;
408
+ const rawPath = typeof httpOption === 'string' ? httpOption : typeof httpOption === 'object' ? httpOption.path ?? 'sync' : 'sync';
409
+ const protocolName = rawPath.replace(/^\//, '');
410
+ // Default-deny cross-origin browser requests, consistent with the other
411
+ // HTTP surfaces. The sync server authenticates every request (signed
412
+ // Enkaku procedures), so CORS is defense-in-depth here, not the security
413
+ // boundary; non-browser sync clients send no Origin and are unaffected.
414
+ // Operators opt into cross-origin via the object form's `allowedOrigin`.
415
+ const allowedOrigin = resolveAllowedOrigin(typeof httpOption === 'object' ? httpOption.allowedOrigin : undefined);
416
+ // Maximum request body size, enforced by the transport (default 1 MiB,
417
+ // matching the Enkaku ServerTransport default).
418
+ const maxRequestBodySize = (typeof httpOption === 'object' ? httpOption.bodyLimit : undefined) ?? 1024 * 1024;
419
+ pluginAPI.syncReady = (async ()=>{
420
+ const httpAPI = await params.engine.getAPI('http');
421
+ await httpAPI.listening;
422
+ httpSyncTransport = new ServerTransport({
423
+ allowedOrigin,
424
+ maxRequestBodySize
425
+ });
426
+ httpSyncServer = new Server({
427
+ runtime: params.runtime,
428
+ handlers: syncHandlers,
429
+ identity,
430
+ // Open at the procedure layer by design: the sync protocol's
431
+ // authorization is enforced downstream, per document (read-ACL and
432
+ // write-delegation tokens), not by a coarse caller allow-list here.
433
+ accessRules: {
434
+ 'sync/*': {
435
+ allow: true
436
+ }
437
+ },
438
+ logger: params.getLogger('sync-http-server'),
439
+ transports: [
440
+ httpSyncTransport
441
+ ]
442
+ });
443
+ syncServers.push(httpSyncServer);
444
+ httpAPI.registerProtocol(protocolName, httpSyncTransport.fetch.bind(httpSyncTransport));
445
+ // Mount the peer protocol alongside sync, advertised under its own
446
+ // well-known name so a connecting device discovers it in `/info`. Same
447
+ // CORS + body-limit policy as the sync surface. Its procedure layer is
448
+ // open (`allow: true`); `peer/groups` gates the verified caller in the
449
+ // handler via `checkPeerAccess`, mirroring the dance mutations.
450
+ httpPeerTransport = new ServerTransport({
451
+ allowedOrigin,
452
+ maxRequestBodySize
453
+ });
454
+ httpPeerServer = new Server({
455
+ runtime: params.runtime,
456
+ handlers: peerHandlers,
457
+ identity,
458
+ // Open at the procedure layer, like the sync server: each peer handler
459
+ // gates the verified caller itself (forged-`sub` rejection +
460
+ // `checkPeerAccess`). Covers `peer/*` and the dance `group/*`.
461
+ accessRules: {
462
+ 'peer/*': {
463
+ allow: true
464
+ },
465
+ 'group/*': {
466
+ allow: true
467
+ }
468
+ },
469
+ logger: params.getLogger('peer-http-server'),
470
+ transports: [
471
+ httpPeerTransport
472
+ ]
473
+ });
474
+ httpAPI.registerProtocol('peer', httpPeerTransport.fetch.bind(httpPeerTransport));
475
+ })();
476
+ }
477
+ return {
478
+ name: 'p2p',
479
+ schemaExtension: (_config)=>createP2PSchemaExtension(emitter, syncManager, params.getLogger('p2p-schema')),
480
+ api: pluginAPI,
481
+ createContextFactory: ()=>{
482
+ return (ctx, stores)=>{
483
+ const deps = buildContextDeps(stores);
484
+ return {
485
+ group: createGroupContext(ctx, deps),
486
+ sync: createSyncContext(ctx, deps),
487
+ join: createJoinContext(ctx, deps),
488
+ hub: createHubContext(ctx, deps),
489
+ delegation: createDelegationContext(ctx, deps),
490
+ peer: createPeerContext(ctx, deps)
491
+ };
492
+ };
493
+ },
494
+ dispose: async ()=>{
495
+ // Unwire the broadcast-sender subscriber first so no new broadcasts
496
+ // are enqueued while teardown is in flight.
497
+ if (unwireBroadcastSender != null) {
498
+ try {
499
+ unwireBroadcastSender();
500
+ } catch {
501
+ // ignore
502
+ }
503
+ }
504
+ if (unwireAccessDefaultSender != null) {
505
+ try {
506
+ unwireAccessDefaultSender();
507
+ } catch {
508
+ // ignore
509
+ }
510
+ }
511
+ // Synchronously drain the batch queue BEFORE other teardown so any
512
+ // pending broadcasts ship through the still-live hub transport.
513
+ if (broadcastQueue != null) {
514
+ try {
515
+ broadcastQueue.dispose();
516
+ } catch {
517
+ // ignore
518
+ }
519
+ }
520
+ // Tear down every live outbound peer connection (client + transport).
521
+ const peerConnectionsPath = peerConnections.disposeAll();
522
+ // Sync + peer HTTP servers depend on their transports — dispose servers
523
+ // first, then the transports. Runs in parallel with syncManager and
524
+ // hub teardown, which have no such dependency.
525
+ const httpPath = (async ()=>{
526
+ for (const server of syncServers){
527
+ await server.dispose();
528
+ }
529
+ if (httpPeerServer != null) {
530
+ await httpPeerServer.dispose();
531
+ }
532
+ if (httpSyncTransport != null) {
533
+ await httpSyncTransport.dispose();
534
+ }
535
+ if (httpPeerTransport != null) {
536
+ await httpPeerTransport.dispose();
537
+ }
538
+ })();
539
+ const disposeLogger = params.getLogger('dispose');
540
+ const results = await Promise.allSettled([
541
+ httpPath,
542
+ peerConnectionsPath,
543
+ syncManager.dispose(),
544
+ hub.dispose()
545
+ ]);
546
+ const labels = [
547
+ 'sync-http',
548
+ 'peer-connections',
549
+ 'sync-manager',
550
+ 'hub'
551
+ ];
552
+ results.forEach((result, i)=>{
553
+ if (result.status === 'rejected') {
554
+ disposeLogger.error('dispose branch failed', {
555
+ branch: labels[i],
556
+ error: result.reason
557
+ });
558
+ }
559
+ });
560
+ }
561
+ };
562
+ };
563
+ }
@@ -0,0 +1,45 @@
1
+ import type { Client } from '@enkaku/client';
2
+ import type { ClientTransport } from '@enkaku/http-fetch';
3
+ import type { Logger } from '@kubun/logger';
4
+ import type { PeerProtocol } from '../protocol.js';
5
+ /**
6
+ * A live, in-memory connection to a remote peer. Holds the Enkaku client used
7
+ * to dispatch peer procedures plus the transport it rides, so the connection
8
+ * can be torn down cleanly on replacement or plugin dispose. Nothing here is
9
+ * persisted — a connection lives only for the process, keyed by `peerDID`.
10
+ */
11
+ export type PeerConnection = {
12
+ peerDID: string;
13
+ /** Base URL the peer was reached at (the `connectPeer` argument). */
14
+ endpoint: string;
15
+ /**
16
+ * Fully-resolved sync-protocol endpoint discovered from the peer's `/info`.
17
+ * Recorded at connect so `sharePeerGroup` can register the sync peer without
18
+ * re-fetching `/info` or assuming the default `/sync` path.
19
+ */
20
+ syncEndpoint: string;
21
+ client: Client<PeerProtocol>;
22
+ transport: ClientTransport<PeerProtocol>;
23
+ };
24
+ /**
25
+ * In-memory registry of live peer connections, keyed by `peerDID`. A second
26
+ * `connectPeer` to the same DID replaces the prior entry and disposes the old
27
+ * client + transport, so a stale connection is never leaked. Purely ephemeral:
28
+ * durable peer state (endpoint, DID) lives in the peer registry, written by the
29
+ * later dance ops — not here.
30
+ */
31
+ export declare class PeerConnectionRegistry {
32
+ #private;
33
+ constructor(params: {
34
+ logger: Logger;
35
+ });
36
+ /**
37
+ * Register a connection, replacing (and disposing) any existing connection
38
+ * for the same `peerDID`.
39
+ */
40
+ set(connection: PeerConnection): Promise<void>;
41
+ get(peerDID: string): PeerConnection | undefined;
42
+ list(): Array<PeerConnection>;
43
+ /** Dispose every held connection and clear the registry. */
44
+ disposeAll(): Promise<void>;
45
+ }