@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
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
import type { GroupHandle } from '@kumiai/mls';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export declare function
|
|
2
|
+
/**
|
|
3
|
+
* Encrypt an application message for the group. `@kumiai/mls` returns framed MLS
|
|
4
|
+
* wire bytes, so kubun puts those on the wire directly — no JSON envelope of its
|
|
5
|
+
* own, and no retired secret material crossing the boundary.
|
|
6
|
+
*/
|
|
7
|
+
export declare function mlsEncryptFramed(handle: GroupHandle, plaintext: Uint8Array): Promise<Uint8Array>;
|
|
8
|
+
/**
|
|
9
|
+
* Decrypt a received application message and recover WHO SENT IT.
|
|
10
|
+
*
|
|
11
|
+
* `handle.decrypt` rather than `processMessage`, which returns an application
|
|
12
|
+
* message's plaintext with no sender. The sender is AUTHENTICATED, not claimed:
|
|
13
|
+
* the leaf index rides sender-data encrypted under the epoch's sender-data
|
|
14
|
+
* secret, and the body opens only under the ratchet key derived at that leaf, so
|
|
15
|
+
* a frame naming a leaf it was not sealed at does not open at all.
|
|
16
|
+
*
|
|
17
|
+
* A missing sender is an ERROR, never a filled-in blank. Upstream types
|
|
18
|
+
* `senderDID` optional because the authenticated leaf may hold no parsable
|
|
19
|
+
* credential — "I cannot name the author", never "there is no author". Any
|
|
20
|
+
* placeholder (empty string, a transport-claimed DID) would hand callers an
|
|
21
|
+
* unauthenticated value wearing an authenticated one's type, so the open fails
|
|
22
|
+
* instead. Callers already treat a throw here as ordinary control flow:
|
|
23
|
+
* `decrypt` throws for every epoch but its own.
|
|
24
|
+
*/
|
|
25
|
+
export declare function mlsDecryptFramed(handle: GroupHandle, framed: Uint8Array): Promise<{
|
|
26
|
+
payload: Uint8Array;
|
|
27
|
+
senderDID: string;
|
|
28
|
+
}>;
|
package/lib/groups/mls-codec.js
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export async function mlsEncryptFramed(handle, plaintext) {
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Encrypt an application message for the group. `@kumiai/mls` returns framed MLS
|
|
3
|
+
* wire bytes, so kubun puts those on the wire directly — no JSON envelope of its
|
|
4
|
+
* own, and no retired secret material crossing the boundary.
|
|
5
|
+
*/ export async function mlsEncryptFramed(handle, plaintext) {
|
|
6
|
+
return await handle.encrypt(plaintext);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Decrypt a received application message and recover WHO SENT IT.
|
|
10
|
+
*
|
|
11
|
+
* `handle.decrypt` rather than `processMessage`, which returns an application
|
|
12
|
+
* message's plaintext with no sender. The sender is AUTHENTICATED, not claimed:
|
|
13
|
+
* the leaf index rides sender-data encrypted under the epoch's sender-data
|
|
14
|
+
* secret, and the body opens only under the ratchet key derived at that leaf, so
|
|
15
|
+
* a frame naming a leaf it was not sealed at does not open at all.
|
|
16
|
+
*
|
|
17
|
+
* A missing sender is an ERROR, never a filled-in blank. Upstream types
|
|
18
|
+
* `senderDID` optional because the authenticated leaf may hold no parsable
|
|
19
|
+
* credential — "I cannot name the author", never "there is no author". Any
|
|
20
|
+
* placeholder (empty string, a transport-claimed DID) would hand callers an
|
|
21
|
+
* unauthenticated value wearing an authenticated one's type, so the open fails
|
|
22
|
+
* instead. Callers already treat a throw here as ordinary control flow:
|
|
23
|
+
* `decrypt` throws for every epoch but its own.
|
|
24
|
+
*/ export async function mlsDecryptFramed(handle, framed) {
|
|
25
|
+
const { payload, senderDID } = await handle.decrypt(framed);
|
|
26
|
+
if (senderDID == null) {
|
|
27
|
+
throw new Error('MLS application message opened at a leaf with no nameable sender');
|
|
28
|
+
}
|
|
8
29
|
return {
|
|
9
|
-
|
|
10
|
-
|
|
30
|
+
payload,
|
|
31
|
+
senderDID
|
|
11
32
|
};
|
|
12
33
|
}
|
|
13
|
-
export async function mlsDecryptFramed(handle, framed) {
|
|
14
|
-
const message = JSON.parse(textDecoder.decode(framed), reviver);
|
|
15
|
-
return await handle.decrypt(message, {
|
|
16
|
-
commitPolicy: anchorImmutabilityPolicy
|
|
17
|
-
});
|
|
18
|
-
}
|
|
@@ -19,12 +19,15 @@ import { mlsDecryptFramed, mlsEncryptFramed } from './mls-codec.js';
|
|
|
19
19
|
this.#groupID = params.groupID;
|
|
20
20
|
}
|
|
21
21
|
encrypt(plaintext) {
|
|
22
|
-
return this.#registry.withHandle(this.#groupID,
|
|
23
|
-
const { framed } = await mlsEncryptFramed(handle, plaintext);
|
|
24
|
-
return framed;
|
|
25
|
-
});
|
|
22
|
+
return this.#registry.withHandle(this.#groupID, (handle)=>mlsEncryptFramed(handle, plaintext));
|
|
26
23
|
}
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
// The `Encryptor` port is bytes-in/bytes-out, so the authenticated sender the
|
|
25
|
+
// open also recovers is discarded here. The sync-transport path this serves
|
|
26
|
+
// carries its own per-entry signed tokens and authorizes on those; it has no
|
|
27
|
+
// seam to receive a frame sender through. Dropping it is deliberate — not an
|
|
28
|
+
// oversight to be routed around by widening the port.
|
|
29
|
+
async decrypt(ciphertext) {
|
|
30
|
+
const { payload } = await this.#registry.withHandle(this.#groupID, (handle)=>mlsDecryptFramed(handle, ciphertext));
|
|
31
|
+
return payload;
|
|
29
32
|
}
|
|
30
33
|
}
|
|
@@ -1,4 +1,39 @@
|
|
|
1
|
-
import { type GroupHandle } from '@kumiai/mls';
|
|
1
|
+
import { type GroupHandle, type VerifiedLedgerEntry } from '@kumiai/mls';
|
|
2
2
|
import { type SerializedMLSGroupState } from './mls-state.js';
|
|
3
3
|
export type MLSGroupHandle = GroupHandle;
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Fetch control-ledger entry bodies a handle does not hold, by content id.
|
|
6
|
+
*
|
|
7
|
+
* A commit's envelope names the entries it enacts by content id, not by body, and
|
|
8
|
+
* the ONLY source for a body a receiver lacks is the commit's own frame, wired in
|
|
9
|
+
* for the duration of applying it. The answer is untrusted: only a token whose
|
|
10
|
+
* digest matches the requested id and whose signature verifies is kept.
|
|
11
|
+
*
|
|
12
|
+
* A handle built without a resolver resolves nothing, deliberately: kubun's store
|
|
13
|
+
* writes its copy AFTER apply, so reading it would answer every miss with an empty
|
|
14
|
+
* list — indistinguishable from a resolver that ran. Passing none instead lets
|
|
15
|
+
* `MissingLedgerEntriesError` reach the lane, which drops the frame as poison.
|
|
16
|
+
*/
|
|
17
|
+
export type LedgerEntryResolver = (ids: Array<string>) => Promise<Array<string>>;
|
|
18
|
+
/**
|
|
19
|
+
* Receive the notarized non-`group.role` entries an accepted commit carried, in
|
|
20
|
+
* envelope order. Synchronous by contract, so a sink can only COLLECT: any store
|
|
21
|
+
* write the entries drive must run after `processMessage` returns.
|
|
22
|
+
*/
|
|
23
|
+
export type LedgerEntrySink = (entries: Array<VerifiedLedgerEntry>) => void;
|
|
24
|
+
export type RestoreMLSGroupHandleParams = {
|
|
25
|
+
serialized: SerializedMLSGroupState;
|
|
26
|
+
/**
|
|
27
|
+
* Resolves the entry bodies a received commit's envelope names, from the
|
|
28
|
+
* commit's own frame. Supplied only while the restored handle is applying such
|
|
29
|
+
* a commit.
|
|
30
|
+
*/
|
|
31
|
+
resolveLedgerEntries?: LedgerEntryResolver;
|
|
32
|
+
/**
|
|
33
|
+
* Collects the notarized entries an accepted commit surfaces on this handle,
|
|
34
|
+
* so the caller can fold them into the projections once `processMessage` has
|
|
35
|
+
* returned.
|
|
36
|
+
*/
|
|
37
|
+
onLedgerEntries?: LedgerEntrySink;
|
|
38
|
+
};
|
|
39
|
+
export declare function restoreMLSGroupHandle(params: RestoreMLSGroupHandleParams): Promise<MLSGroupHandle>;
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { restoreGroup } from '@kumiai/mls';
|
|
2
2
|
import { deserializeMLSGroupState } from './mls-state.js';
|
|
3
|
-
export async function restoreMLSGroupHandle(
|
|
4
|
-
const { state, credential,
|
|
5
|
-
return restoreGroup({
|
|
3
|
+
export async function restoreMLSGroupHandle(params) {
|
|
4
|
+
const { state, credential, ledgerEntries } = deserializeMLSGroupState(params.serialized);
|
|
5
|
+
return await restoreGroup({
|
|
6
6
|
state,
|
|
7
7
|
credential,
|
|
8
|
-
|
|
8
|
+
ledgerEntries,
|
|
9
|
+
options: {
|
|
10
|
+
...params.resolveLedgerEntries != null ? {
|
|
11
|
+
resolveLedgerEntries: params.resolveLedgerEntries
|
|
12
|
+
} : {},
|
|
13
|
+
...params.onLedgerEntries != null ? {
|
|
14
|
+
onLedgerEntries: params.onLedgerEntries
|
|
15
|
+
} : {}
|
|
16
|
+
}
|
|
9
17
|
});
|
|
10
18
|
}
|
|
@@ -11,9 +11,7 @@
|
|
|
11
11
|
* ordered receive stream.
|
|
12
12
|
*/
|
|
13
13
|
export declare class DecryptError extends Error {
|
|
14
|
-
#private;
|
|
15
14
|
constructor(message: string, cause: unknown);
|
|
16
|
-
get cause(): unknown;
|
|
17
15
|
}
|
|
18
16
|
/**
|
|
19
17
|
* Thrown when a received MLS Commit targets a future epoch the local handle has
|
|
@@ -9,14 +9,11 @@
|
|
|
9
9
|
* not help, so callers should ack-and-skip rather than wedge the group's
|
|
10
10
|
* ordered receive stream.
|
|
11
11
|
*/ export class DecryptError extends Error {
|
|
12
|
-
#cause;
|
|
13
12
|
constructor(message, cause){
|
|
14
|
-
super(message
|
|
13
|
+
super(message, {
|
|
14
|
+
cause
|
|
15
|
+
});
|
|
15
16
|
this.name = 'DecryptError';
|
|
16
|
-
this.#cause = cause;
|
|
17
|
-
}
|
|
18
|
-
get cause() {
|
|
19
|
-
return this.#cause;
|
|
20
17
|
}
|
|
21
18
|
}
|
|
22
19
|
/**
|
|
@@ -2,13 +2,15 @@ import type { GroupMLSState, InsertGroupMLSState } from '@kubun/store-p2p';
|
|
|
2
2
|
import { type ClientState, type GroupHandle, type MemberCredential } from '@kumiai/mls';
|
|
3
3
|
/**
|
|
4
4
|
* Serialized form of a GroupHandle for DB persistence.
|
|
5
|
-
* The MLS ClientState is opaque binary; the credential
|
|
5
|
+
* The MLS ClientState is opaque binary; the credential and the control-ledger
|
|
6
|
+
* tokens are JSON.
|
|
6
7
|
*/
|
|
7
8
|
export type SerializedMLSGroupState = {
|
|
8
9
|
mlsState: Uint8Array;
|
|
9
10
|
credential: string;
|
|
10
11
|
epoch: number;
|
|
11
|
-
|
|
12
|
+
/** The handle's signed ledger tokens, in enactment order, JSON-encoded. */
|
|
13
|
+
ledgerEntries: string;
|
|
12
14
|
};
|
|
13
15
|
/**
|
|
14
16
|
* Serialize a GroupHandle's state for persistence.
|
|
@@ -19,7 +21,7 @@ export declare function serializeMLSGroupState(group: GroupHandle): SerializedML
|
|
|
19
21
|
export type MLSGroupState = {
|
|
20
22
|
state: ClientState;
|
|
21
23
|
credential: MemberCredential;
|
|
22
|
-
|
|
24
|
+
ledgerEntries: Array<string>;
|
|
23
25
|
};
|
|
24
26
|
/**
|
|
25
27
|
* Deserialize persisted state back into GroupHandle constructor params.
|
package/lib/groups/mls-state.js
CHANGED
|
@@ -9,7 +9,10 @@ import { normalizeCredential } from '../context/types.js';
|
|
|
9
9
|
mlsState: encodeClientState(group.state),
|
|
10
10
|
credential: JSON.stringify(group.credential),
|
|
11
11
|
epoch: Number(group.epoch),
|
|
12
|
-
|
|
12
|
+
// The tokens are the ledger's only export form — the verified entries a
|
|
13
|
+
// handle folds cannot be reconstructed from anything else — so persisting
|
|
14
|
+
// them is what lets a restored handle refold the roster it had.
|
|
15
|
+
ledgerEntries: JSON.stringify(group.ledgerTokens)
|
|
13
16
|
};
|
|
14
17
|
}
|
|
15
18
|
/**
|
|
@@ -23,9 +26,23 @@ import { normalizeCredential } from '../context/types.js';
|
|
|
23
26
|
return {
|
|
24
27
|
state,
|
|
25
28
|
credential: JSON.parse(serialized.credential),
|
|
26
|
-
|
|
29
|
+
ledgerEntries: parseLedgerEntries(serialized.ledgerEntries)
|
|
27
30
|
};
|
|
28
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Ledger tokens are stored as a JSON array of strings. An empty column (a row
|
|
34
|
+
* written before any entry was enacted) parses to an empty ledger, which folds
|
|
35
|
+
* to the anchor-seeded roster — the same state the handle held.
|
|
36
|
+
*/ function parseLedgerEntries(value) {
|
|
37
|
+
if (value === '') {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const parsed = JSON.parse(value);
|
|
41
|
+
if (!Array.isArray(parsed)) {
|
|
42
|
+
throw new Error('Could not decode persisted control-ledger tokens');
|
|
43
|
+
}
|
|
44
|
+
return parsed;
|
|
45
|
+
}
|
|
29
46
|
/**
|
|
30
47
|
* Map a persisted `GroupMLSState` DB row to the `SerializedMLSGroupState` shape
|
|
31
48
|
* expected by `BroadcastService.prepareSend` / `processReceived`.
|
|
@@ -38,9 +55,15 @@ import { normalizeCredential } from '../context/types.js';
|
|
|
38
55
|
mlsState: row.mls_state,
|
|
39
56
|
credential: normalizeCredential(row.credential),
|
|
40
57
|
epoch: row.epoch,
|
|
41
|
-
|
|
58
|
+
ledgerEntries: normalizeLedgerEntriesColumn(row.ledger_entries)
|
|
42
59
|
};
|
|
43
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* The ledger-tokens column is TEXT, but `ParseJSONResultsPlugin` may hand back
|
|
63
|
+
* an already-parsed array — the same asymmetry `normalizeCredential` handles.
|
|
64
|
+
*/ function normalizeLedgerEntriesColumn(value) {
|
|
65
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
66
|
+
}
|
|
44
67
|
/**
|
|
45
68
|
* Map a `SerializedMLSGroupState` plus scope (group + device) to an
|
|
46
69
|
* `InsertGroupMLSState` row suitable for `P2PStoreAPI.saveMLSState`.
|
|
@@ -51,6 +74,6 @@ import { normalizeCredential } from '../context/types.js';
|
|
|
51
74
|
mls_state: serialized.mlsState,
|
|
52
75
|
credential: serialized.credential,
|
|
53
76
|
epoch: serialized.epoch,
|
|
54
|
-
|
|
77
|
+
ledger_entries: serialized.ledgerEntries
|
|
55
78
|
};
|
|
56
79
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { StoreProvider } from '@kubun/db';
|
|
2
|
+
import { HLC } from '@kubun/hlc';
|
|
3
|
+
import type { Logger } from '@kubun/logger';
|
|
4
|
+
import type { GatheredPeer, LocalPeerAnnouncement, PeerGatherOptions, PresenceReason } from '../types.js';
|
|
5
|
+
import type { GroupBroadcastMessage } from './broadcast.js';
|
|
6
|
+
import type { PeerQueryParam } from './group-protocols.js';
|
|
7
|
+
/** One `peer/query` reply as the gather surface hands it back. */
|
|
8
|
+
type RawReply = {
|
|
9
|
+
senderDID: string;
|
|
10
|
+
value: unknown;
|
|
11
|
+
};
|
|
12
|
+
export type PeerPresenceParams = {
|
|
13
|
+
stores: StoreProvider;
|
|
14
|
+
/** This device's authenticated DID. */
|
|
15
|
+
localDID: string;
|
|
16
|
+
/** Device-wide clock. Every announcement is stamped from it and never by a caller. */
|
|
17
|
+
hlc: HLC;
|
|
18
|
+
/** The epoch stamped onto this device's own row, as the apply path stamps a received one. */
|
|
19
|
+
getGroupEpoch: (groupID: string) => number | undefined;
|
|
20
|
+
/** Fan one broadcast out across every live peer of the group. */
|
|
21
|
+
publish: (groupID: string, message: GroupBroadcastMessage) => Promise<void>;
|
|
22
|
+
/** Gather `peer/query` replies from the group's live peers. */
|
|
23
|
+
query: (groupID: string, param: PeerQueryParam, options: PeerGatherOptions) => Promise<Array<RawReply>>;
|
|
24
|
+
/**
|
|
25
|
+
* Fold one reply into the projection through the SAME apply a broadcast frame
|
|
26
|
+
* takes, so a gathered announcement and a pushed one cannot diverge.
|
|
27
|
+
*/
|
|
28
|
+
applyAnnouncement: (groupID: string, message: GroupBroadcastMessage) => Promise<void>;
|
|
29
|
+
/** Every group this device currently belongs to, for a device-wide announce. */
|
|
30
|
+
joinedGroupIDs: () => Array<string>;
|
|
31
|
+
/**
|
|
32
|
+
* Called after this device declares (or redeclares) its profile — the moment
|
|
33
|
+
* it becomes visible, and so the moment a co-member may start dialling it.
|
|
34
|
+
* Whatever answers those dials is stood up here rather than on a timer.
|
|
35
|
+
*/
|
|
36
|
+
onProfileSet?: () => void;
|
|
37
|
+
logger?: Logger;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Everything that makes this device visible to its co-members, and everything
|
|
41
|
+
* that asks who else is.
|
|
42
|
+
*
|
|
43
|
+
* A GATHER IS LIVENESS, NEVER MEMBERSHIP. It reports who answered inside a
|
|
44
|
+
* window; it never deletes a row, and a device that says nothing keeps whatever
|
|
45
|
+
* it last said. Only a roster removal deletes.
|
|
46
|
+
*/
|
|
47
|
+
export type PeerPresence = {
|
|
48
|
+
/** Record what this device is, then announce it to every group it belongs to. */
|
|
49
|
+
setProfile: (profile: LocalPeerAnnouncement) => Promise<void>;
|
|
50
|
+
getProfile: () => Promise<LocalPeerAnnouncement | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Announce this device's stored profile to one group. Reports whether anything
|
|
53
|
+
* was said — a device with no profile advertises nothing rather than inventing
|
|
54
|
+
* a label for itself.
|
|
55
|
+
*/
|
|
56
|
+
announce: (groupID: string) => Promise<boolean>;
|
|
57
|
+
/** Ask who is live. Annotates the projection; never prunes it. */
|
|
58
|
+
gather: (groupID: string, options?: PeerGatherOptions) => Promise<Array<GatheredPeer>>;
|
|
59
|
+
/**
|
|
60
|
+
* Announce, then gather — what "I have just (re)appeared" means, and what every
|
|
61
|
+
* internal trigger runs.
|
|
62
|
+
*
|
|
63
|
+
* A device with no declared profile does NEITHER: presence is opt-in, and a host
|
|
64
|
+
* that never said what its device is has not joined the peer lane. So the whole
|
|
65
|
+
* invariant is "no profile, no peer-lane traffic" — an explicit
|
|
66
|
+
* {@link PeerPresence.gather} still asks, because there the host did.
|
|
67
|
+
*/
|
|
68
|
+
refresh: (groupID: string, options?: PeerGatherOptions) => Promise<Array<GatheredPeer>>;
|
|
69
|
+
/** Fire-and-forget {@link refresh}, off the current turn. For an event handler. */
|
|
70
|
+
scheduleRefresh: (groupID: string, reason: PresenceReason) => void;
|
|
71
|
+
/**
|
|
72
|
+
* Fire-and-forget {@link announce}, off the current turn and COALESCED per
|
|
73
|
+
* group. Only the epoch lane uses this, and only it needs to: a device catching
|
|
74
|
+
* up walks many commits back to back, and one announce per commit is a storm
|
|
75
|
+
* where one announce per burst is the same information.
|
|
76
|
+
*/
|
|
77
|
+
scheduleAnnounce: (groupID: string, reason: PresenceReason) => void;
|
|
78
|
+
/**
|
|
79
|
+
* Stop scheduling, and WAIT for whatever is already running.
|
|
80
|
+
*
|
|
81
|
+
* Cancelling the pending timers is only half of it: a scheduled task whose
|
|
82
|
+
* timer has already fired is a live store read, and the caller's next step is
|
|
83
|
+
* closing the database. On Postgres a query overlapping the close wedges it
|
|
84
|
+
* permanently, so abandoning that read hangs the shutdown it was meant to
|
|
85
|
+
* finish.
|
|
86
|
+
*/
|
|
87
|
+
dispose: () => Promise<void>;
|
|
88
|
+
};
|
|
89
|
+
export declare function createPeerPresence(params: PeerPresenceParams): PeerPresence;
|
|
90
|
+
export {};
|