@kubun/plugin-p2p 0.10.0 → 0.11.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 (136) hide show
  1. package/lib/context/delegation.js +130 -1
  2. package/lib/context/group.js +1319 -1
  3. package/lib/context/hub.js +150 -1
  4. package/lib/context/join.js +80 -1
  5. package/lib/context/sync.js +19 -1
  6. package/lib/context/types.d.ts +12 -3
  7. package/lib/context/types.js +28 -1
  8. package/lib/groups/admin-roster.d.ts +130 -0
  9. package/lib/groups/admin-roster.js +184 -0
  10. package/lib/groups/broadcast-codec.js +6 -1
  11. package/lib/groups/broadcast.d.ts +122 -38
  12. package/lib/groups/broadcast.js +627 -1
  13. package/lib/groups/circle-projection.d.ts +122 -0
  14. package/lib/groups/circle-projection.js +288 -0
  15. package/lib/groups/circle-reducers.d.ts +178 -0
  16. package/lib/groups/circle-reducers.js +198 -0
  17. package/lib/groups/events.d.ts +7 -2
  18. package/lib/groups/events.js +10 -1
  19. package/lib/groups/group-anchor.d.ts +63 -0
  20. package/lib/groups/group-anchor.js +107 -0
  21. package/lib/groups/group-crypto.d.ts +19 -0
  22. package/lib/groups/group-crypto.js +85 -0
  23. package/lib/groups/group-handle-registry.d.ts +69 -2
  24. package/lib/groups/group-handle-registry.js +243 -1
  25. package/lib/groups/group-handlers.d.ts +29 -0
  26. package/lib/groups/group-handlers.js +92 -0
  27. package/lib/groups/group-health-monitor.d.ts +56 -0
  28. package/lib/groups/group-health-monitor.js +139 -0
  29. package/lib/groups/group-mls.d.ts +35 -0
  30. package/lib/groups/group-mls.js +164 -0
  31. package/lib/groups/group-peer-manager.d.ts +108 -0
  32. package/lib/groups/group-peer-manager.js +344 -0
  33. package/lib/groups/group-protocols.d.ts +297 -0
  34. package/lib/groups/group-protocols.js +174 -0
  35. package/lib/groups/invite-payload.d.ts +1 -1
  36. package/lib/groups/invite-payload.js +26 -1
  37. package/lib/groups/join-utils.d.ts +1 -0
  38. package/lib/groups/join-utils.js +104 -1
  39. package/lib/groups/ledger-affected-events.d.ts +15 -0
  40. package/lib/groups/ledger-affected-events.js +96 -0
  41. package/lib/groups/ledger-entry.d.ts +37 -0
  42. package/lib/groups/ledger-entry.js +56 -0
  43. package/lib/groups/ledger-fold.d.ts +56 -0
  44. package/lib/groups/ledger-fold.js +53 -0
  45. package/lib/groups/manager.d.ts +29 -71
  46. package/lib/groups/manager.js +281 -1
  47. package/lib/groups/mls-codec.d.ts +1 -1
  48. package/lib/groups/mls-codec.js +18 -1
  49. package/lib/groups/mls-encryptor.d.ts +2 -2
  50. package/lib/groups/mls-encryptor.js +30 -1
  51. package/lib/groups/mls-group-handle.d.ts +1 -1
  52. package/lib/groups/mls-group-handle.js +10 -1
  53. package/lib/groups/mls-json.js +17 -1
  54. package/lib/groups/mls-receive-errors.d.ts +29 -0
  55. package/lib/groups/mls-receive-errors.js +42 -0
  56. package/lib/groups/mls-state.d.ts +1 -1
  57. package/lib/groups/mls-state.js +56 -1
  58. package/lib/groups/store-received-grant.d.ts +4 -1
  59. package/lib/groups/store-received-grant.js +116 -1
  60. package/lib/groups/store-received-revocation.d.ts +3 -3
  61. package/lib/groups/store-received-revocation.js +82 -1
  62. package/lib/hub/ack-backoff.d.ts +22 -0
  63. package/lib/hub/ack-backoff.js +88 -0
  64. package/lib/hub/did-observing-transport.d.ts +13 -2
  65. package/lib/hub/did-observing-transport.js +126 -1
  66. package/lib/hub/errors.d.ts +4 -8
  67. package/lib/hub/errors.js +33 -1
  68. package/lib/hub/http-client.d.ts +28 -3
  69. package/lib/hub/http-client.js +32 -1
  70. package/lib/hub/hub-like.d.ts +24 -0
  71. package/lib/hub/hub-like.js +535 -0
  72. package/lib/hub/manager.d.ts +5 -4
  73. package/lib/hub/manager.js +198 -1
  74. package/lib/hub/wiring.d.ts +28 -41
  75. package/lib/hub/wiring.js +132 -1
  76. package/lib/index.d.ts +29 -13
  77. package/lib/index.js +392 -1
  78. package/lib/protocol.d.ts +8 -1
  79. package/lib/protocol.js +251 -1
  80. package/lib/schema.d.ts +2 -1
  81. package/lib/schema.js +471 -2
  82. package/lib/sync/authorize.d.ts +25 -0
  83. package/lib/sync/authorize.js +73 -0
  84. package/lib/sync/broadcast-queue.js +183 -1
  85. package/lib/sync/broadcast-sender.js +179 -1
  86. package/lib/sync/catalog-scope.js +68 -1
  87. package/lib/sync/errors.d.ts +12 -0
  88. package/lib/sync/errors.js +19 -0
  89. package/lib/sync/forwarder.js +277 -1
  90. package/lib/sync/handlers.d.ts +3 -22
  91. package/lib/sync/handlers.js +160 -1
  92. package/lib/sync/hub-tunnel-sync-listener.d.ts +9 -3
  93. package/lib/sync/hub-tunnel-sync-listener.js +172 -1
  94. package/lib/sync/hub-tunnel-sync-provider.d.ts +14 -14
  95. package/lib/sync/hub-tunnel-sync-provider.js +64 -1
  96. package/lib/sync/merkle-apply.d.ts +5 -0
  97. package/lib/sync/merkle-apply.js +194 -1
  98. package/lib/sync/merkle-channel.d.ts +19 -0
  99. package/lib/sync/merkle-channel.js +182 -1
  100. package/lib/sync/merkle-tree.js +120 -1
  101. package/lib/sync/peer-registry.d.ts +8 -1
  102. package/lib/sync/peer-registry.js +107 -1
  103. package/lib/sync/receive-access-gate.js +24 -1
  104. package/lib/sync/scope-resolver.js +41 -1
  105. package/lib/sync/sync-client.d.ts +5 -3
  106. package/lib/sync/sync-client.js +192 -1
  107. package/lib/sync/sync-manager.d.ts +2 -2
  108. package/lib/sync/sync-manager.js +272 -1
  109. package/lib/types.d.ts +41 -0
  110. package/lib/types.js +2 -1
  111. package/lib/util/mutex.js +10 -1
  112. package/lib/utils.d.ts +5 -0
  113. package/lib/utils.js +21 -0
  114. package/package.json +48 -41
  115. package/lib/groups/broadcast-service.d.ts +0 -189
  116. package/lib/groups/broadcast-service.js +0 -1
  117. package/lib/groups/wire-frame.d.ts +0 -32
  118. package/lib/groups/wire-frame.js +0 -1
  119. package/lib/hub/connection-pool.d.ts +0 -43
  120. package/lib/hub/connection-pool.js +0 -1
  121. package/lib/hub/forward-remote-broadcast.d.ts +0 -15
  122. package/lib/hub/forward-remote-broadcast.js +0 -1
  123. package/lib/hub/group-channel.d.ts +0 -67
  124. package/lib/hub/group-channel.js +0 -1
  125. package/lib/hub/hub-connection.d.ts +0 -96
  126. package/lib/hub/hub-connection.js +0 -1
  127. package/lib/hub/receive-handler.d.ts +0 -33
  128. package/lib/hub/receive-handler.js +0 -1
  129. package/lib/hub/relay-manager.d.ts +0 -127
  130. package/lib/hub/relay-manager.js +0 -1
  131. package/lib/hub/send-handler.d.ts +0 -40
  132. package/lib/hub/send-handler.js +0 -1
  133. package/lib/hub/tunnel-inbox.d.ts +0 -20
  134. package/lib/hub/tunnel-inbox.js +0 -1
  135. package/lib/hub/wait-for-gate.d.ts +0 -14
  136. package/lib/hub/wait-for-gate.js +0 -1
