@kubun/plugin-p2p 0.10.1 → 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.
Files changed (201) hide show
  1. package/lib/context/delegation.js +130 -1
  2. package/lib/context/group.js +1646 -1
  3. package/lib/context/hub.js +151 -1
  4. package/lib/context/join.js +115 -1
  5. package/lib/context/peer.d.ts +92 -0
  6. package/lib/context/peer.js +1216 -0
  7. package/lib/context/require-admin.d.ts +57 -0
  8. package/lib/context/require-admin.js +57 -0
  9. package/lib/context/sync.d.ts +12 -0
  10. package/lib/context/sync.js +252 -1
  11. package/lib/context/types.d.ts +58 -22
  12. package/lib/context/types.js +48 -1
  13. package/lib/groups/access-default-token.d.ts +44 -0
  14. package/lib/groups/access-default-token.js +103 -0
  15. package/lib/groups/admin-roster.d.ts +55 -0
  16. package/lib/groups/admin-roster.js +69 -0
  17. package/lib/groups/anchor-store.d.ts +15 -0
  18. package/lib/groups/anchor-store.js +37 -0
  19. package/lib/groups/app-cursor-store.d.ts +17 -0
  20. package/lib/groups/app-cursor-store.js +34 -0
  21. package/lib/groups/broadcast-codec.js +6 -1
  22. package/lib/groups/broadcast.d.ts +238 -106
  23. package/lib/groups/broadcast.js +702 -1
  24. package/lib/groups/catalog-token.d.ts +50 -0
  25. package/lib/groups/catalog-token.js +96 -0
  26. package/lib/groups/circle-projection.d.ts +90 -0
  27. package/lib/groups/circle-projection.js +202 -0
  28. package/lib/groups/circle-reducers.d.ts +139 -0
  29. package/lib/groups/circle-reducers.js +128 -0
  30. package/lib/groups/commit-adoption.d.ts +77 -0
  31. package/lib/groups/commit-adoption.js +108 -0
  32. package/lib/groups/commit-journal.d.ts +14 -0
  33. package/lib/groups/commit-journal.js +65 -0
  34. package/lib/groups/control-request.d.ts +66 -0
  35. package/lib/groups/control-request.js +101 -0
  36. package/lib/groups/events.d.ts +19 -2
  37. package/lib/groups/events.js +10 -1
  38. package/lib/groups/group-anchor.d.ts +24 -0
  39. package/lib/groups/group-anchor.js +22 -0
  40. package/lib/groups/group-crypto.d.ts +42 -0
  41. package/lib/groups/group-crypto.js +159 -0
  42. package/lib/groups/group-handle-registry.d.ts +138 -9
  43. package/lib/groups/group-handle-registry.js +533 -1
  44. package/lib/groups/group-handlers.d.ts +30 -0
  45. package/lib/groups/group-handlers.js +218 -0
  46. package/lib/groups/group-health-monitor.d.ts +11 -0
  47. package/lib/groups/group-health-monitor.js +139 -1
  48. package/lib/groups/group-mls.d.ts +66 -0
  49. package/lib/groups/group-mls.js +696 -0
  50. package/lib/groups/group-peer-manager.d.ts +301 -0
  51. package/lib/groups/group-peer-manager.js +1336 -0
  52. package/lib/groups/group-protocols.d.ts +754 -0
  53. package/lib/groups/group-protocols.js +448 -0
  54. package/lib/groups/invite-payload.d.ts +49 -1
  55. package/lib/groups/invite-payload.js +26 -1
  56. package/lib/groups/join-request-identity.d.ts +21 -0
  57. package/lib/groups/join-request-identity.js +43 -0
  58. package/lib/groups/join-utils.d.ts +61 -0
  59. package/lib/groups/join-utils.js +366 -1
  60. package/lib/groups/ledger-affected-events.d.ts +20 -0
  61. package/lib/groups/ledger-affected-events.js +85 -0
  62. package/lib/groups/ledger-commit-fold.d.ts +107 -0
  63. package/lib/groups/ledger-commit-fold.js +167 -0
  64. package/lib/groups/ledger-ingest.d.ts +124 -0
  65. package/lib/groups/ledger-ingest.js +212 -0
  66. package/lib/groups/ledger.d.ts +44 -0
  67. package/lib/groups/ledger.js +30 -0
  68. package/lib/groups/manager.d.ts +217 -85
  69. package/lib/groups/manager.js +798 -1
  70. package/lib/groups/mls-codec.d.ts +28 -7
  71. package/lib/groups/mls-codec.js +33 -1
  72. package/lib/groups/mls-encryptor.d.ts +2 -2
  73. package/lib/groups/mls-encryptor.js +33 -1
  74. package/lib/groups/mls-group-handle.d.ts +37 -2
  75. package/lib/groups/mls-group-handle.js +18 -1
  76. package/lib/groups/mls-json.js +17 -1
  77. package/lib/groups/mls-receive-errors.d.ts +27 -0
  78. package/lib/groups/mls-receive-errors.js +39 -0
  79. package/lib/groups/mls-state.d.ts +6 -4
  80. package/lib/groups/mls-state.js +79 -1
  81. package/lib/groups/peer-presence.d.ts +90 -0
  82. package/lib/groups/peer-presence.js +295 -0
  83. package/lib/groups/peer-selection.d.ts +37 -0
  84. package/lib/groups/peer-selection.js +72 -0
  85. package/lib/groups/roster-projection.d.ts +53 -0
  86. package/lib/groups/roster-projection.js +65 -0
  87. package/lib/groups/store-received-grant.d.ts +7 -3
  88. package/lib/groups/store-received-grant.js +161 -1
  89. package/lib/groups/store-received-revocation.d.ts +19 -16
  90. package/lib/groups/store-received-revocation.js +83 -1
  91. package/lib/hub/ack-backoff.d.ts +22 -0
  92. package/lib/hub/ack-backoff.js +88 -0
  93. package/lib/hub/http-client.d.ts +18 -10
  94. package/lib/hub/http-client.js +15 -1
  95. package/lib/hub/hub-like.d.ts +75 -0
  96. package/lib/hub/hub-like.js +1022 -0
  97. package/lib/hub/loopback-log-hub.d.ts +20 -0
  98. package/lib/hub/loopback-log-hub.js +178 -0
  99. package/lib/hub/manager.d.ts +25 -40
  100. package/lib/hub/manager.js +155 -1
  101. package/lib/hub/peer-scoped-hub-view.d.ts +35 -0
  102. package/lib/hub/peer-scoped-hub-view.js +92 -0
  103. package/lib/hub/sender-scoped-hub-view.d.ts +41 -0
  104. package/lib/hub/sender-scoped-hub-view.js +59 -0
  105. package/lib/hub/server-did.d.ts +67 -0
  106. package/lib/hub/server-did.js +131 -0
  107. package/lib/hub/wiring.d.ts +77 -51
  108. package/lib/hub/wiring.js +189 -1
  109. package/lib/index.d.ts +58 -22
  110. package/lib/index.js +563 -1
  111. package/lib/peer/connection-registry.d.ts +45 -0
  112. package/lib/peer/connection-registry.js +52 -0
  113. package/lib/peer/handlers.d.ts +69 -0
  114. package/lib/peer/handlers.js +110 -0
  115. package/lib/protocol.d.ts +214 -1
  116. package/lib/protocol.js +519 -1
  117. package/lib/schema.d.ts +3 -1
  118. package/lib/schema.js +1150 -39
  119. package/lib/sync/access-default-sender.d.ts +23 -0
  120. package/lib/sync/access-default-sender.js +80 -0
  121. package/lib/sync/authorize.d.ts +58 -4
  122. package/lib/sync/authorize.js +114 -1
  123. package/lib/sync/broadcast-queue.d.ts +3 -4
  124. package/lib/sync/broadcast-queue.js +182 -1
  125. package/lib/sync/broadcast-sender.js +179 -1
  126. package/lib/sync/catalog-scope.d.ts +8 -7
  127. package/lib/sync/catalog-scope.js +84 -1
  128. package/lib/sync/errors.d.ts +12 -2
  129. package/lib/sync/errors.js +29 -1
  130. package/lib/sync/forwarder.d.ts +13 -19
  131. package/lib/sync/forwarder.js +271 -1
  132. package/lib/sync/handlers.js +200 -1
  133. package/lib/sync/hub-tunnel-sync-listener.d.ts +19 -5
  134. package/lib/sync/hub-tunnel-sync-listener.js +261 -1
  135. package/lib/sync/hub-tunnel-sync-provider.d.ts +34 -14
  136. package/lib/sync/hub-tunnel-sync-provider.js +113 -1
  137. package/lib/sync/merkle-apply.d.ts +5 -0
  138. package/lib/sync/merkle-apply.js +194 -1
  139. package/lib/sync/merkle-channel.d.ts +11 -3
  140. package/lib/sync/merkle-channel.js +0 -0
  141. package/lib/sync/merkle-tree.js +120 -1
  142. package/lib/sync/peer-registry.d.ts +15 -11
  143. package/lib/sync/peer-registry.js +82 -1
  144. package/lib/sync/receive-access-gate.js +24 -1
  145. package/lib/sync/scope-resolver.js +41 -1
  146. package/lib/sync/sync-client.d.ts +14 -4
  147. package/lib/sync/sync-client.js +254 -1
  148. package/lib/sync/sync-manager.d.ts +24 -6
  149. package/lib/sync/sync-manager.js +301 -1
  150. package/lib/sync/tunnel-listeners.d.ts +52 -0
  151. package/lib/sync/tunnel-listeners.js +175 -0
  152. package/lib/sync/tunnel-observability.d.ts +25 -0
  153. package/lib/sync/tunnel-observability.js +63 -0
  154. package/lib/sync/tunnel-topics.d.ts +20 -0
  155. package/lib/sync/tunnel-topics.js +28 -0
  156. package/lib/types.d.ts +704 -22
  157. package/lib/types.js +1 -1
  158. package/lib/util/handler-error.d.ts +16 -0
  159. package/lib/util/handler-error.js +64 -0
  160. package/lib/util/mutex.d.ts +14 -1
  161. package/lib/util/mutex.js +44 -1
  162. package/lib/utils.js +21 -1
  163. package/package.json +58 -47
  164. package/lib/groups/broadcast-service.d.ts +0 -263
  165. package/lib/groups/broadcast-service.js +0 -1
  166. package/lib/groups/rejoin-codec.d.ts +0 -14
  167. package/lib/groups/rejoin-codec.js +0 -1
  168. package/lib/groups/wire-frame.d.ts +0 -34
  169. package/lib/groups/wire-frame.js +0 -1
  170. package/lib/hub/circle-catchup-requester.d.ts +0 -58
  171. package/lib/hub/circle-catchup-requester.js +0 -1
  172. package/lib/hub/circle-catchup-responder.d.ts +0 -50
  173. package/lib/hub/circle-catchup-responder.js +0 -1
  174. package/lib/hub/connection-pool.d.ts +0 -43
  175. package/lib/hub/connection-pool.js +0 -1
  176. package/lib/hub/did-observing-transport.d.ts +0 -64
  177. package/lib/hub/did-observing-transport.js +0 -1
  178. package/lib/hub/epoch-stale-detector.d.ts +0 -18
  179. package/lib/hub/epoch-stale-detector.js +0 -1
  180. package/lib/hub/errors.d.ts +0 -30
  181. package/lib/hub/errors.js +0 -1
  182. package/lib/hub/forward-remote-broadcast.d.ts +0 -15
  183. package/lib/hub/forward-remote-broadcast.js +0 -1
  184. package/lib/hub/group-channel.d.ts +0 -89
  185. package/lib/hub/group-channel.js +0 -1
  186. package/lib/hub/hub-connection.d.ts +0 -96
  187. package/lib/hub/hub-connection.js +0 -1
  188. package/lib/hub/receive-handler.d.ts +0 -51
  189. package/lib/hub/receive-handler.js +0 -1
  190. package/lib/hub/rejoin-manager.d.ts +0 -78
  191. package/lib/hub/rejoin-manager.js +0 -1
  192. package/lib/hub/rejoin-responder.d.ts +0 -32
  193. package/lib/hub/rejoin-responder.js +0 -1
  194. package/lib/hub/relay-manager.d.ts +0 -142
  195. package/lib/hub/relay-manager.js +0 -1
  196. package/lib/hub/send-handler.d.ts +0 -40
  197. package/lib/hub/send-handler.js +0 -1
  198. package/lib/hub/tunnel-inbox.d.ts +0 -20
  199. package/lib/hub/tunnel-inbox.js +0 -1
  200. package/lib/hub/wait-for-gate.d.ts +0 -14
  201. package/lib/hub/wait-for-gate.js +0 -1
