@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.
Files changed (156) hide show
  1. package/lib/context/group.js +1073 -746
  2. package/lib/context/hub.js +2 -1
  3. package/lib/context/join.js +50 -15
  4. package/lib/context/peer.d.ts +92 -0
  5. package/lib/context/peer.js +1216 -0
  6. package/lib/context/require-admin.d.ts +57 -0
  7. package/lib/context/require-admin.js +57 -0
  8. package/lib/context/sync.d.ts +12 -0
  9. package/lib/context/sync.js +234 -1
  10. package/lib/context/types.d.ts +56 -20
  11. package/lib/context/types.js +23 -3
  12. package/lib/groups/access-default-token.d.ts +44 -0
  13. package/lib/groups/access-default-token.js +103 -0
  14. package/lib/groups/admin-roster.d.ts +42 -117
  15. package/lib/groups/admin-roster.js +53 -168
  16. package/lib/groups/anchor-store.d.ts +15 -0
  17. package/lib/groups/anchor-store.js +37 -0
  18. package/lib/groups/app-cursor-store.d.ts +17 -0
  19. package/lib/groups/app-cursor-store.js +34 -0
  20. package/lib/groups/broadcast.d.ts +199 -81
  21. package/lib/groups/broadcast.js +443 -368
  22. package/lib/groups/catalog-token.d.ts +50 -0
  23. package/lib/groups/catalog-token.js +96 -0
  24. package/lib/groups/circle-projection.d.ts +32 -64
  25. package/lib/groups/circle-projection.js +95 -181
  26. package/lib/groups/circle-reducers.d.ts +71 -110
  27. package/lib/groups/circle-reducers.js +57 -127
  28. package/lib/groups/commit-adoption.d.ts +77 -0
  29. package/lib/groups/commit-adoption.js +108 -0
  30. package/lib/groups/commit-journal.d.ts +14 -0
  31. package/lib/groups/commit-journal.js +65 -0
  32. package/lib/groups/control-request.d.ts +66 -0
  33. package/lib/groups/control-request.js +101 -0
  34. package/lib/groups/events.d.ts +14 -1
  35. package/lib/groups/group-anchor.d.ts +18 -57
  36. package/lib/groups/group-anchor.js +15 -100
  37. package/lib/groups/group-crypto.d.ts +36 -13
  38. package/lib/groups/group-crypto.js +124 -50
  39. package/lib/groups/group-handle-registry.d.ts +106 -36
  40. package/lib/groups/group-handle-registry.js +351 -61
  41. package/lib/groups/group-handlers.d.ts +5 -4
  42. package/lib/groups/group-handlers.js +155 -29
  43. package/lib/groups/group-health-monitor.d.ts +1 -1
  44. package/lib/groups/group-health-monitor.js +1 -1
  45. package/lib/groups/group-mls.d.ts +35 -4
  46. package/lib/groups/group-mls.js +594 -62
  47. package/lib/groups/group-peer-manager.d.ts +212 -19
  48. package/lib/groups/group-peer-manager.js +1061 -69
  49. package/lib/groups/group-protocols.d.ts +485 -28
  50. package/lib/groups/group-protocols.js +289 -15
  51. package/lib/groups/invite-payload.d.ts +48 -0
  52. package/lib/groups/join-request-identity.d.ts +21 -0
  53. package/lib/groups/join-request-identity.js +43 -0
  54. package/lib/groups/join-utils.d.ts +60 -0
  55. package/lib/groups/join-utils.js +262 -0
  56. package/lib/groups/ledger-affected-events.d.ts +11 -6
  57. package/lib/groups/ledger-affected-events.js +15 -26
  58. package/lib/groups/ledger-commit-fold.d.ts +107 -0
  59. package/lib/groups/ledger-commit-fold.js +167 -0
  60. package/lib/groups/ledger-ingest.d.ts +124 -0
  61. package/lib/groups/ledger-ingest.js +212 -0
  62. package/lib/groups/ledger.d.ts +44 -0
  63. package/lib/groups/ledger.js +30 -0
  64. package/lib/groups/manager.d.ts +202 -19
  65. package/lib/groups/manager.js +623 -106
  66. package/lib/groups/mls-codec.d.ts +27 -6
  67. package/lib/groups/mls-codec.js +30 -15
  68. package/lib/groups/mls-encryptor.js +9 -6
  69. package/lib/groups/mls-group-handle.d.ts +37 -2
  70. package/lib/groups/mls-group-handle.js +12 -4
  71. package/lib/groups/mls-receive-errors.d.ts +0 -2
  72. package/lib/groups/mls-receive-errors.js +3 -6
  73. package/lib/groups/mls-state.d.ts +5 -3
  74. package/lib/groups/mls-state.js +27 -4
  75. package/lib/groups/peer-presence.d.ts +90 -0
  76. package/lib/groups/peer-presence.js +295 -0
  77. package/lib/groups/peer-selection.d.ts +37 -0
  78. package/lib/groups/peer-selection.js +72 -0
  79. package/lib/groups/roster-projection.d.ts +53 -0
  80. package/lib/groups/roster-projection.js +65 -0
  81. package/lib/groups/store-received-grant.d.ts +4 -3
  82. package/lib/groups/store-received-grant.js +80 -35
  83. package/lib/groups/store-received-revocation.d.ts +16 -13
  84. package/lib/groups/store-received-revocation.js +23 -22
  85. package/lib/hub/http-client.d.ts +10 -27
  86. package/lib/hub/http-client.js +2 -19
  87. package/lib/hub/hub-like.d.ts +57 -6
  88. package/lib/hub/hub-like.js +539 -52
  89. package/lib/hub/loopback-log-hub.d.ts +20 -0
  90. package/lib/hub/loopback-log-hub.js +178 -0
  91. package/lib/hub/manager.d.ts +25 -40
  92. package/lib/hub/manager.js +51 -94
  93. package/lib/hub/peer-scoped-hub-view.d.ts +35 -0
  94. package/lib/hub/peer-scoped-hub-view.js +92 -0
  95. package/lib/hub/sender-scoped-hub-view.d.ts +41 -0
  96. package/lib/hub/sender-scoped-hub-view.js +59 -0
  97. package/lib/hub/server-did.d.ts +67 -0
  98. package/lib/hub/server-did.js +131 -0
  99. package/lib/hub/wiring.d.ts +71 -14
  100. package/lib/hub/wiring.js +80 -23
  101. package/lib/index.d.ts +48 -13
  102. package/lib/index.js +257 -86
  103. package/lib/peer/connection-registry.d.ts +45 -0
  104. package/lib/peer/connection-registry.js +52 -0
  105. package/lib/peer/handlers.d.ts +69 -0
  106. package/lib/peer/handlers.js +110 -0
  107. package/lib/protocol.d.ts +213 -0
  108. package/lib/protocol.js +268 -0
  109. package/lib/schema.d.ts +2 -1
  110. package/lib/schema.js +805 -131
  111. package/lib/sync/access-default-sender.d.ts +23 -0
  112. package/lib/sync/access-default-sender.js +80 -0
  113. package/lib/sync/authorize.d.ts +57 -3
  114. package/lib/sync/authorize.js +45 -4
  115. package/lib/sync/broadcast-queue.d.ts +3 -4
  116. package/lib/sync/broadcast-queue.js +2 -3
  117. package/lib/sync/catalog-scope.d.ts +8 -7
  118. package/lib/sync/catalog-scope.js +55 -39
  119. package/lib/sync/errors.d.ts +9 -0
  120. package/lib/sync/errors.js +10 -0
  121. package/lib/sync/forwarder.d.ts +13 -19
  122. package/lib/sync/forwarder.js +13 -19
  123. package/lib/sync/handlers.js +54 -14
  124. package/lib/sync/hub-tunnel-sync-listener.d.ts +16 -6
  125. package/lib/sync/hub-tunnel-sync-listener.js +126 -37
  126. package/lib/sync/hub-tunnel-sync-provider.d.ts +31 -11
  127. package/lib/sync/hub-tunnel-sync-provider.js +68 -19
  128. package/lib/sync/merkle-channel.d.ts +9 -1
  129. package/lib/sync/merkle-channel.js +0 -0
  130. package/lib/sync/peer-registry.d.ts +14 -10
  131. package/lib/sync/peer-registry.js +0 -25
  132. package/lib/sync/sync-client.d.ts +11 -1
  133. package/lib/sync/sync-client.js +72 -10
  134. package/lib/sync/sync-manager.d.ts +22 -4
  135. package/lib/sync/sync-manager.js +39 -10
  136. package/lib/sync/tunnel-listeners.d.ts +52 -0
  137. package/lib/sync/tunnel-listeners.js +175 -0
  138. package/lib/sync/tunnel-observability.d.ts +25 -0
  139. package/lib/sync/tunnel-observability.js +63 -0
  140. package/lib/sync/tunnel-topics.d.ts +20 -0
  141. package/lib/sync/tunnel-topics.js +28 -0
  142. package/lib/types.d.ts +699 -28
  143. package/lib/types.js +0 -1
  144. package/lib/util/handler-error.d.ts +16 -0
  145. package/lib/util/handler-error.js +64 -0
  146. package/lib/util/mutex.d.ts +14 -1
  147. package/lib/util/mutex.js +37 -3
  148. package/package.json +56 -52
  149. package/lib/groups/ledger-entry.d.ts +0 -37
  150. package/lib/groups/ledger-entry.js +0 -56
  151. package/lib/groups/ledger-fold.d.ts +0 -56
  152. package/lib/groups/ledger-fold.js +0 -53
  153. package/lib/hub/did-observing-transport.d.ts +0 -75
  154. package/lib/hub/did-observing-transport.js +0 -126
  155. package/lib/hub/errors.d.ts +0 -24
  156. package/lib/hub/errors.js +0 -33
