@kubun/plugin-p2p 0.10.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/lib/context/delegation.js +130 -1
  2. package/lib/context/group.js +1646 -1
  3. package/lib/context/hub.js +151 -1
  4. package/lib/context/join.js +115 -1
  5. package/lib/context/peer.d.ts +92 -0
  6. package/lib/context/peer.js +1216 -0
  7. package/lib/context/require-admin.d.ts +57 -0
  8. package/lib/context/require-admin.js +57 -0
  9. package/lib/context/sync.d.ts +12 -0
  10. package/lib/context/sync.js +252 -1
  11. package/lib/context/types.d.ts +58 -22
  12. package/lib/context/types.js +48 -1
  13. package/lib/groups/access-default-token.d.ts +44 -0
  14. package/lib/groups/access-default-token.js +103 -0
  15. package/lib/groups/admin-roster.d.ts +55 -0
  16. package/lib/groups/admin-roster.js +69 -0
  17. package/lib/groups/anchor-store.d.ts +15 -0
  18. package/lib/groups/anchor-store.js +37 -0
  19. package/lib/groups/app-cursor-store.d.ts +17 -0
  20. package/lib/groups/app-cursor-store.js +34 -0
  21. package/lib/groups/broadcast-codec.js +6 -1
  22. package/lib/groups/broadcast.d.ts +238 -106
  23. package/lib/groups/broadcast.js +702 -1
  24. package/lib/groups/catalog-token.d.ts +50 -0
  25. package/lib/groups/catalog-token.js +96 -0
  26. package/lib/groups/circle-projection.d.ts +90 -0
  27. package/lib/groups/circle-projection.js +202 -0
  28. package/lib/groups/circle-reducers.d.ts +139 -0
  29. package/lib/groups/circle-reducers.js +128 -0
  30. package/lib/groups/commit-adoption.d.ts +77 -0
  31. package/lib/groups/commit-adoption.js +108 -0
  32. package/lib/groups/commit-journal.d.ts +14 -0
  33. package/lib/groups/commit-journal.js +65 -0
  34. package/lib/groups/control-request.d.ts +66 -0
  35. package/lib/groups/control-request.js +101 -0
  36. package/lib/groups/events.d.ts +19 -2
  37. package/lib/groups/events.js +10 -1
  38. package/lib/groups/group-anchor.d.ts +24 -0
  39. package/lib/groups/group-anchor.js +22 -0
  40. package/lib/groups/group-crypto.d.ts +42 -0
  41. package/lib/groups/group-crypto.js +159 -0
  42. package/lib/groups/group-handle-registry.d.ts +138 -9
  43. package/lib/groups/group-handle-registry.js +533 -1
  44. package/lib/groups/group-handlers.d.ts +30 -0
  45. package/lib/groups/group-handlers.js +218 -0
  46. package/lib/groups/group-health-monitor.d.ts +11 -0
  47. package/lib/groups/group-health-monitor.js +139 -1
  48. package/lib/groups/group-mls.d.ts +66 -0
  49. package/lib/groups/group-mls.js +696 -0
  50. package/lib/groups/group-peer-manager.d.ts +301 -0
  51. package/lib/groups/group-peer-manager.js +1336 -0
  52. package/lib/groups/group-protocols.d.ts +754 -0
  53. package/lib/groups/group-protocols.js +448 -0
  54. package/lib/groups/invite-payload.d.ts +49 -1
  55. package/lib/groups/invite-payload.js +26 -1
  56. package/lib/groups/join-request-identity.d.ts +21 -0
  57. package/lib/groups/join-request-identity.js +43 -0
  58. package/lib/groups/join-utils.d.ts +61 -0
  59. package/lib/groups/join-utils.js +366 -1
  60. package/lib/groups/ledger-affected-events.d.ts +20 -0
  61. package/lib/groups/ledger-affected-events.js +85 -0
  62. package/lib/groups/ledger-commit-fold.d.ts +107 -0
  63. package/lib/groups/ledger-commit-fold.js +167 -0
  64. package/lib/groups/ledger-ingest.d.ts +124 -0
  65. package/lib/groups/ledger-ingest.js +212 -0
  66. package/lib/groups/ledger.d.ts +44 -0
  67. package/lib/groups/ledger.js +30 -0
  68. package/lib/groups/manager.d.ts +217 -85
  69. package/lib/groups/manager.js +798 -1
  70. package/lib/groups/mls-codec.d.ts +28 -7
  71. package/lib/groups/mls-codec.js +33 -1
  72. package/lib/groups/mls-encryptor.d.ts +2 -2
  73. package/lib/groups/mls-encryptor.js +33 -1
  74. package/lib/groups/mls-group-handle.d.ts +37 -2
  75. package/lib/groups/mls-group-handle.js +18 -1
  76. package/lib/groups/mls-json.js +17 -1
  77. package/lib/groups/mls-receive-errors.d.ts +27 -0
  78. package/lib/groups/mls-receive-errors.js +39 -0
  79. package/lib/groups/mls-state.d.ts +6 -4
  80. package/lib/groups/mls-state.js +79 -1
  81. package/lib/groups/peer-presence.d.ts +90 -0
  82. package/lib/groups/peer-presence.js +295 -0
  83. package/lib/groups/peer-selection.d.ts +37 -0
  84. package/lib/groups/peer-selection.js +72 -0
  85. package/lib/groups/roster-projection.d.ts +53 -0
  86. package/lib/groups/roster-projection.js +65 -0
  87. package/lib/groups/store-received-grant.d.ts +7 -3
  88. package/lib/groups/store-received-grant.js +161 -1
  89. package/lib/groups/store-received-revocation.d.ts +19 -16
  90. package/lib/groups/store-received-revocation.js +83 -1
  91. package/lib/hub/ack-backoff.d.ts +22 -0
  92. package/lib/hub/ack-backoff.js +88 -0
  93. package/lib/hub/http-client.d.ts +18 -10
  94. package/lib/hub/http-client.js +15 -1
  95. package/lib/hub/hub-like.d.ts +75 -0
  96. package/lib/hub/hub-like.js +1022 -0
  97. package/lib/hub/loopback-log-hub.d.ts +20 -0
  98. package/lib/hub/loopback-log-hub.js +178 -0
  99. package/lib/hub/manager.d.ts +25 -40
  100. package/lib/hub/manager.js +155 -1
  101. package/lib/hub/peer-scoped-hub-view.d.ts +35 -0
  102. package/lib/hub/peer-scoped-hub-view.js +92 -0
  103. package/lib/hub/sender-scoped-hub-view.d.ts +41 -0
  104. package/lib/hub/sender-scoped-hub-view.js +59 -0
  105. package/lib/hub/server-did.d.ts +67 -0
  106. package/lib/hub/server-did.js +131 -0
  107. package/lib/hub/wiring.d.ts +77 -51
  108. package/lib/hub/wiring.js +189 -1
  109. package/lib/index.d.ts +58 -22
  110. package/lib/index.js +563 -1
  111. package/lib/peer/connection-registry.d.ts +45 -0
  112. package/lib/peer/connection-registry.js +52 -0
  113. package/lib/peer/handlers.d.ts +69 -0
  114. package/lib/peer/handlers.js +110 -0
  115. package/lib/protocol.d.ts +214 -1
  116. package/lib/protocol.js +519 -1
  117. package/lib/schema.d.ts +3 -1
  118. package/lib/schema.js +1150 -39
  119. package/lib/sync/access-default-sender.d.ts +23 -0
  120. package/lib/sync/access-default-sender.js +80 -0
  121. package/lib/sync/authorize.d.ts +58 -4
  122. package/lib/sync/authorize.js +114 -1
  123. package/lib/sync/broadcast-queue.d.ts +3 -4
  124. package/lib/sync/broadcast-queue.js +182 -1
  125. package/lib/sync/broadcast-sender.js +179 -1
  126. package/lib/sync/catalog-scope.d.ts +8 -7
  127. package/lib/sync/catalog-scope.js +84 -1
  128. package/lib/sync/errors.d.ts +12 -2
  129. package/lib/sync/errors.js +29 -1
  130. package/lib/sync/forwarder.d.ts +13 -19
  131. package/lib/sync/forwarder.js +271 -1
  132. package/lib/sync/handlers.js +200 -1
  133. package/lib/sync/hub-tunnel-sync-listener.d.ts +19 -5
  134. package/lib/sync/hub-tunnel-sync-listener.js +261 -1
  135. package/lib/sync/hub-tunnel-sync-provider.d.ts +34 -14
  136. package/lib/sync/hub-tunnel-sync-provider.js +113 -1
  137. package/lib/sync/merkle-apply.d.ts +5 -0
  138. package/lib/sync/merkle-apply.js +194 -1
  139. package/lib/sync/merkle-channel.d.ts +11 -3
  140. package/lib/sync/merkle-channel.js +0 -0
  141. package/lib/sync/merkle-tree.js +120 -1
  142. package/lib/sync/peer-registry.d.ts +15 -11
  143. package/lib/sync/peer-registry.js +82 -1
  144. package/lib/sync/receive-access-gate.js +24 -1
  145. package/lib/sync/scope-resolver.js +41 -1
  146. package/lib/sync/sync-client.d.ts +14 -4
  147. package/lib/sync/sync-client.js +254 -1
  148. package/lib/sync/sync-manager.d.ts +24 -6
  149. package/lib/sync/sync-manager.js +301 -1
  150. package/lib/sync/tunnel-listeners.d.ts +52 -0
  151. package/lib/sync/tunnel-listeners.js +175 -0
  152. package/lib/sync/tunnel-observability.d.ts +25 -0
  153. package/lib/sync/tunnel-observability.js +63 -0
  154. package/lib/sync/tunnel-topics.d.ts +20 -0
  155. package/lib/sync/tunnel-topics.js +28 -0
  156. package/lib/types.d.ts +704 -22
  157. package/lib/types.js +1 -1
  158. package/lib/util/handler-error.d.ts +16 -0
  159. package/lib/util/handler-error.js +64 -0
  160. package/lib/util/mutex.d.ts +14 -1
  161. package/lib/util/mutex.js +44 -1
  162. package/lib/utils.js +21 -1
  163. package/package.json +58 -47
  164. package/lib/groups/broadcast-service.d.ts +0 -263
  165. package/lib/groups/broadcast-service.js +0 -1
  166. package/lib/groups/rejoin-codec.d.ts +0 -14
  167. package/lib/groups/rejoin-codec.js +0 -1
  168. package/lib/groups/wire-frame.d.ts +0 -34
  169. package/lib/groups/wire-frame.js +0 -1
  170. package/lib/hub/circle-catchup-requester.d.ts +0 -58
  171. package/lib/hub/circle-catchup-requester.js +0 -1
  172. package/lib/hub/circle-catchup-responder.d.ts +0 -50
  173. package/lib/hub/circle-catchup-responder.js +0 -1
  174. package/lib/hub/connection-pool.d.ts +0 -43
  175. package/lib/hub/connection-pool.js +0 -1
  176. package/lib/hub/did-observing-transport.d.ts +0 -64
  177. package/lib/hub/did-observing-transport.js +0 -1
  178. package/lib/hub/epoch-stale-detector.d.ts +0 -18
  179. package/lib/hub/epoch-stale-detector.js +0 -1
  180. package/lib/hub/errors.d.ts +0 -30
  181. package/lib/hub/errors.js +0 -1
  182. package/lib/hub/forward-remote-broadcast.d.ts +0 -15
  183. package/lib/hub/forward-remote-broadcast.js +0 -1
  184. package/lib/hub/group-channel.d.ts +0 -89
  185. package/lib/hub/group-channel.js +0 -1
  186. package/lib/hub/hub-connection.d.ts +0 -96
  187. package/lib/hub/hub-connection.js +0 -1
  188. package/lib/hub/receive-handler.d.ts +0 -51
  189. package/lib/hub/receive-handler.js +0 -1
  190. package/lib/hub/rejoin-manager.d.ts +0 -78
  191. package/lib/hub/rejoin-manager.js +0 -1
  192. package/lib/hub/rejoin-responder.d.ts +0 -32
  193. package/lib/hub/rejoin-responder.js +0 -1
  194. package/lib/hub/relay-manager.d.ts +0 -142
  195. package/lib/hub/relay-manager.js +0 -1
  196. package/lib/hub/send-handler.d.ts +0 -40
  197. package/lib/hub/send-handler.js +0 -1
  198. package/lib/hub/tunnel-inbox.d.ts +0 -20
  199. package/lib/hub/tunnel-inbox.js +0 -1
  200. package/lib/hub/wait-for-gate.d.ts +0 -14
  201. package/lib/hub/wait-for-gate.js +0 -1
