@kubun/plugin-p2p 0.14.0 → 0.15.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/peer.js +250 -3
- package/lib/context/sync.js +134 -25
- package/lib/context/types.d.ts +7 -0
- package/lib/groups/broadcast-message.d.ts +29 -0
- package/lib/groups/broadcast.d.ts +11 -1
- package/lib/groups/broadcast.js +44 -2
- package/lib/groups/credential-apply.d.ts +64 -2
- package/lib/groups/credential-apply.js +215 -30
- package/lib/groups/credential-grant.d.ts +22 -0
- package/lib/groups/credential-grant.js +76 -2
- package/lib/groups/credential-manifest-token.d.ts +31 -0
- package/lib/groups/credential-manifest-token.js +49 -0
- package/lib/groups/credential-readiness.d.ts +69 -0
- package/lib/groups/credential-readiness.js +172 -0
- package/lib/groups/credential-wrapping-deps.d.ts +23 -0
- package/lib/groups/credential-wrapping-deps.js +25 -0
- package/lib/groups/did-cache-seed.d.ts +26 -0
- package/lib/groups/did-cache-seed.js +38 -0
- package/lib/groups/grantor-authority.d.ts +65 -0
- package/lib/groups/grantor-authority.js +107 -0
- package/lib/groups/group-handlers.js +7 -0
- package/lib/groups/group-peer-manager.d.ts +32 -1
- package/lib/groups/group-peer-manager.js +77 -0
- package/lib/groups/group-protocols.d.ts +47 -0
- package/lib/groups/group-protocols.js +28 -0
- package/lib/hub/http-client.js +6 -1
- package/lib/hub/hub-like.js +5 -2
- package/lib/hub/peer-scoped-hub-view.d.ts +6 -0
- package/lib/hub/peer-scoped-hub-view.js +11 -1
- package/lib/hub/wiring.d.ts +31 -1
- package/lib/hub/wiring.js +20 -1
- package/lib/index.d.ts +14 -0
- package/lib/index.js +72 -4
- package/lib/protocol.d.ts +30 -0
- package/lib/protocol.js +36 -0
- package/lib/schema.js +78 -1
- package/lib/sync/handlers.js +21 -2
- package/lib/sync/held-delegations.d.ts +14 -0
- package/lib/sync/held-delegations.js +34 -0
- package/lib/sync/hub-tunnel-service-listener.d.ts +84 -0
- package/lib/sync/hub-tunnel-service-listener.js +294 -0
- package/lib/sync/hub-tunnel-service-provider.d.ts +46 -0
- package/lib/sync/hub-tunnel-service-provider.js +100 -0
- package/lib/sync/hub-tunnel-sync-listener.d.ts +8 -1
- package/lib/sync/hub-tunnel-sync-listener.js +6 -1
- package/lib/sync/service-tunnel-listeners.d.ts +41 -0
- package/lib/sync/service-tunnel-listeners.js +183 -0
- package/lib/sync/sync-manager.d.ts +7 -0
- package/lib/sync/sync-manager.js +4 -1
- package/lib/sync/tunnel-listeners.d.ts +7 -1
- package/lib/sync/tunnel-listeners.js +18 -0
- package/lib/sync/tunnel-topics.d.ts +19 -1
- package/lib/sync/tunnel-topics.js +7 -3
- package/lib/types.d.ts +172 -0
- package/package.json +49 -48
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { normalizeDID } from '@kokuin/token';
|
|
2
|
+
import { getP2PStore } from '@kubun/store-p2p';
|
|
3
|
+
import { seedDIDCacheFromRoster } from '../groups/did-cache-seed.js';
|
|
4
|
+
import { HubTunnelServiceListener } from './hub-tunnel-service-listener.js';
|
|
5
|
+
/**
|
|
6
|
+
* The answering half of the service tunnel — one `HubTunnelServiceListener` per
|
|
7
|
+
* co-member per group, so a device dialing on the service lane finds something
|
|
8
|
+
* draining the mailbox and handing the session to the injected `serve()`.
|
|
9
|
+
*
|
|
10
|
+
* Structurally identical to {@link createTunnelListeners} (the sync lane's
|
|
11
|
+
* answering half): same groupID→peerDID map, same epoch/hub-change teardown,
|
|
12
|
+
* same per-group serialized reconciles — only the listener class and what's
|
|
13
|
+
* threaded into it differ, so the two lanes never share an instance or ratchet.
|
|
14
|
+
*/ export function createServiceTunnelListeners(params) {
|
|
15
|
+
const selfDID = normalizeDID(params.localDID);
|
|
16
|
+
const { logger } = params;
|
|
17
|
+
// groupID → peerDID → its listener.
|
|
18
|
+
const listeners = new Map();
|
|
19
|
+
// Which drain each group's listeners were built on. A listener holds its hub
|
|
20
|
+
// for life, so a group that switches hubs needs new ones — and the peer set
|
|
21
|
+
// alone cannot show that, since the same members are still wanted.
|
|
22
|
+
const attachedHubs = new Map();
|
|
23
|
+
// And which epoch they derived their topics at. A live listener's subscription
|
|
24
|
+
// is pinned to its spawn epoch; only a disposed transport re-derives, so a
|
|
25
|
+
// group whose epoch moved would otherwise keep draining a stale topic.
|
|
26
|
+
const attachedEpochs = new Map();
|
|
27
|
+
// Reconciles of one group run in sequence: an epoch burst fires several, and
|
|
28
|
+
// two interleaved passes over the same map would each start a listener the
|
|
29
|
+
// other has not recorded yet.
|
|
30
|
+
const queues = new Map();
|
|
31
|
+
let disposed = false;
|
|
32
|
+
const stopGroup = async (groupID)=>{
|
|
33
|
+
attachedHubs.delete(groupID);
|
|
34
|
+
attachedEpochs.delete(groupID);
|
|
35
|
+
const group = listeners.get(groupID);
|
|
36
|
+
if (group == null) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
listeners.delete(groupID);
|
|
40
|
+
await Promise.allSettled([
|
|
41
|
+
...group.values()
|
|
42
|
+
].map((listener)=>listener.stop()));
|
|
43
|
+
};
|
|
44
|
+
const runReconcile = async (groupID)=>{
|
|
45
|
+
if (disposed) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const store = await getP2PStore(params.stores);
|
|
49
|
+
const profile = await store.getLocalPeerProfile(selfDID);
|
|
50
|
+
if (profile == null) {
|
|
51
|
+
await stopGroup(groupID);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const hub = params.tunnelHub(groupID);
|
|
55
|
+
if (hub == null) {
|
|
56
|
+
// No hub bound: there is no mailbox to drain. A binding arriving later
|
|
57
|
+
// reconciles again.
|
|
58
|
+
await stopGroup(groupID);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const epoch = params.registry.groupEpoch(groupID);
|
|
62
|
+
if (attachedHubs.get(groupID) !== hub || attachedEpochs.get(groupID) !== epoch) {
|
|
63
|
+
await stopGroup(groupID);
|
|
64
|
+
}
|
|
65
|
+
attachedHubs.set(groupID, hub);
|
|
66
|
+
attachedEpochs.set(groupID, epoch);
|
|
67
|
+
// Bind and membership change both land here, so this is the one seam where a
|
|
68
|
+
// co-member's roster long form is refreshed into the shared cache before any
|
|
69
|
+
// service session that must resolve its short-form issuer can spawn.
|
|
70
|
+
if (params.cache != null) {
|
|
71
|
+
await seedDIDCacheFromRoster({
|
|
72
|
+
cache: params.cache,
|
|
73
|
+
registry: params.registry,
|
|
74
|
+
groupID,
|
|
75
|
+
stores: params.stores,
|
|
76
|
+
...logger != null ? {
|
|
77
|
+
logger
|
|
78
|
+
} : {}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const members = await store.listGroupMembers(groupID);
|
|
82
|
+
const wanted = new Set(members.map((member)=>normalizeDID(member.member_did)).filter((did)=>did !== selfDID));
|
|
83
|
+
let group = listeners.get(groupID);
|
|
84
|
+
if (group == null) {
|
|
85
|
+
group = new Map();
|
|
86
|
+
listeners.set(groupID, group);
|
|
87
|
+
}
|
|
88
|
+
for (const [peerDID, listener] of group){
|
|
89
|
+
if (!wanted.has(peerDID)) {
|
|
90
|
+
group.delete(peerDID);
|
|
91
|
+
try {
|
|
92
|
+
await listener.stop();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
logger?.warn('service tunnel listener stop failed', {
|
|
95
|
+
groupID,
|
|
96
|
+
peerDID,
|
|
97
|
+
error
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Every await above is a point where the group could have been left or the
|
|
103
|
+
// whole thing disposed. A pass that continued past one would repopulate
|
|
104
|
+
// `listeners` with fresh, STARTED listeners for a group this device is no
|
|
105
|
+
// longer in, and they would drain that group's inbox topic indefinitely —
|
|
106
|
+
// nothing later removes what `removeGroup` already walked past.
|
|
107
|
+
if (disposed || listeners.get(groupID) !== group) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
for (const peerDID of wanted){
|
|
111
|
+
if (group.has(peerDID)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const listener = new HubTunnelServiceListener({
|
|
115
|
+
hub,
|
|
116
|
+
registry: params.registry,
|
|
117
|
+
groupID,
|
|
118
|
+
localDID: selfDID,
|
|
119
|
+
peerDID,
|
|
120
|
+
runtime: params.runtime,
|
|
121
|
+
serve: params.serve,
|
|
122
|
+
services: params.services,
|
|
123
|
+
...params.cache != null ? {
|
|
124
|
+
cache: params.cache
|
|
125
|
+
} : {},
|
|
126
|
+
...params.idleTimeoutMs != null ? {
|
|
127
|
+
idleTimeoutMs: params.idleTimeoutMs
|
|
128
|
+
} : {},
|
|
129
|
+
...logger != null ? {
|
|
130
|
+
logger
|
|
131
|
+
} : {}
|
|
132
|
+
});
|
|
133
|
+
group.set(peerDID, listener);
|
|
134
|
+
listener.start();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const reconcile = (groupID)=>{
|
|
138
|
+
const queued = (queues.get(groupID) ?? Promise.resolve()).then(()=>runReconcile(groupID), ()=>runReconcile(groupID));
|
|
139
|
+
// Never let one group's failure reject a caller that only asked for a
|
|
140
|
+
// reconcile — the next signal retries, and a throw here would surface on
|
|
141
|
+
// whichever unrelated lane happened to trigger it.
|
|
142
|
+
const settled = queued.catch((error)=>{
|
|
143
|
+
logger?.warn('service tunnel listener reconcile failed', {
|
|
144
|
+
groupID,
|
|
145
|
+
error
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
queues.set(groupID, settled);
|
|
149
|
+
return settled;
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
reconcile,
|
|
153
|
+
reconcileAll: async (groupIDs)=>{
|
|
154
|
+
await Promise.all(groupIDs.map((groupID)=>reconcile(groupID)));
|
|
155
|
+
},
|
|
156
|
+
removeGroup: async (groupID)=>{
|
|
157
|
+
// Await the in-flight reconcile BEFORE tearing down. Dropping the queue
|
|
158
|
+
// entry does not stop the pass already running: it would finish after the
|
|
159
|
+
// stop and leave started listeners behind for a group the device has
|
|
160
|
+
// left. `runReconcile` also re-checks, so this is the pair.
|
|
161
|
+
const queued = queues.get(groupID);
|
|
162
|
+
queues.delete(groupID);
|
|
163
|
+
if (queued != null) {
|
|
164
|
+
await queued.catch(()=>{
|
|
165
|
+
// A failed reconcile is already logged; the teardown proceeds.
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
await stopGroup(groupID);
|
|
169
|
+
},
|
|
170
|
+
dispose: async ()=>{
|
|
171
|
+
disposed = true;
|
|
172
|
+
const inFlight = [
|
|
173
|
+
...queues.values()
|
|
174
|
+
];
|
|
175
|
+
queues.clear();
|
|
176
|
+
await Promise.allSettled(inFlight);
|
|
177
|
+
const groupIDs = [
|
|
178
|
+
...listeners.keys()
|
|
179
|
+
];
|
|
180
|
+
await Promise.allSettled(groupIDs.map((groupID)=>stopGroup(groupID)));
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -64,6 +64,13 @@ export type MerkleSyncParams = {
|
|
|
64
64
|
* is swallowed — the credential lane must never fail the doc sync.
|
|
65
65
|
*/
|
|
66
66
|
reconcileCredentialsHeld?: SyncReconcileCredentialsParam['held'];
|
|
67
|
+
/**
|
|
68
|
+
* Owner DIDs to pull the latest durable manifest for, addressed to this
|
|
69
|
+
* device, alongside the same `sync/reconcile-credentials` request — the PULL
|
|
70
|
+
* repair channel independent of LIVE broadcast. Undefined omits `owners`
|
|
71
|
+
* from the wire request entirely, leaving existing behavior unchanged.
|
|
72
|
+
*/
|
|
73
|
+
reconcileCredentialOwners?: Array<string>;
|
|
67
74
|
};
|
|
68
75
|
export declare class SyncManager {
|
|
69
76
|
#private;
|
package/lib/sync/sync-manager.js
CHANGED
|
@@ -136,7 +136,10 @@ export class SyncManager {
|
|
|
136
136
|
try {
|
|
137
137
|
credentialReconcile = await client.request('sync/reconcile-credentials', {
|
|
138
138
|
param: {
|
|
139
|
-
held: params.reconcileCredentialsHeld
|
|
139
|
+
held: params.reconcileCredentialsHeld,
|
|
140
|
+
...params.reconcileCredentialOwners != null && {
|
|
141
|
+
owners: params.reconcileCredentialOwners
|
|
142
|
+
}
|
|
140
143
|
}
|
|
141
144
|
});
|
|
142
145
|
} catch (error) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProcedureHandlers } from '@enkaku/server';
|
|
2
|
-
import type { OwnIdentity } from '@kokuin/token';
|
|
2
|
+
import type { DIDCache, OwnIdentity } from '@kokuin/token';
|
|
3
3
|
import type { StoreProvider } from '@kubun/db';
|
|
4
4
|
import type { Logger } from '@kubun/logger';
|
|
5
5
|
import type { MailboxHub } from '@kumiai/hub-tunnel';
|
|
@@ -13,6 +13,12 @@ export type TunnelListenersParams = {
|
|
|
13
13
|
localDID: string;
|
|
14
14
|
runtime: Runtime;
|
|
15
15
|
syncHandlers: ProcedureHandlers<SyncProtocol>;
|
|
16
|
+
/**
|
|
17
|
+
* Shared, plugin-lifetime DID cache. Seeded from each group's roster on every
|
|
18
|
+
* reconcile (bind and membership change) and handed to every spawned session's
|
|
19
|
+
* Server, so a co-member's short-form `did:peer:4` issuer resolves there.
|
|
20
|
+
*/
|
|
21
|
+
cache?: DIDCache;
|
|
16
22
|
/** The group's device-wide drain, or `undefined` when no hub is bound. */
|
|
17
23
|
tunnelHub: (groupID: string) => MailboxHub | undefined;
|
|
18
24
|
logger?: Logger;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeDID } from '@kokuin/token';
|
|
2
2
|
import { getP2PStore } from '@kubun/store-p2p';
|
|
3
|
+
import { seedDIDCacheFromRoster } from '../groups/did-cache-seed.js';
|
|
3
4
|
import { HubTunnelSyncListener } from './hub-tunnel-sync-listener.js';
|
|
4
5
|
/**
|
|
5
6
|
* The answering half of the tunnel: one listener per co-member per group, so a
|
|
@@ -73,6 +74,20 @@ import { HubTunnelSyncListener } from './hub-tunnel-sync-listener.js';
|
|
|
73
74
|
}
|
|
74
75
|
attachedHubs.set(groupID, hub);
|
|
75
76
|
attachedEpochs.set(groupID, epoch);
|
|
77
|
+
// Bind and membership change both land here, so this is the one seam where a
|
|
78
|
+
// co-member's roster long form is refreshed into the shared cache before any
|
|
79
|
+
// session that must resolve its short-form issuer can spawn.
|
|
80
|
+
if (params.cache != null) {
|
|
81
|
+
await seedDIDCacheFromRoster({
|
|
82
|
+
cache: params.cache,
|
|
83
|
+
registry: params.registry,
|
|
84
|
+
groupID,
|
|
85
|
+
stores: params.stores,
|
|
86
|
+
...logger != null ? {
|
|
87
|
+
logger
|
|
88
|
+
} : {}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
76
91
|
const members = await store.listGroupMembers(groupID);
|
|
77
92
|
const wanted = new Set(members.map((member)=>normalizeDID(member.member_did)).filter((did)=>did !== selfDID));
|
|
78
93
|
let group = listeners.get(groupID);
|
|
@@ -115,6 +130,9 @@ import { HubTunnelSyncListener } from './hub-tunnel-sync-listener.js';
|
|
|
115
130
|
identity: params.identity,
|
|
116
131
|
syncHandlers: params.syncHandlers,
|
|
117
132
|
runtime: params.runtime,
|
|
133
|
+
...params.cache != null ? {
|
|
134
|
+
cache: params.cache
|
|
135
|
+
} : {},
|
|
118
136
|
...params.idleTimeoutMs != null ? {
|
|
119
137
|
idleTimeoutMs: params.idleTimeoutMs
|
|
120
138
|
} : {},
|
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lane every directed sync tunnel rides, owned by this package.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately not `inboxTopic`: that derives ONE topic per member, so a device
|
|
5
|
+
* dialing a peer and answering that same peer both read and both write the two
|
|
6
|
+
* members' inboxes. The two roles then share a topic, and sender scope cannot
|
|
7
|
+
* separate them — a peer's reply and that peer's own dial request are equally
|
|
8
|
+
* "from the peer". Whichever of the two opens a frame first consumes its MLS
|
|
9
|
+
* ratchet generation and DESTROYS it for the other, which waits out its idle
|
|
10
|
+
* timeout instead. `inboxTopic` is also `@kumiai/rpc`'s reserved lane, and a
|
|
11
|
+
* host protocol has no business publishing into it.
|
|
12
|
+
*/
|
|
13
|
+
export declare const SYNC_TUNNEL_PROTOCOL = "kubun/sync-tunnel/v1";
|
|
14
|
+
/**
|
|
15
|
+
* Distinct protocol label so a service session never shares a ratchet
|
|
16
|
+
* generation with a sync session to the same peer.
|
|
17
|
+
*/
|
|
18
|
+
export declare const SERVICE_TUNNEL_PROTOCOL = "kubun/service-tunnel/v1";
|
|
1
19
|
/**
|
|
2
20
|
* Which side of a directed session a topic belongs to. A `dialer` topic carries
|
|
3
21
|
* the responses flowing back to whoever opened the session; a `responder` topic
|
|
@@ -17,4 +35,4 @@ export type TunnelRole = 'dialer' | 'responder';
|
|
|
17
35
|
* nothing in the sync client opens those, and a session id in the scope would
|
|
18
36
|
* cost a round trip to agree on before either side could subscribe.
|
|
19
37
|
*/
|
|
20
|
-
export declare function tunnelTopic(secret: Uint8Array, epoch: number, role: TunnelRole, ownerDID: string): string;
|
|
38
|
+
export declare function tunnelTopic(secret: Uint8Array, epoch: number, role: TunnelRole, ownerDID: string, protocol?: string): string;
|
|
@@ -10,7 +10,11 @@ import { protocolTopic } from '@kumiai/rpc';
|
|
|
10
10
|
* ratchet generation and DESTROYS it for the other, which waits out its idle
|
|
11
11
|
* timeout instead. `inboxTopic` is also `@kumiai/rpc`'s reserved lane, and a
|
|
12
12
|
* host protocol has no business publishing into it.
|
|
13
|
-
*/ const SYNC_TUNNEL_PROTOCOL = 'kubun/sync-tunnel/v1';
|
|
13
|
+
*/ export const SYNC_TUNNEL_PROTOCOL = 'kubun/sync-tunnel/v1';
|
|
14
|
+
/**
|
|
15
|
+
* Distinct protocol label so a service session never shares a ratchet
|
|
16
|
+
* generation with a sync session to the same peer.
|
|
17
|
+
*/ export const SERVICE_TUNNEL_PROTOCOL = 'kubun/service-tunnel/v1';
|
|
14
18
|
/**
|
|
15
19
|
* The topic on which `ownerDID` receives the frames addressed to it in `role`.
|
|
16
20
|
*
|
|
@@ -23,6 +27,6 @@ import { protocolTopic } from '@kumiai/rpc';
|
|
|
23
27
|
* Two sessions in the same role to the SAME peer at once would still contend —
|
|
24
28
|
* nothing in the sync client opens those, and a session id in the scope would
|
|
25
29
|
* 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,
|
|
30
|
+
*/ export function tunnelTopic(secret, epoch, role, ownerDID, protocol = SYNC_TUNNEL_PROTOCOL) {
|
|
31
|
+
return protocolTopic(secret, epoch, protocol, `${role}:${ownerDID}`);
|
|
28
32
|
}
|
package/lib/types.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ClientTransportOf } from '@enkaku/protocol';
|
|
2
|
+
import type { ServiceProtocol } from '@kubun/plugin-service-api';
|
|
2
3
|
import type { AdaptivePolicy } from '@kubun/plugin-workflow-api';
|
|
3
4
|
import type { AccessLevel } from '@kubun/store-graph';
|
|
4
5
|
import type { ControlRequestKind, ControlRequestOutcome } from '@kubun/store-p2p';
|
|
6
|
+
import type { CredentialGrantOutcomeKind } from './groups/credential-grant.js';
|
|
5
7
|
import type { PeerAvailability, PeerCapability } from './groups/group-protocols.js';
|
|
6
8
|
import type { PeerConnection } from './peer/connection-registry.js';
|
|
7
9
|
import type { SyncDirection, SyncProtocol } from './protocol.js';
|
|
@@ -392,6 +394,17 @@ export type SyncPeerData = {
|
|
|
392
394
|
messagesReceived: number;
|
|
393
395
|
messagesSent: number;
|
|
394
396
|
divergentBuckets: number;
|
|
397
|
+
/**
|
|
398
|
+
* Rows the piggybacked credential reconcile moved in: manifests stored,
|
|
399
|
+
* bundles applied, rows tombstoned. Absent both when the lane never ran and
|
|
400
|
+
* when its apply failed (swallowed to keep doc catch-up non-fatal) — not a
|
|
401
|
+
* completeness signal; use the manifest-vs-store readiness query for that.
|
|
402
|
+
*/
|
|
403
|
+
credentialReconcileApplied?: {
|
|
404
|
+
manifestsApplied: number;
|
|
405
|
+
applied: number;
|
|
406
|
+
tombstoned: number;
|
|
407
|
+
};
|
|
395
408
|
};
|
|
396
409
|
/**
|
|
397
410
|
* Why an automatic catch-up carried nothing — the distinction all-zero counts
|
|
@@ -549,6 +562,12 @@ export type AdmitJoinRequestParams = {
|
|
|
549
562
|
* payload's own `did` field is only cross-checked against it.
|
|
550
563
|
*/
|
|
551
564
|
joinRequest: string;
|
|
565
|
+
/**
|
|
566
|
+
* Delegation tokens delivered to the joiner atomically with admission (e.g.
|
|
567
|
+
* a controller→device grant), via the same InvitePayload.grants path as
|
|
568
|
+
* `requestInvite`.
|
|
569
|
+
*/
|
|
570
|
+
grants?: Array<string>;
|
|
552
571
|
/** See {@link SharePeerGroupParams.sendModels}. */
|
|
553
572
|
sendModels?: Array<string> | null;
|
|
554
573
|
/** See {@link SharePeerGroupParams.receiveActivate}. */
|
|
@@ -570,6 +589,110 @@ export type JoinPeerGroupParams = {
|
|
|
570
589
|
/** The group to join, as discovered from the peer's `connectPeer` group list. */
|
|
571
590
|
groupID: string;
|
|
572
591
|
};
|
|
592
|
+
export type GrantDeviceCredentialsParams = {
|
|
593
|
+
/**
|
|
594
|
+
* The MLS group whose roster `recipientDID` must resolve in — the same
|
|
595
|
+
* lookup `grantCredentialKeyToMember` makes to find a wrappable form.
|
|
596
|
+
*/
|
|
597
|
+
groupID: string;
|
|
598
|
+
/** The device being granted this batch of `ownerDID`'s active credential keys. */
|
|
599
|
+
recipientDID: string;
|
|
600
|
+
/**
|
|
601
|
+
* The credential owner whose active keys are enumerated and granted.
|
|
602
|
+
* Normalized once and reused for the grant calls and the manifest so both
|
|
603
|
+
* name the same principal.
|
|
604
|
+
*/
|
|
605
|
+
ownerDID: string;
|
|
606
|
+
/**
|
|
607
|
+
* Floor for the allocated provisioning epoch (`nextProvisioningEpoch` =
|
|
608
|
+
* `max(stored, minEpoch) + 1`); defaults to 0. Pass a recipient's known
|
|
609
|
+
* held epoch to force the new manifest strictly past it.
|
|
610
|
+
*/
|
|
611
|
+
minEpoch?: number;
|
|
612
|
+
};
|
|
613
|
+
/**
|
|
614
|
+
* One key's outcome from a `grantDeviceCredentials` batch — never thrown; the
|
|
615
|
+
* batch reports every key's fate so a caller can retry only what didn't land.
|
|
616
|
+
*/
|
|
617
|
+
export type CredentialGrantOutcome = {
|
|
618
|
+
keyID: string;
|
|
619
|
+
/** The key's version after this call, or `null` for `not-held`/`error` outcomes. */
|
|
620
|
+
version: number | null;
|
|
621
|
+
outcome: CredentialGrantOutcomeKind;
|
|
622
|
+
/** Present on `not-held` and `error`; the caught error's message. */
|
|
623
|
+
reason?: string;
|
|
624
|
+
};
|
|
625
|
+
export type CredentialProvisioningStatusParams = {
|
|
626
|
+
/**
|
|
627
|
+
* The credential owner whose per-grantor manifests addressed to THIS device
|
|
628
|
+
* name the keys expected here. Normalized once alongside the viewer.
|
|
629
|
+
*/
|
|
630
|
+
ownerDID: string;
|
|
631
|
+
};
|
|
632
|
+
export type CredentialProvisioningStatusData = {
|
|
633
|
+
/** True iff every durable expectation is satisfied — `missing` is empty. */
|
|
634
|
+
complete: boolean;
|
|
635
|
+
/**
|
|
636
|
+
* Expected keyIDs not yet materialized here: absent, unwrapped for this
|
|
637
|
+
* device, or locked behind a wrapping that never decrypt-verifies.
|
|
638
|
+
*/
|
|
639
|
+
missing: Array<string>;
|
|
640
|
+
/**
|
|
641
|
+
* Explains a `false` verdict: `'not-initiated'` (no expectations recorded
|
|
642
|
+
* yet), `'pending'` (an attempt in flight, floor not bound), or
|
|
643
|
+
* `'incomplete'` (recorded but not yet satisfied). Absent when `complete`.
|
|
644
|
+
*/
|
|
645
|
+
reason?: 'not-initiated' | 'pending' | 'incomplete';
|
|
646
|
+
};
|
|
647
|
+
export type BeginProvisioningExpectationParams = {
|
|
648
|
+
/** The credential owner this expectation tracks provisioning for. */
|
|
649
|
+
ownerDID: string;
|
|
650
|
+
/** The grantor this device expects a manifest from. */
|
|
651
|
+
grantorDID: string;
|
|
652
|
+
};
|
|
653
|
+
export type BeginProvisioningExpectationData = {
|
|
654
|
+
/** Freshly minted attempt id; pass it to `recordProvisioningExpectation`/`abandonProvisioningExpectation`. */
|
|
655
|
+
attemptId: string;
|
|
656
|
+
};
|
|
657
|
+
export type RecordProvisioningExpectationParams = {
|
|
658
|
+
ownerDID: string;
|
|
659
|
+
grantorDID: string;
|
|
660
|
+
/** The id `beginProvisioningExpectation` minted for this attempt. */
|
|
661
|
+
attemptId: string;
|
|
662
|
+
/** The epoch this attempt's grant call was allocated and signed under. */
|
|
663
|
+
epoch: number;
|
|
664
|
+
};
|
|
665
|
+
export type RecordProvisioningExpectationData = {
|
|
666
|
+
status: 'recorded';
|
|
667
|
+
} | {
|
|
668
|
+
status: 'retry';
|
|
669
|
+
/** The recipient's now-held epoch; re-run the grant with this as the new `minEpoch`. */
|
|
670
|
+
minEpoch: number;
|
|
671
|
+
};
|
|
672
|
+
export type AbandonProvisioningExpectationParams = {
|
|
673
|
+
ownerDID: string;
|
|
674
|
+
grantorDID: string;
|
|
675
|
+
/** The id to stop tracking. Removes ONLY this id; never binds or lowers a floor. */
|
|
676
|
+
attemptId: string;
|
|
677
|
+
};
|
|
678
|
+
export type ProvisioningHeldEpochParams = {
|
|
679
|
+
ownerDID: string;
|
|
680
|
+
grantorDID: string;
|
|
681
|
+
};
|
|
682
|
+
export type GrantDeviceCredentialsData = {
|
|
683
|
+
/** One entry per active key `ownerDID` holds on this device, in listed order. */
|
|
684
|
+
results: Array<CredentialGrantOutcome>;
|
|
685
|
+
/**
|
|
686
|
+
* A grantor-signed manifest claiming the structural target — every active
|
|
687
|
+
* key this grantor can itself distribute (`store.listDistributableKeys`) —
|
|
688
|
+
* independent of which of those this particular call's per-key grants
|
|
689
|
+
* actually succeeded on. The recipient checks its store against it to prove
|
|
690
|
+
* it holds the whole set, not just what one batch call happened to land.
|
|
691
|
+
*/
|
|
692
|
+
manifest: string;
|
|
693
|
+
/** The provisioning epoch this manifest was allocated and signed under. */
|
|
694
|
+
epoch: number;
|
|
695
|
+
};
|
|
573
696
|
/** The read access-rule fields a `setCircleSync` add/remove writes for a model. */
|
|
574
697
|
export type SetReadDefaultParams = {
|
|
575
698
|
modelID: string;
|
|
@@ -718,6 +841,47 @@ export type PeerRequestContext = {
|
|
|
718
841
|
* the admin opened or closed since the join.
|
|
719
842
|
*/
|
|
720
843
|
listCircleSyncStates: (groupID: string) => Promise<Array<CircleSyncStateData>>;
|
|
844
|
+
/**
|
|
845
|
+
* Grant every one of `ownerDID`'s active credential keys to `recipientDID` in
|
|
846
|
+
* one non-throwing batch (each key's failure is its own outcome, never
|
|
847
|
+
* aborting the rest), then return a grantor-signed manifest over what landed.
|
|
848
|
+
*/
|
|
849
|
+
grantDeviceCredentials: (params: GrantDeviceCredentialsParams) => Promise<GrantDeviceCredentialsData>;
|
|
850
|
+
/**
|
|
851
|
+
* Prove this device holds the complete expected credential set for
|
|
852
|
+
* `ownerDID` — the union of keys named across every per-grantor manifest.
|
|
853
|
+
* A key counts materialized only when its wrapping decrypt-verifies (or,
|
|
854
|
+
* for a zero-entry key, is merely openable); presence and a valid
|
|
855
|
+
* signature chain alone are not enough.
|
|
856
|
+
*/
|
|
857
|
+
credentialProvisioningStatus: (params: CredentialProvisioningStatusParams) => Promise<CredentialProvisioningStatusData>;
|
|
858
|
+
/**
|
|
859
|
+
* Begin tracking a provisioning attempt for `(ownerDID, grantorDID)`: mints
|
|
860
|
+
* an `attemptId` and forces `credentialProvisioningStatus` to `pending`
|
|
861
|
+
* until recorded/abandoned. Call BEFORE asking the grantor to grant, to
|
|
862
|
+
* close the epoch-allocation race window.
|
|
863
|
+
*/
|
|
864
|
+
beginProvisioningExpectation: (params: BeginProvisioningExpectationParams) => Promise<BeginProvisioningExpectationData>;
|
|
865
|
+
/**
|
|
866
|
+
* Record the outcome of a provisioning attempt via an atomic
|
|
867
|
+
* compare-and-record against this device's currently-held epoch. If the
|
|
868
|
+
* attempt's epoch still exceeds it, the floor advances (`'recorded'`);
|
|
869
|
+
* otherwise a higher manifest already arrived and the attempt stays
|
|
870
|
+
* outstanding (`'retry'`, `minEpoch`) — retry the grant and call again with
|
|
871
|
+
* the same `attemptId`.
|
|
872
|
+
*/
|
|
873
|
+
recordProvisioningExpectation: (params: RecordProvisioningExpectationParams) => Promise<RecordProvisioningExpectationData>;
|
|
874
|
+
/**
|
|
875
|
+
* Stop tracking a dead/abandoned provisioning attempt: removes only this
|
|
876
|
+
* `attemptId` from the outstanding set, never touches the epoch floor.
|
|
877
|
+
*/
|
|
878
|
+
abandonProvisioningExpectation: (params: AbandonProvisioningExpectationParams) => Promise<void>;
|
|
879
|
+
/**
|
|
880
|
+
* This device's currently-held epoch for `(ownerDID, grantorDID)` — max
|
|
881
|
+
* epoch among held manifests from that grantor, or `0`. Feeds `minEpoch`
|
|
882
|
+
* for `grantDeviceCredentials` so a new attempt's epoch exceeds it.
|
|
883
|
+
*/
|
|
884
|
+
provisioningHeldEpoch: (params: ProvisioningHeldEpochParams) => Promise<number>;
|
|
721
885
|
};
|
|
722
886
|
export type P2PJoinRequestContext = {
|
|
723
887
|
prepareRequest: () => Promise<{
|
|
@@ -968,6 +1132,14 @@ export type SyncPluginAPI = {
|
|
|
968
1132
|
* no-op (all-zero summary) when no `hub` option is configured.
|
|
969
1133
|
*/
|
|
970
1134
|
requestLedgerCatchup(groupID: string, options?: LedgerCatchupOptions): Promise<LedgerCatchupSummary>;
|
|
1135
|
+
/**
|
|
1136
|
+
* Build the transport for one directed service-lane session to a
|
|
1137
|
+
* co-member, relayed by the group's hub — the service-lane mirror of the
|
|
1138
|
+
* sync tunnel's route resolver. `undefined` when the group has no hub
|
|
1139
|
+
* bound. A route only: it grants no access to what the far end serves;
|
|
1140
|
+
* that's decided by the far end's own `serviceTunnel` config.
|
|
1141
|
+
*/
|
|
1142
|
+
serviceTransportTo(groupID: string, peerDID: string): Promise<ClientTransportOf<ServiceProtocol> | undefined>;
|
|
971
1143
|
/**
|
|
972
1144
|
* Declare what this device IS — its display label, its availability class and
|
|
973
1145
|
* the capabilities it answers — and advertise it to every group it belongs to.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/plugin-p2p",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"license": "see LICENSE.md",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
@@ -14,72 +14,73 @@
|
|
|
14
14
|
"LICENSE.md"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@enkaku/client": "^0.21.
|
|
17
|
+
"@enkaku/client": "^0.21.2",
|
|
18
18
|
"@enkaku/http-fetch": "^0.21.0",
|
|
19
19
|
"@enkaku/http-serve": "^0.21.0",
|
|
20
|
-
"@enkaku/protocol": "^0.21.
|
|
20
|
+
"@enkaku/protocol": "^0.21.1",
|
|
21
21
|
"@enkaku/server": "^0.21.2",
|
|
22
22
|
"@enkaku/transport": "^0.21.0",
|
|
23
23
|
"@kokuin/capability": "^0.3.0",
|
|
24
24
|
"@kokuin/controller": "^0.1.0",
|
|
25
25
|
"@kokuin/token": "^0.5.0",
|
|
26
|
-
"@kubun/credential": "^0.
|
|
27
|
-
"@kubun/db": "^0.
|
|
28
|
-
"@kubun/db-adapter": "^0.
|
|
29
|
-
"@kubun/engine": "^0.
|
|
30
|
-
"@kubun/graphql": "^0.
|
|
31
|
-
"@kubun/hlc": "^0.
|
|
32
|
-
"@kubun/http-util": "^0.
|
|
33
|
-
"@kubun/id": "^0.
|
|
34
|
-
"@kubun/logger": "^0.
|
|
35
|
-
"@kubun/mutation": "^0.
|
|
36
|
-
"@kubun/plugin-blob-api": "^0.
|
|
37
|
-
"@kubun/plugin-http": "^0.
|
|
38
|
-
"@kubun/plugin-http-api": "^0.
|
|
39
|
-
"@kubun/plugin-
|
|
40
|
-
"@kubun/
|
|
41
|
-
"@kubun/
|
|
42
|
-
"@kubun/store-
|
|
43
|
-
"@kubun/store-
|
|
44
|
-
"@kubun/store-
|
|
45
|
-
"@kubun/store-
|
|
46
|
-
"@kubun/store-
|
|
47
|
-
"@
|
|
48
|
-
"@kumiai/
|
|
49
|
-
"@kumiai/hub-
|
|
50
|
-
"@kumiai/hub-
|
|
51
|
-
"@kumiai/
|
|
52
|
-
"@kumiai/mls
|
|
53
|
-
"@kumiai/rpc": "^0.
|
|
54
|
-
"@
|
|
55
|
-
"@noble/
|
|
26
|
+
"@kubun/credential": "^0.15.0",
|
|
27
|
+
"@kubun/db": "^0.15.0",
|
|
28
|
+
"@kubun/db-adapter": "^0.15.0",
|
|
29
|
+
"@kubun/engine": "^0.15.0",
|
|
30
|
+
"@kubun/graphql": "^0.15.0",
|
|
31
|
+
"@kubun/hlc": "^0.15.0",
|
|
32
|
+
"@kubun/http-util": "^0.15.0",
|
|
33
|
+
"@kubun/id": "^0.15.0",
|
|
34
|
+
"@kubun/logger": "^0.15.0",
|
|
35
|
+
"@kubun/mutation": "^0.15.0",
|
|
36
|
+
"@kubun/plugin-blob-api": "^0.15.0",
|
|
37
|
+
"@kubun/plugin-http": "^0.15.0",
|
|
38
|
+
"@kubun/plugin-http-api": "^0.15.0",
|
|
39
|
+
"@kubun/plugin-service-api": "^0.15.1",
|
|
40
|
+
"@kubun/plugin-workflow-api": "^0.15.0",
|
|
41
|
+
"@kubun/protocol": "^0.15.0",
|
|
42
|
+
"@kubun/store-blob": "^0.15.0",
|
|
43
|
+
"@kubun/store-controller": "^0.15.0",
|
|
44
|
+
"@kubun/store-credential": "^0.15.0",
|
|
45
|
+
"@kubun/store-delegation": "^0.15.0",
|
|
46
|
+
"@kubun/store-graph": "^0.15.0",
|
|
47
|
+
"@kubun/store-p2p": "^0.15.0",
|
|
48
|
+
"@kumiai/broadcast": "^0.8.0",
|
|
49
|
+
"@kumiai/hub-protocol": "^0.8.0",
|
|
50
|
+
"@kumiai/hub-server": "^0.8.0",
|
|
51
|
+
"@kumiai/hub-tunnel": "^0.8.0",
|
|
52
|
+
"@kumiai/mls": "^0.8.0",
|
|
53
|
+
"@kumiai/mls-rpc": "^0.8.0",
|
|
54
|
+
"@kumiai/rpc": "^0.8.0",
|
|
55
|
+
"@noble/ciphers": "^2.4.0",
|
|
56
|
+
"@noble/hashes": "^2.4.0",
|
|
56
57
|
"@sozai/async": "^0.2.1",
|
|
57
58
|
"@sozai/codec": "^0.4.0",
|
|
58
59
|
"@sozai/event": "^0.1.3",
|
|
59
60
|
"@sozai/generator": "^0.2.0",
|
|
60
61
|
"@sozai/runtime": "^0.1.0",
|
|
61
|
-
"@sozai/schema": "^0.1.
|
|
62
|
+
"@sozai/schema": "^0.1.2",
|
|
62
63
|
"@sozai/stream": "^0.2.0",
|
|
63
64
|
"graphql": "^16.14.2",
|
|
64
65
|
"kysely": "^0.29.5",
|
|
65
66
|
"ts-mls": "2.0.0-rc.13"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
|
-
"@kubun/blob-backend": "^0.
|
|
69
|
-
"@kubun/client": "^0.
|
|
70
|
-
"@kubun/db-better-sqlite": "^0.
|
|
71
|
-
"@kubun/db-node-sqlite": "^0.
|
|
72
|
-
"@kubun/db-postgres": "^0.
|
|
73
|
-
"@kubun/hub": "^0.
|
|
74
|
-
"@kubun/plugin-blob": "^0.
|
|
75
|
-
"@kubun/plugin-connector": "^0.
|
|
76
|
-
"@kubun/plugin-service-server": "^0.
|
|
77
|
-
"@kubun/plugin-workflow": "^0.
|
|
78
|
-
"@kubun/service-graph-api": "^0.
|
|
79
|
-
"@kubun/store-workflow": "^0.
|
|
69
|
+
"@kubun/blob-backend": "^0.15.0",
|
|
70
|
+
"@kubun/client": "^0.15.0",
|
|
71
|
+
"@kubun/db-better-sqlite": "^0.15.0",
|
|
72
|
+
"@kubun/db-node-sqlite": "^0.15.0",
|
|
73
|
+
"@kubun/db-postgres": "^0.15.0",
|
|
74
|
+
"@kubun/hub": "^0.15.0",
|
|
75
|
+
"@kubun/plugin-blob": "^0.15.0",
|
|
76
|
+
"@kubun/plugin-connector": "^0.15.0",
|
|
77
|
+
"@kubun/plugin-service-server": "^0.15.1",
|
|
78
|
+
"@kubun/plugin-workflow": "^0.15.0",
|
|
79
|
+
"@kubun/service-graph-api": "^0.15.0",
|
|
80
|
+
"@kubun/store-workflow": "^0.15.0",
|
|
80
81
|
"@kubun/test-utils": "^0.13.0",
|
|
81
|
-
"@kumiai/hub-conformance": "^0.
|
|
82
|
-
"@kumiai/rpc-conformance": "^0.
|
|
82
|
+
"@kumiai/hub-conformance": "^0.8.0",
|
|
83
|
+
"@kumiai/rpc-conformance": "^0.8.0",
|
|
83
84
|
"@testcontainers/postgresql": "^12.1.0",
|
|
84
85
|
"get-port": "^7.2.0"
|
|
85
86
|
},
|