@kubun/plugin-p2p 0.15.0 → 0.15.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/groups/did-cache-seed.d.ts +26 -0
- package/lib/groups/did-cache-seed.js +38 -0
- package/lib/groups/group-peer-manager.d.ts +7 -1
- package/lib/groups/group-peer-manager.js +6 -0
- package/lib/hub/http-client.js +6 -1
- package/lib/hub/hub-like.js +5 -2
- 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 +7 -1
- package/lib/hub/wiring.js +3 -0
- package/lib/index.js +16 -4
- 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/tunnel-listeners.d.ts +7 -1
- package/lib/sync/tunnel-listeners.js +18 -0
- package/package.json +5 -5
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type DIDCache } from '@kokuin/token';
|
|
2
|
+
import type { StoreProvider } from '@kubun/db';
|
|
3
|
+
import type { Logger } from '@kubun/logger';
|
|
4
|
+
import type { GroupHandleRegistry } from './group-handle-registry.js';
|
|
5
|
+
export type SeedDIDCacheParams = {
|
|
6
|
+
cache: DIDCache;
|
|
7
|
+
registry: GroupHandleRegistry;
|
|
8
|
+
groupID: string;
|
|
9
|
+
stores?: StoreProvider;
|
|
10
|
+
logger?: Logger;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Seed the shared DID cache with every `did:peer:4` co-member's document, read
|
|
14
|
+
* off the MLS roster's authenticated leaf long forms.
|
|
15
|
+
*
|
|
16
|
+
* A co-member's short-form `iss` on a peer-lane token carries no document, so a
|
|
17
|
+
* freshly-spawned tunnel/sync Server cannot resolve it without this. The roster
|
|
18
|
+
* leaf is the one authenticated source of the long form — the membership row
|
|
19
|
+
* holds only the normalized short form, which carries none.
|
|
20
|
+
*
|
|
21
|
+
* Monotonic and idempotent: a peer:4 doc is content-addressed, so re-seeding an
|
|
22
|
+
* existing entry is a no-op and a departed member's lingering doc is harmless
|
|
23
|
+
* (per-scope authorization gates access downstream, not cache presence). So the
|
|
24
|
+
* membership-change seam only ever adds, never evicts.
|
|
25
|
+
*/
|
|
26
|
+
export declare function seedDIDCacheFromRoster(params: SeedDIDCacheParams): Promise<void>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { decodePeer4, isPeer4 } from '@kokuin/token';
|
|
2
|
+
/**
|
|
3
|
+
* Seed the shared DID cache with every `did:peer:4` co-member's document, read
|
|
4
|
+
* off the MLS roster's authenticated leaf long forms.
|
|
5
|
+
*
|
|
6
|
+
* A co-member's short-form `iss` on a peer-lane token carries no document, so a
|
|
7
|
+
* freshly-spawned tunnel/sync Server cannot resolve it without this. The roster
|
|
8
|
+
* leaf is the one authenticated source of the long form — the membership row
|
|
9
|
+
* holds only the normalized short form, which carries none.
|
|
10
|
+
*
|
|
11
|
+
* Monotonic and idempotent: a peer:4 doc is content-addressed, so re-seeding an
|
|
12
|
+
* existing entry is a no-op and a departed member's lingering doc is harmless
|
|
13
|
+
* (per-scope authorization gates access downstream, not cache presence). So the
|
|
14
|
+
* membership-change seam only ever adds, never evicts.
|
|
15
|
+
*/ export async function seedDIDCacheFromRoster(params) {
|
|
16
|
+
const { cache, registry, groupID, stores, logger } = params;
|
|
17
|
+
const members = await registry.readHandle(groupID, (handle)=>handle.listMembers(), stores == null ? undefined : {
|
|
18
|
+
stores
|
|
19
|
+
});
|
|
20
|
+
for (const member of members){
|
|
21
|
+
// did:key members resolve natively; only a genuine peer:4 long form (≠ the
|
|
22
|
+
// short-form `id`) carries a document worth caching.
|
|
23
|
+
if (!isPeer4(member.longForm) || member.longForm === member.id) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const { shortForm, doc } = decodePeer4(member.longForm);
|
|
28
|
+
await cache.set(shortForm, doc);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
// A single unparseable leaf must not abort seeding the rest of the roster.
|
|
31
|
+
logger?.warn('did-cache roster seed skipped a member', {
|
|
32
|
+
groupID,
|
|
33
|
+
id: member.id,
|
|
34
|
+
error
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -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 { DefaultAccessLevel, GraphInternals } from '@kubun/engine';
|
|
5
5
|
import type { HLC } from '@kubun/hlc';
|
|
@@ -132,6 +132,12 @@ export type GroupPeerManagerParams = {
|
|
|
132
132
|
serviceServe?: ServeService;
|
|
133
133
|
/** Which services to serve on an inbound service-lane session. */
|
|
134
134
|
services?: Record<string, ServiceConfig>;
|
|
135
|
+
/**
|
|
136
|
+
* Shared, plugin-lifetime DID cache forwarded to the sync- and service-tunnel
|
|
137
|
+
* listeners, which seed it from each group's roster and hand it to every
|
|
138
|
+
* spawned session's Server (see {@link TunnelListenersParams.cache}).
|
|
139
|
+
*/
|
|
140
|
+
cache?: DIDCache;
|
|
135
141
|
/** @see TunnelListenersParams.idleTimeoutMs */
|
|
136
142
|
tunnelIdleTimeoutMs?: number;
|
|
137
143
|
};
|
|
@@ -913,6 +913,9 @@ export function createGroupPeerManager(params) {
|
|
|
913
913
|
localDID: params.localDID,
|
|
914
914
|
runtime: tunnelRuntime,
|
|
915
915
|
syncHandlers: params.syncHandlers,
|
|
916
|
+
...params.cache != null ? {
|
|
917
|
+
cache: params.cache
|
|
918
|
+
} : {},
|
|
916
919
|
tunnelHub: (groupID)=>{
|
|
917
920
|
const hubURL = bindings.get(groupID)?.values().next().value;
|
|
918
921
|
return hubURL == null ? undefined : getHubLike(hubURL);
|
|
@@ -932,6 +935,9 @@ export function createGroupPeerManager(params) {
|
|
|
932
935
|
runtime: tunnelRuntime,
|
|
933
936
|
serve: params.serviceServe,
|
|
934
937
|
services: params.services,
|
|
938
|
+
...params.cache != null ? {
|
|
939
|
+
cache: params.cache
|
|
940
|
+
} : {},
|
|
935
941
|
tunnelHub: (groupID)=>{
|
|
936
942
|
const hubURL = bindings.get(groupID)?.values().next().value;
|
|
937
943
|
return hubURL == null ? undefined : getHubLike(hubURL);
|
package/lib/hub/http-client.js
CHANGED
|
@@ -9,7 +9,12 @@ export function createHTTPHubClient(params) {
|
|
|
9
9
|
return new Client({
|
|
10
10
|
transport,
|
|
11
11
|
identity: params.identity,
|
|
12
|
-
serverID: opts?.serverID ?? params.serverID
|
|
12
|
+
serverID: opts?.serverID ?? params.serverID,
|
|
13
|
+
// The hub is a blind relay: no MLS roster, and a `Server` whose DID cache
|
|
14
|
+
// cannot be relied on across this client's reconnect churn. Embed the long
|
|
15
|
+
// form on every hub-facing token so a `did:peer:4` device's short-form
|
|
16
|
+
// `iss` self-resolves at the hub instead of throwing `Unknown DID`.
|
|
17
|
+
embedLongForm: true
|
|
13
18
|
});
|
|
14
19
|
};
|
|
15
20
|
}
|
package/lib/hub/hub-like.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RequestError } from '@enkaku/client';
|
|
2
2
|
import { ErrorCodes } from '@enkaku/protocol';
|
|
3
|
+
import { normalizeDID } from '@kokuin/token';
|
|
3
4
|
import { hubErrorFromCode, RetentionExceededError } from '@kumiai/hub-protocol';
|
|
4
5
|
import { fromB64, toB64 } from '@sozai/codec';
|
|
5
6
|
import { EventEmitter } from '@sozai/event';
|
|
@@ -79,14 +80,16 @@ function rethrowHubError(error) {
|
|
|
79
80
|
#senderDID;
|
|
80
81
|
#topicID;
|
|
81
82
|
constructor(options){
|
|
82
|
-
|
|
83
|
+
// Canonicalize the scope so a peer:4 sender attested by its long form (an
|
|
84
|
+
// embed-long-form hub client) still matches its short-form route DID.
|
|
85
|
+
this.#senderDID = options?.senderDID == null ? undefined : normalizeDID(options.senderDID);
|
|
83
86
|
this.#topicID = options?.topicID;
|
|
84
87
|
}
|
|
85
88
|
push(message) {
|
|
86
89
|
if (this.#stopped) {
|
|
87
90
|
return;
|
|
88
91
|
}
|
|
89
|
-
if (this.#senderDID != null && message.senderDID !== this.#senderDID) {
|
|
92
|
+
if (this.#senderDID != null && normalizeDID(message.senderDID) !== this.#senderDID) {
|
|
90
93
|
return;
|
|
91
94
|
}
|
|
92
95
|
if (this.#topicID != null && message.topicID !== this.#topicID) {
|
|
@@ -25,6 +25,12 @@ export type PeerScopedHubViewParams = {
|
|
|
25
25
|
* MLS-authenticated identity. That is enough: the filter routes, MLS still
|
|
26
26
|
* authorizes, and a hostile hub could only hide frames it can already drop.
|
|
27
27
|
*
|
|
28
|
+
* Both sides are normalized before the match: a `did:peer:4` publisher whose hub
|
|
29
|
+
* client embeds the long form is attested by its long form, while `peerDID` is
|
|
30
|
+
* the roster's short form — a raw `===` would drop every such frame, and its
|
|
31
|
+
* request would vanish with no error (the exact failure this filter is meant to
|
|
32
|
+
* prevent for the OTHER peer's frames).
|
|
33
|
+
*
|
|
28
34
|
* **`unsubscribe` is dropped.** The responder topic belongs to the (group,
|
|
29
35
|
* epoch), not to a session: every listener on the device shares it, one per
|
|
30
36
|
* co-member, and each respawns independently — so a departing transport must not
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeDID } from '@kokuin/token';
|
|
1
2
|
/**
|
|
2
3
|
* A view over one device's hub that surfaces only the frames one peer sent.
|
|
3
4
|
*
|
|
@@ -18,6 +19,12 @@
|
|
|
18
19
|
* MLS-authenticated identity. That is enough: the filter routes, MLS still
|
|
19
20
|
* authorizes, and a hostile hub could only hide frames it can already drop.
|
|
20
21
|
*
|
|
22
|
+
* Both sides are normalized before the match: a `did:peer:4` publisher whose hub
|
|
23
|
+
* client embeds the long form is attested by its long form, while `peerDID` is
|
|
24
|
+
* the roster's short form — a raw `===` would drop every such frame, and its
|
|
25
|
+
* request would vanish with no error (the exact failure this filter is meant to
|
|
26
|
+
* prevent for the OTHER peer's frames).
|
|
27
|
+
*
|
|
21
28
|
* **`unsubscribe` is dropped.** The responder topic belongs to the (group,
|
|
22
29
|
* epoch), not to a session: every listener on the device shares it, one per
|
|
23
30
|
* co-member, and each respawns independently — so a departing transport must not
|
|
@@ -26,6 +33,9 @@
|
|
|
26
33
|
* therefore issued once per topic and held for the life of the view.
|
|
27
34
|
*/ export function createPeerScopedHubView({ hub, peerDID }) {
|
|
28
35
|
const armed = new Set();
|
|
36
|
+
// Match on the canonical form so a peer:4 sender attested by its long form
|
|
37
|
+
// (an embed-long-form hub client) still routes to its short-form roster entry.
|
|
38
|
+
const scopedDID = normalizeDID(peerDID);
|
|
29
39
|
const view = {
|
|
30
40
|
publish: (params)=>hub.publish(params),
|
|
31
41
|
subscribe: async (subscriberDID, topicID, options)=>{
|
|
@@ -58,7 +68,7 @@
|
|
|
58
68
|
done: true
|
|
59
69
|
};
|
|
60
70
|
}
|
|
61
|
-
if (result.value.senderDID ===
|
|
71
|
+
if (normalizeDID(result.value.senderDID) === scopedDID) {
|
|
62
72
|
return result;
|
|
63
73
|
}
|
|
64
74
|
}
|
package/lib/hub/wiring.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ClientTransportOf } from '@enkaku/protocol';
|
|
2
2
|
import type { ProcedureHandlers } from '@enkaku/server';
|
|
3
|
-
import type { OwnIdentity } from '@kokuin/token';
|
|
3
|
+
import type { DIDCache, OwnIdentity } from '@kokuin/token';
|
|
4
4
|
import type { KubunDB } from '@kubun/db';
|
|
5
5
|
import type { DefaultAccessLevel, GraphInternals } from '@kubun/engine';
|
|
6
6
|
import type { HLC } from '@kubun/hlc';
|
|
@@ -154,5 +154,11 @@ export type SetupHubRelayParams = {
|
|
|
154
154
|
serviceServe?: ServeService;
|
|
155
155
|
/** Which services to serve on an inbound service-lane session. */
|
|
156
156
|
services?: Record<string, ServiceConfig>;
|
|
157
|
+
/**
|
|
158
|
+
* Shared, plugin-lifetime DID cache forwarded to the group peer manager's
|
|
159
|
+
* tunnel listeners so a co-member's short-form `did:peer:4` issuer resolves on
|
|
160
|
+
* a spawned session's Server.
|
|
161
|
+
*/
|
|
162
|
+
cache?: DIDCache;
|
|
157
163
|
};
|
|
158
164
|
export declare function setupHubRelay(params: SetupHubRelayParams): HubWiring;
|
package/lib/hub/wiring.js
CHANGED
|
@@ -117,6 +117,9 @@ export function setupHubRelay(params) {
|
|
|
117
117
|
...params.services != null ? {
|
|
118
118
|
services: params.services
|
|
119
119
|
} : {},
|
|
120
|
+
...params.cache != null ? {
|
|
121
|
+
cache: params.cache
|
|
122
|
+
} : {},
|
|
120
123
|
tunnelIdleTimeoutMs: tunnelIdleTimeoutMs ?? DEFAULT_TUNNEL_IDLE_TIMEOUT_MS
|
|
121
124
|
});
|
|
122
125
|
unsubscribes.push(emitter.on('groupJoined', (group)=>manager.addGroup(group.id).catch((error)=>{
|
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';
|
|
@@ -199,6 +199,13 @@ export function createP2PPlugin(options) {
|
|
|
199
199
|
const peerConnections = new PeerConnectionRegistry({
|
|
200
200
|
logger: params.getLogger('peer-connections')
|
|
201
201
|
});
|
|
202
|
+
// One plugin-lifetime DID cache, shared by every peer-lane Server. The
|
|
203
|
+
// tunnel listeners seed it from each group's MLS roster (a co-member's
|
|
204
|
+
// authenticated leaf long form), so a freshly-spawned per-session Server
|
|
205
|
+
// resolves a co-member's short-form `did:peer:4` issuer instead of defaulting
|
|
206
|
+
// its own empty cache. Content-addressed and monotonic — a stale entry is
|
|
207
|
+
// harmless, and it survives no process restart by design (re-seeded on bind).
|
|
208
|
+
const didCache = createInMemoryDIDCache();
|
|
202
209
|
// The p2p plugin manages its own Enkaku server for sync handlers,
|
|
203
210
|
// separate from the graph service's server.
|
|
204
211
|
const syncServers = [];
|
|
@@ -222,6 +229,7 @@ export function createP2PPlugin(options) {
|
|
|
222
229
|
transports: [
|
|
223
230
|
directTransports.server
|
|
224
231
|
],
|
|
232
|
+
cache: didCache,
|
|
225
233
|
signal
|
|
226
234
|
});
|
|
227
235
|
syncServers.push(server);
|
|
@@ -324,7 +332,9 @@ export function createP2PPlugin(options) {
|
|
|
324
332
|
...serviceServe != null ? {
|
|
325
333
|
serviceServe,
|
|
326
334
|
services: serviceConfig
|
|
327
|
-
} : {}
|
|
335
|
+
} : {},
|
|
336
|
+
// The shared roster-seeded cache reaches the tunnel listeners here.
|
|
337
|
+
cache: didCache
|
|
328
338
|
});
|
|
329
339
|
// Turn a `tunnel://<groupID>/<peerDID>` route into a live session over the
|
|
330
340
|
// group's hub. Until this existed the scheme parsed and then failed at
|
|
@@ -629,7 +639,8 @@ export function createP2PPlugin(options) {
|
|
|
629
639
|
logger: params.getLogger('sync-http-server'),
|
|
630
640
|
transports: [
|
|
631
641
|
httpSyncTransport
|
|
632
|
-
]
|
|
642
|
+
],
|
|
643
|
+
cache: didCache
|
|
633
644
|
});
|
|
634
645
|
syncServers.push(httpSyncServer);
|
|
635
646
|
httpAPI.registerProtocol(protocolName, httpSyncTransport.fetch.bind(httpSyncTransport));
|
|
@@ -672,7 +683,8 @@ export function createP2PPlugin(options) {
|
|
|
672
683
|
logger: params.getLogger('peer-http-server'),
|
|
673
684
|
transports: [
|
|
674
685
|
httpPeerTransport
|
|
675
|
-
]
|
|
686
|
+
],
|
|
687
|
+
cache: didCache
|
|
676
688
|
});
|
|
677
689
|
httpAPI.registerProtocol('peer', httpPeerTransport.fetch.bind(httpPeerTransport));
|
|
678
690
|
})();
|
|
@@ -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,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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/plugin-p2p",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"license": "see LICENSE.md",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
"LICENSE.md"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@enkaku/client": "^0.21.
|
|
17
|
+
"@enkaku/client": "^0.21.2",
|
|
18
18
|
"@enkaku/http-fetch": "^0.21.0",
|
|
19
19
|
"@enkaku/http-serve": "^0.21.0",
|
|
20
|
-
"@enkaku/protocol": "^0.21.
|
|
20
|
+
"@enkaku/protocol": "^0.21.1",
|
|
21
21
|
"@enkaku/server": "^0.21.2",
|
|
22
22
|
"@enkaku/transport": "^0.21.0",
|
|
23
23
|
"@kokuin/capability": "^0.3.0",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@kubun/plugin-blob-api": "^0.15.0",
|
|
37
37
|
"@kubun/plugin-http": "^0.15.0",
|
|
38
38
|
"@kubun/plugin-http-api": "^0.15.0",
|
|
39
|
-
"@kubun/plugin-service-api": "^0.15.
|
|
39
|
+
"@kubun/plugin-service-api": "^0.15.1",
|
|
40
40
|
"@kubun/plugin-workflow-api": "^0.15.0",
|
|
41
41
|
"@kubun/protocol": "^0.15.0",
|
|
42
42
|
"@kubun/store-blob": "^0.15.0",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"@kubun/hub": "^0.15.0",
|
|
75
75
|
"@kubun/plugin-blob": "^0.15.0",
|
|
76
76
|
"@kubun/plugin-connector": "^0.15.0",
|
|
77
|
-
"@kubun/plugin-service-server": "^0.15.
|
|
77
|
+
"@kubun/plugin-service-server": "^0.15.1",
|
|
78
78
|
"@kubun/plugin-workflow": "^0.15.0",
|
|
79
79
|
"@kubun/service-graph-api": "^0.15.0",
|
|
80
80
|
"@kubun/store-workflow": "^0.15.0",
|