@kubun/plugin-p2p 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/context/group.js +1073 -746
- package/lib/context/hub.js +2 -1
- package/lib/context/join.js +50 -15
- package/lib/context/peer.d.ts +92 -0
- package/lib/context/peer.js +1216 -0
- package/lib/context/require-admin.d.ts +57 -0
- package/lib/context/require-admin.js +57 -0
- package/lib/context/sync.d.ts +12 -0
- package/lib/context/sync.js +234 -1
- package/lib/context/types.d.ts +56 -20
- package/lib/context/types.js +23 -3
- package/lib/groups/access-default-token.d.ts +44 -0
- package/lib/groups/access-default-token.js +103 -0
- package/lib/groups/admin-roster.d.ts +42 -117
- package/lib/groups/admin-roster.js +53 -168
- package/lib/groups/anchor-store.d.ts +15 -0
- package/lib/groups/anchor-store.js +37 -0
- package/lib/groups/app-cursor-store.d.ts +17 -0
- package/lib/groups/app-cursor-store.js +34 -0
- package/lib/groups/broadcast.d.ts +199 -81
- package/lib/groups/broadcast.js +443 -368
- package/lib/groups/catalog-token.d.ts +50 -0
- package/lib/groups/catalog-token.js +96 -0
- package/lib/groups/circle-projection.d.ts +32 -64
- package/lib/groups/circle-projection.js +95 -181
- package/lib/groups/circle-reducers.d.ts +71 -110
- package/lib/groups/circle-reducers.js +57 -127
- package/lib/groups/commit-adoption.d.ts +77 -0
- package/lib/groups/commit-adoption.js +108 -0
- package/lib/groups/commit-journal.d.ts +14 -0
- package/lib/groups/commit-journal.js +65 -0
- package/lib/groups/control-request.d.ts +66 -0
- package/lib/groups/control-request.js +101 -0
- package/lib/groups/events.d.ts +14 -1
- package/lib/groups/group-anchor.d.ts +18 -57
- package/lib/groups/group-anchor.js +15 -100
- package/lib/groups/group-crypto.d.ts +36 -13
- package/lib/groups/group-crypto.js +124 -50
- package/lib/groups/group-handle-registry.d.ts +106 -36
- package/lib/groups/group-handle-registry.js +351 -61
- package/lib/groups/group-handlers.d.ts +5 -4
- package/lib/groups/group-handlers.js +155 -29
- package/lib/groups/group-health-monitor.d.ts +1 -1
- package/lib/groups/group-health-monitor.js +1 -1
- package/lib/groups/group-mls.d.ts +35 -4
- package/lib/groups/group-mls.js +594 -62
- package/lib/groups/group-peer-manager.d.ts +212 -19
- package/lib/groups/group-peer-manager.js +1061 -69
- package/lib/groups/group-protocols.d.ts +485 -28
- package/lib/groups/group-protocols.js +289 -15
- package/lib/groups/invite-payload.d.ts +48 -0
- package/lib/groups/join-request-identity.d.ts +21 -0
- package/lib/groups/join-request-identity.js +43 -0
- package/lib/groups/join-utils.d.ts +60 -0
- package/lib/groups/join-utils.js +262 -0
- package/lib/groups/ledger-affected-events.d.ts +11 -6
- package/lib/groups/ledger-affected-events.js +15 -26
- package/lib/groups/ledger-commit-fold.d.ts +107 -0
- package/lib/groups/ledger-commit-fold.js +167 -0
- package/lib/groups/ledger-ingest.d.ts +124 -0
- package/lib/groups/ledger-ingest.js +212 -0
- package/lib/groups/ledger.d.ts +44 -0
- package/lib/groups/ledger.js +30 -0
- package/lib/groups/manager.d.ts +202 -19
- package/lib/groups/manager.js +623 -106
- package/lib/groups/mls-codec.d.ts +27 -6
- package/lib/groups/mls-codec.js +30 -15
- package/lib/groups/mls-encryptor.js +9 -6
- package/lib/groups/mls-group-handle.d.ts +37 -2
- package/lib/groups/mls-group-handle.js +12 -4
- package/lib/groups/mls-receive-errors.d.ts +0 -2
- package/lib/groups/mls-receive-errors.js +3 -6
- package/lib/groups/mls-state.d.ts +5 -3
- package/lib/groups/mls-state.js +27 -4
- package/lib/groups/peer-presence.d.ts +90 -0
- package/lib/groups/peer-presence.js +295 -0
- package/lib/groups/peer-selection.d.ts +37 -0
- package/lib/groups/peer-selection.js +72 -0
- package/lib/groups/roster-projection.d.ts +53 -0
- package/lib/groups/roster-projection.js +65 -0
- package/lib/groups/store-received-grant.d.ts +4 -3
- package/lib/groups/store-received-grant.js +80 -35
- package/lib/groups/store-received-revocation.d.ts +16 -13
- package/lib/groups/store-received-revocation.js +23 -22
- package/lib/hub/http-client.d.ts +10 -27
- package/lib/hub/http-client.js +2 -19
- package/lib/hub/hub-like.d.ts +57 -6
- package/lib/hub/hub-like.js +539 -52
- package/lib/hub/loopback-log-hub.d.ts +20 -0
- package/lib/hub/loopback-log-hub.js +178 -0
- package/lib/hub/manager.d.ts +25 -40
- package/lib/hub/manager.js +51 -94
- package/lib/hub/peer-scoped-hub-view.d.ts +35 -0
- package/lib/hub/peer-scoped-hub-view.js +92 -0
- package/lib/hub/sender-scoped-hub-view.d.ts +41 -0
- package/lib/hub/sender-scoped-hub-view.js +59 -0
- package/lib/hub/server-did.d.ts +67 -0
- package/lib/hub/server-did.js +131 -0
- package/lib/hub/wiring.d.ts +71 -14
- package/lib/hub/wiring.js +80 -23
- package/lib/index.d.ts +48 -13
- package/lib/index.js +257 -86
- package/lib/peer/connection-registry.d.ts +45 -0
- package/lib/peer/connection-registry.js +52 -0
- package/lib/peer/handlers.d.ts +69 -0
- package/lib/peer/handlers.js +110 -0
- package/lib/protocol.d.ts +213 -0
- package/lib/protocol.js +268 -0
- package/lib/schema.d.ts +2 -1
- package/lib/schema.js +805 -131
- package/lib/sync/access-default-sender.d.ts +23 -0
- package/lib/sync/access-default-sender.js +80 -0
- package/lib/sync/authorize.d.ts +57 -3
- package/lib/sync/authorize.js +45 -4
- package/lib/sync/broadcast-queue.d.ts +3 -4
- package/lib/sync/broadcast-queue.js +2 -3
- package/lib/sync/catalog-scope.d.ts +8 -7
- package/lib/sync/catalog-scope.js +55 -39
- package/lib/sync/errors.d.ts +9 -0
- package/lib/sync/errors.js +10 -0
- package/lib/sync/forwarder.d.ts +13 -19
- package/lib/sync/forwarder.js +13 -19
- package/lib/sync/handlers.js +54 -14
- package/lib/sync/hub-tunnel-sync-listener.d.ts +16 -6
- package/lib/sync/hub-tunnel-sync-listener.js +126 -37
- package/lib/sync/hub-tunnel-sync-provider.d.ts +31 -11
- package/lib/sync/hub-tunnel-sync-provider.js +68 -19
- package/lib/sync/merkle-channel.d.ts +9 -1
- package/lib/sync/merkle-channel.js +0 -0
- package/lib/sync/peer-registry.d.ts +14 -10
- package/lib/sync/peer-registry.js +0 -25
- package/lib/sync/sync-client.d.ts +11 -1
- package/lib/sync/sync-client.js +72 -10
- package/lib/sync/sync-manager.d.ts +22 -4
- package/lib/sync/sync-manager.js +39 -10
- package/lib/sync/tunnel-listeners.d.ts +52 -0
- package/lib/sync/tunnel-listeners.js +175 -0
- package/lib/sync/tunnel-observability.d.ts +25 -0
- package/lib/sync/tunnel-observability.js +63 -0
- package/lib/sync/tunnel-topics.d.ts +20 -0
- package/lib/sync/tunnel-topics.js +28 -0
- package/lib/types.d.ts +699 -28
- package/lib/types.js +0 -1
- package/lib/util/handler-error.d.ts +16 -0
- package/lib/util/handler-error.js +64 -0
- package/lib/util/mutex.d.ts +14 -1
- package/lib/util/mutex.js +37 -3
- package/package.json +59 -52
- package/lib/groups/ledger-entry.d.ts +0 -37
- package/lib/groups/ledger-entry.js +0 -56
- package/lib/groups/ledger-fold.d.ts +0 -56
- package/lib/groups/ledger-fold.js +0 -53
- package/lib/hub/did-observing-transport.d.ts +0 -75
- package/lib/hub/did-observing-transport.js +0 -126
- package/lib/hub/errors.d.ts +0 -24
- package/lib/hub/errors.js +0 -33
package/lib/index.js
CHANGED
|
@@ -3,41 +3,51 @@ import { Server } from '@enkaku/server';
|
|
|
3
3
|
import { DirectTransports } from '@enkaku/transport';
|
|
4
4
|
import { isFullIdentity } from '@kokuin/token';
|
|
5
5
|
import { resolveAllowedOrigin } from '@kubun/http-util';
|
|
6
|
+
import { getGraphStore } from '@kubun/store-graph';
|
|
6
7
|
import { p2pStoreDefinition } from '@kubun/store-p2p';
|
|
7
8
|
import { createDelegationContext } from './context/delegation.js';
|
|
8
9
|
import { createGroupContext } from './context/group.js';
|
|
9
10
|
import { createHubContext } from './context/hub.js';
|
|
10
11
|
import { createJoinContext } from './context/join.js';
|
|
12
|
+
import { createPeerContext, serveGroupInvite, servePushControl } from './context/peer.js';
|
|
11
13
|
import { createSyncContext } from './context/sync.js';
|
|
12
14
|
import { createP2PEventEmitter } from './groups/events.js';
|
|
13
15
|
import { GroupHandleRegistry } from './groups/group-handle-registry.js';
|
|
14
16
|
import { GroupHealthMonitor } from './groups/group-health-monitor.js';
|
|
15
17
|
import { GroupManager } from './groups/manager.js';
|
|
16
18
|
import { createHTTPHubClient } from './hub/http-client.js';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
+
import { setupHubRelay } from './hub/wiring.js';
|
|
20
|
+
import { PeerConnectionRegistry } from './peer/connection-registry.js';
|
|
21
|
+
import { createPeerHandlers } from './peer/handlers.js';
|
|
19
22
|
import { createP2PSchemaExtension } from './schema.js';
|
|
23
|
+
import { wireAccessDefaultSender } from './sync/access-default-sender.js';
|
|
20
24
|
import { createBroadcastQueue, DEFAULT_BROADCAST_BATCH_CONFIG } from './sync/broadcast-queue.js';
|
|
21
25
|
import { DEFAULT_PUSH_SYNC_CONFIG, wireBroadcastSender } from './sync/broadcast-sender.js';
|
|
22
26
|
import { createSyncHandlers } from './sync/handlers.js';
|
|
23
27
|
import { SyncManager } from './sync/sync-manager.js';
|
|
24
|
-
|
|
25
|
-
export { ADMIN_ROLE_ENTRY_TYPE,
|
|
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';
|
|
26
30
|
export { processBroadcast } from './groups/broadcast.js';
|
|
27
31
|
export { deserializeBroadcast, serializeBroadcast } from './groups/broadcast-codec.js';
|
|
28
32
|
export { createFilteredGenerator, createP2PEventEmitter } from './groups/events.js';
|
|
29
|
-
export {
|
|
33
|
+
export { buildKubunGroupAnchorExtension, readRecoverySecret } from './groups/group-anchor.js';
|
|
30
34
|
export { GroupHandleRegistry } from './groups/group-handle-registry.js';
|
|
31
35
|
export { decodeFullJoinRequest, decodeInvitePayload, decodeJoinRequest, encodeFullJoinRequest, encodeInvitePayload, encodeJoinRequest } from './groups/invite-payload.js';
|
|
32
|
-
export {
|
|
33
|
-
export {
|
|
34
|
-
export { GroupManager } from './groups/manager.js';
|
|
36
|
+
export { resolveJoinRequestDID } from './groups/join-request-identity.js';
|
|
37
|
+
export { entryOrd, foldLedgerEntries, ledgerEntryDigest, signLedgerEntry, verifyLedgerEntry } from './groups/ledger.js';
|
|
35
38
|
export { MLSEncryptor } from './groups/mls-encryptor.js';
|
|
36
39
|
export { restoreMLSGroupHandle } from './groups/mls-group-handle.js';
|
|
37
40
|
export { replacer as mlsJSONReplacer, reviver as mlsJSONReviver } from './groups/mls-json.js';
|
|
38
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';
|
|
39
50
|
export { createP2PSchemaExtension } from './schema.js';
|
|
40
|
-
// ---- Re-exports: sync ----
|
|
41
51
|
export { createBroadcastQueue, DEFAULT_BROADCAST_BATCH_CONFIG } from './sync/broadcast-queue.js';
|
|
42
52
|
export { DEFAULT_PUSH_SYNC_CONFIG, wireBroadcastSender } from './sync/broadcast-sender.js';
|
|
43
53
|
export { resolveCatalogSyncScopes } from './sync/catalog-scope.js';
|
|
@@ -49,18 +59,12 @@ export { HubTunnelSyncProvider } from './sync/hub-tunnel-sync-provider.js';
|
|
|
49
59
|
export { negotiateDirection, PeerRegistry } from './sync/peer-registry.js';
|
|
50
60
|
export { SyncClient } from './sync/sync-client.js';
|
|
51
61
|
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
62
|
/**
|
|
58
63
|
* Default receive config. Under `'persist'` no gate is built and no extra
|
|
59
64
|
* DB lookups occur during receive.
|
|
60
65
|
*/ export const DEFAULT_RECEIVE_CONFIG = {
|
|
61
66
|
storeUnreadable: 'persist'
|
|
62
67
|
};
|
|
63
|
-
// ---- Plugin factory ----
|
|
64
68
|
function isOwnIdentity(identity) {
|
|
65
69
|
return isFullIdentity(identity) && 'privateKey' in identity;
|
|
66
70
|
}
|
|
@@ -79,7 +83,6 @@ export function createP2PPlugin(options) {
|
|
|
79
83
|
// than identical ones (the second of which would silently lose under
|
|
80
84
|
// last-writer-wins).
|
|
81
85
|
const hlc = params.hlc;
|
|
82
|
-
// ---- Receive config ----
|
|
83
86
|
// Computed up front so SyncManager + sync handlers + the hub-relay
|
|
84
87
|
// BroadcastService all see the same receive policy. Hot-path invariant:
|
|
85
88
|
// when `storeUnreadable === 'persist'` (default), no gate is built and
|
|
@@ -92,9 +95,8 @@ export function createP2PPlugin(options) {
|
|
|
92
95
|
read: 'only_owner',
|
|
93
96
|
write: 'only_owner'
|
|
94
97
|
};
|
|
95
|
-
// ---- Group handle registry ----
|
|
96
98
|
// Single canonical access point for the device's MLS GroupHandle instances.
|
|
97
|
-
// Threaded through GroupManager + BroadcastService +
|
|
99
|
+
// Threaded through GroupManager + BroadcastService + the tunnel
|
|
98
100
|
// listener/provider so all five mutation paths serialize through one
|
|
99
101
|
// per-group mutex with write-through persist.
|
|
100
102
|
const registry = new GroupHandleRegistry({
|
|
@@ -102,27 +104,47 @@ export function createP2PPlugin(options) {
|
|
|
102
104
|
deviceID: identity.id,
|
|
103
105
|
logger: params.getLogger('group-handle-registry')
|
|
104
106
|
});
|
|
105
|
-
|
|
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.
|
|
106
112
|
const groupManager = new GroupManager({
|
|
107
113
|
identity,
|
|
108
|
-
|
|
114
|
+
runtime: params.runtime,
|
|
109
115
|
registry,
|
|
110
|
-
hlc
|
|
116
|
+
hlc,
|
|
117
|
+
stores: params.db,
|
|
118
|
+
emitter,
|
|
119
|
+
logger: params.getLogger('group-manager')
|
|
111
120
|
});
|
|
112
|
-
const emitter = createP2PEventEmitter();
|
|
113
121
|
const groupHealthMonitor = new GroupHealthMonitor({
|
|
114
122
|
emitter,
|
|
115
123
|
logger: params.getLogger('group-health-monitor')
|
|
116
124
|
});
|
|
117
|
-
// ---- Sync manager ----
|
|
118
125
|
const syncManager = new SyncManager({
|
|
119
126
|
deployClusters: async (clusters)=>{
|
|
120
127
|
// Values are ClusterModel objects serialized through the protocol —
|
|
121
128
|
// cast through unknown since SyncManager doesn't depend on protocol types.
|
|
122
129
|
const clusterModels = Object.values(clusters);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
+
}
|
|
126
148
|
},
|
|
127
149
|
graph: params.graph,
|
|
128
150
|
runtime: params.runtime,
|
|
@@ -140,9 +162,12 @@ export function createP2PPlugin(options) {
|
|
|
140
162
|
storeUnreadable: receiveConfig.storeUnreadable,
|
|
141
163
|
defaultAccessLevel
|
|
142
164
|
});
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
//
|
|
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
|
+
});
|
|
146
171
|
// The p2p plugin manages its own Enkaku server for sync handlers,
|
|
147
172
|
// separate from plugin-rpc's graph server.
|
|
148
173
|
const syncServers = [];
|
|
@@ -151,7 +176,7 @@ export function createP2PPlugin(options) {
|
|
|
151
176
|
signal
|
|
152
177
|
});
|
|
153
178
|
const server = new Server({
|
|
154
|
-
|
|
179
|
+
runtime: params.runtime,
|
|
155
180
|
handlers: syncHandlers,
|
|
156
181
|
identity,
|
|
157
182
|
// Open at the procedure layer by design: the sync protocol's
|
|
@@ -171,48 +196,95 @@ export function createP2PPlugin(options) {
|
|
|
171
196
|
syncServers.push(server);
|
|
172
197
|
return directTransports.client;
|
|
173
198
|
}
|
|
174
|
-
// ---- Hub relay ----
|
|
175
199
|
const hubRelayLogger = params.getLogger('hub-relay');
|
|
176
|
-
let hub = DISABLED_HUB;
|
|
177
200
|
let unwireBroadcastSender;
|
|
201
|
+
let unwireAccessDefaultSender;
|
|
178
202
|
let broadcastQueue;
|
|
179
203
|
const pushSyncConfig = {
|
|
180
204
|
...DEFAULT_PUSH_SYNC_CONFIG,
|
|
181
205
|
...options?.pushSync
|
|
182
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
|
+
});
|
|
183
271
|
if (options?.hub) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
hub = setupHubRelay({
|
|
189
|
-
identity,
|
|
190
|
-
getRandomID: params.runtime.getRandomID,
|
|
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({
|
|
191
276
|
db: params.db,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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)
|
|
277
|
+
eventBus: params.eventBus,
|
|
278
|
+
identity,
|
|
279
|
+
scheduleBroadcast: hub.scheduleBroadcast,
|
|
280
|
+
logger: params.getLogger('access-default-sender')
|
|
209
281
|
});
|
|
210
282
|
// Subscribe to locally-authored mutations and fan out per-scope
|
|
211
283
|
// `mutation:apply` broadcasts through the hub. Gated independently from
|
|
212
284
|
// the hub itself: the hub is needed for receive + pull regardless, but
|
|
213
|
-
// push-sync is opt-in via `pushSync.enabled`.
|
|
214
|
-
//
|
|
215
|
-
//
|
|
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.
|
|
216
288
|
if (pushSyncConfig.enabled) {
|
|
217
289
|
const batchConfig = {
|
|
218
290
|
...DEFAULT_BROADCAST_BATCH_CONFIG,
|
|
@@ -232,7 +304,60 @@ export function createP2PPlugin(options) {
|
|
|
232
304
|
});
|
|
233
305
|
}
|
|
234
306
|
}
|
|
235
|
-
//
|
|
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
|
+
});
|
|
236
361
|
const pluginAPI = {
|
|
237
362
|
hubReady: hub.ready,
|
|
238
363
|
addPeer: (config)=>syncManager.addPeer({
|
|
@@ -254,12 +379,28 @@ export function createP2PPlugin(options) {
|
|
|
254
379
|
peerDID,
|
|
255
380
|
stores: params.db
|
|
256
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),
|
|
257
387
|
onSyncEvent: (callback)=>syncManager.onSyncEvent(callback),
|
|
258
|
-
|
|
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)
|
|
259
399
|
};
|
|
260
|
-
// ---- HTTP sync transport (optional) ----
|
|
261
400
|
let httpSyncTransport;
|
|
262
401
|
let httpSyncServer;
|
|
402
|
+
let httpPeerTransport;
|
|
403
|
+
let httpPeerServer;
|
|
263
404
|
if (options?.http) {
|
|
264
405
|
// Capture in the outer scope so the union is narrowed before the async
|
|
265
406
|
// closure below (re-reading options.http inside the IIFE would widen it).
|
|
@@ -283,7 +424,7 @@ export function createP2PPlugin(options) {
|
|
|
283
424
|
maxRequestBodySize
|
|
284
425
|
});
|
|
285
426
|
httpSyncServer = new Server({
|
|
286
|
-
|
|
427
|
+
runtime: params.runtime,
|
|
287
428
|
handlers: syncHandlers,
|
|
288
429
|
identity,
|
|
289
430
|
// Open at the procedure layer by design: the sync protocol's
|
|
@@ -301,39 +442,52 @@ export function createP2PPlugin(options) {
|
|
|
301
442
|
});
|
|
302
443
|
syncServers.push(httpSyncServer);
|
|
303
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));
|
|
304
475
|
})();
|
|
305
476
|
}
|
|
306
477
|
return {
|
|
307
478
|
name: 'p2p',
|
|
308
|
-
schemaExtension: (_config)=>createP2PSchemaExtension(emitter, params.getLogger('p2p-schema')),
|
|
479
|
+
schemaExtension: (_config)=>createP2PSchemaExtension(emitter, syncManager, params.getLogger('p2p-schema')),
|
|
309
480
|
api: pluginAPI,
|
|
310
481
|
createContextFactory: ()=>{
|
|
311
482
|
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
|
-
};
|
|
483
|
+
const deps = buildContextDeps(stores);
|
|
331
484
|
return {
|
|
332
485
|
group: createGroupContext(ctx, deps),
|
|
333
486
|
sync: createSyncContext(ctx, deps),
|
|
334
487
|
join: createJoinContext(ctx, deps),
|
|
335
488
|
hub: createHubContext(ctx, deps),
|
|
336
|
-
delegation: createDelegationContext(ctx, deps)
|
|
489
|
+
delegation: createDelegationContext(ctx, deps),
|
|
490
|
+
peer: createPeerContext(ctx, deps)
|
|
337
491
|
};
|
|
338
492
|
};
|
|
339
493
|
},
|
|
@@ -347,6 +501,13 @@ export function createP2PPlugin(options) {
|
|
|
347
501
|
// ignore
|
|
348
502
|
}
|
|
349
503
|
}
|
|
504
|
+
if (unwireAccessDefaultSender != null) {
|
|
505
|
+
try {
|
|
506
|
+
unwireAccessDefaultSender();
|
|
507
|
+
} catch {
|
|
508
|
+
// ignore
|
|
509
|
+
}
|
|
510
|
+
}
|
|
350
511
|
// Synchronously drain the batch queue BEFORE other teardown so any
|
|
351
512
|
// pending broadcasts ship through the still-live hub transport.
|
|
352
513
|
if (broadcastQueue != null) {
|
|
@@ -356,25 +517,35 @@ export function createP2PPlugin(options) {
|
|
|
356
517
|
// ignore
|
|
357
518
|
}
|
|
358
519
|
}
|
|
359
|
-
//
|
|
360
|
-
|
|
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
|
|
361
524
|
// hub teardown, which have no such dependency.
|
|
362
525
|
const httpPath = (async ()=>{
|
|
363
526
|
for (const server of syncServers){
|
|
364
527
|
await server.dispose();
|
|
365
528
|
}
|
|
529
|
+
if (httpPeerServer != null) {
|
|
530
|
+
await httpPeerServer.dispose();
|
|
531
|
+
}
|
|
366
532
|
if (httpSyncTransport != null) {
|
|
367
533
|
await httpSyncTransport.dispose();
|
|
368
534
|
}
|
|
535
|
+
if (httpPeerTransport != null) {
|
|
536
|
+
await httpPeerTransport.dispose();
|
|
537
|
+
}
|
|
369
538
|
})();
|
|
370
539
|
const disposeLogger = params.getLogger('dispose');
|
|
371
540
|
const results = await Promise.allSettled([
|
|
372
541
|
httpPath,
|
|
542
|
+
peerConnectionsPath,
|
|
373
543
|
syncManager.dispose(),
|
|
374
544
|
hub.dispose()
|
|
375
545
|
]);
|
|
376
546
|
const labels = [
|
|
377
547
|
'sync-http',
|
|
548
|
+
'peer-connections',
|
|
378
549
|
'sync-manager',
|
|
379
550
|
'hub'
|
|
380
551
|
];
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory registry of live peer connections, keyed by `peerDID`. A second
|
|
3
|
+
* `connectPeer` to the same DID replaces the prior entry and disposes the old
|
|
4
|
+
* client + transport, so a stale connection is never leaked. Purely ephemeral:
|
|
5
|
+
* durable peer state (endpoint, DID) lives in the peer registry, written by the
|
|
6
|
+
* later dance ops — not here.
|
|
7
|
+
*/ export class PeerConnectionRegistry {
|
|
8
|
+
#connections = new Map();
|
|
9
|
+
#logger;
|
|
10
|
+
constructor(params){
|
|
11
|
+
this.#logger = params.logger;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Register a connection, replacing (and disposing) any existing connection
|
|
15
|
+
* for the same `peerDID`.
|
|
16
|
+
*/ async set(connection) {
|
|
17
|
+
const existing = this.#connections.get(connection.peerDID);
|
|
18
|
+
if (existing != null) {
|
|
19
|
+
await this.#dispose(existing);
|
|
20
|
+
}
|
|
21
|
+
this.#connections.set(connection.peerDID, connection);
|
|
22
|
+
}
|
|
23
|
+
get(peerDID) {
|
|
24
|
+
return this.#connections.get(peerDID);
|
|
25
|
+
}
|
|
26
|
+
list() {
|
|
27
|
+
return Array.from(this.#connections.values());
|
|
28
|
+
}
|
|
29
|
+
/** Dispose every held connection and clear the registry. */ async disposeAll() {
|
|
30
|
+
const all = Array.from(this.#connections.values());
|
|
31
|
+
this.#connections.clear();
|
|
32
|
+
await Promise.all(all.map((connection)=>this.#dispose(connection)));
|
|
33
|
+
}
|
|
34
|
+
async #dispose(connection) {
|
|
35
|
+
try {
|
|
36
|
+
await connection.client.dispose();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
this.#logger.debug('peer client dispose failed', {
|
|
39
|
+
peerDID: connection.peerDID,
|
|
40
|
+
error
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
await connection.transport.dispose();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
this.#logger.debug('peer transport dispose failed', {
|
|
47
|
+
peerDID: connection.peerDID,
|
|
48
|
+
error
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|