@kubun/plugin-p2p 0.10.0 → 0.11.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/delegation.js +130 -1
- package/lib/context/group.js +1319 -1
- package/lib/context/hub.js +150 -1
- package/lib/context/join.js +80 -1
- package/lib/context/sync.js +19 -1
- package/lib/context/types.d.ts +12 -3
- package/lib/context/types.js +28 -1
- package/lib/groups/admin-roster.d.ts +130 -0
- package/lib/groups/admin-roster.js +184 -0
- package/lib/groups/broadcast-codec.js +6 -1
- package/lib/groups/broadcast.d.ts +122 -38
- package/lib/groups/broadcast.js +627 -1
- package/lib/groups/circle-projection.d.ts +122 -0
- package/lib/groups/circle-projection.js +288 -0
- package/lib/groups/circle-reducers.d.ts +178 -0
- package/lib/groups/circle-reducers.js +198 -0
- package/lib/groups/events.d.ts +7 -2
- package/lib/groups/events.js +10 -1
- package/lib/groups/group-anchor.d.ts +63 -0
- package/lib/groups/group-anchor.js +107 -0
- package/lib/groups/group-crypto.d.ts +19 -0
- package/lib/groups/group-crypto.js +85 -0
- package/lib/groups/group-handle-registry.d.ts +69 -2
- package/lib/groups/group-handle-registry.js +243 -1
- package/lib/groups/group-handlers.d.ts +29 -0
- package/lib/groups/group-handlers.js +92 -0
- package/lib/groups/group-health-monitor.d.ts +56 -0
- package/lib/groups/group-health-monitor.js +139 -0
- package/lib/groups/group-mls.d.ts +35 -0
- package/lib/groups/group-mls.js +164 -0
- package/lib/groups/group-peer-manager.d.ts +108 -0
- package/lib/groups/group-peer-manager.js +344 -0
- package/lib/groups/group-protocols.d.ts +297 -0
- package/lib/groups/group-protocols.js +174 -0
- package/lib/groups/invite-payload.d.ts +1 -1
- package/lib/groups/invite-payload.js +26 -1
- package/lib/groups/join-utils.d.ts +1 -0
- package/lib/groups/join-utils.js +104 -1
- package/lib/groups/ledger-affected-events.d.ts +15 -0
- package/lib/groups/ledger-affected-events.js +96 -0
- package/lib/groups/ledger-entry.d.ts +37 -0
- package/lib/groups/ledger-entry.js +56 -0
- package/lib/groups/ledger-fold.d.ts +56 -0
- package/lib/groups/ledger-fold.js +53 -0
- package/lib/groups/manager.d.ts +29 -71
- package/lib/groups/manager.js +281 -1
- package/lib/groups/mls-codec.d.ts +1 -1
- package/lib/groups/mls-codec.js +18 -1
- package/lib/groups/mls-encryptor.d.ts +2 -2
- package/lib/groups/mls-encryptor.js +30 -1
- package/lib/groups/mls-group-handle.d.ts +1 -1
- package/lib/groups/mls-group-handle.js +10 -1
- package/lib/groups/mls-json.js +17 -1
- package/lib/groups/mls-receive-errors.d.ts +29 -0
- package/lib/groups/mls-receive-errors.js +42 -0
- package/lib/groups/mls-state.d.ts +1 -1
- package/lib/groups/mls-state.js +56 -1
- package/lib/groups/store-received-grant.d.ts +4 -1
- package/lib/groups/store-received-grant.js +116 -1
- package/lib/groups/store-received-revocation.d.ts +3 -3
- package/lib/groups/store-received-revocation.js +82 -1
- package/lib/hub/ack-backoff.d.ts +22 -0
- package/lib/hub/ack-backoff.js +88 -0
- package/lib/hub/did-observing-transport.d.ts +13 -2
- package/lib/hub/did-observing-transport.js +126 -1
- package/lib/hub/errors.d.ts +4 -8
- package/lib/hub/errors.js +33 -1
- package/lib/hub/http-client.d.ts +28 -3
- package/lib/hub/http-client.js +32 -1
- package/lib/hub/hub-like.d.ts +24 -0
- package/lib/hub/hub-like.js +535 -0
- package/lib/hub/manager.d.ts +5 -4
- package/lib/hub/manager.js +198 -1
- package/lib/hub/wiring.d.ts +28 -41
- package/lib/hub/wiring.js +132 -1
- package/lib/index.d.ts +29 -13
- package/lib/index.js +392 -1
- package/lib/protocol.d.ts +8 -1
- package/lib/protocol.js +251 -1
- package/lib/schema.d.ts +2 -1
- package/lib/schema.js +471 -2
- package/lib/sync/authorize.d.ts +25 -0
- package/lib/sync/authorize.js +73 -0
- package/lib/sync/broadcast-queue.js +183 -1
- package/lib/sync/broadcast-sender.js +179 -1
- package/lib/sync/catalog-scope.js +68 -1
- package/lib/sync/errors.d.ts +12 -0
- package/lib/sync/errors.js +19 -0
- package/lib/sync/forwarder.js +277 -1
- package/lib/sync/handlers.d.ts +3 -22
- package/lib/sync/handlers.js +160 -1
- package/lib/sync/hub-tunnel-sync-listener.d.ts +9 -3
- package/lib/sync/hub-tunnel-sync-listener.js +172 -1
- package/lib/sync/hub-tunnel-sync-provider.d.ts +14 -14
- package/lib/sync/hub-tunnel-sync-provider.js +64 -1
- package/lib/sync/merkle-apply.d.ts +5 -0
- package/lib/sync/merkle-apply.js +194 -1
- package/lib/sync/merkle-channel.d.ts +19 -0
- package/lib/sync/merkle-channel.js +182 -1
- package/lib/sync/merkle-tree.js +120 -1
- package/lib/sync/peer-registry.d.ts +8 -1
- package/lib/sync/peer-registry.js +107 -1
- package/lib/sync/receive-access-gate.js +24 -1
- package/lib/sync/scope-resolver.js +41 -1
- package/lib/sync/sync-client.d.ts +5 -3
- package/lib/sync/sync-client.js +192 -1
- package/lib/sync/sync-manager.d.ts +2 -2
- package/lib/sync/sync-manager.js +272 -1
- package/lib/types.d.ts +41 -0
- package/lib/types.js +2 -1
- package/lib/util/mutex.js +10 -1
- package/lib/utils.d.ts +5 -0
- package/lib/utils.js +21 -0
- package/package.json +48 -41
- package/lib/groups/broadcast-service.d.ts +0 -189
- package/lib/groups/broadcast-service.js +0 -1
- package/lib/groups/wire-frame.d.ts +0 -32
- package/lib/groups/wire-frame.js +0 -1
- package/lib/hub/connection-pool.d.ts +0 -43
- package/lib/hub/connection-pool.js +0 -1
- package/lib/hub/forward-remote-broadcast.d.ts +0 -15
- package/lib/hub/forward-remote-broadcast.js +0 -1
- package/lib/hub/group-channel.d.ts +0 -67
- package/lib/hub/group-channel.js +0 -1
- package/lib/hub/hub-connection.d.ts +0 -96
- package/lib/hub/hub-connection.js +0 -1
- package/lib/hub/receive-handler.d.ts +0 -33
- package/lib/hub/receive-handler.js +0 -1
- package/lib/hub/relay-manager.d.ts +0 -127
- package/lib/hub/relay-manager.js +0 -1
- package/lib/hub/send-handler.d.ts +0 -40
- package/lib/hub/send-handler.js +0 -1
- package/lib/hub/tunnel-inbox.d.ts +0 -20
- package/lib/hub/tunnel-inbox.js +0 -1
- package/lib/hub/wait-for-gate.d.ts +0 -14
- package/lib/hub/wait-for-gate.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/plugin-p2p",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"license": "see LICENSE.md",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -14,54 +14,61 @@
|
|
|
14
14
|
],
|
|
15
15
|
"sideEffects": false,
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@
|
|
18
|
-
"@
|
|
19
|
-
"@
|
|
20
|
-
"@enkaku/
|
|
21
|
-
"@
|
|
22
|
-
"@
|
|
23
|
-
"@
|
|
24
|
-
"@
|
|
25
|
-
"@
|
|
26
|
-
"@enkaku/
|
|
27
|
-
"@enkaku/
|
|
28
|
-
"@
|
|
29
|
-
"@
|
|
30
|
-
"@enkaku/
|
|
31
|
-
"@
|
|
32
|
-
"@
|
|
33
|
-
"@enkaku/
|
|
34
|
-
"@
|
|
17
|
+
"@sozai/async": "^0.1.0",
|
|
18
|
+
"@kumiai/broadcast": "^0.1.0",
|
|
19
|
+
"@kokuin/capability": "^0.1.0",
|
|
20
|
+
"@enkaku/client": "^0.18.1",
|
|
21
|
+
"@sozai/codec": "^0.1.0",
|
|
22
|
+
"@sozai/event": "^0.1.0",
|
|
23
|
+
"@sozai/generator": "^0.1.0",
|
|
24
|
+
"@kumiai/mls": "^0.1.0",
|
|
25
|
+
"@kumiai/rpc": "^0.1.0",
|
|
26
|
+
"@enkaku/http-fetch": "^0.18.1",
|
|
27
|
+
"@enkaku/http-serve": "^0.18.1",
|
|
28
|
+
"@kumiai/hub-protocol": "^0.1.0",
|
|
29
|
+
"@kumiai/hub-tunnel": "^0.1.0",
|
|
30
|
+
"@enkaku/protocol": "^0.18.1",
|
|
31
|
+
"@sozai/runtime": "^0.1.0",
|
|
32
|
+
"@sozai/schema": "^0.1.0",
|
|
33
|
+
"@enkaku/server": "^0.18.1",
|
|
34
|
+
"@sozai/stream": "^0.1.0",
|
|
35
|
+
"@kokuin/token": "^0.1.1",
|
|
36
|
+
"@enkaku/transport": "^0.18.1",
|
|
35
37
|
"@noble/hashes": "^2.2.0",
|
|
36
|
-
"graphql": "^16.
|
|
38
|
+
"graphql": "^16.14.2",
|
|
37
39
|
"kysely": "^0.29.2",
|
|
38
|
-
"
|
|
39
|
-
"@kubun/db": "^0.
|
|
40
|
-
"@kubun/
|
|
41
|
-
"@kubun/graphql": "^0.
|
|
42
|
-
"@kubun/
|
|
43
|
-
"@kubun/
|
|
44
|
-
"@kubun/
|
|
45
|
-
"@kubun/
|
|
46
|
-
"@kubun/
|
|
47
|
-
"@kubun/
|
|
48
|
-
"@kubun/
|
|
49
|
-
"@kubun/
|
|
40
|
+
"ts-mls": "2.0.0-rc.13",
|
|
41
|
+
"@kubun/db": "^0.11.0",
|
|
42
|
+
"@kubun/db-adapter": "^0.11.0",
|
|
43
|
+
"@kubun/graphql": "^0.11.0",
|
|
44
|
+
"@kubun/hlc": "^0.11.0",
|
|
45
|
+
"@kubun/engine": "^0.11.0",
|
|
46
|
+
"@kubun/http-util": "^0.11.0",
|
|
47
|
+
"@kubun/logger": "^0.11.0",
|
|
48
|
+
"@kubun/client": "^0.11.0",
|
|
49
|
+
"@kubun/id": "^0.11.0",
|
|
50
|
+
"@kubun/mutation": "^0.11.0",
|
|
51
|
+
"@kubun/store-graph": "^0.11.0",
|
|
52
|
+
"@kubun/protocol": "^0.11.0",
|
|
53
|
+
"@kubun/store-delegation": "^0.11.0",
|
|
54
|
+
"@kubun/store-p2p": "^0.11.0"
|
|
50
55
|
},
|
|
51
56
|
"devDependencies": {
|
|
52
|
-
"@
|
|
57
|
+
"@kumiai/hub-server": "^0.1.0",
|
|
58
|
+
"@testcontainers/postgresql": "^12.0.4",
|
|
53
59
|
"get-port": "^7.2.0",
|
|
54
|
-
"@kubun/db-better-sqlite": "^0.
|
|
55
|
-
"@kubun/
|
|
56
|
-
"@kubun/
|
|
57
|
-
"@kubun/db-
|
|
58
|
-
"@kubun/plugin-
|
|
59
|
-
"@kubun/plugin-
|
|
60
|
-
"@kubun/
|
|
60
|
+
"@kubun/db-better-sqlite": "^0.11.0",
|
|
61
|
+
"@kubun/db-node-sqlite": "^0.11.0",
|
|
62
|
+
"@kubun/plugin-connector": "^0.11.0",
|
|
63
|
+
"@kubun/db-postgres": "^0.11.0",
|
|
64
|
+
"@kubun/plugin-rpc": "^0.11.0",
|
|
65
|
+
"@kubun/plugin-http": "^0.11.0",
|
|
66
|
+
"@kubun/hub": "^0.11.0",
|
|
67
|
+
"@kubun/test-utils": "^0.11.0"
|
|
61
68
|
},
|
|
62
69
|
"scripts": {
|
|
63
70
|
"build:clean": "del lib",
|
|
64
|
-
"build:js": "swc src -d ./lib --config-file ../../swc.json --strip-leading-paths",
|
|
71
|
+
"build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
65
72
|
"build:types": "tsc --emitDeclarationOnly --skipLibCheck",
|
|
66
73
|
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|
|
67
74
|
"test:types": "tsc --noEmit -p tsconfig.test.json",
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from '@enkaku/event';
|
|
2
|
-
import type { DefaultAccessLevel, GraphInternals } from '@kubun/engine';
|
|
3
|
-
import { HLC } from '@kubun/hlc';
|
|
4
|
-
import type { Logger } from '@kubun/logger';
|
|
5
|
-
import type { GraphStoreAPI } from '@kubun/store-graph';
|
|
6
|
-
import type { P2PStoreAPI } from '@kubun/store-p2p';
|
|
7
|
-
import type { ForwardingConfig } from '../sync/forwarder.js';
|
|
8
|
-
import type { StoreUnreadableMode } from '../types.js';
|
|
9
|
-
import { type AffectedRow, type GroupBroadcastMessage } from './broadcast.js';
|
|
10
|
-
import type { P2PEventEmitter } from './events.js';
|
|
11
|
-
import type { GroupHandleRegistry } from './group-handle-registry.js';
|
|
12
|
-
/**
|
|
13
|
-
* Thrown when a received broadcast fails to decrypt (MLS restore, decrypt,
|
|
14
|
-
* or deserialize). Callers should typically ack-and-skip these, since the
|
|
15
|
-
* message is unrecoverable and redelivery will not help.
|
|
16
|
-
*/
|
|
17
|
-
export declare class DecryptError extends Error {
|
|
18
|
-
readonly cause: unknown;
|
|
19
|
-
constructor(message: string, cause: unknown);
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Thrown when a successfully decrypted broadcast fails to apply to the local
|
|
23
|
-
* DB. Callers should typically NOT ack these, so the message can be
|
|
24
|
-
* redelivered after the transient failure clears.
|
|
25
|
-
*/
|
|
26
|
-
export declare class ApplyError extends Error {
|
|
27
|
-
readonly cause: unknown;
|
|
28
|
-
constructor(message: string, cause: unknown);
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Thrown when a received MLS Commit targets a future epoch the local handle
|
|
32
|
-
* has not reached yet (a gap in the in-order Commit stream). Callers should
|
|
33
|
-
* NOT ack — the hub redelivers in `sequence_id` order, so the gap-filling
|
|
34
|
-
* Commit arrives first and this one applies on a later delivery.
|
|
35
|
-
*/
|
|
36
|
-
export declare class CommitOutOfOrderError extends Error {
|
|
37
|
-
readonly messageEpoch: bigint;
|
|
38
|
-
readonly handleEpoch: bigint;
|
|
39
|
-
constructor(messageEpoch: bigint, handleEpoch: bigint);
|
|
40
|
-
}
|
|
41
|
-
export type BroadcastEvent = {
|
|
42
|
-
groupID: string;
|
|
43
|
-
message: GroupBroadcastMessage;
|
|
44
|
-
applied: boolean;
|
|
45
|
-
/**
|
|
46
|
-
* Row affected by the broadcast. After-state for create / update,
|
|
47
|
-
* before-state for delete / remove. Undefined when `applied === false`.
|
|
48
|
-
*/
|
|
49
|
-
affected?: AffectedRow;
|
|
50
|
-
/**
|
|
51
|
-
* Count of `mutation:apply` entries the local broadcast log already had.
|
|
52
|
-
* Only set on the `mutation:apply` branch; absent on control-plane
|
|
53
|
-
* variants.
|
|
54
|
-
*/
|
|
55
|
-
duplicates?: number;
|
|
56
|
-
};
|
|
57
|
-
export type BroadcastServiceEvents = {
|
|
58
|
-
broadcast: BroadcastEvent;
|
|
59
|
-
};
|
|
60
|
-
export type SendBroadcastParams = {
|
|
61
|
-
groupID: string;
|
|
62
|
-
message: GroupBroadcastMessage;
|
|
63
|
-
/**
|
|
64
|
-
* Wire-send hook invoked with the MLS-encrypted payload INSIDE the
|
|
65
|
-
* registry's per-group lock. A throw here propagates out of `prepareSend`
|
|
66
|
-
* and the registry skips persist — so a failed send leaves the device's
|
|
67
|
-
* MLS state at its pre-encrypt epoch, preventing the wedge where local
|
|
68
|
-
* state outpaces recipients.
|
|
69
|
-
*/
|
|
70
|
-
send: (encrypted: Uint8Array) => Promise<void>;
|
|
71
|
-
};
|
|
72
|
-
export type SendCommitParams = {
|
|
73
|
-
groupID: string;
|
|
74
|
-
/**
|
|
75
|
-
* Canonical MLS-encoded handshake Commit bytes (an encrypted
|
|
76
|
-
* `PrivateMessage`) produced by the committer's `GroupManager` call.
|
|
77
|
-
*/
|
|
78
|
-
commitBytes: Uint8Array;
|
|
79
|
-
/** Wire-send hook; receives the `mls`-framed bytes. */
|
|
80
|
-
send: (framed: Uint8Array) => Promise<void>;
|
|
81
|
-
};
|
|
82
|
-
export type ReceiveBroadcastParams = {
|
|
83
|
-
groupID: string;
|
|
84
|
-
encrypted: Uint8Array;
|
|
85
|
-
};
|
|
86
|
-
export type ReceiveAppResult = {
|
|
87
|
-
kind: 'app';
|
|
88
|
-
message: GroupBroadcastMessage;
|
|
89
|
-
applied: boolean;
|
|
90
|
-
affected?: AffectedRow;
|
|
91
|
-
duplicates?: number;
|
|
92
|
-
};
|
|
93
|
-
/**
|
|
94
|
-
* Result of processing a received MLS handshake Commit (`mls` wire frame).
|
|
95
|
-
* `removed`/`added` are the membership deltas derived by diffing the group
|
|
96
|
-
* roster before and after applying the Commit. `removed` DIDs are tombstoned
|
|
97
|
-
* via `markMemberRemoved` so the apply-time membership check can reject their
|
|
98
|
-
* post-removal writes.
|
|
99
|
-
*/
|
|
100
|
-
export type ReceiveCommitResult = {
|
|
101
|
-
kind: 'mls';
|
|
102
|
-
epoch: bigint;
|
|
103
|
-
removed: Array<string>;
|
|
104
|
-
added: Array<string>;
|
|
105
|
-
};
|
|
106
|
-
export type ReceiveBroadcastResult = ReceiveAppResult | ReceiveCommitResult;
|
|
107
|
-
/**
|
|
108
|
-
* BroadcastService handles MLS encrypt/decrypt for group broadcasts
|
|
109
|
-
* and applies received messages to the local DB.
|
|
110
|
-
*
|
|
111
|
-
* Encrypt and decrypt go through `GroupHandleRegistry.withHandle` so MLS
|
|
112
|
-
* state mutations serialize through the per-group mutex with write-through
|
|
113
|
-
* persist. The DB-apply step (`processBroadcast`) for receives runs OUTSIDE
|
|
114
|
-
* the lock so a slow apply does not block subsequent encrypt/decrypt on the
|
|
115
|
-
* same group.
|
|
116
|
-
*/
|
|
117
|
-
export type BroadcastServiceParams = {
|
|
118
|
-
p2pStore: P2PStoreAPI;
|
|
119
|
-
graphStore: GraphStoreAPI;
|
|
120
|
-
/**
|
|
121
|
-
* Engine graph internals — required so `mutation:apply` broadcasts route
|
|
122
|
-
* through {@link GraphInternals.applyVerifiedMutation} with `origin: 'peer'`.
|
|
123
|
-
*/
|
|
124
|
-
graph: GraphInternals;
|
|
125
|
-
/** Self DID for `mutation:apply` loopback detection. */
|
|
126
|
-
selfDID: string;
|
|
127
|
-
/** Single canonical access point for MLS `GroupHandle` instances. */
|
|
128
|
-
registry: GroupHandleRegistry;
|
|
129
|
-
/**
|
|
130
|
-
* Local HLC clock for stamping the `removed_at_hlc` of members dropped by a
|
|
131
|
-
* received MLS Commit (the receive side has no HLC otherwise). Defaults to a
|
|
132
|
-
* fresh clock keyed on `selfDID` when omitted.
|
|
133
|
-
*/
|
|
134
|
-
hlc?: HLC;
|
|
135
|
-
/**
|
|
136
|
-
* Receiver storage mode for incoming `mutation:apply` entries. Defaults
|
|
137
|
-
* to `'persist'`. When `'drop'`, `defaultAccessLevel` must be supplied
|
|
138
|
-
* so the gate factory can resolve server-side default access rules.
|
|
139
|
-
*/
|
|
140
|
-
storeUnreadable?: StoreUnreadableMode;
|
|
141
|
-
/** Server default access level — required when `storeUnreadable === 'drop'`. */
|
|
142
|
-
defaultAccessLevel?: DefaultAccessLevel;
|
|
143
|
-
/**
|
|
144
|
-
* Forwarding config. Threaded into `processBroadcast` per call.
|
|
145
|
-
* `false` / undefined → forwarding disabled (default).
|
|
146
|
-
*/
|
|
147
|
-
forwarding?: ForwardingConfig;
|
|
148
|
-
/**
|
|
149
|
-
* Hub forwarding hook — required when `forwarding` is set. Called once per
|
|
150
|
-
* surviving candidate group on the receive path.
|
|
151
|
-
*/
|
|
152
|
-
scheduleBroadcast?: (groupID: string, message: GroupBroadcastMessage) => void | Promise<void>;
|
|
153
|
-
/**
|
|
154
|
-
* Emitter forwarded to `processBroadcast` so `delegation:share` receives
|
|
155
|
-
* surface a `delegationTokenAdded` event to local subscribers.
|
|
156
|
-
*/
|
|
157
|
-
emitter?: P2PEventEmitter;
|
|
158
|
-
logger?: Logger;
|
|
159
|
-
};
|
|
160
|
-
export declare class BroadcastService extends EventEmitter<BroadcastServiceEvents> {
|
|
161
|
-
#private;
|
|
162
|
-
constructor(params: BroadcastServiceParams);
|
|
163
|
-
/**
|
|
164
|
-
* Encrypt + send a broadcast for a group, then persist the advanced MLS state.
|
|
165
|
-
*
|
|
166
|
-
* Failure semantics: send happens inside the registry's per-group lock. A
|
|
167
|
-
* throw from `send` propagates out and the registry skips persist — so the
|
|
168
|
-
* device's MLS state stays at its pre-encrypt epoch and the next attempt
|
|
169
|
-
* re-derives a fresh handle from the prior generation. Wedge protection
|
|
170
|
-
* intact.
|
|
171
|
-
*/
|
|
172
|
-
prepareSend(params: SendBroadcastParams): Promise<void>;
|
|
173
|
-
/**
|
|
174
|
-
* Send a pre-produced MLS handshake Commit to the group, tagged `mls` on the
|
|
175
|
-
* wire. The Commit is already a self-protected (encrypted `PrivateMessage`)
|
|
176
|
-
* framed message produced by the committer's `GroupManager` call — it is NOT
|
|
177
|
-
* re-encrypted through the application path, so this does not touch the
|
|
178
|
-
* handle. The committer's MLS state has already advanced + persisted; the
|
|
179
|
-
* hub's durable + ordered mailbox carries the Commit to existing members and
|
|
180
|
-
* replays it on reconnect.
|
|
181
|
-
*/
|
|
182
|
-
prepareSendCommit(params: SendCommitParams): Promise<void>;
|
|
183
|
-
/**
|
|
184
|
-
* Process a received broadcast: MLS decrypt INSIDE the registry lock,
|
|
185
|
-
* then apply to the DB OUTSIDE the lock so a slow apply doesn't block
|
|
186
|
-
* subsequent encrypt/decrypt on the same group.
|
|
187
|
-
*/
|
|
188
|
-
processReceived(params: ReceiveBroadcastParams): Promise<ReceiveBroadcastResult>;
|
|
189
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{EventEmitter as r}from"@enkaku/event";import{readMessageEpoch as e}from"@enkaku/group";import{HLC as t}from"@kubun/hlc";import{processBroadcast as a}from"./broadcast.js";import{deserializeBroadcast as o,serializeBroadcast as s}from"./broadcast-codec.js";import{mlsDecryptFramed as i,mlsEncryptFramed as c}from"./mls-codec.js";import{frameWire as p,unframeWire as m,WireKind as d}from"./wire-frame.js";export class DecryptError extends Error{cause;constructor(r,e){super(r),this.name="DecryptError",this.cause=e}}export class ApplyError extends Error{cause;constructor(r,e){super(r),this.name="ApplyError",this.cause=e}}export class CommitOutOfOrderError extends Error{messageEpoch;handleEpoch;constructor(r,e){super(`MLS commit epoch ${r} ahead of handle epoch ${e}`),this.name="CommitOutOfOrderError",this.messageEpoch=r,this.handleEpoch=e}}export class BroadcastService extends r{#r;#e;#t;constructor(r){super(),this.#t=r.hlc??new t({nodeID:r.selfDID}),this.#r={p2pStore:r.p2pStore,graphStore:r.graphStore,graph:r.graph,selfDID:r.selfDID,storeUnreadable:r.storeUnreadable,defaultAccessLevel:r.defaultAccessLevel,forwarding:r.forwarding,scheduleBroadcast:r.scheduleBroadcast,emitter:r.emitter,logger:r.logger},this.#e=r.registry}async prepareSend(r){let{groupID:e,message:t,send:a}=r,o=s(t);await this.#e.withHandle(e,async r=>{let{framed:e}=await c(r,o);await a(p(d.app,e))})}async prepareSendCommit(r){let{commitBytes:e,send:t}=r;await t(p(d.mls,e))}async processReceived(r){let e,t,s,c,{groupID:p,encrypted:l}=r;try{({kind:e,payload:t}=m(l))}catch(r){throw new DecryptError("Malformed group wire frame",r)}if(e===d.mls)return await this.#a(p,t);try{s=await this.#e.withHandle(p,async r=>{let e=await i(r,t);return o(e)})}catch(r){throw new DecryptError("Failed to decrypt broadcast",r)}try{c=await a(this.#r,s,p)}catch(r){throw new ApplyError("Failed to apply broadcast",r)}let{applied:h,affected:n,duplicates:u}=c;return await this.emit("broadcast",{groupID:p,message:s,applied:h,affected:n,duplicates:u}),{kind:"app",message:s,applied:h,affected:n,duplicates:u}}async #a(r,a){return await this.#e.withHandle(r,async o=>{let s=e(a),i=o.epoch;if(null!=s&&s<i)return{kind:"mls",epoch:i,removed:[],added:[]};if(null!=s&&s>i)throw new CommitOutOfOrderError(s,i);let c=o.listMembers();try{await o.processMessage(a)}catch(r){throw new DecryptError("Failed to process MLS commit",r)}let p=o.listMembers(),m=new Set(c.map(r=>r.id)),d=new Set(p.map(r=>r.id)),l=c.filter(r=>!d.has(r.id)).map(r=>r.id),h=p.filter(r=>!m.has(r.id)).map(r=>r.id),n=t.serialize(this.#t.now()),{p2pStore:u}=this.#r;for(let e of l)await u.markMemberRemoved(r,e,n);for(let e of h)await u.addGroupMember({group_id:r,member_did:e,role:"member",hlc:n});return{kind:"mls",epoch:o.epoch,removed:l,added:h}})}}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Outer wire-frame discriminator for group hub payloads.
|
|
3
|
-
*
|
|
4
|
-
* The hub relay treats a group send payload as opaque bytes. Kubun prepends a
|
|
5
|
-
* single `kind` byte so the receiver can route a payload to the right decoder
|
|
6
|
-
* without trial-decryption:
|
|
7
|
-
*
|
|
8
|
-
* - `app` — an MLS-encrypted application broadcast (`GroupBroadcastMessage`),
|
|
9
|
-
* decoded via `handle.decrypt` + `deserializeBroadcast`.
|
|
10
|
-
* - `mls` — a framed MLS handshake message (a Commit), fed straight to
|
|
11
|
-
* `handle.processMessage`. The handshake is self-protected (encrypted
|
|
12
|
-
* `PrivateMessage`), so it is NOT wrapped in the application-encryption path.
|
|
13
|
-
*
|
|
14
|
-
* The discriminator lives entirely in kubun's payload bytes; the hub protocol
|
|
15
|
-
* is unchanged.
|
|
16
|
-
*/
|
|
17
|
-
export declare const WireKind: {
|
|
18
|
-
readonly app: 0;
|
|
19
|
-
readonly mls: 1;
|
|
20
|
-
};
|
|
21
|
-
export type WireKind = (typeof WireKind)[keyof typeof WireKind];
|
|
22
|
-
/** Prepend the `kind` byte to a payload. */
|
|
23
|
-
export declare function frameWire(kind: WireKind, payload: Uint8Array): Uint8Array;
|
|
24
|
-
/**
|
|
25
|
-
* Split a framed payload into its `kind` and the remaining payload bytes.
|
|
26
|
-
* Throws on an empty frame or an unrecognized `kind` byte. The returned
|
|
27
|
-
* payload is a view (`subarray`) over the input, not a copy.
|
|
28
|
-
*/
|
|
29
|
-
export declare function unframeWire(framed: Uint8Array): {
|
|
30
|
-
kind: WireKind;
|
|
31
|
-
payload: Uint8Array;
|
|
32
|
-
};
|
package/lib/groups/wire-frame.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const WireKind={app:0,mls:1};export function frameWire(r,e){let n=new Uint8Array(e.length+1);return n[0]=r,n.set(e,1),n}export function unframeWire(r){if(0===r.length)throw Error("empty wire frame");let e=r[0];if(e!==WireKind.app&&e!==WireKind.mls)throw Error(`unknown wire frame kind: ${e}`);return{kind:e,payload:r.subarray(1)}}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { Client } from '@enkaku/client';
|
|
2
|
-
import type { HubProtocol } from '@enkaku/hub-protocol';
|
|
3
|
-
import type { Logger } from '@kubun/logger';
|
|
4
|
-
import type { CreateHubClient } from './relay-manager.js';
|
|
5
|
-
export type HubClientRef = {
|
|
6
|
-
client: Client<HubProtocol>;
|
|
7
|
-
refID: string;
|
|
8
|
-
};
|
|
9
|
-
/**
|
|
10
|
-
* Fired once per freshly-spawned hub client when its first signed response
|
|
11
|
-
* arrives. The pool binds the hub URL into the call site; consumers (hub
|
|
12
|
-
* manager `captureServerDID`) use the URL to look up the local hub row and
|
|
13
|
-
* apply the conflict matrix. Errors are caught and logged — capture never
|
|
14
|
-
* blocks message forwarding, even on `HubServerDIDConflictError`.
|
|
15
|
-
*/
|
|
16
|
-
export type OnServerDIDObserved = (params: {
|
|
17
|
-
hubURL: string;
|
|
18
|
-
serverDID: string;
|
|
19
|
-
}) => void | Promise<void>;
|
|
20
|
-
/**
|
|
21
|
-
* Resolves the locally-pinned `server_did` for a hub URL at spawn time. When
|
|
22
|
-
* the hub row for this URL exists and has `server_did` set, the pool passes
|
|
23
|
-
* it as `expectedServerDID` to the client factory so the wrapper enforces
|
|
24
|
-
* the pin. Returning `null` falls back to TOFU.
|
|
25
|
-
*
|
|
26
|
-
* Resolution happens fresh on every spawn so `updateHub({ serverDID: null })`
|
|
27
|
-
* re-arms TOFU automatically: the next disposed/evicted client triggers a
|
|
28
|
-
* fresh resolve.
|
|
29
|
-
*/
|
|
30
|
-
export type ResolvePinnedServerDID = (hubURL: string) => Promise<string | null>;
|
|
31
|
-
export type HubConnectionPoolParams = {
|
|
32
|
-
createHubClient: CreateHubClient;
|
|
33
|
-
logger?: Logger;
|
|
34
|
-
onServerDIDObserved?: OnServerDIDObserved;
|
|
35
|
-
resolvePinnedDID?: ResolvePinnedServerDID;
|
|
36
|
-
};
|
|
37
|
-
export declare class HubConnectionPool {
|
|
38
|
-
#private;
|
|
39
|
-
constructor(params: HubConnectionPoolParams);
|
|
40
|
-
acquire(hubURL: string): Promise<HubClientRef>;
|
|
41
|
-
release(hubURL: string, refID: string): Promise<void>;
|
|
42
|
-
disposeAll(): Promise<void>;
|
|
43
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export class HubConnectionPool{#e;#t;#i;#r;#s=new Map;#n=0;constructor(e){this.#e=e.createHubClient,this.#t=e.logger,this.#i=e.onServerDIDObserved,this.#r=e.resolvePinnedDID}async acquire(e){let t=this.#s.get(e);if(null!=t&&null!=t.disposing&&(await t.disposing,this.#s.get(e)===t&&this.#s.delete(e),t=void 0),null==t){let i,r=this.#i;if(null!=this.#r)try{let t=await this.#r(e);null!=t&&(i=t)}catch(t){this.#t?.warn("failed to resolve pinned server DID, falling back to TOFU",{hubURL:e,error:t})}let s=await this.#e(e,{onServerDID:null==r?void 0:async t=>{try{await r({hubURL:e,serverDID:t})}catch(i){this.#t?.warn("hub server DID capture failed",{hubURL:e,serverDID:t,error:i})}},expectedServerDID:i,onMismatch:null==i?void 0:e=>{this.#t?.warn("hub server DID mismatch — pinning enforcement triggered",{hubURL:e.hubURL,expectedServerDID:e.expectedServerDID,observedServerDID:e.observedServerDID})}}),n={client:s,refs:new Set};t=n,this.#s.set(e,n),s.disposed.then(()=>{this.#s.get(e)===n&&(this.#t?.debug("hub client disposed externally, evicting pool entry",{hubURL:e,outstandingRefs:[...n.refs]}),this.#s.delete(e))}).catch(()=>{})}let i=String(++this.#n);return t.refs.add(i),{client:t.client,refID:i}}async release(e,t){let i=this.#s.get(e);if(null==i)return void this.#t?.debug("HubConnectionPool.release: unknown hubURL",{hubURL:e,refID:t});if(!i.refs.has(t))return void this.#t?.debug("HubConnectionPool.release: unknown refID",{hubURL:e,refID:t});if(i.refs.delete(t),i.refs.size>0)return;let r=i.client.dispose();i.disposing=r;try{await r}finally{this.#s.get(e)===i&&this.#s.delete(e)}}async disposeAll(){let e=Array.from(this.#s.entries());await Promise.all(e.map(async([e,t])=>{t.refs.size>0&&this.#t?.warn("Disposing hub client with outstanding refs",{hubURL:e,refs:Array.from(t.refs)}),await t.client.dispose()})),this.#s.clear()}}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { Logger } from '@kubun/logger';
|
|
2
|
-
import type { BroadcastEvent } from '../groups/broadcast-service.js';
|
|
3
|
-
import type { P2PEventEmitter } from '../groups/events.js';
|
|
4
|
-
/**
|
|
5
|
-
* Map a `BroadcastEvent` from `BroadcastService` into the matching
|
|
6
|
-
* `P2PEventEmitter` event.
|
|
7
|
-
*
|
|
8
|
-
* - Events with `applied === false` are ignored (no state change).
|
|
9
|
-
* - `catalog:*` messages are currently a no-op. Catalog send-side broadcasts
|
|
10
|
-
* and event propagation are deferred to a future task (see design spec).
|
|
11
|
-
* - Payloads are built from `event.affected.row`, which `processBroadcast`
|
|
12
|
-
* captures at the appropriate moment (after-state for create / update,
|
|
13
|
-
* before-state for delete / remove).
|
|
14
|
-
*/
|
|
15
|
-
export declare function forwardRemoteBroadcast(emitter: P2PEventEmitter, event: BroadcastEvent, logger?: Logger): Promise<void>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{toISO as e}from"../context/types.js";export async function forwardRemoteBroadcast(r,t,a){if(!t.applied)return;let{message:i,affected:c}=t;if("delegation:share"!==i.type){if(null==c)return void a?.warn("remote broadcast applied but affected row missing",{type:i.type});switch(i.type){case"circle:create":{if("circle"!==c.kind)return;let{row:t}=c;await r.emit("circleCreated",{id:t.id,groupID:t.group_id,name:t.name,description:t.description,createdAt:e(t.created_at)});return}case"circle:update":{if("circle"!==c.kind)return;let{row:t}=c,a=null!=i.update.catalogIDs?"circleCatalogsChanged":"circleDataChanged";await r.emit(a,{id:t.id,groupID:t.group_id,name:t.name,description:t.description,createdAt:e(t.created_at),circleID:t.id});return}case"circle:delete":{if("circle"!==c.kind)return;let{row:t}=c;await r.emit("circleDeleted",{id:t.id,groupID:t.group_id,name:t.name,description:t.description,createdAt:e(t.created_at)});return}case"member:add":{if("member"!==c.kind)return;let{row:t}=c;await r.emit("circleMemberAdded",{circleID:t.circle_id,memberDID:t.member_did,role:t.role,createdAt:e(t.created_at)});return}case"member:remove":{if("member"!==c.kind)return;let{row:t}=c;await r.emit("circleMemberRemoved",{circleID:t.circle_id,memberDID:t.member_did,role:t.role,createdAt:e(t.created_at)});return}case"group:update":{if("group"!==c.kind)return;let{row:t}=c;await r.emit("groupDataChanged",{id:t.id,name:t.name,description:t.description,createdBy:t.created_by,createdAt:e(t.created_at),groupID:t.id});return}case"catalog:create":case"catalog:update":case"catalog:delete":return void a?.debug("catalog broadcast ignored (not yet wired)",{type:i.type})}}}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { Disposer } from '@enkaku/async';
|
|
2
|
-
import type { Logger } from '@kubun/logger';
|
|
3
|
-
import type { P2PStoreAPI } from '@kubun/store-p2p';
|
|
4
|
-
import type { GroupBroadcastMessage } from '../groups/broadcast.js';
|
|
5
|
-
import type { BroadcastService } from '../groups/broadcast-service.js';
|
|
6
|
-
import type { HubConnection } from './hub-connection.js';
|
|
7
|
-
export type AckBatchParams = {
|
|
8
|
-
flushMs: number;
|
|
9
|
-
flushMax: number;
|
|
10
|
-
send: (ids: Array<string>) => Promise<void>;
|
|
11
|
-
logger?: Logger;
|
|
12
|
-
};
|
|
13
|
-
export declare class AckBatch {
|
|
14
|
-
#private;
|
|
15
|
-
constructor(params: AckBatchParams);
|
|
16
|
-
add(id: string): void;
|
|
17
|
-
/**
|
|
18
|
-
* Drain the pending id set and send it via the configured sender. Never
|
|
19
|
-
* rejects: send errors are logged and the ids are dropped silently (the hub
|
|
20
|
-
* will redeliver unacknowledged messages, so the next receive will produce a
|
|
21
|
-
* fresh ack).
|
|
22
|
-
*/
|
|
23
|
-
flush(): Promise<void>;
|
|
24
|
-
dispose(): void;
|
|
25
|
-
}
|
|
26
|
-
export declare function computeBackoff(attempt: number, base: number, max: number, jitter: number): number;
|
|
27
|
-
export declare function abortableSleep(ms: number, signal: AbortSignal): Promise<void>;
|
|
28
|
-
export type GroupChannelParams = {
|
|
29
|
-
groupID: string;
|
|
30
|
-
deviceID: string;
|
|
31
|
-
hubConnection: HubConnection;
|
|
32
|
-
broadcastService: BroadcastService;
|
|
33
|
-
p2pStore: P2PStoreAPI;
|
|
34
|
-
logger?: Logger;
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* Per-group handle: thin wrapper around a shared `HubConnection`.
|
|
38
|
-
*
|
|
39
|
-
* `open()` subscribes to the connection (issuing `hub/group/join` once),
|
|
40
|
-
* `close()` unsubscribes (drains the per-group mutex then issues
|
|
41
|
-
* `hub/group/leave`), and `broadcast()` runs the MLS encrypt+send pipeline
|
|
42
|
-
* inside `HubConnection.runInGroup()` so the per-group mutex serializes send
|
|
43
|
-
* and receive on the same MLS state.
|
|
44
|
-
*
|
|
45
|
-
* All transport state (refID, ack batching, receive loop, reconnect/backoff)
|
|
46
|
-
* lives on `HubConnection` and is shared across every channel for that
|
|
47
|
-
* `hubURL`. A peer with N groups uses ONE `hub/receive` stream per
|
|
48
|
-
* `(deviceDID, hubURL)` rather than N — this preserves the hub-server's
|
|
49
|
-
* single-writer-per-DID invariant for bridge peers.
|
|
50
|
-
*/
|
|
51
|
-
export declare class GroupChannel extends Disposer {
|
|
52
|
-
#private;
|
|
53
|
-
constructor(params: GroupChannelParams);
|
|
54
|
-
open(): Promise<void>;
|
|
55
|
-
close(): Promise<void>;
|
|
56
|
-
broadcast(message: GroupBroadcastMessage, opts?: {
|
|
57
|
-
timeoutMs?: number;
|
|
58
|
-
}): Promise<void>;
|
|
59
|
-
/**
|
|
60
|
-
* Fan out a pre-produced MLS handshake Commit to the group's existing
|
|
61
|
-
* members. Mirrors {@link broadcast} but routes through `handleSendCommit`
|
|
62
|
-
* (no application encryption — the Commit is already self-protected).
|
|
63
|
-
*/
|
|
64
|
-
sendCommit(commitBytes: Uint8Array, opts?: {
|
|
65
|
-
timeoutMs?: number;
|
|
66
|
-
}): Promise<void>;
|
|
67
|
-
}
|
package/lib/hub/group-channel.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{Disposer as e}from"@enkaku/async";import{handleReceivedMessage as t}from"./receive-handler.js";import{handleSendBroadcast as r,handleSendCommit as i}from"./send-handler.js";export class AckBatch{#e;#t;#r;#i;#s=new Set;#o=null;constructor(e){this.#e=e.flushMs,this.#t=e.flushMax,this.#r=e.send,this.#i=e.logger}add(e){let t=0===this.#s.size;if(this.#s.add(e),this.#s.size>=this.#t){null!==this.#o&&(clearTimeout(this.#o),this.#o=null),this.flush();return}t&&null===this.#o&&(this.#o=setTimeout(()=>{this.#o=null,this.flush()},this.#e))}async flush(){if(null!==this.#o&&(clearTimeout(this.#o),this.#o=null),0===this.#s.size)return;let e=Array.from(this.#s);this.#s.clear();try{await this.#r(e)}catch(t){this.#i?.error("ack batch send failed",{error:t,count:e.length})}}dispose(){null!==this.#o&&(clearTimeout(this.#o),this.#o=null),this.#s.clear()}}export function computeBackoff(e,t,r,i){let s=Math.min(t*2**e*(1+i*(2*Math.random()-1)),r);return s<0?0:s}export function abortableSleep(e,t){return new Promise((r,i)=>{if(t.aborted)return void i(t.reason??Error("aborted"));let s=setTimeout(()=>{t.removeEventListener("abort",o),r()},e),o=()=>{clearTimeout(s),t.removeEventListener("abort",o),i(t.reason??Error("aborted"))};t.addEventListener("abort",o)})}export class GroupChannel extends e{#n;#h;#a;#u;#c;#i;#d=!1;constructor(e){super({dispose:async()=>{await this.#l()}}),this.#n=e.groupID,this.#h=e.deviceID,this.#a=e.hubConnection,this.#u=e.broadcastService,this.#c=e.p2pStore,this.#i=e.logger}async open(){if(this.#d)throw Error("GroupChannel already opened");let e=await this.#c.getMLSState(this.#n,this.#h);if(null==e)throw Error(`MLS state missing for group ${this.#n}; cannot join hub group`);await this.#a.subscribe(this.#n,e.credential,e=>this.#p(e)),this.#d=!0}#p(e){return t({groupID:this.#n,message:e,broadcastService:this.#u,logger:this.#i})}async close(){this.#d&&await this.dispose()}async #l(){if(this.#d){try{await this.#a.unsubscribe(this.#n)}catch(e){this.#i?.warn("hub-connection unsubscribe failed during close",{groupID:this.#n,error:e})}this.#d=!1}}broadcast(e,t){return this.#d?this.#a.runInGroup(this.#n,async()=>{if(!this.#d)throw Error(`GroupChannel for ${this.#n} is not ready for broadcast`);await r({groupID:this.#n,message:e,broadcastService:this.#u,send:(e,r)=>this.#a.broadcast(e,r,{timeoutMs:t?.timeoutMs}),logger:this.#i})},{timeoutMs:t?.timeoutMs}):Promise.reject(Error(`GroupChannel for ${this.#n} is not ready for broadcast`))}sendCommit(e,t){return this.#d?this.#a.runInGroup(this.#n,async()=>{if(!this.#d)throw Error(`GroupChannel for ${this.#n} is not ready for sendCommit`);await i({groupID:this.#n,commitBytes:e,broadcastService:this.#u,send:(e,r)=>this.#a.broadcast(e,r,{timeoutMs:t?.timeoutMs}),logger:this.#i})},{timeoutMs:t?.timeoutMs}):Promise.reject(Error(`GroupChannel for ${this.#n} is not ready for sendCommit`))}}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { Disposer } from '@enkaku/async';
|
|
2
|
-
import { EventEmitter } from '@enkaku/event';
|
|
3
|
-
import type { StoredMessage } from '@enkaku/hub-protocol';
|
|
4
|
-
import type { Logger } from '@kubun/logger';
|
|
5
|
-
import type { HubConnectionPool } from './connection-pool.js';
|
|
6
|
-
import type { ReceivedHubMessage } from './receive-handler.js';
|
|
7
|
-
export type TunnelMessageStream = AsyncIterable<StoredMessage> & {
|
|
8
|
-
return(): void;
|
|
9
|
-
};
|
|
10
|
-
export type TunnelMessageStreamParams = {
|
|
11
|
-
peerDID: string;
|
|
12
|
-
};
|
|
13
|
-
export type SubscriptionHandler = (message: ReceivedHubMessage) => Promise<{
|
|
14
|
-
ack: boolean;
|
|
15
|
-
}>;
|
|
16
|
-
export type ConnectionLifecycleEvent = {
|
|
17
|
-
type: 'connected';
|
|
18
|
-
} | {
|
|
19
|
-
type: 'disconnected';
|
|
20
|
-
reason?: string;
|
|
21
|
-
} | {
|
|
22
|
-
type: 'error';
|
|
23
|
-
error: unknown;
|
|
24
|
-
} | {
|
|
25
|
-
type: 'reconnecting';
|
|
26
|
-
attempt: number;
|
|
27
|
-
delayMs: number;
|
|
28
|
-
};
|
|
29
|
-
type HubConnectionEvents = {
|
|
30
|
-
lifecycle: ConnectionLifecycleEvent;
|
|
31
|
-
};
|
|
32
|
-
export type HubConnectionParams = {
|
|
33
|
-
hubURL: string;
|
|
34
|
-
deviceID: string;
|
|
35
|
-
pool: HubConnectionPool;
|
|
36
|
-
logger?: Logger;
|
|
37
|
-
ackFlushMs?: number;
|
|
38
|
-
ackFlushMax?: number;
|
|
39
|
-
backoffBaseMs?: number;
|
|
40
|
-
backoffMaxMs?: number;
|
|
41
|
-
backoffJitter?: number;
|
|
42
|
-
};
|
|
43
|
-
export declare class HubConnection extends Disposer {
|
|
44
|
-
#private;
|
|
45
|
-
constructor(params: HubConnectionParams);
|
|
46
|
-
get events(): EventEmitter<HubConnectionEvents>;
|
|
47
|
-
open(): Promise<void>;
|
|
48
|
-
subscribe(groupID: string, credential: string, handler: SubscriptionHandler): Promise<void>;
|
|
49
|
-
unsubscribe(groupID: string): Promise<void>;
|
|
50
|
-
broadcast(groupID: string, encryptedPayload: string, opts?: {
|
|
51
|
-
timeoutMs?: number;
|
|
52
|
-
}): Promise<void>;
|
|
53
|
-
/**
|
|
54
|
-
* Send an opaque tunnel-style message to explicit recipients via
|
|
55
|
-
* `hub/send`. No `groupID` is attached at the hub level — recipients
|
|
56
|
-
* receive the message on the tunnel-routing path (see
|
|
57
|
-
* {@link tunnelMessageStream}). Used by hub-mediated document sync.
|
|
58
|
-
*/
|
|
59
|
-
send(params: {
|
|
60
|
-
recipients: Array<string>;
|
|
61
|
-
payload: Uint8Array;
|
|
62
|
-
timeoutMs?: number;
|
|
63
|
-
}): Promise<{
|
|
64
|
-
sequenceID: string;
|
|
65
|
-
}>;
|
|
66
|
-
/**
|
|
67
|
-
* Run `fn` inside the same per-group mutex used by receive `#dispatch`. This
|
|
68
|
-
* lets callers (e.g. `GroupChannel.broadcast`) serialize MLS-state-mutating
|
|
69
|
-
* pipelines with incoming-message handling on the same group, avoiding races
|
|
70
|
-
* between concurrent send-encrypt and receive-decrypt against shared MLS
|
|
71
|
-
* group state.
|
|
72
|
-
*
|
|
73
|
-
* The gate-await ensures the subscription map is settled before lookup —
|
|
74
|
-
* i.e. mid-reconnect callers wait for the subscription to be (re)registered
|
|
75
|
-
* rather than throwing.
|
|
76
|
-
*/
|
|
77
|
-
runInGroup<T>(groupID: string, fn: () => Promise<T>, opts?: {
|
|
78
|
-
timeoutMs?: number;
|
|
79
|
-
}): Promise<T>;
|
|
80
|
-
/**
|
|
81
|
-
* Register a per-peer tunnel inbox and return a disposable async iterable
|
|
82
|
-
* of {@link StoredMessage} values received from that peer over the tunnel
|
|
83
|
-
* (i.e. direct hub messages with no `groupID`).
|
|
84
|
-
*
|
|
85
|
-
* Used by the hub-mediated document sync server adapter (see Q3 of the
|
|
86
|
-
* hub-mediated document sync plan). The hub's receive-side dispatcher
|
|
87
|
-
* keys tunnel routing on `senderDID` because hub-level messages cannot
|
|
88
|
-
* reveal the in-frame `sessionID` (the body is encrypted). P2P pairs
|
|
89
|
-
* `peerDID` and `sessionID` at session creation, so peer-keying is
|
|
90
|
-
* sufficient. Concurrent tunnel sessions to the same peer are not
|
|
91
|
-
* supported and are the caller's responsibility to avoid.
|
|
92
|
-
*/
|
|
93
|
-
tunnelMessageStream({ peerDID }: TunnelMessageStreamParams): TunnelMessageStream;
|
|
94
|
-
close(): Promise<void>;
|
|
95
|
-
}
|
|
96
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{Disposer as e}from"@enkaku/async";import{fromB64 as t,toB64 as s}from"@enkaku/codec";import{EventEmitter as n}from"@enkaku/event";import{createMutex as i}from"../util/mutex.js";import{ReconnectingError as r}from"./errors.js";import{AckBatch as o,abortableSleep as h,computeBackoff as a}from"./group-channel.js";import{TunnelInbox as c}from"./tunnel-inbox.js";import{waitForGate as l}from"./wait-for-gate.js";let u=Symbol("disposed");export class HubConnection extends e{#e;#t;#s;#n;#i;#r;#o;#h;#a;#c=null;#l=null;#u=null;#b=null;#p=null;#d=new Map;#f=!1;#g=0;#m=null;#y=new Map;#w;constructor(e){super({dispose:async()=>{await this.#L()}}),this.#e=e.hubURL,this.#t=e.deviceID,this.#s=e.pool,this.#n=e.logger,this.#i=e.ackFlushMs??500,this.#r=e.ackFlushMax??10,this.#o=e.backoffBaseMs??1e3,this.#h=e.backoffMaxMs??6e4,this.#a=e.backoffJitter??.25,this.#w=new n}get events(){return this.#w}async #I(e){try{await this.#w.emit("lifecycle",e)}catch(t){this.#n?.warn("hub-connection lifecycle subscriber threw",{hubURL:this.#e,type:e.type,error:t})}}async open(){if(this.#f)throw Error("HubConnection already opened");try{await this.#k(),this.#f=!0,await this.#I({type:"connected"})}catch(e){if(null!=this.#l){try{await this.#s.release(this.#e,this.#l)}catch(e){this.#n?.debug("hub-connection pool release after open failure failed",{hubURL:this.#e,error:e})}this.#l=null}throw this.#c=null,e}}async #k(){if(null!=this.#l){let e=this.#l;this.#l=null,this.#c=null;try{await this.#s.release(this.#e,e)}catch(e){this.#n?.debug("hub-connection pool release before reconnect failed",{hubURL:this.#e,error:e})}}let{client:e,refID:t}=await this.#s.acquire(this.#e);this.#c=e,this.#l=t;let s=e.createChannel("hub/receive",{param:{},signal:this.signal});this.#u=s,s.catch(()=>{}),this.#b=new o({flushMs:this.#i,flushMax:this.#r,send:async e=>{await s.send({ack:e})},logger:this.#n}),this.#p=this.#R(e).catch(e=>{this.#n?.debug("hub-connection receive task tail error",{hubURL:this.#e,error:e})});let n=[...this.#d.entries()];if(n.length>0){let t=await Promise.allSettled(n.map(async([t,s])=>{await e.request("hub/group/join",{param:{groupID:t,credential:s.credential}})}));for(let e=0;e<t.length;e++){let s=t[e];if("rejected"===s.status){let t=n[e][0];this.#n?.warn("hub-connection rejoin failed",{hubURL:this.#e,groupID:t,error:s.reason})}}}}async subscribe(e,t,s){if(await l(this.#m,void 0),!this.#f||null==this.#c)throw Error("HubConnection not open");if(this.#d.has(e))throw Error(`already subscribed to group ${e}`);await this.#c.request("hub/group/join",{param:{groupID:e,credential:t}}),this.#d.set(e,{credential:t,handler:s,mutex:i()})}async unsubscribe(e){await l(this.#m,void 0);let t=this.#c;if(!this.#f||null==t)throw Error("HubConnection not open");let s=this.#d.get(e);if(null==s)throw Error(`not subscribed to group ${e}`);await s.mutex.run(async()=>{}),await t.request("hub/group/leave",{param:{groupID:e}}),this.#d.delete(e)}async broadcast(e,t,s){if(await l(this.#m,s?.timeoutMs,()=>new r(this.#e,this.#g)),!this.#f||null==this.#c)throw Error("HubConnection not open");await this.#c.request("hub/group/send",{param:{groupID:e,payload:t}})}async send(e){if(await l(this.#m,e.timeoutMs,()=>new r(this.#e,this.#g)),!this.#f||null==this.#c)throw Error("HubConnection not open");return{sequenceID:(await this.#c.request("hub/send",{param:{recipients:e.recipients,payload:s(e.payload)}})).sequenceID}}async runInGroup(e,t,s){if(await l(this.#m,s?.timeoutMs,()=>new r(this.#e,this.#g)),!this.#f)throw Error("HubConnection not open");let n=this.#d.get(e);if(null==n)throw Error(`not subscribed to group ${e}`);return n.mutex.run(t)}tunnelMessageStream({peerDID:e}){if(this.#y.has(e))throw Error("tunnelMessageStream: peerDID already registered");let t=new c,s={inbox:t};this.#y.set(e,s);let n=!1;return{[Symbol.asyncIterator]:()=>t.iterator(),return:()=>{n||(n=!0,this.#y.get(e)===s&&this.#y.delete(e),t.close())}}}async #R(e){let t=this.#u;if(null==t)return;let s=t.readable.getReader(),n=null;try{for(;!this.signal.aborted;){let t=s.read(),n=e.disposed.then(()=>u),i=await Promise.race([t,n]);if("symbol"==typeof i){this.#n?.debug("hub-connection receive loop observed hub client disposal",{hubURL:this.#e});break}let{done:r,value:o}=i;if(r)break;this.#D(o),this.#g=0}}catch(e){n=e,this.#n?.warn("hub-connection receive loop error",{hubURL:this.#e,error:e})}finally{try{s.releaseLock()}catch{}}if(!this.signal.aborted){let e=n instanceof Error&&n.message?n.message:"receive loop ended";await this.#I({type:"disconnected",reason:e}),this.#U()}}#U(){this.#p=this.#M().catch(e=>{this.#n?.debug("hub-connection reconnect task tail error",{hubURL:this.#e,error:e})})}async #M(){let e=()=>{};this.#m=new Promise(t=>{e=t});try{for(;!this.signal.aborted;){let e=a(this.#g++,this.#o,this.#h,this.#a);this.#n?.debug("hub-connection reconnect scheduled",{hubURL:this.#e,delay:e,attempt:this.#g});try{await h(e,this.signal)}catch{return}await this.#I({type:"reconnecting",attempt:this.#g,delayMs:e});try{this.#b?.dispose(),this.#b=null,this.#u=null,await this.#k(),this.#g=0,await this.#I({type:"connected"});return}catch(e){this.#n?.warn("hub-connection reconnect attempt failed",{hubURL:this.#e,attempt:this.#g,error:e}),await this.#I({type:"error",error:e})}}}finally{this.#m=null,e()}}#D(e){if(null==e.groupID){let s=this.#y.get(e.senderDID);if(null!=s){let n={sequenceID:e.sequenceID,senderDID:e.senderDID,payload:t(e.payload)};s.inbox.push(n),this.#b?.add(e.sequenceID);return}this.#n?.debug("direct message received, no handler",{hubURL:this.#e,sequenceID:e.sequenceID}),this.#b?.add(e.sequenceID);return}let s=this.#d.get(e.groupID);null==s?this.#n?.debug("message for unsubscribed group",{hubURL:this.#e,groupID:e.groupID,sequenceID:e.sequenceID}):s.mutex.run(()=>s.handler(e)).then(t=>{t.ack&&this.#b?.add(e.sequenceID)}).catch(t=>{this.#n?.warn("hub-connection subscription handler error",{hubURL:this.#e,groupID:e.groupID,sequenceID:e.sequenceID,error:t})})}async close(){this.#f&&await this.dispose()}async #L(){if(!this.#f)return;let e=this.#b;if(this.#b=null,null!=e){try{await e.flush()}catch(e){this.#n?.debug("hub-connection ack flush during close failed",{hubURL:this.#e,error:e})}e.dispose()}try{this.#u?.close()}catch{}try{await this.#p}catch{}let t=[...this.#d.values()].map(e=>e.mutex.run(async()=>{}).catch(()=>{}));if(await Promise.allSettled(t),null!=this.#l){try{await this.#s.release(this.#e,this.#l)}catch(e){this.#n?.debug("hub-connection pool release during close failed",{hubURL:this.#e,error:e})}this.#l=null}for(let e of(this.#c=null,this.#u=null,this.#p=null,this.#d.clear(),this.#y.values()))e.inbox.close();this.#y.clear(),this.#f=!1}}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { Logger } from '@kubun/logger';
|
|
2
|
-
import { type BroadcastService } from '../groups/broadcast-service.js';
|
|
3
|
-
/**
|
|
4
|
-
* Shape of a message pushed by `hub/receive`. Matches `@enkaku/hub-protocol`
|
|
5
|
-
* server-to-client push payload.
|
|
6
|
-
*/
|
|
7
|
-
export type ReceivedHubMessage = {
|
|
8
|
-
sequenceID: string;
|
|
9
|
-
senderDID: string;
|
|
10
|
-
groupID?: string;
|
|
11
|
-
/** base64-encoded encrypted MLS application message */
|
|
12
|
-
payload: string;
|
|
13
|
-
};
|
|
14
|
-
export type HandleReceivedMessageParams = {
|
|
15
|
-
groupID: string;
|
|
16
|
-
message: ReceivedHubMessage;
|
|
17
|
-
broadcastService: BroadcastService;
|
|
18
|
-
logger?: Logger;
|
|
19
|
-
};
|
|
20
|
-
export type HandleReceivedMessageResult = {
|
|
21
|
-
ack: boolean;
|
|
22
|
-
};
|
|
23
|
-
/**
|
|
24
|
-
* Apply a single hub-received message for a group.
|
|
25
|
-
*
|
|
26
|
-
* Returns `{ ack: true }` when the message has been processed to a state where
|
|
27
|
-
* redelivery serves no purpose (applied successfully, or decrypt failed with
|
|
28
|
-
* no recovery path). Returns `{ ack: false }` when the hub should redeliver
|
|
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.
|
|
32
|
-
*/
|
|
33
|
-
export declare function handleReceivedMessage(params: HandleReceivedMessageParams): Promise<HandleReceivedMessageResult>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{fromB64 as e}from"@enkaku/codec";import{ApplyError as r,CommitOutOfOrderError as c,DecryptError as s}from"../groups/broadcast-service.js";export async function handleReceivedMessage(a){let{groupID:n,message:o,broadcastService:i,logger:t}=a;if(o.groupID!==n)return t?.debug("hub message groupID mismatch, skipping",{groupID:n,messageGroupID:o.groupID,sequenceID:o.sequenceID}),{ack:!1};let u=e(o.payload);try{await i.processReceived({groupID:n,encrypted:u})}catch(e){if(e instanceof s)return t?.debug("decrypt failed, acking and skipping",{groupID:n,sequenceID:o.sequenceID,error:e}),{ack:!0};if(e instanceof c)return t?.debug("mls commit out of order, will redeliver after gap fills",{groupID:n,sequenceID:o.sequenceID,messageEpoch:e.messageEpoch.toString(),handleEpoch:e.handleEpoch.toString()}),{ack:!1};if(e instanceof r)return t?.error("broadcast apply failed, will redeliver",{groupID:n,sequenceID:o.sequenceID,error:e}),{ack:!1};return t?.error("unexpected error processing hub message",{groupID:n,sequenceID:o.sequenceID,error:e}),{ack:!1}}return{ack:!0}}
|