@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.
@@ -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);
@@ -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
  }
@@ -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
- this.#senderDID = options?.senderDID;
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 === peerDID) {
71
+ if (normalizeDID(result.value.senderDID) === scopedDID) {
62
72
  return result;
63
73
  }
64
74
  }
@@ -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.0",
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.1",
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.0",
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.0",
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.0",
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",