@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,281 @@
1
- import{commitInvite as e,createGroup as r,createInvite as t,processWelcome as i,removeMember as a}from"@enkaku/group";import{HLC as o}from"@kubun/hlc";import{getGraphStore as s}from"@kubun/store-graph";import{getP2PStore as l}from"@kubun/store-p2p";import{bindHubToGroup as c,upsertHub as n}from"../hub/manager.js";export class GroupManager{#e;#r;#t;#i;constructor(e){this.#e=e.identity.id,this.#r=e.getRandomID,this.#t=new o({nodeID:e.identity.id}),this.#i=e.registry}async createGroup(e){let t=this.#r(),{group:i}=await r(e.identity,t),a=e.hubs??[];return await e.stores.withTransaction(async r=>{let s=await l(r);for(let l of(await s.createGroup({id:t,name:e.name,description:e.description??"",created_by:e.createdBy??e.identity.id,hlc:o.serialize(this.#t.now())}),await this.#i.seed({groupID:t,handle:i,stores:r}),await s.addGroupMember({group_id:t,member_did:e.identity.id,role:"admin",hlc:o.serialize(this.#t.now())}),a)){let e=await n({stores:r,url:l.url,serverDID:l.serverDID??null});await c({stores:r,hubID:e.id,groupID:t})}}),{groupID:t}}async updateGroup(e){let r=o.serialize(this.#t.now()),t=await l(e.stores);return await t.updateGroup(e.groupID,{name:e.update.name,description:e.update.description,hlc:r}),{broadcast:{type:"group:update",groupID:e.groupID,update:{...e.update,hlc:r}}}}async inviteToGroup(r){let i=await this.#i.withHandleReplacing(r.groupID,async i=>{let{invite:a}=await t({group:i,identity:r.identity,recipientDID:r.recipientDID,permission:r.permission}),{commitMessage:o,welcomeMessage:s,newGroup:l}=await e(i,r.recipientKeyPackage);return{result:{invite:a,commitMessage:o,welcomeMessage:s,ratchetTree:l.state.ratchetTree},updated:l}},{stores:r.stores}),a=await l(r.stores);return await a.addGroupMember({group_id:r.groupID,member_did:r.recipientDID,role:"admin"===r.permission?"admin":"member",hlc:o.serialize(this.#t.now())}),i}async joinGroup(e){let{group:r}=await i({identity:e.identity,invite:e.invite,welcome:e.welcomeMessage,keyPackageBundle:e.keyPackageBundle,ratchetTree:e.ratchetTree}),t=e.hubs??[];await e.stores.withTransaction(async i=>{await this.#i.seed({groupID:e.groupID,handle:r,stores:i});let a=await l(i);for(let r of(null==await a.getGroup(e.groupID)&&await a.createGroup({id:e.groupID,name:e.groupName,description:"",created_by:e.identity.id,hlc:o.serialize(this.#t.now())}),await a.addGroupMember({group_id:e.groupID,member_did:e.identity.id,role:"member",hlc:o.serialize(this.#t.now())}),t)){let t=await n({stores:i,url:r.url,serverDID:r.serverDID??null});await c({stores:i,hubID:t.id,groupID:e.groupID})}})}async removeMember(e){let{commitMessage:r}=await this.#i.withHandleReplacing(e.groupID,async r=>{let{commitMessage:t,newGroup:i}=await a(r,e.leafIndex);return{result:{commitMessage:t},updated:i}},{stores:e.stores}),t=await l(e.stores);return await t.removeGroupMember(e.groupID,e.memberDID,o.serialize(this.#t.now())),{commitMessage:r}}async removeGroupMember(e){let{commitMessage:r}=await this.#i.withHandleReplacing(e.groupID,async r=>{let t=r.findMemberLeafIndex(e.memberDID);if(null==t)throw Error(`Member ${e.memberDID} not found in MLS group`);let{commitMessage:i,newGroup:o}=await a(r,t);return{result:{commitMessage:i},updated:o}},{stores:e.stores}),t=await l(e.stores);return await t.removeGroupMember(e.groupID,e.memberDID,o.serialize(this.#t.now())),{commitMessage:r}}async leaveGroup(e){let r=await l(e.stores);await r.deleteMLSState(e.groupID,this.#e),await r.removeGroupMember(e.groupID,e.identity.id,o.serialize(this.#t.now())),e.stores.onCommit(()=>this.#i.invalidate(e.groupID))}async loadGroup(e,r){let t=await l(e),i=await t.getMLSState(r,this.#e);if(null!=i)return{epoch:i.epoch,credential:i.credential}}async createCircle(e){let r=this.#r(),t=o.serialize(this.#t.now()),i=e.catalogIDs??[],a=await l(e.stores);return await a.createCircle({id:r,group_id:e.groupID,name:e.name,description:e.description??"",catalog_ids:i,hlc:t}),{circleID:r,broadcast:{type:"circle:create",circle:{id:r,groupID:e.groupID,name:e.name,description:e.description??"",catalogIDs:i,hlc:t}}}}async updateCircle(e){let r=o.serialize(this.#t.now()),t=[],i=await l(e.stores);if(null!=e.update.catalogIDs){let r=await i.getCircle(e.circleID),a=new Set(null!=r?r.catalog_ids:[]),o=e.update.catalogIDs.filter(e=>!a.has(e)),l=await s(e.stores);for(let e of(await Promise.all(o.map(e=>l.getCatalog(e)))))null!=e&&t.push({type:"catalog:create",catalog:{id:e.id,ownerDID:e.owner_did,name:e.name,description:e.description,filterCriteria:e.filter_criteria,hlc:e.hlc}})}await i.updateCircle(e.circleID,{name:e.update.name,description:e.update.description,catalog_ids:e.update.catalogIDs,hlc:r});let a={type:"circle:update",circleID:e.circleID,update:{...e.update,hlc:r}};return{broadcast:a,broadcasts:[a,...t]}}async deleteCircle(e){let r=o.serialize(this.#t.now()),t=await l(e.stores);return await t.deleteCircle(e.circleID),{broadcast:{type:"circle:delete",circleID:e.circleID,hlc:r}}}async addCircleMember(e){let r=await l(e.stores);if(!await r.isGroupMember(e.groupID,e.memberDID))throw Error(`${e.memberDID} is not a member of group ${e.groupID}`);let t=o.serialize(this.#t.now());return await r.addCircleMember({circle_id:e.circleID,member_did:e.memberDID,role:e.role,hlc:t}),{broadcast:{type:"member:add",member:{circleID:e.circleID,memberDID:e.memberDID,role:e.role,hlc:t}}}}async removeCircleMember(e){let r=o.serialize(this.#t.now()),t=await l(e.stores);return await t.removeCircleMember(e.circleID,e.memberDID),{broadcast:{type:"member:remove",circleID:e.circleID,memberDID:e.memberDID,hlc:r}}}}
1
+ import { normalizeDID } from '@kokuin/token';
2
+ import { HLC } from '@kubun/hlc';
3
+ import { getP2PStore } from '@kubun/store-p2p';
4
+ import { commitInvite, createGroup, createInvite, processWelcome, removeMember } from '@kumiai/mls';
5
+ import { toB64 } from '@sozai/codec';
6
+ import { bindHubToGroup, upsertHub } from '../hub/manager.js';
7
+ import { reprojectAdminRoster } from './admin-roster.js';
8
+ import { reprojectGroupSettings } from './circle-projection.js';
9
+ import { GROUP_SETTINGS_ENTRY_TYPE } from './circle-reducers.js';
10
+ import { buildCurrentGroupAnchorExtension, groupAnchorCapabilities, readGroupAnchor } from './group-anchor.js';
11
+ import { ledgerEntryDigest, signLedgerEntry } from './ledger-entry.js';
12
+ export class GroupManager {
13
+ #deviceID;
14
+ #getRandomID;
15
+ #hlc;
16
+ #registry;
17
+ constructor(params){
18
+ this.#deviceID = params.identity.id;
19
+ this.#getRandomID = params.getRandomID;
20
+ this.#hlc = params.hlc;
21
+ this.#registry = params.registry;
22
+ }
23
+ async createGroup(params) {
24
+ const groupID = this.#getRandomID();
25
+ // The creator DID is baked into the MLS GroupContext as the immutable
26
+ // genesis anchor, alongside a fresh 32-byte recovery seed. Both survive
27
+ // every epoch and are authenticated by the GroupInfo signature, so every
28
+ // member derives the same recovery rendezvous. Every member leaf advertises
29
+ // the anchor extension capability so the group can be invited to.
30
+ const recoverySecret = toB64(crypto.getRandomValues(new Uint8Array(32)));
31
+ const { group } = await createGroup(params.identity, groupID, {
32
+ extensions: [
33
+ buildCurrentGroupAnchorExtension(normalizeDID(params.identity.id), recoverySecret)
34
+ ],
35
+ capabilities: groupAnchorCapabilities()
36
+ });
37
+ // Atomic register + bind: each `hubs` entry is upserted via `upsertHub`
38
+ // and bound to the group inside one transaction. A
39
+ // HubServerDIDConflictError on any entry rolls back the group row, the
40
+ // MLS state, the membership row, and any earlier hub bindings that were
41
+ // already added in this transaction.
42
+ const hubs = params.hubs ?? [];
43
+ await params.stores.withTransaction(async (tx)=>{
44
+ const store = await getP2PStore(tx);
45
+ await store.createGroup({
46
+ id: groupID,
47
+ name: params.name,
48
+ description: params.description ?? '',
49
+ created_by: params.createdBy ?? params.identity.id,
50
+ hlc: HLC.serialize(this.#hlc.now())
51
+ });
52
+ await this.#registry.seed({
53
+ groupID,
54
+ handle: group,
55
+ stores: tx
56
+ });
57
+ // The membership row must exist before projection (setGroupMemberRole is
58
+ // update-only); the role value here is irrelevant — projecting the genesis
59
+ // anchor below overwrites it. The ledger fold is the only writer of role.
60
+ await store.addGroupMember({
61
+ group_id: groupID,
62
+ member_did: params.identity.id,
63
+ role: 'member',
64
+ hlc: HLC.serialize(this.#hlc.now())
65
+ });
66
+ // Derive the creator's admin role from the genesis anchor: folding the
67
+ // (empty) ledger to its seed {creator} projects the creator to 'admin'.
68
+ const anchor = readGroupAnchor(group);
69
+ if (anchor != null) {
70
+ await reprojectAdminRoster({
71
+ store,
72
+ groupID,
73
+ anchor
74
+ });
75
+ }
76
+ // Seed the group name/description into the ledger: the direct
77
+ // `store.createGroup` row write does not propagate, so the ledger is the
78
+ // only channel that carries settings to joiners and survives a reproject.
79
+ // No broadcast at creation (no co-members yet) — the entry rides catch-up
80
+ // when peers join. Reprojecting from the seed yields the same name/desc the
81
+ // group row already holds and sets the settings watermark.
82
+ const settingsHLC = HLC.serialize(this.#hlc.now());
83
+ const settingsValue = {
84
+ name: params.name,
85
+ description: params.description ?? ''
86
+ };
87
+ const settingsToken = await signLedgerEntry(params.identity, {
88
+ type: GROUP_SETTINGS_ENTRY_TYPE,
89
+ subject: groupID,
90
+ value: settingsValue,
91
+ hlc: settingsHLC
92
+ });
93
+ await store.appendLedgerEntry({
94
+ group_id: groupID,
95
+ entry_id: ledgerEntryDigest(settingsToken),
96
+ type: GROUP_SETTINGS_ENTRY_TYPE,
97
+ subject: groupID,
98
+ value: settingsValue,
99
+ hlc: settingsHLC,
100
+ issuer_did: params.identity.id,
101
+ signed_token: settingsToken
102
+ });
103
+ if (anchor != null) {
104
+ await reprojectGroupSettings({
105
+ store,
106
+ groupID,
107
+ anchor
108
+ });
109
+ }
110
+ for (const hub of hubs){
111
+ const upserted = await upsertHub({
112
+ stores: tx,
113
+ url: hub.url,
114
+ serverDID: hub.serverDID ?? null
115
+ });
116
+ await bindHubToGroup({
117
+ stores: tx,
118
+ hubID: upserted.id,
119
+ groupID
120
+ });
121
+ }
122
+ });
123
+ return {
124
+ groupID
125
+ };
126
+ }
127
+ async inviteToGroup(params) {
128
+ const opResult = await this.#registry.withHandleReplacing(params.groupID, async (handle)=>{
129
+ const { invite } = await createInvite({
130
+ group: handle,
131
+ identity: params.identity,
132
+ recipientDID: params.recipientDID,
133
+ permission: params.permission
134
+ });
135
+ const { commitMessage, welcomeMessage, newGroup } = await commitInvite(handle, params.recipientKeyPackage);
136
+ return {
137
+ result: {
138
+ invite,
139
+ commitMessage,
140
+ welcomeMessage,
141
+ ratchetTree: newGroup.state.ratchetTree
142
+ },
143
+ updated: newGroup
144
+ };
145
+ }, {
146
+ stores: params.stores
147
+ });
148
+ const store = await getP2PStore(params.stores);
149
+ await store.addGroupMember({
150
+ group_id: params.groupID,
151
+ member_did: params.recipientDID,
152
+ role: params.permission === 'admin' ? 'admin' : 'member',
153
+ hlc: HLC.serialize(this.#hlc.now())
154
+ });
155
+ return opResult;
156
+ }
157
+ async joinGroup(params) {
158
+ // Process the welcome message using the invitee's key package bundle
159
+ const { group } = await processWelcome({
160
+ identity: params.identity,
161
+ invite: params.invite,
162
+ welcome: params.welcomeMessage,
163
+ keyPackageBundle: params.keyPackageBundle,
164
+ ratchetTree: params.ratchetTree
165
+ });
166
+ // Atomic register + bind: hub entries imported from the invite are
167
+ // upserted and bound to the group inside one transaction. A
168
+ // HubServerDIDConflictError on any entry rolls back MLS state, the
169
+ // (provisional) group row, the membership row, and any earlier hub
170
+ // bindings.
171
+ const hubs = params.hubs ?? [];
172
+ await params.stores.withTransaction(async (tx)=>{
173
+ await this.#registry.seed({
174
+ groupID: params.groupID,
175
+ handle: group,
176
+ stores: tx
177
+ });
178
+ // Provision the local Group row if it does not already exist. Required
179
+ // so downstream group-aware components (e.g. HubRelayManager) can read
180
+ // group metadata from the joining peer's DB.
181
+ const store = await getP2PStore(tx);
182
+ const existing = await store.getGroup(params.groupID);
183
+ if (existing == null) {
184
+ await store.createGroup({
185
+ id: params.groupID,
186
+ name: params.groupName,
187
+ description: '',
188
+ created_by: params.identity.id,
189
+ hlc: HLC.serialize(this.#hlc.now())
190
+ });
191
+ }
192
+ // Seed the full MLS roster (the store roster is the projection of the
193
+ // ratchet tree) so the admin gate can confirm membership of the
194
+ // creator/existing admins on this peer. Roles default to 'member';
195
+ // admin authority is derived from the ledger fold and the role column is
196
+ // corrected when ledger entries arrive.
197
+ const localHLC = HLC.serialize(this.#hlc.now());
198
+ await store.addGroupMembers(group.listMembers().map((member)=>({
199
+ group_id: params.groupID,
200
+ member_did: member.id,
201
+ role: 'member',
202
+ hlc: localHLC
203
+ })));
204
+ for (const hub of hubs){
205
+ const upserted = await upsertHub({
206
+ stores: tx,
207
+ url: hub.url,
208
+ serverDID: hub.serverDID ?? null
209
+ });
210
+ await bindHubToGroup({
211
+ stores: tx,
212
+ hubID: upserted.id,
213
+ groupID: params.groupID
214
+ });
215
+ }
216
+ });
217
+ return undefined;
218
+ }
219
+ async removeMember(params) {
220
+ const { commitMessage } = await this.#registry.withHandleReplacing(params.groupID, async (handle)=>{
221
+ const { commitMessage, newGroup } = await removeMember(handle, params.leafIndex);
222
+ return {
223
+ result: {
224
+ commitMessage
225
+ },
226
+ updated: newGroup
227
+ };
228
+ }, {
229
+ stores: params.stores
230
+ });
231
+ const store = await getP2PStore(params.stores);
232
+ await store.removeGroupMember(params.groupID, params.memberDID, HLC.serialize(this.#hlc.now()));
233
+ return {
234
+ commitMessage
235
+ };
236
+ }
237
+ async removeGroupMember(params) {
238
+ const { commitMessage } = await this.#registry.withHandleReplacing(params.groupID, async (handle)=>{
239
+ const leafIndex = handle.findMemberLeafIndex(params.memberDID);
240
+ if (leafIndex == null) {
241
+ throw new Error(`Member ${params.memberDID} not found in MLS group`);
242
+ }
243
+ const { commitMessage, newGroup } = await removeMember(handle, leafIndex);
244
+ return {
245
+ result: {
246
+ commitMessage
247
+ },
248
+ updated: newGroup
249
+ };
250
+ }, {
251
+ stores: params.stores
252
+ });
253
+ const store = await getP2PStore(params.stores);
254
+ await store.removeGroupMember(params.groupID, params.memberDID, params.hlc);
255
+ return {
256
+ commitMessage
257
+ };
258
+ }
259
+ async leaveGroup(params) {
260
+ // MLS does not allow a member to commit their own removal.
261
+ // leaveGroup performs local cleanup: deletes MLS state and group membership.
262
+ // The actual MLS group removal happens when another member calls removeGroupMember.
263
+ const store = await getP2PStore(params.stores);
264
+ await store.deleteMLSState(params.groupID, this.#deviceID);
265
+ await store.removeGroupMember(params.groupID, params.identity.id, params.hlc);
266
+ // Defer cache eviction to onCommit so a future caller wrapping these
267
+ // deletes in withTransaction does not invalidate the cache when the row
268
+ // delete rolls back. KubunDB's non-tx onCommit fires synchronously, so
269
+ // current callers see invalidate immediately.
270
+ params.stores.onCommit(()=>this.#registry.invalidate(params.groupID));
271
+ }
272
+ async loadGroup(stores, groupID) {
273
+ const store = await getP2PStore(stores);
274
+ const state = await store.getMLSState(groupID, this.#deviceID);
275
+ if (state == null) return undefined;
276
+ return {
277
+ epoch: state.epoch,
278
+ credential: state.credential
279
+ };
280
+ }
281
+ }
@@ -1,4 +1,4 @@
1
- import type { GroupHandle } from '@enkaku/group';
1
+ import type { GroupHandle } from '@kumiai/mls';
2
2
  export type MLSEncryptFramedResult = {
3
3
  framed: Uint8Array;
4
4
  consumed: Array<Uint8Array>;
@@ -1 +1,18 @@
1
- import{replacer as e,reviver as r}from"./mls-json.js";let t=new TextEncoder,n=new TextDecoder;export async function mlsEncryptFramed(r,n){let{message:c,consumed:o}=await r.encrypt(n);return{framed:t.encode(JSON.stringify(c,e)),consumed:o}}export async function mlsDecryptFramed(e,t){let c=JSON.parse(n.decode(t),r);return await e.decrypt(c)}
1
+ import { anchorImmutabilityPolicy } from './group-anchor.js';
2
+ import { replacer, reviver } from './mls-json.js';
3
+ const textEncoder = new TextEncoder();
4
+ const textDecoder = new TextDecoder();
5
+ export async function mlsEncryptFramed(handle, plaintext) {
6
+ const { message, consumed } = await handle.encrypt(plaintext);
7
+ const framed = textEncoder.encode(JSON.stringify(message, replacer));
8
+ return {
9
+ framed,
10
+ consumed
11
+ };
12
+ }
13
+ export async function mlsDecryptFramed(handle, framed) {
14
+ const message = JSON.parse(textDecoder.decode(framed), reviver);
15
+ return await handle.decrypt(message, {
16
+ commitPolicy: anchorImmutabilityPolicy
17
+ });
18
+ }
@@ -1,11 +1,11 @@
1
- import type { Encryptor } from '@enkaku/hub-tunnel';
1
+ import type { Encryptor } from '@kumiai/hub-tunnel';
2
2
  import type { GroupHandleRegistry } from './group-handle-registry.js';
3
3
  export type MLSEncryptorParams = {
4
4
  registry: GroupHandleRegistry;
5
5
  groupID: string;
6
6
  };
7
7
  /**
8
- * Adapts the `GroupHandleRegistry` to the `@enkaku/hub-tunnel` `Encryptor`
8
+ * Adapts the `GroupHandleRegistry` to the `@kumiai/hub-tunnel` `Encryptor`
9
9
  * interface. The registry's per-group mutex serializes encrypt + decrypt
10
10
  * across all encryptor instances bound to the same `(registry, groupID)`.
11
11
  *
@@ -1 +1,30 @@
1
- import{mlsDecryptFramed as r,mlsEncryptFramed as t}from"./mls-codec.js";export class MLSEncryptor{#r;#t;constructor(r){this.#r=r.registry,this.#t=r.groupID}encrypt(r){return this.#r.withHandle(this.#t,async s=>{let{framed:e}=await t(s,r);return e})}decrypt(t){return this.#r.withHandle(this.#t,s=>r(s,t))}}
1
+ import { mlsDecryptFramed, mlsEncryptFramed } from './mls-codec.js';
2
+ /**
3
+ * Adapts the `GroupHandleRegistry` to the `@kumiai/hub-tunnel` `Encryptor`
4
+ * interface. The registry's per-group mutex serializes encrypt + decrypt
5
+ * across all encryptor instances bound to the same `(registry, groupID)`.
6
+ *
7
+ * Wedge-protection note: encrypt persists the advanced ratchet immediately
8
+ * on callback success — BEFORE the wire send happens inside the hub-tunnel
9
+ * transport. If the wire send fails, the persisted state is already at
10
+ * generation N+1; the next encrypt advances to N+2 and generation N is
11
+ * "burned" without wire egress. This is the documented forward-secrecy
12
+ * trade-off for tunnel sessions; the broadcast path (`BroadcastService.
13
+ * prepareSend`) inverts the order by pulling send into the registry callback.
14
+ */ export class MLSEncryptor {
15
+ #registry;
16
+ #groupID;
17
+ constructor(params){
18
+ this.#registry = params.registry;
19
+ this.#groupID = params.groupID;
20
+ }
21
+ encrypt(plaintext) {
22
+ return this.#registry.withHandle(this.#groupID, async (handle)=>{
23
+ const { framed } = await mlsEncryptFramed(handle, plaintext);
24
+ return framed;
25
+ });
26
+ }
27
+ decrypt(ciphertext) {
28
+ return this.#registry.withHandle(this.#groupID, (handle)=>mlsDecryptFramed(handle, ciphertext));
29
+ }
30
+ }
@@ -1,4 +1,4 @@
1
- import { type GroupHandle } from '@enkaku/group';
1
+ import { type GroupHandle } from '@kumiai/mls';
2
2
  import { type SerializedMLSGroupState } from './mls-state.js';
3
3
  export type MLSGroupHandle = GroupHandle;
4
4
  export declare function restoreMLSGroupHandle(serialized: SerializedMLSGroupState): Promise<MLSGroupHandle>;
@@ -1 +1,10 @@
1
- import{restoreGroup as r}from"@enkaku/group";import{deserializeMLSGroupState as o}from"./mls-state.js";export async function restoreMLSGroupHandle(t){let{state:e,credential:n,rootCapability:m}=o(t);return r({state:e,credential:n,rootCapability:m})}
1
+ import { restoreGroup } from '@kumiai/mls';
2
+ import { deserializeMLSGroupState } from './mls-state.js';
3
+ export async function restoreMLSGroupHandle(serialized) {
4
+ const { state, credential, rootCapability } = deserializeMLSGroupState(serialized);
5
+ return restoreGroup({
6
+ state,
7
+ credential,
8
+ rootCapability
9
+ });
10
+ }
@@ -1 +1,17 @@
1
- import{fromB64 as t,toB64 as e}from"@enkaku/codec";let r="\0bi:",i="\0u8:";export function replacer(t,n){if("bigint"==typeof n)return`${r}${n}`;let o=this[t];return o instanceof Uint8Array?`${i}${e(o)}`:n}export function reviver(e,n){return"string"!=typeof n?n:n.startsWith(r)?BigInt(n.slice(r.length)):n.startsWith(i)?t(n.slice(i.length)):n}
1
+ import { fromB64, toB64 } from '@sozai/codec';
2
+ const BIGINT_PREFIX = '\x00bi:';
3
+ const BYTES_PREFIX = '\x00u8:';
4
+ export function replacer(_key, value) {
5
+ if (typeof value === 'bigint') return `${BIGINT_PREFIX}${value}`;
6
+ const raw = this[_key];
7
+ if (raw instanceof Uint8Array) {
8
+ return `${BYTES_PREFIX}${toB64(raw)}`;
9
+ }
10
+ return value;
11
+ }
12
+ export function reviver(_key, value) {
13
+ if (typeof value !== 'string') return value;
14
+ if (value.startsWith(BIGINT_PREFIX)) return BigInt(value.slice(BIGINT_PREFIX.length));
15
+ if (value.startsWith(BYTES_PREFIX)) return fromB64(value.slice(BYTES_PREFIX.length));
16
+ return value;
17
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Errors raised while applying a received MLS handshake Commit to the local
3
+ * group state. Kept in a standalone, transport-independent module so the
4
+ * `GroupMLS` adapter and any other surviving receive path can import them
5
+ * without depending on a specific transport implementation.
6
+ */
7
+ /**
8
+ * Thrown when a received Commit fails the authenticated MLS check at the
9
+ * matching epoch (a corrupt or forged Commit). Unrecoverable: redelivery will
10
+ * not help, so callers should ack-and-skip rather than wedge the group's
11
+ * ordered receive stream.
12
+ */
13
+ export declare class DecryptError extends Error {
14
+ #private;
15
+ constructor(message: string, cause: unknown);
16
+ get cause(): unknown;
17
+ }
18
+ /**
19
+ * Thrown when a received MLS Commit targets a future epoch the local handle has
20
+ * not reached yet (a gap in the in-order Commit stream). Callers should NOT ack
21
+ * — the hub redelivers in order, so the gap-filling Commit arrives first and
22
+ * this one applies on a later delivery.
23
+ */
24
+ export declare class CommitOutOfOrderError extends Error {
25
+ #private;
26
+ constructor(messageEpoch: bigint, handleEpoch: bigint);
27
+ get messageEpoch(): bigint;
28
+ get handleEpoch(): bigint;
29
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Errors raised while applying a received MLS handshake Commit to the local
3
+ * group state. Kept in a standalone, transport-independent module so the
4
+ * `GroupMLS` adapter and any other surviving receive path can import them
5
+ * without depending on a specific transport implementation.
6
+ */ /**
7
+ * Thrown when a received Commit fails the authenticated MLS check at the
8
+ * matching epoch (a corrupt or forged Commit). Unrecoverable: redelivery will
9
+ * not help, so callers should ack-and-skip rather than wedge the group's
10
+ * ordered receive stream.
11
+ */ export class DecryptError extends Error {
12
+ #cause;
13
+ constructor(message, cause){
14
+ super(message);
15
+ this.name = 'DecryptError';
16
+ this.#cause = cause;
17
+ }
18
+ get cause() {
19
+ return this.#cause;
20
+ }
21
+ }
22
+ /**
23
+ * Thrown when a received MLS Commit targets a future epoch the local handle has
24
+ * not reached yet (a gap in the in-order Commit stream). Callers should NOT ack
25
+ * — the hub redelivers in order, so the gap-filling Commit arrives first and
26
+ * this one applies on a later delivery.
27
+ */ export class CommitOutOfOrderError extends Error {
28
+ #messageEpoch;
29
+ #handleEpoch;
30
+ constructor(messageEpoch, handleEpoch){
31
+ super(`MLS commit epoch ${messageEpoch} ahead of handle epoch ${handleEpoch}`);
32
+ this.name = 'CommitOutOfOrderError';
33
+ this.#messageEpoch = messageEpoch;
34
+ this.#handleEpoch = handleEpoch;
35
+ }
36
+ get messageEpoch() {
37
+ return this.#messageEpoch;
38
+ }
39
+ get handleEpoch() {
40
+ return this.#handleEpoch;
41
+ }
42
+ }
@@ -1,5 +1,5 @@
1
- import { type ClientState, type GroupHandle, type MemberCredential } from '@enkaku/group';
2
1
  import type { GroupMLSState, InsertGroupMLSState } from '@kubun/store-p2p';
2
+ import { type ClientState, type GroupHandle, type MemberCredential } from '@kumiai/mls';
3
3
  /**
4
4
  * Serialized form of a GroupHandle for DB persistence.
5
5
  * The MLS ClientState is opaque binary; the credential is JSON.
@@ -1 +1,56 @@
1
- import{decodeClientState as t,encodeClientState as e}from"@enkaku/group";import{normalizeCredential as o}from"../context/types.js";export function serializeMLSGroupState(t){return{mlsState:e(t.state),credential:JSON.stringify(t.credential),epoch:Number(t.epoch),rootCapability:t.rootCapability}}export function deserializeMLSGroupState(e){let o=t(e.mlsState);if(null==o)throw Error("Could not decode ClientState");return{state:o,credential:JSON.parse(e.credential),rootCapability:e.rootCapability}}export function fromMLSStateRow(t){return{mlsState:t.mls_state,credential:o(t.credential),epoch:t.epoch,rootCapability:t.root_capability}}export function toMLSStateInsert(t,e,o){return{group_id:e,device_id:o,mls_state:t.mlsState,credential:t.credential,epoch:t.epoch,root_capability:t.rootCapability}}
1
+ import { decodeClientState, encodeClientState } from '@kumiai/mls';
2
+ import { normalizeCredential } from '../context/types.js';
3
+ /**
4
+ * Serialize a GroupHandle's state for persistence.
5
+ * The implementer must verify the serialization approach works
6
+ * with the installed version of ts-mls.
7
+ */ export function serializeMLSGroupState(group) {
8
+ return {
9
+ mlsState: encodeClientState(group.state),
10
+ credential: JSON.stringify(group.credential),
11
+ epoch: Number(group.epoch),
12
+ rootCapability: group.rootCapability
13
+ };
14
+ }
15
+ /**
16
+ * Deserialize persisted state back into GroupHandle constructor params.
17
+ * Returns the params needed to reconstruct a GroupHandle.
18
+ */ export function deserializeMLSGroupState(serialized) {
19
+ const state = decodeClientState(serialized.mlsState);
20
+ if (state == null) {
21
+ throw new Error('Could not decode ClientState');
22
+ }
23
+ return {
24
+ state,
25
+ credential: JSON.parse(serialized.credential),
26
+ rootCapability: serialized.rootCapability
27
+ };
28
+ }
29
+ /**
30
+ * Map a persisted `GroupMLSState` DB row to the `SerializedMLSGroupState` shape
31
+ * expected by `BroadcastService.prepareSend` / `processReceived`.
32
+ *
33
+ * Credential is stored as TEXT but Kysely's `ParseJSONResultsPlugin` may have
34
+ * already parsed it into an object on read. `SerializedMLSGroupState.credential`
35
+ * is a JSON string, so re-stringify when needed.
36
+ */ export function fromMLSStateRow(row) {
37
+ return {
38
+ mlsState: row.mls_state,
39
+ credential: normalizeCredential(row.credential),
40
+ epoch: row.epoch,
41
+ rootCapability: row.root_capability
42
+ };
43
+ }
44
+ /**
45
+ * Map a `SerializedMLSGroupState` plus scope (group + device) to an
46
+ * `InsertGroupMLSState` row suitable for `P2PStoreAPI.saveMLSState`.
47
+ */ export function toMLSStateInsert(serialized, groupID, deviceID) {
48
+ return {
49
+ group_id: groupID,
50
+ device_id: deviceID,
51
+ mls_state: serialized.mlsState,
52
+ credential: serialized.credential,
53
+ epoch: serialized.epoch,
54
+ root_capability: serialized.rootCapability
55
+ };
56
+ }
@@ -1,8 +1,11 @@
1
1
  import type { Logger } from '@kubun/logger';
2
+ import type { DelegationStoreAPI } from '@kubun/store-delegation';
2
3
  import type { P2PStoreAPI } from '@kubun/store-p2p';
3
4
  import type { P2PEventEmitter } from './events.js';
4
5
  export type StoreReceivedGrantParams = {
5
- /** P2P store the held row is written to. */
6
+ /** Delegation store the held token row and revocation cross-check are written to. */
7
+ delegationStore: DelegationStoreAPI;
8
+ /** P2P store the group-delegation edge (`jti` → group) is written to. */
6
9
  p2pStore: P2PStoreAPI;
7
10
  /** Stringified capability JWT carrying the `document/write` grant. */
8
11
  token: string;