@kubun/plugin-p2p 0.8.3 → 0.10.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 (88) hide show
  1. package/lib/context/delegation.d.ts +4 -0
  2. package/lib/context/delegation.js +1 -0
  3. package/lib/context/group.js +1 -1
  4. package/lib/context/hub.d.ts +4 -0
  5. package/lib/context/hub.js +1 -0
  6. package/lib/context/join.js +1 -1
  7. package/lib/context/types.d.ts +28 -2
  8. package/lib/groups/broadcast-service.d.ts +130 -17
  9. package/lib/groups/broadcast-service.js +1 -1
  10. package/lib/groups/broadcast.d.ts +162 -5
  11. package/lib/groups/broadcast.js +1 -1
  12. package/lib/groups/events.d.ts +26 -5
  13. package/lib/groups/events.js +1 -1
  14. package/lib/groups/group-handle-registry.d.ts +90 -0
  15. package/lib/groups/group-handle-registry.js +1 -0
  16. package/lib/groups/invite-payload.d.ts +21 -1
  17. package/lib/groups/join-utils.d.ts +68 -2
  18. package/lib/groups/join-utils.js +1 -1
  19. package/lib/groups/manager.d.ts +26 -18
  20. package/lib/groups/manager.js +1 -1
  21. package/lib/groups/mls-codec.d.ts +7 -0
  22. package/lib/groups/mls-codec.js +1 -0
  23. package/lib/groups/mls-encryptor.d.ts +25 -0
  24. package/lib/groups/mls-encryptor.js +1 -0
  25. package/lib/groups/store-received-grant.d.ts +47 -0
  26. package/lib/groups/store-received-grant.js +1 -0
  27. package/lib/groups/store-received-revocation.d.ts +46 -0
  28. package/lib/groups/store-received-revocation.js +1 -0
  29. package/lib/groups/wire-frame.d.ts +32 -0
  30. package/lib/groups/wire-frame.js +1 -0
  31. package/lib/hub/connection-pool.d.ts +24 -0
  32. package/lib/hub/connection-pool.js +1 -1
  33. package/lib/hub/did-observing-transport.d.ts +64 -0
  34. package/lib/hub/did-observing-transport.js +1 -0
  35. package/lib/hub/errors.d.ts +28 -0
  36. package/lib/hub/errors.js +1 -0
  37. package/lib/hub/forward-remote-broadcast.d.ts +15 -0
  38. package/lib/hub/forward-remote-broadcast.js +1 -0
  39. package/lib/hub/group-channel.d.ts +37 -21
  40. package/lib/hub/group-channel.js +1 -1
  41. package/lib/hub/http-client.d.ts +17 -0
  42. package/lib/hub/http-client.js +1 -0
  43. package/lib/hub/hub-connection.d.ts +96 -0
  44. package/lib/hub/hub-connection.js +1 -0
  45. package/lib/hub/manager.d.ts +117 -0
  46. package/lib/hub/manager.js +1 -0
  47. package/lib/hub/receive-handler.d.ts +3 -6
  48. package/lib/hub/receive-handler.js +1 -1
  49. package/lib/hub/relay-manager.d.ts +92 -2
  50. package/lib/hub/relay-manager.js +1 -1
  51. package/lib/hub/send-handler.d.ts +28 -16
  52. package/lib/hub/send-handler.js +1 -1
  53. package/lib/hub/tunnel-inbox.d.ts +20 -0
  54. package/lib/hub/tunnel-inbox.js +1 -0
  55. package/lib/hub/wait-for-gate.d.ts +14 -0
  56. package/lib/hub/wait-for-gate.js +1 -0
  57. package/lib/hub/wiring.d.ts +85 -0
  58. package/lib/hub/wiring.js +1 -0
  59. package/lib/index.d.ts +102 -7
  60. package/lib/index.js +1 -1
  61. package/lib/schema.d.ts +2 -2
  62. package/lib/schema.js +113 -21
  63. package/lib/sync/broadcast-queue.d.ts +59 -0
  64. package/lib/sync/broadcast-queue.js +1 -0
  65. package/lib/sync/broadcast-sender.d.ts +52 -0
  66. package/lib/sync/broadcast-sender.js +1 -0
  67. package/lib/sync/forwarder.d.ts +97 -0
  68. package/lib/sync/forwarder.js +1 -0
  69. package/lib/sync/handlers.d.ts +20 -1
  70. package/lib/sync/handlers.js +1 -1
  71. package/lib/sync/hub-tunnel-sync-listener.d.ts +24 -0
  72. package/lib/sync/hub-tunnel-sync-listener.js +1 -0
  73. package/lib/sync/hub-tunnel-sync-provider.d.ts +36 -0
  74. package/lib/sync/hub-tunnel-sync-provider.js +1 -0
  75. package/lib/sync/merkle-apply.d.ts +41 -0
  76. package/lib/sync/merkle-apply.js +1 -1
  77. package/lib/sync/merkle-channel.d.ts +15 -0
  78. package/lib/sync/merkle-channel.js +1 -1
  79. package/lib/sync/receive-access-gate.d.ts +36 -0
  80. package/lib/sync/receive-access-gate.js +1 -0
  81. package/lib/sync/scope-resolver.d.ts +32 -0
  82. package/lib/sync/scope-resolver.js +1 -0
  83. package/lib/sync/sync-manager.d.ts +10 -0
  84. package/lib/sync/sync-manager.js +1 -1
  85. package/lib/types.d.ts +151 -1
  86. package/lib/util/mutex.d.ts +4 -0
  87. package/lib/util/mutex.js +1 -0
  88. package/package.json +40 -37