@@ -1 +1,41 @@
1
- export function computeBroadcastScopes(e){let{resolvedRule:r,senderGroupIDs:o}=e;switch(r.level){case"only_owner":return[];case"anyone":return Array.from(new Set(o));case"restricted":{let e=new Set;for(let t of o)(r.allowedGroupIDs.has(t)||r.allowedCircleParentGroupIDs.has(t)||r.allowedDIDGroupIDs.has(t))&&e.add(t);return Array.from(e)}default:{let e=r.level;throw Error(`Unknown access level: ${String(e)}`)}}}
1
+ /**
2
+ * Compute the set of MLS group IDs to broadcast a document to,
3
+ * given its resolved access rule and the sender's group memberships.
4
+ *
5
+ * Pure, synchronous. The caller is responsible for:
6
+ * - Pre-resolving the access rule into a `ResolvedAccessRule` (all rule-side
7
+ * lookups — circle parent groups and per-DID group memberships —
8
+ * collapsed to MLS group ID sets).
9
+ * - Pre-filtering `senderGroupIDs` by catalog-match against the document.
10
+ *
11
+ * Per spec §3 "Sync routing" (sender side): a sender's own MLS group is a
12
+ * valid broadcast scope iff that group is the encryption perimeter for at
13
+ * least one allowed principal — either the group itself is in allowedGroups,
14
+ * or an allowed circle's parent group matches, or an allowed DID is a member
15
+ * of that group. The receiver applies the per-recipient access check on
16
+ * decrypt; the sender's own circle membership is irrelevant for routing.
17
+ */ export function computeBroadcastScopes(input) {
18
+ const { resolvedRule, senderGroupIDs } = input;
19
+ switch(resolvedRule.level){
20
+ case 'only_owner':
21
+ return [];
22
+ case 'anyone':
23
+ // Dedupe sender group IDs while preserving the input order.
24
+ return Array.from(new Set(senderGroupIDs));
25
+ case 'restricted':
26
+ {
27
+ const scopes = new Set();
28
+ for (const groupID of senderGroupIDs){
29
+ if (resolvedRule.allowedGroupIDs.has(groupID) || resolvedRule.allowedCircleParentGroupIDs.has(groupID) || resolvedRule.allowedDIDGroupIDs.has(groupID)) {
30
+ scopes.add(groupID);
31
+ }
32
+ }
33
+ return Array.from(scopes);
34
+ }
35
+ default:
36
+ {
37
+ const _exhaustive = resolvedRule.level;
38
+ throw new Error(`Unknown access level: ${String(_exhaustive)}`);
39
+ }
40
+ }
41
+ }
@@ -1,21 +1,30 @@
1
1
  import { Client } from '@enkaku/client';
