@kubun/plugin-p2p 0.11.0 → 0.12.1
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 +1073 -746
- package/lib/context/hub.js +2 -1
- package/lib/context/join.js +50 -15
- package/lib/context/peer.d.ts +92 -0
- package/lib/context/peer.js +1216 -0
- package/lib/context/require-admin.d.ts +57 -0
- package/lib/context/require-admin.js +57 -0
- package/lib/context/sync.d.ts +12 -0
- package/lib/context/sync.js +234 -1
- package/lib/context/types.d.ts +56 -20
- package/lib/context/types.js +23 -3
- package/lib/groups/access-default-token.d.ts +44 -0
- package/lib/groups/access-default-token.js +103 -0
- package/lib/groups/admin-roster.d.ts +42 -117
- package/lib/groups/admin-roster.js +53 -168
- package/lib/groups/anchor-store.d.ts +15 -0
- package/lib/groups/anchor-store.js +37 -0
- package/lib/groups/app-cursor-store.d.ts +17 -0
- package/lib/groups/app-cursor-store.js +34 -0
- package/lib/groups/broadcast.d.ts +199 -81
- package/lib/groups/broadcast.js +443 -368
- package/lib/groups/catalog-token.d.ts +50 -0
- package/lib/groups/catalog-token.js +96 -0
- package/lib/groups/circle-projection.d.ts +32 -64
- package/lib/groups/circle-projection.js +95 -181
- package/lib/groups/circle-reducers.d.ts +71 -110
- package/lib/groups/circle-reducers.js +57 -127
- package/lib/groups/commit-adoption.d.ts +77 -0
- package/lib/groups/commit-adoption.js +108 -0
- package/lib/groups/commit-journal.d.ts +14 -0
- package/lib/groups/commit-journal.js +65 -0
- package/lib/groups/control-request.d.ts +66 -0
- package/lib/groups/control-request.js +101 -0
- package/lib/groups/events.d.ts +14 -1
- package/lib/groups/group-anchor.d.ts +18 -57
- package/lib/groups/group-anchor.js +15 -100
- package/lib/groups/group-crypto.d.ts +36 -13
- package/lib/groups/group-crypto.js +124 -50
- package/lib/groups/group-handle-registry.d.ts +106 -36
- package/lib/groups/group-handle-registry.js +351 -61
- package/lib/groups/group-handlers.d.ts +5 -4
- package/lib/groups/group-handlers.js +155 -29
- package/lib/groups/group-health-monitor.d.ts +1 -1
- package/lib/groups/group-health-monitor.js +1 -1
- package/lib/groups/group-mls.d.ts +35 -4
- package/lib/groups/group-mls.js +594 -62
- package/lib/groups/group-peer-manager.d.ts +212 -19
- package/lib/groups/group-peer-manager.js +1061 -69
- package/lib/groups/group-protocols.d.ts +485 -28
- package/lib/groups/group-protocols.js +289 -15
- package/lib/groups/invite-payload.d.ts +48 -0
- package/lib/groups/join-request-identity.d.ts +21 -0
- package/lib/groups/join-request-identity.js +43 -0
- package/lib/groups/join-utils.d.ts +60 -0
- package/lib/groups/join-utils.js +262 -0
- package/lib/groups/ledger-affected-events.d.ts +11 -6
- package/lib/groups/ledger-affected-events.js +15 -26
- package/lib/groups/ledger-commit-fold.d.ts +107 -0
- package/lib/groups/ledger-commit-fold.js +167 -0
- package/lib/groups/ledger-ingest.d.ts +124 -0
- package/lib/groups/ledger-ingest.js +212 -0
- package/lib/groups/ledger.d.ts +44 -0
- package/lib/groups/ledger.js +30 -0
- package/lib/groups/manager.d.ts +202 -19
- package/lib/groups/manager.js +623 -106
- package/lib/groups/mls-codec.d.ts +27 -6
- package/lib/groups/mls-codec.js +30 -15
- package/lib/groups/mls-encryptor.js +9 -6
- package/lib/groups/mls-group-handle.d.ts +37 -2
- package/lib/groups/mls-group-handle.js +12 -4
- package/lib/groups/mls-receive-errors.d.ts +0 -2
- package/lib/groups/mls-receive-errors.js +3 -6
- package/lib/groups/mls-state.d.ts +5 -3
- package/lib/groups/mls-state.js +27 -4
- package/lib/groups/peer-presence.d.ts +90 -0
- package/lib/groups/peer-presence.js +295 -0
- package/lib/groups/peer-selection.d.ts +37 -0
- package/lib/groups/peer-selection.js +72 -0
- package/lib/groups/roster-projection.d.ts +53 -0
- package/lib/groups/roster-projection.js +65 -0
- package/lib/groups/store-received-grant.d.ts +4 -3
- package/lib/groups/store-received-grant.js +80 -35
- package/lib/groups/store-received-revocation.d.ts +16 -13
- package/lib/groups/store-received-revocation.js +23 -22
- package/lib/hub/http-client.d.ts +10 -27
- package/lib/hub/http-client.js +2 -19
- package/lib/hub/hub-like.d.ts +57 -6
- package/lib/hub/hub-like.js +539 -52
- package/lib/hub/loopback-log-hub.d.ts +20 -0
- package/lib/hub/loopback-log-hub.js +178 -0
- package/lib/hub/manager.d.ts +25 -40
- package/lib/hub/manager.js +51 -94
- package/lib/hub/peer-scoped-hub-view.d.ts +35 -0
- package/lib/hub/peer-scoped-hub-view.js +92 -0
- package/lib/hub/sender-scoped-hub-view.d.ts +41 -0
- package/lib/hub/sender-scoped-hub-view.js +59 -0
- package/lib/hub/server-did.d.ts +67 -0
- package/lib/hub/server-did.js +131 -0
- package/lib/hub/wiring.d.ts +71 -14
- package/lib/hub/wiring.js +80 -23
- package/lib/index.d.ts +48 -13
- package/lib/index.js +257 -86
- package/lib/peer/connection-registry.d.ts +45 -0
- package/lib/peer/connection-registry.js +52 -0
- package/lib/peer/handlers.d.ts +69 -0
- package/lib/peer/handlers.js +110 -0
- package/lib/protocol.d.ts +213 -0
- package/lib/protocol.js +268 -0
- package/lib/schema.d.ts +2 -1
- package/lib/schema.js +805 -131
- package/lib/sync/access-default-sender.d.ts +23 -0
- package/lib/sync/access-default-sender.js +80 -0
- package/lib/sync/authorize.d.ts +57 -3
- package/lib/sync/authorize.js +45 -4
- package/lib/sync/broadcast-queue.d.ts +3 -4
- package/lib/sync/broadcast-queue.js +2 -3
- package/lib/sync/catalog-scope.d.ts +8 -7
- package/lib/sync/catalog-scope.js +55 -39
- package/lib/sync/errors.d.ts +9 -0
- package/lib/sync/errors.js +10 -0
- package/lib/sync/forwarder.d.ts +13 -19
- package/lib/sync/forwarder.js +13 -19
- package/lib/sync/handlers.js +54 -14
- package/lib/sync/hub-tunnel-sync-listener.d.ts +16 -6
- package/lib/sync/hub-tunnel-sync-listener.js +126 -37
- package/lib/sync/hub-tunnel-sync-provider.d.ts +31 -11
- package/lib/sync/hub-tunnel-sync-provider.js +68 -19
- package/lib/sync/merkle-channel.d.ts +9 -1
- package/lib/sync/merkle-channel.js +0 -0
- package/lib/sync/peer-registry.d.ts +14 -10
- package/lib/sync/peer-registry.js +0 -25
- package/lib/sync/sync-client.d.ts +11 -1
- package/lib/sync/sync-client.js +72 -10
- package/lib/sync/sync-manager.d.ts +22 -4
- package/lib/sync/sync-manager.js +39 -10
- package/lib/sync/tunnel-listeners.d.ts +52 -0
- package/lib/sync/tunnel-listeners.js +175 -0
- package/lib/sync/tunnel-observability.d.ts +25 -0
- package/lib/sync/tunnel-observability.js +63 -0
- package/lib/sync/tunnel-topics.d.ts +20 -0
- package/lib/sync/tunnel-topics.js +28 -0
- package/lib/types.d.ts +699 -28
- package/lib/types.js +0 -1
- package/lib/util/handler-error.d.ts +16 -0
- package/lib/util/handler-error.js +64 -0
- package/lib/util/mutex.d.ts +14 -1
- package/lib/util/mutex.js +37 -3
- package/package.json +59 -52
- package/lib/groups/ledger-entry.d.ts +0 -37
- package/lib/groups/ledger-entry.js +0 -56
- package/lib/groups/ledger-fold.d.ts +0 -56
- package/lib/groups/ledger-fold.js +0 -53
- package/lib/hub/did-observing-transport.d.ts +0 -75
- package/lib/hub/did-observing-transport.js +0 -126
- package/lib/hub/errors.d.ts +0 -24
- package/lib/hub/errors.js +0 -33
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { normalizeDID } from '@kokuin/token';
|
|
2
|
+
import { getP2PStore } from '@kubun/store-p2p';
|
|
3
|
+
import { HubTunnelSyncListener } from './hub-tunnel-sync-listener.js';
|
|
4
|
+
/**
|
|
5
|
+
* The answering half of the tunnel: one listener per co-member per group, so a
|
|
6
|
+
* device that dials `tunnel://<groupID>/<ourDID>` finds something draining the
|
|
7
|
+
* mailbox.
|
|
8
|
+
*
|
|
9
|
+
* Without this a route resolves, opens, publishes its first frame and idles out:
|
|
10
|
+
* "nobody is home" and "nobody is listening" are the same silence to the dialer.
|
|
11
|
+
*
|
|
12
|
+
* ONE PER PEER, not one per group: every listener on a device receives on the
|
|
13
|
+
* same inbox topic, and two sharing a raw drain destroy each other's frames.
|
|
14
|
+
* `HubTunnelSyncListener` fronts the hub with a per-peer view for exactly this,
|
|
15
|
+
* so peers are separated here rather than multiplexed inside a session.
|
|
16
|
+
*
|
|
17
|
+
* Gated on this device having declared a profile — the same invariant presence
|
|
18
|
+
* runs on. A device that never opted in is in nobody's projection, so nothing
|
|
19
|
+
* can dial it and a listener would hold a subscription open for a session that
|
|
20
|
+
* cannot arrive.
|
|
21
|
+
*/ export function createTunnelListeners(params) {
|
|
22
|
+
const selfDID = normalizeDID(params.localDID);
|
|
23
|
+
const { logger } = params;
|
|
24
|
+
// groupID → peerDID → its listener.
|
|
25
|
+
const listeners = new Map();
|
|
26
|
+
// Which drain each group's listeners were built on. A listener holds its hub
|
|
27
|
+
// for life, so a group that switches hubs needs new ones — and the peer set
|
|
28
|
+
// alone cannot show that, since the same members are still wanted.
|
|
29
|
+
const attachedHubs = new Map();
|
|
30
|
+
// And which epoch they derived their topics at. A live listener's subscription
|
|
31
|
+
// is pinned to the epoch its spawn ran at, and only a disposed transport
|
|
32
|
+
// re-derives — so a group whose epoch moved keeps draining a topic nobody
|
|
33
|
+
// publishes to any more, while every dialer idles out against the silence. The
|
|
34
|
+
// peer set cannot show this either: the same members are still wanted.
|
|
35
|
+
const attachedEpochs = new Map();
|
|
36
|
+
// Reconciles of one group run in sequence: an epoch burst fires several, and
|
|
37
|
+
// two interleaved passes over the same map would each start a listener the
|
|
38
|
+
// other has not recorded yet.
|
|
39
|
+
const queues = new Map();
|
|
40
|
+
let disposed = false;
|
|
41
|
+
const stopGroup = async (groupID)=>{
|
|
42
|
+
attachedHubs.delete(groupID);
|
|
43
|
+
attachedEpochs.delete(groupID);
|
|
44
|
+
const group = listeners.get(groupID);
|
|
45
|
+
if (group == null) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
listeners.delete(groupID);
|
|
49
|
+
await Promise.allSettled([
|
|
50
|
+
...group.values()
|
|
51
|
+
].map((listener)=>listener.stop()));
|
|
52
|
+
};
|
|
53
|
+
const runReconcile = async (groupID)=>{
|
|
54
|
+
if (disposed) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const store = await getP2PStore(params.stores);
|
|
58
|
+
const profile = await store.getLocalPeerProfile(selfDID);
|
|
59
|
+
if (profile == null) {
|
|
60
|
+
await stopGroup(groupID);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const hub = params.tunnelHub(groupID);
|
|
64
|
+
if (hub == null) {
|
|
65
|
+
// No hub bound: there is no mailbox to drain. A binding arriving later
|
|
66
|
+
// reconciles again.
|
|
67
|
+
await stopGroup(groupID);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const epoch = params.registry.groupEpoch(groupID);
|
|
71
|
+
if (attachedHubs.get(groupID) !== hub || attachedEpochs.get(groupID) !== epoch) {
|
|
72
|
+
await stopGroup(groupID);
|
|
73
|
+
}
|
|
74
|
+
attachedHubs.set(groupID, hub);
|
|
75
|
+
attachedEpochs.set(groupID, epoch);
|
|
76
|
+
const members = await store.listGroupMembers(groupID);
|
|
77
|
+
const wanted = new Set(members.map((member)=>normalizeDID(member.member_did)).filter((did)=>did !== selfDID));
|
|
78
|
+
let group = listeners.get(groupID);
|
|
79
|
+
if (group == null) {
|
|
80
|
+
group = new Map();
|
|
81
|
+
listeners.set(groupID, group);
|
|
82
|
+
}
|
|
83
|
+
for (const [peerDID, listener] of group){
|
|
84
|
+
if (!wanted.has(peerDID)) {
|
|
85
|
+
group.delete(peerDID);
|
|
86
|
+
try {
|
|
87
|
+
await listener.stop();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
logger?.warn('tunnel listener stop failed', {
|
|
90
|
+
groupID,
|
|
91
|
+
peerDID,
|
|
92
|
+
error
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Every await above is a point where the group could have been left or the
|
|
98
|
+
// whole thing disposed. A pass that continued past one would repopulate
|
|
99
|
+
// `listeners` with fresh, STARTED listeners for a group this device is no
|
|
100
|
+
// longer in, and they would drain that group's inbox topic indefinitely —
|
|
101
|
+
// nothing later removes what `removeGroup` already walked past.
|
|
102
|
+
if (disposed || listeners.get(groupID) !== group) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
for (const peerDID of wanted){
|
|
106
|
+
if (group.has(peerDID)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const listener = new HubTunnelSyncListener({
|
|
110
|
+
hub,
|
|
111
|
+
registry: params.registry,
|
|
112
|
+
groupID,
|
|
113
|
+
localDID: selfDID,
|
|
114
|
+
peerDID,
|
|
115
|
+
identity: params.identity,
|
|
116
|
+
syncHandlers: params.syncHandlers,
|
|
117
|
+
runtime: params.runtime,
|
|
118
|
+
...params.idleTimeoutMs != null ? {
|
|
119
|
+
idleTimeoutMs: params.idleTimeoutMs
|
|
120
|
+
} : {},
|
|
121
|
+
...logger != null ? {
|
|
122
|
+
logger
|
|
123
|
+
} : {}
|
|
124
|
+
});
|
|
125
|
+
group.set(peerDID, listener);
|
|
126
|
+
listener.start();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const reconcile = (groupID)=>{
|
|
130
|
+
const queued = (queues.get(groupID) ?? Promise.resolve()).then(()=>runReconcile(groupID), ()=>runReconcile(groupID));
|
|
131
|
+
// Never let one group's failure reject a caller that only asked for a
|
|
132
|
+
// reconcile — the next signal retries, and a throw here would surface on
|
|
133
|
+
// whichever unrelated lane happened to trigger it.
|
|
134
|
+
const settled = queued.catch((error)=>{
|
|
135
|
+
logger?.warn('tunnel listener reconcile failed', {
|
|
136
|
+
groupID,
|
|
137
|
+
error
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
queues.set(groupID, settled);
|
|
141
|
+
return settled;
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
reconcile,
|
|
145
|
+
reconcileAll: async (groupIDs)=>{
|
|
146
|
+
await Promise.all(groupIDs.map((groupID)=>reconcile(groupID)));
|
|
147
|
+
},
|
|
148
|
+
removeGroup: async (groupID)=>{
|
|
149
|
+
// Await the in-flight reconcile BEFORE tearing down. Dropping the queue
|
|
150
|
+
// entry does not stop the pass already running: it would finish after the
|
|
151
|
+
// stop and leave started listeners behind for a group the device has
|
|
152
|
+
// left. `runReconcile` also re-checks, so this is the pair.
|
|
153
|
+
const queued = queues.get(groupID);
|
|
154
|
+
queues.delete(groupID);
|
|
155
|
+
if (queued != null) {
|
|
156
|
+
await queued.catch(()=>{
|
|
157
|
+
// A failed reconcile is already logged; the teardown proceeds.
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
await stopGroup(groupID);
|
|
161
|
+
},
|
|
162
|
+
dispose: async ()=>{
|
|
163
|
+
disposed = true;
|
|
164
|
+
const inFlight = [
|
|
165
|
+
...queues.values()
|
|
166
|
+
];
|
|
167
|
+
queues.clear();
|
|
168
|
+
await Promise.allSettled(inFlight);
|
|
169
|
+
const groupIDs = [
|
|
170
|
+
...listeners.keys()
|
|
171
|
+
];
|
|
172
|
+
await Promise.allSettled(groupIDs.map((groupID)=>stopGroup(groupID)));
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Logger } from '@kubun/logger';
|
|
2
|
+
import type { ObservabilityEventListener } from '@kumiai/hub-tunnel';
|
|
3
|
+
/**
|
|
4
|
+
* Which side of a tunnel session a transport is. Carried on every line a
|
|
5
|
+
* session logs, because the two are indistinguishable otherwise: both receive
|
|
6
|
+
* on this device's inbox topic for the epoch, and a drop only means something
|
|
7
|
+
* once you know whether the reader was answering a peer or dialling one.
|
|
8
|
+
*/
|
|
9
|
+
export type TunnelSessionRole = 'dialer' | 'listener';
|
|
10
|
+
/**
|
|
11
|
+
* Turn a tunnel transport's observability events into log lines, and pass them
|
|
12
|
+
* on to a listener the caller supplied.
|
|
13
|
+
*
|
|
14
|
+
* Until this existed the transports' `onEvent` port had no producer anywhere in
|
|
15
|
+
* the repo — declared, forwarded to the transport, and set by nobody. Every
|
|
16
|
+
* destroyed frame was therefore silent, in tests and on devices alike: a
|
|
17
|
+
* starved session showed up as an idle timeout half a minute later with nothing
|
|
18
|
+
* saying why. That is the entire reason a two-device sync failure had to be
|
|
19
|
+
* diagnosed by reading code rather than by reading a log.
|
|
20
|
+
*
|
|
21
|
+
* `logger` is expected to carry the session's context already (`groupID`,
|
|
22
|
+
* `peerDID`, `role`, `epoch`, topics), so a line needs no arguments beyond what
|
|
23
|
+
* the event itself adds.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createTunnelEventLogger(logger: Logger, delegate?: ObservabilityEventListener): ObservabilityEventListener;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drops a healthy lane never produces, so each one is a frame that reached this
|
|
3
|
+
* device and was thrown away.
|
|
4
|
+
*
|
|
5
|
+
* - `decrypt` — the ciphertext did not open. On a lane whose key is right, that
|
|
6
|
+
* means something else opened it first: MLS consumes a ratchet generation per
|
|
7
|
+
* open, so a second reader of the same frame is handed a key that is gone.
|
|
8
|
+
* - `session-mismatch` — it opened, and belonged to a session this reader is not
|
|
9
|
+
* serving. The frame is destroyed for the session it WAS for, which is now
|
|
10
|
+
* waiting out its idle timeout with nothing raised.
|
|
11
|
+
* - `group-mismatch` — a frame for another group arrived on this topic, which
|
|
12
|
+
* the envelope catches before the cipher. Same-key misroute, never routine.
|
|
13
|
+
*
|
|
14
|
+
* Everything else is routine: `dedup` is the hub redelivering, `topic-mismatch`
|
|
15
|
+
* is the shared device drain doing its job, `envelope-decode` is a foreign
|
|
16
|
+
* publisher on the topic.
|
|
17
|
+
*/ const UNEXPECTED_REASONS = new Set([
|
|
18
|
+
'decrypt',
|
|
19
|
+
'session-mismatch',
|
|
20
|
+
'group-mismatch'
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* Turn a tunnel transport's observability events into log lines, and pass them
|
|
24
|
+
* on to a listener the caller supplied.
|
|
25
|
+
*
|
|
26
|
+
* Until this existed the transports' `onEvent` port had no producer anywhere in
|
|
27
|
+
* the repo — declared, forwarded to the transport, and set by nobody. Every
|
|
28
|
+
* destroyed frame was therefore silent, in tests and on devices alike: a
|
|
29
|
+
* starved session showed up as an idle timeout half a minute later with nothing
|
|
30
|
+
* saying why. That is the entire reason a two-device sync failure had to be
|
|
31
|
+
* diagnosed by reading code rather than by reading a log.
|
|
32
|
+
*
|
|
33
|
+
* `logger` is expected to carry the session's context already (`groupID`,
|
|
34
|
+
* `peerDID`, `role`, `epoch`, topics), so a line needs no arguments beyond what
|
|
35
|
+
* the event itself adds.
|
|
36
|
+
*/ export function createTunnelEventLogger(logger, delegate) {
|
|
37
|
+
return (event)=>{
|
|
38
|
+
switch(event.type){
|
|
39
|
+
case 'decrypt-failed':
|
|
40
|
+
logger.warn('tunnel frame failed to decrypt', {
|
|
41
|
+
error: event.error
|
|
42
|
+
});
|
|
43
|
+
break;
|
|
44
|
+
case 'envelope-decode-failed':
|
|
45
|
+
logger.debug('tunnel envelope failed to decode', {
|
|
46
|
+
error: event.error
|
|
47
|
+
});
|
|
48
|
+
break;
|
|
49
|
+
case 'frame-dropped':
|
|
50
|
+
if (UNEXPECTED_REASONS.has(event.reason)) {
|
|
51
|
+
logger.warn('tunnel frame dropped', {
|
|
52
|
+
reason: event.reason
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
logger.debug('tunnel frame dropped', {
|
|
56
|
+
reason: event.reason
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
delegate?.(event);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which side of a directed session a topic belongs to. A `dialer` topic carries
|
|
3
|
+
* the responses flowing back to whoever opened the session; a `responder` topic
|
|
4
|
+
* carries the requests arriving at whoever answers.
|
|
5
|
+
*/
|
|
6
|
+
export type TunnelRole = 'dialer' | 'responder';
|
|
7
|
+
/**
|
|
8
|
+
* The topic on which `ownerDID` receives the frames addressed to it in `role`.
|
|
9
|
+
*
|
|
10
|
+
* Both sides derive both topics from the same (secret, epoch), so a dialer knows
|
|
11
|
+
* where to write and a responder knows where to answer without negotiating
|
|
12
|
+
* anything. Topics rotate with the MLS epoch exactly as the inbox ones did.
|
|
13
|
+
*
|
|
14
|
+
* Scoped by role and owner but NOT by session: a device holds one drain per
|
|
15
|
+
* (role, peer) and separates concurrent sessions to different peers by sender.
|
|
16
|
+
* Two sessions in the same role to the SAME peer at once would still contend —
|
|
17
|
+
* nothing in the sync client opens those, and a session id in the scope would
|
|
18
|
+
* cost a round trip to agree on before either side could subscribe.
|
|
19
|
+
*/
|
|
20
|
+
export declare function tunnelTopic(secret: Uint8Array, epoch: number, role: TunnelRole, ownerDID: string): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { protocolTopic } from '@kumiai/rpc';
|
|
2
|
+
/**
|
|
3
|
+
* The lane every directed sync tunnel rides, owned by this package.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately not `inboxTopic`: that derives ONE topic per member, so a device
|
|
6
|
+
* dialing a peer and answering that same peer both read and both write the two
|
|
7
|
+
* members' inboxes. The two roles then share a topic, and sender scope cannot
|
|
8
|
+
* separate them — a peer's reply and that peer's own dial request are equally
|
|
9
|
+
* "from the peer". Whichever of the two opens a frame first consumes its MLS
|
|
10
|
+
* ratchet generation and DESTROYS it for the other, which waits out its idle
|
|
11
|
+
* timeout instead. `inboxTopic` is also `@kumiai/rpc`'s reserved lane, and a
|
|
12
|
+
* host protocol has no business publishing into it.
|
|
13
|
+
*/ const SYNC_TUNNEL_PROTOCOL = 'kubun/sync-tunnel/v1';
|
|
14
|
+
/**
|
|
15
|
+
* The topic on which `ownerDID` receives the frames addressed to it in `role`.
|
|
16
|
+
*
|
|
17
|
+
* Both sides derive both topics from the same (secret, epoch), so a dialer knows
|
|
18
|
+
* where to write and a responder knows where to answer without negotiating
|
|
19
|
+
* anything. Topics rotate with the MLS epoch exactly as the inbox ones did.
|
|
20
|
+
*
|
|
21
|
+
* Scoped by role and owner but NOT by session: a device holds one drain per
|
|
22
|
+
* (role, peer) and separates concurrent sessions to different peers by sender.
|
|
23
|
+
* Two sessions in the same role to the SAME peer at once would still contend —
|
|
24
|
+
* nothing in the sync client opens those, and a session id in the scope would
|
|
25
|
+
* cost a round trip to agree on before either side could subscribe.
|
|
26
|
+
*/ export function tunnelTopic(secret, epoch, role, ownerDID) {
|
|
27
|
+
return protocolTopic(secret, epoch, SYNC_TUNNEL_PROTOCOL, `${role}:${ownerDID}`);
|
|
28
|
+
}
|