@kubun/plugin-p2p 0.10.0 → 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 (136) 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 +12 -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 +122 -38
  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 +7 -2
  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 +69 -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 +56 -0
  28. package/lib/groups/group-health-monitor.js +139 -0
  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 +29 -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 +4 -8
  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.d.ts +5 -4
  73. package/lib/hub/manager.js +198 -1
  74. package/lib/hub/wiring.d.ts +28 -41
  75. package/lib/hub/wiring.js +132 -1
  76. package/lib/index.d.ts +29 -13
  77. package/lib/index.js +392 -1
  78. package/lib/protocol.d.ts +8 -1
  79. package/lib/protocol.js +251 -1
  80. package/lib/schema.d.ts +2 -1
  81. package/lib/schema.js +471 -2
  82. package/lib/sync/authorize.d.ts +25 -0
  83. package/lib/sync/authorize.js +73 -0
  84. package/lib/sync/broadcast-queue.js +183 -1
  85. package/lib/sync/broadcast-sender.js +179 -1
  86. package/lib/sync/catalog-scope.js +68 -1
  87. package/lib/sync/errors.d.ts +12 -0
  88. package/lib/sync/errors.js +19 -0
  89. package/lib/sync/forwarder.js +277 -1
  90. package/lib/sync/handlers.d.ts +3 -22
  91. package/lib/sync/handlers.js +160 -1
  92. package/lib/sync/hub-tunnel-sync-listener.d.ts +9 -3
  93. package/lib/sync/hub-tunnel-sync-listener.js +172 -1
  94. package/lib/sync/hub-tunnel-sync-provider.d.ts +14 -14
  95. package/lib/sync/hub-tunnel-sync-provider.js +64 -1
  96. package/lib/sync/merkle-apply.d.ts +5 -0
  97. package/lib/sync/merkle-apply.js +194 -1
  98. package/lib/sync/merkle-channel.d.ts +19 -0
  99. package/lib/sync/merkle-channel.js +182 -1
  100. package/lib/sync/merkle-tree.js +120 -1
  101. package/lib/sync/peer-registry.d.ts +8 -1
  102. package/lib/sync/peer-registry.js +107 -1
  103. package/lib/sync/receive-access-gate.js +24 -1
  104. package/lib/sync/scope-resolver.js +41 -1
  105. package/lib/sync/sync-client.d.ts +5 -3
  106. package/lib/sync/sync-client.js +192 -1
  107. package/lib/sync/sync-manager.d.ts +2 -2
  108. package/lib/sync/sync-manager.js +272 -1
  109. package/lib/types.d.ts +41 -0
  110. package/lib/types.js +2 -1
  111. package/lib/util/mutex.js +10 -1
  112. package/lib/utils.d.ts +5 -0
  113. package/lib/utils.js +21 -0
  114. package/package.json +48 -41
  115. package/lib/groups/broadcast-service.d.ts +0 -189
  116. package/lib/groups/broadcast-service.js +0 -1
  117. package/lib/groups/wire-frame.d.ts +0 -32
  118. package/lib/groups/wire-frame.js +0 -1
  119. package/lib/hub/connection-pool.d.ts +0 -43
  120. package/lib/hub/connection-pool.js +0 -1
  121. package/lib/hub/forward-remote-broadcast.d.ts +0 -15
  122. package/lib/hub/forward-remote-broadcast.js +0 -1
  123. package/lib/hub/group-channel.d.ts +0 -67
  124. package/lib/hub/group-channel.js +0 -1
  125. package/lib/hub/hub-connection.d.ts +0 -96
  126. package/lib/hub/hub-connection.js +0 -1
  127. package/lib/hub/receive-handler.d.ts +0 -33
  128. package/lib/hub/receive-handler.js +0 -1
  129. package/lib/hub/relay-manager.d.ts +0 -127
  130. package/lib/hub/relay-manager.js +0 -1
  131. package/lib/hub/send-handler.d.ts +0 -40
  132. package/lib/hub/send-handler.js +0 -1
  133. package/lib/hub/tunnel-inbox.d.ts +0 -20
  134. package/lib/hub/tunnel-inbox.js +0 -1
  135. package/lib/hub/wait-for-gate.d.ts +0 -14
  136. package/lib/hub/wait-for-gate.js +0 -1