@@ -0,0 +1,69 @@
1
+ import type { OwnIdentity } from '@kokuin/token';
2
+ import type { KubunDB } from '@kubun/db';
3
+ import type { Logger } from '@kubun/logger';
4
+ import type { InviteAccessDefault } from '../groups/invite-payload.js';
5
+ export type CreatePeerHandlersParams = {
6
+ db: KubunDB;
7
+ identity: OwnIdentity;
8
+ logger: Logger;
9
+ /**
10
+ * DIDs permitted to reach the peer procedures. Mirrors the gating the join
11
+ * context applies (`prepareJoinRequest`, `completeJoin`): when set,
12
+ * only these DIDs may list the device's groups; when omitted, only the
13
+ * device's own identity (self / same-DID other device) may.
14
+ */
15
+ autoAcceptPeers?: Array<string>;
16
+ /**
17
+ * Run the SAME context op as the `prepareJoinRequest` GraphQL mutation, on
18
+ * behalf of the verified caller. Wired by the plugin factory to
19
+ * `createJoinContext(...).prepareRequest`; the handler does not reimplement
20
+ * MLS logic.
21
+ */
22
+ runPrepareJoin: (callerDID: string) => Promise<{
23
+ joinRequest: string;
24
+ }>;
25
+ /**
26
+ * Run the SAME context op as the `completeJoin` GraphQL mutation, on behalf of
27
+ * the verified caller. Wired to `createJoinContext(...).complete`.
28
+ */
29
+ runCompleteJoin: (callerDID: string, invitePayload: string) => Promise<{
30
+ group: {
31
+ id: string;
32
+ } | null;
33
+ }>;
34
+ /**
35
+ * Run the inviter leg of the inbound dance on behalf of the verified caller:
36
+ * add the caller to the group's share circles and run the invite as this
37
+ * device's own identity. Wired by the plugin factory to `serveGroupInvite`;
38
+ * the handler does not reimplement MLS or circle logic.
39
+ */
40
+ runInvite: (callerDID: string, groupID: string, joinRequest: string) => Promise<{
41
+ invitePayload: string;
42
+ }>;
43
+ /**
44
+ * Apply the access-default rows a verified caller pushed for itself. Wired by
45
+ * the plugin factory to `servePushControl`; the handler does not reimplement
46
+ * the untrusted-source apply — access-default rows are sender-bound to the
47
+ * caller.
48
+ */
49
+ runPushControl: (callerDID: string, param: {
50
+ groupID: string;
51
+ accessDefaults: Array<InviteAccessDefault>;
52
+ }) => Promise<{
53
+ accessDefaultsApplied: number;
54
+ }>;
55
+ };
56
+ /**
57
+ * Create the peer-protocol handlers served by plugin-p2p's own Enkaku server,
58
+ * a sibling to the sync handlers. `peer/groups` returns the serving device's
59
+ * DID and its groups so a connecting device can build its `connectPeer`
60
+ * payload.
61
+ *
62
+ * Unlike `sync/*` — whose procedure layer is intentionally open because
63
+ * authorization is enforced downstream per document — `peer/groups` has no
64
+ * downstream per-document gate: it lists group metadata directly. So the caller
65
+ * is authorized here, using the SAME gate the low-level dance mutations use
66
+ * (`checkPeerAccess`): the peer procedures wrap those same ops, and the group
67
+ * list is only served to DIDs already trusted to run the dance.
68
+ */
69
+ export declare function createPeerHandlers(params: CreatePeerHandlersParams): Record<string, unknown>;
@@ -0,0 +1,110 @@
1
+ import { getP2PStore } from '@kubun/store-p2p';
2
+ import { checkPeerAccess } from '../context/types.js';
3
+ import { SyncAccessDeniedError } from '../sync/errors.js';
4
+ import { wrapHandlers } from '../util/handler-error.js';
5
+ /**
6
+ * Create the peer-protocol handlers served by plugin-p2p's own Enkaku server,
7
+ * a sibling to the sync handlers. `peer/groups` returns the serving device's
8
+ * DID and its groups so a connecting device can build its `connectPeer`
9
+ * payload.
10
+ *
11
+ * Unlike `sync/*` — whose procedure layer is intentionally open because
12
+ * authorization is enforced downstream per document — `peer/groups` has no
13
+ * downstream per-document gate: it lists group metadata directly. So the caller
14
+ * is authorized here, using the SAME gate the low-level dance mutations use
15
+ * (`checkPeerAccess`): the peer procedures wrap those same ops, and the group
16
+ * list is only served to DIDs already trusted to run the dance.
17
+ */ export function createPeerHandlers(params) {
18
+ const { db, identity, logger, autoAcceptPeers, runPrepareJoin, runCompleteJoin, runInvite, runPushControl } = params;
19
+ // Derive the cryptographically-verified caller DID from the signed payload and
20
+ // reject a forged subject. Shared by every peer procedure — the same
21
+ // forged-`sub` guard `sync/negotiate` applies before any authorization.
22
+ function verifiedCaller(payload) {
23
+ const signed = payload;
24
+ if (signed.sub != null && signed.sub !== signed.iss) {
25
+ throw new SyncAccessDeniedError('forged-subject', 'Peer access denied: signed payload subject does not match issuer');
26
+ }
27
+ return signed.iss;
28
+ }
29
+ // Wrapped so a refusal carrying a registered KubunErrorCode reaches the
30
+ // calling device with its code and reason instead of enkaku's generic EK01.
31
+ return wrapHandlers({
32
+ 'peer/groups': async (ctx)=>{
33
+ const callerDID = verifiedCaller(ctx.message.payload);
34
+ // Same gate as the dance mutations: only a trusted caller (in
35
+ // `autoAcceptPeers`, or the device's own DID when unset) may list groups.
36
+ checkPeerAccess(callerDID, identity.id, autoAcceptPeers);
37
+ const store = await getP2PStore(db);
38
+ const rows = await store.listGroups();
39
+ logger.debug('peer/groups served', {
40
+ callerDID,
41
+ groupCount: rows.length
42
+ });
43
+ return {
44
+ did: identity.id,
45
+ groups: rows.map((row)=>({
46
+ id: row.id,
47
+ name: row.name
48
+ }))
49
+ };
50
+ },
51
+ // The joining device mints its KeyPackage bundle. `runPrepareJoin` runs the
52
+ // same context op as `prepareJoinRequest`, which itself re-checks
53
+ // `checkPeerAccess` for the verified caller and stashes the pending bundle
54
+ // keyed by that caller DID.
55
+ 'group/prepare-join': async (ctx)=>{
56
+ const callerDID = verifiedCaller(ctx.message.payload);
57
+ const result = await runPrepareJoin(callerDID);
58
+ logger.debug('group/prepare-join served', {
59
+ callerDID
60
+ });
61
+ return {
62
+ joinRequest: result.joinRequest
63
+ };
64
+ },
65
+ // The joining device processes the Welcome the caller returns. `runCompleteJoin`
66
+ // runs the same context op as `completeJoin` (`joinGroup` + invite seeding),
67
+ // re-checking `checkPeerAccess` and consuming the pending bundle.
68
+ 'group/complete-join': async (ctx)=>{
69
+ const callerDID = verifiedCaller(ctx.message.payload);
70
+ const result = await runCompleteJoin(callerDID, ctx.param.invitePayload);
71
+ logger.debug('group/complete-join served', {
72
+ callerDID,
73
+ groupID: result.group?.id ?? null
74
+ });
75
+ return {
76
+ groupID: result.group?.id ?? null
77
+ };
78
+ },
79
+ // The inviter leg of the inbound dance. Same gate as `peer/groups`: only a
80
+ // trusted caller may pull an invite. `runInvite` adds the caller to the
81
+ // group's share circles and runs the invite as this device's own identity,
82
+ // returning the Welcome + seeds for the caller to complete the join.
83
+ 'group/invite': async (ctx)=>{
84
+ const callerDID = verifiedCaller(ctx.message.payload);
85
+ checkPeerAccess(callerDID, identity.id, autoAcceptPeers);
86
+ const result = await runInvite(callerDID, ctx.param.groupID, ctx.param.joinRequest);
87
+ logger.debug('group/invite served', {
88
+ callerDID,
89
+ groupID: ctx.param.groupID
90
+ });
91
+ return {
92
+ invitePayload: result.invitePayload
93
+ };
94
+ },
95
+ // A verified caller applies access-default rows it minted for itself. Same
96
+ // gate as `group/invite`. `runPushControl` applies those rows sender-bound to
97
+ // `callerDID`, so a trusted peer cannot inject another owner's sharing policy.
98
+ 'group/push-control': async (ctx)=>{
99
+ const callerDID = verifiedCaller(ctx.message.payload);
100
+ checkPeerAccess(callerDID, identity.id, autoAcceptPeers);
101
+ const result = await runPushControl(callerDID, ctx.param);
102
+ logger.debug('group/push-control served', {
103
+ callerDID,
104
+ groupID: ctx.param.groupID,
105
+ accessDefaultsApplied: result.accessDefaultsApplied
106
+ });
107
+ return result;
108
+ }
109
+ });
110
+ }
package/lib/protocol.d.ts CHANGED
@@ -205,6 +205,217 @@ export declare const syncProtocol: {
205
205
  };