2
2
  import type { ClientTransportOf } from '@enkaku/protocol';
3
- import type { Runtime } from '@enkaku/runtime';
4
- import type { SigningIdentity } from '@enkaku/token';
3
+ import type { SigningIdentity } from '@kokuin/token';
5
4
  import type { Logger } from '@kubun/logger';
5
+ import type { Runtime } from '@sozai/runtime';
6
6
  import type { SyncDirection, SyncProtocol } from '../protocol.js';
7
7
  /**
8
8
  * Provider for sync-specific transport to a peer's sync server.
9
9
  * The p2p plugin exposes this via `createSyncTransport()`.
10
10
  */
11
11
  export type SyncTransportProvider = {
12
- createSyncTransport: (signal?: AbortSignal) => ClientTransportOf<SyncProtocol>;
12
+ createSyncTransport: (signal?: AbortSignal) => ClientTransportOf<SyncProtocol> | Promise<ClientTransportOf<SyncProtocol>>;
13
13
  };
14
+ /**
15
+ * What a route carries beyond the peer DID. `tunnel://` names the group whose
16
+ * MLS epoch keys the session's inbox topics; `direct://` names nothing, so a
17
+ * resolver serving only in-process peers can ignore the second argument.
18
+ */
19
+ export type SyncRoute = {
20
+ groupID: string;
21
+ };
22
+ export type SyncServerResolver = (serverID: string, route?: SyncRoute) => SyncTransportProvider | undefined;
14
23
  export type SyncClientParams = {
15
24
  runtime: Runtime;
16
25
  identity: SigningIdentity;
17
26
  logger: Logger;
18
- serverResolver?: (serverID: string) => SyncTransportProvider | undefined;
27
+ serverResolver?: SyncServerResolver;
19
28
  };