@@ -1 +1,198 @@
1
- import{getP2PStore as r}from"@kubun/store-p2p";export class HubServerDIDConflictError extends Error{hubID;url;existingServerDID;providedServerDID;constructor(r){super(`hub at ${r.url} is pinned to ${r.existingServerDID}; refusing to overwrite with ${r.providedServerDID}`),this.name="HubServerDIDConflictError",this.hubID=r.hubID,this.url=r.url,this.existingServerDID=r.existingServerDID,this.providedServerDID=r.providedServerDID}}function e(r){return{id:r.id,label:r.label,url:r.url,serverDID:r.server_did,createdAt:new Date(r.created_at),updatedAt:null==r.updated_at?null:new Date(r.updated_at)}}export async function createHub(t){let u=await r(t.stores),i=t.serverDID??null,a=await u.getHubByURL(t.url);if(null!=a){if(null!=a.server_did&&null!=i&&a.server_did!==i)throw new HubServerDIDConflictError({hubID:a.id,url:t.url,existingServerDID:a.server_did,providedServerDID:i});if(null==a.server_did&&null!=i){await u.updateHub(a.id,{server_did:i});let r=await u.getHub(a.id);if(null==r)throw Error("Failed to read hub after upgrade");return e(r)}return e(a)}let l=crypto.randomUUID();await u.insertHub({id:l,label:t.label??null,url:t.url,server_did:i});let n=await u.getHub(l);if(null==n)throw Error("Failed to read hub after insert");return e(n)}export async function upsertHub(r){return await createHub({stores:r.stores,url:r.url,serverDID:r.serverDID})}export async function updateHub(t){let u=await r(t.stores),i=await u.getHub(t.id);if(null==i)throw Error(`Hub ${t.id} not found`);if(null!=t.url&&t.url!==i.url){let r=await u.getHubByURL(t.url);if(null!=r&&r.id!==t.id)throw Error(`URL ${t.url} already in use by hub ${r.id}`)}let a={};void 0!==t.label&&(a.label=t.label),void 0!==t.url&&(a.url=t.url),void 0!==t.serverDID&&(a.server_did=t.serverDID),await u.updateHub(t.id,a);let l=await u.getHub(t.id);if(null==l)throw Error("Failed to read hub after update");return e(l)}export async function deleteHub(e){let t=await r(e.stores),u=await t.listGroupIDsByHubID(e.id);return u.length>0&&!e.force?{success:!1,boundGroupIDs:u}:(u.length>0&&await t.deleteGroupHubsByHubID(e.id),await t.deleteHub(e.id),{success:!0,boundGroupIDs:u})}export async function getHub(t){let u=await r(t.stores),i=await u.getHub(t.id);return null==i?null:e(i)}export async function listHubs(t){let u=await r(t.stores);return(await u.listHubs()).map(e)}export async function bindHubToGroup(e){let t=await r(e.stores);return await t.insertGroupHub({group_id:e.groupID,hub_id:e.hubID})}export async function unbindHubFromGroup(e){let t=await r(e.stores);return await t.deleteGroupHub({groupID:e.groupID,hubID:e.hubID})}export async function listHubsByGroup(t){let u=await r(t.stores);return(await u.listHubsByGroupID(t.groupID)).map(e)}export async function listGroupsByHub(e){let t=await r(e.stores);return await t.listGroupIDsByHubID(e.hubID)}export async function captureServerDID(e){let t=await r(e.stores),u=await t.getHubByURL(e.hubURL);if(null==u)return!1;if(null==u.server_did)return await t.updateHub(u.id,{server_did:e.serverDID}),!0;if(u.server_did===e.serverDID)return!1;throw new HubServerDIDConflictError({hubID:u.id,url:e.hubURL,existingServerDID:u.server_did,providedServerDID:e.serverDID})}
1
+ import { getP2PStore } from '@kubun/store-p2p';
2
+ export class HubServerDIDConflictError extends Error {
3
+ #hubID;
4
+ #url;
5
+ #existingServerDID;
6
+ #providedServerDID;
7
+ constructor(params){
8
+ super(`hub at ${params.url} is pinned to ${params.existingServerDID}; refusing to overwrite with ${params.providedServerDID}`);
9
+ this.name = 'HubServerDIDConflictError';
10
+ this.#hubID = params.hubID;
11
+ this.#url = params.url;
12
+ this.#existingServerDID = params.existingServerDID;
13
+ this.#providedServerDID = params.providedServerDID;
14
+ }
15
+ get hubID() {
16
+ return this.#hubID;
17
+ }
18
+ get url() {
19
+ return this.#url;
20
+ }
21
+ get existingServerDID() {
22
+ return this.#existingServerDID;
23
+ }
24
+ get providedServerDID() {
25
+ return this.#providedServerDID;
26
+ }
27
+ }
28
+ function rowToHub(row) {
29
+ return {
30
+ id: row.id,
31
+ label: row.label,
32
+ url: row.url,
33
+ serverDID: row.server_did,
34
+ createdAt: new Date(row.created_at),
35
+ updatedAt: row.updated_at == null ? null : new Date(row.updated_at)
36
+ };
37
+ }
38
+ export async function createHub(params) {
39
+ const store = await getP2PStore(params.stores);
40
+ const providedDID = params.serverDID ?? null;
41
+ const existing = await store.getHubByURL(params.url);
42
+ if (existing != null) {
43
+ if (existing.server_did != null && providedDID != null && existing.server_did !== providedDID) {
44
+ throw new HubServerDIDConflictError({
45
+ hubID: existing.id,
46
+ url: params.url,
47
+ existingServerDID: existing.server_did,
48
+ providedServerDID: providedDID
49
+ });
50
+ }
51
+ if (existing.server_did == null && providedDID != null) {
52
+ await store.updateHub(existing.id, {
53
+ server_did: providedDID
54
+ });
55
+ const updated = await store.getHub(existing.id);
56
+ if (updated == null) throw new Error('Failed to read hub after upgrade');
57
+ return rowToHub(updated);
58
+ }
59
+ return rowToHub(existing);
60
+ }
61
+ const id = crypto.randomUUID();
62
+ await store.insertHub({
63
+ id,
64
+ label: params.label ?? null,
65
+ url: params.url,
66
+ server_did: providedDID
67
+ });
68
+ const created = await store.getHub(id);
69
+ if (created == null) throw new Error('Failed to read hub after insert');
70
+ return rowToHub(created);
71
+ }
72
+ /**
73
+ * Idempotent register-or-pin entry-point. Applies the conflict matrix:
74
+ *
75
+ * - URL not registered → insert a fresh row with `serverDID` (or null) and
76
+ * return it.
77
+ * - existing row, `server_did === null`, provided null → no-op.
78
+ * - existing row, `server_did === null`, provided DID-A → upgrade pin to
79
+ * DID-A.
80
+ * - existing row, `server_did === DID-A`, provided null or matching DID-A →
81
+ * no-op (no silent downgrade).
82
+ * - existing row, `server_did === DID-A`, provided DID-B → throws
83
+ * `HubServerDIDConflictError`. Caller must explicit `updateHub` to change
84
+ * a pinned DID.
85
+ *
86
+ * Unlike `createHub`, `upsertHub` takes no `label` — it is for atomic
87
+ * register+bind paths where the inviter only ships connectivity + pin.
88
+ */ export async function upsertHub(params) {
89
+ return await createHub({
90
+ stores: params.stores,
91
+ url: params.url,
92
+ serverDID: params.serverDID
93
+ });
94
+ }
95
+ export async function updateHub(params) {
96
+ const store = await getP2PStore(params.stores);
97
+ const existing = await store.getHub(params.id);
98
+ if (existing == null) {
99
+ throw new Error(`Hub ${params.id} not found`);
100
+ }
101
+ if (params.url != null && params.url !== existing.url) {
102
+ const collision = await store.getHubByURL(params.url);
103
+ if (collision != null && collision.id !== params.id) {
104
+ throw new Error(`URL ${params.url} already in use by hub ${collision.id}`);
105
+ }
106
+ }
107
+ const update = {};
108
+ if (params.label !== undefined) update.label = params.label;
109
+ if (params.url !== undefined) update.url = params.url;
110
+ if (params.serverDID !== undefined) update.server_did = params.serverDID;
111
+ await store.updateHub(params.id, update);
112
+ const updated = await store.getHub(params.id);
113
+ if (updated == null) throw new Error('Failed to read hub after update');
114
+ return rowToHub(updated);
115
+ }
116
+ export async function deleteHub(params) {
117
+ const store = await getP2PStore(params.stores);
118
+ const boundGroupIDs = await store.listGroupIDsByHubID(params.id);
119
+ if (boundGroupIDs.length > 0 && !params.force) {
120
+ return {
121
+ success: false,
122
+ boundGroupIDs
123
+ };
124
+ }
125
+ if (boundGroupIDs.length > 0) {
126
+ await store.deleteGroupHubsByHubID(params.id);
127
+ }
128
+ await store.deleteHub(params.id);
129
+ return {
130
+ success: true,
131
+ boundGroupIDs
132
+ };
133
+ }
134
+ export async function getHub(params) {
135
+ const store = await getP2PStore(params.stores);
136
+ const row = await store.getHub(params.id);
137
+ return row == null ? null : rowToHub(row);
138
+ }
139
+ export async function listHubs(params) {
140
+ const store = await getP2PStore(params.stores);
141
+ const rows = await store.listHubs();
142
+ return rows.map(rowToHub);
143
+ }
144
+ export async function bindHubToGroup(params) {
145
+ const store = await getP2PStore(params.stores);
146
+ return await store.insertGroupHub({
147
+ group_id: params.groupID,
148
+ hub_id: params.hubID
149
+ });
150
+ }
151
+ export async function unbindHubFromGroup(params) {
152
+ const store = await getP2PStore(params.stores);
153
+ return await store.deleteGroupHub({
154
+ groupID: params.groupID,
155
+ hubID: params.hubID
156
+ });
157
+ }
158
+ export async function listHubsByGroup(params) {
159
+ const store = await getP2PStore(params.stores);
160
+ const rows = await store.listHubsByGroupID(params.groupID);
161
+ return rows.map(rowToHub);
162
+ }
163
+ export async function listGroupsByHub(params) {
164
+ const store = await getP2PStore(params.stores);
165
+ return await store.listGroupIDsByHubID(params.hubID);
166
+ }
167
+ /**
168
+ * TOFU capture path. Looks up the local hub row by URL and applies the
169
+ * conflict matrix:
170
+ * - URL not registered → no-op, returns `false`.
171
+ * - existing `server_did` is null → upgrade to `serverDID`, returns `true`.
172
+ * - existing `server_did` matches `serverDID` → no-op, returns `false`.
173
+ * - existing `server_did` differs from `serverDID` → throws
174
+ * `HubServerDIDConflictError`. Pinned-mismatch enforcement (hard-fail
175
+ * before any message is delivered) lives in the transport wrapper and
176
+ * surfaces `HubServerDIDMismatchError` to callers.
177
+ */ export async function captureServerDID(params) {
178
+ const store = await getP2PStore(params.stores);
179
+ const existing = await store.getHubByURL(params.hubURL);
180
+ if (existing == null) {
181
+ return false;
182
+ }
183
+ if (existing.server_did == null) {
184
+ await store.updateHub(existing.id, {
185
+ server_did: params.serverDID
186
+ });
187
+ return true;
188
+ }
189
+ if (existing.server_did === params.serverDID) {
190
+ return false;
191
+ }
192
+ throw new HubServerDIDConflictError({
193
+ hubID: existing.id,
194
+ url: params.hubURL,
195
+ existingServerDID: existing.server_did,
196
+ providedServerDID: params.serverDID
197
+ });
198
+ }
@@ -1,17 +1,14 @@
1
- import type { OwnIdentity } from '@enkaku/token';
1
+ import type { OwnIdentity } from '@kokuin/token';
2
2
  import type { KubunDB } from '@kubun/db';