206
206
  };
207
207
  export type SyncProtocol = typeof syncProtocol;
208
+ /**
209
+ * Peer protocol — the wire leg of the high-level peer ops. Served as a sibling
210
+ * to {@link syncProtocol} and advertised in `/info` under its own name so a
211
+ * connecting device can discover it independently of the data-sync protocol.
212
+ *
213
+ * The GraphQL mutations remain the app-facing surface; these procedures are the
214
+ * device-to-device leg the mutations dispatch to. `peer/groups` returns the
215
+ * serving device's DID and its groups for the `connectPeer` payload. The MLS
216
+ * dance procedures (`group/prepare-join`, `group/invite`, `group/complete-join`)
217
+ * extend this same protocol in later work.
218
+ */
219
+ export declare const peerProtocol: {
220
+ readonly 'peer/groups': {
221
+ readonly type: "request";
222
+ readonly param: {
223
+ readonly type: "object";
224
+ readonly properties: {};
225
+ readonly additionalProperties: false;
226
+ };
227
+ readonly result: {
228
+ readonly type: "object";
229
+ readonly properties: {
230
+ readonly did: {
231
+ readonly type: "string";
232
+ readonly description: "DID of the serving device";
233
+ };
234
+ readonly groups: {
235
+ readonly type: "array";
236
+ readonly items: {
237
+ readonly type: "object";
238
+ readonly properties: {
239
+ readonly id: {
240
+ readonly type: "string";
241
+ };
242
+ readonly name: {
243
+ readonly type: "string";
244
+ };
245
+ };
246
+ readonly required: readonly ["id", "name"];
247
+ readonly additionalProperties: false;
248
+ };
249
+ readonly description: "The serving device's groups (id + name)";
250
+ };
251
+ };
252
+ readonly required: readonly ["did", "groups"];
253
+ readonly additionalProperties: false;
254
+ };
255
+ };
256
+ readonly 'group/prepare-join': {
257
+ readonly type: "request";
258
+ readonly param: {
259
+ readonly type: "object";
260
+ readonly properties: {};
261
+ readonly additionalProperties: false;
262
+ };
263
+ readonly result: {
264
+ readonly type: "object";
265
+ readonly properties: {
266
+ readonly joinRequest: {
267
+ readonly type: "string";
268
+ readonly description: "The serving device's encoded join request (its KeyPackage)";
269
+ };
270
+ };
271
+ readonly required: readonly ["joinRequest"];
272
+ readonly additionalProperties: false;
273
+ };
274
+ };
275
+ readonly 'group/complete-join': {
276
+ readonly type: "request";
277
+ readonly param: {
278
+ readonly type: "object";
279
+ readonly properties: {
280
+ readonly invitePayload: {
281
+ readonly type: "string";
282
+ readonly description: "The invite payload (Welcome + seeds) minted by the caller";
283
+ };
284
+ };
285
+ readonly required: readonly ["invitePayload"];
286
+ readonly additionalProperties: false;
287
+ };
288
+ readonly result: {
289
+ readonly type: "object";
290
+ readonly properties: {
291
+ readonly groupID: {
292
+ readonly type: readonly ["string", "null"];
293
+ readonly description: "The joined group ID, or null when the group row was not found";
294
+ };
295
+ };
296
+ readonly required: readonly ["groupID"];
297
+ readonly additionalProperties: false;
298
+ };
299
+ };
300
+ readonly 'group/invite': {
301
+ readonly type: "request";
302
+ readonly param: {
303
+ readonly type: "object";
304
+ readonly properties: {
305
+ readonly groupID: {
306
+ readonly type: "string";
307
+ readonly description: "The group the caller wants to join";
308
+ };
309
+ readonly joinRequest: {
310
+ readonly type: "string";
311
+ readonly description: "The caller's encoded join request (its KeyPackage)";
312
+ };
313
+ };
314
+ readonly required: readonly ["groupID", "joinRequest"];
315
+ readonly additionalProperties: false;
316
+ };
317
+ readonly result: {
318
+ readonly type: "object";
319
+ readonly properties: {
320
+ readonly invitePayload: {
321
+ readonly type: "string";
322
+ readonly description: "The invite payload (Welcome + seeds) for the caller to complete the join";
323
+ };
324
+ };
325
+ readonly required: readonly ["invitePayload"];
326
+ readonly additionalProperties: false;
327
+ };
328
+ };
329
+ readonly 'group/push-control': {
330
+ readonly type: "request";
331
+ readonly param: {
332
+ readonly type: "object";
333
+ readonly properties: {
334
+ readonly groupID: {
335
+ readonly type: "string";
336
+ readonly description: "The group whose control state is being pushed";
337
+ };
338
+ readonly accessDefaults: {
339
+ readonly type: "array";
340
+ readonly items: {
341
+ readonly type: "object";
342
+ readonly properties: {
343
+ readonly ownerDID: {
344
+ readonly type: "string";
345
+ };
346
+ readonly modelID: {
347
+ readonly type: "string";
348
+ };
349
+ readonly permissionType: {
350
+ readonly type: "string";
351
+ readonly enum: readonly ["read", "write"];
352
+ };
353
+ readonly rule: {
354
+ readonly type: "object";
355
+ readonly properties: {
356
+ readonly level: {
357
+ readonly type: "string";
358
+ };
359
+ readonly allowedDIDs: {
360
+ readonly type: readonly ["array", "null"];
361
+ readonly items: {
362
+ readonly type: "string";
363
+ };
364
+ };
365
+ readonly allowedCircles: {
366
+ readonly type: readonly ["array", "null"];
367
+ readonly items: {
368
+ readonly type: "string";
369
+ };
370
+ };
371
+ readonly allowedGroups: {
372
+ readonly type: readonly ["array", "null"];
373
+ readonly items: {
374
+ readonly type: "string";
375
+ };
376
+ };
377
+ };
378
+ readonly required: readonly ["level", "allowedDIDs", "allowedCircles", "allowedGroups"];
379
+ readonly additionalProperties: false;
380
+ };
381
+ readonly hlc: {
382
+ readonly type: "string";
383
+ };
384
+ readonly auth: {
385
+ readonly type: "string";
386
+ };
387
+ };
388
+ readonly required: readonly ["ownerDID", "modelID", "permissionType", "rule", "hlc", "auth"];
389
+ readonly additionalProperties: false;
390
+ };
391
+ readonly description: "The caller's own signed access-default rows (sender-bound on apply)";
392
+ };
393
+ };
394
+ readonly required: readonly ["groupID", "accessDefaults"];
395
+ readonly additionalProperties: false;
396
+ };
397
+ readonly result: {
398
+ readonly type: "object";
399
+ readonly properties: {
400
+ readonly accessDefaultsApplied: {
401
+ readonly type: "number";
402
+ readonly description: "Count of pushed access-default rows applied (sender-bound, LWW)";
403
+ };
404
+ };
405
+ readonly required: readonly ["accessDefaultsApplied"];
406
+ readonly additionalProperties: false;
407
+ };
408
+ };
409
+ };
410
+ export type PeerProtocol = typeof peerProtocol;
411
+ export type PeerGroupsResult = FromSchema<PeerProtocol['peer/groups']['result']>;
412
+ export type PrepareJoinResult = FromSchema<PeerProtocol['group/prepare-join']['result']>;
413
+ export type CompleteJoinParam = FromSchema<PeerProtocol['group/complete-join']['param']>;
414
+ export type CompleteJoinResult = FromSchema<PeerProtocol['group/complete-join']['result']>;
415
+ export type GroupInviteParam = FromSchema<PeerProtocol['group/invite']['param']>;
416
+ export type GroupInviteResult = FromSchema<PeerProtocol['group/invite']['result']>;
417
+ export type PushControlParam = FromSchema<PeerProtocol['group/push-control']['param']>;
418
+ export type PushControlResult = FromSchema<PeerProtocol['group/push-control']['result']>;
208
419
  export type SyncNegotiateParams = FromSchema<SyncProtocol['sync/negotiate']['param']>;