20
29
  export type SyncScope = {
21
30
  modelID: string;
@@ -34,6 +43,7 @@ export declare class SyncClient {
34
43
  /**
35
44
  * Connect to a peer server using the specified endpoint.
36
45
  * - `direct://server-id` - Use in-process direct transport via sync server
46
+ * - `tunnel://group-id/peer-did` - Use a hub tunnel through the named group
37
47
  * - `http://...` or `https://...` - Use HTTP transport
38
48
  */
39
49
  connect(endpoint: string, serverID?: string): Promise<Client<SyncProtocol>>;
@@ -1 +1,254 @@
1
- import{Client as e}from"@enkaku/client";import{ClientTransport as t}from"@enkaku/http-client-transport";import{findDivergentBuckets as r,getTimeBuckets as n,SYNC_BATCH_SIZE as i}from"./merkle-tree.js";export class SyncClient{#e;#t;#r;#n;constructor(e){this.#e=e.runtime,this.#t=e.identity,this.#r=e.logger,this.#n=e.serverResolver}async connect(e,t){if(e.startsWith("direct://"))return this.#i(e);if(e.startsWith("http://")||e.startsWith("https://"))return this.#s(e,t);throw Error(`Unsupported endpoint scheme: ${e}`)}#s(r,n){if(null==n||""===n)throw Error("HTTP sync transport requires a serverID (peer DID) to authenticate; without it the server rejects all requests");let i=new t({url:r,fetch:this.#e.fetch});return new e({runtime:this.#e,identity:this.#t,logger:this.#r.getChild("sync-client"),serverID:n,transport:i})}#i(t){let r=t.replace("direct://","");if(!this.#n)throw Error("Server resolver not configured for direct transport");let n=this.#n(r);if(!n)throw Error(`Server not found: ${r}`);let i=n.createSyncTransport();return new e({runtime:this.#e,identity:this.#t,logger:this.#r.getChild("sync-client"),serverID:r,transport:i})}async negotiate(e,t,r=[],n,i){this.#r.debug("Negotiate: sending {scopeCount} scopes",{scopeCount:t.length});let s=await e.request("sync/negotiate",{param:{scopes:t,delegationTokens:r,knownModelIDs:n,direction:i}});return this.#r.debug("Negotiate: accepted {acceptedCount} scopes, excluded {excludedCount} docs",{acceptedCount:s.acceptedScopes.length,excludedCount:s.excludedDocumentIDs.length}),s}async merkleSync(e,t){let{scopes:s,excludedDocumentIDs:o,localTree:c,direction:l,localEntries:a,delegationTokens:u}=t;this.#r.debug("MerkleSync: creating channel with {buckets} local buckets",{buckets:Object.keys(c.buckets).length});let h=e.createChannel("sync/merkle-sync",{param:{scopes:s,excludedDocumentIDs:o,tree:c.buckets,direction:l,...null!=u&&{delegationTokens:u}}}),d=h.readable.getReader(),g=await d.read();if(g.done)throw Error("MerkleSync: channel closed before receiving remote tree");let p=g.value;if("tree"!==p.type)throw Error(`MerkleSync: expected 'tree' message, got '${p.type}'`);let m=r(c,{root:p.tree?.root??"",buckets:p.tree??{}}),y=m.length;this.#r.debug("MerkleSync: found {count} divergent buckets",{count:y});let f=0,k=async()=>{if("push"===l||"both"===l){let e=new Set(m),t=a.filter(t=>{let{minute:r}=n(t.hlc);return e.has(r)});for(let e=0;e<t.length;e+=i){let r=t.slice(e,e+i);await h.send({type:"mutations",mutationJWTs:r.map(e=>e.mutation_jwt)}),f+=r.length}}await h.send({type:"complete"})},v=async()=>{let e=[];for(;;){let{done:t,value:r}=await d.read();if(t)break;if("mutations"===r.type&&null!=r.mutationJWTs)e.push(...r.mutationJWTs);else if("complete"===r.type)break}return d.releaseLock(),e},[,w]=await Promise.all([k(),v()]),b=await h;if(null!=b&&!b.success)throw Error("MerkleSync: responder reported failure");return this.#r.debug("MerkleSync: completed with {received} received, {sent} sent",{received:w.length,sent:f}),{mutationJWTs:w,divergentBuckets:y,mutationsSent:f}}}
1
+ import { Client } from '@enkaku/client';
2
+ import { ClientTransport as HTTPClientTransport } from '@enkaku/http-fetch';
3
+ import { findDivergentBuckets, getTimeBuckets, SYNC_BATCH_SIZE } from './merkle-tree.js';
4
+ /**
5
+ * Client for connecting to peer servers for sync operations.
6
+ * Supports both direct (in-process) and HTTP transports.
7
+ *
8
+ * Uses a raw Enkaku `Client<SyncProtocol>` for sync connections rather than
9
+ * `KubunClient`, since only sync procedures (negotiate, merkle-sync) are needed.
10
+ */ export class SyncClient {
11
+ #runtime;
12
+ #identity;
13
+ #logger;
14
+ #serverResolver;
15
+ /**
16
+ * The context each session's lines carry, keyed by the client `connect` built
17
+ * for it.
18
+ *
19
+ * Held here rather than passed through `negotiate` / `merkleSync` because the
20
+ * peer a session is talking to is fixed when it is dialled and is not a
21
+ * parameter of either call. Without it these logs report bucket and mutation
22
+ * counts with nothing naming the peer, which is what makes two devices' logs
23
+ * impossible to join except by grepping DIDs out of unrelated lines.
24
+ *
25
+ * The transport's own `sessionID` is not here: it is minted inside the tunnel
26
+ * provider, which logs it against the same (group, peer, epoch).
27
+ */ #sessionLoggers = new WeakMap();
28
+ constructor(params){
29
+ this.#runtime = params.runtime;
30
+ this.#identity = params.identity;
31
+ this.#logger = params.logger;
32
+ this.#serverResolver = params.serverResolver;
33
+ }
34
+ /**
35
+ * Connect to a peer server using the specified endpoint.
36
+ * - `direct://server-id` - Use in-process direct transport via sync server
37
+ * - `tunnel://group-id/peer-did` - Use a hub tunnel through the named group
38
+ * - `http://...` or `https://...` - Use HTTP transport
39
+ */ async connect(endpoint, serverID) {
40
+ if (endpoint.startsWith('direct://')) {
41
+ return this.#connectDirect(endpoint);
42
+ }
43
+ if (endpoint.startsWith('tunnel://')) {
44
+ return this.#connectTunnel(endpoint);
45
+ }
46
+ if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) {
47
+ return this.#connectHTTP(endpoint, serverID);
48
+ }
49
+ throw new Error(`Unsupported endpoint scheme: ${endpoint}`);
50
+ }
51
+ /** The session's own logger, or the client-wide one for a session built elsewhere. */ #loggerFor(client) {
52
+ return this.#sessionLoggers.get(client) ?? this.#logger;
53
+ }
54
+ #trackSession(client, context) {
55
+ this.#sessionLoggers.set(client, this.#logger.getChild('sync-client').with(context));
56
+ return client;
57
+ }
58
+ #connectHTTP(endpoint, serverID) {
59
+ if (serverID == null || serverID === '') {
60
+ throw new Error('HTTP sync transport requires a serverID (peer DID) to authenticate; without it the server rejects all requests');
61
+ }
62
+ const transport = new HTTPClientTransport({
63
+ url: endpoint,
64
+ fetch: this.#runtime.fetch
65
+ });
66
+ return this.#trackSession(new Client({
67
+ runtime: this.#runtime,
68
+ identity: this.#identity,
69
+ logger: this.#logger.getChild('sync-client'),
70
+ serverID,
71
+ transport
72
+ }), {
73
+ peerDID: serverID,
74
+ scheme: 'http'
75
+ });
76
+ }
77
+ async #connectDirect(endpoint) {
78
+ return await this.#connectResolved(endpoint.replace('direct://', ''), 'direct');
79
+ }
80
+ /**
81
+ * `tunnel://<groupID>/<peerDID>` — a session relayed by the group's hub, with
82
+ * no server listening on either device. The audience comes from the route
83
+ * itself rather than from `connect`'s `serverID` argument: the route names the
84
+ * device whose inbox the frames are addressed to, so the two cannot disagree.
85
+ */ async #connectTunnel(endpoint) {
86
+ const route = endpoint.slice('tunnel://'.length);
87
+ const separator = route.indexOf('/');
88
+ const groupID = separator === -1 ? '' : route.slice(0, separator);
89
+ const peerDID = separator === -1 ? '' : route.slice(separator + 1);
90
+ if (groupID === '' || peerDID === '') {
91
+ throw new Error(`Invalid tunnel endpoint: ${endpoint} — expected tunnel://<groupID>/<peerDID>`);
92
+ }
93
+ return await this.#connectResolved(peerDID, 'tunnel', {
94
+ groupID
95
+ });
96
+ }
97
+ async #connectResolved(serverID, scheme, route) {
98
+ if (!this.#serverResolver) {
99
+ throw new Error(`Server resolver not configured for ${scheme} transport`);
100
+ }
101
+ const provider = this.#serverResolver(serverID, route);
102
+ if (!provider) {
103
+ throw new Error(`Server not found: ${serverID}`);
104
+ }
105
+ // Get a sync-specific transport from the peer's p2p plugin. The provider may
106
+ // build the transport asynchronously (e.g. the hub-tunnel provider derives
107
+ // its epoch-bound inbox topics from the live MLS handle), so await it; an
108
+ // in-process provider returning a transport synchronously is unaffected.
109
+ const transport = await provider.createSyncTransport();
110
+ return this.#trackSession(new Client({
111
+ runtime: this.#runtime,
112
+ identity: this.#identity,
113
+ logger: this.#logger.getChild('sync-client'),
114
+ serverID,
115
+ transport
116
+ }), {
117
+ peerDID: serverID,
118
+ scheme,
119
+ ...route == null ? {} : {
120
+ groupID: route.groupID
121
+ }
122
+ });
123
+ }
124
+ /**
125
+ * Negotiate sync scopes with peer.
126
+ */ async negotiate(client, scopes, delegationTokens = [], knownModelIDs, direction) {
127
+ const logger = this.#loggerFor(client);
128
+ logger.debug('Negotiate: sending {scopeCount} scopes', {
129
+ scopeCount: scopes.length
130
+ });
131
+ const result = await client.request('sync/negotiate', {
132
+ param: {
133
+ scopes,
134
+ delegationTokens,
135
+ knownModelIDs,
136
+ direction
137
+ }
138
+ });
139
+ logger.debug('Negotiate: accepted {acceptedCount} scopes, excluded {excludedCount} docs', {
140
+ acceptedCount: result.acceptedScopes.length,
141
+ excludedCount: result.excludedDocumentIDs.length
142
+ });
143
+ return result;
144
+ }
145
+ /**
146
+ * Perform Merkle tree-based sync with peer using bidirectional channel.
147
+ * Sends local tree and divergent mutations, receives remote mutations.
148
+ */ async merkleSync(client, params) {
149
+ const { scopes, excludedDocumentIDs, localTree, direction, localEntries, delegationTokens } = params;
150
+ const logger = this.#loggerFor(client);
151
+ logger.debug('MerkleSync: creating channel with {buckets} local buckets', {
152
+ buckets: Object.keys(localTree.buckets).length
153
+ });
154
+ const channelCall = client.createChannel('sync/merkle-sync', {
155
+ param: {
156
+ scopes,
157
+ excludedDocumentIDs,
158
+ tree: localTree.buckets,
159
+ direction,
160
+ ...delegationTokens != null && {
161
+ delegationTokens
162
+ }
163
+ }
164
+ });
165
+ // Marked handled the moment it exists, because the call is awaited LAST —
166
+ // after the send/receive pair below. A transport that aborts rejects all
167
+ // three, `Promise.all` throws first, and this one's rejection then reaches
168
+ // nobody: on React Native that is an error overlay with no message, which
169
+ // is exactly how a starved tunnel session presented during device QA. The
170
+ // await at the end still sees the rejection; this only stops it counting as
171
+ // unhandled in the window before that.
172
+ void channelCall.catch(()=>{});
173
+ // Step 1: Read the responder's tree from the first message
174
+ const reader = channelCall.readable.getReader();
175
+ const firstRead = await reader.read();
176
+ if (firstRead.done) {
177
+ throw new Error('MerkleSync: channel closed before receiving remote tree');
178
+ }
179
+ const firstMessage = firstRead.value;
180
+ if (firstMessage.type !== 'tree') {
181
+ throw new Error(`MerkleSync: expected 'tree' message, got '${firstMessage.type}'`);
182
+ }
183
+ // Step 2: Compute divergent buckets
184
+ const remoteTree = {
185
+ root: firstMessage.tree?.root ?? '',
186
+ buckets: firstMessage.tree ?? {}
187
+ };
188
+ const divergentBuckets = findDivergentBuckets(localTree, remoteTree);
189
+ const divergentBucketCount = divergentBuckets.length;
190
+ logger.debug('MerkleSync: found {count} divergent buckets', {
191
+ count: divergentBucketCount
192
+ });
193
+ // Step 3: Concurrent send and receive
194
+ let mutationsSent = 0;
195
+ // Send task: send divergent mutations to responder
196
+ const sendTask = async ()=>{
197
+ if (direction === 'push' || direction === 'both') {
198
+ // Filter local entries to those in divergent buckets
199
+ const divergentSet = new Set(divergentBuckets);
200
+ const entriesToSend = localEntries.filter((entry)=>{
201
+ const { minute } = getTimeBuckets(entry.hlc);
202
+ return divergentSet.has(minute);
203
+ });
204
+ // Send in batches
205
+ for(let i = 0; i < entriesToSend.length; i += SYNC_BATCH_SIZE){
206
+ const batch = entriesToSend.slice(i, i + SYNC_BATCH_SIZE);
207
+ await channelCall.send({
208
+ type: 'mutations',
209
+ mutationJWTs: batch.map((e)=>e.mutation_jwt)
210
+ });
211
+ mutationsSent += batch.length;
212
+ }
213
+ }
214
+ // Signal completion
215
+ await channelCall.send({
216
+ type: 'complete'
217
+ });
218
+ };
219
+ // Receive task: collect mutations from responder
220
+ const receiveTask = async ()=>{
221
+ const collectedJWTs = [];
222
+ while(true){
223
+ const { done, value } = await reader.read();
224
+ if (done) break;
225
+ if (value.type === 'mutations' && value.mutationJWTs != null) {
226
+ collectedJWTs.push(...value.mutationJWTs);
227
+ } else if (value.type === 'complete') {
228
+ break;
229
+ }
230
+ }
231
+ reader.releaseLock();
232
+ return collectedJWTs;
233
+ };
234
+ const [, mutationJWTs] = await Promise.all([
235
+ sendTask(),
236
+ receiveTask()
237
+ ]);
238
+ // Wait for the handler to finish processing (applying pushed mutations)
239
+ // before returning — prevents transport teardown racing with mutation application
240
+ const handlerResult = await channelCall;
241
+ if (handlerResult != null && !handlerResult.success) {
242
+ throw new Error('MerkleSync: responder reported failure');
243
+ }
244
+ logger.debug('MerkleSync: completed with {received} received, {sent} sent', {
245
+ received: mutationJWTs.length,
246
+ sent: mutationsSent
247
+ });
248
+ return {
249
+ mutationJWTs,
250
+ divergentBuckets: divergentBucketCount,
251
+ mutationsSent
252
+ };
253
+ }
254
+ }
@@ -1,12 +1,12 @@
1
- import type { Runtime } from '@enkaku/runtime';
2
- import { type Identity, type SigningIdentity } from '@enkaku/token';
1
+ import { type Identity, type SigningIdentity } from '@kokuin/token';
3
2
  import type { StoreProvider } from '@kubun/db';