package/lib/index.js CHANGED
@@ -1 +1,392 @@
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{HLC as s}from"@kubun/hlc";import{p2pStoreDefinition as n}from"@kubun/store-p2p";import{createDelegationContext as a}from"./context/delegation.js";import{createGroupContext as i}from"./context/group.js";import{createHubContext as c}from"./context/hub.js";import{createJoinContext as p}from"./context/join.js";import{createSyncContext as d}from"./context/sync.js";import{createP2PEventEmitter as u}from"./groups/events.js";import{GroupHandleRegistry as l}from"./groups/group-handle-registry.js";import{GroupManager as g}from"./groups/manager.js";import{forwardRemoteBroadcast as m}from"./hub/forward-remote-broadcast.js";import{createHTTPHubClient as b}from"./hub/http-client.js";import{captureServerDID as y}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 S,wireBroadcastSender as w}from"./sync/broadcast-sender.js";import{createSyncHandlers as D}from"./sync/handlers.js";import{SyncManager as P}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{evaluateAndForward}from"./sync/forwarder.js";export{checkSyncDelegation,negotiateDirection}from"./sync/handlers.js";export{HubTunnelSyncListener}from"./sync/hub-tunnel-sync-listener.js";export{HubTunnelSyncProvider}from"./sync/hub-tunnel-sync-provider.js";export{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(I){return R=>{var E;let H,L,B,C;R.db.register(n);let G=R.db.adapter;if(!(t(E=R.identity)&&"privateKey"in E))throw Error("createP2PPlugin requires an OwnIdentity (full identity with privateKey) — received a narrower Identity");let A=R.identity,F=new s({nodeID:A.id}),k={...DEFAULT_RECEIVE_CONFIG,...I?.receive},T=I?.defaultAccessLevel??{read:"only_owner",write:"only_owner"},U=new l({stores:R.db,deviceID:A.id,logger:R.getLogger("group-handle-registry")}),_=new g({identity:A,getRandomID:R.runtime.getRandomID,registry:U}),O=u(),q=new P({deployClusters:async e=>{let r=Object.values(e);await R.engine.deployGraph({clusters:r})},graph:R.graph,runtime:R.runtime,identity:R.identity,logger:R.getLogger("sync"),storeUnreadable:k.storeUnreadable,defaultAccessLevel:T}),N=D({db:R.db,graph:R.graph,logger:R.getLogger("sync-handlers"),peerRegistry:q.peerRegistry,selfDID:A.id,storeUnreadable:k.storeUnreadable,defaultAccessLevel:T}),M=new Map,J=[],z=R.getLogger("hub-relay"),K=f,V={...S,...I?.pushSync};if(I?.hub){let e="object"==typeof I.hub&&null!=I.hub.createHubClient?I.hub.createHubClient:b({identity:A,fetch:R.runtime.fetch});K=h({identity:A,db:R.db,graph:R.graph,emitter:O,createHubClient:e,registry:U,onRemoteBroadcast:e=>{m(O,e,z).catch(e=>{z.error("forwardRemoteBroadcast failed",{error:e})})},logger:z,storeUnreadable:k.storeUnreadable,defaultAccessLevel:T,forwarding:I.forwarding,onServerDIDObserved:({hubURL:e,serverDID:r})=>y({stores:R.db,hubURL:e,serverDID:r}).then(()=>void 0)}),V.enabled&&(L=v({config:{...x,...I.batch},scheduleBroadcast:K.scheduleBroadcast,logger:R.getLogger("broadcast-queue")}),H=w({db:R.db,eventBus:R.eventBus,selfDID:A.id,queue:L,logger:R.getLogger("broadcast-sender")}))}let W={hubReady:K.ready,addPeer:e=>q.addPeer({config:e,stores:R.db}),removePeer:e=>q.removePeer({peerDID:e,stores:R.db}),listPeers:()=>q.listPeers({stores:R.db}),syncWithPeer:e=>q.merkleSyncWithPeer({...e,stores:R.db}),getStatus:e=>q.getStatus({peerDID:e,stores:R.db}),onSyncEvent:e=>q.onSyncEvent(e),createSyncTransport:function(e){let t=new o({signal:e}),s=new r({getRandomID:R.runtime.getRandomID,handlers:N,logger:R.getLogger("sync-server"),transports:[t.server],signal:e});return J.push(s),t.client}};if(I?.http){let t="string"==typeof I.http?I.http.replace(/^\//,""):"sync";W.syncReady=(async()=>{let o=await R.engine.getAPI("http");await o.listening,B=new e({allowedOrigin:["*"]}),C=new r({getRandomID:R.runtime.getRandomID,handlers:N,logger:R.getLogger("sync-http-server"),transports:[B]}),J.push(C),o.registerProtocol(t,B.fetch.bind(B))})()}return{name:"p2p",schemaExtension:e=>j(O),api:W,createContextFactory:()=>(e,r)=>{let t={identity:A,groupManager:_,registry:U,syncManager:q,stores:r,adapter:G,hlc:F,emitter:O,pendingJoinRequests:M,runtime:R.runtime,autoAcceptPeers:I?.autoAcceptPeers,scheduleBroadcast:K.scheduleBroadcast,broadcastNow:K.broadcastNow,scheduleSendCommit:K.scheduleSendCommit,logger:R.getLogger("context")};return{group:i(e,t),sync:d(e,t),join:p(e,t),hub:c(e,t),delegation:a(e,t)}},dispose:async()=>{if(null!=H)try{H()}catch{}if(null!=L)try{L.dispose()}catch{}let e=(async()=>{for(let e of J)await e.dispose();null!=B&&await B.dispose()})(),r=R.getLogger("dispose"),t=await Promise.allSettled([e,q.dispose(),K.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 { p2pStoreDefinition } from '@kubun/store-p2p';
7
+ import { createDelegationContext } from './context/delegation.js';
8
+ import { createGroupContext } from './context/group.js';
9
+ import { createHubContext } from './context/hub.js';
10
+ import { createJoinContext } from './context/join.js';
11
+ import { createSyncContext } from './context/sync.js';
12
+ import { createP2PEventEmitter } from './groups/events.js';
13
+ import { GroupHandleRegistry } from './groups/group-handle-registry.js';
14
+ import { GroupHealthMonitor } from './groups/group-health-monitor.js';
15
+ import { GroupManager } from './groups/manager.js';
16
+ import { createHTTPHubClient } from './hub/http-client.js';
17
+ import { captureServerDID } from './hub/manager.js';
18
+ import { DISABLED_HUB, setupHubRelay } from './hub/wiring.js';
19
+ import { createP2PSchemaExtension } from './schema.js';
20
+ import { createBroadcastQueue, DEFAULT_BROADCAST_BATCH_CONFIG } from './sync/broadcast-queue.js';
21
+ import { DEFAULT_PUSH_SYNC_CONFIG, wireBroadcastSender } from './sync/broadcast-sender.js';
22
+ import { createSyncHandlers } from './sync/handlers.js';
23
+ import { SyncManager } from './sync/sync-manager.js';
24
+ // ---- Re-exports: groups ----
25
+ export { ADMIN_ROLE_ENTRY_TYPE, ADMIN_ROSTER_PROJECTION, adminRosterReducer, applyAdminEntryIncremental, foldAdminRoster, projectAdminRoster, projectAdminRosterFromStore, reprojectAdminRoster } from './groups/admin-roster.js';
26
+ export { processBroadcast } from './groups/broadcast.js';
27
+ export { deserializeBroadcast, serializeBroadcast } from './groups/broadcast-codec.js';
28
+ export { createFilteredGenerator, createP2PEventEmitter } from './groups/events.js';
29
+ export { groupAnchorCapabilities, readGroupAnchor } from './groups/group-anchor.js';
30
+ export { GroupHandleRegistry } from './groups/group-handle-registry.js';
31
+ export { decodeFullJoinRequest, decodeInvitePayload, decodeJoinRequest, encodeFullJoinRequest, encodeInvitePayload, encodeJoinRequest } from './groups/invite-payload.js';
32
+ export { ledgerEntryDigest, signLedgerEntry, verifyLedgerEntry } from './groups/ledger-entry.js';
33
+ export { foldLedger } from './groups/ledger-fold.js';
34
+ export { GroupManager } from './groups/manager.js';
35
+ export { MLSEncryptor } from './groups/mls-encryptor.js';
36
+ export { restoreMLSGroupHandle } from './groups/mls-group-handle.js';
37
+ export { replacer as mlsJSONReplacer, reviver as mlsJSONReviver } from './groups/mls-json.js';
38
+ export { deserializeMLSGroupState, serializeMLSGroupState } from './groups/mls-state.js';
39
+ export { createP2PSchemaExtension } from './schema.js';
40
+ // ---- Re-exports: sync ----
41
+ export { createBroadcastQueue, DEFAULT_BROADCAST_BATCH_CONFIG } from './sync/broadcast-queue.js';
42
+ export { DEFAULT_PUSH_SYNC_CONFIG, wireBroadcastSender } from './sync/broadcast-sender.js';
43
+ export { resolveCatalogSyncScopes } from './sync/catalog-scope.js';
44
+ export { SyncAccessDeniedError } from './sync/errors.js';
45
+ export { evaluateAndForward } from './sync/forwarder.js';
46
+ export { checkSyncDelegation } from './sync/handlers.js';
47
+ export { HubTunnelSyncListener } from './sync/hub-tunnel-sync-listener.js';
48
+ export { HubTunnelSyncProvider } from './sync/hub-tunnel-sync-provider.js';
49
+ export { negotiateDirection, PeerRegistry } from './sync/peer-registry.js';
50
+ export { SyncClient } from './sync/sync-client.js';
51
+ export { SyncManager } from './sync/sync-manager.js';
52
+ // ---- Re-exports: hub ----
53
+ export { DIDObservingTransport } from './hub/did-observing-transport.js';
54
+ export { createHTTPHubClient } from './hub/http-client.js';
55
+ export { bindHubToGroup, captureServerDID, createHub, deleteHub, getHub, HubServerDIDConflictError, listGroupsByHub, listHubs, listHubsByGroup, unbindHubFromGroup, updateHub, upsertHub } from './hub/manager.js';
56
+ export { DISABLED_HUB, setupHubRelay } from './hub/wiring.js';
57
+ /**
58
+ * Default receive config. Under `'persist'` no gate is built and no extra
59
+ * DB lookups occur during receive.
60
+ */ export const DEFAULT_RECEIVE_CONFIG = {
61
+ storeUnreadable: 'persist'
62
+ };
63
+ // ---- Plugin factory ----
64
+ function isOwnIdentity(identity) {
65
+ return isFullIdentity(identity) && 'privateKey' in identity;
66
+ }
67
+ export function createP2PPlugin(options) {
68
+ return (params)=>{
69
+ params.db.register(p2pStoreDefinition);
70
+ const adapter = params.db.adapter;
71
+ if (!isOwnIdentity(params.identity)) {
72
+ throw new Error('createP2PPlugin requires an OwnIdentity (full identity with privateKey) — received a narrower Identity');
73
+ }
74
+ const identity = params.identity;
75
+ // The engine's single device-wide clock. Sharing it across the p2p group/
76
+ // circle/member metadata paths (context, GroupManager, BroadcastService)
77
+ // keeps every write from this device on one monotonic counter, so two
78
+ // writes in the same millisecond mint strictly-increasing timestamps rather
79
+ // than identical ones (the second of which would silently lose under
80
+ // last-writer-wins).
81
+ const hlc = params.hlc;
82
+ // ---- Receive config ----
83
+ // Computed up front so SyncManager + sync handlers + the hub-relay
84
+ // BroadcastService all see the same receive policy. Hot-path invariant:
85
+ // when `storeUnreadable === 'persist'` (default), no gate is built and
86
+ // no extra DB lookups occur.
87
+ const receiveConfig = {
88
+ ...DEFAULT_RECEIVE_CONFIG,
89
+ ...options?.receive
90
+ };
91
+ const defaultAccessLevel = options?.defaultAccessLevel ?? {
92
+ read: 'only_owner',
93
+ write: 'only_owner'
94
+ };
95
+ // ---- Group handle registry ----
96
+ // Single canonical access point for the device's MLS GroupHandle instances.
97
+ // Threaded through GroupManager + BroadcastService + (Phase 4) tunnel
98
+ // listener/provider so all five mutation paths serialize through one
99
+ // per-group mutex with write-through persist.
100
+ const registry = new GroupHandleRegistry({
101
+ stores: params.db,
102
+ deviceID: identity.id,
103
+ logger: params.getLogger('group-handle-registry')
104
+ });
105
+ // ---- Group manager ----
106
+ const groupManager = new GroupManager({
107
+ identity,
108
+ getRandomID: params.runtime.getRandomID,
109
+ registry,
110
+ hlc
111
+ });
112
+ const emitter = createP2PEventEmitter();
113
+ const groupHealthMonitor = new GroupHealthMonitor({
114
+ emitter,
115
+ logger: params.getLogger('group-health-monitor')
116
+ });
117
+ // ---- Sync manager ----
118
+ const syncManager = new SyncManager({
119
+ deployClusters: async (clusters)=>{
120
+ // Values are ClusterModel objects serialized through the protocol —
121
+ // cast through unknown since SyncManager doesn't depend on protocol types.
122
+ const clusterModels = Object.values(clusters);
123
+ await params.engine.deployGraph({
124
+ clusters: clusterModels
125
+ });
126
+ },
127
+ graph: params.graph,
128
+ runtime: params.runtime,
129
+ identity: params.identity,
130
+ logger: params.getLogger('sync'),
131
+ storeUnreadable: receiveConfig.storeUnreadable,
132
+ defaultAccessLevel
133
+ });
134
+ const syncHandlers = createSyncHandlers({
135
+ db: params.db,
136
+ graph: params.graph,
137
+ logger: params.getLogger('sync-handlers'),
138
+ peerRegistry: syncManager.peerRegistry,
139
+ selfDID: identity.id,
140
+ storeUnreadable: receiveConfig.storeUnreadable,
141
+ defaultAccessLevel
142
+ });
143
+ // ---- Pending join requests (for CLI peering flow) ----
144
+ const pendingJoinRequests = new Map();
145
+ // ---- Sync server ----
146
+ // The p2p plugin manages its own Enkaku server for sync handlers,
147
+ // separate from plugin-rpc's graph server.
148
+ const syncServers = [];
149
+ function createSyncTransport(signal) {
150
+ const directTransports = new DirectTransports({
151
+ signal
152
+ });
153
+ const server = new Server({
154
+ getRandomID: params.runtime.getRandomID,
155
+ handlers: syncHandlers,
156
+ identity,
157
+ // Open at the procedure layer by design: the sync protocol's
158
+ // authorization is enforced downstream, per document (read-ACL and
159
+ // write-delegation tokens), not by a coarse caller allow-list here.
160
+ accessRules: {
161
+ 'sync/*': {
162
+ allow: true
163
+ }
164
+ },
165
+ logger: params.getLogger('sync-server'),
166
+ transports: [
167
+ directTransports.server
168
+ ],
169
+ signal
170
+ });
171
+ syncServers.push(server);
172
+ return directTransports.client;
173
+ }
174
+ // ---- Hub relay ----
175
+ const hubRelayLogger = params.getLogger('hub-relay');
176
+ let hub = DISABLED_HUB;
177
+ let unwireBroadcastSender;
178
+ let broadcastQueue;
179
+ const pushSyncConfig = {
180
+ ...DEFAULT_PUSH_SYNC_CONFIG,
181
+ ...options?.pushSync
182
+ };
183
+ if (options?.hub) {
184
+ const createHubClient = typeof options.hub === 'object' && options.hub.createHubClient != null ? options.hub.createHubClient : createHTTPHubClient({
185
+ identity,
186
+ fetch: params.runtime.fetch
187
+ });
188
+ hub = setupHubRelay({
189
+ identity,
190
+ getRandomID: params.runtime.getRandomID,
191
+ db: params.db,
192
+ hlc,
193
+ graph: params.graph,
194
+ emitter,
195
+ createHubClient,
196
+ registry,
197
+ logger: hubRelayLogger,
198
+ storeUnreadable: receiveConfig.storeUnreadable,
199
+ defaultAccessLevel,
200
+ // TOFU capture. The pool fires this once per fresh client when the
201
+ // first signed response arrives. Manager-level conflicts are caught
202
+ // and logged inside the pool; the pinned-DID mismatch path surfaces
203
+ // a typed error to callers via the transport wrapper.
204
+ onServerDIDObserved: ({ hubURL, serverDID })=>captureServerDID({
205
+ stores: params.db,
206
+ hubURL,
207
+ serverDID
208
+ }).then(()=>undefined)
209
+ });
210
+ // Subscribe to locally-authored mutations and fan out per-scope
211
+ // `mutation:apply` broadcasts through the hub. Gated independently from
212
+ // the hub itself: the hub is needed for receive + pull regardless, but
213
+ // push-sync is opt-in via `pushSync.enabled`. With `DISABLED_HUB` we'd
214
+ // also skip wiring (scheduleBroadcast no-ops + spurious broadcast_log
215
+ // rows), but the explicit gate here makes intent observable.
216
+ if (pushSyncConfig.enabled) {
217
+ const batchConfig = {
218
+ ...DEFAULT_BROADCAST_BATCH_CONFIG,
219
+ ...options.batch
220
+ };
221
+ broadcastQueue = createBroadcastQueue({
222
+ config: batchConfig,
223
+ scheduleBroadcast: hub.scheduleBroadcast,
224
+ logger: params.getLogger('broadcast-queue')
225
+ });
226
+ unwireBroadcastSender = wireBroadcastSender({
227
+ db: params.db,
228
+ eventBus: params.eventBus,
229
+ selfDID: identity.id,
230
+ queue: broadcastQueue,
231
+ logger: params.getLogger('broadcast-sender')
232
+ });
233
+ }
234
+ }
235
+ // ---- Plugin API ----
236
+ const pluginAPI = {
237
+ hubReady: hub.ready,
238
+ addPeer: (config)=>syncManager.addPeer({
239
+ config,
240
+ stores: params.db
241
+ }),
242
+ removePeer: (peerDID)=>syncManager.removePeer({
243
+ peerDID,
244
+ stores: params.db
245
+ }),
246
+ listPeers: ()=>syncManager.listPeers({
247
+ stores: params.db
248
+ }),
249
+ syncWithPeer: (syncParams)=>syncManager.merkleSyncWithPeer({
250
+ ...syncParams,
251
+ stores: params.db
252
+ }),
253
+ getStatus: (peerDID)=>syncManager.getStatus({
254
+ peerDID,
255
+ stores: params.db
256
+ }),
257
+ onSyncEvent: (callback)=>syncManager.onSyncEvent(callback),
258
+ createSyncTransport
259
+ };
260
+ // ---- HTTP sync transport (optional) ----
261
+ let httpSyncTransport;
262
+ let httpSyncServer;
263
+ if (options?.http) {
264
+ // Capture in the outer scope so the union is narrowed before the async
265
+ // closure below (re-reading options.http inside the IIFE would widen it).
266
+ const httpOption = options.http;
267
+ const rawPath = typeof httpOption === 'string' ? httpOption : typeof httpOption === 'object' ? httpOption.path ?? 'sync' : 'sync';
268
+ const protocolName = rawPath.replace(/^\//, '');
269
+ // Default-deny cross-origin browser requests, consistent with the other
270
+ // HTTP surfaces. The sync server authenticates every request (signed
271
+ // Enkaku procedures), so CORS is defense-in-depth here, not the security
272
+ // boundary; non-browser sync clients send no Origin and are unaffected.
273
+ // Operators opt into cross-origin via the object form's `allowedOrigin`.
274
+ const allowedOrigin = resolveAllowedOrigin(typeof httpOption === 'object' ? httpOption.allowedOrigin : undefined);
275
+ // Maximum request body size, enforced by the transport (default 1 MiB,
276
+ // matching the Enkaku ServerTransport default).
277
+ const maxRequestBodySize = (typeof httpOption === 'object' ? httpOption.bodyLimit : undefined) ?? 1024 * 1024;
278
+ pluginAPI.syncReady = (async ()=>{
279
+ const httpAPI = await params.engine.getAPI('http');
280
+ await httpAPI.listening;
281
+ httpSyncTransport = new ServerTransport({
282
+ allowedOrigin,
283
+ maxRequestBodySize
284
+ });
285
+ httpSyncServer = new Server({
286
+ getRandomID: params.runtime.getRandomID,
287
+ handlers: syncHandlers,
288
+ identity,
289
+ // Open at the procedure layer by design: the sync protocol's
290
+ // authorization is enforced downstream, per document (read-ACL and
291
+ // write-delegation tokens), not by a coarse caller allow-list here.
292
+ accessRules: {
293
+ 'sync/*': {
294
+ allow: true
295
+ }
296
+ },
297
+ logger: params.getLogger('sync-http-server'),
298
+ transports: [
299
+ httpSyncTransport
300
+ ]
301
+ });
302
+ syncServers.push(httpSyncServer);
303
+ httpAPI.registerProtocol(protocolName, httpSyncTransport.fetch.bind(httpSyncTransport));
304
+ })();
305
+ }
306
+ return {
307
+ name: 'p2p',
308
+ schemaExtension: (_config)=>createP2PSchemaExtension(emitter, params.getLogger('p2p-schema')),
309
+ api: pluginAPI,
310
+ createContextFactory: ()=>{
311
+ return (ctx, stores)=>{
312
+ const deps = {
313
+ identity,
314
+ groupManager,
315
+ registry,
316
+ syncManager,
317
+ stores,
318
+ adapter,
319
+ hlc,
320
+ emitter,
321
+ pendingJoinRequests,
322
+ runtime: params.runtime,
323
+ autoAcceptPeers: options?.autoAcceptPeers,
324
+ scheduleBroadcast: hub.scheduleBroadcast,
325
+ broadcastNow: hub.broadcastNow,
326
+ scheduleSendCommit: hub.scheduleSendCommit,
327
+ monitor: groupHealthMonitor,
328
+ rejoinGroup: hub.rejoinGroup,
329
+ logger: params.getLogger('context')
330
+ };
331
+ return {
332
+ group: createGroupContext(ctx, deps),
333
+ sync: createSyncContext(ctx, deps),
334
+ join: createJoinContext(ctx, deps),
335
+ hub: createHubContext(ctx, deps),
336
+ delegation: createDelegationContext(ctx, deps)
337
+ };
338
+ };
339
+ },
340
+ dispose: async ()=>{
341
+ // Unwire the broadcast-sender subscriber first so no new broadcasts
342
+ // are enqueued while teardown is in flight.
343
+ if (unwireBroadcastSender != null) {
344
+ try {
345
+ unwireBroadcastSender();
346
+ } catch {
347
+ // ignore
348
+ }
349
+ }
350
+ // Synchronously drain the batch queue BEFORE other teardown so any
351
+ // pending broadcasts ship through the still-live hub transport.
352
+ if (broadcastQueue != null) {
353
+ try {
354
+ broadcastQueue.dispose();
355
+ } catch {
356
+ // ignore
357
+ }
358
+ }
359
+ // Sync HTTP server depends on httpSyncTransport — dispose servers
360
+ // first, then the transport. Runs in parallel with syncManager and
361
+ // hub teardown, which have no such dependency.
362
+ const httpPath = (async ()=>{
363
+ for (const server of syncServers){
364
+ await server.dispose();
365
+ }
366
+ if (httpSyncTransport != null) {
367
+ await httpSyncTransport.dispose();
368
+ }
369
+ })();
370
+ const disposeLogger = params.getLogger('dispose');
371
+ const results = await Promise.allSettled([
372
+ httpPath,
373
+ syncManager.dispose(),
374
+ hub.dispose()
375
+ ]);
376
+ const labels = [
377
+ 'sync-http',
378
+ 'sync-manager',
379
+ 'hub'
380
+ ];
381
+ results.forEach((result, i)=>{
382
+ if (result.status === 'rejected') {
383
+ disposeLogger.error('dispose branch failed', {
384
+ branch: labels[i],
385
+ error: result.reason
386
+ });
387
+ }
388
+ });
389
+ }
390
+ };
391
+ };
392
+ }
package/lib/protocol.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { AnyClientMessageOf, AnyServerMessageOf } from '@enkaku/protocol';
2
- import type { FromSchema } from '@enkaku/schema';
2
+ import type { FromSchema } from '@sozai/schema';
3
3
  export declare const syncProtocol: {
4
4
  readonly 'sync/negotiate': {
5
5
  readonly type: "request";
@@ -129,6 +129,13 @@ export declare const syncProtocol: {
129
129
  readonly type: "string";
130
130
  readonly enum: readonly ["push", "pull", "both"];
131
131
  };
132
+ readonly delegationTokens: {
133
+ readonly type: "array";
134
+ readonly items: {
135
+ readonly type: "string";
136
+ };
137
+ readonly description: "Delegation JWTs authorizing the caller for non-owned scopes";
138
+ };
132
139
  };
133
140
  readonly required: readonly ["scopes", "excludedDocumentIDs", "tree", "direction"];
134
141
  readonly additionalProperties: false;