@@ -1 +1 @@
1
- import{HubConnectionPool as e}from"./connection-pool.js";import{GroupChannel as a}from"./group-channel.js";export class HubRelayManager{#e;#a;#t;#s;#r;#o;#i;#l;#c;#h;#u;#p=new Map;constructor(t){this.#e=t.p2pStore,this.#a=t.broadcastService,this.#t=t.deviceID,this.#r=t.logger,this.#o=t.ackFlushMs??500,this.#i=t.ackFlushMax??10,this.#l=t.backoffBaseMs??1e3,this.#c=t.backoffMaxMs??6e4,this.#h=t.backoffJitter??.25,this.#s=new e({createHubClient:t.createHubClient,logger:t.logger}),this.#u=t.createGroupChannel??(e=>new a(e))}async start(e){(await Promise.allSettled(e.map(e=>this.addGroup(e)))).forEach((a,t)=>{"rejected"===a.status&&this.#r?.error("addGroup failed during start",{groupID:e[t],error:a.reason})})}async addGroup(e){if(this.#p.has(e))return void this.#r?.debug("group already added",{groupID:e});let a=await this.#e.getGroup(e);if(null==a)throw Error(`Group ${e} not found`);let t=a.hub_urls;if(null==t||0===t.length)return void this.#r?.warn("group has no hub URLs, skipping",{groupID:e});t.length>1&&this.#r?.warn("multi-hub not yet supported, using first",{groupID:e,hubURLs:t});let s=this.#u({groupID:e,hubURL:t[0],deviceID:this.#t,pool:this.#s,broadcastService:this.#a,p2pStore:this.#e,logger:this.#r,ackFlushMs:this.#o,ackFlushMax:this.#i,backoffBaseMs:this.#l,backoffMaxMs:this.#c,backoffJitter:this.#h});await s.open(),this.#p.set(e,s)}async removeGroup(e){let a=this.#p.get(e);null!=a&&(this.#p.delete(e),await a.close())}async broadcast(e,a){let t=this.#p.get(e);if(null==t)throw Error(`Group ${e} is not active in HubRelayManager`);await t.broadcast(a)}async stop(){let e=[...this.#p.values()];this.#p.clear(),(await Promise.allSettled(e.map(e=>e.close()))).forEach(e=>{"rejected"===e.status&&this.#r?.warn("GroupChannel close failed",{error:e.reason})}),await this.#s.disposeAll()}}
1
+ import{EventEmitter as e}from"@enkaku/event";import{HubConnectionPool as t}from"./connection-pool.js";import{GroupChannel as s}from"./group-channel.js";import{HubConnection as r}from"./hub-connection.js";export class HubRelayManager{#e;#t;#s;#r;#o;#a;#i;#n;#l;#c;#u;#h;#p;#g=new Map;#d=new Map;#b=new Map;#f=new Map;relayEvents;constructor(o){this.#e=o.p2pStore,this.#t=o.broadcastService,this.#s=o.deviceID,this.#o=o.logger,this.#a=o.ackFlushMs??500,this.#i=o.ackFlushMax??10,this.#n=o.backoffBaseMs??1e3,this.#l=o.backoffMaxMs??6e4,this.#c=o.backoffJitter??.25,this.#u=o.broadcastTimeoutMs;let a=o.p2pStore;this.#r=new t({createHubClient:o.createHubClient,logger:o.logger,onServerDIDObserved:o.onServerDIDObserved,resolvePinnedDID:async e=>{let t=await a.getHubByURL(e);return t?.server_did??null}}),this.#h=o.createGroupChannel??(e=>new s(e)),this.#p=o.createHubConnection??(e=>new r(e)),this.relayEvents=new e}async #M(e){let t=this.#g.get(e);if(null!=t)return t;let s=this.#p({hubURL:e,deviceID:this.#s,pool:this.#r,logger:this.#o,ackFlushMs:this.#a,ackFlushMax:this.#i,backoffBaseMs:this.#n,backoffMaxMs:this.#l,backoffJitter:this.#c});try{await s.open()}catch(e){try{await s.dispose()}catch{}throw e}let r=s.events.on("lifecycle",async t=>{let s=Object.freeze([...this.#b.entries()].filter(([,t])=>t===e).map(([e])=>e));await this.relayEvents.emit("relay",{...t,hubURL:e,groupIDs:s})});return this.#f.set(e,r),this.#g.set(e,s),s}async start(e){(await Promise.allSettled(e.map(e=>this.addGroup(e)))).forEach((t,s)=>{"rejected"===t.status&&this.#o?.error("addGroup failed during start",{groupID:e[s],error:t.reason})})}async addGroup(e){if(this.#d.has(e))return void this.#o?.debug("group already added",{groupID:e});if(null==await this.#e.getGroup(e))throw Error(`Group ${e} not found`);let t=await this.#e.listHubsByGroupID(e);0===t.length?this.#o?.warn("group has no hub bindings, skipping",{groupID:e}):(t.length>1&&this.#o?.warn("multi-hub not yet supported, using first",{groupID:e,hubURLs:t.map(e=>e.url)}),await this.#m(e,t[0].url))}async #m(e,t){let s=await this.#M(t),r=this.#h({groupID:e,deviceID:this.#s,hubConnection:s,broadcastService:this.#t,p2pStore:this.#e,logger:this.#o});await r.open(),this.#d.set(e,r),this.#b.set(e,t)}async addBinding(e,t){let s=this.#b.get(e);null!=s?s===t?this.#o?.debug("binding already active",{groupID:e,hubURL:t}):this.#o?.warn("multi-hub not yet supported, ignoring extra binding",{groupID:e,activeHubURL:s,newHubURL:t}):await this.#m(e,t)}async removeBinding(e,t){this.#b.get(e)!==t?this.#o?.debug("binding not active, ignoring removal",{groupID:e,hubURL:t}):await this.removeGroup(e)}async removeGroup(e){let t=this.#d.get(e);null!=t&&(await t.close(),this.#d.delete(e),this.#b.delete(e))}async broadcast(e,t,s){let r=this.#d.get(e);if(null==r)throw Error(`Group ${e} is not active in HubRelayManager`);let o=s?.timeoutMs??this.#u;await r.broadcast(t,{timeoutMs:o})}async sendCommit(e,t,s){let r=this.#d.get(e);if(null==r)throw Error(`Group ${e} is not active in HubRelayManager`);let o=s?.timeoutMs??this.#u;await r.sendCommit(t,{timeoutMs:o})}async stop(){for(let e of this.#f.values())e();this.#f.clear();let e=[...this.#d.values()];this.#d.clear(),this.#b.clear(),(await Promise.allSettled(e.map(e=>e.close()))).forEach(e=>{"rejected"===e.status&&this.#o?.warn("GroupChannel close failed",{error:e.reason})});let t=[...this.#g.values()];this.#g.clear(),(await Promise.allSettled(t.map(e=>e.close()))).forEach(e=>{"rejected"===e.status&&this.#o?.warn("HubConnection close failed",{error:e.reason})}),await this.#r.disposeAll()}}
@@ -1,28 +1,40 @@
1
- import type { Client } from '@enkaku/client';
2
- import type { HubProtocol } from '@enkaku/hub-protocol';
3
1
  import type { Logger } from '@kubun/logger';
4
- import type { P2PStoreAPI } from '@kubun/store-p2p';
5
2
  import type { GroupBroadcastMessage } from '../groups/broadcast.js';
6
3
  import type { BroadcastService } from '../groups/broadcast-service.js';
7
4
  export type HandleSendBroadcastParams = {
8
5
  groupID: string;
9
- deviceID: string;
10
6
  message: GroupBroadcastMessage;
11
- p2pStore: P2PStoreAPI;
12
7
  broadcastService: BroadcastService;
13
- hubClient: Client<HubProtocol>;
8
+ /**
9
+ * Wire-send callback. Receives the groupID and the base64-encoded encrypted
10
+ * MLS application message, and is expected to fan it out via the underlying
11
+ * hub transport (e.g. `HubConnection.broadcast`). Decoupling from the hub
12
+ * client lets callers serialize the send through a per-group mutex shared
13
+ * with the receive path.
14
+ */
15
+ send: (groupID: string, payloadB64: string) => Promise<void>;
14
16
  logger?: Logger;
15
17
  };
16
18
  /**
17
- * Encrypt + send a broadcast for a group, then persist the advanced MLS state.
18
- *
19
- * Failure semantics:
20
- * - Missing MLS state for (group, device) throw, caller cannot broadcast.
21
- * - `prepareSend` failure → throw, no save (epoch unchanged on disk).
22
- * - `hub/group/send` failure → throw, do NOT save advanced MLS state.
23
- * Prevents an MLS epoch wedge where local state outpaces recipients.
24
- * - `saveMLSState` failure → throw. State has advanced in memory; on next call
25
- * the load-from-disk path will re-derive a fresh handle from the prior epoch
26
- * and re-encrypt. (Wedge protection only applies pre-save by design.)
19
+ * Encrypt + send a broadcast for a group. MLS state persistence is handled
20
+ * by the registry inside `BroadcastService.prepareSend`. Wedge protection
21
+ * (failed send → no advance of persisted state) is preserved because send
22
+ * runs INSIDE the registry's per-group lock and a throw skips persist.
27
23
  */
28
24
  export declare function handleSendBroadcast(params: HandleSendBroadcastParams): Promise<void>;
25
+ export type HandleSendCommitParams = {
26
+ groupID: string;
27
+ /** Framed MLSMessage(Commit) bytes from the committer's `GroupManager` call. */
28
+ commitBytes: Uint8Array;
29
+ broadcastService: BroadcastService;
30
+ /** Same wire-send callback as {@link HandleSendBroadcastParams.send}. */
31
+ send: (groupID: string, payloadB64: string) => Promise<void>;
32
+ logger?: Logger;
33
+ };
34
+ /**
35
+ * Fan out a pre-produced MLS handshake Commit to a group's existing members.
36
+ * Unlike {@link handleSendBroadcast} this does not encrypt — the Commit is
37
+ * already a self-protected `PrivateMessage`; `prepareSendCommit` just tags it
38
+ * `mls` on the wire.
39
+ */
40
+ export declare function handleSendCommit(params: HandleSendCommitParams): Promise<void>;
@@ -1 +1 @@
1
- import{toB64 as a}from"@enkaku/codec";import{fromMLSStateRow as r,toMLSStateInsert as t}from"../groups/mls-state.js";export async function handleSendBroadcast(e){let{groupID:o,deviceID:s,message:n,p2pStore:i,broadcastService:p,hubClient:d,logger:u}=e,c=await i.getMLSState(o,s);if(null==c)throw Error(`MLS state missing for group ${o}; cannot broadcast`);let l=r(c),{encrypted:m,updatedGroupState:S}=await p.prepareSend({groupID:o,message:n,groupState:l});try{await d.request("hub/group/send",{param:{groupID:o,payload:a(m)}})}catch(a){throw u?.error("hub/group/send failed",{groupID:o,error:a}),a}await i.saveMLSState(t(S,o,s))}
1
+ import{toB64 as r}from"@enkaku/codec";export async function handleSendBroadcast(e){let{groupID:o,message:t,broadcastService:a,send:n,logger:d}=e;try{await a.prepareSend({groupID:o,message:t,send:e=>n(o,r(e))})}catch(r){throw d?.error("hub/group/send failed",{groupID:o,error:r}),r}}export async function handleSendCommit(e){let{groupID:o,commitBytes:t,broadcastService:a,send:n,logger:d}=e;try{await a.prepareSendCommit({groupID:o,commitBytes:t,send:e=>n(o,r(e))})}catch(r){throw d?.error("hub/group/send (mls commit) failed",{groupID:o,error:r}),r}}
@@ -0,0 +1,20 @@
1
+ import type { StoredMessage } from '@enkaku/hub-protocol';
2
+ /**
3
+ * FIFO queue used by `HubConnection.tunnelMessageStream` to deliver synthetic /
4
+ * forwarded `StoredMessage` values to tunnel transports. Callers `push` messages;
5
+ * each consumer drains via an `AsyncIterator` returned from
6
+ * {@link TunnelInbox.iterator}. Disposal is idempotent: once `close` runs,
7
+ * subsequent pushes are dropped and pending iteration resolves to `done`.
8
+ *
9
+ * Multiple sequential iterators are supported (one consumer at a time, but
10
+ * iterators may be created and returned across the inbox's lifetime). Each
11
+ * iterator tracks its own pending waiter so `iterator.return()` drains only
12
+ * that iterator's waiter — leaving queued messages and other consumers'
13
+ * waiters intact for future iterators.
14
+ */
15
+ export declare class TunnelInbox {
16
+ #private;
17
+ push(message: StoredMessage): void;
18
+ close(): void;
19
+ iterator(onReturn?: () => void): AsyncIterator<StoredMessage>;
20
+ }
@@ -0,0 +1 @@
1
+ export class TunnelInbox{#e=[];#s=[];#t=!1;push(e){if(this.#t)return;let s=this.#s.shift();null!=s?s.resolve({value:e,done:!1}):this.#e.push(e)}close(){if(!this.#t)for(this.#t=!0;this.#s.length>0;){let e=this.#s.shift();e?.resolve({value:void 0,done:!0})}}iterator(e){let s=!1,t=new Set,i=()=>{if(!s){for(let e of(s=!0,t)){let s=this.#s.indexOf(e);s>=0&&this.#s.splice(s,1),e.resolve({value:void 0,done:!0})}t.clear(),e?.()}};return{next:()=>{let e;return s?Promise.resolve({value:void 0,done:!0}):this.#e.length>0?Promise.resolve({value:this.#e.shift(),done:!1}):this.#t?(i(),Promise.resolve({value:void 0,done:!0})):new Promise(s=>{e={resolve:s},t.add(e),this.#s.push(e)}).then(s=>(null!=e&&t.delete(e),!0===s.done&&i(),s))},return:()=>(i(),Promise.resolve({value:void 0,done:!0}))}}}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Wait for a reconnect gate (or similar boolean-style gate) to clear, with an
3
+ * optional timeout. If `gate` is null the call returns immediately without
4
+ * allocating any timer or signal — this is the hot path while the connection
5
+ * is healthy. If `timeoutMs` is undefined we simply await the gate without
6
+ * arming a timer (callers with no timeout can also omit `onTimeout` to skip
7
+ * the closure allocation). Both set: race the gate against
8
+ * `AbortSignal.timeout`; the first to settle wins.
9
+ *
10
+ * `onTimeout` is invoked lazily — only when the timeout actually fires — so
11
+ * callers can capture mutable state (e.g. current reconnect attempt) at the
12
+ * moment of failure rather than at call time.
13
+ */
14
+ export declare function waitForGate(gate: Promise<void> | null, timeoutMs: number | undefined, onTimeout?: () => Error): Promise<void>;
@@ -0,0 +1 @@
1
+ export async function waitForGate(e,t,r){if(null==e)return;if(null==t)return void await e;if(null==r)throw Error("waitForGate: onTimeout required when timeoutMs is set");let i=AbortSignal.timeout(t),n=null,o=new Promise((e,t)=>{i.aborted?t(r()):(n=()=>{t(r())},i.addEventListener("abort",n,{once:!0}))});try{await Promise.race([e,o])}finally{null!=n&&i.removeEventListener("abort",n)}}
@@ -0,0 +1,85 @@
1
+ import type { OwnIdentity } from '@enkaku/token';
2
+ import type { KubunDB } from '@kubun/db';
3
+ import type { DefaultAccessLevel, GraphInternals } from '@kubun/engine';
4
+ import type { Logger } from '@kubun/logger';
5
+ import type { GroupBroadcastMessage } from '../groups/broadcast.js';
6
+ import { type BroadcastEvent } from '../groups/broadcast-service.js';
7
+ import type { P2PEventEmitter } from '../groups/events.js';
8
+ import type { GroupHandleRegistry } from '../groups/group-handle-registry.js';
9
+ import type { ForwardingConfig } from '../sync/forwarder.js';
10
+ import type { StoreUnreadableMode } from '../types.js';
11
+ import type { OnServerDIDObserved } from './connection-pool.js';
12
+ import type { GroupChannel, GroupChannelParams } from './group-channel.js';
13
+ import type { HubConnection, HubConnectionParams } from './hub-connection.js';
14
+ import { type CreateHubClient } from './relay-manager.js';
15
+ export type HubWiring = {
16
+ ready: Promise<void>;
17
+ scheduleBroadcast: (groupID: string, message: GroupBroadcastMessage) => void;
18
+ /**
19
+ * Awaitable broadcast — resolves only after the message is encrypted + sent.
20
+ * Use when ordering matters relative to a subsequent local op (e.g. a
21
+ * leave-request that must be encrypted with the still-live MLS handle before
22
+ * `leaveGroup` tears it down). Rejects on send failure.
23
+ */
24
+ broadcastNow: (groupID: string, message: GroupBroadcastMessage) => Promise<void>;
25
+ /**
26
+ * Fire-and-forget fan-out of a pre-produced MLS handshake Commit to a
27
+ * group's existing members. Wired to `HubRelayManager.sendCommit`.
28
+ */
29
+ scheduleSendCommit: (groupID: string, commitBytes: Uint8Array) => void;
30
+ dispose: () => Promise<void>;
31
+ };
32
+ export type SetupHubRelayParams = {
33
+ identity: OwnIdentity;
34
+ db: KubunDB;
35
+ /**
36
+ * Engine graph internals — threaded through to `BroadcastService` so the
37
+ * `mutation:apply` broadcast variant can route peer-authored mutations
38
+ * through the engine with `origin: 'peer'`.
39
+ */
40
+ graph: GraphInternals;
41
+ emitter: P2PEventEmitter;
42
+ createHubClient: CreateHubClient;
43
+ onRemoteBroadcast: (event: BroadcastEvent) => void;
44
+ /**
45
+ * Single canonical access point for MLS `GroupHandle` instances. Threaded
46
+ * into `BroadcastService` so encrypt/decrypt go through the per-group
47
+ * mutex once Phase 3 lands. Currently stored but unused.
48
+ */
49
+ registry: GroupHandleRegistry;
50
+ logger: Logger;
51
+ /**
52
+ * Optional testing seam forwarded to `HubRelayManager`. Production code
53
+ * leaves this unset so the manager uses its default `GroupChannel`
54
+ * constructor.
55
+ */
56
+ createGroupChannel?: (params: GroupChannelParams) => GroupChannel;
57
+ /**
58
+ * Optional testing seam forwarded to `HubRelayManager`. Production code
59
+ * leaves this unset so the manager uses its default `HubConnection`
60
+ * constructor.
61
+ */
62
+ createHubConnection?: (params: HubConnectionParams) => HubConnection;
63
+ /**
64
+ * Receive-side storage mode. Forwarded to `BroadcastService` →
65
+ * `processBroadcast`. Defaults to `'persist'` when omitted.
66
+ */
67
+ storeUnreadable?: StoreUnreadableMode;
68
+ /** Server default access level — required when `storeUnreadable === 'drop'`. */
69
+ defaultAccessLevel?: DefaultAccessLevel;
70
+ /**
71
+ * Forwarding config. Threaded through to `BroadcastService` →
72
+ * `processBroadcast` along with the hub's own `scheduleBroadcast` hook.
73
+ * Default `false` disables the receive-time forwarding pipeline.
74
+ */
75
+ forwarding?: ForwardingConfig;
76
+ /**
77
+ * Forwarded to `HubRelayManager` → `HubConnectionPool`. Fired with the
78
+ * observed `serverDID` after the first signed response from a freshly-spawned
79
+ * client. Lets callers route capture into `captureServerDID` for TOFU
80
+ * pinning; omitting it skips capture entirely.
81
+ */
82
+ onServerDIDObserved?: OnServerDIDObserved;
83
+ };
84
+ export declare function setupHubRelay(params: SetupHubRelayParams): HubWiring;
85
+ export declare const DISABLED_HUB: HubWiring;
@@ -0,0 +1 @@
1
+ import{HLC as e}from"@kubun/hlc";import{BroadcastService as r}from"../groups/broadcast-service.js";import{HubRelayManager as o}from"./relay-manager.js";export function setupHubRelay(a){let t,{identity:d,db:i,graph:n,emitter:s,createHubClient:c,onRemoteBroadcast:u,registry:p,logger:h,createGroupChannel:l,createHubConnection:m,storeUnreadable:g,defaultAccessLevel:f,forwarding:b,onServerDIDObserved:y}=a,w=[],D=(e,r)=>{t.then(o=>o.broadcast(e,r).catch(r=>{h.error("broadcast failed",{groupID:e,error:r})}),()=>{})},v=async(e,r)=>{let o=await t;await o.broadcast(e,r)};return(t=(async()=>{let[a,t]=await Promise.all([i.getStore("p2p"),i.getStore("graph")]),v=new r({p2pStore:a,graphStore:t,graph:n,selfDID:d.id,registry:p,hlc:new e({nodeID:d.id}),storeUnreadable:g,defaultAccessLevel:f,forwarding:b,scheduleBroadcast:D,emitter:s,logger:h}),B=new o({p2pStore:a,broadcastService:v,deviceID:d.id,createHubClient:c,logger:h,createGroupChannel:l,createHubConnection:m,onServerDIDObserved:y});w.push(v.on("broadcast",u)),w.push(s.on("groupJoined",e=>B.addGroup(e.id).catch(r=>{h.error("addGroup failed",{groupID:e.id,error:r})}))),w.push(s.on("groupLeft",e=>B.removeGroup(e.groupID).catch(r=>{h.error("removeGroup failed",{groupID:e.groupID,error:r})}))),w.push(s.on("hubBound",({groupID:e,hubURL:r})=>B.addBinding(e,r).catch(o=>{h.error("addBinding failed",{groupID:e,hubURL:r,error:o})}))),w.push(s.on("hubUnbound",({groupID:e,hubURL:r})=>B.removeBinding(e,r).catch(o=>{h.error("removeBinding failed",{groupID:e,hubURL:r,error:o})})));let I=await a.listGroups();return await B.start(I.map(e=>e.id)),B})()).catch(e=>{h.error("hub relay setup failed",{error:e})}),{ready:t.then(()=>void 0),scheduleBroadcast:D,broadcastNow:v,scheduleSendCommit:(e,r)=>{t.then(o=>o.sendCommit(e,r).catch(r=>{h.error("sendCommit failed",{groupID:e,error:r})}),()=>{})},dispose:async()=>{for(let e of w)try{e()}catch{}w.length=0;try{let e=await t;await e.stop()}catch{}}}}export const DISABLED_HUB={ready:Promise.resolve(),scheduleBroadcast:()=>{},broadcastNow:async()=>{},scheduleSendCommit:()=>{},dispose:async()=>{}};
package/lib/index.d.ts CHANGED
@@ -1,26 +1,121 @@
1
- import type { KubunPlugin, PluginFactoryParams } from '@kubun/engine';
1
+ import type { DefaultAccessLevel, KubunPlugin, PluginFactoryParams } from '@kubun/engine';
2
+ import type { CreateHubClient } from './hub/relay-manager.js';
3
+ import { type BroadcastBatchConfig } from './sync/broadcast-queue.js';
4
+ import { type PushSyncConfig } from './sync/broadcast-sender.js';
5
+ import type { ForwardingConfig } from './sync/forwarder.js';
6
+ import type { StoreUnreadableMode } from './types.js';
2
7
  export type { ContextDeps, PendingJoinRequest } from './context/types.js';
3
- export { type GroupBroadcastMessage, type ProcessBroadcastParams, processBroadcast, } from './groups/broadcast.js';
8
+ export { type AffectedRow, type GroupBroadcastMessage, type MutationApplyEntry, type ProcessBroadcastParams, type ProcessBroadcastResult, processBroadcast, } from './groups/broadcast.js';
4
9
  export { deserializeBroadcast, serializeBroadcast } from './groups/broadcast-codec.js';
5
- export { ApplyError, type BroadcastEvent, BroadcastService, type BroadcastServiceEvents, type BroadcastServiceParams, DecryptError, type ReceiveBroadcastParams, type ReceiveBroadcastResult, type SendBroadcastParams, type SendBroadcastResult, } from './groups/broadcast-service.js';
6
- export { createFilteredGenerator, createGroupEventEmitter, type GroupEventEmitter, type GroupEventMap, } from './groups/events.js';
7
- export { decodeFullJoinRequest, decodeInvitePayload, decodeJoinRequest, encodeFullJoinRequest, encodeInvitePayload, encodeJoinRequest, type FullJoinRequestPayload, type InvitePayload, type JoinRequestPayload, } from './groups/invite-payload.js';
10
+ export { ApplyError, type BroadcastEvent, BroadcastService, type BroadcastServiceEvents, type BroadcastServiceParams, DecryptError, type ReceiveBroadcastParams, type ReceiveBroadcastResult, type SendBroadcastParams, } from './groups/broadcast-service.js';
11
+ export { createFilteredGenerator, createP2PEventEmitter, type P2PEventEmitter, type P2PEventMap, } from './groups/events.js';
12
+ export { GroupHandleRegistry, type GroupHandleRegistryParams, type SeedParams, } from './groups/group-handle-registry.js';
13
+ export { decodeFullJoinRequest, decodeInvitePayload, decodeJoinRequest, encodeFullJoinRequest, encodeInvitePayload, encodeJoinRequest, type FullJoinRequestPayload, type InvitePayload, type JoinRequestPayload, type SuggestedHub, } from './groups/invite-payload.js';
8
14
  export { type AddCircleMemberParams, type AddCircleMemberResult, type CreateCircleParams, type CreateCircleResult, type CreateGroupParams, type CreateGroupResult, type DeleteCircleParams, type DeleteCircleResult, GroupManager, type GroupManagerParams, type InviteToGroupParams, type InviteToGroupResult, type JoinGroupParams, type JoinGroupResult, type LeaveGroupParams, type LeaveGroupResult, type RemoveCircleMemberParams, type RemoveCircleMemberResult, type RemoveGroupMemberParams, type RemoveGroupMemberResult, type RemoveMemberParams, type RemoveMemberResult, type UpdateCircleParams, type UpdateCircleResult, type UpdateGroupParams, type UpdateGroupResult, } from './groups/manager.js';
15
+ export { MLSEncryptor, type MLSEncryptorParams } from './groups/mls-encryptor.js';
9
16
  export { type MLSGroupHandle, restoreMLSGroupHandle } from './groups/mls-group-handle.js';
10
17
  export { replacer as mlsJSONReplacer, reviver as mlsJSONReviver } from './groups/mls-json.js';
11
18
  export { deserializeMLSGroupState, type MLSGroupState, type SerializedMLSGroupState, serializeMLSGroupState, } from './groups/mls-state.js';
12
19
  export type { SyncClientMessage, SyncProtocol, SyncServerMessage } from './protocol.js';
13
20
  export { createP2PSchemaExtension } from './schema.js';
21
+ export { type BroadcastBatchConfig, type BroadcastQueue, type BroadcastQueueParams, createBroadcastQueue, DEFAULT_BROADCAST_BATCH_CONFIG, } from './sync/broadcast-queue.js';
22
+ export { type BroadcastSenderParams, DEFAULT_PUSH_SYNC_CONFIG, type PushSyncConfig, wireBroadcastSender, } from './sync/broadcast-sender.js';
14
23
  export { type CatalogSyncScope, resolveCatalogSyncScopes } from './sync/catalog-scope.js';
24
+ export { type EvaluateAndForwardParams, type EvaluateAndForwardResult, evaluateAndForward, type ForwardContext, type ForwardFilter, type ForwardingConfig, } from './sync/forwarder.js';
15
25
  export { checkSyncDelegation, negotiateDirection } from './sync/handlers.js';
26
+ export { HubTunnelSyncListener, type HubTunnelSyncListenerParams, } from './sync/hub-tunnel-sync-listener.js';
27
+ export { HubTunnelSyncProvider, type HubTunnelSyncProviderParams, } from './sync/hub-tunnel-sync-provider.js';
16
28
  export { type PeerConfig, type PeerConfigWithID, PeerRegistry } from './sync/peer-registry.js';
17
29
  export { SyncClient, type SyncClientParams, type SyncScope, type SyncTransportProvider, } from './sync/sync-client.js';
18
30
  export { type SyncEvent, SyncManager, type SyncManagerParams, type SyncSessionInfo, type SyncStatus, } from './sync/sync-manager.js';
31
+ export type { OnServerDIDObserved } from './hub/connection-pool.js';
32
+ export { DIDObservingTransport, type DIDObservingTransportParams, type ServerDIDObserver, } from './hub/did-observing-transport.js';
33
+ export { ReconnectingError } from './hub/errors.js';
19
34
  export type { GroupChannel, GroupChannelParams } from './hub/group-channel.js';
20
- export { type CreateGroupChannel, type CreateHubClient, HubRelayManager, type HubRelayManagerParams, } from './hub/relay-manager.js';
21
- export type { CatalogData, CircleData, CircleMemberData, DeleteCatalogData, DeleteCircleData, GroupData, GroupMemberData, GroupRequestContext, InviteToGroupData, JoinGroupData, LeaveGroupData, P2PContext, P2PJoinRequestContext, RemoveCircleMemberData, RemoveGroupMemberData, SyncPluginAPI, SyncRequestContext, SyncSessionResult, UpdateCircleInput, UpdateGroupInput, } from './types.js';
35
+ export { type CreateHTTPHubClientParams, createHTTPHubClient } from './hub/http-client.js';
36
+ export type { ConnectionLifecycleEvent } from './hub/hub-connection.js';
37
+ export { type BindHubToGroupParams, bindHubToGroup, type CaptureServerDIDParams, type CreateHubParams, captureServerDID, createHub, type DeleteHubParams, type DeleteHubResult, deleteHub, type GetHubParams, getHub, type Hub, HubServerDIDConflictError, type ListGroupsByHubParams, type ListHubsByGroupParams, type ListHubsParams, listGroupsByHub, listHubs, listHubsByGroup, type UnbindHubFromGroupParams, type UpdateHubParams, type UpsertHubParams, unbindHubFromGroup, updateHub, upsertHub, } from './hub/manager.js';
38
+ export { type CreateGroupChannel, type CreateHubClient, type CreateHubClientOptions, type CreateHubConnection, HubRelayManager, type HubRelayManagerParams, type RelayEvent, } from './hub/relay-manager.js';
39
+ export { DISABLED_HUB, type HubWiring, type SetupHubRelayParams, setupHubRelay, } from './hub/wiring.js';
40
+ export type { CatalogData, CircleData, CircleMemberData, DeleteCatalogData, DeleteCircleData, GroupData, GroupMemberData, GroupRequestContext, InviteToGroupData, JoinGroupData, LeaveGroupData, P2PContext, P2PJoinRequestContext, RemoveCircleMemberData, RemoveGroupMemberData, StoreUnreadableMode, SyncPluginAPI, SyncRequestContext, SyncSessionResult, UpdateCircleInput, UpdateGroupInput, } from './types.js';
41
+ export type ReceiveConfig = {
42
+ /**
43
+ * Receiver storage mode for incoming peer mutations.
44
+ * - `'persist'` (default) — store every doc that passes catalog/group
45
+ * checks. Access enforced at query time; unreadable docs remain in DB
46
+ * but filtered.
47
+ * - `'drop'` — apply read-access check at receive time using the local
48
+ * peer's own DID as viewer. Mutations the local peer cannot read are
49
+ * not persisted.
50
+ */
51
+ storeUnreadable: StoreUnreadableMode;
52
+ };
53
+ /**
54
+ * Default receive config. Under `'persist'` no gate is built and no extra
55
+ * DB lookups occur during receive.
56
+ */
57
+ export declare const DEFAULT_RECEIVE_CONFIG: ReceiveConfig;
22
58
  export type P2PPluginOptions = {
23
59
  autoAcceptPeers?: Array<string>;
24
60
  http?: boolean | string;
61
+ /**
62
+ * Enable hub-mediated group broadcasts.
63
+ * - `true` / `{}` — build a default HTTP hub client factory from
64
+ * `params.identity` + `params.runtime.fetch`.
65
+ * - `{ createHubClient }` — caller supplies a custom factory.
66
+ * - absent / `false` — hub relay disabled; all broadcast call-sites are
67
+ * no-ops.
68
+ */
69
+ hub?: boolean | {
70
+ createHubClient?: CreateHubClient;
71
+ };
72
+ /**
73
+ * Batch broadcast queue config. Sender-side mutation:apply broadcasts are
74
+ * accumulated per target MLS group and flushed when window/count/byte
75
+ * thresholds trip. Defaults from {@link DEFAULT_BROADCAST_BATCH_CONFIG}.
76
+ * Set `enabled: false` to bypass the queue and emit one broadcast per
77
+ * mutation per scope.
78
+ */
79
+ batch?: Partial<BroadcastBatchConfig>;
80
+ /**
81
+ * Push-sync config. Gates the local `engine:mutation:authored` subscriber
82
+ * that drives `mutation:apply` broadcasts.
83
+ * - When `enabled: false` (default), no `mutation:apply` broadcasts are
84
+ * emitted from this peer when local mutations land — even if `hub` is
85
+ * configured. Merkle-pull continues to work, and the receive path
86
+ * (incoming `mutation:apply` broadcasts) is unaffected.
87
+ * - When `enabled: true`, locally-authored mutations fan out per-scope
88
+ * through the hub.
89
+ *
90
+ * Defaults from {@link DEFAULT_PUSH_SYNC_CONFIG}.
91
+ */
92
+ pushSync?: Partial<PushSyncConfig>;
93
+ /**
94
+ * Receive-side storage config. Controls whether peer mutations the local
95
+ * peer cannot read are persisted to the local DB or dropped at receive
96
+ * time. Defaults to {@link DEFAULT_RECEIVE_CONFIG} (persist).
97
+ */
98
+ receive?: Partial<ReceiveConfig>;
99
+ /**
100
+ * Server default access level used by the receive-time access gate when
101
+ * `receive.storeUnreadable === 'drop'` and no document or per-user-model
102
+ * default rule is declared. Defaults to `{ read: 'only_owner', write:
103
+ * 'only_owner' }` (no fan-out for undeclared models). Ignored under
104
+ * `'persist'`.
105
+ */
106
+ defaultAccessLevel?: DefaultAccessLevel;
107
+ /**
108
+ * Forwarding config. Controls per-peer N+1 re-broadcast.
109
+ * - `false` (default) — incoming `mutation:apply` broadcasts are NOT
110
+ * re-broadcast.
111
+ * - `true` — re-broadcast each applied entry to every other MLS group the
112
+ * local peer belongs to whose access + catalog gates pass.
113
+ * - {@link ForwardFilter} — same as `true`, plus per-candidate filter
114
+ * callback that further narrows. Filter receives a {@link ForwardContext}.
115
+ *
116
+ * Push-sync config (`pushSync.enabled`) is orthogonal — forwarding fires
117
+ * regardless of whether locally-authored mutations are broadcast.
118
+ */
119
+ forwarding?: ForwardingConfig;
25
120
  };
26
121
  export declare function createP2PPlugin(options?: P2PPluginOptions): (params: PluginFactoryParams) => KubunPlugin;
package/lib/index.js CHANGED
@@ -1 +1 @@
1
- import{ServerTransport as e}from"@enkaku/http-server-transport";import{Server as r}from"@enkaku/server";import{DirectTransports as t}from"@enkaku/transport";import{HLC as o}from"@kubun/hlc";import{p2pStoreDefinition as s}from"@kubun/store-p2p";import{createGroupContext as n}from"./context/group.js";import{createJoinContext as a}from"./context/join.js";import{createSyncContext as i}from"./context/sync.js";import{createGroupEventEmitter as p}from"./groups/events.js";import{GroupManager as c}from"./groups/manager.js";import{createP2PSchemaExtension as m}from"./schema.js";import{createSyncHandlers as g}from"./sync/handlers.js";import{SyncManager as d}from"./sync/sync-manager.js";export{processBroadcast}from"./groups/broadcast.js";export{deserializeBroadcast,serializeBroadcast}from"./groups/broadcast-codec.js";export{ApplyError,BroadcastService,DecryptError}from"./groups/broadcast-service.js";export{createFilteredGenerator,createGroupEventEmitter}from"./groups/events.js";export{decodeFullJoinRequest,decodeInvitePayload,decodeJoinRequest,encodeFullJoinRequest,encodeInvitePayload,encodeJoinRequest}from"./groups/invite-payload.js";export{GroupManager}from"./groups/manager.js";export{restoreMLSGroupHandle}from"./groups/mls-group-handle.js";export{replacer as mlsJSONReplacer,reviver as mlsJSONReviver}from"./groups/mls-json.js";export{deserializeMLSGroupState,serializeMLSGroupState}from"./groups/mls-state.js";export{createP2PSchemaExtension}from"./schema.js";export{resolveCatalogSyncScopes}from"./sync/catalog-scope.js";export{checkSyncDelegation,negotiateDirection}from"./sync/handlers.js";export{PeerRegistry}from"./sync/peer-registry.js";export{SyncClient}from"./sync/sync-client.js";export{SyncManager}from"./sync/sync-manager.js";export{HubRelayManager}from"./hub/relay-manager.js";export function createP2PPlugin(l){return u=>{let y,f;u.db.register(s);let j=u.db.adapter,h=u.identity,v=new o({nodeID:h.id}),x=new c({identity:h,getRandomID:u.runtime.getRandomID}),P=p(),S=new d({deployClusters:async e=>{let r=Object.values(e);await u.engine.deployGraph({clusters:r})},runtime:u.runtime,identity:u.identity,logger:u.getLogger("sync")}),b=g({db:u.db,logger:u.getLogger("sync-handlers"),peerRegistry:S.peerRegistry}),R=new Map,w=[],k={addPeer:e=>S.addPeer({config:e,stores:u.db}),removePeer:e=>S.removePeer({peerDID:e,stores:u.db}),listPeers:()=>S.listPeers({stores:u.db}),syncWithPeer:e=>S.merkleSyncWithPeer({...e,stores:u.db}),getStatus:e=>S.getStatus({peerDID:e,stores:u.db}),onSyncEvent:e=>S.onSyncEvent(e),createSyncTransport:function(e){let o=new t({signal:e}),s=new r({accessControl:!1,getRandomID:u.runtime.getRandomID,handlers:b,logger:u.getLogger("sync-server"),transports:[o.server],signal:e});return w.push(s),o.client}};if(l?.http){let t="string"==typeof l.http?l.http.replace(/^\//,""):"sync";k.syncReady=(async()=>{let o=await u.engine.getAPI("http");await o.listening,y=new e({allowedOrigin:["*"]}),f=new r({accessControl:!1,getRandomID:u.runtime.getRandomID,handlers:b,logger:u.getLogger("sync-http-server"),transports:[y]}),w.push(f),o.registerProtocol(t,y.fetch.bind(y))})()}return{name:"p2p",schemaExtension:e=>m(P),api:k,createContextFactory:()=>(e,r)=>{let t={identity:h,groupManager:x,syncManager:S,stores:r,adapter:j,hlc:v,emitter:P,pendingJoinRequests:R,runtime:u.runtime,autoAcceptPeers:l?.autoAcceptPeers};return{group:n(e,t),sync:i(e,t),join:a(e,t)}},dispose:async()=>{for(let e of w)await e.dispose();await y?.dispose(),await S.dispose()}}}}
1
+ import{ServerTransport as e}from"@enkaku/http-server-transport";import{Server as r}from"@enkaku/server";import{isFullIdentity as t}from"@enkaku/token";import{DirectTransports as o}from"@enkaku/transport";import{HLC as s}from"@kubun/hlc";import{p2pStoreDefinition as n}from"@kubun/store-p2p";import{createDelegationContext as a}from"./context/delegation.js";import{createGroupContext as i}from"./context/group.js";import{createHubContext as c}from"./context/hub.js";import{createJoinContext as p}from"./context/join.js";import{createSyncContext as d}from"./context/sync.js";import{createP2PEventEmitter as u}from"./groups/events.js";import{GroupHandleRegistry as l}from"./groups/group-handle-registry.js";import{GroupManager as g}from"./groups/manager.js";import{forwardRemoteBroadcast as m}from"./hub/forward-remote-broadcast.js";import{createHTTPHubClient as b}from"./hub/http-client.js";import{captureServerDID as y}from"./hub/manager.js";import{DISABLED_HUB as f,setupHubRelay as h}from"./hub/wiring.js";import{createP2PSchemaExtension as j}from"./schema.js";import{createBroadcastQueue as v,DEFAULT_BROADCAST_BATCH_CONFIG as x}from"./sync/broadcast-queue.js";import{DEFAULT_PUSH_SYNC_CONFIG as S,wireBroadcastSender as w}from"./sync/broadcast-sender.js";import{createSyncHandlers as D}from"./sync/handlers.js";import{SyncManager as P}from"./sync/sync-manager.js";export{processBroadcast}from"./groups/broadcast.js";export{deserializeBroadcast,serializeBroadcast}from"./groups/broadcast-codec.js";export{ApplyError,BroadcastService,DecryptError}from"./groups/broadcast-service.js";export{createFilteredGenerator,createP2PEventEmitter}from"./groups/events.js";export{GroupHandleRegistry}from"./groups/group-handle-registry.js";export{decodeFullJoinRequest,decodeInvitePayload,decodeJoinRequest,encodeFullJoinRequest,encodeInvitePayload,encodeJoinRequest}from"./groups/invite-payload.js";export{GroupManager}from"./groups/manager.js";export{MLSEncryptor}from"./groups/mls-encryptor.js";export{restoreMLSGroupHandle}from"./groups/mls-group-handle.js";export{replacer as mlsJSONReplacer,reviver as mlsJSONReviver}from"./groups/mls-json.js";export{deserializeMLSGroupState,serializeMLSGroupState}from"./groups/mls-state.js";export{createP2PSchemaExtension}from"./schema.js";export{createBroadcastQueue,DEFAULT_BROADCAST_BATCH_CONFIG}from"./sync/broadcast-queue.js";export{DEFAULT_PUSH_SYNC_CONFIG,wireBroadcastSender}from"./sync/broadcast-sender.js";export{resolveCatalogSyncScopes}from"./sync/catalog-scope.js";export{evaluateAndForward}from"./sync/forwarder.js";export{checkSyncDelegation,negotiateDirection}from"./sync/handlers.js";export{HubTunnelSyncListener}from"./sync/hub-tunnel-sync-listener.js";export{HubTunnelSyncProvider}from"./sync/hub-tunnel-sync-provider.js";export{PeerRegistry}from"./sync/peer-registry.js";export{SyncClient}from"./sync/sync-client.js";export{SyncManager}from"./sync/sync-manager.js";export{DIDObservingTransport}from"./hub/did-observing-transport.js";export{ReconnectingError}from"./hub/errors.js";export{createHTTPHubClient}from"./hub/http-client.js";export{bindHubToGroup,captureServerDID,createHub,deleteHub,getHub,HubServerDIDConflictError,listGroupsByHub,listHubs,listHubsByGroup,unbindHubFromGroup,updateHub,upsertHub}from"./hub/manager.js";export{HubRelayManager}from"./hub/relay-manager.js";export{DISABLED_HUB,setupHubRelay}from"./hub/wiring.js";export const DEFAULT_RECEIVE_CONFIG={storeUnreadable:"persist"};export function createP2PPlugin(I){return R=>{var E;let H,L,B,C;R.db.register(n);let G=R.db.adapter;if(!(t(E=R.identity)&&"privateKey"in E))throw Error("createP2PPlugin requires an OwnIdentity (full identity with privateKey) — received a narrower Identity");let A=R.identity,F=new s({nodeID:A.id}),k={...DEFAULT_RECEIVE_CONFIG,...I?.receive},T=I?.defaultAccessLevel??{read:"only_owner",write:"only_owner"},U=new l({stores:R.db,deviceID:A.id,logger:R.getLogger("group-handle-registry")}),_=new g({identity:A,getRandomID:R.runtime.getRandomID,registry:U}),O=u(),q=new P({deployClusters:async e=>{let r=Object.values(e);await R.engine.deployGraph({clusters:r})},graph:R.graph,runtime:R.runtime,identity:R.identity,logger:R.getLogger("sync"),storeUnreadable:k.storeUnreadable,defaultAccessLevel:T}),N=D({db:R.db,graph:R.graph,logger:R.getLogger("sync-handlers"),peerRegistry:q.peerRegistry,selfDID:A.id,storeUnreadable:k.storeUnreadable,defaultAccessLevel:T}),M=new Map,J=[],z=R.getLogger("hub-relay"),K=f,V={...S,...I?.pushSync};if(I?.hub){let e="object"==typeof I.hub&&null!=I.hub.createHubClient?I.hub.createHubClient:b({identity:A,fetch:R.runtime.fetch});K=h({identity:A,db:R.db,graph:R.graph,emitter:O,createHubClient:e,registry:U,onRemoteBroadcast:e=>{m(O,e,z).catch(e=>{z.error("forwardRemoteBroadcast failed",{error:e})})},logger:z,storeUnreadable:k.storeUnreadable,defaultAccessLevel:T,forwarding:I.forwarding,onServerDIDObserved:({hubURL:e,serverDID:r})=>y({stores:R.db,hubURL:e,serverDID:r}).then(()=>void 0)}),V.enabled&&(L=v({config:{...x,...I.batch},scheduleBroadcast:K.scheduleBroadcast,logger:R.getLogger("broadcast-queue")}),H=w({db:R.db,eventBus:R.eventBus,selfDID:A.id,queue:L,logger:R.getLogger("broadcast-sender")}))}let W={hubReady:K.ready,addPeer:e=>q.addPeer({config:e,stores:R.db}),removePeer:e=>q.removePeer({peerDID:e,stores:R.db}),listPeers:()=>q.listPeers({stores:R.db}),syncWithPeer:e=>q.merkleSyncWithPeer({...e,stores:R.db}),getStatus:e=>q.getStatus({peerDID:e,stores:R.db}),onSyncEvent:e=>q.onSyncEvent(e),createSyncTransport:function(e){let t=new o({signal:e}),s=new r({getRandomID:R.runtime.getRandomID,handlers:N,logger:R.getLogger("sync-server"),transports:[t.server],signal:e});return J.push(s),t.client}};if(I?.http){let t="string"==typeof I.http?I.http.replace(/^\//,""):"sync";W.syncReady=(async()=>{let o=await R.engine.getAPI("http");await o.listening,B=new e({allowedOrigin:["*"]}),C=new r({getRandomID:R.runtime.getRandomID,handlers:N,logger:R.getLogger("sync-http-server"),transports:[B]}),J.push(C),o.registerProtocol(t,B.fetch.bind(B))})()}return{name:"p2p",schemaExtension:e=>j(O),api:W,createContextFactory:()=>(e,r)=>{let t={identity:A,groupManager:_,registry:U,syncManager:q,stores:r,adapter:G,hlc:F,emitter:O,pendingJoinRequests:M,runtime:R.runtime,autoAcceptPeers:I?.autoAcceptPeers,scheduleBroadcast:K.scheduleBroadcast,broadcastNow:K.broadcastNow,scheduleSendCommit:K.scheduleSendCommit,logger:R.getLogger("context")};return{group:i(e,t),sync:d(e,t),join:p(e,t),hub:c(e,t),delegation:a(e,t)}},dispose:async()=>{if(null!=H)try{H()}catch{}if(null!=L)try{L.dispose()}catch{}let e=(async()=>{for(let e of J)await e.dispose();null!=B&&await B.dispose()})(),r=R.getLogger("dispose"),t=await Promise.allSettled([e,q.dispose(),K.dispose()]),o=["sync-http","sync-manager","hub"];t.forEach((e,t)=>{"rejected"===e.status&&r.error("dispose branch failed",{branch:o[t],error:e.reason})})}}}}
package/lib/schema.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import type { SchemaExtension } from '@kubun/engine';
2
- import { type GroupEventEmitter } from './groups/events.js';
3
- export declare function createP2PSchemaExtension(emitter: GroupEventEmitter): SchemaExtension;
2
+ import { type P2PEventEmitter } from './groups/events.js';
3
+ export declare function createP2PSchemaExtension(emitter: P2PEventEmitter): SchemaExtension;
package/lib/schema.js CHANGED
@@ -1,17 +1,17 @@
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=`
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}async function*t(){}let o=`
2
2
  type PeerGroup {
3
3
  id: ID!
4
4
  name: String!
5
5
  description: String
6
- createdBy: String!
7
- createdAt: String!
6
+ createdBy: DID!
7
+ createdAt: DateTimeISO!
8
8
  }
9
9
 
10
10
  type PeerGroupMember {
11
11
  groupID: ID!
12
- memberDID: String!
12
+ memberDID: DID!
13
13
  role: String!
14
- createdAt: String!
14
+ createdAt: DateTimeISO!
15
15
  }
16
16
 
17
17
  type PeerCircle {
@@ -19,28 +19,43 @@ type PeerCircle {
19
19
  groupID: ID!
20
20
  name: String!
21
21
  description: String
22
- createdAt: String!
22
+ createdAt: DateTimeISO!
23
23
  }
24
24
 
25
25
  type PeerCircleMember {
26
26
  circleID: ID!
27
- memberDID: String!
27
+ memberDID: DID!
28
28
  role: String!
29
- createdAt: String!
29
+ createdAt: DateTimeISO!
30
30
  }
31
31
 
32
32
  type PeerCatalog {
33
33
  id: ID!
34
- ownerDID: String!
34
+ ownerDID: DID!
35
35
  name: String!
36
36
  description: String
37
37
  filterCriteria: String!
38
- createdAt: String!
38
+ createdAt: DateTimeISO!
39
+ }
40
+
41
+ type PeerHub {
42
+ id: ID!
43
+ label: String
44
+ url: URL!
45
+ serverDID: DID
46
+ createdAt: DateTimeISO!
47
+ updatedAt: DateTimeISO
48
+ }
49
+
50
+ input SuggestedHubInput {
51
+ url: URL!
52
+ serverDID: DID
39
53
  }
40
54
 
41
55
  input CreateGroupInput {
42
56
  name: String!
43
57
  description: String
58
+ hubs: [SuggestedHubInput!]
44
59
  }
45
60
 
46
61
  type CreateGroupPayload {
@@ -110,13 +125,13 @@ type DeleteCirclePayload {
110
125
 
111
126
  type RemoveCircleMemberPayload {
112
127
  circleID: ID!
113
- memberDID: String!
128
+ memberDID: DID!
114
129
  }
115
130
 
116
131
  type SyncPeer {
117
132
  id: String!
118
- peerDID: String!
119
- endpoint: String!
133
+ peerDID: DID!
134
+ endpoint: URL!
120
135
  mode: String!
121
136
  priority: String!
122
137
  trustLevel: String!
@@ -140,35 +155,110 @@ type RemoveSyncPeerPayload {
140
155
  success: Boolean!
141
156
  }
142
157
 
158
+ input CreateHubInput {
159
+ url: URL!
160
+ label: String
161
+ serverDID: DID
162
+ }
163
+
164
+ input UpdateHubInput {
165
+ id: ID!
166
+ url: URL
167
+ label: String
168
+ serverDID: DID
169
+ }
170
+
171
+ type DeleteHubPayload {
172
+ success: Boolean!
173
+ boundGroupIDs: [ID!]!
174
+ }
175
+
176
+ type DelegationToken {
177
+ jwt: String!
178
+ jti: String!
179
+ grantor: ID!
180
+ audience: ID!
181
+ resource: String!
182
+ exp: Int!
183
+ }
184
+
185
+ input DelegationTokenCovers {
186
+ modelID: ID
187
+ docID: ID
188
+ }
189
+
190
+ """
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.
195
+ """
196
+ type RevokedDelegationToken {
197
+ jti: ID!
198
+ grantor: ID!
199
+ audience: ID!
200
+ revokerDID: DID!
201
+ "Issuer's revocation-record timestamp (iat), Unix epoch seconds."
202
+ 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
+ """
209
+ verifiedAt: Int!
210
+ capExp: Int
211
+ groupID: ID!
212
+ }
213
+
143
214
  extend type Query {
144
215
  groups: [PeerGroup!]!
145
216
  group(id: ID!): PeerGroup
146
217
  groupMembers(groupID: ID!): [PeerGroupMember!]!
147
218
  circles(groupID: ID!): [PeerCircle!]!
219
+ hubs: [PeerHub!]!
220
+ hub(id: ID!): PeerHub
148
221
  syncPeers: [SyncPeer!]!
149
222
  syncStatus: SyncStatus!
223
+ ownDelegationTokens(grantor: ID, covers: DelegationTokenCovers): [DelegationToken!]!
224
+ ownRevokedDelegationTokens(grantor: ID): [RevokedDelegationToken!]!
150
225
  }
151
226
 
152
227
  extend type Mutation {
153
228
  createGroup(input: CreateGroupInput!): CreateGroupPayload!
154
- addGroupMember(groupID: ID!, memberDID: String!, role: String!): PeerGroupMember!
229
+ addGroupMember(groupID: ID!, memberDID: DID!, role: String!): PeerGroupMember!
155
230
  createCircle(groupID: ID!, name: String!, description: String): PeerCircle!
156
- addCircleMember(circleID: ID!, memberDID: String!, role: String!): PeerCircleMember!
231
+ addCircleMember(circleID: ID!, memberDID: DID!, role: String!): PeerCircleMember!
157
232
  createCatalog(name: String!, description: String, filterCriteria: String!): PeerCatalog!
158
- inviteToGroup(groupID: ID!, joinRequest: String!): InviteToGroupPayload!
233
+ inviteToGroup(groupID: ID!, joinRequest: String!, grants: [String!], revocations: [String!]): InviteToGroupPayload!
159
234
  joinGroup(invitePayload: String!, joinRequestPayload: String!): JoinGroupPayload!
160
235
  prepareJoinRequest: PrepareJoinRequestPayload!
161
236
  completeJoin(invitePayload: String!): CompleteJoinPayload!
162
237
  leaveGroup(groupID: ID!): LeaveGroupPayload!
163
- removeGroupMember(groupID: ID!, memberDID: String!): RemoveGroupMemberPayload!
238
+ removeGroupMember(groupID: ID!, memberDID: DID!): RemoveGroupMemberPayload!
164
239
  updateCatalog(input: UpdateCatalogInput!): PeerCatalog!
165
240
  updateGroup(input: UpdateGroupInput!): UpdateGroupPayload!
166
241
  updateCircle(input: UpdateCircleInput!): UpdateCirclePayload!
167
242
  deleteCatalog(catalogID: ID!): DeleteCatalogPayload!
168
243
  deleteCircle(circleID: ID!): DeleteCirclePayload!
169
- removeCircleMember(circleID: ID!, memberDID: String!): RemoveCircleMemberPayload!
170
- addSyncPeer(peerDID: String!, endpoint: String!, mode: String, priority: String, trustLevel: String): Boolean!
171
- removeSyncPeer(peerDID: String!): RemoveSyncPeerPayload!
244
+ removeCircleMember(circleID: ID!, memberDID: DID!): RemoveCircleMemberPayload!
245
+ grantWriteCapability(to: DID!, res: String!, groupID: ID!, expiresIn: Int): String!
246
+ revokeCapability(jti: ID!): Boolean!
247
+ addSyncPeer(peerDID: DID!, endpoint: URL!, mode: String, priority: String, trustLevel: String): Boolean!
248
+ removeSyncPeer(peerDID: DID!): RemoveSyncPeerPayload!
249
+ createHub(input: CreateHubInput!): PeerHub!
250
+ updateHub(input: UpdateHubInput!): PeerHub!
251
+ deleteHub(id: ID!, force: Boolean): DeleteHubPayload!
252
+ bindHubToGroup(hubID: ID!, groupID: ID!): Boolean!
253
+ unbindHubFromGroup(hubID: ID!, groupID: ID!): Boolean!
254
+ }
255
+
256
+ extend type PeerGroup {
257
+ hubs: [PeerHub!]!
258
+ }
259
+
260
+ extend type PeerHub {
261
+ groups: [PeerGroup!]!
172
262
  }
173
263
 
174
264
  extend type Subscription {
@@ -183,5 +273,7 @@ extend type Subscription {
183
273
  circleMemberAdded(circleID: ID!): PeerCircleMember!
184
274
  circleMemberRemoved(circleID: ID!): PeerCircleMember!
185
275
  circleCatalogsChanged(circleID: ID!): PeerCircle!
276
+ ownDelegationTokenAdded: DelegationToken!
277
+ ownDelegationTokenRevoked: RevokedDelegationToken!
186
278
  }
187
- `;export function createP2PSchemaExtension(i){return{sdl:t,resolvers:{queryFields:{groups:async(e,t,i)=>await r(i).group.list(),group:async(e,t,i)=>await r(i).group.get(t.id),groupMembers:async(e,t,i)=>await r(i).group.listMembers(t.groupID),circles:async(e,t,i)=>await r(i).group.listCircles(t.groupID),syncPeers:async(e,t,i)=>await r(i).sync.listPeers(),syncStatus:(e,t,i)=>{let o=r(i).sync.getStatus();return{...o,lastSyncByPeer:JSON.stringify(o.lastSyncByPeer)}}},mutationFields:{createGroup:async(e,t,i)=>{let o=t.input;return{group:await r(i).group.create(o)}},addGroupMember:async(e,t,i)=>await r(i).group.addMember(t.groupID,t.memberDID,t.role),createCircle:async(e,t,i)=>await r(i).group.createCircle({groupID:t.groupID,name:t.name,description:t.description}),addCircleMember:async(e,t,i)=>await r(i).group.addCircleMember(t.circleID,t.memberDID,t.role),createCatalog:async(e,t,i)=>await r(i).group.createCatalog({name:t.name,description:t.description,filterCriteria:t.filterCriteria}),inviteToGroup:async(e,t,i)=>await r(i).group.invite(t.groupID,t.joinRequest),joinGroup:async(e,t,i)=>await r(i).group.join(t.invitePayload,t.joinRequestPayload),prepareJoinRequest:async(e,t,i)=>await r(i).join.prepareRequest(),completeJoin:async(e,t,i)=>await r(i).join.complete(t.invitePayload),leaveGroup:async(e,t,i)=>await r(i).group.leave(t.groupID),removeGroupMember:async(e,t,i)=>await r(i).group.removeMember(t.groupID,t.memberDID),updateCatalog:async(e,t,i)=>{let o=t.input;return await r(i).group.updateCatalog(o)},updateGroup:async(e,t,i)=>{let o=t.input;return{group:await r(i).group.update(o)}},updateCircle:async(e,t,i)=>{let o=t.input;return{circle:await r(i).group.updateCircle(o)}},deleteCatalog:async(e,t,i)=>await r(i).group.deleteCatalog(t.catalogID),deleteCircle:async(e,t,i)=>await r(i).group.deleteCircle(t.circleID),removeCircleMember:async(e,t,i)=>await r(i).group.removeCircleMember(t.circleID,t.memberDID),addSyncPeer:async(e,t,i)=>(await r(i).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,i)=>(await r(i).sync.removePeer(t.peerDID),{success:!0})},subscriptionFields:{groupJoined:{resolve:e=>e,subscribe:()=>e(i,"groupJoined")},groupLeft:{resolve:e=>e,subscribe:()=>e(i,"groupLeft")},groupDataChanged:{resolve:e=>e,subscribe:(r,t)=>e(i,"groupDataChanged",e=>e.groupID===t.groupID)},groupMemberJoined:{resolve:e=>e,subscribe:(r,t)=>e(i,"groupMemberJoined",e=>e.groupID===t.groupID)},groupMemberLeft:{resolve:e=>e,subscribe:(r,t)=>e(i,"groupMemberLeft",e=>e.groupID===t.groupID)},circleCreated:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleCreated",e=>e.groupID===t.groupID)},circleDeleted:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleDeleted",e=>e.groupID===t.groupID)},circleDataChanged:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleDataChanged",e=>e.circleID===t.circleID)},circleMemberAdded:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleMemberAdded",e=>e.circleID===t.circleID)},circleMemberRemoved:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleMemberRemoved",e=>e.circleID===t.circleID)},circleCatalogsChanged:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleCatalogsChanged",e=>e.circleID===t.circleID)}}}}}
279
+ `;export function createP2PSchemaExtension(i){return{sdl:o,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 i=r(o).sync.getStatus();return{...i,lastSyncByPeer:JSON.stringify(i.lastSyncByPeer)}},ownDelegationTokens:async(e,r,t)=>{let o=t.getViewer();if(null==o||null==t.p2p)return[];let i=r.grantor??null,a=r.covers??null;return await t.p2p.delegation.listOwnTokens({viewerDID:o,grantor:i,covers:a})},ownRevokedDelegationTokens:async(e,r,t)=>{let o=t.getViewer();if(null==o||null==t.p2p)return[];let i=r.grantor??null;return await t.p2p.delegation.listOwnRevokedTokens({viewerDID:o,grantor:i})}},mutationFields:{createGroup:async(e,t,o)=>{let i=t.input;return{group:await r(o).group.create(i)}},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 i=t.input;return await r(o).group.updateCatalog(i)},updateGroup:async(e,t,o)=>{let i=t.input;return{group:await r(o).group.update(i)}},updateCircle:async(e,t,o)=>{let i=t.input;return{circle:await r(o).group.updateCircle(i)}},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 i=t.input;return await r(o).hub.create(i)},updateHub:async(e,t,o)=>{let i=t.input;return await r(o).hub.update(i)},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})},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(i,"groupJoined")},groupLeft:{resolve:e=>e,subscribe:()=>e(i,"groupLeft")},groupDataChanged:{resolve:e=>e,subscribe:(r,t)=>e(i,"groupDataChanged",e=>e.groupID===t.groupID)},groupMemberJoined:{resolve:e=>e,subscribe:(r,t)=>e(i,"groupMemberJoined",e=>e.groupID===t.groupID)},groupMemberLeft:{resolve:e=>e,subscribe:(r,t)=>e(i,"groupMemberLeft",e=>e.groupID===t.groupID)},circleCreated:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleCreated",e=>e.groupID===t.groupID)},circleDeleted:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleDeleted",e=>e.groupID===t.groupID)},circleDataChanged:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleDataChanged",e=>e.circleID===t.circleID)},circleMemberAdded:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleMemberAdded",e=>e.circleID===t.circleID)},circleMemberRemoved:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleMemberRemoved",e=>e.circleID===t.circleID)},circleCatalogsChanged:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleCatalogsChanged",e=>e.circleID===t.circleID)},ownDelegationTokenAdded:{resolve:e=>e,subscribe:(r,o,a)=>{let n=a.getViewer();return null==n?t():e(i,"delegationTokenAdded",e=>e.audience===n)}},ownDelegationTokenRevoked:{resolve:e=>e,subscribe:(r,o,a)=>{let n=a.getViewer();return null==n?t():e(i,"delegationTokenRevoked",e=>e.audience===n)}}}}}}