@kubun/plugin-p2p 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/context/group.js +3 -0
- package/lib/context/join.js +3 -0
- package/lib/context/peer.js +7 -2
- package/lib/context/sync.js +63 -5
- package/lib/context/types.d.ts +14 -1
- package/lib/context/types.js +12 -0
- package/lib/groups/access-default-apply.d.ts +42 -0
- package/lib/groups/access-default-apply.js +75 -0
- package/lib/groups/broadcast-codec.d.ts +1 -1
- package/lib/groups/broadcast-message.d.ts +240 -0
- package/lib/groups/broadcast-message.js +1 -0
- package/lib/groups/broadcast.d.ts +19 -266
- package/lib/groups/broadcast.js +30 -204
- package/lib/groups/credential-apply.d.ts +82 -0
- package/lib/groups/credential-apply.js +410 -0
- package/lib/groups/credential-grant-token.d.ts +42 -0
- package/lib/groups/credential-grant-token.js +51 -0
- package/lib/groups/credential-grant.d.ts +98 -0
- package/lib/groups/credential-grant.js +165 -0
- package/lib/groups/group-handlers.js +25 -2
- package/lib/groups/group-mls.d.ts +5 -0
- package/lib/groups/group-mls.js +4 -1
- package/lib/groups/group-peer-manager.d.ts +7 -1
- package/lib/groups/group-peer-manager.js +44 -1
- package/lib/groups/group-protocols.d.ts +227 -0
- package/lib/groups/group-protocols.js +146 -0
- package/lib/groups/join-utils.d.ts +5 -0
- package/lib/groups/join-utils.js +5 -1
- package/lib/groups/ledger-adopt.d.ts +84 -0
- package/lib/groups/ledger-adopt.js +142 -0
- package/lib/groups/ledger-commit-fold.d.ts +3 -1
- package/lib/groups/ledger-commit-fold.js +3 -0
- package/lib/groups/ledger-ingest.d.ts +23 -1
- package/lib/groups/ledger-ingest.js +30 -1
- package/lib/groups/manager.d.ts +5 -0
- package/lib/groups/manager.js +6 -1
- package/lib/groups/peer-presence.d.ts +9 -2
- package/lib/groups/peer-presence.js +14 -2
- package/lib/groups/peer-selection.d.ts +9 -0
- package/lib/groups/peer-selection.js +10 -0
- package/lib/hub/wiring.d.ts +6 -1
- package/lib/hub/wiring.js +2 -1
- package/lib/index.d.ts +5 -1
- package/lib/index.js +58 -6
- package/lib/peer/blob-fetch.d.ts +45 -0
- package/lib/peer/blob-fetch.js +89 -0
- package/lib/peer/blob-handlers.d.ts +11 -0
- package/lib/peer/blob-handlers.js +123 -0
- package/lib/peer/controller-fetch.d.ts +6 -0
- package/lib/peer/controller-fetch.js +59 -0
- package/lib/peer/controller-handlers.d.ts +10 -0
- package/lib/peer/controller-handlers.js +28 -0
- package/lib/protocol.d.ts +279 -0
- package/lib/protocol.js +358 -0
- package/lib/sync/access-default-sender.d.ts +1 -1
- package/lib/sync/broadcast-queue.d.ts +1 -1
- package/lib/sync/forwarder.d.ts +1 -1
- package/lib/sync/handlers.js +98 -1
- package/lib/sync/sync-manager.d.ts +10 -1
- package/lib/sync/sync-manager.js +24 -2
- package/lib/types.d.ts +18 -0
- package/package.json +55 -43
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { normalizeDID } from '@kokuin/token';
|
|
2
|
+
import { toB64U } from '@sozai/codec';
|
|
3
|
+
import { credentialEntriesDigest, credentialKeyBranchesDigest, credentialWrappingDigest, signCredentialKeyGrant } from './credential-grant-token.js';
|
|
4
|
+
/**
|
|
5
|
+
* The recipient of a grant holds no leaf in the group, so there is no
|
|
6
|
+
* authenticated document to wrap to.
|
|
7
|
+
*
|
|
8
|
+
* Named rather than left to fail at wrap time: an unresolved recipient reaches
|
|
9
|
+
* `deriveSharedSecret` as whatever string the caller passed, and comes back as a
|
|
10
|
+
* DID-shaped complaint about a form — which reads as a bad argument when what
|
|
11
|
+
* happened is that the group has no such member.
|
|
12
|
+
*/ export class CredentialRecipientNotInGroup extends Error {
|
|
13
|
+
#groupID;
|
|
14
|
+
#recipientDID;
|
|
15
|
+
constructor(groupID, recipientDID){
|
|
16
|
+
super(`Group ${groupID} has no member ${recipientDID} to grant a credential key to`);
|
|
17
|
+
this.name = 'CredentialRecipientNotInGroup';
|
|
18
|
+
this.#groupID = groupID;
|
|
19
|
+
this.#recipientDID = recipientDID;
|
|
20
|
+
}
|
|
21
|
+
get groupID() {
|
|
22
|
+
return this.#groupID;
|
|
23
|
+
}
|
|
24
|
+
get recipientDID() {
|
|
25
|
+
return this.#recipientDID;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The rows a grant wrote are gone by the time the frame is assembled.
|
|
30
|
+
*
|
|
31
|
+
* Reachable: a rotation landing between the grant's version read and this read
|
|
32
|
+
* back leaves the new wrapping below the current version, so it is not among the
|
|
33
|
+
* ones read here. Refusing is what stops a frame going out that names a version
|
|
34
|
+
* with no way in.
|
|
35
|
+
*/ export class CredentialGrantNotReadable extends Error {
|
|
36
|
+
#keyID;
|
|
37
|
+
constructor(keyID, detail){
|
|
38
|
+
super(`Credential key ${keyID} cannot be read back for a grant frame: ${detail}`);
|
|
39
|
+
this.name = 'CredentialGrantNotReadable';
|
|
40
|
+
this.#keyID = keyID;
|
|
41
|
+
}
|
|
42
|
+
get keyID() {
|
|
43
|
+
return this.#keyID;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Grant a credential key to a co-member, resolving what to wrap to from the MLS
|
|
48
|
+
* roster, and broadcast the frame that carries it there.
|
|
49
|
+
*
|
|
50
|
+
* The leaf credential is the authenticated original — signed, and immutable for
|
|
51
|
+
* as long as the leaf exists — so it is the only copy of a member's resolvable
|
|
52
|
+
* DID that cannot have been rewritten. The membership row cannot answer: its
|
|
53
|
+
* `member_did` is normalized, and for `did:peer:4` that is the short form, which
|
|
54
|
+
* carries no document and so no agreement key.
|
|
55
|
+
*
|
|
56
|
+
* The write happens OUTSIDE the handle lock. Resolving is a pure read of the
|
|
57
|
+
* ratchet tree, while the grant is a store call, and holding a group's mutex
|
|
58
|
+
* across one is the deadlock this repo has already paid for once.
|
|
59
|
+
*/ export async function grantCredentialKeyToMember(params) {
|
|
60
|
+
const { registry, credentials, identity, store, groupID, keyID, recipientDID, stores } = params;
|
|
61
|
+
const wrappableDID = await registry.readHandle(groupID, (handle)=>handle.findMemberLongForm(recipientDID), {
|
|
62
|
+
stores
|
|
63
|
+
});
|
|
64
|
+
if (wrappableDID == null) {
|
|
65
|
+
throw new CredentialRecipientNotInGroup(groupID, recipientDID);
|
|
66
|
+
}
|
|
67
|
+
// `methods` stays undefined here — a resolved roster long form needs none — and
|
|
68
|
+
// the per-request authority overrides the manager's constructed default.
|
|
69
|
+
const result = await credentials.grant(keyID, wrappableDID, {
|
|
70
|
+
authority: params.authority
|
|
71
|
+
});
|
|
72
|
+
// Read back NOW and dispatch on commit, not the reverse. The read has to run
|
|
73
|
+
// on the caller's connection while the transaction is still open, and it has
|
|
74
|
+
// to see the state the grant left rather than whatever a later write leaves —
|
|
75
|
+
// an entry written after this point belongs to its own `credential:entry-put`.
|
|
76
|
+
const frame = await readGrantFrame(store, identity, keyID, wrappableDID);
|
|
77
|
+
stores.onCommit(()=>params.scheduleBroadcast(groupID, frame));
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Rebuild the frame from what the grant persisted, rather than from what the
|
|
82
|
+
* manager held mid-call.
|
|
83
|
+
*
|
|
84
|
+
* The stored rows are the only copy that survives the call, and they are what a
|
|
85
|
+
* receiver has to end up with — so assembling from anything else would let the
|
|
86
|
+
* frame and the granter's own store disagree without either being wrong.
|
|
87
|
+
*/ async function readGrantFrame(store, identity, keyID, recipientWrappableDID) {
|
|
88
|
+
const bundle = await readCredentialKeyBundle(store, keyID, recipientWrappableDID);
|
|
89
|
+
return {
|
|
90
|
+
type: 'credential:key-grant',
|
|
91
|
+
...bundle,
|
|
92
|
+
// Digests over the carried bytes, not a re-read: the token has to describe
|
|
93
|
+
// the frame that ships, so anything computed from a second read could
|
|
94
|
+
// authenticate something other than what travels.
|
|
95
|
+
auth: await signCredentialKeyGrant(identity, {
|
|
96
|
+
keyID: bundle.keyID,
|
|
97
|
+
keyVersion: bundle.keyVersion,
|
|
98
|
+
suite: bundle.suite,
|
|
99
|
+
recipientDID: bundle.wrapping.recipientDID,
|
|
100
|
+
wrappingDigest: credentialWrappingDigest(bundle.wrapping),
|
|
101
|
+
entriesDigest: credentialEntriesDigest(bundle.entries),
|
|
102
|
+
keyBranchesDigest: credentialKeyBranchesDigest(bundle.keyBranches)
|
|
103
|
+
})
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Read one key's row bytes back for a recipient, as a grant carries them minus
|
|
108
|
+
* the `auth` envelope. Shared by the grant path and the credential reconcile
|
|
109
|
+
* lane, which both have to hand a receiver exactly the granter's stored rows.
|
|
110
|
+
*
|
|
111
|
+
* Throws {@link CredentialGrantNotReadable} on a granter-side gap — a missing
|
|
112
|
+
* key row, or a wrapping a rotation removed between the version read and this
|
|
113
|
+
* read back — so a caller never ships a bundle that names bytes with no way in.
|
|
114
|
+
*/ export async function readCredentialKeyBundle(store, keyID, recipientWrappableDID) {
|
|
115
|
+
/**
|
|
116
|
+
* A row with no op is a row nothing vouches for, and shipping it would ask the
|
|
117
|
+
* receiver to invent an author. Granter-side failure, like a missing wrapping.
|
|
118
|
+
*/ const readOp = async (kind, subjectID)=>{
|
|
119
|
+
const op = await store.getOp(kind, subjectID);
|
|
120
|
+
if (op == null || op.deleted !== 0) {
|
|
121
|
+
throw new CredentialGrantNotReadable(keyID, `no live op for ${kind} ${subjectID}`);
|
|
122
|
+
}
|
|
123
|
+
return op.op_jwt;
|
|
124
|
+
};
|
|
125
|
+
const key = await store.getKey(keyID);
|
|
126
|
+
if (key == null) {
|
|
127
|
+
throw new CredentialGrantNotReadable(keyID, 'no key row');
|
|
128
|
+
}
|
|
129
|
+
const recipient = normalizeDID(recipientWrappableDID);
|
|
130
|
+
const wrappings = await store.listWrappings(keyID, key.version);
|
|
131
|
+
const wrapping = wrappings.find((candidate)=>candidate.recipient_did != null && normalizeDID(candidate.recipient_did) === recipient);
|
|
132
|
+
const wrappingRecipient = wrapping?.recipient_did;
|
|
133
|
+
if (wrapping == null || wrappingRecipient == null) {
|
|
134
|
+
throw new CredentialGrantNotReadable(keyID, `no wrapping for ${recipient} at v${key.version}`);
|
|
135
|
+
}
|
|
136
|
+
const entries = await store.listEntries(keyID);
|
|
137
|
+
const carriedWrapping = {
|
|
138
|
+
wrappingID: wrapping.wrapping_id,
|
|
139
|
+
factors: wrapping.factors,
|
|
140
|
+
iv: toB64U(wrapping.iv),
|
|
141
|
+
wrappedKey: toB64U(wrapping.wrapped_key),
|
|
142
|
+
recipientDID: wrappingRecipient,
|
|
143
|
+
op: await readOp('wrapping', wrapping.wrapping_id)
|
|
144
|
+
};
|
|
145
|
+
// Only the granted version. An entry under a retired one is unreadable with
|
|
146
|
+
// this wrapping, and shipping it would be ciphertext the recipient is
|
|
147
|
+
// structurally unable to open.
|
|
148
|
+
const carriedEntries = await Promise.all(entries.filter((entry)=>entry.key_version === key.version).map(async (entry)=>({
|
|
149
|
+
entryID: entry.entry_id,
|
|
150
|
+
iv: toB64U(entry.iv),
|
|
151
|
+
ciphertext: toB64U(entry.ciphertext),
|
|
152
|
+
hlc: entry.hlc,
|
|
153
|
+
op: await readOp('entry', entry.entry_id)
|
|
154
|
+
})));
|
|
155
|
+
return {
|
|
156
|
+
keyID,
|
|
157
|
+
keyVersion: key.version,
|
|
158
|
+
suite: key.suite,
|
|
159
|
+
ownerDID: key.owner_did,
|
|
160
|
+
keyOp: await readOp('key', keyID),
|
|
161
|
+
keyBranches: (await store.listKeyBranches(keyID, key.version)).map((branch)=>branch.op_jwt),
|
|
162
|
+
wrapping: carriedWrapping,
|
|
163
|
+
entries: carriedEntries
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -7,8 +7,8 @@ import { processBroadcast } from './broadcast.js';
|
|
|
7
7
|
* broadcast responder writes a reply for whatever a handler returns, and the
|
|
8
8
|
* requester's gather drops replies that carried an error and keeps the rest. So a
|
|
9
9
|
* decline bounds the RESULT, not the traffic — every member still publishes one
|
|
10
|
-
* reply frame — and
|
|
11
|
-
*
|
|
10
|
+
* reply frame — and "only matching devices reply" is true of what a gather
|
|
11
|
+
* reports, not of what crosses the hub.
|
|
12
12
|
*/ class PeerQueryDeclined extends Error {
|
|
13
13
|
constructor(reason){
|
|
14
14
|
super(`peer/query declined: ${reason}`);
|
|
@@ -100,6 +100,29 @@ import { processBroadcast } from './broadcast.js';
|
|
|
100
100
|
hlc: data.hlc,
|
|
101
101
|
auth: data.auth
|
|
102
102
|
}, groupID)),
|
|
103
|
+
// Carried back verbatim, for the same reason the access-default frames
|
|
104
|
+
// are: the token covers digests over these exact bytes, so a handler that
|
|
105
|
+
// filled a field in from context would hand `processBroadcast` a frame its
|
|
106
|
+
// own signature no longer describes.
|
|
107
|
+
'control/credentialKeyGrant': ({ data })=>safeApply(()=>processBroadcast(params, {
|
|
108
|
+
type: 'credential:key-grant',
|
|
109
|
+
keyID: data.keyID,
|
|
110
|
+
keyVersion: data.keyVersion,
|
|
111
|
+
suite: data.suite,
|
|
112
|
+
ownerDID: data.ownerDID,
|
|
113
|
+
keyOp: data.keyOp,
|
|
114
|
+
keyBranches: data.keyBranches,
|
|
115
|
+
wrapping: {
|
|
116
|
+
wrappingID: data.wrapping.wrappingID,
|
|
117
|
+
factors: data.wrapping.factors,
|
|
118
|
+
iv: data.wrapping.iv,
|
|
119
|
+
wrappedKey: data.wrapping.wrappedKey,
|
|
120
|
+
recipientDID: data.wrapping.recipientDID,
|
|
121
|
+
op: data.wrapping.op
|
|
122
|
+
},
|
|
123
|
+
entries: data.entries,
|
|
124
|
+
auth: data.auth
|
|
125
|
+
}, groupID)),
|
|
103
126
|
'control/groupLeaveRequest': ({ data })=>safeApply(()=>processBroadcast(params, {
|
|
104
127
|
type: 'group:leaveRequest',
|
|
105
128
|
groupID,
|
|
@@ -36,6 +36,11 @@ export type CreateGroupMLSParams = {
|
|
|
36
36
|
* timestamps (the second would silently lose under last-writer-wins).
|
|
37
37
|
*/
|
|
38
38
|
hlc: HLC;
|
|
39
|
+
/**
|
|
40
|
+
* How far into the future a folded control entry's `ord` may sit before its
|
|
41
|
+
* clock merge is refused — see `LedgerIngestParams.maxDriftMS`.
|
|
42
|
+
*/
|
|
43
|
+
maxDriftMS?: number;
|
|
39
44
|
groupID: string;
|
|
40
45
|
/**
|
|
41
46
|
* Emitter for the domain events a landed commit's control entries drive
|
package/lib/groups/group-mls.js
CHANGED
|
@@ -5,8 +5,8 @@ import { getP2PStore } from '@kubun/store-p2p';
|
|
|
5
5
|
import { CommitRejectedError, createRecoveryRequest, joinGroupExternal, MissingLedgerEntriesError, openSealedGroupInfo, openSealedLedger, readGroupAnchor, readMessageEpoch, sealGroupInfo, sealLedger } from '@kumiai/mls';
|
|
6
6
|
import { fromB64 } from '@sozai/codec';
|
|
7
7
|
import { toISO } from '../context/types.js';
|
|
8
|
-
import { bootstrapGroupLedger } from './broadcast.js';
|
|
9
8
|
import { readRecoverySecret } from './group-anchor.js';
|
|
9
|
+
import { bootstrapGroupLedger } from './ledger-adopt.js';
|
|
10
10
|
import { emitLedgerAffectedEvents } from './ledger-affected-events.js';
|
|
11
11
|
import { entriesRetainedByCommit, foldCommittedLedgerEntries } from './ledger-commit-fold.js';
|
|
12
12
|
import { DecryptError } from './mls-receive-errors.js';
|
|
@@ -383,6 +383,9 @@ import { mirrorRosterRoles, roleFromRoster } from './roster-projection.js';
|
|
|
383
383
|
p2pStore,
|
|
384
384
|
groupID,
|
|
385
385
|
hlc,
|
|
386
|
+
...params.maxDriftMS != null ? {
|
|
387
|
+
maxDriftMS: params.maxDriftMS
|
|
388
|
+
} : {},
|
|
386
389
|
logger
|
|
387
390
|
});
|
|
388
391
|
return {
|
|
@@ -12,7 +12,7 @@ import { type DeviceHub, type ReconnectingDeviceHub } from '../hub/hub-like.js';
|
|
|
12
12
|
import type { SyncProtocol } from '../protocol.js';
|
|
13
13
|
import type { ForwardingConfig } from '../sync/forwarder.js';
|
|
14
14
|
import type { LedgerCatchupOptions, LedgerCatchupSummary, RejoinResult, StoreUnreadableMode } from '../types.js';
|
|
15
|
-
import type { GroupBroadcastMessage } from './broadcast.js';
|
|
15
|
+
import type { GroupBroadcastMessage } from './broadcast-message.js';
|
|
16
16
|
import { type SettleLostControlRequestDeps } from './commit-adoption.js';
|
|
17
17
|
import type { P2PEventEmitter } from './events.js';
|
|
18
18
|
import type { GroupHandleRegistry } from './group-handle-registry.js';
|
|
@@ -60,6 +60,12 @@ export type GroupPeerManagerParams = {
|
|
|
60
60
|
graph: GraphInternals;
|
|
61
61
|
/** Device-wide monotonic clock shared with the engine and MLS receive path. */
|
|
62
62
|
hlc: HLC;
|
|
63
|
+
/**
|
|
64
|
+
* The engine's future-drift bound, forwarded to the apply path. Optional: a
|
|
65
|
+
* suite that builds a manager by hand falls back to the same default the
|
|
66
|
+
* engine does, so absence is never an absent bound.
|
|
67
|
+
*/
|
|
68
|
+
maxDriftMS?: number;
|
|
63
69
|
/** Authenticated DID of this device (normalized). */
|
|
64
70
|
localDID: string;
|
|
65
71
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CREDENTIAL_STORE, getCredentialStore } from '@kubun/store-credential';
|
|
1
2
|
import { getDelegationStore } from '@kubun/store-delegation';
|
|
2
3
|
import { getP2PStore } from '@kubun/store-p2p';
|
|
3
4
|
import { readGroupAnchor } from '@kumiai/mls';
|
|
@@ -6,9 +7,10 @@ import { createHubLike } from '../hub/hub-like.js';
|
|
|
6
7
|
import { createLoopbackLogHub } from '../hub/loopback-log-hub.js';
|
|
7
8
|
import { createHubServerDIDResolver } from '../hub/server-did.js';
|
|
8
9
|
import { createTunnelListeners } from '../sync/tunnel-listeners.js';
|
|
10
|
+
import { applyAccessDefaultSetToken } from './access-default-apply.js';
|
|
9
11
|
import { createAnchorStore } from './anchor-store.js';
|
|
10
12
|
import { createAppCursorStore } from './app-cursor-store.js';
|
|
11
|
-
import {
|
|
13
|
+
import { processBroadcast } from './broadcast.js';
|
|
12
14
|
import { reprojectGroupSettings } from './circle-projection.js';
|
|
13
15
|
import { adoptCommitJournalBlob, readJournalRequestID, settleLostControlRequest } from './commit-adoption.js';
|
|
14
16
|
import { createCommitJournal } from './commit-journal.js';
|
|
@@ -18,7 +20,9 @@ import { buildGroupHandlers } from './group-handlers.js';
|
|
|
18
20
|
import { createGroupMLS } from './group-mls.js';
|
|
19
21
|
import { groupProtocols } from './group-protocols.js';
|
|
20
22
|
import { ledgerEntryDigest } from './ledger.js';
|
|
23
|
+
import { bootstrapGroupLedger } from './ledger-adopt.js';
|
|
21
24
|
import { createPeerPresence } from './peer-presence.js';
|
|
25
|
+
import { CREDENTIAL_SYNC_PROTOCOL, CREDENTIAL_SYNC_VERSION } from './peer-selection.js';
|
|
22
26
|
const peerKey = (groupID, hubURL)=>`${groupID}|${hubURL}`;
|
|
23
27
|
/**
|
|
24
28
|
* Send-side union → procedure mapping — the inverse of `buildGroupHandlers`.
|
|
@@ -91,6 +95,23 @@ const peerKey = (groupID, hubURL)=>`${groupID}|${hubURL}`;
|
|
|
91
95
|
type: message.type
|
|
92
96
|
});
|
|
93
97
|
return;
|
|
98
|
+
case 'credential:key-grant':
|
|
99
|
+
// Every field rides, including the `ownerDID` the receiver ignores: the
|
|
100
|
+
// token digests the wrapping and the entries as they are carried, so a
|
|
101
|
+
// payload that dropped or rewrote one produces a frame the receiver
|
|
102
|
+
// verifies and correctly rejects.
|
|
103
|
+
await peer.protocol('control').dispatch('control/credentialKeyGrant', {
|
|
104
|
+
keyID: message.keyID,
|
|
105
|
+
keyVersion: message.keyVersion,
|
|
106
|
+
suite: message.suite,
|
|
107
|
+
ownerDID: message.ownerDID,
|
|
108
|
+
keyOp: message.keyOp,
|
|
109
|
+
keyBranches: message.keyBranches,
|
|
110
|
+
wrapping: message.wrapping,
|
|
111
|
+
entries: message.entries,
|
|
112
|
+
auth: message.auth
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
94
115
|
default:
|
|
95
116
|
{
|
|
96
117
|
// Exhaustiveness gate. A broadcast type with no case here used to fall off
|
|
@@ -440,9 +461,17 @@ export function createGroupPeerManager(params) {
|
|
|
440
461
|
getP2PStore(params.stores),
|
|
441
462
|
getDelegationStore(params.stores)
|
|
442
463
|
]);
|
|
464
|
+
// Registered by `@kubun/plugin-credential`, not here, so its absence is
|
|
465
|
+
// "that plugin is not installed" rather than a fault. Asked through
|
|
466
|
+
// `hasStore` because `getStore` on an unregistered name throws, and a device
|
|
467
|
+
// with no credential plugin must still bind its peers.
|
|
468
|
+
const credentialStore = params.stores.hasStore(CREDENTIAL_STORE) ? await getCredentialStore(params.stores) : undefined;
|
|
443
469
|
return {
|
|
444
470
|
p2pStore,
|
|
445
471
|
delegationStore,
|
|
472
|
+
...credentialStore != null ? {
|
|
473
|
+
credentialStore
|
|
474
|
+
} : {},
|
|
446
475
|
graphStore: params.graphStore,
|
|
447
476
|
graph: params.graph,
|
|
448
477
|
selfDID: params.localDID,
|
|
@@ -455,6 +484,7 @@ export function createGroupPeerManager(params) {
|
|
|
455
484
|
scheduleBroadcast: params.scheduleBroadcast,
|
|
456
485
|
emitter: params.emitter,
|
|
457
486
|
hlc: params.hlc,
|
|
487
|
+
maxDriftMS: params.maxDriftMS,
|
|
458
488
|
// Reads the genesis anchor baked into the MLS GroupContext so role
|
|
459
489
|
// projection on a received ledger entry runs against the authenticated
|
|
460
490
|
// epoch-0 creator. Null for a group with no anchor (e.g. external).
|
|
@@ -547,6 +577,9 @@ export function createGroupPeerManager(params) {
|
|
|
547
577
|
stores: params.stores,
|
|
548
578
|
identity: params.identity,
|
|
549
579
|
hlc: params.hlc,
|
|
580
|
+
...params.maxDriftMS != null ? {
|
|
581
|
+
maxDriftMS: params.maxDriftMS
|
|
582
|
+
} : {},
|
|
550
583
|
groupID,
|
|
551
584
|
// A received commit's control entries fold into this device's
|
|
552
585
|
// projections, so the changes they carry reach subscribers as the same
|
|
@@ -791,6 +824,16 @@ export function createGroupPeerManager(params) {
|
|
|
791
824
|
}
|
|
792
825
|
},
|
|
793
826
|
getGroupEpoch: (groupID)=>params.registry.groupEpoch(groupID) ?? undefined,
|
|
827
|
+
// Read per announce: a credential store registered after the profile was set
|
|
828
|
+
// still makes this device advertise the lane, and a device without one never
|
|
829
|
+
// does — the opt-out is the absence of the store.
|
|
830
|
+
deviceCapabilities: ()=>params.stores.hasStore(CREDENTIAL_STORE) ? [
|
|
831
|
+
{
|
|
832
|
+
protocol: CREDENTIAL_SYNC_PROTOCOL,
|
|
833
|
+
version: CREDENTIAL_SYNC_VERSION,
|
|
834
|
+
transports: null
|
|
835
|
+
}
|
|
836
|
+
] : [],
|
|
794
837
|
publish: broadcastToPeers,
|
|
795
838
|
// Gathered over every hub-peer of the group, concurrently: `timeoutMs` bounds
|
|
796
839
|
// each window, so in sequence a dark hub burns its whole window before the
|
|
@@ -197,6 +197,119 @@ export declare const controlProtocol: {
|
|
|
197
197
|
readonly additionalProperties: false;
|
|
198
198
|
};
|
|
199
199
|
};
|
|
200
|
+
readonly 'control/credentialKeyGrant': {
|
|
201
|
+
readonly type: "event";
|
|
202
|
+
readonly retain: "log";
|
|
203
|
+
readonly description: "Hand one co-member what it needs to open a credential key: the key's public record, the single wrapping addressed to that member, and every entry ciphertext at that version. Group-wide because confidentiality is content-level; only the addressed DID applies it. `auth` signs identifiers and content digests, and the receiver recomputes both from this frame.";
|
|
204
|
+
readonly data: {
|
|
205
|
+
readonly type: "object";
|
|
206
|
+
readonly properties: {
|
|
207
|
+
readonly keyID: {
|
|
208
|
+
readonly type: "string";
|
|
209
|
+
};
|
|
210
|
+
readonly keyVersion: {
|
|
211
|
+
readonly type: "number";
|
|
212
|
+
};
|
|
213
|
+
readonly suite: {
|
|
214
|
+
readonly type: "number";
|
|
215
|
+
};
|
|
216
|
+
readonly ownerDID: {
|
|
217
|
+
readonly type: "string";
|
|
218
|
+
readonly description: "Claimed owner. A mirror — the receiver stores the `auth` issuer instead.";
|
|
219
|
+
};
|
|
220
|
+
readonly keyOp: {
|
|
221
|
+
readonly type: "string";
|
|
222
|
+
readonly description: "The granter's signed op for the key row, stored verbatim. The key row rides inline on this frame, so its op has nowhere else to hang.";
|
|
223
|
+
};
|
|
224
|
+
readonly keyBranches: {
|
|
225
|
+
readonly type: "array";
|
|
226
|
+
readonly items: {
|
|
227
|
+
readonly type: "string";
|
|
228
|
+
};
|
|
229
|
+
readonly description: "The proven key-op JWTs that introduced this key version's branches.";
|
|
230
|
+
};
|
|
231
|
+
readonly wrapping: {
|
|
232
|
+
readonly type: "object";
|
|
233
|
+
readonly properties: {
|
|
234
|
+
readonly wrappingID: {
|
|
235
|
+
readonly type: "string";
|
|
236
|
+
};
|
|
237
|
+
readonly factors: {
|
|
238
|
+
readonly type: "array";
|
|
239
|
+
readonly items: {
|
|
240
|
+
readonly type: "object";
|
|
241
|
+
readonly properties: {
|
|
242
|
+
readonly kind: {
|
|
243
|
+
readonly type: "string";
|
|
244
|
+
};
|
|
245
|
+
readonly params: {
|
|
246
|
+
readonly type: "object";
|
|
247
|
+
readonly description: "Per-kind PUBLIC material — a salt or an ephemeral public key.";
|
|
248
|
+
readonly additionalProperties: true;
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
readonly required: readonly ["kind", "params"];
|
|
252
|
+
readonly additionalProperties: false;
|
|
253
|
+
};
|
|
254
|
+
readonly description: "ORDERED, and bound into the wrapping AAD: a permuted copy will not open.";
|
|
255
|
+
};
|
|
256
|
+
readonly iv: {
|
|
257
|
+
readonly type: "string";
|
|
258
|
+
readonly description: "base64url";
|
|
259
|
+
};
|
|
260
|
+
readonly wrappedKey: {
|
|
261
|
+
readonly type: "string";
|
|
262
|
+
readonly description: "base64url";
|
|
263
|
+
};
|
|
264
|
+
readonly recipientDID: {
|
|
265
|
+
readonly type: "string";
|
|
266
|
+
readonly description: "Un-normalized: a key-resolution input, not an identity to compare.";
|
|
267
|
+
};
|
|
268
|
+
readonly op: {
|
|
269
|
+
readonly type: "string";
|
|
270
|
+
readonly description: "The granter's signed op for this row, stored verbatim — the receiver never mints its own.";
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
readonly required: readonly ["wrappingID", "factors", "iv", "wrappedKey", "recipientDID", "op"];
|
|
274
|
+
readonly additionalProperties: false;
|
|
275
|
+
};
|
|
276
|
+
readonly entries: {
|
|
277
|
+
readonly type: "array";
|
|
278
|
+
readonly items: {
|
|
279
|
+
readonly type: "object";
|
|
280
|
+
readonly properties: {
|
|
281
|
+
readonly entryID: {
|
|
282
|
+
readonly type: "string";
|
|
283
|
+
};
|
|
284
|
+
readonly iv: {
|
|
285
|
+
readonly type: "string";
|
|
286
|
+
readonly description: "base64url";
|
|
287
|
+
};
|
|
288
|
+
readonly ciphertext: {
|
|
289
|
+
readonly type: "string";
|
|
290
|
+
readonly description: "base64url";
|
|
291
|
+
};
|
|
292
|
+
readonly hlc: {
|
|
293
|
+
readonly type: "string";
|
|
294
|
+
};
|
|
295
|
+
readonly op: {
|
|
296
|
+
readonly type: "string";
|
|
297
|
+
readonly description: "The granter's signed op for this row, stored verbatim.";
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
readonly required: readonly ["entryID", "iv", "ciphertext", "hlc", "op"];
|
|
301
|
+
readonly additionalProperties: false;
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
readonly auth: {
|
|
305
|
+
readonly type: "string";
|
|
306
|
+
readonly description: "Signed credential key-grant token.";
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
readonly required: readonly ["keyID", "keyVersion", "suite", "ownerDID", "keyOp", "keyBranches", "wrapping", "entries", "auth"];
|
|
310
|
+
readonly additionalProperties: false;
|
|
311
|
+
};
|
|
312
|
+
};
|
|
200
313
|
readonly 'control/groupLeaveRequest': {
|
|
201
314
|
readonly type: "event";
|
|
202
315
|
readonly retain: "ephemeral";
|
|
@@ -380,6 +493,7 @@ export type DelegationShareData = FromSchema<(typeof controlProtocol)['control/d
|
|
|
380
493
|
export type DelegationRevokeData = FromSchema<(typeof controlProtocol)['control/delegationRevoke']['data']>;
|
|
381
494
|
export type AccessDefaultSetData = FromSchema<(typeof controlProtocol)['control/accessDefaultSet']['data']>;
|
|
382
495
|
export type AccessDefaultRemoveData = FromSchema<(typeof controlProtocol)['control/accessDefaultRemove']['data']>;
|
|
496
|
+
export type CredentialKeyGrantData = FromSchema<(typeof controlProtocol)['control/credentialKeyGrant']['data']>;
|
|
383
497
|
export type GroupLeaveRequestData = FromSchema<(typeof controlProtocol)['control/groupLeaveRequest']['data']>;
|
|
384
498
|
export type MutationApplyData = FromSchema<(typeof syncProtocol)['sync/mutationApply']['data']>;
|
|
385
499
|
export type PeerAnnounceData = FromSchema<(typeof peerProtocol)['peer/announce']['data']>;
|
|
@@ -579,6 +693,119 @@ export declare const groupProtocols: {
|
|
|
579
693
|
readonly additionalProperties: false;
|
|
580
694
|
};
|
|
581
695
|
};
|
|
696
|
+
readonly 'control/credentialKeyGrant': {
|
|
697
|
+
readonly type: "event";
|
|
698
|
+
readonly retain: "log";
|
|
699
|
+
readonly description: "Hand one co-member what it needs to open a credential key: the key's public record, the single wrapping addressed to that member, and every entry ciphertext at that version. Group-wide because confidentiality is content-level; only the addressed DID applies it. `auth` signs identifiers and content digests, and the receiver recomputes both from this frame.";
|
|
700
|
+
readonly data: {
|
|
701
|
+
readonly type: "object";
|
|
702
|
+
readonly properties: {
|
|
703
|
+
readonly keyID: {
|
|
704
|
+
readonly type: "string";
|
|
705
|
+
};
|
|
706
|
+
readonly keyVersion: {
|
|
707
|
+
readonly type: "number";
|
|
708
|
+
};
|
|
709
|
+
readonly suite: {
|
|
710
|
+
readonly type: "number";
|
|
711
|
+
};
|
|
712
|
+
readonly ownerDID: {
|
|
713
|
+
readonly type: "string";
|
|
714
|
+
readonly description: "Claimed owner. A mirror — the receiver stores the `auth` issuer instead.";
|
|
715
|
+
};
|
|
716
|
+
readonly keyOp: {
|
|
717
|
+
readonly type: "string";
|
|
718
|
+
readonly description: "The granter's signed op for the key row, stored verbatim. The key row rides inline on this frame, so its op has nowhere else to hang.";
|
|
719
|
+
};
|
|
720
|
+
readonly keyBranches: {
|
|
721
|
+
readonly type: "array";
|
|
722
|
+
readonly items: {
|
|
723
|
+
readonly type: "string";
|
|
724
|
+
};
|
|
725
|
+
readonly description: "The proven key-op JWTs that introduced this key version's branches.";
|
|
726
|
+
};
|
|
727
|
+
readonly wrapping: {
|
|
728
|
+
readonly type: "object";
|
|
729
|
+
readonly properties: {
|
|
730
|
+
readonly wrappingID: {
|
|
731
|
+
readonly type: "string";
|
|
732
|
+
};
|
|
733
|
+
readonly factors: {
|
|
734
|
+
readonly type: "array";
|
|
735
|
+
readonly items: {
|
|
736
|
+
readonly type: "object";
|
|
737
|
+
readonly properties: {
|
|
738
|
+
readonly kind: {
|
|
739
|
+
readonly type: "string";
|
|
740
|
+
};
|
|
741
|
+
readonly params: {
|
|
742
|
+
readonly type: "object";
|
|
743
|
+
readonly description: "Per-kind PUBLIC material — a salt or an ephemeral public key.";
|
|
744
|
+
readonly additionalProperties: true;
|
|
745
|
+
};
|
|
746
|
+
};
|
|
747
|
+
readonly required: readonly ["kind", "params"];
|
|
748
|
+
readonly additionalProperties: false;
|
|
749
|
+
};
|
|
750
|
+
readonly description: "ORDERED, and bound into the wrapping AAD: a permuted copy will not open.";
|
|
751
|
+
};
|
|
752
|
+
readonly iv: {
|
|
753
|
+
readonly type: "string";
|
|
754
|
+
readonly description: "base64url";
|
|
755
|
+
};
|
|
756
|
+
readonly wrappedKey: {
|
|
757
|
+
readonly type: "string";
|
|
758
|
+
readonly description: "base64url";
|
|
759
|
+
};
|
|
760
|
+
readonly recipientDID: {
|
|
761
|
+
readonly type: "string";
|
|
762
|
+
readonly description: "Un-normalized: a key-resolution input, not an identity to compare.";
|
|
763
|
+
};
|
|
764
|
+
readonly op: {
|
|
765
|
+
readonly type: "string";
|
|
766
|
+
readonly description: "The granter's signed op for this row, stored verbatim — the receiver never mints its own.";
|
|
767
|
+
};
|
|
768
|
+
};
|
|
769
|
+
readonly required: readonly ["wrappingID", "factors", "iv", "wrappedKey", "recipientDID", "op"];
|
|
770
|
+
readonly additionalProperties: false;
|
|
771
|
+
};
|
|
772
|
+
readonly entries: {
|
|
773
|
+
readonly type: "array";
|
|
774
|
+
readonly items: {
|
|
775
|
+
readonly type: "object";
|
|
776
|
+
readonly properties: {
|
|
777
|
+
readonly entryID: {
|
|
778
|
+
readonly type: "string";
|
|
779
|
+
};
|
|
780
|
+
readonly iv: {
|
|
781
|
+
readonly type: "string";
|
|
782
|
+
readonly description: "base64url";
|
|
783
|
+
};
|
|
784
|
+
readonly ciphertext: {
|
|
785
|
+
readonly type: "string";
|
|
786
|
+
readonly description: "base64url";
|
|
787
|
+
};
|
|
788
|
+
readonly hlc: {
|
|
789
|
+
readonly type: "string";
|
|
790
|
+
};
|
|
791
|
+
readonly op: {
|
|
792
|
+
readonly type: "string";
|
|
793
|
+
readonly description: "The granter's signed op for this row, stored verbatim.";
|
|
794
|
+
};
|
|
795
|
+
};
|
|
796
|
+
readonly required: readonly ["entryID", "iv", "ciphertext", "hlc", "op"];
|
|
797
|
+
readonly additionalProperties: false;
|
|
798
|
+
};
|
|
799
|
+
};
|
|
800
|
+
readonly auth: {
|
|
801
|
+
readonly type: "string";
|
|
802
|
+
readonly description: "Signed credential key-grant token.";
|
|
803
|
+
};
|
|
804
|
+
};
|
|
805
|
+
readonly required: readonly ["keyID", "keyVersion", "suite", "ownerDID", "keyOp", "keyBranches", "wrapping", "entries", "auth"];
|
|
806
|
+
readonly additionalProperties: false;
|
|
807
|
+
};
|
|
808
|
+
};
|
|
582
809
|
readonly 'control/groupLeaveRequest': {
|
|
583
810
|
readonly type: "event";
|
|
584
811
|
readonly retain: "ephemeral";
|