package/lib/schema.js CHANGED
@@ -1,4 +1,152 @@
1
- import{createFilteredGenerator as e}from"./groups/events.js";function r(e){if(null==e.p2p)throw Error("p2p plugin is not wired into this context");return e.p2p}let t={healthy:"HEALTHY",degraded:"DEGRADED",recovering:"RECOVERING",recovered:"RECOVERED","recovery-failed":"RECOVERY_FAILED"},o={"epoch-stale":"EPOCH_STALE"};async function*a(){}let i=`
1
+ import { TransactionFatalError } from '@kubun/engine';
2
+ import { PluginNodeID } from '@kubun/id';
3
+ import { getKubunLogger } from '@kubun/logger';
4
+ import { GraphQLError } from 'graphql';
5
+ import { createFilteredGenerator } from './groups/events.js';
6
+ function requireP2P(ctx) {
7
+ if (ctx.p2p == null) {
8
+ throw new Error('p2p plugin is not wired into this context');
9
+ }
10
+ return ctx.p2p;
11
+ }
12
+ /**
13
+ * The read-default writers `setCircleSync` runs its share diff through.
14
+ *
15
+ * Owner-signed via the engine's access-default executors, so
16
+ * `engine:access-default:set` / `:removed` fire and the broadcast + push seeding
17
+ * stay consistent — the same path `setModelAccessDefaults` takes. Shared by every
18
+ * caller that applies a circle's sync state, so two entry points cannot write the
19
+ * same rule two ways.
20
+ */ function accessDefaultExecutors(context) {
21
+ return {
22
+ setReadDefault: async (p)=>{
23
+ await context.executeSetModelAccessDefaults({
24
+ modelID: p.modelID,
25
+ permissionType: 'read',
26
+ accessLevel: p.accessLevel,
27
+ allowedDIDs: p.allowedDIDs,
28
+ allowedCircles: p.allowedCircles,
29
+ allowedGroups: p.allowedGroups
30
+ });
31
+ },
32
+ removeReadDefault: async (modelID)=>{
33
+ await context.executeRemoveModelAccessDefaults(modelID, [
34
+ 'read'
35
+ ]);
36
+ }
37
+ };
38
+ }
39
+ /**
40
+ * Re-raise any failure from a membership/circle operation as the engine's
41
+ * transaction-rollback marker so `mutateGraph` rolls back the whole transaction.
42
+ * These ops do multi-step writes (an MLS ratchet advance plus a roster write, or
43
+ * a ledger append plus a reprojection); graphql-js would otherwise swallow a
44
+ * follow-on throw into `result.errors` and commit a partial result, leaving e.g.
45
+ * a removed member untombstoned and still writable. The original error's message
46
+ * is forwarded unchanged so an expected validation error (e.g. access-denied)
47
+ * still reaches the client as before; the cause carries the full original error.
48
+ *
49
+ * Wrap ONLY multi-write ops whose partial commit would fail OPEN — toward more
50
+ * privilege, leaving a removed or demoted DID authoritative — plus every
51
+ * request-shaped mutation, which enqueues a request row nothing heals. Ops touching
52
+ * benign metadata are left unwrapped.
53
+ *
54
+ * A thrown `GraphQLError`'s `extensions` ARE carried across the re-raise, so a
55
+ * refusal that can only be decided by reading the state it refuses — the last
56
+ * admin leaving, demoting themselves, or being removed — keeps the code an app
57
+ * matches on. An authorization gate still belongs BEFORE the `try`: it reads
58
+ * only, so there is nothing to roll back and nothing to un-write. Use
59
+ * {@link gatedTransaction} rather than calling this directly.
60
+ */ function failTransactionFatal(operation, error) {
61
+ const message = error instanceof Error ? error.message : `${operation} failed`;
62
+ // A typed refusal keeps its code across the re-raise: graphql-js copies
63
+ // `originalError.extensions` onto the GraphQLError it wraps this throw in.
64
+ // Only a check that must READ the state it refuses needs this — a pure
65
+ // authorization gate still belongs before the `try`, where nothing has been
66
+ // written yet (see {@link gatedTransaction}).
67
+ const extensions = error instanceof GraphQLError && Object.keys(error.extensions ?? {}).length > 0 ? error.extensions : undefined;
68
+ throw new TransactionFatalError(message, {
69
+ cause: error,
70
+ ...extensions == null ? {} : {
71
+ extensions
72
+ }
73
+ });
74
+ }
75
+ /**
76
+ * Run an authorization gate and then a rollback-wrapped body, in the only order
77
+ * that preserves a typed refusal.
78
+ *
79
+ * `gate` runs OUTSIDE the `try` because it only reads: there is nothing to roll
80
+ * back, and a refusal that never opened a transaction should not pretend to have
81
+ * one. Taking it as its own argument is what stops a mutation gating inside the
82
+ * wrapper. (A code raised inside `run` reaches the client too — see
83
+ * {@link failTransactionFatal} — so this is about where the refusal belongs,
84
+ * not about whether it survives.)
85
+ */ async function gatedTransaction(operation, gate, run) {
86
+ await gate();
87
+ try {
88
+ return await run();
89
+ } catch (error) {
90
+ failTransactionFatal(operation, error);
91
+ }
92
+ }
93
+ // Internal lowercase-kebab health values -> SDL enum names.
94
+ const HEALTH_STATE_TO_SDL = {
95
+ healthy: 'HEALTHY',
96
+ degraded: 'DEGRADED',
97
+ recovering: 'RECOVERING',
98
+ recovered: 'RECOVERED',
99
+ 'recovery-failed': 'RECOVERY_FAILED'
100
+ };
101
+ const HEALTH_CONDITION_TO_SDL = {
102
+ 'epoch-stale': 'EPOCH_STALE'
103
+ };
104
+ // Internal lowercase control-request values -> SDL enum names.
105
+ const CONTROL_REQUEST_STATUS_TO_SDL = {
106
+ pending: 'PENDING',
107
+ committed: 'COMMITTED',
108
+ rejected: 'REJECTED',
109
+ lost: 'LOST',
110
+ no_op: 'NO_OP'
111
+ };
112
+ const CONTROL_REQUEST_KIND_TO_SDL = {
113
+ ledger: 'LEDGER',
114
+ invite: 'INVITE',
115
+ remove: 'REMOVE'
116
+ };
117
+ /** Project a control request into its SDL shape, mapping both enums to uppercase. */ function toControlRequestSDL(request) {
118
+ return {
119
+ ...request,
120
+ kind: CONTROL_REQUEST_KIND_TO_SDL[request.kind],
121
+ status: CONTROL_REQUEST_STATUS_TO_SDL[request.status]
122
+ };
123
+ }
124
+ /**
125
+ * Deliver one request's terminal status, whenever it happens, then complete.
126
+ *
127
+ * `stream` must already be listening when this is called — the read below can
128
+ * only see a settle that has landed, and the stream can only see one that has
129
+ * not, so between them every outcome is covered exactly once. A settle is
130
+ * final, so the first delivery ends the stream.
131
+ */ async function* watchOneControlRequest(stream, read) {
132
+ try {
133
+ const existing = await read();
134
+ if (existing != null && existing.status !== 'pending') {
135
+ yield existing;
136
+ return;
137
+ }
138
+ for await (const settled of stream){
139
+ yield settled;
140
+ return;
141
+ }
142
+ } finally{
143
+ await stream.return();
144
+ }
145
+ }
146
+ async function* emptyAsyncGenerator() {
147
+ // no-op: closes immediately so subscribers without a viewer get no events
148
+ }
149
+ const sdl = `
2
150
  type PeerGroup {
3
151
  id: ID!
4
152
  name: String!
@@ -9,8 +157,8 @@ type PeerGroup {
9
157
 
10
158
  type PeerGroupMember {
11
159
  groupID: ID!
12
- memberDID: DID!
13
- role: String!
160
+ account: AccountNode!
161
+ role: GroupMemberRole!
14
162
  createdAt: DateTimeISO!
15
163
  }
16
164
 
@@ -19,9 +167,20 @@ type PeerCircle {
19
167
  groupID: ID!
20
168
  name: String!
21
169
  description: String
170
+ """Admin-authored: every group member reaches an open circle implicitly."""
171
+ open: Boolean!
22
172
  createdAt: DateTimeISO!
23
173
  }
24
174
 
175
+ type PeerDevice {
176
+ peerDID: DID!
177
+ """Self-declared display hint, never an identity."""
178
+ label: String!
179
+ availability: String!
180
+ """The announcer's own stamp; the row is last-writer-wins keyed by it."""
181
+ hlc: String!
182
+ }
183
+
25
184
  type PeerCircleMember {
26
185
  circleID: ID!
27
186
  memberDID: DID!
@@ -35,6 +194,9 @@ type PeerCatalog {
35
194
  name: String!
36
195
  description: String
37
196
  filterCriteria: String!
197
+ active: Boolean!
198
+ sourceGroupID: ID
199
+ sourceCircleID: ID
38
200
  createdAt: DateTimeISO!
39
201
  }
40
202
 
@@ -62,8 +224,21 @@ type CreateGroupPayload {
62
224
  group: PeerGroup!
63
225
  }
64
226
 
65
- type InviteToGroupPayload {
66
- invitePayload: String!
227
+ input RequestInviteToGroupInput {
228
+ groupID: ID!
229
+ joinRequest: String!
230
+ grants: [String!]
231
+ revocations: [String!]
232
+ }
233
+
234
+ """
235
+ Waits for its own commit, unlike the other request-shaped mutations. invitePayload
236
+ is present only when status is COMMITTED; PENDING carries none, REJECTED and LOST
237
+ raise. Ack the request once the payload has been passed on.
238
+ """
239
+ type RequestInviteToGroupPayload {
240
+ request: ControlRequest!
241
+ invitePayload: String
67
242
  groupID: ID!
68
243
  groupName: String!
69
244
  }
@@ -84,8 +259,30 @@ type LeaveGroupPayload {
84
259
  groupID: ID!
85
260
  }
86
261
 
87
- type RemoveGroupMemberPayload {
262
+ input RequestRemoveGroupMemberInput {
263
+ groupID: ID!
264
+ memberDID: DID!
265
+ }
266
+
267
+ """The request, not the removal: the tombstone lands with the commit. Watch it."""
268
+ type RequestRemoveGroupMemberPayload {
269
+ request: ControlRequest!
270
+ }
271
+
272
+ enum GroupMemberRole {
273
+ admin
274
+ member
275
+ }
276
+
277
+ input RequestSetMemberRoleInput {
88
278
  groupID: ID!
279
+ memberDID: DID!
280
+ role: GroupMemberRole!
281
+ }
282
+
283
+ """The request, not the role: it moves with the commit. Watch it."""
284
+ type RequestSetMemberRolePayload {
285
+ request: ControlRequest!
89
286
  }
90
287
 
91
288
  input UpdateCatalogInput {
@@ -94,14 +291,49 @@ input UpdateCatalogInput {
94
291
  filterCriteria: String
95
292
  }
96
293
 
97
- input UpdateGroupInput {
294
+ input RequestUpdateGroupInput {
98
295
  groupID: ID!
99
296
  name: String
100
297
  description: String
298
+ openCircleIDs: [String!]
299
+ commitHub: SuggestedHubInput
101
300
  }
102
301
 
103
- type UpdateGroupPayload {
104
- group: PeerGroup!
302
+ """
303
+ The request, not the settings: they move with the commit. Watch it. No field
304
+ beyond groupID settles NO_OP.
305
+ """
306
+ type RequestUpdateGroupPayload {
307
+ request: ControlRequest!
308
+ }
309
+
310
+ input RequestCreateCircleInput {
311
+ groupID: ID!
312
+ name: String!
313
+ description: String
314
+ }
315
+
316
+ """
317
+ The request plus the new circle's id, minted up front so a caller can name the
318
+ circle before the commit enacts it. Not queryable until then; watch the request.
319
+ """
320
+ type RequestCreateCirclePayload {
321
+ request: ControlRequest!
322
+ circleID: ID!
323
+ }
324
+
325
+ input RequestAddCircleMemberInput {
326
+ circleID: ID!
327
+ memberDID: DID!
328
+ role: String!
329
+ }
330
+
331
+ """
332
+ The request, not the membership: the row appears with the commit. Watch it. A DID
333
+ already in the circle at this role settles NO_OP.
334
+ """
335
+ type RequestAddCircleMemberPayload {
336
+ request: ControlRequest!
105
337
  }
106
338
 
107
339
  input UpdateCircleInput {
@@ -111,30 +343,47 @@ input UpdateCircleInput {
111
343
  catalogIDs: [String!]
112
344
  }
113
345
 
114
- type UpdateCirclePayload {
115
- circle: PeerCircle!
346
+ """
347
+ The request, not the definition: it moves with the commit. Watch it. No field
348
+ beyond circleID settles NO_OP.
349
+ """
350
+ type RequestUpdateCirclePayload {
351
+ request: ControlRequest!
116
352
  }
117
353
 
118
354
  type DeleteCatalogPayload {
119
355
  deletedID: ID!
120
356
  }
121
357
 
122
- type DeleteCirclePayload {
123
- deletedID: ID!
358
+ input RequestDeleteCircleInput {
359
+ circleID: ID!
124
360
  }
125
361
 
126
- type RemoveCircleMemberPayload {
362
+ """
363
+ The request, not the deletion: the tombstone lands with the commit. Watch it. An
364
+ already-gone circle settles NO_OP.
365
+ """
366
+ type RequestDeleteCirclePayload {
367
+ request: ControlRequest!
368
+ }
369
+
370
+ input RequestRemoveCircleMemberInput {
127
371
  circleID: ID!
128
372
  memberDID: DID!
129
373
  }
130
374
 
375
+ """
376
+ The request, not the removal: the tombstone lands with the commit. Watch it. A DID
377
+ that is not an active member settles NO_OP.
378
+ """
379
+ type RequestRemoveCircleMemberPayload {
380
+ request: ControlRequest!
381
+ }
382
+
131
383
  type SyncPeer {
132
384
  id: String!
133
385
  peerDID: DID!
134
386
  endpoint: URL!
135
- mode: String!
136
- priority: String!
137
- trustLevel: String!
138
387
  createdAt: Int!
139
388
  updatedAt: Int!
140
389
  }
@@ -155,6 +404,20 @@ type RemoveSyncPeerPayload {
155
404
  success: Boolean!
156
405
  }
157
406
 
407
+ type SyncPeerPayload {
408
+ messagesReceived: Int!
409
+ messagesSent: Int!
410
+ divergentBuckets: Int!
411
+ }
412
+
413
+ type SyncEvent {
414
+ type: String!
415
+ peerID: ID!
416
+ documentID: ID
417
+ timestamp: Float!
418
+ error: String
419
+ }
420
+
158
421
  input CreateHubInput {
159
422
  url: URL!
160
423
  label: String
@@ -188,10 +451,8 @@ input DelegationTokenCovers {
188
451
  }
189
452
 
190
453
  """
191
- A verified revocation of a delegation token addressed to the viewer.
192
- Only surfaces while the underlying held capability still exists locally;
193
- revocations whose cap was deleted or never arrived are excluded by the
194
- audience-derivation join.
454
+ A verified revocation of a delegation token addressed to the viewer. Surfaces only
455
+ while the held capability still exists locally.
195
456
  """
196
457
  type RevokedDelegationToken {
197
458
  jti: ID!
@@ -200,22 +461,114 @@ type RevokedDelegationToken {
200
461
  revokerDID: DID!
201
462
  "Issuer's revocation-record timestamp (iat), Unix epoch seconds."
202
463
  revokedAt: Int!
203
- """
204
- Wall-clock time the revocation became binding, Unix epoch seconds. For
205
- self-issued revocations this equals revokedAt (verified at mint time);
206
- for received revocations it is the receiver's clock at the moment the
207
- cap-vs-revoker cross-check succeeded.
208
- """
464
+ "When the revocation became binding here, Unix epoch seconds."
209
465
  verifiedAt: Int!
210
466
  capExp: Int
211
467
  groupID: ID!
212
468
  }
213
469
 
470
+ type RemoteGroupInfo {
471
+ id: ID!
472
+ name: String!
473
+ }
474
+
475
+ type PeerInfo {
476
+ did: ID!
477
+ endpoint: String!
478
+ groups: [RemoteGroupInfo!]!
479
+ }
480
+
481
+ type ConnectPeerPayload {
482
+ peer: PeerInfo!
483
+ }
484
+
485
+ """
486
+ Models to share into a circle. Omitted on sharePeerGroup leaves that leg untouched.
487
+ On setCircleSync it is the desired END STATE — an empty list stops sharing.
488
+ """
489
+ input ShareInput {
490
+ models: [ID!]!
491
+ }
492
+
493
+ input ShareReceiveInput {
494
+ activate: Boolean!
495
+ }
496
+
497
+ enum CircleModelAccess {
498
+ MODEL_GRANTED
499
+ MODEL_PRE_EXISTING
500
+ DOCUMENT_ONLY
501
+ }
502
+
503
+ type ModelShareResult {
504
+ modelID: ID!
505
+ access: CircleModelAccess!
506
+ }
507
+
508
+ type SharePeerGroupPayload {
509
+ group: PeerGroup!
510
+ modelAccess: [ModelShareResult!]!
511
+ catalogID: ID
512
+ }
513
+
514
+ type AdmitJoinRequestPayload {
515
+ group: PeerGroup!
516
+ modelAccess: [ModelShareResult!]!
517
+ catalogID: ID
518
+ """Derived from the join request's KeyPackage credential, not its did field."""
519
+ peerDID: ID!
520
+ """
521
+ The invite to hand to the joiner. Carries the access designated above, so the
522
+ joiner lands in the share circle at join.
523
+ """
524
+ invitePayload: String!
525
+ }
526
+
527
+ """
528
+ A circle this device can reach, with its own sync state for it. pulling is true only
529
+ when the circle has catalogs and every one is active locally; sharing holds the
530
+ models this device lets the circle read.
531
+ """
532
+ type CircleSyncState {
533
+ id: ID!
534
+ groupID: ID!
535
+ name: String!
536
+ description: String!
537
+ catalogIDs: [ID!]!
538
+ """Admin-authored: every group member reaches an open circle implicitly."""
539
+ open: Boolean!
540
+ pulling: Boolean!
541
+ sharing: [ID!]!
542
+ }
543
+
544
+ type JoinPeerGroupPayload {
545
+ group: PeerGroup!
546
+ knownCatalogIDs: [ID!]!
547
+ alreadyJoined: Boolean!
548
+ """
549
+ The group's OPEN circles, for an opt-in picker. A closed circle this device holds
550
+ a grant for is excluded; circleSync returns every reachable circle.
551
+ """
552
+ openCircles: [CircleSyncState!]!
553
+ }
554
+
214
555
  extend type Query {
215
556
  groups: [PeerGroup!]!
216
557
  group(id: ID!): PeerGroup
217
558
  groupMembers(groupID: ID!): [PeerGroupMember!]!
218
559
  circles(groupID: ID!): [PeerCircle!]!
560
+ """
561
+ Every device this one has heard announce in the group, its own row included. A
562
+ projection, never liveness: no row is deleted on silence, so a sleeping device is
563
+ still listed and a screen marks it stale rather than losing it.
564
+ """
565
+ peerDevices(groupID: ID!): [PeerDevice!]!
566
+ """
567
+ Every circle in the group this device can currently reach, with its pull/share
568
+ state. Re-read to pick up a circle the admin opened or closed since the join.
569
+ """
570
+ circleSync(groupID: ID!): [CircleSyncState!]!
571
+ catalogs(active: Boolean): [PeerCatalog!]!
219
572
  hubs: [PeerHub!]!
220
573
  hub(id: ID!): PeerHub
221
574
  syncPeers: [SyncPeer!]!
@@ -223,6 +576,11 @@ extend type Query {
223
576
  ownDelegationTokens(grantor: ID, covers: DelegationTokenCovers): [DelegationToken!]!
224
577
  ownRevokedDelegationTokens(grantor: ID): [RevokedDelegationToken!]!
225
578
  groupHealth(groupID: ID!): GroupHealthStatus!
579
+ """
580
+ This device's own un-acked control requests, oldest first, optionally scoped to
581
+ one group. The store is per-device.
582
+ """
583
+ controlRequests(groupID: ID): [ControlRequest!]!
226
584
  }
227
585
 
228
586
  enum GroupHealthCondition {
@@ -254,27 +612,93 @@ type RejoinGroupPayload {
254
612
  state: GroupHealthState!
255
613
  }
256
614
 
615
+ enum ControlRequestStatus {
616
+ PENDING
617
+ COMMITTED
618
+ REJECTED
619
+ LOST
620
+ """
621
+ The state already held, so no commit was driven. Terminal, and deliberately not
622
+ COMMITTED: nothing was committed.
623
+ """
624
+ NO_OP
625
+ }
626
+
627
+ enum ControlRequestKind {
628
+ LEDGER
629
+ INVITE
630
+ REMOVE
631
+ }
632
+
633
+ """
634
+ One entry of this device's control-request queue: a control-plane mutation whose
635
+ commit lands later. A settled request keeps its row until ackControlRequest drops it.
636
+ """
637
+ type ControlRequest implements Node {
638
+ id: ID!
639
+ groupID: ID!
640
+ kind: ControlRequestKind!
641
+ status: ControlRequestStatus!
642
+ error: String
643
+ enqueuedAt: DateTimeISO!
644
+ settledAt: DateTimeISO
645
+ }
646
+
257
647
  extend type Mutation {
648
+ connectPeer(url: String!): ConnectPeerPayload!
649
+ sharePeerGroup(peerDID: ID!, groupID: ID, name: String, send: ShareInput, receive: ShareReceiveInput): SharePeerGroupPayload!
650
+ """
651
+ Admit a join request's author from local state alone: designate access as
652
+ sharePeerGroup does, then mint the invite carrying it. No peer connection is
653
+ opened — the caller hands the payload to the joiner however it can. The admitted
654
+ DID comes from the request's KeyPackage credential; a disagreeing did field, or a
655
+ DID already on the roster, is refused.
656
+ """
657
+ admitJoinRequest(groupID: ID!, joinRequest: String!, send: ShareInput, receive: ShareReceiveInput): AdmitJoinRequestPayload!
658
+ joinPeerGroup(peerDID: ID!, groupID: ID!): JoinPeerGroupPayload!
659
+ """
660
+ Apply one circle's desired sync end state. A null argument leaves that dimension
661
+ untouched; share with an empty model list stops sharing into the circle.
662
+ """
663
+ setCircleSync(circleID: ID!, pull: Boolean, share: ShareInput): CircleSyncState!
664
+ """
665
+ Sync with one named device now. groupID selects the ROUTE, not the scope — the
666
+ session carries the device's whole active-catalog scope, so one call can move
667
+ documents belonging to a different group.
668
+ """
669
+ syncPeer(groupID: ID!, peerDID: ID!): SyncPeerPayload!
258
670
  createGroup(input: CreateGroupInput!): CreateGroupPayload!
259
- addGroupMember(groupID: ID!, memberDID: DID!, role: String!): PeerGroupMember!
260
- createCircle(groupID: ID!, name: String!, description: String): PeerCircle!
261
- addCircleMember(circleID: ID!, memberDID: DID!, role: String!): PeerCircleMember!
671
+ requestCreateCircle(input: RequestCreateCircleInput!): RequestCreateCirclePayload!
672
+ requestAddCircleMember(input: RequestAddCircleMemberInput!): RequestAddCircleMemberPayload!
262
673
  createCatalog(name: String!, description: String, filterCriteria: String!): PeerCatalog!
263
- inviteToGroup(groupID: ID!, joinRequest: String!, grants: [String!], revocations: [String!]): InviteToGroupPayload!
674
+ requestInviteToGroup(input: RequestInviteToGroupInput!): RequestInviteToGroupPayload!
264
675
  joinGroup(invitePayload: String!, joinRequestPayload: String!): JoinGroupPayload!
265
676
  prepareJoinRequest: PrepareJoinRequestPayload!
266
- completeJoin(invitePayload: String!): CompleteJoinPayload!
677
+ """
678
+ Join the group an invite names, declaring in the same call what this device shares
679
+ back and whether it pulls. send / receive mirror admitJoinRequest. Left off, the
680
+ join is plumbing only: a member that grants nothing and pulls nothing. Applied to
681
+ every circle this device now belongs to, which is exactly what the invite seeded.
682
+ """
683
+ completeJoin(
684
+ invitePayload: String!
685
+ send: ShareInput
686
+ receive: ShareReceiveInput
687
+ ): CompleteJoinPayload!
267
688
  leaveGroup(groupID: ID!): LeaveGroupPayload!
268
- removeGroupMember(groupID: ID!, memberDID: DID!): RemoveGroupMemberPayload!
689
+ requestRemoveGroupMember(input: RequestRemoveGroupMemberInput!): RequestRemoveGroupMemberPayload!
690
+ requestSetMemberRole(input: RequestSetMemberRoleInput!): RequestSetMemberRolePayload!
269
691
  updateCatalog(input: UpdateCatalogInput!): PeerCatalog!
270
- updateGroup(input: UpdateGroupInput!): UpdateGroupPayload!
271
- updateCircle(input: UpdateCircleInput!): UpdateCirclePayload!
692
+ activateCatalogSync(catalogID: ID!): PeerCatalog!
693
+ deactivateCatalogSync(catalogID: ID!): PeerCatalog!
694
+ requestUpdateGroup(input: RequestUpdateGroupInput!): RequestUpdateGroupPayload!
695
+ requestUpdateCircle(input: UpdateCircleInput!): RequestUpdateCirclePayload!
272
696
  deleteCatalog(catalogID: ID!): DeleteCatalogPayload!
273
- deleteCircle(circleID: ID!): DeleteCirclePayload!
274
- removeCircleMember(circleID: ID!, memberDID: DID!): RemoveCircleMemberPayload!
697
+ requestDeleteCircle(input: RequestDeleteCircleInput!): RequestDeleteCirclePayload!
698
+ requestRemoveCircleMember(input: RequestRemoveCircleMemberInput!): RequestRemoveCircleMemberPayload!
275
699
  grantWriteCapability(to: DID!, res: String!, groupID: ID!, expiresIn: Int): String!
276
700
  revokeCapability(jti: ID!): Boolean!
277
- addSyncPeer(peerDID: DID!, endpoint: URL!, mode: String, priority: String, trustLevel: String): Boolean!
701
+ addSyncPeer(peerDID: DID!, endpoint: URL!): Boolean!
278
702
  removeSyncPeer(peerDID: DID!): RemoveSyncPeerPayload!
279
703
  createHub(input: CreateHubInput!): PeerHub!
280
704
  updateHub(input: UpdateHubInput!): PeerHub!
@@ -282,10 +706,26 @@ extend type Mutation {
282
706
  bindHubToGroup(hubID: ID!, groupID: ID!): Boolean!
283
707
  unbindHubFromGroup(hubID: ID!, groupID: ID!): Boolean!
284
708
  rejoinGroup(groupID: ID!): RejoinGroupPayload!
709
+ """
710
+ Drop a settled request from the queue — the app saying it has observed the
711
+ outcome. Acking a pending or unknown request is a no-op. node(id:) on an acked
712
+ request throws.
713
+ """
714
+ ackControlRequest(id: ID!): Boolean!
285
715
  }
286
716
 
287
717
  extend type PeerGroup {
288
718
  hubs: [PeerHub!]!
719
+ """
720
+ Whether the viewer may perform this group's admin-only operations right now. Read
721
+ from the membership row the admin gate consults, so the two never disagree.
722
+ """
723
+ viewerIsAdmin: Boolean!
724
+ """
725
+ Every current admin, from the same membership rows. Moves when the MLS commit
726
+ carrying a role change is enacted.
727
+ """
728
+ admins: [AccountNode!]!
289
729
  }
290
730
 
291
731
  extend type PeerHub {
@@ -298,6 +738,8 @@ extend type Subscription {
298
738
  groupDataChanged(groupID: ID!): PeerGroup!
299
739
  groupMemberJoined(groupID: ID!): PeerGroupMember!
300
740
  groupMemberLeft(groupID: ID!): PeerGroupMember!
741
+ """A member's role as it stands after the enacted commit that changed it."""
742
+ groupMemberRoleChanged(groupID: ID!): PeerGroupMember!
301
743
  circleCreated(groupID: ID!): PeerCircle!
302
744
  circleDeleted(groupID: ID!): PeerCircle!
303
745
  circleDataChanged(circleID: ID!): PeerCircle!
@@ -307,5 +749,674 @@ extend type Subscription {
307
749
  ownDelegationTokenAdded: DelegationToken!
308
750
  ownDelegationTokenRevoked: RevokedDelegationToken!
309
751
  groupHealthChanged(groupID: ID): GroupHealthEvent!
752
+ syncEvents(peerDID: ID): SyncEvent!
753
+ """
754
+ A control request reached a terminal status, under the same global id the
755
+ enqueuing mutation returned. requestID watches one request, replaying a status
756
+ that settled before the subscription opened, then completes; groupID watches every
757
+ request in the group and stays open. An unknown requestID waits rather than
758
+ failing — indistinguishable from an uncommitted enqueue — so bring your own timeout.
759
+ """
760
+ controlRequestSettled(groupID: ID, requestID: ID): ControlRequest!
761
+ }
762
+ `;
763
+ export function createP2PSchemaExtension(emitter, syncManager, logger = getKubunLogger('p2p-schema')) {
764
+ return {
765
+ sdl,
766
+ // These mutations perform network I/O (peer discovery, the MLS dance,
767
+ // merkle sync) and/or apply received mutations in their own per-step
768
+ // transactions. Running them inside `mutateGraph`'s write transaction would
769
+ // hold the DB connection across a network round-trip and deadlock a nested
770
+ // apply on single-connection SQLite. They still run owner-signed through
771
+ // `mutateGraph`'s authorization path — only the outer transaction is
772
+ // dropped, and their local writes autocommit per step (each step is
773
+ // idempotent, so a partial run is retry-safe).
774
+ nonTransactionalMutationFields: [
775
+ 'connectPeer',
776
+ 'syncPeer',
777
+ 'sharePeerGroup',
778
+ // Runs the same designation as `sharePeerGroup` — every step awaits a
779
+ // control-request settle — and then awaits its own Add commit.
780
+ 'admitJoinRequest',
781
+ // The mirror of `admitJoinRequest`, and network I/O for the same reason:
782
+ // it calls `setCircleSync` per circle, which pushes `group/push-control`
783
+ // over a live peer connection. Latent while the offline CLI join holds no
784
+ // connection — that arm iterates an empty list — and a deadlock the first
785
+ // time a joiner completes with one held.
786
+ 'completeJoin',
787
+ 'joinPeerGroup',
788
+ 'setCircleSync',
789
+ // Waits for its Add commit to land. The lane reads the group's handle on
790
+ // the registry's own connection, so holding a write transaction across
791
+ // that wait deadlocks single-connection SQLite.
792
+ 'requestInviteToGroup'
793
+ ],
794
+ resolvers: {
795
+ queryFields: {
796
+ groups: async (_source, _args, context)=>{
797
+ return await requireP2P(context).group.list();
798
+ },
799
+ group: async (_source, args, context)=>{
800
+ return await requireP2P(context).group.get(args.id);
801
+ },
802
+ groupMembers: async (_source, args, context)=>{
803
+ return await requireP2P(context).group.listMembers(args.groupID);
804
+ },
805
+ circles: async (_source, args, context)=>{
806
+ return await requireP2P(context).group.listCircles(args.groupID);
807
+ },
808
+ peerDevices: async (_source, args, context)=>{
809
+ return await requireP2P(context).sync.listPeerDevices(args.groupID);
810
+ },
811
+ circleSync: async (_source, args, context)=>{
812
+ return await requireP2P(context).peer.listCircleSyncStates(args.groupID);
813
+ },
814
+ catalogs: async (_source, args, context)=>{
815
+ const active = args.active;
816
+ return await requireP2P(context).group.listStoredCatalogs(active != null ? {
817
+ active
818
+ } : undefined);
819
+ },
820
+ hubs: async (_source, _args, context)=>{
821
+ return await requireP2P(context).hub.list();
822
+ },
823
+ hub: async (_source, args, context)=>{
824
+ return await requireP2P(context).hub.get(args.id);
825
+ },
826
+ syncPeers: async (_source, _args, context)=>{
827
+ return await requireP2P(context).sync.listPeers();
828
+ },
829
+ syncStatus: (_source, _args, context)=>{
830
+ const status = requireP2P(context).sync.getStatus();
831
+ return {
832
+ ...status,
833
+ lastSyncByPeer: JSON.stringify(status.lastSyncByPeer)
834
+ };
835
+ },
836
+ // INERT: always reports `healthy` — nothing signals the monitor. Live
837
+ // scaffolding for epoch-stale recovery, kept so the surface exists once
838
+ // something drives it.
839
+ groupHealth: (_source, args, context)=>{
840
+ const groupID = args.groupID;
841
+ const { state, lastTransitionAt } = requireP2P(context).hub.getGroupHealth(groupID);
842
+ return {
843
+ groupID,
844
+ state: HEALTH_STATE_TO_SDL[state],
845
+ lastTransitionAt: lastTransitionAt == null ? null : new Date(lastTransitionAt).toISOString()
846
+ };
847
+ },
848
+ controlRequests: async (_source, args, context)=>{
849
+ const requests = await requireP2P(context).group.listControlRequests(args.groupID);
850
+ return requests.map(toControlRequestSDL);
851
+ },
852
+ ownDelegationTokens: async (_source, args, context)=>{
853
+ const viewerDID = context.getViewer();
854
+ if (viewerDID == null || context.p2p == null) {
855
+ return [];
856
+ }
857
+ const grantor = args.grantor ?? null;
858
+ const covers = args.covers ?? null;
859
+ return await context.p2p.delegation.listOwnTokens({
860
+ viewerDID,
861
+ grantor,
862
+ covers
863
+ });
864
+ },
865
+ ownRevokedDelegationTokens: async (_source, args, context)=>{
866
+ const viewerDID = context.getViewer();
867
+ if (viewerDID == null || context.p2p == null) {
868
+ return [];
869
+ }
870
+ const grantor = args.grantor ?? null;
871
+ return await context.p2p.delegation.listOwnRevokedTokens({
872
+ viewerDID,
873
+ grantor
874
+ });
875
+ }
876
+ },
877
+ mutationFields: {
878
+ connectPeer: async (_source, args, context)=>{
879
+ const peer = await requireP2P(context).peer.connect(args.url);
880
+ return {
881
+ peer
882
+ };
883
+ },
884
+ sharePeerGroup: async (_source, args, context)=>{
885
+ const send = args.send;
886
+ const receive = args.receive;
887
+ return await requireP2P(context).peer.sharePeerGroup({
888
+ peerDID: args.peerDID,
889
+ groupID: args.groupID ?? null,
890
+ name: args.name ?? null,
891
+ // No `send` = no access legs (plumbing only); the leg exists only from
892
+ // this explicit user selection. `receive.activate` opts the sharer into
893
+ // pulling the share scope (default off).
894
+ sendModels: send?.models ?? null,
895
+ receiveActivate: receive?.activate ?? false,
896
+ // Model-default rows go through the engine's owner-signed executor so
897
+ // `engine:access-default:set` fires and broadcast/invite seeding stay
898
+ // consistent — the same path the `setModelAccessDefaults` mutation uses.
899
+ setReadDefaultForCircle: async (modelID, circleID)=>{
900
+ await context.executeSetModelAccessDefaults({
901
+ modelID,
902
+ permissionType: 'read',
903
+ accessLevel: 'restricted',
904
+ allowedDIDs: null,
905
+ allowedCircles: [
906
+ circleID
907
+ ],
908
+ allowedGroups: null
909
+ });
910
+ }
911
+ });
912
+ },
913
+ admitJoinRequest: async (_source, args, context)=>{
914
+ const send = args.send;
915
+ const receive = args.receive;
916
+ return await requireP2P(context).peer.admitJoinRequest({
917
+ groupID: args.groupID,
918
+ joinRequest: args.joinRequest,
919
+ sendModels: send?.models ?? null,
920
+ receiveActivate: receive?.activate ?? false,
921
+ // Same owner-signed executor `sharePeerGroup` binds, so the rows this
922
+ // designation writes are indistinguishable from a connected share's.
923
+ setReadDefaultForCircle: async (modelID, circleID)=>{
924
+ await context.executeSetModelAccessDefaults({
925
+ modelID,
926
+ permissionType: 'read',
927
+ accessLevel: 'restricted',
928
+ allowedDIDs: null,
929
+ allowedCircles: [
930
+ circleID
931
+ ],
932
+ allowedGroups: null
933
+ });
934
+ }
935
+ });
936
+ },
937
+ joinPeerGroup: async (_source, args, context)=>{
938
+ return await requireP2P(context).peer.joinPeerGroup({
939
+ peerDID: args.peerDID,
940
+ groupID: args.groupID
941
+ });
942
+ },
943
+ setCircleSync: async (_source, args, context)=>{
944
+ const share = args.share;
945
+ return await requireP2P(context).peer.setCircleSync({
946
+ circleID: args.circleID,
947
+ pull: args.pull ?? null,
948
+ share: share ?? null,
949
+ ...accessDefaultExecutors(context)
950
+ });
951
+ },
952
+ syncPeer: async (_source, args, context)=>{
953
+ return await requireP2P(context).sync.syncPeer(args.groupID, args.peerDID);
954
+ },
955
+ createGroup: async (_source, args, context)=>{
956
+ const input = args.input;
957
+ const group = await requireP2P(context).group.create(input);
958
+ return {
959
+ group
960
+ };
961
+ },
962
+ requestCreateCircle: async (_source, args, context)=>{
963
+ const input = args.input;
964
+ // The context method gates again for callers that reach it directly.
965
+ //
966
+ // Returns `pending`, not awaited: the commit lane runs on the outermost
967
+ // commit, after this transaction closes.
968
+ return await gatedTransaction('requestCreateCircle', ()=>requireP2P(context).group.requireAdmin(input.groupID, 'create a circle'), async ()=>{
969
+ // Only the three public fields are forwarded: `circleID` is an
970
+ // internal affordance for a caller that derives it, and letting a
971
+ // client name it would merge two admins' circles under one subject.
972
+ const { request, circleID } = await requireP2P(context).group.requestCreateCircle({
973
+ groupID: input.groupID,
974
+ name: input.name,
975
+ description: input.description
976
+ });
977
+ return {
978
+ request: toControlRequestSDL(request),
979
+ circleID
980
+ };
981
+ });
982
+ },
983
+ requestAddCircleMember: async (_source, args, context)=>{
984
+ const input = args.input;
985
+ // The group is reached through the circle, so the gate resolves it first.
986
+ return await gatedTransaction('requestAddCircleMember', ()=>requireP2P(context).group.requireCircleAdmin(input.circleID, 'add a circle member'), async ()=>{
987
+ const request = await requireP2P(context).group.requestAddCircleMember(input);
988
+ return {
989
+ request: toControlRequestSDL(request)
990
+ };
991
+ });
992
+ },
993
+ createCatalog: async (_source, args, context)=>{
994
+ return await requireP2P(context).group.createCatalog({
995
+ name: args.name,
996
+ description: args.description,
997
+ filterCriteria: args.filterCriteria
998
+ });
999
+ },
1000
+ requestInviteToGroup: async (_source, args, context)=>{
1001
+ const input = args.input;
1002
+ // No rollback wrapper: this field is non-transactional, so there is no
1003
+ // outer write transaction to undo. It cannot be transactional — it
1004
+ // waits for the commit lane, and the lane's own reads need the
1005
+ // connection a write transaction would be holding.
1006
+ const result = await requireP2P(context).group.requestInvite(input);
1007
+ return {
1008
+ ...result,
1009
+ request: toControlRequestSDL(result.request)
1010
+ };
1011
+ },
1012
+ joinGroup: async (_source, args, context)=>{
1013
+ return await requireP2P(context).group.join(args.invitePayload, args.joinRequestPayload);
1014
+ },
1015
+ prepareJoinRequest: async (_source, _args, context)=>{
1016
+ return await requireP2P(context).join.prepareRequest();
1017
+ },
1018
+ completeJoin: async (_source, args, context)=>{
1019
+ const p2p = requireP2P(context);
1020
+ const result = await p2p.join.complete(args.invitePayload);
1021
+ const send = args.send;
1022
+ const receive = args.receive;
1023
+ const groupID = result.group?.id;
1024
+ if (groupID == null || send == null && receive == null) {
1025
+ return result;
1026
+ }
1027
+ // Every circle this device now reaches in the joined group. It joined a
1028
+ // moment ago, so those are the circles the invite seeded and no others
1029
+ // — deriving the scope from the invite payload would say the same thing
1030
+ // through a second reading of it.
1031
+ // Only circles carrying a catalog. A circle without one is not a scope:
1032
+ // `pull` has nothing to activate and a read default naming it is granted
1033
+ // to a set no catalog will ever ask for — the pair reads as configured
1034
+ // and moves nothing, which is the failure this argument exists to close.
1035
+ const circles = (await p2p.peer.listCircleSyncStates(groupID)).filter((circle)=>circle.catalogIDs.length > 0);
1036
+ if (circles.length === 0) {
1037
+ throw new Error('completeJoin: the invite designated no circle to share into — the admitting device admitted without `send`, so this pair can move nothing');
1038
+ }
1039
+ const executors = accessDefaultExecutors(context);
1040
+ for (const circle of circles){
1041
+ await p2p.peer.setCircleSync({
1042
+ circleID: circle.id,
1043
+ pull: receive?.activate ?? null,
1044
+ share: send ?? null,
1045
+ ...executors
1046
+ });
1047
+ }
1048
+ return result;
1049
+ },
1050
+ leaveGroup: async (_source, args, context)=>{
1051
+ // leave tears down the MLS handle and emits a leave event: any throw
1052
+ // must roll back so a partial teardown is never committed.
1053
+ try {
1054
+ return await requireP2P(context).group.leave(args.groupID);
1055
+ } catch (error) {
1056
+ failTransactionFatal('leaveGroup', error);
1057
+ }
1058
+ },
1059
+ requestRemoveGroupMember: async (_source, args, context)=>{
1060
+ const input = args.input;
1061
+ // This transaction owns only the request row — the MLS advance, the roster
1062
+ // tombstone and the demotion belong to the accepted commit — so a throw
1063
+ // must roll it back: a queued request no lane settles is `pending` forever.
1064
+ // Returns `pending`, not awaited: the commit lane runs on the outermost
1065
+ // commit, after this transaction closes.
1066
+ return await gatedTransaction('requestRemoveGroupMember', ()=>requireP2P(context).group.requireAdmin(input.groupID, 'remove a member'), async ()=>{
1067
+ const request = await requireP2P(context).group.requestRemoveMember(input);
1068
+ return {
1069
+ request: toControlRequestSDL(request)
1070
+ };
1071
+ });
1072
+ },
1073
+ requestSetMemberRole: async (_source, args, context)=>{
1074
+ const input = args.input;
1075
+ // The entry and the request that reports it are written in ONE transaction,
1076
+ // so neither can outlive the other's rollback.
1077
+ // Returns `pending`, not awaited: the commit lane runs on the outermost
1078
+ // commit, after this transaction closes.
1079
+ //
1080
+ return await gatedTransaction('requestSetMemberRole', ()=>requireP2P(context).group.requireAdmin(input.groupID, 'set member roles'), async ()=>{
1081
+ const request = await requireP2P(context).group.requestSetMemberRole(input);
1082
+ return {
1083
+ request: toControlRequestSDL(request)
1084
+ };
1085
+ });
1086
+ },
1087
+ updateCatalog: async (_source, args, context)=>{
1088
+ const input = args.input;
1089
+ return await requireP2P(context).group.updateCatalog(input);
1090
+ },
1091
+ activateCatalogSync: async (_source, args, context)=>{
1092
+ return await requireP2P(context).group.activateCatalogSync(args.catalogID);
1093
+ },
1094
+ deactivateCatalogSync: async (_source, args, context)=>{
1095
+ return await requireP2P(context).group.deactivateCatalogSync(args.catalogID);
1096
+ },
1097
+ requestUpdateGroup: async (_source, args, context)=>{
1098
+ const input = args.input;
1099
+ // The context method gates again, ahead of its own no-op check, so a
1100
+ // caller with no authority is refused rather than told NO_OP.
1101
+ //
1102
+ // Nothing is appended to the ledger here — the fold off the enacted commit
1103
+ // is the only writer — so a throw must roll back the request row.
1104
+ // Returns `pending`, not awaited: the commit lane runs on the outermost
1105
+ // commit, after this transaction closes.
1106
+ //
1107
+ return await gatedTransaction('requestUpdateGroup', ()=>requireP2P(context).group.requireAdmin(input.groupID, 'update a group'), async ()=>{
1108
+ const request = await requireP2P(context).group.requestUpdate(input);
1109
+ return {
1110
+ request: toControlRequestSDL(request)
1111
+ };
1112
+ });
1113
+ },
1114
+ requestUpdateCircle: async (_source, args, context)=>{
1115
+ const input = args.input;
1116
+ // The gate runs ahead of the context method's own no-op check, so a
1117
+ // caller with no authority is refused rather than told NO_OP.
1118
+ return await gatedTransaction('requestUpdateCircle', ()=>requireP2P(context).group.requireCircleAdmin(input.circleID, 'update a circle'), async ()=>{
1119
+ const request = await requireP2P(context).group.requestUpdateCircle(input);
1120
+ return {
1121
+ request: toControlRequestSDL(request)
1122
+ };
1123
+ });
1124
+ },
1125
+ deleteCatalog: async (_source, args, context)=>{
1126
+ return await requireP2P(context).group.deleteCatalog(args.catalogID);
1127
+ },
1128
+ requestDeleteCircle: async (_source, args, context)=>{
1129
+ const input = args.input;
1130
+ // The context method gates again for callers that reach it directly.
1131
+ //
1132
+ // Returns `pending`, not awaited: the commit lane runs on the outermost
1133
+ // commit, after this transaction closes.
1134
+ //
1135
+ return await gatedTransaction('requestDeleteCircle', ()=>requireP2P(context).group.requireCircleAdmin(input.circleID, 'delete a circle'), async ()=>{
1136
+ const request = await requireP2P(context).group.requestDeleteCircle(input);
1137
+ return {
1138
+ request: toControlRequestSDL(request)
1139
+ };
1140
+ });
1141
+ },
1142
+ requestRemoveCircleMember: async (_source, args, context)=>{
1143
+ const input = args.input;
1144
+ // The context method gates again for direct callers; the entry plus
1145
+ // the request that reports its outcome are written in one transaction,
1146
+ // so any throw rolls back both.
1147
+ return await gatedTransaction('requestRemoveCircleMember', ()=>requireP2P(context).group.requireCircleAdmin(input.circleID, 'remove a circle member'), async ()=>{
1148
+ const request = await requireP2P(context).group.requestRemoveCircleMember(input);
1149
+ return {
1150
+ request: toControlRequestSDL(request)
1151
+ };
1152
+ });
1153
+ },
1154
+ grantWriteCapability: async (_source, args, context)=>{
1155
+ return await requireP2P(context).group.grantWriteCapability({
1156
+ to: args.to,
1157
+ res: args.res,
1158
+ groupID: args.groupID,
1159
+ expiresIn: args.expiresIn
1160
+ });
1161
+ },
1162
+ revokeCapability: async (_source, args, context)=>{
1163
+ return await requireP2P(context).group.revokeCapability({
1164
+ jti: args.jti
1165
+ });
1166
+ },
1167
+ addSyncPeer: async (_source, args, context)=>{
1168
+ await requireP2P(context).sync.addPeer({
1169
+ peerDID: args.peerDID,
1170
+ endpoint: args.endpoint
1171
+ });
1172
+ return true;
1173
+ },
1174
+ removeSyncPeer: async (_source, args, context)=>{
1175
+ await requireP2P(context).sync.removePeer(args.peerDID);
1176
+ return {
1177
+ success: true
1178
+ };
1179
+ },
1180
+ createHub: async (_source, args, context)=>{
1181
+ const input = args.input;
1182
+ return await requireP2P(context).hub.create(input);
1183
+ },
1184
+ updateHub: async (_source, args, context)=>{
1185
+ const input = args.input;
1186
+ return await requireP2P(context).hub.update(input);
1187
+ },
1188
+ deleteHub: async (_source, args, context)=>{
1189
+ return await requireP2P(context).hub.delete(args.id, args.force ?? false);
1190
+ },
1191
+ bindHubToGroup: async (_source, args, context)=>{
1192
+ return await requireP2P(context).hub.bind({
1193
+ hubID: args.hubID,
1194
+ groupID: args.groupID
1195
+ });
1196
+ },
1197
+ unbindHubFromGroup: async (_source, args, context)=>{
1198
+ return await requireP2P(context).hub.unbind({
1199
+ hubID: args.hubID,
1200
+ groupID: args.groupID
1201
+ });
1202
+ },
1203
+ // Takes the GLOBAL id and decodes it, unlike `Connector`'s local-key
1204
+ // convention: a client only ever holds `ControlRequest.id`, and that is
1205
+ // the global one. The `id` type-field and this decode are the only
1206
+ // translators between the two.
1207
+ ackControlRequest: async (_source, args, context)=>{
1208
+ const localID = PluginNodeID.fromString(args.id).localID;
1209
+ await requireP2P(context).group.ackControlRequest(localID);
1210
+ return true;
1211
+ },
1212
+ // `hub.rejoinGroup` genuinely recovers through the rendezvous now, but
1213
+ // this kickoff branch stays unreachable: `getGroupHealth` always returns
1214
+ // `healthy` because nothing signals the monitor yet, so `state` never
1215
+ // leaves `healthy`. Wiring the auto-detection that would fire this is
1216
+ // separate; see docs/agents/plans/backlog/recovery-rendezvous-transport.md.
1217
+ rejoinGroup: (_source, args, context)=>{
1218
+ const groupID = args.groupID;
1219
+ const hub = requireP2P(context).hub;
1220
+ const { state } = hub.getGroupHealth(groupID);
1221
+ // Async kickoff (when wired): already-current is a no-op; an in-flight
1222
+ // rejoin is not restarted. The terminal outcome would arrive via the
1223
+ // groupHealthChanged stream.
1224
+ if (state !== 'healthy' && state !== 'recovering') {
1225
+ // Log-only: catch so a throw (e.g. no active channel) can't leak.
1226
+ hub.rejoinGroup(groupID).catch((error)=>{
1227
+ logger.error('rejoinGroup kickoff failed', {
1228
+ groupID,
1229
+ error
1230
+ });
1231
+ });
1232
+ }
1233
+ const reported = state === 'healthy' ? 'HEALTHY' : 'RECOVERING';
1234
+ return {
1235
+ groupID,
1236
+ state: reported
1237
+ };
1238
+ }
1239
+ },
1240
+ typeFields: {
1241
+ PeerGroup: {
1242
+ hubs: async (source, _args, context)=>{
1243
+ const group = source;
1244
+ return await requireP2P(context).hub.listByGroup(group.id);
1245
+ },
1246
+ // Reads the viewer's membership row through the same store call the
1247
+ // admin gate makes, so the answer this field gives and the answer a
1248
+ // refused mutation gives can never come apart. An unauthenticated
1249
+ // request has no membership row to be an admin on, so it reads false
1250
+ // rather than failing.
1251
+ viewerIsAdmin: async (source, _args, context)=>{
1252
+ const viewerDID = context.getViewer();
1253
+ if (viewerDID == null) {
1254
+ return false;
1255
+ }
1256
+ const group = source;
1257
+ return await requireP2P(context).group.isAdmin(group.id, viewerDID);
1258
+ },
1259
+ // Same membership rows, same liveness boundary as `viewerIsAdmin`: a
1260
+ // removed member is not listed, and a DID carrying a role without a
1261
+ // membership row cannot appear at all.
1262
+ admins: async (source, _args, context)=>{
1263
+ const group = source;
1264
+ const members = await requireP2P(context).group.listMembers(group.id);
1265
+ return members.filter((m)=>m.role === 'admin').map((m)=>m.memberDID);
1266
+ }
1267
+ },
1268
+ PeerGroupMember: {
1269
+ // A DID string is all `AccountNode` needs: the shared node fetcher
1270
+ // returns a `did:` id unchanged and types it as an account.
1271
+ account: (source)=>source.memberDID
1272
+ },
1273
+ PeerHub: {
1274
+ groups: async (source, _args, context)=>{
1275
+ const hub = source;
1276
+ return await requireP2P(context).hub.listGroups(hub.id);
1277
+ }
1278
+ },
1279
+ ControlRequest: {
1280
+ id: (source)=>{
1281
+ const request = source;
1282
+ return PluginNodeID.create('p2p', 'ControlRequest', request.id).toString();
1283
+ }
1284
+ }
1285
+ },
1286
+ nodeResolvers: {
1287
+ ControlRequest: {
1288
+ resolve: async (localID, context)=>{
1289
+ const ctx = context;
1290
+ const request = await requireP2P(ctx).group.getControlRequest(localID);
1291
+ return request == null ? null : toControlRequestSDL(request);
1292
+ }
1293
+ }
1294
+ },
1295
+ subscriptionFields: {
1296
+ groupJoined: {
1297
+ resolve: (group)=>group,
1298
+ subscribe: ()=>createFilteredGenerator(emitter, 'groupJoined')
1299
+ },
1300
+ groupLeft: {
1301
+ resolve: (group)=>group,
1302
+ subscribe: ()=>createFilteredGenerator(emitter, 'groupLeft')
1303
+ },
1304
+ groupDataChanged: {
1305
+ resolve: (group)=>group,
1306
+ subscribe: (_src, args)=>createFilteredGenerator(emitter, 'groupDataChanged', (d)=>d.groupID === args.groupID)
1307
+ },
1308
+ groupMemberJoined: {
1309
+ resolve: (member)=>member,
1310
+ subscribe: (_src, args)=>createFilteredGenerator(emitter, 'groupMemberJoined', (d)=>d.groupID === args.groupID)
1311
+ },
1312
+ groupMemberLeft: {
1313
+ resolve: (member)=>member,
1314
+ subscribe: (_src, args)=>createFilteredGenerator(emitter, 'groupMemberLeft', (d)=>d.groupID === args.groupID)
1315
+ },
1316
+ groupMemberRoleChanged: {
1317
+ resolve: (member)=>member,
1318
+ subscribe: (_src, args)=>createFilteredGenerator(emitter, 'groupMemberRoleChanged', (d)=>d.groupID === args.groupID)
1319
+ },
1320
+ circleCreated: {
1321
+ resolve: (circle)=>circle,
1322
+ subscribe: (_src, args)=>createFilteredGenerator(emitter, 'circleCreated', (d)=>d.groupID === args.groupID)
1323
+ },
1324
+ circleDeleted: {
1325
+ resolve: (circle)=>circle,
1326
+ subscribe: (_src, args)=>createFilteredGenerator(emitter, 'circleDeleted', (d)=>d.groupID === args.groupID)
1327
+ },
1328
+ circleDataChanged: {
1329
+ resolve: (circle)=>circle,
1330
+ subscribe: (_src, args)=>createFilteredGenerator(emitter, 'circleDataChanged', (d)=>d.circleID === args.circleID)
1331
+ },
1332
+ circleMemberAdded: {
1333
+ resolve: (member)=>member,
1334
+ subscribe: (_src, args)=>createFilteredGenerator(emitter, 'circleMemberAdded', (d)=>d.circleID === args.circleID)
1335
+ },
1336
+ circleMemberRemoved: {
1337
+ resolve: (member)=>member,
1338
+ subscribe: (_src, args)=>createFilteredGenerator(emitter, 'circleMemberRemoved', (d)=>d.circleID === args.circleID)
1339
+ },
1340
+ circleCatalogsChanged: {
1341
+ resolve: (circle)=>circle,
1342
+ subscribe: (_src, args)=>createFilteredGenerator(emitter, 'circleCatalogsChanged', (d)=>d.circleID === args.circleID)
1343
+ },
1344
+ ownDelegationTokenAdded: {
1345
+ resolve: (data)=>data,
1346
+ subscribe: (_src, _args, context)=>{
1347
+ const viewerDID = context.getViewer();
1348
+ if (viewerDID == null) {
1349
+ return emptyAsyncGenerator();
1350
+ }
1351
+ return createFilteredGenerator(emitter, 'delegationTokenAdded', (data)=>data.audience === viewerDID);
1352
+ }
1353
+ },
1354
+ ownDelegationTokenRevoked: {
1355
+ resolve: (data)=>data,
1356
+ subscribe: (_src, _args, context)=>{
1357
+ const viewerDID = context.getViewer();
1358
+ if (viewerDID == null) {
1359
+ return emptyAsyncGenerator();
1360
+ }
1361
+ // Audience-side filter mirrors `ownDelegationTokenAdded` — the
1362
+ // viewer subscribes to revocations of caps they hold (or held).
1363
+ return createFilteredGenerator(emitter, 'delegationTokenRevoked', (data)=>data.audience === viewerDID);
1364
+ }
1365
+ },
1366
+ syncEvents: {
1367
+ resolve: (event)=>{
1368
+ const syncEvent = event;
1369
+ return {
1370
+ type: syncEvent.type,
1371
+ peerID: syncEvent.peerID,
1372
+ documentID: syncEvent.documentID ?? null,
1373
+ timestamp: syncEvent.timestamp,
1374
+ error: syncEvent.error ?? null
1375
+ };
1376
+ },
1377
+ subscribe: (_src, args)=>{
1378
+ const peerDID = args.peerDID;
1379
+ return syncManager.subscribeEvents(peerDID == null ? undefined : (event)=>event.peerID === peerDID);
1380
+ }
1381
+ },
1382
+ controlRequestSettled: {
1383
+ resolve: (data)=>toControlRequestSDL(data),
1384
+ subscribe: (_src, args, context)=>{
1385
+ const globalRequestID = args.requestID;
1386
+ if (globalRequestID != null) {
1387
+ // The client holds the global id the enqueuing payload returned;
1388
+ // the row and the emitted event carry the local one.
1389
+ const requestID = PluginNodeID.fromString(globalRequestID).localID;
1390
+ // The stream is opened HERE, synchronously, before the generator
1391
+ // body below ever reads the row: a settle landing between the two
1392
+ // is queued by the listener, so there is no window where an
1393
+ // outcome is neither in the row nor in the stream. A settle that
1394
+ // already happened is only in the row, which is why the read has
1395
+ // to happen at all.
1396
+ const stream = createFilteredGenerator(emitter, 'controlRequestSettled', (d)=>d.id === requestID);
1397
+ return watchOneControlRequest(stream, ()=>requireP2P(context).group.getControlRequest(requestID));
1398
+ }
1399
+ const groupID = args.groupID;
1400
+ return createFilteredGenerator(emitter, 'controlRequestSettled', groupID == null ? undefined : (d)=>d.groupID === groupID);
1401
+ }
1402
+ },
1403
+ // INERT: this subscription can never fire — `groupHealthChanged` has no
1404
+ // live producer. Reserved for epoch-stale recovery.
1405
+ groupHealthChanged: {
1406
+ resolve: (data)=>{
1407
+ const event = data;
1408
+ return {
1409
+ groupID: event.groupID,
1410
+ condition: HEALTH_CONDITION_TO_SDL[event.condition],
1411
+ state: HEALTH_STATE_TO_SDL[event.state]
1412
+ };
1413
+ },
1414
+ subscribe: (_src, args)=>{
1415
+ const groupID = args.groupID;
1416
+ return createFilteredGenerator(emitter, 'groupHealthChanged', groupID == null ? undefined : (d)=>d.groupID === groupID);
1417
+ }
1418
+ }
1419
+ }
1420
+ }
1421
+ };
310
1422
  }
311
- `;export function createP2PSchemaExtension(n){return{sdl:i,resolvers:{queryFields:{groups:async(e,t,o)=>await r(o).group.list(),group:async(e,t,o)=>await r(o).group.get(t.id),groupMembers:async(e,t,o)=>await r(o).group.listMembers(t.groupID),circles:async(e,t,o)=>await r(o).group.listCircles(t.groupID),hubs:async(e,t,o)=>await r(o).hub.list(),hub:async(e,t,o)=>await r(o).hub.get(t.id),syncPeers:async(e,t,o)=>await r(o).sync.listPeers(),syncStatus:(e,t,o)=>{let a=r(o).sync.getStatus();return{...a,lastSyncByPeer:JSON.stringify(a.lastSyncByPeer)}},groupHealth:(e,o,a)=>{let i=o.groupID,{state:n,lastTransitionAt:u}=r(a).hub.getGroupHealth(i);return{groupID:i,state:t[n],lastTransitionAt:null==u?null:new Date(u).toISOString()}},ownDelegationTokens:async(e,r,t)=>{let o=t.getViewer();if(null==o||null==t.p2p)return[];let a=r.grantor??null,i=r.covers??null;return await t.p2p.delegation.listOwnTokens({viewerDID:o,grantor:a,covers:i})},ownRevokedDelegationTokens:async(e,r,t)=>{let o=t.getViewer();if(null==o||null==t.p2p)return[];let a=r.grantor??null;return await t.p2p.delegation.listOwnRevokedTokens({viewerDID:o,grantor:a})}},mutationFields:{createGroup:async(e,t,o)=>{let a=t.input;return{group:await r(o).group.create(a)}},addGroupMember:async(e,t,o)=>await r(o).group.addMember(t.groupID,t.memberDID,t.role),createCircle:async(e,t,o)=>await r(o).group.createCircle({groupID:t.groupID,name:t.name,description:t.description}),addCircleMember:async(e,t,o)=>await r(o).group.addCircleMember(t.circleID,t.memberDID,t.role),createCatalog:async(e,t,o)=>await r(o).group.createCatalog({name:t.name,description:t.description,filterCriteria:t.filterCriteria}),inviteToGroup:async(e,t,o)=>await r(o).group.invite(t.groupID,t.joinRequest,t.grants,t.revocations),joinGroup:async(e,t,o)=>await r(o).group.join(t.invitePayload,t.joinRequestPayload),prepareJoinRequest:async(e,t,o)=>await r(o).join.prepareRequest(),completeJoin:async(e,t,o)=>await r(o).join.complete(t.invitePayload),leaveGroup:async(e,t,o)=>await r(o).group.leave(t.groupID),removeGroupMember:async(e,t,o)=>await r(o).group.removeMember(t.groupID,t.memberDID),updateCatalog:async(e,t,o)=>{let a=t.input;return await r(o).group.updateCatalog(a)},updateGroup:async(e,t,o)=>{let a=t.input;return{group:await r(o).group.update(a)}},updateCircle:async(e,t,o)=>{let a=t.input;return{circle:await r(o).group.updateCircle(a)}},deleteCatalog:async(e,t,o)=>await r(o).group.deleteCatalog(t.catalogID),deleteCircle:async(e,t,o)=>await r(o).group.deleteCircle(t.circleID),removeCircleMember:async(e,t,o)=>await r(o).group.removeCircleMember(t.circleID,t.memberDID),grantWriteCapability:async(e,t,o)=>await r(o).group.grantWriteCapability({to:t.to,res:t.res,groupID:t.groupID,expiresIn:t.expiresIn}),revokeCapability:async(e,t,o)=>await r(o).group.revokeCapability({jti:t.jti}),addSyncPeer:async(e,t,o)=>(await r(o).sync.addPeer({peerDID:t.peerDID,endpoint:t.endpoint,mode:t.mode??"on-demand",allowedUsers:{all:!0},priority:t.priority??"0",trustLevel:t.trustLevel??"trusted"}),!0),removeSyncPeer:async(e,t,o)=>(await r(o).sync.removePeer(t.peerDID),{success:!0}),createHub:async(e,t,o)=>{let a=t.input;return await r(o).hub.create(a)},updateHub:async(e,t,o)=>{let a=t.input;return await r(o).hub.update(a)},deleteHub:async(e,t,o)=>await r(o).hub.delete(t.id,t.force??!1),bindHubToGroup:async(e,t,o)=>await r(o).hub.bind({hubID:t.hubID,groupID:t.groupID}),unbindHubFromGroup:async(e,t,o)=>await r(o).hub.unbind({hubID:t.hubID,groupID:t.groupID}),rejoinGroup:(e,t,o)=>{let a=t.groupID,i=r(o).hub,{state:n}=i.getGroupHealth(a);return"healthy"!==n&&"recovering"!==n&&i.rejoinGroup(a),{groupID:a,state:"healthy"===n?"HEALTHY":"RECOVERING"}}},typeFields:{PeerGroup:{hubs:async(e,t,o)=>await r(o).hub.listByGroup(e.id)},PeerHub:{groups:async(e,t,o)=>await r(o).hub.listGroups(e.id)}},subscriptionFields:{groupJoined:{resolve:e=>e,subscribe:()=>e(n,"groupJoined")},groupLeft:{resolve:e=>e,subscribe:()=>e(n,"groupLeft")},groupDataChanged:{resolve:e=>e,subscribe:(r,t)=>e(n,"groupDataChanged",e=>e.groupID===t.groupID)},groupMemberJoined:{resolve:e=>e,subscribe:(r,t)=>e(n,"groupMemberJoined",e=>e.groupID===t.groupID)},groupMemberLeft:{resolve:e=>e,subscribe:(r,t)=>e(n,"groupMemberLeft",e=>e.groupID===t.groupID)},circleCreated:{resolve:e=>e,subscribe:(r,t)=>e(n,"circleCreated",e=>e.groupID===t.groupID)},circleDeleted:{resolve:e=>e,subscribe:(r,t)=>e(n,"circleDeleted",e=>e.groupID===t.groupID)},circleDataChanged:{resolve:e=>e,subscribe:(r,t)=>e(n,"circleDataChanged",e=>e.circleID===t.circleID)},circleMemberAdded:{resolve:e=>e,subscribe:(r,t)=>e(n,"circleMemberAdded",e=>e.circleID===t.circleID)},circleMemberRemoved:{resolve:e=>e,subscribe:(r,t)=>e(n,"circleMemberRemoved",e=>e.circleID===t.circleID)},circleCatalogsChanged:{resolve:e=>e,subscribe:(r,t)=>e(n,"circleCatalogsChanged",e=>e.circleID===t.circleID)},ownDelegationTokenAdded:{resolve:e=>e,subscribe:(r,t,o)=>{let i=o.getViewer();return null==i?a():e(n,"delegationTokenAdded",e=>e.audience===i)}},ownDelegationTokenRevoked:{resolve:e=>e,subscribe:(r,t,o)=>{let i=o.getViewer();return null==i?a():e(n,"delegationTokenRevoked",e=>e.audience===i)}},groupHealthChanged:{resolve:e=>({groupID:e.groupID,condition:o[e.condition],state:t[e.state]}),subscribe:(r,t)=>{let o=t.groupID;return e(n,"groupHealthChanged",null==o?void 0:e=>e.groupID===o)}}}}}}