@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
package/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { ClientTransportOf } from '@enkaku/protocol';
|
|
2
|
+
import type { AccessLevel } from '@kubun/store-graph';
|
|
3
|
+
import type { ControlRequestKind, ControlRequestOutcome } from '@kubun/store-p2p';
|
|
4
|
+
import type { PeerAvailability, PeerCapability } from './groups/group-protocols.js';
|
|
5
|
+
import type { PeerConnection } from './peer/connection-registry.js';
|
|
2
6
|
import type { SyncDirection, SyncProtocol } from './protocol.js';
|
|
3
7
|
import type { PeerConfig, PeerConfigWithID } from './sync/peer-registry.js';
|
|
4
8
|
import type { SyncScope } from './sync/sync-client.js';
|
|
@@ -30,8 +34,29 @@ export type CircleData = {
|
|
|
30
34
|
groupID: string;
|
|
31
35
|
name: string;
|
|
32
36
|
description: string;
|
|
37
|
+
/** Admin-authored: every group member reaches an open circle implicitly. */
|
|
38
|
+
open: boolean;
|
|
33
39
|
createdAt: string;
|
|
34
40
|
};
|
|
41
|
+
/** `pending` until the commit lands, then whichever outcome it landed on. */
|
|
42
|
+
export type ControlRequestStatusValue = 'pending' | ControlRequestOutcome;
|
|
43
|
+
/**
|
|
44
|
+
* One entry of this device's control-request queue. Retained after settling,
|
|
45
|
+
* whatever the outcome, until the app acks it.
|
|
46
|
+
*
|
|
47
|
+
* Every `request*` operation below returns one of these rather than the row it
|
|
48
|
+
* asks for: a control entry moves a projection only when the commit carrying it is
|
|
49
|
+
* ENACTED, so at return the requested state is nowhere yet.
|
|
50
|
+
*/
|
|
51
|
+
export type ControlRequestData = {
|
|
52
|
+
id: string;
|
|
53
|
+
groupID: string;
|
|
54
|
+
kind: ControlRequestKind;
|
|
55
|
+
status: ControlRequestStatusValue;
|
|
56
|
+
error: string | null;
|
|
57
|
+
enqueuedAt: string;
|
|
58
|
+
settledAt: string | null;
|
|
59
|
+
};
|
|
35
60
|
export type CircleMemberData = {
|
|
36
61
|
circleID: string;
|
|
37
62
|
memberDID: string;
|
|
@@ -44,6 +69,11 @@ export type CatalogData = {
|
|
|
44
69
|
name: string;
|
|
45
70
|
description: string;
|
|
46
71
|
filterCriteria: string;
|
|
72
|
+
/** Local sync activation: true = active, false = known (not activated). */
|
|
73
|
+
active: boolean;
|
|
74
|
+
/** First-discovery provenance; null for own catalogs. */
|
|
75
|
+
sourceGroupID: string | null;
|
|
76
|
+
sourceCircleID: string | null;
|
|
47
77
|
createdAt: string;
|
|
48
78
|
};
|
|
49
79
|
export type HubData = {
|
|
@@ -54,8 +84,34 @@ export type HubData = {
|
|
|
54
84
|
createdAt: string;
|
|
55
85
|
updatedAt: string | null;
|
|
56
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* A hub answered under a DID other than the one this device had recorded, and
|
|
89
|
+
* the new one has been adopted. A rotated hub key is routine, so the device
|
|
90
|
+
* reconnects rather than refusing — but it is never silent: the application is
|
|
91
|
+
* told which hub changed identity, and from what to what.
|
|
92
|
+
*/
|
|
93
|
+
export type HubServerDIDChangedData = {
|
|
94
|
+
hubURL: string;
|
|
95
|
+
/** The DID that was recorded before the change — never absent, or there is no change. */
|
|
96
|
+
previousServerDID: string;
|
|
97
|
+
/** The DID the hub now advertises at `/info`, now recorded in its place. */
|
|
98
|
+
serverDID: string;
|
|
99
|
+
};
|
|
100
|
+
export type InviteToGroupInput = {
|
|
101
|
+
groupID: string;
|
|
102
|
+
joinRequest: string;
|
|
103
|
+
grants?: Array<string>;
|
|
104
|
+
revocations?: Array<string>;
|
|
105
|
+
};
|
|
57
106
|
export type InviteToGroupData = {
|
|
58
|
-
|
|
107
|
+
/** The settled request the invite was enacted through. */
|
|
108
|
+
request: ControlRequestData;
|
|
109
|
+
/**
|
|
110
|
+
* Welcome + ratchet tree for the joiner. `null` while the Add commit is parked
|
|
111
|
+
* (inviter offline) and `request.status` is `pending`. A settled-negative invite
|
|
112
|
+
* throws rather than returning null here.
|
|
113
|
+
*/
|
|
114
|
+
invitePayload: string | null;
|
|
59
115
|
groupID: string;
|
|
60
116
|
groupName: string;
|
|
61
117
|
};
|
|
@@ -65,16 +121,35 @@ export type JoinGroupData = {
|
|
|
65
121
|
export type LeaveGroupData = {
|
|
66
122
|
groupID: string;
|
|
67
123
|
};
|
|
68
|
-
export type
|
|
124
|
+
export type RemoveGroupMemberInput = {
|
|
69
125
|
groupID: string;
|
|
126
|
+
memberDID: string;
|
|
70
127
|
};
|
|
71
128
|
export type DeleteCatalogData = {
|
|
72
129
|
deletedID: string;
|
|
73
130
|
};
|
|
74
|
-
export type
|
|
75
|
-
|
|
131
|
+
export type CreateCircleInput = {
|
|
132
|
+
groupID: string;
|
|
133
|
+
name: string;
|
|
134
|
+
description?: string;
|
|
135
|
+
/**
|
|
136
|
+
* Internal callers only, never the GraphQL surface, and it MUST be DERIVED:
|
|
137
|
+
* `circle.def` is whole-value LWW per subject, so two callers naming one id merge
|
|
138
|
+
* two access scopes — and a member subject embeds the circle id, so both callers'
|
|
139
|
+
* grants land in the merged scope. A derived id is safe because a collision then
|
|
140
|
+
* means they meant the same circle. Omitted, one is minted.
|
|
141
|
+
*/
|
|
142
|
+
circleID?: string;
|
|
143
|
+
};
|
|
144
|
+
export type AddCircleMemberInput = {
|
|
145
|
+
circleID: string;
|
|
146
|
+
memberDID: string;
|
|
147
|
+
role: string;
|
|
148
|
+
};
|
|
149
|
+
export type DeleteCircleInput = {
|
|
150
|
+
circleID: string;
|
|
76
151
|
};
|
|
77
|
-
export type
|
|
152
|
+
export type RemoveCircleMemberInput = {
|
|
78
153
|
circleID: string;
|
|
79
154
|
memberDID: string;
|
|
80
155
|
};
|
|
@@ -82,6 +157,17 @@ export type UpdateGroupInput = {
|
|
|
82
157
|
groupID: string;
|
|
83
158
|
name?: string;
|
|
84
159
|
description?: string;
|
|
160
|
+
/** Circles admins mark open. */
|
|
161
|
+
openCircleIDs?: Array<string>;
|
|
162
|
+
/**
|
|
163
|
+
* The group's commit-hub designation: the shared hub identity every device
|
|
164
|
+
* folds and commits through. Setting it is admin re-designation; omitting it
|
|
165
|
+
* leaves the prior designation in place.
|
|
166
|
+
*/
|
|
167
|
+
commitHub?: {
|
|
168
|
+
url: string;
|
|
169
|
+
serverDID?: string | null;
|
|
170
|
+
};
|
|
85
171
|
};
|
|
86
172
|
export type UpdateCircleInput = {
|
|
87
173
|
circleID: string;
|
|
@@ -100,54 +186,154 @@ export type GroupRequestContext = {
|
|
|
100
186
|
}) => Promise<GroupData>;
|
|
101
187
|
list: () => Promise<Array<GroupData>>;
|
|
102
188
|
get: (id: string) => Promise<GroupData | null>;
|
|
103
|
-
addMember: (groupID: string, memberDID: string, role: string) => Promise<GroupMemberData>;
|
|
104
189
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
190
|
+
* Throw unless the calling identity is an admin of the group, with
|
|
191
|
+
* `extensions.code = 'KB14'` and `extensions.reason = 'NOT_GROUP_ADMIN'`.
|
|
192
|
+
*
|
|
193
|
+
* Exposed so a caller can gate BEFORE entering the transaction wrapper, which
|
|
194
|
+
* re-raises and strips extensions — that ordering is what keeps the code visible.
|
|
109
195
|
*/
|
|
110
|
-
|
|
196
|
+
requireAdmin: (groupID: string, action: string) => Promise<void>;
|
|
197
|
+
/**
|
|
198
|
+
* `requireAdmin` for an operation that only knows a circle: resolves the
|
|
199
|
+
* circle's group and gates on that.
|
|
200
|
+
*
|
|
201
|
+
* An unknown circle resolves to no group and is NOT refused here, so a missing
|
|
202
|
+
* circle never reads as an authority problem.
|
|
203
|
+
*/
|
|
204
|
+
requireCircleAdmin: (circleID: string, action: string) => Promise<void>;
|
|
205
|
+
/**
|
|
206
|
+
* Whether `did` is an admin right now, from the same membership row
|
|
207
|
+
* `requireAdmin` gates on — so a read surface cannot offer a control the gate
|
|
208
|
+
* refuses.
|
|
209
|
+
*/
|
|
210
|
+
isAdmin: (groupID: string, did: string) => Promise<boolean>;
|
|
211
|
+
/**
|
|
212
|
+
* Promote or demote a member, by emitting a self-signed role ledger entry that
|
|
213
|
+
* rides an MLS commit. Authority is the member row's mirrored role.
|
|
214
|
+
*
|
|
215
|
+
* Demoting a DID that is not currently an admin is minted already settled.
|
|
216
|
+
*/
|
|
217
|
+
requestSetMemberRole: (params: {
|
|
111
218
|
groupID: string;
|
|
112
219
|
memberDID: string;
|
|
113
220
|
role: 'admin' | 'member';
|
|
114
|
-
}) => Promise<
|
|
221
|
+
}) => Promise<ControlRequestData>;
|
|
115
222
|
listMembers: (groupID: string) => Promise<Array<GroupMemberData>>;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Request a new circle, by emitting a self-signed `circle.def` entry that rides
|
|
225
|
+
* an MLS commit. Authority is the caller's mirrored admin role.
|
|
226
|
+
*
|
|
227
|
+
* The id is minted here and handed back immediately, because every step that
|
|
228
|
+
* follows a create names the circle by id.
|
|
229
|
+
*/
|
|
230
|
+
requestCreateCircle: (input: CreateCircleInput) => Promise<{
|
|
231
|
+
request: ControlRequestData;
|
|
232
|
+
circleID: string;
|
|
233
|
+
}>;
|
|
121
234
|
listCircles: (groupID: string) => Promise<Array<CircleData>>;
|
|
122
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Add a DID to a circle at a role, by emitting a self-signed `circle.member`
|
|
237
|
+
* entry that rides an MLS commit. Re-adding an active member at a DIFFERENT role
|
|
238
|
+
* is how a role change is expressed; re-adding at the same role settles `no_op`.
|
|
239
|
+
*/
|
|
240
|
+
requestAddCircleMember: (input: AddCircleMemberInput) => Promise<ControlRequestData>;
|
|
123
241
|
createCatalog: (params: {
|
|
124
242
|
name: string;
|
|
125
243
|
description?: string;
|
|
126
244
|
filterCriteria: string;
|
|
245
|
+
/**
|
|
246
|
+
* Internal callers only, and it must be DERIVED so a re-run converges on the
|
|
247
|
+
* same catalog. Omitted, one is minted.
|
|
248
|
+
*/
|
|
249
|
+
catalogID?: string;
|
|
127
250
|
}) => Promise<CatalogData>;
|
|
128
|
-
|
|
251
|
+
/** All locally-known catalogs (any owner), optionally filtered by activation. */
|
|
252
|
+
listStoredCatalogs: (filter?: {
|
|
253
|
+
active?: boolean;
|
|
254
|
+
}) => Promise<Array<CatalogData>>;
|
|
255
|
+
/**
|
|
256
|
+
* This device's un-acked control requests, oldest first. The store is
|
|
257
|
+
* per-device, so the queue is viewer-scoped by the database itself.
|
|
258
|
+
*/
|
|
259
|
+
listControlRequests: (groupID?: string) => Promise<Array<ControlRequestData>>;
|
|
260
|
+
/** One control request by id, or `null` if it was never enqueued or is already acked. */
|
|
261
|
+
getControlRequest: (id: string) => Promise<ControlRequestData | null>;
|
|
129
262
|
/**
|
|
130
|
-
*
|
|
263
|
+
* Drop a settled request from the queue. Acking a `pending` (or unknown)
|
|
264
|
+
* request is a no-op, never an error.
|
|
265
|
+
*/
|
|
266
|
+
ackControlRequest: (id: string) => Promise<void>;
|
|
267
|
+
/**
|
|
268
|
+
* Resolve when a control request reaches a terminal status, with the settled
|
|
269
|
+
* request. For a caller that must not run its next step until this one landed.
|
|
270
|
+
*
|
|
271
|
+
* Rejects when `timeoutMs` elapses with the request still pending: callers
|
|
272
|
+
* await this inside a mutation, where waiting forever is a request that never
|
|
273
|
+
* answers.
|
|
274
|
+
*/
|
|
275
|
+
awaitSettle: (id: string, timeoutMs: number) => Promise<ControlRequestData>;
|
|
276
|
+
/** Turn on local sync for a known catalog. Throws on an unknown catalogID. */
|
|
277
|
+
activateCatalogSync: (catalogID: string) => Promise<CatalogData>;
|
|
278
|
+
/** Turn off local sync for a catalog. Throws on an unknown catalogID. */
|
|
279
|
+
deactivateCatalogSync: (catalogID: string) => Promise<CatalogData>;
|
|
280
|
+
/**
|
|
281
|
+
* Request an invite for a join request, by driving an MLS Add commit, and
|
|
282
|
+
* return the payload the accepted commit produced.
|
|
283
|
+
*
|
|
284
|
+
* AWAITS the commit — the payload exists only once the group takes the attempt
|
|
285
|
+
* that minted it, and it is deliberately not readable through the control-request
|
|
286
|
+
* node. So the CALLER MUST BE OUTSIDE any write transaction: the lane's own reads
|
|
287
|
+
* need that connection.
|
|
131
288
|
*
|
|
132
289
|
* `grants` are stringified write-capability JWTs the caller wants the joiner
|
|
133
290
|
* to hold immediately on join. `revocations` are signed revocation JWTs the
|
|
134
291
|
* caller wants the joiner to apply immediately — only verified ones travel;
|
|
135
292
|
* pending revocations (cap not yet known by the inviter) do not.
|
|
136
293
|
*/
|
|
137
|
-
|
|
294
|
+
requestInvite: (input: InviteToGroupInput) => Promise<InviteToGroupData>;
|
|
138
295
|
join: (invitePayload: string, joinRequestPayload: string) => Promise<JoinGroupData>;
|
|
139
296
|
leave: (groupID: string) => Promise<LeaveGroupData>;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Request a member's removal, by driving an MLS Remove commit that also
|
|
299
|
+
* carries the demotion entry an admin's eviction must enact. Authority is the
|
|
300
|
+
* caller's mirrored admin role, so a non-admin is refused before anything is
|
|
301
|
+
* written, as is a removal that would leave the group with no admin.
|
|
302
|
+
*/
|
|
303
|
+
requestRemoveMember: (input: RemoveGroupMemberInput) => Promise<ControlRequestData>;
|
|
304
|
+
/**
|
|
305
|
+
* Request a change to the group's settings, by emitting a self-signed
|
|
306
|
+
* `group.settings` entry that rides an MLS commit. Authority is the caller's
|
|
307
|
+
* mirrored admin role. An input carrying no field beyond `groupID` is minted
|
|
308
|
+
* already settled `no_op`.
|
|
309
|
+
*/
|
|
310
|
+
requestUpdate: (input: UpdateGroupInput) => Promise<ControlRequestData>;
|
|
311
|
+
/**
|
|
312
|
+
* Request a change to a circle's definition, by emitting a self-signed
|
|
313
|
+
* `circle.def` entry that rides an MLS commit. `circle.def` is whole-value LWW,
|
|
314
|
+
* so the patch is merged over the current definition BEFORE it is signed. An
|
|
315
|
+
* input carrying no field beyond `circleID` is minted already settled `no_op`.
|
|
316
|
+
*/
|
|
317
|
+
requestUpdateCircle: (input: UpdateCircleInput) => Promise<ControlRequestData>;
|
|
143
318
|
updateCatalog: (params: {
|
|
144
319
|
catalogID: string;
|
|
145
320
|
name?: string;
|
|
146
321
|
filterCriteria?: string;
|
|
147
322
|
}) => Promise<CatalogData>;
|
|
148
323
|
deleteCatalog: (catalogID: string) => Promise<DeleteCatalogData>;
|
|
149
|
-
|
|
150
|
-
|
|
324
|
+
/**
|
|
325
|
+
* Request that a circle be deleted, by emitting a self-signed `removed`
|
|
326
|
+
* definition entry that rides an MLS commit. Authority is the caller's mirrored
|
|
327
|
+
* admin role. A circle already absent or tombstoned is minted settled `no_op`.
|
|
328
|
+
*/
|
|
329
|
+
requestDeleteCircle: (input: DeleteCircleInput) => Promise<ControlRequestData>;
|
|
330
|
+
/**
|
|
331
|
+
* Request that a DID be removed from a circle, by emitting a self-signed
|
|
332
|
+
* `removed` membership entry that rides an MLS commit. Same authority as
|
|
333
|
+
* {@link GroupRequestContext.requestDeleteCircle}. A DID that is not an active
|
|
334
|
+
* member settles `no_op`.
|
|
335
|
+
*/
|
|
336
|
+
requestRemoveCircleMember: (input: RemoveCircleMemberInput) => Promise<ControlRequestData>;
|
|
151
337
|
/**
|
|
152
338
|
* Mint a `document/write` capability granting `to` write access on resources
|
|
153
339
|
* matching `res`. Token is stored grantor-side, broadcast to the group, and
|
|
@@ -196,11 +382,341 @@ export type GroupRequestContext = {
|
|
|
196
382
|
jti: string;
|
|
197
383
|
}) => Promise<boolean>;
|
|
198
384
|
};
|
|
385
|
+
/**
|
|
386
|
+
* Result of a `syncPeer` run, mirroring the `SyncPeerPayload` GraphQL type. All
|
|
387
|
+
* zero when the local device has no active catalog (the opt-in resting state):
|
|
388
|
+
* a clean no-op, never an error.
|
|
389
|
+
*/
|
|
390
|
+
export type SyncPeerData = {
|
|
391
|
+
messagesReceived: number;
|
|
392
|
+
messagesSent: number;
|
|
393
|
+
divergentBuckets: number;
|
|
394
|
+
};
|
|
395
|
+
/**
|
|
396
|
+
* Why an automatic catch-up carried nothing — the distinction all-zero counts
|
|
397
|
+
* cannot draw, and the one a screen has to draw to say anything true.
|
|
398
|
+
*
|
|
399
|
+
* - `synced` — a session ran. Zero counts here mean the two devices agreed.
|
|
400
|
+
* - `no-scopes` — this device has activated no catalog. Nothing was attempted,
|
|
401
|
+
* and nothing is wrong: the opt-in resting state.
|
|
402
|
+
* - `no-candidates` — no co-member has ever announced merkle catch-up in this
|
|
403
|
+
* group. Peering is incomplete, or nobody else has come online yet.
|
|
404
|
+
* - `no-route` — candidates existed and none answered. Every one was tried.
|
|
405
|
+
*/
|
|
406
|
+
export type PeerCatchUpOutcome = 'synced' | 'no-scopes' | 'no-candidates' | 'no-route';
|
|
407
|
+
/**
|
|
408
|
+
* What an automatic catch-up did, and — the part a caller cannot infer — WHICH
|
|
409
|
+
* peer the ranking picked. `peerDID` names the peer a session ran against, the
|
|
410
|
+
* last one tried when none answered, and `null` when none was tried at all.
|
|
411
|
+
*
|
|
412
|
+
* Read {@link outcome}, never the counts: three different situations produce
|
|
413
|
+
* all-zero, and only one of them is a device that is up to date.
|
|
414
|
+
*/
|
|
415
|
+
export type PeerCatchUpData = SyncPeerData & {
|
|
416
|
+
peerDID: string | null;
|
|
417
|
+
outcome: PeerCatchUpOutcome;
|
|
418
|
+
/**
|
|
419
|
+
* Why the last candidate failed, when the outcome is `no-route`. Absent for
|
|
420
|
+
* every other outcome, and for a `no-route` whose failure carried no message.
|
|
421
|
+
*
|
|
422
|
+
* `no-route` on its own cannot separate a device that is switched off from a
|
|
423
|
+
* session that connected, was answered, and then starved — two states that
|
|
424
|
+
* want opposite responses from whoever reads them. The failure the loop
|
|
425
|
+
* already catches is the only thing that tells them apart, and it used to be
|
|
426
|
+
* dropped after a debug line.
|
|
427
|
+
*/
|
|
428
|
+
failureReason?: string;
|
|
429
|
+
};
|
|
199
430
|
export type SyncRequestContext = {
|
|
200
431
|
addPeer: (config: PeerConfig) => Promise<void>;
|
|
201
432
|
removePeer: (peerDID: string) => Promise<void>;
|
|
202
433
|
listPeers: () => Promise<Array<PeerConfigWithID>>;
|
|
203
434
|
getStatus: (peerDID?: string) => SyncStatus;
|
|
435
|
+
/**
|
|
436
|
+
* Run a sync session against one named device, scoped by the local device's
|
|
437
|
+
* ACTIVE catalogs (resolved via `resolveCatalogSyncScopes`). Zero active
|
|
438
|
+
* catalogs → an all-zero no-op payload.
|
|
439
|
+
*
|
|
440
|
+
* `groupID` picks the ROUTE and nothing else: a co-member is reached through
|
|
441
|
+
* that group's hub tunnel, falling back to the persisted registry for a peer
|
|
442
|
+
* added by URL. It does not narrow the scopes — those stay device-wide, which
|
|
443
|
+
* is what lets one session carry documents belonging to another group.
|
|
444
|
+
*
|
|
445
|
+
* Both lookups are persisted, so this works after a restart with no prior
|
|
446
|
+
* `connectPeer`. The peer is taken as given: a user-named device is served
|
|
447
|
+
* whatever its availability class says about it, so a phone is never refused.
|
|
448
|
+
* Unreachable is an error here, not a report — someone asked.
|
|
449
|
+
*/
|
|
450
|
+
syncPeer: (groupID: string, peerDID: string) => Promise<SyncPeerData>;
|
|
451
|
+
/**
|
|
452
|
+
* Run the same session against the best candidate the group's discovery
|
|
453
|
+
* projection offers — availability tier first, most recently applied
|
|
454
|
+
* announcement breaking ties — over the same scopes {@link syncPeer} uses.
|
|
455
|
+
*
|
|
456
|
+
* Walks down the ranking when a candidate does not answer, because a row says
|
|
457
|
+
* a device once announced and never that it is on. Reports {@link
|
|
458
|
+
* PeerCatchUpData.outcome} rather than throwing: nobody named these peers, so
|
|
459
|
+
* nobody is waiting to be told which of them was off.
|
|
460
|
+
*
|
|
461
|
+
* No trigger and no timer: the caller decides WHEN (a foreground, a hub
|
|
462
|
+
* connect, a detected gap), and this decides only WHO.
|
|
463
|
+
*/
|
|
464
|
+
catchUpWithBestPeer: (groupID: string) => Promise<PeerCatchUpData>;
|
|
465
|
+
/**
|
|
466
|
+
* Every co-member device this one has ever heard announce in the group.
|
|
467
|
+
*
|
|
468
|
+
* The PROJECTION, not a gather: a row records that a device once said what it
|
|
469
|
+
* is, and rows are never deleted on silence. So this is "what is known", and
|
|
470
|
+
* a gather is what annotates it with "and who answered just now" — a screen
|
|
471
|
+
* built on the gather alone shows a device disappearing every time it sleeps.
|
|
472
|
+
*
|
|
473
|
+
* Includes this device's own row, because the projection holds it and hiding
|
|
474
|
+
* it here would make the list disagree with the store for one DID. Callers
|
|
475
|
+
* rendering "my other devices" filter it out.
|
|
476
|
+
*/
|
|
477
|
+
listPeerDevices: (groupID: string) => Promise<Array<PeerDeviceData>>;
|
|
478
|
+
};
|
|
479
|
+
/** One device's last word about itself, as the projection holds it. */
|
|
480
|
+
export type PeerDeviceData = {
|
|
481
|
+
peerDID: string;
|
|
482
|
+
/** Self-declared display hint, rendered beside the DID and never read as one. */
|
|
483
|
+
label: string;
|
|
484
|
+
availability: string;
|
|
485
|
+
capabilities: Array<PeerCapability>;
|
|
486
|
+
/** The announcer's own stamp, which the row is LWW-keyed by. */
|
|
487
|
+
hlc: string;
|
|
488
|
+
};
|
|
489
|
+
export type RemoteGroupInfoData = {
|
|
490
|
+
id: string;
|
|
491
|
+
name: string;
|
|
492
|
+
};
|
|
493
|
+
export type PeerInfoData = {
|
|
494
|
+
did: string;
|
|
495
|
+
endpoint: string;
|
|
496
|
+
groups: Array<RemoteGroupInfoData>;
|
|
497
|
+
};
|
|
498
|
+
/**
|
|
499
|
+
* Per-model outcome of an outbound share, mirroring the `CircleModelAccess`
|
|
500
|
+
* GraphQL enum. Reported only for the models the user explicitly listed in
|
|
501
|
+
* `send.models`. High-level ops write only fresh rows — an existing default is
|
|
502
|
+
* reported, never widened.
|
|
503
|
+
*/
|
|
504
|
+
export type CircleModelAccess = 'MODEL_GRANTED' | 'MODEL_PRE_EXISTING' | 'DOCUMENT_ONLY';
|
|
505
|
+
export type ModelShareResultData = {
|
|
506
|
+
modelID: string;
|
|
507
|
+
access: CircleModelAccess;
|
|
508
|
+
};
|
|
509
|
+
export type SharePeerGroupParams = {
|
|
510
|
+
peerDID: string;
|
|
511
|
+
/** Share an existing local group. Mutually exclusive with `name`. */
|
|
512
|
+
groupID?: string | null;
|
|
513
|
+
/** Create a new group with this name. Mutually exclusive with `groupID`. */
|
|
514
|
+
name?: string | null;
|
|
515
|
+
/**
|
|
516
|
+
* The user's exact model selection from the share dialog. `null` = no send
|
|
517
|
+
* leg: no catalog is created and no default rows are written (plumbing only).
|
|
518
|
+
* An entry per listed model is returned in `modelAccess`.
|
|
519
|
+
*/
|
|
520
|
+
sendModels?: Array<string> | null;
|
|
521
|
+
/**
|
|
522
|
+
* Opt the sharer into pulling the share scope: the share circle's catalog is
|
|
523
|
+
* activated (created active with a fresh `send`, or an existing catalog flipped
|
|
524
|
+
* active when re-sharing an already-configured group). Default `false` — the
|
|
525
|
+
* catalog is created known and the sharer configures pulling later.
|
|
526
|
+
*/
|
|
527
|
+
receiveActivate: boolean;
|
|
528
|
+
/**
|
|
529
|
+
* Write the owner's read model-default naming the share circle, through the
|
|
530
|
+
* engine's owner-signed access-default executor. Bound by the resolver to
|
|
531
|
+
* `context.executeSetModelAccessDefaults` so `engine:access-default:set` fires
|
|
532
|
+
* and broadcast/invite seeding stay consistent. Called only for MODEL_GRANTED.
|
|
533
|
+
*/
|
|
534
|
+
setReadDefaultForCircle: (modelID: string, circleID: string) => Promise<void>;
|
|
535
|
+
};
|
|
536
|
+
export type SharePeerGroupData = {
|
|
537
|
+
group: GroupData;
|
|
538
|
+
modelAccess: Array<ModelShareResultData>;
|
|
539
|
+
/** The share circle's catalog, or null when no `send` leg created/reused one. */
|
|
540
|
+
catalogID: string | null;
|
|
541
|
+
};
|
|
542
|
+
export type AdmitJoinRequestParams = {
|
|
543
|
+
/** The group to admit into. It must already exist on this device. */
|
|
544
|
+
groupID: string;
|
|
545
|
+
/**
|
|
546
|
+
* The join request its author emitted, verbatim. UNTRUSTED: nothing signed the
|
|
547
|
+
* hand-off, so the joiner's DID comes from the KeyPackage credential and the
|
|
548
|
+
* payload's own `did` field is only cross-checked against it.
|
|
549
|
+
*/
|
|
550
|
+
joinRequest: string;
|
|
551
|
+
/** See {@link SharePeerGroupParams.sendModels}. */
|
|
552
|
+
sendModels?: Array<string> | null;
|
|
553
|
+
/** See {@link SharePeerGroupParams.receiveActivate}. */
|
|
554
|
+
receiveActivate: boolean;
|
|
555
|
+
/** See {@link SharePeerGroupParams.setReadDefaultForCircle}. */
|
|
556
|
+
setReadDefaultForCircle: (modelID: string, circleID: string) => Promise<void>;
|
|
557
|
+
};
|
|
558
|
+
export type AdmitJoinRequestData = SharePeerGroupData & {
|
|
559
|
+
/** The admitted DID, derived from the join request's KeyPackage credential. */
|
|
560
|
+
peerDID: string;
|
|
561
|
+
/**
|
|
562
|
+
* The invite the joiner completes with. Carries the designation above, so a
|
|
563
|
+
* joiner that completes it lands in the share circle already.
|
|
564
|
+
*/
|
|
565
|
+
invitePayload: string;
|
|
566
|
+
};
|
|
567
|
+
export type JoinPeerGroupParams = {
|
|
568
|
+
peerDID: string;
|
|
569
|
+
/** The group to join, as discovered from the peer's `connectPeer` group list. */
|
|
570
|
+
groupID: string;
|
|
571
|
+
};
|
|
572
|
+
/** The read access-rule fields a `setCircleSync` add/remove writes for a model. */
|
|
573
|
+
export type SetReadDefaultParams = {
|
|
574
|
+
modelID: string;
|
|
575
|
+
accessLevel: AccessLevel;
|
|
576
|
+
allowedDIDs: Array<string> | null;
|
|
577
|
+
allowedCircles: Array<string> | null;
|
|
578
|
+
allowedGroups: Array<string> | null;
|
|
579
|
+
};
|
|
580
|
+
export type SetCircleSyncParams = {
|
|
581
|
+
circleID: string;
|
|
582
|
+
/**
|
|
583
|
+
* `true` activates every catalog in the circle; `false` deactivates them all;
|
|
584
|
+
* `null` leaves activation untouched. Purely local — no entry, no broadcast.
|
|
585
|
+
*/
|
|
586
|
+
pull: boolean | null;
|
|
587
|
+
/**
|
|
588
|
+
* The caller's desired sharing END STATE into this circle. `{ models }` makes
|
|
589
|
+
* the caller's read defaults naming the circle become EXACTLY these models
|
|
590
|
+
* (`[]` stops sharing entirely); `null` leaves sharing untouched.
|
|
591
|
+
*/
|
|
592
|
+
share: {
|
|
593
|
+
models: Array<string>;
|
|
594
|
+
} | null;
|
|
595
|
+
/**
|
|
596
|
+
* Owner-signed read access-default writer. Bound by the resolver to
|
|
597
|
+
* `context.executeSetModelAccessDefaults` (permissionType `read`) so
|
|
598
|
+
* `engine:access-default:set` fires and the broadcast/push seeding stay
|
|
599
|
+
* consistent. Only ever writes the caller's own rows.
|
|
600
|
+
*/
|
|
601
|
+
setReadDefault: (params: SetReadDefaultParams) => Promise<void>;
|
|
602
|
+
/**
|
|
603
|
+
* Owner-signed read access-default remover. Bound by the resolver to
|
|
604
|
+
* `context.executeRemoveModelAccessDefaults(modelID, ['read'])` so
|
|
605
|
+
* `engine:access-default:removed` fires and the removal broadcasts. Used when
|
|
606
|
+
* dropping the circle empties a restricted rule of every grantee.
|
|
607
|
+
*/
|
|
608
|
+
removeReadDefault: (modelID: string) => Promise<void>;
|
|
609
|
+
};
|
|
610
|
+
/**
|
|
611
|
+
* A circle the device can reach, carrying THIS device's sync state for it.
|
|
612
|
+
* Computed from local state alone (no wire entry, no network round-trip), and by
|
|
613
|
+
* one shared helper, so the join-time picker, the `circleSync` query, and
|
|
614
|
+
* `setCircleSync`'s post-state cannot drift from one another.
|
|
615
|
+
*/
|
|
616
|
+
export type CircleSyncStateData = {
|
|
617
|
+
id: string;
|
|
618
|
+
groupID: string;
|
|
619
|
+
name: string;
|
|
620
|
+
description: string;
|
|
621
|
+
/** The circle's catalog IDs (the scope a pull activates together). */
|
|
622
|
+
catalogIDs: Array<string>;
|
|
623
|
+
/** Admin-authored: every group member reaches an open circle implicitly. */
|
|
624
|
+
open: boolean;
|
|
625
|
+
/**
|
|
626
|
+
* True only when the circle has at least one catalog AND every one is active
|
|
627
|
+
* on this device — the post-state of a pull that activates all the circle's
|
|
628
|
+
* catalogs together (and a pull-off that deactivates them all inverts). A
|
|
629
|
+
* zero-catalog circle has nothing to pull, so it is false.
|
|
630
|
+
*/
|
|
631
|
+
pulling: boolean;
|
|
632
|
+
/**
|
|
633
|
+
* Models this device shares INTO the circle: its OWN read access-default rows
|
|
634
|
+
* whose `allowedCircles` names this circle. Write defaults are excluded.
|
|
635
|
+
*/
|
|
636
|
+
sharing: Array<string>;
|
|
637
|
+
};
|
|
638
|
+
export type JoinPeerGroupData = {
|
|
639
|
+
group: GroupData;
|
|
640
|
+
/**
|
|
641
|
+
* Catalogs seeded (or already known) for this group — the group's circles'
|
|
642
|
+
* catalog IDs present locally. Apps render the receive side of the share
|
|
643
|
+
* settings from these; they are activated only behind `receiveActivate`.
|
|
644
|
+
*/
|
|
645
|
+
knownCatalogIDs: Array<string>;
|
|
646
|
+
/**
|
|
647
|
+
* True when this device already held the full local group state (MLS state +
|
|
648
|
+
* own membership row) and the dance was skipped entirely, so the app can
|
|
649
|
+
* inform the user the group was already joined.
|
|
650
|
+
*/
|
|
651
|
+
alreadyJoined: boolean;
|
|
652
|
+
/**
|
|
653
|
+
* The group's OPEN circles with this device's current `pulling` / `sharing`,
|
|
654
|
+
* so the app can render an opt-in picker. Open circles only — a closed circle
|
|
655
|
+
* the device holds a grant for is not a user opt-in and is excluded.
|
|
656
|
+
*/
|
|
657
|
+
openCircles: Array<CircleSyncStateData>;
|
|
658
|
+
};
|
|
659
|
+
export type PeerRequestContext = {
|
|
660
|
+
/**
|
|
661
|
+
* Discover a peer at `url` (GET `/info`), validate it advertises the sync +
|
|
662
|
+
* peer protocols, open an Enkaku peer client with the engine identity, fetch
|
|
663
|
+
* the remote DID + groups via `peer/groups`, and hold the connection in
|
|
664
|
+
* memory keyed by the remote DID. Persists nothing durable.
|
|
665
|
+
*/
|
|
666
|
+
connect: (url: string) => Promise<PeerInfoData>;
|
|
667
|
+
/**
|
|
668
|
+
* Compose the outbound share. Plumbing always: create/reuse a group, a share
|
|
669
|
+
* circle scoped to both DIDs, run the MLS invite dance over the held peer
|
|
670
|
+
* connection, register the sync peer. Access legs only from an explicit
|
|
671
|
+
* `sendModels`: a catalog over exactly those models plus fresh read model
|
|
672
|
+
* defaults naming the share circle (activated per `receiveActivate`). No send
|
|
673
|
+
* = no catalog, no rows. Every step is idempotent so a retry after a mid-flow
|
|
674
|
+
* failure no-ops the completed steps.
|
|
675
|
+
*/
|
|
676
|
+
sharePeerGroup: (params: SharePeerGroupParams) => Promise<SharePeerGroupData>;
|
|
677
|
+
/**
|
|
678
|
+
* Admit the author of a join request into an existing group from local state
|
|
679
|
+
* alone — no peer connection is opened, read, or required. Designates the
|
|
680
|
+
* joiner's access exactly as `sharePeerGroup` does (the same function), then
|
|
681
|
+
* mints the invite that snapshots it, for the operator to carry to the joiner
|
|
682
|
+
* by whatever means it has.
|
|
683
|
+
*
|
|
684
|
+
* The admitted DID comes from the join request's KeyPackage credential, and a
|
|
685
|
+
* payload whose plaintext `did` disagrees with it is refused. A DID already in
|
|
686
|
+
* the MLS roster is also refused — never evicted, which is what the connected
|
|
687
|
+
* path does on the strength of a signature this one does not have.
|
|
688
|
+
*/
|
|
689
|
+
admitJoinRequest: (params: AdmitJoinRequestParams) => Promise<AdmitJoinRequestData>;
|
|
690
|
+
/**
|
|
691
|
+
* Compose the inbound join. Plumbing only: mint a KeyPackage bundle, pull an
|
|
692
|
+
* invite from the held peer via the `group/invite` procedure, complete the
|
|
693
|
+
* MLS join (seeding ledger/circles/access-defaults/catalogs from the invite
|
|
694
|
+
* payload as KNOWN), register the sync peer, and trigger a ledger-catchup
|
|
695
|
+
* backstop. It writes no access rows, activates nothing, and runs no sync —
|
|
696
|
+
* the user opts into what to send/receive afterward via the explicit surfaces.
|
|
697
|
+
* The dance is skipped (and `alreadyJoined` reported) only when the device
|
|
698
|
+
* holds the full local group state — MLS state + own membership row; every
|
|
699
|
+
* step is idempotent so a retry re-runs the legs as no-ops and heals a partial
|
|
700
|
+
* run.
|
|
701
|
+
*/
|
|
702
|
+
joinPeerGroup: (params: JoinPeerGroupParams) => Promise<JoinPeerGroupData>;
|
|
703
|
+
/**
|
|
704
|
+
* Apply one circle's desired sync end state idempotently, gated on the
|
|
705
|
+
* caller's own access to the circle. `pull` toggles local catalog activation;
|
|
706
|
+
* `share` makes the caller's sharing into the circle become EXACTLY
|
|
707
|
+
* `share.models` via a read-modify-write diff over the caller's own read
|
|
708
|
+
* defaults. Local pull first, then the share rows, then a delivery-guarantee
|
|
709
|
+
* push of the shared models' SET rows to the group's connected in-group peers.
|
|
710
|
+
* Every step is idempotent so a partial run heals on retry.
|
|
711
|
+
*/
|
|
712
|
+
setCircleSync: (params: SetCircleSyncParams) => Promise<CircleSyncStateData>;
|
|
713
|
+
/**
|
|
714
|
+
* Every circle in the group this device can currently reach — explicitly or
|
|
715
|
+
* through the open arm — with this device's `pulling` / `sharing` for each. The
|
|
716
|
+
* read path behind the join-time picker: an app re-reads it to pick up a circle
|
|
717
|
+
* the admin opened or closed since the join.
|
|
718
|
+
*/
|
|
719
|
+
listCircleSyncStates: (groupID: string) => Promise<Array<CircleSyncStateData>>;
|
|
204
720
|
};
|
|
205
721
|
export type P2PJoinRequestContext = {
|
|
206
722
|
prepareRequest: () => Promise<{
|
|
@@ -291,6 +807,7 @@ export type P2PContext = {
|
|
|
291
807
|
join: P2PJoinRequestContext;
|
|
292
808
|
hub: HubRequestContext;
|
|
293
809
|
delegation: DelegationRequestContext;
|
|
810
|
+
peer: PeerRequestContext;
|
|
294
811
|
};
|
|
295
812
|
declare module '@kubun/graphql' {
|
|
296
813
|
interface PluginContextMap {
|
|
@@ -326,12 +843,87 @@ export type GroupHealthSnapshot = {
|
|
|
326
843
|
export type RejoinResult = {
|
|
327
844
|
status: 'recovered' | 'recovery-failed';
|
|
328
845
|
};
|
|
846
|
+
/** Tuning for a ledger catch-up gather round-trip. */
|
|
847
|
+
export type LedgerCatchupOptions = {
|
|
848
|
+
/**
|
|
849
|
+
* Per-hub gather timeout in milliseconds. Omitted uses the broadcast client's
|
|
850
|
+
* own default. The gather always waits the full window unless `quorum` caps it.
|
|
851
|
+
*/
|
|
852
|
+
timeoutMs?: number;
|
|
853
|
+
/** Resolve a hub's gather as soon as this many replies arrive. */
|
|
854
|
+
quorum?: number;
|
|
855
|
+
};
|
|
856
|
+
/** What a ledger catch-up round-trip folded into the local ledger. */
|
|
857
|
+
export type LedgerCatchupSummary = {
|
|
858
|
+
/**
|
|
859
|
+
* Size of the ledger adopted from the one accepted reply.
|
|
860
|
+
*
|
|
861
|
+
* NOT a count of what was new: a gather installs a whole ledger over whatever
|
|
862
|
+
* was held, so "new" is not a question it can answer — re-running a catch-up
|
|
863
|
+
* that changes nothing still reports the full size. Zero means no responder's
|
|
864
|
+
* ledger was accepted, which `responders` distinguishes from no responder at
|
|
865
|
+
* all.
|
|
866
|
+
*/
|
|
867
|
+
applied: number;
|
|
868
|
+
/** Entries dropped because their signed token failed verification. */
|
|
869
|
+
rejected: number;
|
|
870
|
+
/** Replies considered, refused ones included (a member on two hubs counts twice). */
|
|
871
|
+
responders: number;
|
|
872
|
+
};
|
|
873
|
+
/**
|
|
874
|
+
* What this device says about itself on a group's peer lane. No DID: the
|
|
875
|
+
* announcer is the frame's MLS-authenticated sender, resolved by the receiver's
|
|
876
|
+
* own lane, so there is nothing here for a caller to claim to be.
|
|
877
|
+
*/
|
|
878
|
+
export type LocalPeerAnnouncement = {
|
|
879
|
+
/** Display hint only — a device name, never an identity. */
|
|
880
|
+
label: string;
|
|
881
|
+
availability: PeerAvailability;
|
|
882
|
+
capabilities: Array<PeerCapability>;
|
|
883
|
+
};
|
|
884
|
+
/**
|
|
885
|
+
* One device that ANSWERED a gather, inside the window. Not a member list: a
|
|
886
|
+
* co-member that was asleep is absent from this and keeps its projection row.
|
|
887
|
+
*/
|
|
888
|
+
export type GatheredPeer = LocalPeerAnnouncement & {
|
|
889
|
+
/** The MLS-authenticated responder. Never anything the reply body claimed. */
|
|
890
|
+
peerDID: string;
|
|
891
|
+
/** The responder's own stamp, the same one its projection row is keyed by. */
|
|
892
|
+
hlc: string;
|
|
893
|
+
};
|
|
894
|
+
export type PeerGatherOptions = {
|
|
895
|
+
/**
|
|
896
|
+
* Only devices answering one of these protocols reply. Omitted means any.
|
|
897
|
+
* Filtering at the responder is what bounds a gather in a large group — and it
|
|
898
|
+
* is what makes a future WebRTC query return the offer-capable subset for free.
|
|
899
|
+
*/
|
|
900
|
+
protocols?: Array<string>;
|
|
901
|
+
/** How long to hold the window open. The gather always costs its full window unless `quorum` closes it. */
|
|
902
|
+
timeoutMs?: number;
|
|
903
|
+
/**
|
|
904
|
+
* Close a hub's window as soon as this many distinct devices have answered
|
|
905
|
+
* SUCCESSFULLY — a declined reply is dropped before it is counted, so a target
|
|
906
|
+
* that includes a device which cannot answer is a target that is never met.
|
|
907
|
+
* Counted per hub, then de-duplicated by DID across hubs afterwards.
|
|
908
|
+
*
|
|
909
|
+
* Omitted means every device this group's projection holds that advertises a
|
|
910
|
+
* matching capability, so a gather ends when the last one that could answer
|
|
911
|
+
* has. Pass a number to ask for less.
|
|
912
|
+
*/
|
|
913
|
+
quorum?: number;
|
|
914
|
+
};
|
|
915
|
+
/**
|
|
916
|
+
* Why a presence announce/gather ran. Log-only: it names the trigger for a reader
|
|
917
|
+
* of the logs, and nothing branches on it.
|
|
918
|
+
*/
|
|
919
|
+
export type PresenceReason = 'hub-connected' | 'epoch-changed' | 'app-window-pruned' | 'requested';
|
|
329
920
|
export type SyncPluginAPI = {
|
|
330
921
|
/** Resolves when the HTTP sync transport is registered (only present when http option is enabled). */
|
|
331
922
|
syncReady?: Promise<void>;
|
|
332
923
|
/**
|
|
333
|
-
* Resolves when hub relay setup completes.
|
|
334
|
-
*
|
|
924
|
+
* Resolves when hub relay setup completes. The relay always runs — it is what
|
|
925
|
+
* gives each joined group its commit lane — so this resolves regardless of
|
|
926
|
+
* whether the `hub` plugin option is set.
|
|
335
927
|
*/
|
|
336
928
|
hubReady: Promise<void>;
|
|
337
929
|
addPeer(config: PeerConfig): Promise<void>;
|
|
@@ -345,7 +937,86 @@ export type SyncPluginAPI = {
|
|
|
345
937
|
direction?: SyncDirection;
|
|
346
938
|
}): Promise<SyncSessionResult>;
|
|
347
939
|
getStatus(peerDID?: string): SyncStatus;
|
|
940
|
+
/**
|
|
941
|
+
* Catch up with the best candidate one group's discovery projection offers, over
|
|
942
|
+
* the local device's active-catalog scopes. See
|
|
943
|
+
* {@link SyncRequestContext.catchUpWithBestPeer} — the host decides when, this
|
|
944
|
+
* decides who, and an explicit `syncPeer` ignores the ranking entirely.
|
|
945
|
+
*/
|
|
946
|
+
catchUpWithBestPeer(groupID: string): Promise<PeerCatchUpData>;
|
|
947
|
+
/**
|
|
948
|
+
* Every device this one has heard announce in the group — the projection, and
|
|
949
|
+
* never a liveness answer. {@link SyncPluginAPI.gatherPeers} is what says who
|
|
950
|
+
* is reachable now; a screen needs both, and the two are different questions.
|
|
951
|
+
*/
|
|
952
|
+
listPeerDevices(groupID: string): Promise<Array<PeerDeviceData>>;
|
|
348
953
|
onSyncEvent(callback: (event: SyncEvent) => void): () => void;
|
|
954
|
+
/**
|
|
955
|
+
* Subscribe to hub identity changes. A hub whose signing key rotated is
|
|
956
|
+
* reconnected to under its new DID without the application doing anything —
|
|
957
|
+
* this is how it learns that happened. Returns the unsubscribe.
|
|
958
|
+
*/
|
|
959
|
+
onHubServerDIDChanged(listener: (data: HubServerDIDChangedData) => void): () => void;
|
|
349
960
|
/** Create a sync-specific transport for direct in-process connections. */
|
|
350
961
|
createSyncTransport(signal?: AbortSignal): ClientTransportOf<SyncProtocol>;
|
|
962
|
+
/**
|
|
963
|
+
* Ask the group's current members for the full control ledger and fold any
|
|
964
|
+
* missing entries locally. Backfills entries an invite snapshot missed (or a
|
|
965
|
+
* member missed while its peer was offline); every token self-authenticates,
|
|
966
|
+
* so the responders are untrusted and duplicates are idempotent no-ops. A
|
|
967
|
+
* no-op (all-zero summary) when no `hub` option is configured.
|
|
968
|
+
*/
|
|
969
|
+
requestLedgerCatchup(groupID: string, options?: LedgerCatchupOptions): Promise<LedgerCatchupSummary>;
|
|
970
|
+
/**
|
|
971
|
+
* Declare what this device IS — its display label, its availability class and
|
|
972
|
+
* the capabilities it answers — and advertise it to every group it belongs to.
|
|
973
|
+
* THE one way to advertise: there is no per-group or bodies-only variant.
|
|
974
|
+
*
|
|
975
|
+
* This is the "local capability or label change" trigger: the host calls it when
|
|
976
|
+
* either changes, and every group hears the new shape without the host tracking
|
|
977
|
+
* which groups exist. The profile is durable, so every other trigger (a hub
|
|
978
|
+
* connect, an epoch rotation, a pruned app window) has something to announce
|
|
979
|
+
* after a restart. A device that never calls this advertises NOTHING — no label
|
|
980
|
+
* is invented for it, and it stays invisible to its co-members' discovery.
|
|
981
|
+
*
|
|
982
|
+
* Resolves once each group's frame is sealed and sent. A group with no live hub
|
|
983
|
+
* peer is a no-op rather than a throw, matching every other broadcast.
|
|
984
|
+
*/
|
|
985
|
+
setLocalPeerProfile(profile: LocalPeerAnnouncement): Promise<void>;
|
|
986
|
+
/** What this device last declared itself to be, or `null` if it never has. */
|
|
987
|
+
getLocalPeerProfile(): Promise<LocalPeerAnnouncement | null>;
|
|
988
|
+
/**
|
|
989
|
+
* Ask a group's members who is reachable RIGHT NOW, optionally narrowed to
|
|
990
|
+
* devices answering named protocols, and fold every answer into the projection.
|
|
991
|
+
*
|
|
992
|
+
* THE RESULT IS "WHO ANSWERED WITHIN THE WINDOW", NEVER "WHO EXISTS". A device
|
|
993
|
+
* that was asleep is absent from it and keeps its projection row untouched;
|
|
994
|
+
* only a roster removal deletes a row. Surfacing this as a member list is how
|
|
995
|
+
* an empty result comes to read as "my laptop was removed from the group".
|
|
996
|
+
*/
|
|
997
|
+
gatherPeers(groupID: string, options?: PeerGatherOptions): Promise<Array<GatheredPeer>>;
|
|
998
|
+
/**
|
|
999
|
+
* Announce this device and gather its co-members, in that order — what a host
|
|
1000
|
+
* calls when it comes to the foreground. The same pair the hub-connect and
|
|
1001
|
+
* pruned-window triggers run internally.
|
|
1002
|
+
*/
|
|
1003
|
+
refreshPeerPresence(groupID: string, options?: PeerGatherOptions): Promise<Array<GatheredPeer>>;
|
|
1004
|
+
/**
|
|
1005
|
+
* Try this device's hubs now instead of waiting out the reconnect backoff, and
|
|
1006
|
+
* answer with whether any is connected afterwards.
|
|
1007
|
+
*
|
|
1008
|
+
* The backoff climbs to half a minute, which is the right cost for a device
|
|
1009
|
+
* nobody is watching and the wrong one for a person who has just started their
|
|
1010
|
+
* hub and is looking at the screen. Wire this to whatever that person presses.
|
|
1011
|
+
* `false` means the attempt was made and did not connect — the automatic
|
|
1012
|
+
* retries carry on either way.
|
|
1013
|
+
*/
|
|
1014
|
+
retryHubConnection(): Promise<boolean>;
|
|
1015
|
+
/**
|
|
1016
|
+
* Look up the live in-memory connection to a peer, keyed by remote DID.
|
|
1017
|
+
* Present only after a successful `connectPeer`; the later dance ops
|
|
1018
|
+
* (`joinPeerGroup` / `sharePeerGroup`) dispatch their peer procedures over
|
|
1019
|
+
* this held connection.
|
|
1020
|
+
*/
|
|
1021
|
+
getPeerConnection(peerDID: string): PeerConnection | undefined;
|
|
351
1022
|
};
|