@kubun/plugin-p2p 0.8.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/context/group.js +1 -1
- package/lib/context/hub.d.ts +4 -0
- package/lib/context/hub.js +1 -0
- package/lib/context/join.js +1 -1
- package/lib/context/types.d.ts +12 -0
- package/lib/groups/broadcast-service.d.ts +71 -17
- package/lib/groups/broadcast-service.js +1 -1
- package/lib/groups/broadcast.d.ts +112 -5
- package/lib/groups/broadcast.js +1 -1
- package/lib/groups/events.d.ts +11 -0
- package/lib/groups/group-handle-registry.d.ts +90 -0
- package/lib/groups/group-handle-registry.js +1 -0
- package/lib/groups/invite-payload.d.ts +5 -1
- package/lib/groups/manager.d.ts +20 -14
- package/lib/groups/manager.js +1 -1
- package/lib/groups/mls-codec.d.ts +7 -0
- package/lib/groups/mls-codec.js +1 -0
- package/lib/groups/mls-encryptor.d.ts +25 -0
- package/lib/groups/mls-encryptor.js +1 -0
- package/lib/hub/connection-pool.d.ts +24 -0
- package/lib/hub/connection-pool.js +1 -1
- package/lib/hub/did-observing-transport.d.ts +64 -0
- package/lib/hub/did-observing-transport.js +1 -0
- package/lib/hub/errors.d.ts +28 -0
- package/lib/hub/errors.js +1 -0
- package/lib/hub/forward-remote-broadcast.d.ts +15 -0
- package/lib/hub/forward-remote-broadcast.js +1 -0
- package/lib/hub/group-channel.d.ts +29 -21
- package/lib/hub/group-channel.js +1 -1
- package/lib/hub/http-client.d.ts +17 -0
- package/lib/hub/http-client.js +1 -0
- package/lib/hub/hub-connection.d.ts +96 -0
- package/lib/hub/hub-connection.js +1 -0
- package/lib/hub/manager.d.ts +117 -0
- package/lib/hub/manager.js +1 -0
- package/lib/hub/receive-handler.d.ts +3 -6
- package/lib/hub/receive-handler.js +1 -1
- package/lib/hub/relay-manager.d.ts +85 -2
- package/lib/hub/relay-manager.js +1 -1
- package/lib/hub/send-handler.d.ts +12 -16
- package/lib/hub/send-handler.js +1 -1
- package/lib/hub/tunnel-inbox.d.ts +20 -0
- package/lib/hub/tunnel-inbox.js +1 -0
- package/lib/hub/wait-for-gate.d.ts +14 -0
- package/lib/hub/wait-for-gate.js +1 -0
- package/lib/hub/wiring.d.ts +73 -0
- package/lib/hub/wiring.js +1 -0
- package/lib/index.d.ts +102 -6
- package/lib/index.js +1 -1
- package/lib/schema.js +67 -19
- package/lib/sync/broadcast-queue.d.ts +59 -0
- package/lib/sync/broadcast-queue.js +1 -0
- package/lib/sync/broadcast-sender.d.ts +52 -0
- package/lib/sync/broadcast-sender.js +1 -0
- package/lib/sync/catalog-match.d.ts +13 -0
- package/lib/sync/catalog-match.js +1 -0
- package/lib/sync/forwarder.d.ts +96 -0
- package/lib/sync/forwarder.js +1 -0
- package/lib/sync/handlers.d.ts +15 -0
- package/lib/sync/handlers.js +1 -1
- package/lib/sync/hub-tunnel-sync-listener.d.ts +24 -0
- package/lib/sync/hub-tunnel-sync-listener.js +1 -0
- package/lib/sync/hub-tunnel-sync-provider.d.ts +36 -0
- package/lib/sync/hub-tunnel-sync-provider.js +1 -0
- package/lib/sync/merkle-apply.d.ts +41 -0
- package/lib/sync/merkle-apply.js +1 -1
- package/lib/sync/merkle-channel.d.ts +15 -0
- package/lib/sync/merkle-channel.js +1 -1
- package/lib/sync/receive-access-gate.d.ts +36 -0
- package/lib/sync/receive-access-gate.js +1 -0
- package/lib/sync/scope-resolver.d.ts +32 -0
- package/lib/sync/scope-resolver.js +1 -0
- package/lib/sync/sync-manager.d.ts +10 -0
- package/lib/sync/sync-manager.js +1 -1
- package/lib/types.d.ts +59 -0
- package/lib/util/mutex.d.ts +4 -0
- package/lib/util/mutex.js +1 -0
- package/package.json +40 -37
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { StoreProvider } from '@kubun/db';
|
|
2
|
+
export type Hub = {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string | null;
|
|
5
|
+
url: string;
|
|
6
|
+
serverDID: string | null;
|
|
7
|
+
createdAt: Date;
|
|
8
|
+
updatedAt: Date | null;
|
|
9
|
+
};
|
|
10
|
+
export type CreateHubParams = {
|
|
11
|
+
stores: StoreProvider;
|
|
12
|
+
url: string;
|
|
13
|
+
label?: string | null;
|
|
14
|
+
serverDID?: string | null;
|
|
15
|
+
};
|
|
16
|
+
export type UpdateHubParams = {
|
|
17
|
+
stores: StoreProvider;
|
|
18
|
+
id: string;
|
|
19
|
+
label?: string | null;
|
|
20
|
+
url?: string;
|
|
21
|
+
serverDID?: string | null;
|
|
22
|
+
};
|
|
23
|
+
export type DeleteHubParams = {
|
|
24
|
+
stores: StoreProvider;
|
|
25
|
+
id: string;
|
|
26
|
+
force?: boolean;
|
|
27
|
+
};
|
|
28
|
+
export type DeleteHubResult = {
|
|
29
|
+
success: boolean;
|
|
30
|
+
boundGroupIDs: Array<string>;
|
|
31
|
+
};
|
|
32
|
+
export type GetHubParams = {
|
|
33
|
+
stores: StoreProvider;
|
|
34
|
+
id: string;
|
|
35
|
+
};
|
|
36
|
+
export type ListHubsParams = {
|
|
37
|
+
stores: StoreProvider;
|
|
38
|
+
};
|
|
39
|
+
export type BindHubToGroupParams = {
|
|
40
|
+
stores: StoreProvider;
|
|
41
|
+
hubID: string;
|
|
42
|
+
groupID: string;
|
|
43
|
+
};
|
|
44
|
+
export type UnbindHubFromGroupParams = {
|
|
45
|
+
stores: StoreProvider;
|
|
46
|
+
hubID: string;
|
|
47
|
+
groupID: string;
|
|
48
|
+
};
|
|
49
|
+
export type ListHubsByGroupParams = {
|
|
50
|
+
stores: StoreProvider;
|
|
51
|
+
groupID: string;
|
|
52
|
+
};
|
|
53
|
+
export type ListGroupsByHubParams = {
|
|
54
|
+
stores: StoreProvider;
|
|
55
|
+
hubID: string;
|
|
56
|
+
};
|
|
57
|
+
export type CaptureServerDIDParams = {
|
|
58
|
+
stores: StoreProvider;
|
|
59
|
+
hubURL: string;
|
|
60
|
+
serverDID: string;
|
|
61
|
+
};
|
|
62
|
+
export type UpsertHubParams = {
|
|
63
|
+
stores: StoreProvider;
|
|
64
|
+
url: string;
|
|
65
|
+
serverDID?: string | null;
|
|
66
|
+
};
|
|
67
|
+
export declare class HubServerDIDConflictError extends Error {
|
|
68
|
+
readonly hubID: string;
|
|
69
|
+
readonly url: string;
|
|
70
|
+
readonly existingServerDID: string;
|
|
71
|
+
readonly providedServerDID: string;
|
|
72
|
+
constructor(params: {
|
|
73
|
+
hubID: string;
|
|
74
|
+
url: string;
|
|
75
|
+
existingServerDID: string;
|
|
76
|
+
providedServerDID: string;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
export declare function createHub(params: CreateHubParams): Promise<Hub>;
|
|
80
|
+
/**
|
|
81
|
+
* Idempotent register-or-pin entry-point. Applies the conflict matrix:
|
|
82
|
+
*
|
|
83
|
+
* - URL not registered → insert a fresh row with `serverDID` (or null) and
|
|
84
|
+
* return it.
|
|
85
|
+
* - existing row, `server_did === null`, provided null → no-op.
|
|
86
|
+
* - existing row, `server_did === null`, provided DID-A → upgrade pin to
|
|
87
|
+
* DID-A.
|
|
88
|
+
* - existing row, `server_did === DID-A`, provided null or matching DID-A →
|
|
89
|
+
* no-op (no silent downgrade).
|
|
90
|
+
* - existing row, `server_did === DID-A`, provided DID-B → throws
|
|
91
|
+
* `HubServerDIDConflictError`. Caller must explicit `updateHub` to change
|
|
92
|
+
* a pinned DID.
|
|
93
|
+
*
|
|
94
|
+
* Unlike `createHub`, `upsertHub` takes no `label` — it is for atomic
|
|
95
|
+
* register+bind paths where the inviter only ships connectivity + pin.
|
|
96
|
+
*/
|
|
97
|
+
export declare function upsertHub(params: UpsertHubParams): Promise<Hub>;
|
|
98
|
+
export declare function updateHub(params: UpdateHubParams): Promise<Hub>;
|
|
99
|
+
export declare function deleteHub(params: DeleteHubParams): Promise<DeleteHubResult>;
|
|
100
|
+
export declare function getHub(params: GetHubParams): Promise<Hub | null>;
|
|
101
|
+
export declare function listHubs(params: ListHubsParams): Promise<Array<Hub>>;
|
|
102
|
+
export declare function bindHubToGroup(params: BindHubToGroupParams): Promise<boolean>;
|
|
103
|
+
export declare function unbindHubFromGroup(params: UnbindHubFromGroupParams): Promise<boolean>;
|
|
104
|
+
export declare function listHubsByGroup(params: ListHubsByGroupParams): Promise<Array<Hub>>;
|
|
105
|
+
export declare function listGroupsByHub(params: ListGroupsByHubParams): Promise<Array<string>>;
|
|
106
|
+
/**
|
|
107
|
+
* TOFU capture path. Looks up the local hub row by URL and applies the
|
|
108
|
+
* conflict matrix:
|
|
109
|
+
* - URL not registered → no-op, returns `false`.
|
|
110
|
+
* - existing `server_did` is null → upgrade to `serverDID`, returns `true`.
|
|
111
|
+
* - existing `server_did` matches `serverDID` → no-op, returns `false`.
|
|
112
|
+
* - existing `server_did` differs from `serverDID` → throws
|
|
113
|
+
* `HubServerDIDConflictError`. Pinned-mismatch enforcement (hard-fail
|
|
114
|
+
* before any message is delivered) lives in the transport wrapper and
|
|
115
|
+
* surfaces `HubServerDIDMismatchError` to callers.
|
|
116
|
+
*/
|
|
117
|
+
export declare function captureServerDID(params: CaptureServerDIDParams): Promise<boolean>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{getP2PStore as r}from"@kubun/store-p2p";export class HubServerDIDConflictError extends Error{hubID;url;existingServerDID;providedServerDID;constructor(r){super(`hub at ${r.url} is pinned to ${r.existingServerDID}; refusing to overwrite with ${r.providedServerDID}`),this.name="HubServerDIDConflictError",this.hubID=r.hubID,this.url=r.url,this.existingServerDID=r.existingServerDID,this.providedServerDID=r.providedServerDID}}function e(r){return{id:r.id,label:r.label,url:r.url,serverDID:r.server_did,createdAt:new Date(r.created_at),updatedAt:null==r.updated_at?null:new Date(r.updated_at)}}export async function createHub(t){let u=await r(t.stores),i=t.serverDID??null,a=await u.getHubByURL(t.url);if(null!=a){if(null!=a.server_did&&null!=i&&a.server_did!==i)throw new HubServerDIDConflictError({hubID:a.id,url:t.url,existingServerDID:a.server_did,providedServerDID:i});if(null==a.server_did&&null!=i){await u.updateHub(a.id,{server_did:i});let r=await u.getHub(a.id);if(null==r)throw Error("Failed to read hub after upgrade");return e(r)}return e(a)}let l=crypto.randomUUID();await u.insertHub({id:l,label:t.label??null,url:t.url,server_did:i});let n=await u.getHub(l);if(null==n)throw Error("Failed to read hub after insert");return e(n)}export async function upsertHub(r){return await createHub({stores:r.stores,url:r.url,serverDID:r.serverDID})}export async function updateHub(t){let u=await r(t.stores),i=await u.getHub(t.id);if(null==i)throw Error(`Hub ${t.id} not found`);if(null!=t.url&&t.url!==i.url){let r=await u.getHubByURL(t.url);if(null!=r&&r.id!==t.id)throw Error(`URL ${t.url} already in use by hub ${r.id}`)}let a={};void 0!==t.label&&(a.label=t.label),void 0!==t.url&&(a.url=t.url),void 0!==t.serverDID&&(a.server_did=t.serverDID),await u.updateHub(t.id,a);let l=await u.getHub(t.id);if(null==l)throw Error("Failed to read hub after update");return e(l)}export async function deleteHub(e){let t=await r(e.stores),u=await t.listGroupIDsByHubID(e.id);return u.length>0&&!e.force?{success:!1,boundGroupIDs:u}:(u.length>0&&await t.deleteGroupHubsByHubID(e.id),await t.deleteHub(e.id),{success:!0,boundGroupIDs:u})}export async function getHub(t){let u=await r(t.stores),i=await u.getHub(t.id);return null==i?null:e(i)}export async function listHubs(t){let u=await r(t.stores);return(await u.listHubs()).map(e)}export async function bindHubToGroup(e){let t=await r(e.stores);return await t.insertGroupHub({group_id:e.groupID,hub_id:e.hubID})}export async function unbindHubFromGroup(e){let t=await r(e.stores);return await t.deleteGroupHub({groupID:e.groupID,hubID:e.hubID})}export async function listHubsByGroup(t){let u=await r(t.stores);return(await u.listHubsByGroupID(t.groupID)).map(e)}export async function listGroupsByHub(e){let t=await r(e.stores);return await t.listGroupIDsByHubID(e.hubID)}export async function captureServerDID(e){let t=await r(e.stores),u=await t.getHubByURL(e.hubURL);if(null==u)return!1;if(null==u.server_did)return await t.updateHub(u.id,{server_did:e.serverDID}),!0;if(u.server_did===e.serverDID)return!1;throw new HubServerDIDConflictError({hubID:u.id,url:e.hubURL,existingServerDID:u.server_did,providedServerDID:e.serverDID})}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Logger } from '@kubun/logger';
|
|
2
|
-
import type { P2PStoreAPI } from '@kubun/store-p2p';
|
|
3
2
|
import { type BroadcastService } from '../groups/broadcast-service.js';
|
|
4
3
|
/**
|
|
5
4
|
* Shape of a message pushed by `hub/receive`. Matches `@enkaku/hub-protocol`
|
|
@@ -14,9 +13,7 @@ export type ReceivedHubMessage = {
|
|
|
14
13
|
};
|
|
15
14
|
export type HandleReceivedMessageParams = {
|
|
16
15
|
groupID: string;
|
|
17
|
-
deviceID: string;
|
|
18
16
|
message: ReceivedHubMessage;
|
|
19
|
-
p2pStore: P2PStoreAPI;
|
|
20
17
|
broadcastService: BroadcastService;
|
|
21
18
|
logger?: Logger;
|
|
22
19
|
};
|
|
@@ -29,8 +26,8 @@ export type HandleReceivedMessageResult = {
|
|
|
29
26
|
* Returns `{ ack: true }` when the message has been processed to a state where
|
|
30
27
|
* redelivery serves no purpose (applied successfully, or decrypt failed with
|
|
31
28
|
* no recovery path). Returns `{ ack: false }` when the hub should redeliver
|
|
32
|
-
* (transient apply or
|
|
33
|
-
*
|
|
34
|
-
*
|
|
29
|
+
* (transient apply failure or unexpected error). Skips with `{ ack: false }`
|
|
30
|
+
* on groupID mismatch so the message stays queued for whichever recipient it
|
|
31
|
+
* was intended for.
|
|
35
32
|
*/
|
|
36
33
|
export declare function handleReceivedMessage(params: HandleReceivedMessageParams): Promise<HandleReceivedMessageResult>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fromB64 as e}from"@enkaku/codec";import{ApplyError as r,DecryptError as
|
|
1
|
+
import{fromB64 as e}from"@enkaku/codec";import{ApplyError as r,DecryptError as c}from"../groups/broadcast-service.js";export async function handleReceivedMessage(s){let{groupID:a,message:n,broadcastService:u,logger:i}=s;if(n.groupID!==a)return i?.debug("hub message groupID mismatch, skipping",{groupID:a,messageGroupID:n.groupID,sequenceID:n.sequenceID}),{ack:!1};let o=e(n.payload);try{await u.processReceived({groupID:a,encrypted:o})}catch(e){if(e instanceof c)return i?.debug("decrypt failed, acking and skipping",{groupID:a,sequenceID:n.sequenceID,error:e}),{ack:!0};if(e instanceof r)return i?.error("broadcast apply failed, will redeliver",{groupID:a,sequenceID:n.sequenceID,error:e}),{ack:!1};return i?.error("unexpected error processing hub message",{groupID:a,sequenceID:n.sequenceID,error:e}),{ack:!1}}return{ack:!0}}
|
|
@@ -1,12 +1,65 @@
|
|
|
1
1
|
import type { Client } from '@enkaku/client';
|
|
2
|
+
import { EventEmitter } from '@enkaku/event';
|
|
2
3
|
import type { HubProtocol } from '@enkaku/hub-protocol';
|
|
3
4
|
import type { Logger } from '@kubun/logger';
|
|
4
5
|
import type { P2PStoreAPI } from '@kubun/store-p2p';
|
|
5
6
|
import type { GroupBroadcastMessage } from '../groups/broadcast.js';
|
|
6
7
|
import type { BroadcastService } from '../groups/broadcast-service.js';
|
|
8
|
+
import { type OnServerDIDObserved } from './connection-pool.js';
|
|
9
|
+
import type { ServerDIDMismatchObserver, ServerDIDObserver } from './did-observing-transport.js';
|
|
7
10
|
import { GroupChannel, type GroupChannelParams } from './group-channel.js';
|
|
8
|
-
|
|
11
|
+
import { HubConnection, type HubConnectionParams } from './hub-connection.js';
|
|
12
|
+
export type CreateHubClientOptions = {
|
|
13
|
+
/**
|
|
14
|
+
* Per-call observer fired once when the first signed response arrives. Set
|
|
15
|
+
* by `HubConnectionPool` so the pool can route the observed server DID
|
|
16
|
+
* through its `onServerDIDObserved` handler with the hub URL bound. Factories
|
|
17
|
+
* that don't honour this opt simply skip TOFU capture for pool-spawned
|
|
18
|
+
* clients.
|
|
19
|
+
*/
|
|
20
|
+
onServerDID?: ServerDIDObserver;
|
|
21
|
+
/**
|
|
22
|
+
* Pinned `server_did` resolved by the pool from the local hub row at spawn
|
|
23
|
+
* time. When set, the underlying `DIDObservingTransport` enforces it: a
|
|
24
|
+
* non-matching first response raises `HubServerDIDMismatchError` and no
|
|
25
|
+
* message is delivered to the inner Client (hard-fail path). When unset,
|
|
26
|
+
* the wrapper falls back to TOFU capture via `onServerDID`.
|
|
27
|
+
*/
|
|
28
|
+
expectedServerDID?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Side-channel notifier fired when a pinned-DID mismatch is detected. Lets
|
|
31
|
+
* the pool log + evict on top of the typed error that propagates through
|
|
32
|
+
* the read loop.
|
|
33
|
+
*/
|
|
34
|
+
onMismatch?: ServerDIDMismatchObserver;
|
|
35
|
+
};
|
|
36
|
+
export type CreateHubClient = (hubURL: string, opts?: CreateHubClientOptions) => Promise<Client<HubProtocol>>;
|
|
9
37
|
export type CreateGroupChannel = (params: GroupChannelParams) => GroupChannel;
|
|
38
|
+
export type CreateHubConnection = (params: HubConnectionParams) => HubConnection;
|
|
39
|
+
export type RelayEvent = {
|
|
40
|
+
type: 'connected';
|
|
41
|
+
hubURL: string;
|
|
42
|
+
groupIDs: Array<string>;
|
|
43
|
+
} | {
|
|
44
|
+
type: 'disconnected';
|
|
45
|
+
hubURL: string;
|
|
46
|
+
groupIDs: Array<string>;
|
|
47
|
+
reason?: string;
|
|
48
|
+
} | {
|
|
49
|
+
type: 'error';
|
|
50
|
+
hubURL: string;
|
|
51
|
+
groupIDs: Array<string>;
|
|
52
|
+
error: unknown;
|
|
53
|
+
} | {
|
|
54
|
+
type: 'reconnecting';
|
|
55
|
+
hubURL: string;
|
|
56
|
+
groupIDs: Array<string>;
|
|
57
|
+
attempt: number;
|
|
58
|
+
delayMs: number;
|
|
59
|
+
};
|
|
60
|
+
type RelayManagerEvents = {
|
|
61
|
+
relay: RelayEvent;
|
|
62
|
+
};
|
|
10
63
|
export type HubRelayManagerParams = {
|
|
11
64
|
p2pStore: P2PStoreAPI;
|
|
12
65
|
broadcastService: BroadcastService;
|
|
@@ -18,10 +71,21 @@ export type HubRelayManagerParams = {
|
|
|
18
71
|
backoffBaseMs?: number;
|
|
19
72
|
backoffMaxMs?: number;
|
|
20
73
|
backoffJitter?: number;
|
|
74
|
+
broadcastTimeoutMs?: number;
|
|
21
75
|
createGroupChannel?: CreateGroupChannel;
|
|
76
|
+
createHubConnection?: CreateHubConnection;
|
|
77
|
+
/**
|
|
78
|
+
* Forwarded to the underlying `HubConnectionPool`. Fired with the observed
|
|
79
|
+
* `serverDID` after the first signed response from a freshly-spawned client.
|
|
80
|
+
* Used by the hub manager to apply the TOFU capture conflict matrix (the
|
|
81
|
+
* `null → DID-A` upgrade). Errors are caught and logged by the pool;
|
|
82
|
+
* pinned-mismatch enforcement is a separate path inside the wrapper.
|
|
83
|
+
*/
|
|
84
|
+
onServerDIDObserved?: OnServerDIDObserved;
|
|
22
85
|
};
|
|
23
86
|
export declare class HubRelayManager {
|
|
24
87
|
#private;
|
|
88
|
+
readonly relayEvents: EventEmitter<RelayManagerEvents>;
|
|
25
89
|
constructor(params: HubRelayManagerParams);
|
|
26
90
|
/**
|
|
27
91
|
* Open channels for each group in parallel. Per-group failures are logged
|
|
@@ -31,7 +95,26 @@ export declare class HubRelayManager {
|
|
|
31
95
|
*/
|
|
32
96
|
start(groupIDs: Array<string>): Promise<void>;
|
|
33
97
|
addGroup(groupID: string): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Post-join binding-add hook. Single-hub regime — when the group already
|
|
100
|
+
* has an active channel:
|
|
101
|
+
* - same hubURL → debug log, no-op (idempotent re-bind);
|
|
102
|
+
* - different hubURL → warn, no-op (extra binding deferred until
|
|
103
|
+
* multi-hub support lands).
|
|
104
|
+
* When the group has no active channel yet (e.g. binding was created
|
|
105
|
+
* before relay setup ran), open one for the supplied hub.
|
|
106
|
+
*/
|
|
107
|
+
addBinding(groupID: string, hubURL: string): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Post-join binding-remove hook. No-op when the unbound hub is not the
|
|
110
|
+
* group's active hub (single-hub regime: unbinding a non-active hub leaves
|
|
111
|
+
* the channel intact). When it matches, behaves like `removeGroup`.
|
|
112
|
+
*/
|
|
113
|
+
removeBinding(groupID: string, hubURL: string): Promise<void>;
|
|
34
114
|
removeGroup(groupID: string): Promise<void>;
|
|
35
|
-
broadcast(groupID: string, message: GroupBroadcastMessage
|
|
115
|
+
broadcast(groupID: string, message: GroupBroadcastMessage, opts?: {
|
|
116
|
+
timeoutMs?: number;
|
|
117
|
+
}): Promise<void>;
|
|
36
118
|
stop(): Promise<void>;
|
|
37
119
|
}
|
|
120
|
+
export {};
|
package/lib/hub/relay-manager.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
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.#v(e,t[0].url))}async #v(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.#v(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 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,24 @@
|
|
|
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
|
-
|
|
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
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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>;
|
package/lib/hub/send-handler.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{toB64 as
|
|
1
|
+
import{toB64 as r}from"@enkaku/codec";export async function handleSendBroadcast(e){let{groupID:a,message:o,broadcastService:n,send:t,logger:d}=e;try{await n.prepareSend({groupID:a,message:o,send:e=>t(a,r(e))})}catch(r){throw d?.error("hub/group/send failed",{groupID:a,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,73 @@
|
|
|
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 { GroupEventEmitter } 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
|
+
dispose: () => Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
export type SetupHubRelayParams = {
|
|
21
|
+
identity: OwnIdentity;
|
|
22
|
+
db: KubunDB;
|
|
23
|
+
/**
|
|
24
|
+
* Engine graph internals — threaded through to `BroadcastService` so the
|
|
25
|
+
* `mutation:apply` broadcast variant can route peer-authored mutations
|
|
26
|
+
* through the engine with `origin: 'peer'`.
|
|
27
|
+
*/
|
|
28
|
+
graph: GraphInternals;
|
|
29
|
+
emitter: GroupEventEmitter;
|
|
30
|
+
createHubClient: CreateHubClient;
|
|
31
|
+
onRemoteBroadcast: (event: BroadcastEvent) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Single canonical access point for MLS `GroupHandle` instances. Threaded
|
|
34
|
+
* into `BroadcastService` so encrypt/decrypt go through the per-group
|
|
35
|
+
* mutex once Phase 3 lands. Currently stored but unused.
|
|
36
|
+
*/
|
|
37
|
+
registry: GroupHandleRegistry;
|
|
38
|
+
logger: Logger;
|
|
39
|
+
/**
|
|
40
|
+
* Optional testing seam forwarded to `HubRelayManager`. Production code
|
|
41
|
+
* leaves this unset so the manager uses its default `GroupChannel`
|
|
42
|
+
* constructor.
|
|
43
|
+
*/
|
|
44
|
+
createGroupChannel?: (params: GroupChannelParams) => GroupChannel;
|
|
45
|
+
/**
|
|
46
|
+
* Optional testing seam forwarded to `HubRelayManager`. Production code
|
|
47
|
+
* leaves this unset so the manager uses its default `HubConnection`
|
|
48
|
+
* constructor.
|
|
49
|
+
*/
|
|
50
|
+
createHubConnection?: (params: HubConnectionParams) => HubConnection;
|
|
51
|
+
/**
|
|
52
|
+
* Receive-side storage mode. Forwarded to `BroadcastService` →
|
|
53
|
+
* `processBroadcast`. Defaults to `'persist'` when omitted.
|
|
54
|
+
*/
|
|
55
|
+
storeUnreadable?: StoreUnreadableMode;
|
|
56
|
+
/** Server default access level — required when `storeUnreadable === 'drop'`. */
|
|
57
|
+
defaultAccessLevel?: DefaultAccessLevel;
|
|
58
|
+
/**
|
|
59
|
+
* Forwarding config. Threaded through to `BroadcastService` →
|
|
60
|
+
* `processBroadcast` along with the hub's own `scheduleBroadcast` hook.
|
|
61
|
+
* Default `false` disables the receive-time forwarding pipeline.
|
|
62
|
+
*/
|
|
63
|
+
forwarding?: ForwardingConfig;
|
|
64
|
+
/**
|
|
65
|
+
* Forwarded to `HubRelayManager` → `HubConnectionPool`. Fired with the
|
|
66
|
+
* observed `serverDID` after the first signed response from a freshly-spawned
|
|
67
|
+
* client. Lets callers route capture into `captureServerDID` for TOFU
|
|
68
|
+
* pinning; omitting it skips capture entirely.
|
|
69
|
+
*/
|
|
70
|
+
onServerDIDObserved?: OnServerDIDObserved;
|
|
71
|
+
};
|
|
72
|
+
export declare function setupHubRelay(params: SetupHubRelayParams): HubWiring;
|
|
73
|
+
export declare const DISABLED_HUB: HubWiring;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{BroadcastService as r}from"../groups/broadcast-service.js";import{HubRelayManager as e}from"./relay-manager.js";export function setupHubRelay(o){let a,{identity:t,db:d,graph:i,emitter:s,createHubClient:n,onRemoteBroadcast:p,registry:c,logger:u,createGroupChannel:h,createHubConnection:l,storeUnreadable:g,defaultAccessLevel:f,forwarding:m,onServerDIDObserved:y}=o,b=[],D=(r,e)=>{a.then(o=>o.broadcast(r,e).catch(e=>{u.error("broadcast failed",{groupID:r,error:e})}),()=>{})};return(a=(async()=>{let[o,a]=await Promise.all([d.getStore("p2p"),d.getStore("graph")]),v=new r({p2pStore:o,graphStore:a,graph:i,selfDID:t.id,registry:c,storeUnreadable:g,defaultAccessLevel:f,forwarding:m,scheduleBroadcast:D,logger:u}),B=new e({p2pStore:o,broadcastService:v,deviceID:t.id,createHubClient:n,logger:u,createGroupChannel:h,createHubConnection:l,onServerDIDObserved:y});b.push(v.on("broadcast",p)),b.push(s.on("groupJoined",r=>B.addGroup(r.id).catch(e=>{u.error("addGroup failed",{groupID:r.id,error:e})}))),b.push(s.on("groupLeft",r=>B.removeGroup(r.groupID).catch(e=>{u.error("removeGroup failed",{groupID:r.groupID,error:e})}))),b.push(s.on("hubBound",({groupID:r,hubURL:e})=>B.addBinding(r,e).catch(o=>{u.error("addBinding failed",{groupID:r,hubURL:e,error:o})}))),b.push(s.on("hubUnbound",({groupID:r,hubURL:e})=>B.removeBinding(r,e).catch(o=>{u.error("removeBinding failed",{groupID:r,hubURL:e,error:o})})));let w=await o.listGroups();return await B.start(w.map(r=>r.id)),B})()).catch(r=>{u.error("hub relay setup failed",{error:r})}),{ready:a.then(()=>void 0),scheduleBroadcast:D,dispose:async()=>{for(let r of b)try{r()}catch{}b.length=0;try{let r=await a;await r.stop()}catch{}}}}export const DISABLED_HUB={ready:Promise.resolve(),scheduleBroadcast:()=>{},dispose:async()=>{}};
|
package/lib/index.d.ts
CHANGED
|
@@ -1,26 +1,122 @@
|
|
|
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,
|
|
10
|
+
export { ApplyError, type BroadcastEvent, BroadcastService, type BroadcastServiceEvents, type BroadcastServiceParams, DecryptError, type ReceiveBroadcastParams, type ReceiveBroadcastResult, type SendBroadcastParams, } from './groups/broadcast-service.js';
|
|
6
11
|
export { createFilteredGenerator, createGroupEventEmitter, type GroupEventEmitter, type GroupEventMap, } from './groups/events.js';
|
|
7
|
-
export {
|
|
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';
|
|
23
|
+
export { catalogMatchesDoc } from './sync/catalog-match.js';
|
|
14
24
|
export { type CatalogSyncScope, resolveCatalogSyncScopes } from './sync/catalog-scope.js';
|
|
25
|
+
export { type EvaluateAndForwardParams, type EvaluateAndForwardResult, evaluateAndForward, type ForwardContext, type ForwardFilter, type ForwardingConfig, } from './sync/forwarder.js';
|
|
15
26
|
export { checkSyncDelegation, negotiateDirection } from './sync/handlers.js';
|
|
27
|
+
export { HubTunnelSyncListener, type HubTunnelSyncListenerParams, } from './sync/hub-tunnel-sync-listener.js';
|
|
28
|
+
export { HubTunnelSyncProvider, type HubTunnelSyncProviderParams, } from './sync/hub-tunnel-sync-provider.js';
|
|
16
29
|
export { type PeerConfig, type PeerConfigWithID, PeerRegistry } from './sync/peer-registry.js';
|
|
17
30
|
export { SyncClient, type SyncClientParams, type SyncScope, type SyncTransportProvider, } from './sync/sync-client.js';
|
|
18
31
|
export { type SyncEvent, SyncManager, type SyncManagerParams, type SyncSessionInfo, type SyncStatus, } from './sync/sync-manager.js';
|
|
32
|
+
export type { OnServerDIDObserved } from './hub/connection-pool.js';
|
|
33
|
+
export { DIDObservingTransport, type DIDObservingTransportParams, type ServerDIDObserver, } from './hub/did-observing-transport.js';
|
|
34
|
+
export { ReconnectingError } from './hub/errors.js';
|
|
19
35
|
export type { GroupChannel, GroupChannelParams } from './hub/group-channel.js';
|
|
20
|
-
export { type
|
|
21
|
-
export type {
|
|
36
|
+
export { type CreateHTTPHubClientParams, createHTTPHubClient } from './hub/http-client.js';
|
|
37
|
+
export type { ConnectionLifecycleEvent } from './hub/hub-connection.js';
|
|
38
|
+
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';
|
|
39
|
+
export { type CreateGroupChannel, type CreateHubClient, type CreateHubClientOptions, type CreateHubConnection, HubRelayManager, type HubRelayManagerParams, type RelayEvent, } from './hub/relay-manager.js';
|
|
40
|
+
export { DISABLED_HUB, type HubWiring, type SetupHubRelayParams, setupHubRelay, } from './hub/wiring.js';
|
|
41
|
+
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';
|
|
42
|
+
export type ReceiveConfig = {
|
|
43
|
+
/**
|
|
44
|
+
* Receiver storage mode for incoming peer mutations.
|
|
45
|
+
* - `'persist'` (default) — store every doc that passes catalog/group
|
|
46
|
+
* checks. Access enforced at query time; unreadable docs remain in DB
|
|
47
|
+
* but filtered.
|
|
48
|
+
* - `'drop'` — apply read-access check at receive time using the local
|
|
49
|
+
* peer's own DID as viewer. Mutations the local peer cannot read are
|
|
50
|
+
* not persisted.
|
|
51
|
+
*/
|
|
52
|
+
storeUnreadable: StoreUnreadableMode;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Default receive config. Under `'persist'` no gate is built and no extra
|
|
56
|
+
* DB lookups occur during receive.
|
|
57
|
+
*/
|
|
58
|
+
export declare const DEFAULT_RECEIVE_CONFIG: ReceiveConfig;
|
|
22
59
|
export type P2PPluginOptions = {
|
|
23
60
|
autoAcceptPeers?: Array<string>;
|
|
24
61
|
http?: boolean | string;
|
|
62
|
+
/**
|
|
63
|
+
* Enable hub-mediated group broadcasts.
|
|
64
|
+
* - `true` / `{}` — build a default HTTP hub client factory from
|
|
65
|
+
* `params.identity` + `params.runtime.fetch`.
|
|
66
|
+
* - `{ createHubClient }` — caller supplies a custom factory.
|
|
67
|
+
* - absent / `false` — hub relay disabled; all broadcast call-sites are
|
|
68
|
+
* no-ops.
|
|
69
|
+
*/
|
|
70
|
+
hub?: boolean | {
|
|
71
|
+
createHubClient?: CreateHubClient;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Batch broadcast queue config. Sender-side mutation:apply broadcasts are
|
|
75
|
+
* accumulated per target MLS group and flushed when window/count/byte
|
|
76
|
+
* thresholds trip. Defaults from {@link DEFAULT_BROADCAST_BATCH_CONFIG}.
|
|
77
|
+
* Set `enabled: false` to bypass the queue and emit one broadcast per
|
|
78
|
+
* mutation per scope.
|
|
79
|
+
*/
|
|
80
|
+
batch?: Partial<BroadcastBatchConfig>;
|
|
81
|
+
/**
|
|
82
|
+
* Push-sync config. Gates the local `engine:mutation:authored` subscriber
|
|
83
|
+
* that drives `mutation:apply` broadcasts.
|
|
84
|
+
* - When `enabled: false` (default), no `mutation:apply` broadcasts are
|
|
85
|
+
* emitted from this peer when local mutations land — even if `hub` is
|
|
86
|
+
* configured. Merkle-pull continues to work, and the receive path
|
|
87
|
+
* (incoming `mutation:apply` broadcasts) is unaffected.
|
|
88
|
+
* - When `enabled: true`, locally-authored mutations fan out per-scope
|
|
89
|
+
* through the hub.
|
|
90
|
+
*
|
|
91
|
+
* Defaults from {@link DEFAULT_PUSH_SYNC_CONFIG}.
|
|
92
|
+
*/
|
|
93
|
+
pushSync?: Partial<PushSyncConfig>;
|
|
94
|
+
/**
|
|
95
|
+
* Receive-side storage config. Controls whether peer mutations the local
|
|
96
|
+
* peer cannot read are persisted to the local DB or dropped at receive
|
|
97
|
+
* time. Defaults to {@link DEFAULT_RECEIVE_CONFIG} (persist).
|
|
98
|
+
*/
|
|
99
|
+
receive?: Partial<ReceiveConfig>;
|
|
100
|
+
/**
|
|
101
|
+
* Server default access level used by the receive-time access gate when
|
|
102
|
+
* `receive.storeUnreadable === 'drop'` and no document or per-user-model
|
|
103
|
+
* default rule is declared. Defaults to `{ read: 'only_owner', write:
|
|
104
|
+
* 'only_owner' }` (no fan-out for undeclared models). Ignored under
|
|
105
|
+
* `'persist'`.
|
|
106
|
+
*/
|
|
107
|
+
defaultAccessLevel?: DefaultAccessLevel;
|
|
108
|
+
/**
|
|
109
|
+
* Forwarding config. Controls per-peer N+1 re-broadcast.
|
|
110
|
+
* - `false` (default) — incoming `mutation:apply` broadcasts are NOT
|
|
111
|
+
* re-broadcast.
|
|
112
|
+
* - `true` — re-broadcast each applied entry to every other MLS group the
|
|
113
|
+
* local peer belongs to whose access + catalog gates pass.
|
|
114
|
+
* - {@link ForwardFilter} — same as `true`, plus per-candidate filter
|
|
115
|
+
* callback that further narrows. Filter receives a {@link ForwardContext}.
|
|
116
|
+
*
|
|
117
|
+
* Push-sync config (`pushSync.enabled`) is orthogonal — forwarding fires
|
|
118
|
+
* regardless of whether locally-authored mutations are broadcast.
|
|
119
|
+
*/
|
|
120
|
+
forwarding?: ForwardingConfig;
|
|
25
121
|
};
|
|
26
122
|
export declare function createP2PPlugin(options?: P2PPluginOptions): (params: PluginFactoryParams) => KubunPlugin;
|