@kubun/plugin-p2p 0.15.0 → 0.16.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.
- package/lib/groups/did-cache-seed.d.ts +26 -0
- package/lib/groups/did-cache-seed.js +38 -0
- package/lib/groups/group-crypto.d.ts +8 -9
- package/lib/groups/group-crypto.js +51 -55
- package/lib/groups/group-handlers.d.ts +50 -14
- package/lib/groups/group-handlers.js +190 -24
- package/lib/groups/group-peer-manager.d.ts +52 -12
- package/lib/groups/group-peer-manager.js +215 -58
- package/lib/groups/group-protocols.d.ts +407 -0
- package/lib/groups/group-protocols.js +279 -11
- package/lib/groups/mls-codec.d.ts +19 -13
- package/lib/groups/mls-codec.js +28 -15
- package/lib/groups/peer-presence.d.ts +6 -0
- package/lib/groups/peer-presence.js +5 -0
- package/lib/hub/http-client.js +6 -1
- package/lib/hub/hub-like.js +5 -2
- package/lib/hub/loopback-log-hub.js +4 -1
- package/lib/hub/peer-scoped-hub-view.d.ts +6 -0
- package/lib/hub/peer-scoped-hub-view.js +11 -1
- package/lib/hub/wiring.d.ts +36 -7
- package/lib/hub/wiring.js +18 -0
- package/lib/index.js +95 -20
- package/lib/sync/hub-tunnel-service-listener.d.ts +9 -0
- package/lib/sync/hub-tunnel-service-listener.js +6 -1
- package/lib/sync/hub-tunnel-sync-listener.d.ts +8 -1
- package/lib/sync/hub-tunnel-sync-listener.js +6 -1
- package/lib/sync/service-tunnel-listeners.d.ts +7 -1
- package/lib/sync/service-tunnel-listeners.js +18 -0
- package/lib/sync/sync-manager.d.ts +1 -5
- package/lib/sync/sync-manager.js +0 -5
- package/lib/sync/tunnel-listeners.d.ts +7 -1
- package/lib/sync/tunnel-listeners.js +18 -0
- package/lib/types.d.ts +15 -1
- package/lib/types.js +4 -0
- package/package.json +48 -47
package/lib/hub/wiring.js
CHANGED
|
@@ -44,6 +44,12 @@ export function setupHubRelay(params) {
|
|
|
44
44
|
};
|
|
45
45
|
const presence = async ()=>(await ready).presence;
|
|
46
46
|
const retryHubs = async ()=>await (await ready).retryHubs();
|
|
47
|
+
const discoverWorkflows = async (groupID, options)=>await (await ready).discoverWorkflows(groupID, options);
|
|
48
|
+
const requestWorkflow = async (groupID, targetDID, method, param)=>await (await ready).requestWorkflow(groupID, targetDID, method, param);
|
|
49
|
+
const reauthorize = async (groupID)=>{
|
|
50
|
+
;
|
|
51
|
+
(await ready).reauthorize(groupID);
|
|
52
|
+
};
|
|
47
53
|
const syncTransportTo = async (groupID, peerDID)=>{
|
|
48
54
|
const hub = (await ready).tunnelHub(groupID);
|
|
49
55
|
if (hub == null) {
|
|
@@ -85,6 +91,12 @@ export function setupHubRelay(params) {
|
|
|
85
91
|
graphStore,
|
|
86
92
|
graph,
|
|
87
93
|
hlc,
|
|
94
|
+
...params.ready != null ? {
|
|
95
|
+
ready: params.ready
|
|
96
|
+
} : {},
|
|
97
|
+
...params.getWorkflowAPI != null ? {
|
|
98
|
+
getWorkflowAPI: params.getWorkflowAPI
|
|
99
|
+
} : {},
|
|
88
100
|
maxDriftMS,
|
|
89
101
|
localDID: identity.id,
|
|
90
102
|
identity,
|
|
@@ -117,6 +129,9 @@ export function setupHubRelay(params) {
|
|
|
117
129
|
...params.services != null ? {
|
|
118
130
|
services: params.services
|
|
119
131
|
} : {},
|
|
132
|
+
...params.cache != null ? {
|
|
133
|
+
cache: params.cache
|
|
134
|
+
} : {},
|
|
120
135
|
tunnelIdleTimeoutMs: tunnelIdleTimeoutMs ?? DEFAULT_TUNNEL_IDLE_TIMEOUT_MS
|
|
121
136
|
});
|
|
122
137
|
unsubscribes.push(emitter.on('groupJoined', (group)=>manager.addGroup(group.id).catch((error)=>{
|
|
@@ -184,6 +199,9 @@ export function setupHubRelay(params) {
|
|
|
184
199
|
requestLedgerCatchup,
|
|
185
200
|
presence,
|
|
186
201
|
retryHubs,
|
|
202
|
+
discoverWorkflows,
|
|
203
|
+
requestWorkflow,
|
|
204
|
+
reauthorize,
|
|
187
205
|
syncTransportTo,
|
|
188
206
|
serviceTransportTo,
|
|
189
207
|
dispose: async ()=>{
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ServerTransport } from '@enkaku/http-serve';
|
|
2
2
|
import { Server } from '@enkaku/server';
|
|
3
3
|
import { DirectTransports } from '@enkaku/transport';
|
|
4
|
-
import { isFullIdentity } from '@kokuin/token';
|
|
4
|
+
import { createInMemoryDIDCache, isFullIdentity } from '@kokuin/token';
|
|
5
5
|
import { createDeviceAuthority } from '@kubun/credential';
|
|
6
6
|
import { resolveAllowedOrigin } from '@kubun/http-util';
|
|
7
7
|
import { getGraphStore } from '@kubun/store-graph';
|
|
@@ -107,11 +107,10 @@ export function createP2PPlugin(options) {
|
|
|
107
107
|
}
|
|
108
108
|
const identity = params.identity;
|
|
109
109
|
// The engine's single device-wide clock. Sharing it across the p2p group/
|
|
110
|
-
// circle/member metadata paths
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
// last-writer-wins).
|
|
110
|
+
// circle/member metadata paths keeps every write from this device on one
|
|
111
|
+
// monotonic counter, so two writes in the same millisecond mint
|
|
112
|
+
// strictly-increasing timestamps rather than identical ones (the second of
|
|
113
|
+
// which would silently lose under last-writer-wins).
|
|
115
114
|
const hlc = params.hlc;
|
|
116
115
|
// Computed up front so SyncManager + sync handlers + the hub-relay
|
|
117
116
|
// BroadcastService all see the same receive policy. Hot-path invariant:
|
|
@@ -199,6 +198,13 @@ export function createP2PPlugin(options) {
|
|
|
199
198
|
const peerConnections = new PeerConnectionRegistry({
|
|
200
199
|
logger: params.getLogger('peer-connections')
|
|
201
200
|
});
|
|
201
|
+
// One plugin-lifetime DID cache, shared by every peer-lane Server. The
|
|
202
|
+
// tunnel listeners seed it from each group's MLS roster (a co-member's
|
|
203
|
+
// authenticated leaf long form), so a freshly-spawned per-session Server
|
|
204
|
+
// resolves a co-member's short-form `did:peer:4` issuer instead of defaulting
|
|
205
|
+
// its own empty cache. Content-addressed and monotonic — a stale entry is
|
|
206
|
+
// harmless, and it survives no process restart by design (re-seeded on bind).
|
|
207
|
+
const didCache = createInMemoryDIDCache();
|
|
202
208
|
// The p2p plugin manages its own Enkaku server for sync handlers,
|
|
203
209
|
// separate from the graph service's server.
|
|
204
210
|
const syncServers = [];
|
|
@@ -222,6 +228,7 @@ export function createP2PPlugin(options) {
|
|
|
222
228
|
transports: [
|
|
223
229
|
directTransports.server
|
|
224
230
|
],
|
|
231
|
+
cache: didCache,
|
|
225
232
|
signal
|
|
226
233
|
});
|
|
227
234
|
syncServers.push(server);
|
|
@@ -290,18 +297,33 @@ export function createP2PPlugin(options) {
|
|
|
290
297
|
runtime: params.runtime,
|
|
291
298
|
db: params.db,
|
|
292
299
|
hlc,
|
|
300
|
+
// The engine's identity gate, forwarded to the presence coordinator so a
|
|
301
|
+
// self-started announce waits for verification before it mints.
|
|
302
|
+
ready: ()=>params.engine.ready(),
|
|
303
|
+
// A thunk, not the resolved value: `getWorkflowAPI` is defined further down
|
|
304
|
+
// this factory body, so it is only in the temporal dead zone until the body
|
|
305
|
+
// finishes — and the manager invokes this on peer creation, long after.
|
|
306
|
+
getWorkflowAPI: ()=>getWorkflowAPI(),
|
|
293
307
|
maxDriftMS: params.maxDriftMS,
|
|
294
308
|
graph: params.graph,
|
|
295
309
|
emitter,
|
|
296
310
|
createHubClient,
|
|
297
311
|
registry,
|
|
298
312
|
// Re-drive a lost `ledger` commit's surviving tokens through the group
|
|
299
|
-
// manager's own ledger write path, so the seam stays a consumer of it.
|
|
300
|
-
|
|
313
|
+
// manager's own ledger write path, so the seam stays a consumer of it. The
|
|
314
|
+
// redrive is a self-start (the hub relay, not a request), so its commit
|
|
315
|
+
// build waits behind `ready()` before it mints under the engine's clock.
|
|
316
|
+
buildLedgerRedrive: (groupID, tokens, requestID)=>{
|
|
317
|
+
const build = groupManager.buildEnactLedgerCommit({
|
|
301
318
|
groupID,
|
|
302
319
|
tokens,
|
|
303
320
|
requestID
|
|
304
|
-
})
|
|
321
|
+
});
|
|
322
|
+
return async ()=>{
|
|
323
|
+
await params.engine.ready();
|
|
324
|
+
return build();
|
|
325
|
+
};
|
|
326
|
+
},
|
|
305
327
|
logger: hubRelayLogger,
|
|
306
328
|
storeUnreadable: receiveConfig.storeUnreadable,
|
|
307
329
|
defaultAccessLevel,
|
|
@@ -324,7 +346,9 @@ export function createP2PPlugin(options) {
|
|
|
324
346
|
...serviceServe != null ? {
|
|
325
347
|
serviceServe,
|
|
326
348
|
services: serviceConfig
|
|
327
|
-
} : {}
|
|
349
|
+
} : {},
|
|
350
|
+
// The shared roster-seeded cache reaches the tunnel listeners here.
|
|
351
|
+
cache: didCache
|
|
328
352
|
});
|
|
329
353
|
// Turn a `tunnel://<groupID>/<peerDID>` route into a live session over the
|
|
330
354
|
// group's hub. Until this existed the scheme parsed and then failed at
|
|
@@ -424,30 +448,47 @@ export function createP2PPlugin(options) {
|
|
|
424
448
|
// use, executed on behalf of the verified caller against the plugin's own
|
|
425
449
|
// DB (peer procedures run outside an engine transaction; `joinGroup` manages
|
|
426
450
|
// its own inner transaction). No MLS logic is reimplemented here.
|
|
451
|
+
// The inbound peer dance stamps group/roster/ledger metadata under the
|
|
452
|
+
// engine's clock outside any GraphQL write entry, so each runner is a
|
|
453
|
+
// self-start driver: it awaits `ready()` before touching the deps that mint.
|
|
454
|
+
// `ready()` is resolved for a booted engine, so this only delays a dance that
|
|
455
|
+
// arrives in the window before identity verification settles.
|
|
427
456
|
const peerHandlers = createPeerHandlers({
|
|
428
457
|
db: params.db,
|
|
429
458
|
identity,
|
|
430
459
|
logger: params.getLogger('peer-handlers'),
|
|
431
460
|
autoAcceptPeers: options?.autoAcceptPeers,
|
|
432
|
-
runPrepareJoin: (callerDID)=>
|
|
461
|
+
runPrepareJoin: async (callerDID)=>{
|
|
462
|
+
await params.engine.ready();
|
|
463
|
+
return createJoinContext(// Out-of-band peer-handler context for the verified caller; the join ops
|
|
433
464
|
// never consult the authority, so a self-scoped device authority fits.
|
|
434
465
|
{
|
|
435
466
|
viewerDID: callerDID,
|
|
436
467
|
credentialAuthority: createDeviceAuthority(callerDID)
|
|
437
|
-
}, buildContextDeps(params.db)).prepareRequest()
|
|
438
|
-
|
|
468
|
+
}, buildContextDeps(params.db)).prepareRequest();
|
|
469
|
+
},
|
|
470
|
+
runCompleteJoin: async (callerDID, invitePayload)=>{
|
|
471
|
+
await params.engine.ready();
|
|
472
|
+
return createJoinContext({
|
|
439
473
|
viewerDID: callerDID,
|
|
440
474
|
credentialAuthority: createDeviceAuthority(callerDID)
|
|
441
|
-
}, buildContextDeps(params.db)).complete(invitePayload)
|
|
442
|
-
|
|
475
|
+
}, buildContextDeps(params.db)).complete(invitePayload);
|
|
476
|
+
},
|
|
477
|
+
runInvite: async (callerDID, groupID, joinRequest)=>{
|
|
478
|
+
await params.engine.ready();
|
|
479
|
+
return serveGroupInvite(buildContextDeps(params.db), {
|
|
443
480
|
callerDID,
|
|
444
481
|
groupID,
|
|
445
482
|
joinRequest
|
|
446
|
-
})
|
|
447
|
-
|
|
483
|
+
});
|
|
484
|
+
},
|
|
485
|
+
runPushControl: async (callerDID, param)=>{
|
|
486
|
+
await params.engine.ready();
|
|
487
|
+
return servePushControl(buildContextDeps(params.db), {
|
|
448
488
|
callerDID,
|
|
449
489
|
...param
|
|
450
|
-
})
|
|
490
|
+
});
|
|
491
|
+
}
|
|
451
492
|
});
|
|
452
493
|
// Blob transfer control-lane handlers, mounted on the same peer server.
|
|
453
494
|
const blobHandlers = createBlobHandlers({
|
|
@@ -557,6 +598,7 @@ export function createP2PPlugin(options) {
|
|
|
557
598
|
// runners do: the scopes an automatic catch-up asks for cannot drift from
|
|
558
599
|
// the ones an explicit one asks for if there is only one implementation.
|
|
559
600
|
catchUpWithBestPeer: (groupID)=>createSyncContext(null, buildContextDeps(params.db)).catchUpWithBestPeer(groupID),
|
|
601
|
+
reauthorize: (groupID)=>hub.reauthorize(groupID),
|
|
560
602
|
listPeerDevices: (groupID)=>createSyncContext(null, buildContextDeps(params.db)).listPeerDevices(groupID),
|
|
561
603
|
onSyncEvent: (callback)=>syncManager.onSyncEvent(callback),
|
|
562
604
|
onHubServerDIDChanged: (listener)=>emitter.on('hubServerDIDChanged', listener),
|
|
@@ -567,6 +609,37 @@ export function createP2PPlugin(options) {
|
|
|
567
609
|
await (await hub.presence()).setProfile(profile);
|
|
568
610
|
},
|
|
569
611
|
getLocalPeerProfile: async ()=>await (await hub.presence()).getProfile(),
|
|
612
|
+
// The typed workflow control plane: `groupID` selects the peers, the hub
|
|
613
|
+
// wiring handles the multi-hub fan-out (discover) and directed `.to()`
|
|
614
|
+
// request (the rest). The wire results are shaped to the contract types
|
|
615
|
+
// here; a business refusal on a command travels as data, and a caller
|
|
616
|
+
// helper (plugin-workflow) reconstitutes it into a throw.
|
|
617
|
+
discoverWorkflows: (groupID, options)=>hub.discoverWorkflows(groupID, options),
|
|
618
|
+
listWorkflows: async (groupID, targetDID)=>// `workflow/list` takes no fields but its param schema is a (closed) object,
|
|
619
|
+
// so an omitted param serializes to an invalid request frame — pass `{}`.
|
|
620
|
+
await hub.requestWorkflow(groupID, targetDID, 'workflow/list', {}),
|
|
621
|
+
workflowStatus: async (groupID, targetDID, instanceID)=>await hub.requestWorkflow(groupID, targetDID, 'workflow/status', {
|
|
622
|
+
instanceID
|
|
623
|
+
}),
|
|
624
|
+
enqueueWorkflow: async (groupID, targetDID, name, params, opts)=>await hub.requestWorkflow(groupID, targetDID, 'workflow/enqueue', {
|
|
625
|
+
name,
|
|
626
|
+
params,
|
|
627
|
+
// A stable idempotency key makes the cross-hub retry safe: a directed
|
|
628
|
+
// request can throw on the RESPONSE leg (the target already enqueued,
|
|
629
|
+
// but the reply was lost), and the retry re-sends to the same mailbox.
|
|
630
|
+
// Under one key the target returns the existing instance instead of
|
|
631
|
+
// creating a second. A caller-supplied key wins; else one per call.
|
|
632
|
+
opts: {
|
|
633
|
+
...opts,
|
|
634
|
+
idempotencyKey: opts?.idempotencyKey ?? crypto.randomUUID()
|
|
635
|
+
}
|
|
636
|
+
}),
|
|
637
|
+
cancelWorkflow: async (groupID, targetDID, instanceID)=>await hub.requestWorkflow(groupID, targetDID, 'workflow/cancel', {
|
|
638
|
+
instanceID
|
|
639
|
+
}),
|
|
640
|
+
retryWorkflow: async (groupID, targetDID, instanceID)=>await hub.requestWorkflow(groupID, targetDID, 'workflow/retry', {
|
|
641
|
+
instanceID
|
|
642
|
+
}),
|
|
570
643
|
gatherPeers: async (groupID, options)=>await (await hub.presence()).gather(groupID, options),
|
|
571
644
|
refreshPeerPresence: async (groupID, options)=>await (await hub.presence()).refresh(groupID, options),
|
|
572
645
|
retryHubConnection: async ()=>await hub.retryHubs(),
|
|
@@ -629,7 +702,8 @@ export function createP2PPlugin(options) {
|
|
|
629
702
|
logger: params.getLogger('sync-http-server'),
|
|
630
703
|
transports: [
|
|
631
704
|
httpSyncTransport
|
|
632
|
-
]
|
|
705
|
+
],
|
|
706
|
+
cache: didCache
|
|
633
707
|
});
|
|
634
708
|
syncServers.push(httpSyncServer);
|
|
635
709
|
httpAPI.registerProtocol(protocolName, httpSyncTransport.fetch.bind(httpSyncTransport));
|
|
@@ -672,7 +746,8 @@ export function createP2PPlugin(options) {
|
|
|
672
746
|
logger: params.getLogger('peer-http-server'),
|
|
673
747
|
transports: [
|
|
674
748
|
httpPeerTransport
|
|
675
|
-
]
|
|
749
|
+
],
|
|
750
|
+
cache: didCache
|
|
676
751
|
});
|
|
677
752
|
httpAPI.registerProtocol('peer', httpPeerTransport.fetch.bind(httpPeerTransport));
|
|
678
753
|
})();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ServerTransportOf } from '@enkaku/protocol';
|
|
2
2
|
import type { Server } from '@enkaku/server';
|
|
3
|
+
import type { DIDCache } from '@kokuin/token';
|
|
3
4
|
import type { Logger } from '@kubun/logger';
|
|
4
5
|
import type { ServiceConfig, ServiceProtocol } from '@kubun/plugin-service-api';
|
|
5
6
|
import { type MailboxHub } from '@kumiai/hub-tunnel';
|
|
@@ -16,6 +17,8 @@ export type ServeService = (params: {
|
|
|
16
17
|
transport: ServerTransportOf<ServiceProtocol>;
|
|
17
18
|
services: Record<string, ServiceConfig>;
|
|
18
19
|
requireAuth?: false;
|
|
20
|
+
/** Shared, roster-seeded DID cache forwarded to the served Server. */
|
|
21
|
+
cache?: DIDCache;
|
|
19
22
|
}) => Server<ServiceProtocol>;
|
|
20
23
|
/**
|
|
21
24
|
* The injected {@link ServeService} was called before the `service-server`
|
|
@@ -55,6 +58,12 @@ export type HubTunnelServiceListenerParams = {
|
|
|
55
58
|
serve: ServeService;
|
|
56
59
|
/** Which services to serve, forwarded verbatim to {@link serve} on every spawn. */
|
|
57
60
|
services: Record<string, ServiceConfig>;
|
|
61
|
+
/**
|
|
62
|
+
* Shared, roster-seeded DID cache forwarded to {@link serve} on every spawn,
|
|
63
|
+
* so a co-member's short-form `did:peer:4` issuer resolves on the served
|
|
64
|
+
* Server.
|
|
65
|
+
*/
|
|
66
|
+
cache?: DIDCache;
|
|
58
67
|
idleTimeoutMs?: number;
|
|
59
68
|
logger?: Logger;
|
|
60
69
|
};
|
|
@@ -40,6 +40,7 @@ import { SERVICE_TUNNEL_PROTOCOL, tunnelTopic } from './tunnel-topics.js';
|
|
|
40
40
|
#runtime;
|
|
41
41
|
#serve;
|
|
42
42
|
#services;
|
|
43
|
+
#cache;
|
|
43
44
|
#idleTimeoutMs;
|
|
44
45
|
#logger;
|
|
45
46
|
#started = false;
|
|
@@ -64,6 +65,7 @@ import { SERVICE_TUNNEL_PROTOCOL, tunnelTopic } from './tunnel-topics.js';
|
|
|
64
65
|
this.#runtime = params.runtime;
|
|
65
66
|
this.#serve = params.serve;
|
|
66
67
|
this.#services = params.services;
|
|
68
|
+
this.#cache = params.cache;
|
|
67
69
|
this.#idleTimeoutMs = params.idleTimeoutMs;
|
|
68
70
|
this.#logger = params.logger;
|
|
69
71
|
}
|
|
@@ -225,7 +227,10 @@ import { SERVICE_TUNNEL_PROTOCOL, tunnelTopic } from './tunnel-topics.js';
|
|
|
225
227
|
try {
|
|
226
228
|
server = this.#serve({
|
|
227
229
|
transport,
|
|
228
|
-
services: this.#services
|
|
230
|
+
services: this.#services,
|
|
231
|
+
...this.#cache != null ? {
|
|
232
|
+
cache: this.#cache
|
|
233
|
+
} : {}
|
|
229
234
|
});
|
|
230
235
|
} catch (error) {
|
|
231
236
|
await transport.dispose().catch(()=>{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ProcedureHandlers } from '@enkaku/server';
|
|
2
|
-
import type { OwnIdentity } from '@kokuin/token';
|
|
2
|
+
import type { DIDCache, OwnIdentity } from '@kokuin/token';
|
|
3
3
|
import type { Logger } from '@kubun/logger';
|
|
4
4
|
import { type MailboxHub, type ObservabilityEventListener } from '@kumiai/hub-tunnel';
|
|
5
5
|
import type { Runtime } from '@sozai/runtime';
|
|
@@ -26,6 +26,13 @@ export type HubTunnelSyncListenerParams = {
|
|
|
26
26
|
* keeps one generator across the whole plugin.
|
|
27
27
|
*/
|
|
28
28
|
runtime: Runtime;
|
|
29
|
+
/**
|
|
30
|
+
* Shared, roster-seeded DID cache handed to every spawned session's Server, so
|
|
31
|
+
* a co-member's short-form `did:peer:4` issuer resolves without a resolver the
|
|
32
|
+
* blind hub lane has none of. A fresh per-session Server would otherwise
|
|
33
|
+
* default its own empty cache.
|
|
34
|
+
*/
|
|
35
|
+
cache?: DIDCache;
|
|
29
36
|
idleTimeoutMs?: number;
|
|
30
37
|
reconnectTimeoutMs?: number;
|
|
31
38
|
inboxCapacity?: number;
|
|
@@ -15,6 +15,7 @@ export class HubTunnelSyncListener {
|
|
|
15
15
|
#identity;
|
|
16
16
|
#syncHandlers;
|
|
17
17
|
#runtime;
|
|
18
|
+
#cache;
|
|
18
19
|
#idleTimeoutMs;
|
|
19
20
|
#reconnectTimeoutMs;
|
|
20
21
|
#inboxCapacity;
|
|
@@ -42,6 +43,7 @@ export class HubTunnelSyncListener {
|
|
|
42
43
|
this.#identity = params.identity;
|
|
43
44
|
this.#syncHandlers = params.syncHandlers;
|
|
44
45
|
this.#runtime = params.runtime;
|
|
46
|
+
this.#cache = params.cache;
|
|
45
47
|
this.#idleTimeoutMs = params.idleTimeoutMs;
|
|
46
48
|
this.#reconnectTimeoutMs = params.reconnectTimeoutMs;
|
|
47
49
|
this.#inboxCapacity = params.inboxCapacity;
|
|
@@ -203,7 +205,10 @@ export class HubTunnelSyncListener {
|
|
|
203
205
|
},
|
|
204
206
|
transports: [
|
|
205
207
|
transport
|
|
206
|
-
]
|
|
208
|
+
],
|
|
209
|
+
...this.#cache != null ? {
|
|
210
|
+
cache: this.#cache
|
|
211
|
+
} : {}
|
|
207
212
|
});
|
|
208
213
|
const session = {
|
|
209
214
|
transport,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OwnIdentity } from '@kokuin/token';
|
|
1
|
+
import type { DIDCache, OwnIdentity } from '@kokuin/token';
|
|
2
2
|
import type { StoreProvider } from '@kubun/db';
|
|
3
3
|
import type { Logger } from '@kubun/logger';
|
|
4
4
|
import type { ServiceConfig } from '@kubun/plugin-service-api';
|
|
@@ -17,6 +17,12 @@ export type ServiceTunnelListenersParams = {
|
|
|
17
17
|
serve: ServeService;
|
|
18
18
|
/** Which services to serve, forwarded verbatim to `serve` on every spawn. */
|
|
19
19
|
services: Record<string, ServiceConfig>;
|
|
20
|
+
/**
|
|
21
|
+
* Shared, plugin-lifetime DID cache. Seeded from each group's roster on every
|
|
22
|
+
* reconcile and forwarded to `serve` on every spawn, so a co-member's
|
|
23
|
+
* short-form `did:peer:4` issuer resolves on the served Server.
|
|
24
|
+
*/
|
|
25
|
+
cache?: DIDCache;
|
|
20
26
|
/** The group's device-wide drain, or `undefined` when no hub is bound. */
|
|
21
27
|
tunnelHub: (groupID: string) => MailboxHub | undefined;
|
|
22
28
|
logger?: Logger;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeDID } from '@kokuin/token';
|
|
2
2
|
import { getP2PStore } from '@kubun/store-p2p';
|
|
3
|
+
import { seedDIDCacheFromRoster } from '../groups/did-cache-seed.js';
|
|
3
4
|
import { HubTunnelServiceListener } from './hub-tunnel-service-listener.js';
|
|
4
5
|
/**
|
|
5
6
|
* The answering half of the service tunnel — one `HubTunnelServiceListener` per
|
|
@@ -63,6 +64,20 @@ import { HubTunnelServiceListener } from './hub-tunnel-service-listener.js';
|
|
|
63
64
|
}
|
|
64
65
|
attachedHubs.set(groupID, hub);
|
|
65
66
|
attachedEpochs.set(groupID, epoch);
|
|
67
|
+
// Bind and membership change both land here, so this is the one seam where a
|
|
68
|
+
// co-member's roster long form is refreshed into the shared cache before any
|
|
69
|
+
// service session that must resolve its short-form issuer can spawn.
|
|
70
|
+
if (params.cache != null) {
|
|
71
|
+
await seedDIDCacheFromRoster({
|
|
72
|
+
cache: params.cache,
|
|
73
|
+
registry: params.registry,
|
|
74
|
+
groupID,
|
|
75
|
+
stores: params.stores,
|
|
76
|
+
...logger != null ? {
|
|
77
|
+
logger
|
|
78
|
+
} : {}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
66
81
|
const members = await store.listGroupMembers(groupID);
|
|
67
82
|
const wanted = new Set(members.map((member)=>normalizeDID(member.member_did)).filter((did)=>did !== selfDID));
|
|
68
83
|
let group = listeners.get(groupID);
|
|
@@ -105,6 +120,9 @@ import { HubTunnelServiceListener } from './hub-tunnel-service-listener.js';
|
|
|
105
120
|
runtime: params.runtime,
|
|
106
121
|
serve: params.serve,
|
|
107
122
|
services: params.services,
|
|
123
|
+
...params.cache != null ? {
|
|
124
|
+
cache: params.cache
|
|
125
|
+
} : {},
|
|
108
126
|
...params.idleTimeoutMs != null ? {
|
|
109
127
|
idleTimeoutMs: params.idleTimeoutMs
|
|
110
128
|
} : {},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Identity
|
|
1
|
+
import { type Identity } from '@kokuin/token';
|
|
2
2
|
import type { StoreProvider } from '@kubun/db';
|
|
3
3
|
import type { DefaultAccessLevel, GraphInternals } from '@kubun/engine';
|
|
4
4
|
import type { Logger } from '@kubun/logger';
|
|
@@ -76,10 +76,6 @@ export declare class SyncManager {
|
|
|
76
76
|
#private;
|
|
77
77
|
constructor(params: SyncManagerParams);
|
|
78
78
|
get peerRegistry(): PeerRegistry;
|
|
79
|
-
/**
|
|
80
|
-
* Configure the signing identity used for sync operations.
|
|
81
|
-
*/
|
|
82
|
-
setIdentity(identity: SigningIdentity): void;
|
|
83
79
|
/**
|
|
84
80
|
* Configure the resolver for the transports built locally rather than dialled:
|
|
85
81
|
* `direct://` (in-process) and `tunnel://` (relayed by the group's hub, whose
|
package/lib/sync/sync-manager.js
CHANGED
|
@@ -36,11 +36,6 @@ export class SyncManager {
|
|
|
36
36
|
return this.#peerRegistry;
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
|
-
* Configure the signing identity used for sync operations.
|
|
40
|
-
*/ setIdentity(identity) {
|
|
41
|
-
this.#identity = identity;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
39
|
* Configure the resolver for the transports built locally rather than dialled:
|
|
45
40
|
* `direct://` (in-process) and `tunnel://` (relayed by the group's hub, whose
|
|
46
41
|
* group id arrives as the resolver's `route`).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProcedureHandlers } from '@enkaku/server';
|
|
2
|
-
import type { OwnIdentity } from '@kokuin/token';
|
|
2
|
+
import type { DIDCache, OwnIdentity } from '@kokuin/token';
|
|
3
3
|
import type { StoreProvider } from '@kubun/db';
|
|
4
4
|
import type { Logger } from '@kubun/logger';
|
|
5
5
|
import type { MailboxHub } from '@kumiai/hub-tunnel';
|
|
@@ -13,6 +13,12 @@ export type TunnelListenersParams = {
|
|
|
13
13
|
localDID: string;
|
|
14
14
|
runtime: Runtime;
|
|
15
15
|
syncHandlers: ProcedureHandlers<SyncProtocol>;
|
|
16
|
+
/**
|
|
17
|
+
* Shared, plugin-lifetime DID cache. Seeded from each group's roster on every
|
|
18
|
+
* reconcile (bind and membership change) and handed to every spawned session's
|
|
19
|
+
* Server, so a co-member's short-form `did:peer:4` issuer resolves there.
|
|
20
|
+
*/
|
|
21
|
+
cache?: DIDCache;
|
|
16
22
|
/** The group's device-wide drain, or `undefined` when no hub is bound. */
|
|
17
23
|
tunnelHub: (groupID: string) => MailboxHub | undefined;
|
|
18
24
|
logger?: Logger;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeDID } from '@kokuin/token';
|
|
2
2
|
import { getP2PStore } from '@kubun/store-p2p';
|
|
3
|
+
import { seedDIDCacheFromRoster } from '../groups/did-cache-seed.js';
|
|
3
4
|
import { HubTunnelSyncListener } from './hub-tunnel-sync-listener.js';
|
|
4
5
|
/**
|
|
5
6
|
* The answering half of the tunnel: one listener per co-member per group, so a
|
|
@@ -73,6 +74,20 @@ import { HubTunnelSyncListener } from './hub-tunnel-sync-listener.js';
|
|
|
73
74
|
}
|
|
74
75
|
attachedHubs.set(groupID, hub);
|
|
75
76
|
attachedEpochs.set(groupID, epoch);
|
|
77
|
+
// Bind and membership change both land here, so this is the one seam where a
|
|
78
|
+
// co-member's roster long form is refreshed into the shared cache before any
|
|
79
|
+
// session that must resolve its short-form issuer can spawn.
|
|
80
|
+
if (params.cache != null) {
|
|
81
|
+
await seedDIDCacheFromRoster({
|
|
82
|
+
cache: params.cache,
|
|
83
|
+
registry: params.registry,
|
|
84
|
+
groupID,
|
|
85
|
+
stores: params.stores,
|
|
86
|
+
...logger != null ? {
|
|
87
|
+
logger
|
|
88
|
+
} : {}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
76
91
|
const members = await store.listGroupMembers(groupID);
|
|
77
92
|
const wanted = new Set(members.map((member)=>normalizeDID(member.member_did)).filter((did)=>did !== selfDID));
|
|
78
93
|
let group = listeners.get(groupID);
|
|
@@ -115,6 +130,9 @@ import { HubTunnelSyncListener } from './hub-tunnel-sync-listener.js';
|
|
|
115
130
|
identity: params.identity,
|
|
116
131
|
syncHandlers: params.syncHandlers,
|
|
117
132
|
runtime: params.runtime,
|
|
133
|
+
...params.cache != null ? {
|
|
134
|
+
cache: params.cache
|
|
135
|
+
} : {},
|
|
118
136
|
...params.idleTimeoutMs != null ? {
|
|
119
137
|
idleTimeoutMs: params.idleTimeoutMs
|
|
120
138
|
} : {},
|
package/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ClientTransportOf } from '@enkaku/protocol';
|
|
2
|
+
import type { WorkflowControlPlaneAPI } from '@kubun/plugin-p2p-api';
|
|
2
3
|
import type { ServiceProtocol } from '@kubun/plugin-service-api';
|
|
3
4
|
import type { AdaptivePolicy } from '@kubun/plugin-workflow-api';
|
|
4
5
|
import type { AccessLevel } from '@kubun/store-graph';
|
|
@@ -1082,7 +1083,7 @@ export type PeerGatherOptions = {
|
|
|
1082
1083
|
* of the logs, and nothing branches on it.
|
|
1083
1084
|
*/
|
|
1084
1085
|
export type PresenceReason = 'hub-connected' | 'epoch-changed' | 'app-window-pruned' | 'requested';
|
|
1085
|
-
export type SyncPluginAPI = {
|
|
1086
|
+
export type SyncPluginAPI = WorkflowControlPlaneAPI & {
|
|
1086
1087
|
/** Resolves when the HTTP sync transport is registered (only present when http option is enabled). */
|
|
1087
1088
|
syncReady?: Promise<void>;
|
|
1088
1089
|
/**
|
|
@@ -1109,6 +1110,19 @@ export type SyncPluginAPI = {
|
|
|
1109
1110
|
* decides who, and an explicit `syncPeer` ignores the ranking entirely.
|
|
1110
1111
|
*/
|
|
1111
1112
|
catchUpWithBestPeer(groupID: string): Promise<PeerCatchUpData>;
|
|
1113
|
+
/**
|
|
1114
|
+
* Re-drive any group subscription this device's hub-peers latched as refused.
|
|
1115
|
+
*
|
|
1116
|
+
* A device subscribes its group topics the instant it joins, before the
|
|
1117
|
+
* APPLICATION-level authorization that gates them at the hub has landed — so
|
|
1118
|
+
* the hub answers `AuthorizationDeniedError` and the peer's mux latches it
|
|
1119
|
+
* permanent (a busy retry against an answer would be worse). This layer cannot
|
|
1120
|
+
* see the app's authorization state, so it cannot know on its own when to ask
|
|
1121
|
+
* again; the app calls this once it knows the device is authorized (e.g. its
|
|
1122
|
+
* membership row has replicated after pairing completes). Idempotent and a
|
|
1123
|
+
* no-op when nothing is refused — safe to call on every authorization change.
|
|
1124
|
+
*/
|
|
1125
|
+
reauthorize(groupID: string): Promise<void>;
|
|
1112
1126
|
/**
|
|
1113
1127
|
* Every device this one has heard announce in the group — the projection, and
|
|
1114
1128
|
* never a liveness answer. {@link SyncPluginAPI.gatherPeers} is what says who
|
package/lib/types.js
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
+
// The six caller-facing workflow control-plane methods (discover/list/status/
|
|
2
|
+
// enqueue/cancel/retry, each keyed by `groupID`) are contributed by
|
|
3
|
+
// WorkflowControlPlaneAPI so this plugin's public surface and the type-only
|
|
4
|
+
// contract plugin-workflow imports cannot drift.
|
|
1
5
|
export { };
|