@kubun/plugin-p2p 0.10.1 → 0.11.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 (145) hide show
  1. package/lib/context/delegation.js +130 -1
  2. package/lib/context/group.js +1319 -1
  3. package/lib/context/hub.js +150 -1
  4. package/lib/context/join.js +80 -1
  5. package/lib/context/sync.js +19 -1
  6. package/lib/context/types.d.ts +3 -3
  7. package/lib/context/types.js +28 -1
  8. package/lib/groups/admin-roster.d.ts +130 -0
  9. package/lib/groups/admin-roster.js +184 -0
  10. package/lib/groups/broadcast-codec.js +6 -1
  11. package/lib/groups/broadcast.d.ts +105 -91
  12. package/lib/groups/broadcast.js +627 -1
  13. package/lib/groups/circle-projection.d.ts +122 -0
  14. package/lib/groups/circle-projection.js +288 -0
  15. package/lib/groups/circle-reducers.d.ts +178 -0
  16. package/lib/groups/circle-reducers.js +198 -0
  17. package/lib/groups/events.d.ts +5 -1
  18. package/lib/groups/events.js +10 -1
  19. package/lib/groups/group-anchor.d.ts +63 -0
  20. package/lib/groups/group-anchor.js +107 -0
  21. package/lib/groups/group-crypto.d.ts +19 -0
  22. package/lib/groups/group-crypto.js +85 -0
  23. package/lib/groups/group-handle-registry.d.ts +61 -2
  24. package/lib/groups/group-handle-registry.js +243 -1
  25. package/lib/groups/group-handlers.d.ts +29 -0
  26. package/lib/groups/group-handlers.js +92 -0
  27. package/lib/groups/group-health-monitor.d.ts +11 -0
  28. package/lib/groups/group-health-monitor.js +139 -1
  29. package/lib/groups/group-mls.d.ts +35 -0
  30. package/lib/groups/group-mls.js +164 -0
  31. package/lib/groups/group-peer-manager.d.ts +108 -0
  32. package/lib/groups/group-peer-manager.js +344 -0
  33. package/lib/groups/group-protocols.d.ts +297 -0
  34. package/lib/groups/group-protocols.js +174 -0
  35. package/lib/groups/invite-payload.d.ts +1 -1
  36. package/lib/groups/invite-payload.js +26 -1
  37. package/lib/groups/join-utils.d.ts +1 -0
  38. package/lib/groups/join-utils.js +104 -1
  39. package/lib/groups/ledger-affected-events.d.ts +15 -0
  40. package/lib/groups/ledger-affected-events.js +96 -0
  41. package/lib/groups/ledger-entry.d.ts +37 -0
  42. package/lib/groups/ledger-entry.js +56 -0
  43. package/lib/groups/ledger-fold.d.ts +56 -0
  44. package/lib/groups/ledger-fold.js +53 -0
  45. package/lib/groups/manager.d.ts +20 -71
  46. package/lib/groups/manager.js +281 -1
  47. package/lib/groups/mls-codec.d.ts +1 -1
  48. package/lib/groups/mls-codec.js +18 -1
  49. package/lib/groups/mls-encryptor.d.ts +2 -2
  50. package/lib/groups/mls-encryptor.js +30 -1
  51. package/lib/groups/mls-group-handle.d.ts +1 -1
  52. package/lib/groups/mls-group-handle.js +10 -1
  53. package/lib/groups/mls-json.js +17 -1
  54. package/lib/groups/mls-receive-errors.d.ts +29 -0
  55. package/lib/groups/mls-receive-errors.js +42 -0
  56. package/lib/groups/mls-state.d.ts +1 -1
  57. package/lib/groups/mls-state.js +56 -1
  58. package/lib/groups/store-received-grant.d.ts +4 -1
  59. package/lib/groups/store-received-grant.js +116 -1
  60. package/lib/groups/store-received-revocation.d.ts +3 -3
  61. package/lib/groups/store-received-revocation.js +82 -1
  62. package/lib/hub/ack-backoff.d.ts +22 -0
  63. package/lib/hub/ack-backoff.js +88 -0
  64. package/lib/hub/did-observing-transport.d.ts +13 -2
  65. package/lib/hub/did-observing-transport.js +126 -1
  66. package/lib/hub/errors.d.ts +0 -6
  67. package/lib/hub/errors.js +33 -1
  68. package/lib/hub/http-client.d.ts +28 -3
  69. package/lib/hub/http-client.js +32 -1
  70. package/lib/hub/hub-like.d.ts +24 -0
  71. package/lib/hub/hub-like.js +535 -0
  72. package/lib/hub/manager.js +198 -1
  73. package/lib/hub/wiring.d.ts +17 -48
  74. package/lib/hub/wiring.js +132 -1
  75. package/lib/index.d.ts +17 -16
  76. package/lib/index.js +392 -1
  77. package/lib/protocol.d.ts +1 -1
  78. package/lib/protocol.js +251 -1
  79. package/lib/schema.d.ts +2 -1
  80. package/lib/schema.js +439 -2
  81. package/lib/sync/authorize.d.ts +1 -1
  82. package/lib/sync/authorize.js +73 -1
  83. package/lib/sync/broadcast-queue.js +183 -1
  84. package/lib/sync/broadcast-sender.js +179 -1
  85. package/lib/sync/catalog-scope.js +68 -1
  86. package/lib/sync/errors.d.ts +3 -2
  87. package/lib/sync/errors.js +19 -1
  88. package/lib/sync/forwarder.js +277 -1
  89. package/lib/sync/handlers.js +160 -1
  90. package/lib/sync/hub-tunnel-sync-listener.d.ts +8 -4
  91. package/lib/sync/hub-tunnel-sync-listener.js +172 -1
  92. package/lib/sync/hub-tunnel-sync-provider.d.ts +14 -14
  93. package/lib/sync/hub-tunnel-sync-provider.js +64 -1
  94. package/lib/sync/merkle-apply.d.ts +5 -0
  95. package/lib/sync/merkle-apply.js +194 -1
  96. package/lib/sync/merkle-channel.d.ts +2 -2
  97. package/lib/sync/merkle-channel.js +182 -1
  98. package/lib/sync/merkle-tree.js +120 -1
  99. package/lib/sync/peer-registry.d.ts +1 -1
  100. package/lib/sync/peer-registry.js +107 -1
  101. package/lib/sync/receive-access-gate.js +24 -1
  102. package/lib/sync/scope-resolver.js +41 -1
  103. package/lib/sync/sync-client.d.ts +3 -3
  104. package/lib/sync/sync-client.js +192 -1
  105. package/lib/sync/sync-manager.d.ts +2 -2
  106. package/lib/sync/sync-manager.js +272 -1
  107. package/lib/types.d.ts +11 -0
  108. package/lib/types.js +2 -1
  109. package/lib/util/mutex.js +10 -1
  110. package/lib/utils.js +21 -1
  111. package/package.json +47 -40
  112. package/lib/groups/broadcast-service.d.ts +0 -263
  113. package/lib/groups/broadcast-service.js +0 -1
  114. package/lib/groups/rejoin-codec.d.ts +0 -14
  115. package/lib/groups/rejoin-codec.js +0 -1
  116. package/lib/groups/wire-frame.d.ts +0 -34
  117. package/lib/groups/wire-frame.js +0 -1
  118. package/lib/hub/circle-catchup-requester.d.ts +0 -58
  119. package/lib/hub/circle-catchup-requester.js +0 -1
  120. package/lib/hub/circle-catchup-responder.d.ts +0 -50
  121. package/lib/hub/circle-catchup-responder.js +0 -1
  122. package/lib/hub/connection-pool.d.ts +0 -43
  123. package/lib/hub/connection-pool.js +0 -1
  124. package/lib/hub/epoch-stale-detector.d.ts +0 -18
  125. package/lib/hub/epoch-stale-detector.js +0 -1
  126. package/lib/hub/forward-remote-broadcast.d.ts +0 -15
  127. package/lib/hub/forward-remote-broadcast.js +0 -1
  128. package/lib/hub/group-channel.d.ts +0 -89
  129. package/lib/hub/group-channel.js +0 -1
  130. package/lib/hub/hub-connection.d.ts +0 -96
  131. package/lib/hub/hub-connection.js +0 -1
  132. package/lib/hub/receive-handler.d.ts +0 -51
  133. package/lib/hub/receive-handler.js +0 -1
  134. package/lib/hub/rejoin-manager.d.ts +0 -78
  135. package/lib/hub/rejoin-manager.js +0 -1
  136. package/lib/hub/rejoin-responder.d.ts +0 -32
  137. package/lib/hub/rejoin-responder.js +0 -1
  138. package/lib/hub/relay-manager.d.ts +0 -142
  139. package/lib/hub/relay-manager.js +0 -1
  140. package/lib/hub/send-handler.d.ts +0 -40
  141. package/lib/hub/send-handler.js +0 -1
  142. package/lib/hub/tunnel-inbox.d.ts +0 -20
  143. package/lib/hub/tunnel-inbox.js +0 -1
  144. package/lib/hub/wait-for-gate.d.ts +0 -14
  145. package/lib/hub/wait-for-gate.js +0 -1