4
3
  import type { DefaultAccessLevel, GraphInternals } from '@kubun/engine';
5
4
  import type { Logger } from '@kubun/logger';
5
+ import type { Runtime } from '@sozai/runtime';
6
6
  import type { SyncDirection } from '../protocol.js';
7
7
  import type { StoreUnreadableMode } from '../types.js';
8
8
  import { type PeerConfig, type PeerConfigWithID, PeerRegistry } from './peer-registry.js';
9
- import { type SyncScope, type SyncTransportProvider } from './sync-client.js';
9
+ import { type SyncScope, type SyncServerResolver } from './sync-client.js';
10
10
  export type SyncSessionInfo = {
11
11
  peerID: string;
12
12
  startTime: number;
@@ -31,7 +31,7 @@ export type SyncManagerParams = {
31
31
  runtime: Runtime;
32
32
  identity: Identity;
33
33
  logger: Logger;
34
- serverResolver?: (serverID: string) => SyncTransportProvider | undefined;
34
+ serverResolver?: SyncServerResolver;
35
35
  /**
36
36
  * Receive-side storage mode forwarded to `applySyncMutations`.
37
37
  * Defaults to `'persist'` when omitted.
@@ -47,6 +47,15 @@ export type MerkleSyncParams = {
47
47
  delegationTokens?: Array<string>;
48
48
  knownModelIDs?: Array<string>;
49
49
  direction?: SyncDirection;
50
+ /**
51
+ * Where to reach the peer, when the caller already resolved it. Omitted, the
52
+ * persisted registry answers and a peer with no row is an error.
53
+ *
54
+ * A route, and only a route: what a session serves is decided per document
55
+ * downstream, so supplying one reaches a peer the registry never heard of and
56
+ * grants it nothing.
57
+ */
58
+ endpoint?: string;
50
59
  };
51
60
  export declare class SyncManager {
52
61
  #private;
@@ -57,9 +66,11 @@ export declare class SyncManager {
57
66
  */
58
67
  setIdentity(identity: SigningIdentity): void;
59
68
  /**
60
- * Configure the server resolver for direct transport connections.
69
+ * Configure the resolver for the transports built locally rather than dialled:
70
+ * `direct://` (in-process) and `tunnel://` (relayed by the group's hub, whose
71
+ * group id arrives as the resolver's `route`).
61
72
  */
62
- setServerResolver(resolver: (serverID: string) => SyncTransportProvider | undefined): void;
73
+ setServerResolver(resolver: SyncServerResolver): void;
63
74
  addPeer(params: {
64
75
  config: PeerConfig;
65
76
  stores: StoreProvider;
@@ -92,5 +103,12 @@ export declare class SyncManager {
92
103
  stores: StoreProvider;
93
104
  }): SyncStatus;
94
105
  onSyncEvent(callback: (event: SyncEvent) => void): () => void;
106
+ /**
107
+ * Async-iterable view of the sync event stream, for bridging into a GraphQL
108
+ * subscription. An optional `filter` narrows to a single peer. Closing the
109
+ * generator (its `return()`, e.g. a subscriber disconnecting) unsubscribes the
110
+ * underlying listener, so no listener leaks past the consumer.
111
+ */
112
+ subscribeEvents(filter?: (event: SyncEvent) => boolean): AsyncGenerator<SyncEvent, void, void>;
95
113
  dispose(): Promise<void>;
96
114
  }