@kubun/plugin-p2p 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/context/group.js +1073 -746
- package/lib/context/hub.js +2 -1
- package/lib/context/join.js +50 -15
- package/lib/context/peer.d.ts +92 -0
- package/lib/context/peer.js +1216 -0
- package/lib/context/require-admin.d.ts +57 -0
- package/lib/context/require-admin.js +57 -0
- package/lib/context/sync.d.ts +12 -0
- package/lib/context/sync.js +234 -1
- package/lib/context/types.d.ts +56 -20
- package/lib/context/types.js +23 -3
- package/lib/groups/access-default-token.d.ts +44 -0
- package/lib/groups/access-default-token.js +103 -0
- package/lib/groups/admin-roster.d.ts +42 -117
- package/lib/groups/admin-roster.js +53 -168
- package/lib/groups/anchor-store.d.ts +15 -0
- package/lib/groups/anchor-store.js +37 -0
- package/lib/groups/app-cursor-store.d.ts +17 -0
- package/lib/groups/app-cursor-store.js +34 -0
- package/lib/groups/broadcast.d.ts +199 -81
- package/lib/groups/broadcast.js +443 -368
- package/lib/groups/catalog-token.d.ts +50 -0
- package/lib/groups/catalog-token.js +96 -0
- package/lib/groups/circle-projection.d.ts +32 -64
- package/lib/groups/circle-projection.js +95 -181
- package/lib/groups/circle-reducers.d.ts +71 -110
- package/lib/groups/circle-reducers.js +57 -127
- package/lib/groups/commit-adoption.d.ts +77 -0
- package/lib/groups/commit-adoption.js +108 -0
- package/lib/groups/commit-journal.d.ts +14 -0
- package/lib/groups/commit-journal.js +65 -0
- package/lib/groups/control-request.d.ts +66 -0
- package/lib/groups/control-request.js +101 -0
- package/lib/groups/events.d.ts +14 -1
- package/lib/groups/group-anchor.d.ts +18 -57
- package/lib/groups/group-anchor.js +15 -100
- package/lib/groups/group-crypto.d.ts +36 -13
- package/lib/groups/group-crypto.js +124 -50
- package/lib/groups/group-handle-registry.d.ts +106 -36
- package/lib/groups/group-handle-registry.js +351 -61
- package/lib/groups/group-handlers.d.ts +5 -4
- package/lib/groups/group-handlers.js +155 -29
- package/lib/groups/group-health-monitor.d.ts +1 -1
- package/lib/groups/group-health-monitor.js +1 -1
- package/lib/groups/group-mls.d.ts +35 -4
- package/lib/groups/group-mls.js +594 -62
- package/lib/groups/group-peer-manager.d.ts +212 -19
- package/lib/groups/group-peer-manager.js +1061 -69
- package/lib/groups/group-protocols.d.ts +485 -28
- package/lib/groups/group-protocols.js +289 -15
- package/lib/groups/invite-payload.d.ts +48 -0
- package/lib/groups/join-request-identity.d.ts +21 -0
- package/lib/groups/join-request-identity.js +43 -0
- package/lib/groups/join-utils.d.ts +60 -0
- package/lib/groups/join-utils.js +262 -0
- package/lib/groups/ledger-affected-events.d.ts +11 -6
- package/lib/groups/ledger-affected-events.js +15 -26
- package/lib/groups/ledger-commit-fold.d.ts +107 -0
- package/lib/groups/ledger-commit-fold.js +167 -0
- package/lib/groups/ledger-ingest.d.ts +124 -0
- package/lib/groups/ledger-ingest.js +212 -0
- package/lib/groups/ledger.d.ts +44 -0
- package/lib/groups/ledger.js +30 -0
- package/lib/groups/manager.d.ts +202 -19
- package/lib/groups/manager.js +623 -106
- package/lib/groups/mls-codec.d.ts +27 -6
- package/lib/groups/mls-codec.js +30 -15
- package/lib/groups/mls-encryptor.js +9 -6
- package/lib/groups/mls-group-handle.d.ts +37 -2
- package/lib/groups/mls-group-handle.js +12 -4
- package/lib/groups/mls-receive-errors.d.ts +0 -2
- package/lib/groups/mls-receive-errors.js +3 -6
- package/lib/groups/mls-state.d.ts +5 -3
- package/lib/groups/mls-state.js +27 -4
- package/lib/groups/peer-presence.d.ts +90 -0
- package/lib/groups/peer-presence.js +295 -0
- package/lib/groups/peer-selection.d.ts +37 -0
- package/lib/groups/peer-selection.js +72 -0
- package/lib/groups/roster-projection.d.ts +53 -0
- package/lib/groups/roster-projection.js +65 -0
- package/lib/groups/store-received-grant.d.ts +4 -3
- package/lib/groups/store-received-grant.js +80 -35
- package/lib/groups/store-received-revocation.d.ts +16 -13
- package/lib/groups/store-received-revocation.js +23 -22
- package/lib/hub/http-client.d.ts +10 -27
- package/lib/hub/http-client.js +2 -19
- package/lib/hub/hub-like.d.ts +57 -6
- package/lib/hub/hub-like.js +539 -52
- package/lib/hub/loopback-log-hub.d.ts +20 -0
- package/lib/hub/loopback-log-hub.js +178 -0
- package/lib/hub/manager.d.ts +25 -40
- package/lib/hub/manager.js +51 -94
- package/lib/hub/peer-scoped-hub-view.d.ts +35 -0
- package/lib/hub/peer-scoped-hub-view.js +92 -0
- package/lib/hub/sender-scoped-hub-view.d.ts +41 -0
- package/lib/hub/sender-scoped-hub-view.js +59 -0
- package/lib/hub/server-did.d.ts +67 -0
- package/lib/hub/server-did.js +131 -0
- package/lib/hub/wiring.d.ts +71 -14
- package/lib/hub/wiring.js +80 -23
- package/lib/index.d.ts +48 -13
- package/lib/index.js +257 -86
- package/lib/peer/connection-registry.d.ts +45 -0
- package/lib/peer/connection-registry.js +52 -0
- package/lib/peer/handlers.d.ts +69 -0
- package/lib/peer/handlers.js +110 -0
- package/lib/protocol.d.ts +213 -0
- package/lib/protocol.js +268 -0
- package/lib/schema.d.ts +2 -1
- package/lib/schema.js +805 -131
- package/lib/sync/access-default-sender.d.ts +23 -0
- package/lib/sync/access-default-sender.js +80 -0
- package/lib/sync/authorize.d.ts +57 -3
- package/lib/sync/authorize.js +45 -4
- package/lib/sync/broadcast-queue.d.ts +3 -4
- package/lib/sync/broadcast-queue.js +2 -3
- package/lib/sync/catalog-scope.d.ts +8 -7
- package/lib/sync/catalog-scope.js +55 -39
- package/lib/sync/errors.d.ts +9 -0
- package/lib/sync/errors.js +10 -0
- package/lib/sync/forwarder.d.ts +13 -19
- package/lib/sync/forwarder.js +13 -19
- package/lib/sync/handlers.js +54 -14
- package/lib/sync/hub-tunnel-sync-listener.d.ts +16 -6
- package/lib/sync/hub-tunnel-sync-listener.js +126 -37
- package/lib/sync/hub-tunnel-sync-provider.d.ts +31 -11
- package/lib/sync/hub-tunnel-sync-provider.js +68 -19
- package/lib/sync/merkle-channel.d.ts +9 -1
- package/lib/sync/merkle-channel.js +0 -0
- package/lib/sync/peer-registry.d.ts +14 -10
- package/lib/sync/peer-registry.js +0 -25
- package/lib/sync/sync-client.d.ts +11 -1
- package/lib/sync/sync-client.js +72 -10
- package/lib/sync/sync-manager.d.ts +22 -4
- package/lib/sync/sync-manager.js +39 -10
- package/lib/sync/tunnel-listeners.d.ts +52 -0
- package/lib/sync/tunnel-listeners.js +175 -0
- package/lib/sync/tunnel-observability.d.ts +25 -0
- package/lib/sync/tunnel-observability.js +63 -0
- package/lib/sync/tunnel-topics.d.ts +20 -0
- package/lib/sync/tunnel-topics.js +28 -0
- package/lib/types.d.ts +699 -28
- package/lib/types.js +0 -1
- package/lib/util/handler-error.d.ts +16 -0
- package/lib/util/handler-error.js +64 -0
- package/lib/util/mutex.d.ts +14 -1
- package/lib/util/mutex.js +37 -3
- package/package.json +56 -52
- package/lib/groups/ledger-entry.d.ts +0 -37
- package/lib/groups/ledger-entry.js +0 -56
- package/lib/groups/ledger-fold.d.ts +0 -56
- package/lib/groups/ledger-fold.js +0 -53
- package/lib/hub/did-observing-transport.d.ts +0 -75
- package/lib/hub/did-observing-transport.js +0 -126
- package/lib/hub/errors.d.ts +0 -24
- package/lib/hub/errors.js +0 -33
|
@@ -14,26 +14,40 @@ import { defineGroupProtocol } from '@kumiai/rpc';
|
|
|
14
14
|
* self-authenticating at the payload level (signed tokens / HLC-stamped
|
|
15
15
|
* records), so the lane provides ordering + delivery, not authorization.
|
|
16
16
|
*/ export const controlProtocol = defineGroupProtocol({
|
|
17
|
-
'control/
|
|
18
|
-
type: '
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
'control/ledgerCatchup': {
|
|
18
|
+
type: 'request',
|
|
19
|
+
retain: 'ephemeral',
|
|
20
|
+
description: 'Ask current members for the group control ledger to backfill missed entries. Gathered: each member replies with its entries; the requester re-verifies every token.',
|
|
21
|
+
param: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {},
|
|
24
|
+
additionalProperties: false
|
|
25
|
+
},
|
|
26
|
+
result: {
|
|
21
27
|
type: 'object',
|
|
22
28
|
properties: {
|
|
23
|
-
|
|
24
|
-
type: '
|
|
25
|
-
|
|
29
|
+
entries: {
|
|
30
|
+
type: 'array',
|
|
31
|
+
items: {
|
|
32
|
+
type: 'string'
|
|
33
|
+
},
|
|
34
|
+
description: 'Signed control-ledger entry tokens.'
|
|
26
35
|
}
|
|
27
36
|
},
|
|
28
37
|
required: [
|
|
29
|
-
'
|
|
38
|
+
'entries'
|
|
30
39
|
],
|
|
31
40
|
additionalProperties: false
|
|
32
41
|
}
|
|
33
42
|
},
|
|
34
|
-
'control/
|
|
43
|
+
'control/policyCatchup': {
|
|
35
44
|
type: 'request',
|
|
36
|
-
|
|
45
|
+
// Nothing kept: the reply is a snapshot of what each responder's rules are
|
|
46
|
+
// right now, and one that outlived its window would be answering for a
|
|
47
|
+
// policy that may since have been rewritten. The durable half of this lane
|
|
48
|
+
// is `control/accessDefaultSet`, which this repairs rather than replaces.
|
|
49
|
+
retain: 'ephemeral',
|
|
50
|
+
description: 'Ask current members to restate their OWN model access-defaults, for a member whose read position fell below the hub retention floor and can no longer read the frames that carried them. Gathered: each member replies with signed tokens for its own rules; the requester verifies every token and applies it under the same sender-bound LWW rules as a live frame.',
|
|
37
51
|
param: {
|
|
38
52
|
type: 'object',
|
|
39
53
|
properties: {},
|
|
@@ -42,22 +56,23 @@ import { defineGroupProtocol } from '@kumiai/rpc';
|
|
|
42
56
|
result: {
|
|
43
57
|
type: 'object',
|
|
44
58
|
properties: {
|
|
45
|
-
|
|
59
|
+
defaults: {
|
|
46
60
|
type: 'array',
|
|
47
61
|
items: {
|
|
48
62
|
type: 'string'
|
|
49
63
|
},
|
|
50
|
-
description: 'Signed
|
|
64
|
+
description: 'Signed access-default set tokens, one per rule the responder owns.'
|
|
51
65
|
}
|
|
52
66
|
},
|
|
53
67
|
required: [
|
|
54
|
-
'
|
|
68
|
+
'defaults'
|
|
55
69
|
],
|
|
56
70
|
additionalProperties: false
|
|
57
71
|
}
|
|
58
72
|
},
|
|
59
73
|
'control/delegationShare': {
|
|
60
74
|
type: 'event',
|
|
75
|
+
retain: 'log',
|
|
61
76
|
description: 'Distribute a minted document/write capability token to group co-members.',
|
|
62
77
|
data: {
|
|
63
78
|
type: 'object',
|
|
@@ -80,6 +95,7 @@ import { defineGroupProtocol } from '@kumiai/rpc';
|
|
|
80
95
|
},
|
|
81
96
|
'control/delegationRevoke': {
|
|
82
97
|
type: 'event',
|
|
98
|
+
retain: 'log',
|
|
83
99
|
description: 'Distribute a signed revocation record for a previously minted capability.',
|
|
84
100
|
data: {
|
|
85
101
|
type: 'object',
|
|
@@ -100,8 +116,142 @@ import { defineGroupProtocol } from '@kumiai/rpc';
|
|
|
100
116
|
additionalProperties: false
|
|
101
117
|
}
|
|
102
118
|
},
|
|
119
|
+
'control/accessDefaultSet': {
|
|
120
|
+
type: 'event',
|
|
121
|
+
// Durable policy, not a notification: a member offline while the owner
|
|
122
|
+
// rewrites its sharing rule must learn it on return, or it keeps serving
|
|
123
|
+
// that owner's documents under a rule the owner replaced. Retained like the
|
|
124
|
+
// delegation frames, and for the same reason.
|
|
125
|
+
retain: 'log',
|
|
126
|
+
description: "Distribute a member's own model access-default to co-members. `auth` is the signed statement; every other field mirrors it so the receiver can rebuild the frame before verifying.",
|
|
127
|
+
data: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
modelID: {
|
|
131
|
+
type: 'string'
|
|
132
|
+
},
|
|
133
|
+
permissionType: {
|
|
134
|
+
type: 'string',
|
|
135
|
+
enum: [
|
|
136
|
+
'read',
|
|
137
|
+
'write'
|
|
138
|
+
]
|
|
139
|
+
},
|
|
140
|
+
rule: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
level: {
|
|
144
|
+
type: 'string'
|
|
145
|
+
},
|
|
146
|
+
allowedDIDs: {
|
|
147
|
+
type: [
|
|
148
|
+
'array',
|
|
149
|
+
'null'
|
|
150
|
+
],
|
|
151
|
+
items: {
|
|
152
|
+
type: 'string'
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
allowedCircles: {
|
|
156
|
+
type: [
|
|
157
|
+
'array',
|
|
158
|
+
'null'
|
|
159
|
+
],
|
|
160
|
+
items: {
|
|
161
|
+
type: 'string'
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
allowedGroups: {
|
|
165
|
+
type: [
|
|
166
|
+
'array',
|
|
167
|
+
'null'
|
|
168
|
+
],
|
|
169
|
+
items: {
|
|
170
|
+
type: 'string'
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
required: [
|
|
175
|
+
'level',
|
|
176
|
+
'allowedDIDs',
|
|
177
|
+
'allowedCircles',
|
|
178
|
+
'allowedGroups'
|
|
179
|
+
],
|
|
180
|
+
additionalProperties: false
|
|
181
|
+
},
|
|
182
|
+
ownerDID: {
|
|
183
|
+
type: 'string',
|
|
184
|
+
description: 'Claimed owner; checked against the `auth` issuer.'
|
|
185
|
+
},
|
|
186
|
+
hlc: {
|
|
187
|
+
type: 'string',
|
|
188
|
+
description: 'Owner-stamped LWW anchor, mirroring the stored row.'
|
|
189
|
+
},
|
|
190
|
+
auth: {
|
|
191
|
+
type: 'string',
|
|
192
|
+
description: 'Signed access-default token.'
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
required: [
|
|
196
|
+
'modelID',
|
|
197
|
+
'permissionType',
|
|
198
|
+
'rule',
|
|
199
|
+
'ownerDID',
|
|
200
|
+
'hlc',
|
|
201
|
+
'auth'
|
|
202
|
+
],
|
|
203
|
+
additionalProperties: false
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
'control/accessDefaultRemove': {
|
|
207
|
+
type: 'event',
|
|
208
|
+
// Same retention as the set, and load-bearing in the other direction: an
|
|
209
|
+
// ephemeral removal would distribute every widening and swallow the
|
|
210
|
+
// narrowings, leaving an absent member serving under a withdrawn rule.
|
|
211
|
+
retain: 'log',
|
|
212
|
+
description: "Distribute a member's own model access-default removal to co-members.",
|
|
213
|
+
data: {
|
|
214
|
+
type: 'object',
|
|
215
|
+
properties: {
|
|
216
|
+
modelID: {
|
|
217
|
+
type: 'string'
|
|
218
|
+
},
|
|
219
|
+
permissionTypes: {
|
|
220
|
+
type: 'array',
|
|
221
|
+
items: {
|
|
222
|
+
type: 'string',
|
|
223
|
+
enum: [
|
|
224
|
+
'read',
|
|
225
|
+
'write'
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
ownerDID: {
|
|
230
|
+
type: 'string',
|
|
231
|
+
description: 'Claimed owner; checked against the `auth` issuer.'
|
|
232
|
+
},
|
|
233
|
+
hlc: {
|
|
234
|
+
type: 'string',
|
|
235
|
+
description: 'Owner-stamped LWW anchor for the removal.'
|
|
236
|
+
},
|
|
237
|
+
auth: {
|
|
238
|
+
type: 'string',
|
|
239
|
+
description: 'Signed access-default removal token.'
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
required: [
|
|
243
|
+
'modelID',
|
|
244
|
+
'permissionTypes',
|
|
245
|
+
'ownerDID',
|
|
246
|
+
'hlc',
|
|
247
|
+
'auth'
|
|
248
|
+
],
|
|
249
|
+
additionalProperties: false
|
|
250
|
+
}
|
|
251
|
+
},
|
|
103
252
|
'control/groupLeaveRequest': {
|
|
104
253
|
type: 'event',
|
|
254
|
+
retain: 'ephemeral',
|
|
105
255
|
description: 'Advisory voluntary-leave notice. Not authoritative — an admin must issue a real removeMember; receivers surface it as an event and never tombstone on it.',
|
|
106
256
|
data: {
|
|
107
257
|
type: 'object',
|
|
@@ -128,6 +278,7 @@ import { defineGroupProtocol } from '@kumiai/rpc';
|
|
|
128
278
|
*/ export const syncProtocol = defineGroupProtocol({
|
|
129
279
|
'sync/mutationApply': {
|
|
130
280
|
type: 'event',
|
|
281
|
+
retain: 'ephemeral',
|
|
131
282
|
description: 'Broadcast a batch of signed document mutations to group members.',
|
|
132
283
|
data: {
|
|
133
284
|
type: 'object',
|
|
@@ -168,7 +319,130 @@ import { defineGroupProtocol } from '@kumiai/rpc';
|
|
|
168
319
|
}
|
|
169
320
|
}
|
|
170
321
|
});
|
|
171
|
-
/**
|
|
322
|
+
/**
|
|
323
|
+
* Peer plane: what each device advertises about itself to its co-members. Its
|
|
324
|
+
* own protocol rather than a third control procedure, so its traffic and
|
|
325
|
+
* retention floor stay independent of the control ledger's — the topic derives
|
|
326
|
+
* per protocol NAME (`protocolTopic(secret, epoch, name)`), so a new name can
|
|
327
|
+
* only add a lane.
|
|
328
|
+
*
|
|
329
|
+
* ATTRIBUTION IS THE LANE'S, NOT THE BODY'S: `peer/announce` carries no DID
|
|
330
|
+
* field and the receiver keys the row on `message.payload.iss`, the
|
|
331
|
+
* MLS-authenticated sender. A convenience `peerDID` in the body would let any
|
|
332
|
+
* member write any other member's row — a defect this project has shipped once.
|
|
333
|
+
*/ /**
|
|
334
|
+
* What a device says about itself. ONE schema object, used as `peer/announce`'s
|
|
335
|
+
* body and as `peer/query`'s result: a query reply IS an announcement, delivered
|
|
336
|
+
* on demand, and two schemas that happen to agree today would drift the first
|
|
337
|
+
* time one of them gained a field.
|
|
338
|
+
*/ const peerAnnouncementBody = {
|
|
339
|
+
type: 'object',
|
|
340
|
+
properties: {
|
|
341
|
+
label: {
|
|
342
|
+
type: 'string',
|
|
343
|
+
description: 'Self-declared display hint, Bluetooth-style. Rendered beside the DID, never read as an identity.'
|
|
344
|
+
},
|
|
345
|
+
availability: {
|
|
346
|
+
type: 'string',
|
|
347
|
+
enum: [
|
|
348
|
+
'always-on',
|
|
349
|
+
'interactive',
|
|
350
|
+
'mobile'
|
|
351
|
+
],
|
|
352
|
+
description: 'Steers automatic selection only; it never refuses to serve.'
|
|
353
|
+
},
|
|
354
|
+
capabilities: {
|
|
355
|
+
type: 'array',
|
|
356
|
+
items: {
|
|
357
|
+
type: 'object',
|
|
358
|
+
properties: {
|
|
359
|
+
protocol: {
|
|
360
|
+
type: 'string'
|
|
361
|
+
},
|
|
362
|
+
version: {
|
|
363
|
+
type: 'number'
|
|
364
|
+
},
|
|
365
|
+
transports: {
|
|
366
|
+
type: [
|
|
367
|
+
'array',
|
|
368
|
+
'null'
|
|
369
|
+
],
|
|
370
|
+
items: {
|
|
371
|
+
type: 'string'
|
|
372
|
+
},
|
|
373
|
+
description: 'Addressing hints for a capability the hub tunnel does not already reach. Null for one it does.'
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
// Nullable-and-required rather than optional, as the access-default
|
|
377
|
+
// rule's arms are: an absent key and an explicit null would otherwise
|
|
378
|
+
// be two encodings of one fact, and only one of them round-trips.
|
|
379
|
+
required: [
|
|
380
|
+
'protocol',
|
|
381
|
+
'version',
|
|
382
|
+
'transports'
|
|
383
|
+
],
|
|
384
|
+
additionalProperties: false
|
|
385
|
+
},
|
|
386
|
+
description: 'Matched EXACTLY on protocol AND version.'
|
|
387
|
+
},
|
|
388
|
+
hlc: {
|
|
389
|
+
type: 'string',
|
|
390
|
+
description: 'Announcer-stamped LWW anchor for its own projection row.'
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
required: [
|
|
394
|
+
'label',
|
|
395
|
+
'availability',
|
|
396
|
+
'capabilities',
|
|
397
|
+
'hlc'
|
|
398
|
+
],
|
|
399
|
+
additionalProperties: false
|
|
400
|
+
};
|
|
401
|
+
export const peerProtocol = defineGroupProtocol({
|
|
402
|
+
'peer/announce': {
|
|
403
|
+
type: 'event',
|
|
404
|
+
// Durable state, not a notification. A device that was offline (or was added
|
|
405
|
+
// at a later epoch, and so was never on the old topic) has no other way to
|
|
406
|
+
// learn who else is reachable: the projection is the source of truth and this
|
|
407
|
+
// lane is what fills it.
|
|
408
|
+
retain: 'log',
|
|
409
|
+
description: 'Advertise this device to its co-members: display label, availability class, and the exact capabilities it answers on. The announcer is taken from the authenticated sender, so the body names nobody.',
|
|
410
|
+
data: peerAnnouncementBody
|
|
411
|
+
},
|
|
412
|
+
'peer/query': {
|
|
413
|
+
type: 'request',
|
|
414
|
+
// Liveness, so nothing is kept: a reply that outlived its window would be
|
|
415
|
+
// answering a question nobody is still asking, and the durable half of this
|
|
416
|
+
// lane is `peer/announce`.
|
|
417
|
+
retain: 'ephemeral',
|
|
418
|
+
description: 'Ask who is reachable right now, optionally narrowed to devices answering named protocols. Gathered: every matching device replies with its own advertisement within the window. A result is "who answered", never "who exists".',
|
|
419
|
+
param: {
|
|
420
|
+
type: 'object',
|
|
421
|
+
properties: {
|
|
422
|
+
protocols: {
|
|
423
|
+
type: [
|
|
424
|
+
'array',
|
|
425
|
+
'null'
|
|
426
|
+
],
|
|
427
|
+
items: {
|
|
428
|
+
type: 'string'
|
|
429
|
+
},
|
|
430
|
+
description: "Only devices answering one of these reply. Null means any. Nullable-and-required, as the announce body's `transports` is."
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
required: [
|
|
434
|
+
'protocols'
|
|
435
|
+
],
|
|
436
|
+
additionalProperties: false
|
|
437
|
+
},
|
|
438
|
+
result: peerAnnouncementBody
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
/**
|
|
442
|
+
* The app-lane protocols, keyed for `createGroupPeer({ protocols })`. Each key
|
|
443
|
+
* names its own topic, so adding one moves no existing lane.
|
|
444
|
+
*/ export const groupProtocols = {
|
|
172
445
|
control: controlProtocol,
|
|
173
|
-
sync: syncProtocol
|
|
446
|
+
sync: syncProtocol,
|
|
447
|
+
peer: peerProtocol
|
|
174
448
|
};
|
|
@@ -1,4 +1,36 @@
|
|
|
1
|
+
import type { CatalogRecord } from '@kubun/protocol';
|
|
1
2
|
import type { Invite, KeyPackageBundle } from '@kumiai/mls';
|
|
3
|
+
import type { AccessDefaultRule } from './access-default-token.js';
|
|
4
|
+
/**
|
|
5
|
+
* The inviter's own model access-default, carried as a self-authenticating seed
|
|
6
|
+
* so the joiner holds it before any broadcast arrives. `auth` is a token signed
|
|
7
|
+
* by the owner; the joiner derives the authoritative fields from it and applies
|
|
8
|
+
* the rule only when `ownerDID` equals the verified issuer (sender-bound), LWW
|
|
9
|
+
* by `hlc`. Mirrors the `access-default:set` broadcast frame minus the `type`.
|
|
10
|
+
*/
|
|
11
|
+
export type InviteAccessDefault = {
|
|
12
|
+
ownerDID: string;
|
|
13
|
+
modelID: string;
|
|
14
|
+
permissionType: 'read' | 'write';
|
|
15
|
+
rule: AccessDefaultRule;
|
|
16
|
+
hlc: string;
|
|
17
|
+
auth: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A group catalog seeded into a joiner, tagged with the circle whose
|
|
21
|
+
* `catalog_ids` referenced it. `token` is the creator's original signed
|
|
22
|
+
* `catalog:set` token, forwarded verbatim: the joiner re-verifies it with the
|
|
23
|
+
* same helper the broadcast receive path uses (creator-binding `ownerDID ===
|
|
24
|
+
* iss`, payload matches the signed record) before storing anything, so an
|
|
25
|
+
* inviter cannot inject a spoofed catalog. The joiner stores it as a KNOWN (not
|
|
26
|
+
* active) catalog with provenance `source_group_id` = the joined group and
|
|
27
|
+
* `source_circle_id` = this circle — activation stays an explicit local decision.
|
|
28
|
+
*/
|
|
29
|
+
export type InviteCatalogSeed = {
|
|
30
|
+
catalog: CatalogRecord;
|
|
31
|
+
sourceCircleID: string;
|
|
32
|
+
token: string;
|
|
33
|
+
};
|
|
2
34
|
export type JoinRequestPayload = {
|
|
3
35
|
did: string;
|
|
4
36
|
publicPackage: KeyPackageBundle['publicPackage'];
|
|
@@ -35,6 +67,22 @@ export type InvitePayload = {
|
|
|
35
67
|
* against any caps it already holds.
|
|
36
68
|
*/
|
|
37
69
|
revocations?: Array<string>;
|
|
70
|
+
/**
|
|
71
|
+
* The inviter's OWN model access-default rows, each carrying a token signed by
|
|
72
|
+
* the inviter. Applied sender-bound (owner must equal the token issuer) and
|
|
73
|
+
* LWW by HLC at join, so a third-party row a malicious inviter tries to inject
|
|
74
|
+
* cannot be signed and is dropped.
|
|
75
|
+
*/
|
|
76
|
+
accessDefaults?: Array<InviteAccessDefault>;
|
|
77
|
+
/**
|
|
78
|
+
* The group's catalog records (those referenced by the group's circles), each
|
|
79
|
+
* tagged with the referencing circle. Stored as KNOWN (not active) catalogs at
|
|
80
|
+
* join — discovery applies no sync, so activation stays an explicit local
|
|
81
|
+
* decision. Carried as plain records: catalogs are sync-scope hints, not
|
|
82
|
+
* authorization, so they are trusted from the inviter like `groupName` and
|
|
83
|
+
* `suggestedHubs` rather than individually signed.
|
|
84
|
+
*/
|
|
85
|
+
catalogs?: Array<InviteCatalogSeed>;
|
|
38
86
|
};
|
|
39
87
|
export declare function encodeJoinRequest(payload: JoinRequestPayload): string;
|
|
40
88
|
export declare function decodeJoinRequest(encoded: string): JoinRequestPayload;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DID of the device that authored a join request.
|
|
3
|
+
*
|
|
4
|
+
* Read from the MLS credential inside the KeyPackage, which the leaf signature
|
|
5
|
+
* covers; NEVER from the payload's sibling `did` field, which its author writes
|
|
6
|
+
* freely. Those two are one identity claim written in two places, so a
|
|
7
|
+
* disagreement REFUSES — resolving it in either direction would pick a winner
|
|
8
|
+
* silently.
|
|
9
|
+
*
|
|
10
|
+
* Every path that mints an invite resolves the recipient through here, because
|
|
11
|
+
* the mint hands the two halves to different subsystems: the KeyPackage becomes
|
|
12
|
+
* an MLS leaf and the DID becomes a membership row and a role entry. Taking the
|
|
13
|
+
* DID from the payload lets a forged pairing put those two names out of step —
|
|
14
|
+
* a leaf for one identity, an authenticated membership row for another — and no
|
|
15
|
+
* later fold reconciles them, because each side is doing exactly what it was
|
|
16
|
+
* told.
|
|
17
|
+
*
|
|
18
|
+
* An envelope-bound caller can additionally compare the result against the DID
|
|
19
|
+
* that signed the request, which is a check this function cannot make.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveJoinRequestDID(joinRequest: string): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { normalizeDID } from '@kokuin/token';
|
|
2
|
+
import { parseMLSCredentialIdentity } from '@kumiai/mls';
|
|
3
|
+
import { GraphQLError } from 'graphql';
|
|
4
|
+
import { defaultCredentialTypes, isDefaultCredential } from 'ts-mls';
|
|
5
|
+
import { decodeJoinRequest } from './invite-payload.js';
|
|
6
|
+
/**
|
|
7
|
+
* The DID of the device that authored a join request.
|
|
8
|
+
*
|
|
9
|
+
* Read from the MLS credential inside the KeyPackage, which the leaf signature
|
|
10
|
+
* covers; NEVER from the payload's sibling `did` field, which its author writes
|
|
11
|
+
* freely. Those two are one identity claim written in two places, so a
|
|
12
|
+
* disagreement REFUSES — resolving it in either direction would pick a winner
|
|
13
|
+
* silently.
|
|
14
|
+
*
|
|
15
|
+
* Every path that mints an invite resolves the recipient through here, because
|
|
16
|
+
* the mint hands the two halves to different subsystems: the KeyPackage becomes
|
|
17
|
+
* an MLS leaf and the DID becomes a membership row and a role entry. Taking the
|
|
18
|
+
* DID from the payload lets a forged pairing put those two names out of step —
|
|
19
|
+
* a leaf for one identity, an authenticated membership row for another — and no
|
|
20
|
+
* later fold reconciles them, because each side is doing exactly what it was
|
|
21
|
+
* told.
|
|
22
|
+
*
|
|
23
|
+
* An envelope-bound caller can additionally compare the result against the DID
|
|
24
|
+
* that signed the request, which is a check this function cannot make.
|
|
25
|
+
*/ export function resolveJoinRequestDID(joinRequest) {
|
|
26
|
+
const decoded = decodeJoinRequest(joinRequest);
|
|
27
|
+
const credential = decoded.publicPackage.leafNode.credential;
|
|
28
|
+
// Only a basic credential carries an `identity` at all — x509 carries
|
|
29
|
+
// certificates and a custom one opaque bytes — so a non-basic credential binds
|
|
30
|
+
// no DID. Refused rather than coerced back onto the payload field.
|
|
31
|
+
if (!isDefaultCredential(credential) || credential.credentialType !== defaultCredentialTypes.basic) {
|
|
32
|
+
throw new GraphQLError(`the join request carries a non-basic MLS credential (type ${credential.credentialType}), which binds no identity`);
|
|
33
|
+
}
|
|
34
|
+
const credentialDID = parseMLSCredentialIdentity(credential.identity).id;
|
|
35
|
+
if (credentialDID === '') {
|
|
36
|
+
throw new GraphQLError('the join request credential carries no identity');
|
|
37
|
+
}
|
|
38
|
+
const payloadDID = typeof decoded.did === 'string' ? decoded.did : null;
|
|
39
|
+
if (payloadDID == null || normalizeDID(payloadDID) !== normalizeDID(credentialDID)) {
|
|
40
|
+
throw new GraphQLError(`the join request's did field (${String(payloadDID)}) disagrees with its KeyPackage credential (${credentialDID})`);
|
|
41
|
+
}
|
|
42
|
+
return credentialDID;
|
|
43
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { type OwnIdentity } from '@kokuin/token';
|
|
1
2
|
import type { StoreProvider } from '@kubun/db';
|
|
2
3
|
import type { HLC } from '@kubun/hlc';
|
|
3
4
|
import type { Logger } from '@kubun/logger';
|
|
5
|
+
import type { GroupAnchor, LedgerLogEntry } from '@kumiai/mls';
|
|
4
6
|
import type { GroupData } from '../types.js';
|
|
5
7
|
import type { P2PEventEmitter } from './events.js';
|
|
8
|
+
import type { InviteAccessDefault, InviteCatalogSeed } from './invite-payload.js';
|
|
6
9
|
export type FinalizeJoinedGroupParams = {
|
|
7
10
|
stores: StoreProvider;
|
|
8
11
|
emitter: P2PEventEmitter;
|
|
@@ -78,3 +81,60 @@ export type ApplyInviteRevocationsParams = {
|
|
|
78
81
|
* Returns the number of revocations that resulted in a stored row.
|
|
79
82
|
*/
|
|
80
83
|
export declare function applyInviteRevocations(params: ApplyInviteRevocationsParams): Promise<number>;
|
|
84
|
+
/**
|
|
85
|
+
* The group's control state an invite carries so a joiner is current at join.
|
|
86
|
+
*
|
|
87
|
+
* NOT the control ledger. That travels on the invite `@kumiai/mls` builds, as
|
|
88
|
+
* the list its own head check is computed over — a second copy assembled here
|
|
89
|
+
* could only agree with it or corrupt it.
|
|
90
|
+
*/
|
|
91
|
+
export type InviteControlSeeds = {
|
|
92
|
+
accessDefaults: Array<InviteAccessDefault>;
|
|
93
|
+
catalogs: Array<InviteCatalogSeed>;
|
|
94
|
+
};
|
|
95
|
+
export type CollectInviteSeedsParams = {
|
|
96
|
+
stores: StoreProvider;
|
|
97
|
+
/** Inviter identity — signs the access-default seeds so they self-authenticate. */
|
|
98
|
+
identity: OwnIdentity;
|
|
99
|
+
/** Owner DID whose model access-defaults are seeded (the inviter). */
|
|
100
|
+
ownerDID: string;
|
|
101
|
+
groupID: string;
|
|
102
|
+
logger?: Logger;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Gather the group state an invite seeds into the joiner: the inviter's own
|
|
106
|
+
* model access-defaults (each signed so it self-authenticates), and the catalogs
|
|
107
|
+
* the group's circles reference. Access-default rows without an HLC anchor are
|
|
108
|
+
* skipped — they carry no LWW ordering and cannot be authenticated for
|
|
109
|
+
* replication.
|
|
110
|
+
*/
|
|
111
|
+
export declare function collectInviteSeeds(params: CollectInviteSeedsParams): Promise<InviteControlSeeds>;
|
|
112
|
+
export type ApplyInviteControlStateParams = {
|
|
113
|
+
stores: StoreProvider;
|
|
114
|
+
groupID: string;
|
|
115
|
+
accessDefaults?: Array<InviteAccessDefault>;
|
|
116
|
+
catalogs?: Array<InviteCatalogSeed>;
|
|
117
|
+
/** Device-wide clock — the joined ledger's HLCs merge into it. */
|
|
118
|
+
hlc: HLC;
|
|
119
|
+
/** Resolves the group's genesis anchor so ledger entries can project at join. */
|
|
120
|
+
getGroupAnchor?: (groupID: string) => Promise<GroupAnchor | null>;
|
|
121
|
+
/** Resolves the group's control ledger, which every projection folds from. */
|
|
122
|
+
getGroupLedger?: (groupID: string) => Promise<ReadonlyArray<LedgerLogEntry>>;
|
|
123
|
+
logger?: Logger;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Apply the control state a join produced, after the MLS join completes.
|
|
127
|
+
*
|
|
128
|
+
* The control ledger is NOT among the parameters, deliberately: it arrived on
|
|
129
|
+
* the MLS invite and `processWelcome` has already checked it against the
|
|
130
|
+
* authenticated head and folded it into the handle, which is what every
|
|
131
|
+
* projection reads. All that is left is to adopt what the handle now holds. A
|
|
132
|
+
* second copy carried beside it could only agree or disagree, and a joiner has
|
|
133
|
+
* no way to tell which.
|
|
134
|
+
*
|
|
135
|
+
* The rest authenticates independently: access-defaults apply sender-bound + LWW
|
|
136
|
+
* via their owner-signed tokens, and catalogs upsert as plain sync-scope records
|
|
137
|
+
* trusted from the inviter. A third-party access-default row is dropped without
|
|
138
|
+
* blocking the rest.
|
|
139
|
+
*/
|
|
140
|
+
export declare function applyInviteControlState(params: ApplyInviteControlStateParams): Promise<void>;
|