209
420
  export type SyncNegotiateResult = FromSchema<SyncProtocol['sync/negotiate']['result']>;
210
421
  export type SyncMerkleSyncParams = FromSchema<SyncProtocol['sync/merkle-sync']['param']>;
@@ -214,3 +425,5 @@ export type SyncMerkleSyncResult = FromSchema<SyncProtocol['sync/merkle-sync']['
214
425
  export type SyncDirection = 'push' | 'pull' | 'both';
215
426
  export type SyncClientMessage = AnyClientMessageOf<SyncProtocol>;
216
427
  export type SyncServerMessage = AnyServerMessageOf<SyncProtocol>;
428
+ export type PeerClientMessage = AnyClientMessageOf<PeerProtocol>;
429
+ export type PeerServerMessage = AnyServerMessageOf<PeerProtocol>;
package/lib/protocol.js CHANGED
@@ -249,3 +249,271 @@ export const syncProtocol = {
249
249
  }
250
250
  }
251
251
  };
252
+ /**
253
+ * Peer protocol — the wire leg of the high-level peer ops. Served as a sibling
254
+ * to {@link syncProtocol} and advertised in `/info` under its own name so a
255
+ * connecting device can discover it independently of the data-sync protocol.
256
+ *
257
+ * The GraphQL mutations remain the app-facing surface; these procedures are the
258
+ * device-to-device leg the mutations dispatch to. `peer/groups` returns the
259
+ * serving device's DID and its groups for the `connectPeer` payload. The MLS
260
+ * dance procedures (`group/prepare-join`, `group/invite`, `group/complete-join`)
261
+ * extend this same protocol in later work.
262
+ */ export const peerProtocol = {
263
+ 'peer/groups': {
264
+ type: 'request',
265
+ param: {
266
+ type: 'object',
267
+ properties: {},
268
+ additionalProperties: false
269
+ },
270
+ result: {
271
+ type: 'object',
272
+ properties: {
273
+ did: {
274
+ type: 'string',
275
+ description: 'DID of the serving device'
276
+ },
277
+ groups: {
278
+ type: 'array',
279
+ items: {
280
+ type: 'object',
281
+ properties: {
282
+ id: {
283
+ type: 'string'
284
+ },
285
+ name: {
286
+ type: 'string'
287
+ }
288
+ },
289
+ required: [
290
+ 'id',
291
+ 'name'
292
+ ],
293
+ additionalProperties: false
294
+ },
295
+ description: "The serving device's groups (id + name)"
296
+ }
297
+ },
298
+ required: [
299
+ 'did',
300
+ 'groups'
301
+ ],
302
+ additionalProperties: false
303
+ }
304
+ },
305
+ // The peer leg of the MLS invite dance. `group/prepare-join` and
306
+ // `group/complete-join` wrap the SAME context ops as the `prepareJoinRequest`
307
+ // and `completeJoin` GraphQL mutations — the serving (joining) device mints
308
+ // its KeyPackage bundle, then processes the Welcome the caller returns. Gated
309
+ // exactly like `peer/groups` (forged-`sub` rejection + `checkPeerAccess`).
310
+ 'group/prepare-join': {
311
+ type: 'request',
312
+ param: {
313
+ type: 'object',
314
+ properties: {},
315
+ additionalProperties: false
316
+ },
317
+ result: {
318
+ type: 'object',
319
+ properties: {
320
+ joinRequest: {
321
+ type: 'string',
322
+ description: "The serving device's encoded join request (its KeyPackage)"
323
+ }
324
+ },
325
+ required: [
326
+ 'joinRequest'
327
+ ],
328
+ additionalProperties: false
329
+ }
330
+ },
331
+ 'group/complete-join': {
332
+ type: 'request',
333
+ param: {
334
+ type: 'object',
335
+ properties: {
336
+ invitePayload: {
337
+ type: 'string',
338
+ description: 'The invite payload (Welcome + seeds) minted by the caller'
339
+ }
340
+ },
341
+ required: [
342
+ 'invitePayload'
343
+ ],
344
+ additionalProperties: false
345
+ },
346
+ result: {
347
+ type: 'object',
348
+ properties: {
349
+ groupID: {
350
+ type: [
351
+ 'string',
352
+ 'null'
353
+ ],
354
+ description: 'The joined group ID, or null when the group row was not found'
355
+ }
356
+ },
357
+ required: [
358
+ 'groupID'
359
+ ],
360
+ additionalProperties: false
361
+ }
362
+ },
363
+ // The inviter leg of the inbound dance (`joinPeerGroup`). The caller (joiner)
364
+ // sends the group it wants to join plus its KeyPackage bundle; the serving
365
+ // device (inviter) adds the caller to the group's share circles, runs the
366
+ // invite as ITSELF, and returns the Welcome + invite payload. Gated exactly
367
+ // like the other peer procedures (forged-`sub` rejection + `checkPeerAccess`
368
+ // against the inviter's `autoAcceptPeers`), so only a trusted caller can pull
369
+ // an invite from this device.
370
+ 'group/invite': {
371
+ type: 'request',
372
+ param: {
373
+ type: 'object',
374
+ properties: {
375
+ groupID: {
376
+ type: 'string',
377
+ description: 'The group the caller wants to join'
378
+ },
379
+ joinRequest: {
380
+ type: 'string',
381
+ description: "The caller's encoded join request (its KeyPackage)"
382
+ }
383
+ },
384
+ required: [
385
+ 'groupID',
386
+ 'joinRequest'
387
+ ],
388
+ additionalProperties: false
389
+ },
390
+ result: {
391
+ type: 'object',
392
+ properties: {
393
+ invitePayload: {
394
+ type: 'string',
395
+ description: 'The invite payload (Welcome + seeds) for the caller to complete the join'
396
+ }
397
+ },
398
+ required: [
399
+ 'invitePayload'
400
+ ],
401
+ additionalProperties: false
402
+ }
403
+ },
404
+ // The caller hands the serving device its own access-default rows over the
405
+ // held connection, so the serving device applies them without waiting on hub
406
+ // fan-out (which is dropped for a group with no live binding). The source is
407
+ // UNTRUSTED: access-default rows are sender-bound (owner must equal the
408
+ // verified token issuer AND the authenticated caller). Gated exactly like
409
+ // `group/invite` (forged-`sub` rejection + `checkPeerAccess`).
410
+ 'group/push-control': {
411
+ type: 'request',
412
+ param: {
413
+ type: 'object',
414
+ properties: {
415
+ groupID: {
416
+ type: 'string',
417
+ description: 'The group whose control state is being pushed'
418
+ },
419
+ accessDefaults: {
420
+ type: 'array',
421
+ items: {
422
+ type: 'object',
423
+ properties: {
424
+ ownerDID: {
425
+ type: 'string'
426
+ },
427
+ modelID: {
428
+ type: 'string'
429
+ },
430
+ permissionType: {
431
+ type: 'string',
432
+ enum: [
433
+ 'read',
434
+ 'write'
435
+ ]
436
+ },
437
+ rule: {
438
+ type: 'object',
439
+ properties: {
440
+ level: {
441
+ type: 'string'
442
+ },
443
+ allowedDIDs: {
444
+ type: [
445
+ 'array',
446
+ 'null'
447
+ ],
448
+ items: {
449
+ type: 'string'
450
+ }
451
+ },
452
+ allowedCircles: {
453
+ type: [
454
+ 'array',
455
+ 'null'
456
+ ],
457
+ items: {
458
+ type: 'string'
459
+ }
460
+ },
461
+ allowedGroups: {
462
+ type: [
463
+ 'array',
464
+ 'null'
465
+ ],
466
+ items: {
467
+ type: 'string'
468
+ }
469
+ }
470
+ },
471
+ required: [
472
+ 'level',
473
+ 'allowedDIDs',
474
+ 'allowedCircles',
475
+ 'allowedGroups'
476
+ ],
477
+ additionalProperties: false
478
+ },
479
+ hlc: {
480
+ type: 'string'
481
+ },
482
+ auth: {
483
+ type: 'string'
484
+ }
485
+ },
486
+ required: [
487
+ 'ownerDID',
488
+ 'modelID',
489
+ 'permissionType',
490
+ 'rule',
491
+ 'hlc',
492
+ 'auth'
493
+ ],
494
+ additionalProperties: false
495
+ },
496
+ description: "The caller's own signed access-default rows (sender-bound on apply)"
497
+ }
498
+ },
499
+ required: [
500
+ 'groupID',
501
+ 'accessDefaults'
502
+ ],
503
+ additionalProperties: false
504
+ },
505
+ result: {
506
+ type: 'object',
507
+ properties: {
508
+ accessDefaultsApplied: {
509
+ type: 'number',
510
+ description: 'Count of pushed access-default rows applied (sender-bound, LWW)'
511
+ }
512
+ },
513
+ required: [
514
+ 'accessDefaultsApplied'
515
+ ],
516
+ additionalProperties: false
517
+ }
518
+ }
519
+ };
package/lib/schema.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { SchemaExtension } from '@kubun/engine';
2
2
  import { type Logger } from '@kubun/logger';
3
3
  import { type P2PEventEmitter } from './groups/events.js';
4
- export declare function createP2PSchemaExtension(emitter: P2PEventEmitter, logger?: Logger): SchemaExtension;
4
+ import type { SyncManager } from './sync/sync-manager.js';
5
+ export declare function createP2PSchemaExtension(emitter: P2PEventEmitter, syncManager: SyncManager, logger?: Logger): SchemaExtension;