@@ -1 +1,107 @@
1
- import{getP2PStore as e}from"@kubun/store-p2p";export function negotiateDirection(e,t){return"both"===t?e:"both"===e?t:e===t?e:t}function t(e){var t,r;let o="string"==typeof(t=e.config)?JSON.parse(t):t??{};return{id:e.id,peerDID:e.peer_did,endpoint:e.endpoint,mode:e.mode,allowedUsers:"string"==typeof(r=e.allowed_users)?JSON.parse(r):r,priority:e.priority,trustLevel:e.trust_level,allowedDirection:o.allowedDirection,createdAt:e.created_at,updatedAt:e.updated_at}}export class PeerRegistry{#e;constructor(e){this.#e=e.runtime}async addPeer(t){let r=await e(t.stores);await r.addSyncPeer({id:this.#e.getRandomID(),peer_did:t.config.peerDID,endpoint:t.config.endpoint,mode:t.config.mode,allowed_users:t.config.allowedUsers,priority:t.config.priority,trust_level:t.config.trustLevel,config:{allowedDirection:t.config.allowedDirection},created_at:Date.now(),updated_at:Date.now()})}async getPeer(r){let o=await e(r.stores),i=await o.getSyncPeer(r.peerDID);if(i)return t(i)}async listPeers(r){let o=await e(r.stores);return(await o.listSyncPeers()).map(e=>t(e))}async updatePeer(t){let r=await this.getPeer({peerDID:t.peerDID,stores:t.stores});if(!r)throw Error(`Peer ${t.peerDID} not found`);let o={...r,...t.updates},i=await e(t.stores);await i.updateSyncPeer(t.peerDID,{endpoint:o.endpoint,mode:o.mode,allowed_users:o.allowedUsers,priority:o.priority,trust_level:o.trustLevel,config:{allowedDirection:o.allowedDirection}})}async removePeer(t){let r=await e(t.stores);await r.removeSyncPeer(t.peerDID)}async isPeerAllowed(e){return void 0!==await this.getPeer({peerDID:e.peerDID,stores:e.stores})}}
1
+ import { getP2PStore } from '@kubun/store-p2p';
2
+ /**
3
+ * Resolve the effective sync direction given what the initiator requested
4
+ * and what the responder's per-peer policy allows.
5
+ * If allowed is 'both' (or unset), the request passes through.
6
+ * Otherwise the responder's policy wins.
7
+ */ export function negotiateDirection(requested, allowed) {
8
+ if (allowed === 'both') return requested;
9
+ if (requested === 'both') return allowed;
10
+ if (requested === allowed) return requested;
11
+ // Incompatible — responder policy wins
12
+ return allowed;
13
+ }
14
+ function parseAllowedUsers(value) {
15
+ if (typeof value === 'string') {
16
+ return JSON.parse(value);
17
+ }
18
+ return value;
19
+ }
20
+ function parseConfig(value) {
21
+ if (typeof value === 'string') {
22
+ return JSON.parse(value);
23
+ }
24
+ return value ?? {};
25
+ }
26
+ function rowToPeerConfig(row) {
27
+ const config = parseConfig(row.config);
28
+ return {
29
+ id: row.id,
30
+ peerDID: row.peer_did,
31
+ endpoint: row.endpoint,
32
+ mode: row.mode,
33
+ allowedUsers: parseAllowedUsers(row.allowed_users),
34
+ priority: row.priority,
35
+ trustLevel: row.trust_level,
36
+ allowedDirection: config.allowedDirection,
37
+ createdAt: row.created_at,
38
+ updatedAt: row.updated_at
39
+ };
40
+ }
41
+ export class PeerRegistry {
42
+ #runtime;
43
+ constructor(params){
44
+ this.#runtime = params.runtime;
45
+ }
46
+ async addPeer(params) {
47
+ const store = await getP2PStore(params.stores);
48
+ await store.addSyncPeer({
49
+ id: this.#runtime.getRandomID(),
50
+ peer_did: params.config.peerDID,
51
+ endpoint: params.config.endpoint,
52
+ mode: params.config.mode,
53
+ allowed_users: params.config.allowedUsers,
54
+ priority: params.config.priority,
55
+ trust_level: params.config.trustLevel,
56
+ config: {
57
+ allowedDirection: params.config.allowedDirection
58
+ },
59
+ created_at: Date.now(),
60
+ updated_at: Date.now()
61
+ });
62
+ }
63
+ async getPeer(params) {
64
+ const store = await getP2PStore(params.stores);
65
+ const row = await store.getSyncPeer(params.peerDID);
66
+ if (!row) return undefined;
67
+ return rowToPeerConfig(row);
68
+ }
69
+ async listPeers(params) {
70
+ const store = await getP2PStore(params.stores);
71
+ const rows = await store.listSyncPeers();
72
+ return rows.map((row)=>rowToPeerConfig(row));
73
+ }
74
+ async updatePeer(params) {
75
+ const current = await this.getPeer({
76
+ peerDID: params.peerDID,
77
+ stores: params.stores
78
+ });
79
+ if (!current) throw new Error(`Peer ${params.peerDID} not found`);
80
+ const updated = {
81
+ ...current,
82
+ ...params.updates
83
+ };
84
+ const store = await getP2PStore(params.stores);
85
+ await store.updateSyncPeer(params.peerDID, {
86
+ endpoint: updated.endpoint,
87
+ mode: updated.mode,
88
+ allowed_users: updated.allowedUsers,
89
+ priority: updated.priority,
90
+ trust_level: updated.trustLevel,
91
+ config: {
92
+ allowedDirection: updated.allowedDirection
93
+ }
94
+ });
95
+ }
96
+ async removePeer(params) {
97
+ const store = await getP2PStore(params.stores);
98
+ await store.removeSyncPeer(params.peerDID);
99
+ }
100
+ async isPeerAllowed(params) {
101
+ const peer = await this.getPeer({
102
+ peerDID: params.peerDID,
103
+ stores: params.stores
104
+ });
105
+ return peer !== undefined;
106
+ }
107
+ }
@@ -1 +1,24 @@
1
- import{createAccessChecker as e}from"@kubun/engine";export function createReceiveAccessGate(c){let t=e({viewerDID:c.selfDID,db:c.db,defaultAccessLevel:c.defaultAccessLevel});return e=>t(e,"read")}
1
+ import { createAccessChecker } from '@kubun/engine';
2
+ /**
3
+ * Build an {@link AccessGate} that returns `true` iff the local peer can
4
+ * read the post-state document under the resolved access rule.
5
+ *
6
+ * Used by the receive paths (`processBroadcast` mutation:apply branch and
7
+ * `applySyncMutations`) when `storeUnreadable: 'drop'` is configured. The
8
+ * gate is intentionally cheap to construct — the heavy work (rule
9
+ * resolution + membership lookups) happens lazily when the gate is invoked
10
+ * inside the engine's apply pipeline.
11
+ *
12
+ * Drop-mode access checks happen at receive time without delegation
13
+ * token context (the broadcast doesn't carry tokens). A peer who would
14
+ * gain read access only via a delegation token will have the doc
15
+ * dropped under `storeUnreadable: 'drop'`. Use `'persist'` if delegation
16
+ * chains must be honored at receive time.
17
+ */ export function createReceiveAccessGate(params) {
18
+ const checker = createAccessChecker({
19
+ viewerDID: params.selfDID,
20
+ db: params.db,
21
+ defaultAccessLevel: params.defaultAccessLevel
22
+ });
23
+ return (postState)=>checker(postState, 'read');
24
+ }
@@ -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,15 +1,15 @@
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
14
  export type SyncClientParams = {
15
15
  runtime: Runtime;
@@ -1 +1,192 @@
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
+ constructor(params){
16
+ this.#runtime = params.runtime;
17
+ this.#identity = params.identity;
18
+ this.#logger = params.logger;
19
+ this.#serverResolver = params.serverResolver;
20
+ }
21
+ /**
22
+ * Connect to a peer server using the specified endpoint.
23
+ * - `direct://server-id` - Use in-process direct transport via sync server
24
+ * - `http://...` or `https://...` - Use HTTP transport
25
+ */ async connect(endpoint, serverID) {
26
+ if (endpoint.startsWith('direct://')) {
27
+ return this.#connectDirect(endpoint);
28
+ }
29
+ if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) {
30
+ return this.#connectHTTP(endpoint, serverID);
31
+ }
32
+ throw new Error(`Unsupported endpoint scheme: ${endpoint}`);
33
+ }
34
+ #connectHTTP(endpoint, serverID) {
35
+ if (serverID == null || serverID === '') {
36
+ throw new Error('HTTP sync transport requires a serverID (peer DID) to authenticate; without it the server rejects all requests');
37
+ }
38
+ const transport = new HTTPClientTransport({
39
+ url: endpoint,
40
+ fetch: this.#runtime.fetch
41
+ });
42
+ return new Client({
43
+ runtime: this.#runtime,
44
+ identity: this.#identity,
45
+ logger: this.#logger.getChild('sync-client'),
46
+ serverID,
47
+ transport
48
+ });
49
+ }
50
+ async #connectDirect(endpoint) {
51
+ const serverID = endpoint.replace('direct://', '');
52
+ if (!this.#serverResolver) {
53
+ throw new Error('Server resolver not configured for direct transport');
54
+ }
55
+ const provider = this.#serverResolver(serverID);
56
+ if (!provider) {
57
+ throw new Error(`Server not found: ${serverID}`);
58
+ }
59
+ // Get a sync-specific transport from the peer's p2p plugin. The provider may
60
+ // build the transport asynchronously (e.g. the hub-tunnel provider derives
61
+ // its epoch-bound inbox topics from the live MLS handle), so await it; an
62
+ // in-process provider returning a transport synchronously is unaffected.
63
+ const transport = await provider.createSyncTransport();
64
+ return new Client({
65
+ runtime: this.#runtime,
66
+ identity: this.#identity,
67
+ logger: this.#logger.getChild('sync-client'),
68
+ serverID,
69
+ transport
70
+ });
71
+ }
72
+ /**
73
+ * Negotiate sync scopes with peer.
74
+ */ async negotiate(client, scopes, delegationTokens = [], knownModelIDs, direction) {
75
+ this.#logger.debug('Negotiate: sending {scopeCount} scopes', {
76
+ scopeCount: scopes.length
77
+ });
78
+ const result = await client.request('sync/negotiate', {
79
+ param: {
80
+ scopes,
81
+ delegationTokens,
82
+ knownModelIDs,
83
+ direction
84
+ }
85
+ });
86
+ this.#logger.debug('Negotiate: accepted {acceptedCount} scopes, excluded {excludedCount} docs', {
87
+ acceptedCount: result.acceptedScopes.length,
88
+ excludedCount: result.excludedDocumentIDs.length
89
+ });
90
+ return result;
91
+ }
92
+ /**
93
+ * Perform Merkle tree-based sync with peer using bidirectional channel.
94
+ * Sends local tree and divergent mutations, receives remote mutations.
95
+ */ async merkleSync(client, params) {
96
+ const { scopes, excludedDocumentIDs, localTree, direction, localEntries, delegationTokens } = params;
97
+ this.#logger.debug('MerkleSync: creating channel with {buckets} local buckets', {
98
+ buckets: Object.keys(localTree.buckets).length
99
+ });
100
+ const channelCall = client.createChannel('sync/merkle-sync', {
101
+ param: {
102
+ scopes,
103
+ excludedDocumentIDs,
104
+ tree: localTree.buckets,
105
+ direction,
106
+ ...delegationTokens != null && {
107
+ delegationTokens
108
+ }
109
+ }
110
+ });
111
+ // Step 1: Read the responder's tree from the first message
112
+ const reader = channelCall.readable.getReader();
113
+ const firstRead = await reader.read();
114
+ if (firstRead.done) {
115
+ throw new Error('MerkleSync: channel closed before receiving remote tree');
116
+ }
117
+ const firstMessage = firstRead.value;
118
+ if (firstMessage.type !== 'tree') {
119
+ throw new Error(`MerkleSync: expected 'tree' message, got '${firstMessage.type}'`);
120
+ }
121
+ // Step 2: Compute divergent buckets
122
+ const remoteTree = {
123
+ root: firstMessage.tree?.root ?? '',
124
+ buckets: firstMessage.tree ?? {}
125
+ };
126
+ const divergentBuckets = findDivergentBuckets(localTree, remoteTree);
127
+ const divergentBucketCount = divergentBuckets.length;
128
+ this.#logger.debug('MerkleSync: found {count} divergent buckets', {
129
+ count: divergentBucketCount
130
+ });
131
+ // Step 3: Concurrent send and receive
132
+ let mutationsSent = 0;
133
+ // Send task: send divergent mutations to responder
134
+ const sendTask = async ()=>{
135
+ if (direction === 'push' || direction === 'both') {
136
+ // Filter local entries to those in divergent buckets
137
+ const divergentSet = new Set(divergentBuckets);
138
+ const entriesToSend = localEntries.filter((entry)=>{
139
+ const { minute } = getTimeBuckets(entry.hlc);
140
+ return divergentSet.has(minute);
141
+ });
142
+ // Send in batches
143
+ for(let i = 0; i < entriesToSend.length; i += SYNC_BATCH_SIZE){
144
+ const batch = entriesToSend.slice(i, i + SYNC_BATCH_SIZE);
145
+ await channelCall.send({
146
+ type: 'mutations',
147
+ mutationJWTs: batch.map((e)=>e.mutation_jwt)
148
+ });
149
+ mutationsSent += batch.length;
150
+ }
151
+ }
152
+ // Signal completion
153
+ await channelCall.send({
154
+ type: 'complete'
155
+ });
156
+ };
157
+ // Receive task: collect mutations from responder
158
+ const receiveTask = async ()=>{
159
+ const collectedJWTs = [];
160
+ while(true){
161
+ const { done, value } = await reader.read();
162
+ if (done) break;
163
+ if (value.type === 'mutations' && value.mutationJWTs != null) {
164
+ collectedJWTs.push(...value.mutationJWTs);
165
+ } else if (value.type === 'complete') {
166
+ break;
167
+ }
168
+ }
169
+ reader.releaseLock();
170
+ return collectedJWTs;
171
+ };
172
+ const [, mutationJWTs] = await Promise.all([
173
+ sendTask(),
174
+ receiveTask()
175
+ ]);
176
+ // Wait for the handler to finish processing (applying pushed mutations)
177
+ // before returning — prevents transport teardown racing with mutation application
178
+ const handlerResult = await channelCall;
179
+ if (handlerResult != null && !handlerResult.success) {
180
+ throw new Error('MerkleSync: responder reported failure');
181
+ }
182
+ this.#logger.debug('MerkleSync: completed with {received} received, {sent} sent', {
183
+ received: mutationJWTs.length,
184
+ sent: mutationsSent
185
+ });
186
+ return {
187
+ mutationJWTs,
188
+ divergentBuckets: divergentBucketCount,
189
+ mutationsSent
190
+ };
191
+ }
192
+ }
@@ -1,8 +1,8 @@
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';