@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.
- package/lib/context/delegation.js +130 -1
- package/lib/context/group.js +1319 -1
- package/lib/context/hub.js +150 -1
- package/lib/context/join.js +80 -1
- package/lib/context/sync.js +19 -1
- package/lib/context/types.d.ts +3 -3
- package/lib/context/types.js +28 -1
- package/lib/groups/admin-roster.d.ts +130 -0
- package/lib/groups/admin-roster.js +184 -0
- package/lib/groups/broadcast-codec.js +6 -1
- package/lib/groups/broadcast.d.ts +105 -91
- package/lib/groups/broadcast.js +627 -1
- package/lib/groups/circle-projection.d.ts +122 -0
- package/lib/groups/circle-projection.js +288 -0
- package/lib/groups/circle-reducers.d.ts +178 -0
- package/lib/groups/circle-reducers.js +198 -0
- package/lib/groups/events.d.ts +5 -1
- package/lib/groups/events.js +10 -1
- package/lib/groups/group-anchor.d.ts +63 -0
- package/lib/groups/group-anchor.js +107 -0
- package/lib/groups/group-crypto.d.ts +19 -0
- package/lib/groups/group-crypto.js +85 -0
- package/lib/groups/group-handle-registry.d.ts +61 -2
- package/lib/groups/group-handle-registry.js +243 -1
- package/lib/groups/group-handlers.d.ts +29 -0
- package/lib/groups/group-handlers.js +92 -0
- package/lib/groups/group-health-monitor.d.ts +11 -0
- package/lib/groups/group-health-monitor.js +139 -1
- package/lib/groups/group-mls.d.ts +35 -0
- package/lib/groups/group-mls.js +164 -0
- package/lib/groups/group-peer-manager.d.ts +108 -0
- package/lib/groups/group-peer-manager.js +344 -0
- package/lib/groups/group-protocols.d.ts +297 -0
- package/lib/groups/group-protocols.js +174 -0
- package/lib/groups/invite-payload.d.ts +1 -1
- package/lib/groups/invite-payload.js +26 -1
- package/lib/groups/join-utils.d.ts +1 -0
- package/lib/groups/join-utils.js +104 -1
- package/lib/groups/ledger-affected-events.d.ts +15 -0
- package/lib/groups/ledger-affected-events.js +96 -0
- package/lib/groups/ledger-entry.d.ts +37 -0
- package/lib/groups/ledger-entry.js +56 -0
- package/lib/groups/ledger-fold.d.ts +56 -0
- package/lib/groups/ledger-fold.js +53 -0
- package/lib/groups/manager.d.ts +20 -71
- package/lib/groups/manager.js +281 -1
- package/lib/groups/mls-codec.d.ts +1 -1
- package/lib/groups/mls-codec.js +18 -1
- package/lib/groups/mls-encryptor.d.ts +2 -2
- package/lib/groups/mls-encryptor.js +30 -1
- package/lib/groups/mls-group-handle.d.ts +1 -1
- package/lib/groups/mls-group-handle.js +10 -1
- package/lib/groups/mls-json.js +17 -1
- package/lib/groups/mls-receive-errors.d.ts +29 -0
- package/lib/groups/mls-receive-errors.js +42 -0
- package/lib/groups/mls-state.d.ts +1 -1
- package/lib/groups/mls-state.js +56 -1
- package/lib/groups/store-received-grant.d.ts +4 -1
- package/lib/groups/store-received-grant.js +116 -1
- package/lib/groups/store-received-revocation.d.ts +3 -3
- package/lib/groups/store-received-revocation.js +82 -1
- package/lib/hub/ack-backoff.d.ts +22 -0
- package/lib/hub/ack-backoff.js +88 -0
- package/lib/hub/did-observing-transport.d.ts +13 -2
- package/lib/hub/did-observing-transport.js +126 -1
- package/lib/hub/errors.d.ts +0 -6
- package/lib/hub/errors.js +33 -1
- package/lib/hub/http-client.d.ts +28 -3
- package/lib/hub/http-client.js +32 -1
- package/lib/hub/hub-like.d.ts +24 -0
- package/lib/hub/hub-like.js +535 -0
- package/lib/hub/manager.js +198 -1
- package/lib/hub/wiring.d.ts +17 -48
- package/lib/hub/wiring.js +132 -1
- package/lib/index.d.ts +17 -16
- package/lib/index.js +392 -1
- package/lib/protocol.d.ts +1 -1
- package/lib/protocol.js +251 -1
- package/lib/schema.d.ts +2 -1
- package/lib/schema.js +439 -2
- package/lib/sync/authorize.d.ts +1 -1
- package/lib/sync/authorize.js +73 -1
- package/lib/sync/broadcast-queue.js +183 -1
- package/lib/sync/broadcast-sender.js +179 -1
- package/lib/sync/catalog-scope.js +68 -1
- package/lib/sync/errors.d.ts +3 -2
- package/lib/sync/errors.js +19 -1
- package/lib/sync/forwarder.js +277 -1
- package/lib/sync/handlers.js +160 -1
- package/lib/sync/hub-tunnel-sync-listener.d.ts +8 -4
- package/lib/sync/hub-tunnel-sync-listener.js +172 -1
- package/lib/sync/hub-tunnel-sync-provider.d.ts +14 -14
- package/lib/sync/hub-tunnel-sync-provider.js +64 -1
- package/lib/sync/merkle-apply.d.ts +5 -0
- package/lib/sync/merkle-apply.js +194 -1
- package/lib/sync/merkle-channel.d.ts +2 -2
- package/lib/sync/merkle-channel.js +182 -1
- package/lib/sync/merkle-tree.js +120 -1
- package/lib/sync/peer-registry.d.ts +1 -1
- package/lib/sync/peer-registry.js +107 -1
- package/lib/sync/receive-access-gate.js +24 -1
- package/lib/sync/scope-resolver.js +41 -1
- package/lib/sync/sync-client.d.ts +3 -3
- package/lib/sync/sync-client.js +192 -1
- package/lib/sync/sync-manager.d.ts +2 -2
- package/lib/sync/sync-manager.js +272 -1
- package/lib/types.d.ts +11 -0
- package/lib/types.js +2 -1
- package/lib/util/mutex.js +10 -1
- package/lib/utils.js +21 -1
- package/package.json +47 -40
- package/lib/groups/broadcast-service.d.ts +0 -263
- package/lib/groups/broadcast-service.js +0 -1
- package/lib/groups/rejoin-codec.d.ts +0 -14
- package/lib/groups/rejoin-codec.js +0 -1
- package/lib/groups/wire-frame.d.ts +0 -34
- package/lib/groups/wire-frame.js +0 -1
- package/lib/hub/circle-catchup-requester.d.ts +0 -58
- package/lib/hub/circle-catchup-requester.js +0 -1
- package/lib/hub/circle-catchup-responder.d.ts +0 -50
- package/lib/hub/circle-catchup-responder.js +0 -1
- package/lib/hub/connection-pool.d.ts +0 -43
- package/lib/hub/connection-pool.js +0 -1
- package/lib/hub/epoch-stale-detector.d.ts +0 -18
- package/lib/hub/epoch-stale-detector.js +0 -1
- package/lib/hub/forward-remote-broadcast.d.ts +0 -15
- package/lib/hub/forward-remote-broadcast.js +0 -1
- package/lib/hub/group-channel.d.ts +0 -89
- package/lib/hub/group-channel.js +0 -1
- package/lib/hub/hub-connection.d.ts +0 -96
- package/lib/hub/hub-connection.js +0 -1
- package/lib/hub/receive-handler.d.ts +0 -51
- package/lib/hub/receive-handler.js +0 -1
- package/lib/hub/rejoin-manager.d.ts +0 -78
- package/lib/hub/rejoin-manager.js +0 -1
- package/lib/hub/rejoin-responder.d.ts +0 -32
- package/lib/hub/rejoin-responder.js +0 -1
- package/lib/hub/relay-manager.d.ts +0 -142
- package/lib/hub/relay-manager.js +0 -1
- package/lib/hub/send-handler.d.ts +0 -40
- package/lib/hub/send-handler.js +0 -1
- package/lib/hub/tunnel-inbox.d.ts +0 -20
- package/lib/hub/tunnel-inbox.js +0 -1
- package/lib/hub/wait-for-gate.d.ts +0 -14
- package/lib/hub/wait-for-gate.js +0 -1
package/lib/groups/manager.js
CHANGED
|
@@ -1 +1,281 @@
|
|
|
1
|
-
import
|
|
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
|
+
}
|
package/lib/groups/mls-codec.js
CHANGED
|
@@ -1 +1,18 @@
|
|
|
1
|
-
import{
|
|
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 '@
|
|
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 `@
|
|
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
|
|
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 '@
|
|
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
|
|
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
|
+
}
|
package/lib/groups/mls-json.js
CHANGED
|
@@ -1 +1,17 @@
|
|
|
1
|
-
import{fromB64
|
|
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.
|
package/lib/groups/mls-state.js
CHANGED
|
@@ -1 +1,56 @@
|
|
|
1
|
-
import{decodeClientState
|
|
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
|
-
/**
|
|
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;
|