3
3
  import type { DefaultAccessLevel, GraphInternals } from '@kubun/engine';
4
+ import type { HLC } from '@kubun/hlc';
4
5
  import type { Logger } from '@kubun/logger';
5
6
  import type { GroupBroadcastMessage } from '../groups/broadcast.js';
6
- import { type BroadcastEvent } from '../groups/broadcast-service.js';
7
7
  import type { P2PEventEmitter } from '../groups/events.js';
8
8
  import type { GroupHandleRegistry } from '../groups/group-handle-registry.js';
9
- import type { ForwardingConfig } from '../sync/forwarder.js';
10
- import type { StoreUnreadableMode } from '../types.js';
11
- import type { OnServerDIDObserved } from './connection-pool.js';
12
- import type { GroupChannel, GroupChannelParams } from './group-channel.js';
13
- import type { HubConnection, HubConnectionParams } from './hub-connection.js';
14
- import { type CreateHubClient } from './relay-manager.js';
9
+ import type { RejoinResult, StoreUnreadableMode } from '../types.js';
10
+ import type { OnServerDIDObserved } from './did-observing-transport.js';
11
+ import type { CreateHubClient } from './http-client.js';
15
12
  export type HubWiring = {
16
13
  ready: Promise<void>;
17
14
  scheduleBroadcast: (groupID: string, message: GroupBroadcastMessage) => void;
@@ -24,62 +21,52 @@ export type HubWiring = {
24
21
  broadcastNow: (groupID: string, message: GroupBroadcastMessage) => Promise<void>;
25
22
  /**
26
23
  * Fire-and-forget fan-out of a pre-produced MLS handshake Commit to a
27
- * group's existing members. Wired to `HubRelayManager.sendCommit`.
24
+ * group's existing members. Wired to `GroupPeerManager.sendCommit`.
28
25
  */
29
26
  scheduleSendCommit: (groupID: string, commitBytes: Uint8Array) => void;
27
+ /**
28
+ * Run stale-device MLS epoch recovery for an active group. Wired to
29
+ * `GroupPeerManager.rejoinGroup`; deep recovery is deferred so this currently
30
+ * reports `recovery-failed`.
31
+ */
32
+ rejoinGroup: (groupID: string) => Promise<RejoinResult>;
30
33
  dispose: () => Promise<void>;
31
34
  };
32
35
  export type SetupHubRelayParams = {
33
36
  identity: OwnIdentity;
37
+ /** Fresh correlation ids for group-rpc. */
38
+ getRandomID: () => string;
34
39
  db: KubunDB;
35
40
  /**
36
- * Engine graph internals — threaded through to `BroadcastService` so the
37
- * `mutation:apply` broadcast variant can route peer-authored mutations
38
- * through the engine with `origin: 'peer'`.
41
+ * Engine graph internals — threaded through to the per-group peer apply path
42
+ * so a `sync/mutationApply` frame routes peer-authored mutations through the
43
+ * engine with `origin: 'peer'`.
39
44
  */
40
45
  graph: GraphInternals;
41
46
  emitter: P2PEventEmitter;
42
47
  createHubClient: CreateHubClient;
43
- onRemoteBroadcast: (event: BroadcastEvent) => void;
44
- /**
45
- * Single canonical access point for MLS `GroupHandle` instances. Threaded
46
- * into `BroadcastService` so encrypt/decrypt go through the per-group
47
- * mutex once Phase 3 lands. Currently stored but unused.
48
- */
48
+ /** Single canonical access point for MLS `GroupHandle` instances. */
49
49
  registry: GroupHandleRegistry;
50
50
  logger: Logger;
51
51
  /**
52
- * Optional testing seam forwarded to `HubRelayManager`. Production code
53
- * leaves this unset so the manager uses its default `GroupChannel`
54
- * constructor.
55
- */
56
- createGroupChannel?: (params: GroupChannelParams) => GroupChannel;
57
- /**
58
- * Optional testing seam forwarded to `HubRelayManager`. Production code
59
- * leaves this unset so the manager uses its default `HubConnection`
60
- * constructor.
61
- */
62
- createHubConnection?: (params: HubConnectionParams) => HubConnection;
63
- /**
64
- * Receive-side storage mode. Forwarded to `BroadcastService` →
65
- * `processBroadcast`. Defaults to `'persist'` when omitted.
52
+ * Receive-side storage mode. Forwarded to the apply path. Defaults to
53
+ * `'persist'` when omitted.
66
54
  */
67
55
  storeUnreadable?: StoreUnreadableMode;
68
56
  /** Server default access level — required when `storeUnreadable === 'drop'`. */
69
57
  defaultAccessLevel?: DefaultAccessLevel;
70
58
  /**
71
- * Forwarding config. Threaded through to `BroadcastService`
72
- * `processBroadcast` along with the hub's own `scheduleBroadcast` hook.
73
- * Default `false` disables the receive-time forwarding pipeline.
59
+ * Fired with the observed `serverDID` after the first signed response from a
60
+ * freshly-spawned client, hub URL bound. Lets callers route capture into
61
+ * `captureServerDID` for TOFU pinning; omitting it skips capture.
74
62
  */
75
- forwarding?: ForwardingConfig;
63
+ onServerDIDObserved?: OnServerDIDObserved;
76
64
  /**
77
- * Forwarded to `HubRelayManager` `HubConnectionPool`. Fired with the
78
- * observed `serverDID` after the first signed response from a freshly-spawned
79
- * client. Lets callers route capture into `captureServerDID` for TOFU
80
- * pinning; omitting it skips capture entirely.
65
+ * Device-wide monotonic clock forwarded to the apply path for stamping
66
+ * `removed_at_hlc` on roster reconciliation. Required: production passes the
67
+ * engine's single instance so roster writes share the device's one clock.
81
68
  */
82
- onServerDIDObserved?: OnServerDIDObserved;
69
+ hlc: HLC;
83
70
  };
84
71
  export declare function setupHubRelay(params: SetupHubRelayParams): HubWiring;
85
72
  export declare const DISABLED_HUB: HubWiring;
package/lib/hub/wiring.js CHANGED
@@ -1 +1,132 @@
1
- import{HLC as e}from"@kubun/hlc";import{BroadcastService as r}from"../groups/broadcast-service.js";import{HubRelayManager as o}from"./relay-manager.js";export function setupHubRelay(a){let t,{identity:d,db:i,graph:n,emitter:s,createHubClient:c,onRemoteBroadcast:u,registry:p,logger:h,createGroupChannel:l,createHubConnection:m,storeUnreadable:g,defaultAccessLevel:f,forwarding:b,onServerDIDObserved:y}=a,w=[],D=(e,r)=>{t.then(o=>o.broadcast(e,r).catch(r=>{h.error("broadcast failed",{groupID:e,error:r})}),()=>{})},v=async(e,r)=>{let o=await t;await o.broadcast(e,r)};return(t=(async()=>{let[a,t]=await Promise.all([i.getStore("p2p"),i.getStore("graph")]),v=new r({p2pStore:a,graphStore:t,graph:n,selfDID:d.id,registry:p,hlc:new e({nodeID:d.id}),storeUnreadable:g,defaultAccessLevel:f,forwarding:b,scheduleBroadcast:D,emitter:s,logger:h}),B=new o({p2pStore:a,broadcastService:v,deviceID:d.id,createHubClient:c,logger:h,createGroupChannel:l,createHubConnection:m,onServerDIDObserved:y});w.push(v.on("broadcast",u)),w.push(s.on("groupJoined",e=>B.addGroup(e.id).catch(r=>{h.error("addGroup failed",{groupID:e.id,error:r})}))),w.push(s.on("groupLeft",e=>B.removeGroup(e.groupID).catch(r=>{h.error("removeGroup failed",{groupID:e.groupID,error:r})}))),w.push(s.on("hubBound",({groupID:e,hubURL:r})=>B.addBinding(e,r).catch(o=>{h.error("addBinding failed",{groupID:e,hubURL:r,error:o})}))),w.push(s.on("hubUnbound",({groupID:e,hubURL:r})=>B.removeBinding(e,r).catch(o=>{h.error("removeBinding failed",{groupID:e,hubURL:r,error:o})})));let I=await a.listGroups();return await B.start(I.map(e=>e.id)),B})()).catch(e=>{h.error("hub relay setup failed",{error:e})}),{ready:t.then(()=>void 0),scheduleBroadcast:D,broadcastNow:v,scheduleSendCommit:(e,r)=>{t.then(o=>o.sendCommit(e,r).catch(r=>{h.error("sendCommit failed",{groupID:e,error:r})}),()=>{})},dispose:async()=>{for(let e of w)try{e()}catch{}w.length=0;try{let e=await t;await e.stop()}catch{}}}}export const DISABLED_HUB={ready:Promise.resolve(),scheduleBroadcast:()=>{},broadcastNow:async()=>{},scheduleSendCommit:()=>{},dispose:async()=>{}};
1
+ import { createGroupPeerManager } from '../groups/group-peer-manager.js';
2
+ export function setupHubRelay(params) {
3
+ const { identity, getRandomID, db, graph, emitter, createHubClient, registry, logger, storeUnreadable, defaultAccessLevel, onServerDIDObserved, hlc } = params;
4
+ const unsubscribes = [];
5
+ // Late-bound `ready` resolving to the live manager. The fire-and-forget
6
+ // schedulers chain off it so a caller can enqueue work before boot completes;
7
+ // the awaitable variants simply await it.
8
+ let ready;
9
+ const scheduleBroadcast = (groupID, message)=>{
10
+ void ready.then((manager)=>manager.broadcast(groupID, message).catch((error)=>{
11
+ logger.error('broadcast failed', {
12
+ groupID,
13
+ error
14
+ });
15
+ }), ()=>{
16
+ // ready rejected; setup failure already logged below
17
+ });
18
+ };
19
+ const broadcastNow = async (groupID, message)=>{
20
+ const manager = await ready;
21
+ await manager.broadcast(groupID, message);
22
+ };
23
+ const scheduleSendCommit = (groupID, commitBytes)=>{
24
+ void ready.then((manager)=>manager.sendCommit(groupID, commitBytes).catch((error)=>{
25
+ logger.error('sendCommit failed', {
26
+ groupID,
27
+ error
28
+ });
29
+ }), ()=>{
30
+ // ready rejected; setup failure already logged below
31
+ });
32
+ };
33
+ const rejoinGroup = async (groupID)=>{
34
+ const manager = await ready;
35
+ return await manager.rejoinGroup(groupID);
36
+ };
37
+ ready = (async ()=>{
38
+ const [p2pStore, graphStore] = await Promise.all([
39
+ db.getStore('p2p'),
40
+ db.getStore('graph')
41
+ ]);
42
+ const manager = createGroupPeerManager({
43
+ createHubClient,
44
+ registry,
45
+ stores: db,
46
+ graphStore,
47
+ graph,
48
+ hlc,
49
+ localDID: identity.id,
50
+ emitter,
51
+ getRandomID,
52
+ logger,
53
+ storeUnreadable,
54
+ defaultAccessLevel,
55
+ onServerDIDObserved
56
+ });
57
+ unsubscribes.push(emitter.on('groupJoined', (group)=>manager.addGroup(group.id).catch((error)=>{
58
+ logger.error('addGroup failed', {
59
+ groupID: group.id,
60
+ error
61
+ });
62
+ })));
63
+ unsubscribes.push(emitter.on('groupLeft', (group)=>manager.removeGroup(group.groupID).catch((error)=>{
64
+ logger.error('removeGroup failed', {
65
+ groupID: group.groupID,
66
+ error
67
+ });
68
+ })));
69
+ unsubscribes.push(emitter.on('hubBound', ({ groupID, hubURL })=>manager.addBinding(groupID, hubURL).catch((error)=>{
70
+ logger.error('addBinding failed', {
71
+ groupID,
72
+ hubURL,
73
+ error
74
+ });
75
+ })));
76
+ unsubscribes.push(emitter.on('hubUnbound', ({ groupID, hubURL })=>manager.removeBinding(groupID, hubURL).catch((error)=>{
77
+ logger.error('removeBinding failed', {
78
+ groupID,
79
+ hubURL,
80
+ error
81
+ });
82
+ })));
83
+ const groups = await p2pStore.listGroups();
84
+ await manager.start(groups.map((g)=>g.id));
85
+ return manager;
86
+ })();
87
+ // Prevent an unhandled rejection if nobody awaits `ready` directly.
88
+ ready.catch((error)=>{
89
+ logger.error('hub relay setup failed', {
90
+ error
91
+ });
92
+ });
93
+ return {
94
+ // Handle rejection here too: the base `.catch` covers the root `ready`, not
95
+ // this derived chain, and callers of `.ready` don't always await it.
96
+ ready: ready.then(()=>undefined, (error)=>{
97
+ logger.error('hub relay setup failed', {
98
+ error
99
+ });
100
+ }),
101
+ scheduleBroadcast,
102
+ broadcastNow,
103
+ scheduleSendCommit,
104
+ rejoinGroup,
105
+ dispose: async ()=>{
106
+ for (const off of unsubscribes){
107
+ try {
108
+ off();
109
+ } catch {
110
+ // ignore
111
+ }
112
+ }
113
+ unsubscribes.length = 0;
114
+ try {
115
+ const manager = await ready;
116
+ await manager.stop();
117
+ } catch {
118
+ // setup failed; nothing to stop
119
+ }
120
+ }
121
+ };
122
+ }
123
+ export const DISABLED_HUB = {
124
+ ready: Promise.resolve(),
125
+ scheduleBroadcast: ()=>{},
126
+ broadcastNow: async ()=>{},
127
+ scheduleSendCommit: ()=>{},
128
+ rejoinGroup: async ()=>({
129
+ status: 'recovery-failed'
130
+ }),
131
+ dispose: async ()=>{}
132
+ };
package/lib/index.d.ts CHANGED
@@ -1,17 +1,20 @@
1
1
  import type { DefaultAccessLevel, KubunPlugin, PluginFactoryParams } from '@kubun/engine';
2
- import type { CreateHubClient } from './hub/relay-manager.js';
2
+ import { type CreateHubClient } from './hub/http-client.js';
3
3
  import { type BroadcastBatchConfig } from './sync/broadcast-queue.js';
4
4
  import { type PushSyncConfig } from './sync/broadcast-sender.js';
5
5
  import type { ForwardingConfig } from './sync/forwarder.js';
6
6
  import type { StoreUnreadableMode } from './types.js';
7
7
  export type { ContextDeps, PendingJoinRequest } from './context/types.js';
8
+ export { ADMIN_ROLE_ENTRY_TYPE, ADMIN_ROSTER_PROJECTION, type AdminRoleValue, type AdminRosterState, adminRosterReducer, applyAdminEntryIncremental, foldAdminRoster, type ProjectAdminRosterParams, projectAdminRoster, projectAdminRosterFromStore, reprojectAdminRoster, } from './groups/admin-roster.js';
8
9
  export { type AffectedRow, type GroupBroadcastMessage, type MutationApplyEntry, type ProcessBroadcastParams, type ProcessBroadcastResult, processBroadcast, } from './groups/broadcast.js';
9
10
  export { deserializeBroadcast, serializeBroadcast } from './groups/broadcast-codec.js';
10
- export { ApplyError, type BroadcastEvent, BroadcastService, type BroadcastServiceEvents, type BroadcastServiceParams, DecryptError, type ReceiveBroadcastParams, type ReceiveBroadcastResult, type SendBroadcastParams, } from './groups/broadcast-service.js';
11
11
  export { createFilteredGenerator, createP2PEventEmitter, type P2PEventEmitter, type P2PEventMap, } from './groups/events.js';
12
+ export { type GroupAnchor, groupAnchorCapabilities, readGroupAnchor, } from './groups/group-anchor.js';
12
13
  export { GroupHandleRegistry, type GroupHandleRegistryParams, type SeedParams, } from './groups/group-handle-registry.js';
13
14
  export { decodeFullJoinRequest, decodeInvitePayload, decodeJoinRequest, encodeFullJoinRequest, encodeInvitePayload, encodeJoinRequest, type FullJoinRequestPayload, type InvitePayload, type JoinRequestPayload, type SuggestedHub, } from './groups/invite-payload.js';
14
- export { type AddCircleMemberParams, type AddCircleMemberResult, type CreateCircleParams, type CreateCircleResult, type CreateGroupParams, type CreateGroupResult, type DeleteCircleParams, type DeleteCircleResult, GroupManager, type GroupManagerParams, type InviteToGroupParams, type InviteToGroupResult, type JoinGroupParams, type JoinGroupResult, type LeaveGroupParams, type LeaveGroupResult, type RemoveCircleMemberParams, type RemoveCircleMemberResult, type RemoveGroupMemberParams, type RemoveGroupMemberResult, type RemoveMemberParams, type RemoveMemberResult, type UpdateCircleParams, type UpdateCircleResult, type UpdateGroupParams, type UpdateGroupResult, } from './groups/manager.js';
15
+ export { type LedgerEntry, ledgerEntryDigest, signLedgerEntry, type VerifiedLedgerEntry, verifyLedgerEntry, } from './groups/ledger-entry.js';
16
+ export { type FoldInput, foldLedger, type LedgerReducer, } from './groups/ledger-fold.js';
17
+ export { type CreateGroupParams, type CreateGroupResult, GroupManager, type GroupManagerParams, type InviteToGroupParams, type InviteToGroupResult, type JoinGroupParams, type JoinGroupResult, type LeaveGroupParams, type LeaveGroupResult, type RemoveGroupMemberParams, type RemoveGroupMemberResult, type RemoveMemberParams, type RemoveMemberResult, } from './groups/manager.js';
15
18
  export { MLSEncryptor, type MLSEncryptorParams } from './groups/mls-encryptor.js';
16
19
  export { type MLSGroupHandle, restoreMLSGroupHandle } from './groups/mls-group-handle.js';
17
20
  export { replacer as mlsJSONReplacer, reviver as mlsJSONReviver } from './groups/mls-json.js';
@@ -21,21 +24,17 @@ export { createP2PSchemaExtension } from './schema.js';
21
24
  export { type BroadcastBatchConfig, type BroadcastQueue, type BroadcastQueueParams, createBroadcastQueue, DEFAULT_BROADCAST_BATCH_CONFIG, } from './sync/broadcast-queue.js';
22
25
  export { type BroadcastSenderParams, DEFAULT_PUSH_SYNC_CONFIG, type PushSyncConfig, wireBroadcastSender, } from './sync/broadcast-sender.js';
23
26
  export { type CatalogSyncScope, resolveCatalogSyncScopes } from './sync/catalog-scope.js';
27
+ export { SyncAccessDeniedError } from './sync/errors.js';
24
28
  export { type EvaluateAndForwardParams, type EvaluateAndForwardResult, evaluateAndForward, type ForwardContext, type ForwardFilter, type ForwardingConfig, } from './sync/forwarder.js';
25
- export { checkSyncDelegation, negotiateDirection } from './sync/handlers.js';
29
+ export { checkSyncDelegation } from './sync/handlers.js';
26
30
  export { HubTunnelSyncListener, type HubTunnelSyncListenerParams, } from './sync/hub-tunnel-sync-listener.js';
27
31
  export { HubTunnelSyncProvider, type HubTunnelSyncProviderParams, } from './sync/hub-tunnel-sync-provider.js';
28
- export { type PeerConfig, type PeerConfigWithID, PeerRegistry } from './sync/peer-registry.js';
32
+ export { negotiateDirection, type PeerConfig, type PeerConfigWithID, PeerRegistry, } from './sync/peer-registry.js';
29
33
  export { SyncClient, type SyncClientParams, type SyncScope, type SyncTransportProvider, } from './sync/sync-client.js';
30
34
  export { type SyncEvent, SyncManager, type SyncManagerParams, type SyncSessionInfo, type SyncStatus, } from './sync/sync-manager.js';
31
- export type { OnServerDIDObserved } from './hub/connection-pool.js';
32
- export { DIDObservingTransport, type DIDObservingTransportParams, type ServerDIDObserver, } from './hub/did-observing-transport.js';
33
- export { ReconnectingError } from './hub/errors.js';
34
- export type { GroupChannel, GroupChannelParams } from './hub/group-channel.js';
35
- export { type CreateHTTPHubClientParams, createHTTPHubClient } from './hub/http-client.js';
36
- export type { ConnectionLifecycleEvent } from './hub/hub-connection.js';
35
+ export { DIDObservingTransport, type DIDObservingTransportParams, type OnServerDIDObserved, type ServerDIDObserver, } from './hub/did-observing-transport.js';
36
+ export { type CreateHTTPHubClientParams, type CreateHubClient, type CreateHubClientOptions, createHTTPHubClient, } from './hub/http-client.js';
37
37
  export { type BindHubToGroupParams, bindHubToGroup, type CaptureServerDIDParams, type CreateHubParams, captureServerDID, createHub, type DeleteHubParams, type DeleteHubResult, deleteHub, type GetHubParams, getHub, type Hub, HubServerDIDConflictError, type ListGroupsByHubParams, type ListHubsByGroupParams, type ListHubsParams, listGroupsByHub, listHubs, listHubsByGroup, type UnbindHubFromGroupParams, type UpdateHubParams, type UpsertHubParams, unbindHubFromGroup, updateHub, upsertHub, } from './hub/manager.js';
38
- export { type CreateGroupChannel, type CreateHubClient, type CreateHubClientOptions, type CreateHubConnection, HubRelayManager, type HubRelayManagerParams, type RelayEvent, } from './hub/relay-manager.js';
39
38
  export { DISABLED_HUB, type HubWiring, type SetupHubRelayParams, setupHubRelay, } from './hub/wiring.js';
40
39
  export type { CatalogData, CircleData, CircleMemberData, DeleteCatalogData, DeleteCircleData, GroupData, GroupMemberData, GroupRequestContext, InviteToGroupData, JoinGroupData, LeaveGroupData, P2PContext, P2PJoinRequestContext, RemoveCircleMemberData, RemoveGroupMemberData, StoreUnreadableMode, SyncPluginAPI, SyncRequestContext, SyncSessionResult, UpdateCircleInput, UpdateGroupInput, } from './types.js';
41
40
  export type ReceiveConfig = {
@@ -57,7 +56,24 @@ export type ReceiveConfig = {
57
56
  export declare const DEFAULT_RECEIVE_CONFIG: ReceiveConfig;
58
57
  export type P2PPluginOptions = {
59
58
  autoAcceptPeers?: Array<string>;
60
- http?: boolean | string;
59
+ /**
60
+ * Expose the sync protocol over the HTTP plugin transport.
61
+ * - `true` / `{}` — enable at the default protocol path `'sync'`. CORS
62
+ * defaults to deny cross-origin browser requests.
63
+ * - `'name'` / `'/name'` — enable at the given protocol path.
64
+ * - `{ path?, allowedOrigin?, bodyLimit? }` — `path` is the protocol path
65
+ * (default `'sync'`); `allowedOrigin` opts into cross-origin (default deny).
66
+ * The sync server authenticates every request, so CORS is defense-in-depth
67
+ * rather than the security boundary; non-browser clients are unaffected.
68
+ * `bodyLimit` is the maximum request body size in bytes (default 1 MiB),
69
+ * enforced by the transport.
70
+ * - absent / `false` — HTTP sync transport disabled.
71
+ */
72
+ http?: boolean | string | {
73
+ path?: string;
74
+ allowedOrigin?: string | Array<string>;
75
+ bodyLimit?: number;
76
+ };
61
77
  /**
62
78
  * Enable hub-mediated group broadcasts.
63
79
  * - `true` / `{}` — build a default HTTP hub client factory from