@kubun/plugin-p2p 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/context/group.js +1073 -746
- package/lib/context/hub.js +2 -1
- package/lib/context/join.js +50 -15
- package/lib/context/peer.d.ts +92 -0
- package/lib/context/peer.js +1216 -0
- package/lib/context/require-admin.d.ts +57 -0
- package/lib/context/require-admin.js +57 -0
- package/lib/context/sync.d.ts +12 -0
- package/lib/context/sync.js +234 -1
- package/lib/context/types.d.ts +56 -20
- package/lib/context/types.js +23 -3
- package/lib/groups/access-default-token.d.ts +44 -0
- package/lib/groups/access-default-token.js +103 -0
- package/lib/groups/admin-roster.d.ts +42 -117
- package/lib/groups/admin-roster.js +53 -168
- package/lib/groups/anchor-store.d.ts +15 -0
- package/lib/groups/anchor-store.js +37 -0
- package/lib/groups/app-cursor-store.d.ts +17 -0
- package/lib/groups/app-cursor-store.js +34 -0
- package/lib/groups/broadcast.d.ts +199 -81
- package/lib/groups/broadcast.js +443 -368
- package/lib/groups/catalog-token.d.ts +50 -0
- package/lib/groups/catalog-token.js +96 -0
- package/lib/groups/circle-projection.d.ts +32 -64
- package/lib/groups/circle-projection.js +95 -181
- package/lib/groups/circle-reducers.d.ts +71 -110
- package/lib/groups/circle-reducers.js +57 -127
- package/lib/groups/commit-adoption.d.ts +77 -0
- package/lib/groups/commit-adoption.js +108 -0
- package/lib/groups/commit-journal.d.ts +14 -0
- package/lib/groups/commit-journal.js +65 -0
- package/lib/groups/control-request.d.ts +66 -0
- package/lib/groups/control-request.js +101 -0
- package/lib/groups/events.d.ts +14 -1
- package/lib/groups/group-anchor.d.ts +18 -57
- package/lib/groups/group-anchor.js +15 -100
- package/lib/groups/group-crypto.d.ts +36 -13
- package/lib/groups/group-crypto.js +124 -50
- package/lib/groups/group-handle-registry.d.ts +106 -36
- package/lib/groups/group-handle-registry.js +351 -61
- package/lib/groups/group-handlers.d.ts +5 -4
- package/lib/groups/group-handlers.js +155 -29
- package/lib/groups/group-health-monitor.d.ts +1 -1
- package/lib/groups/group-health-monitor.js +1 -1
- package/lib/groups/group-mls.d.ts +35 -4
- package/lib/groups/group-mls.js +594 -62
- package/lib/groups/group-peer-manager.d.ts +212 -19
- package/lib/groups/group-peer-manager.js +1061 -69
- package/lib/groups/group-protocols.d.ts +485 -28
- package/lib/groups/group-protocols.js +289 -15
- package/lib/groups/invite-payload.d.ts +48 -0
- package/lib/groups/join-request-identity.d.ts +21 -0
- package/lib/groups/join-request-identity.js +43 -0
- package/lib/groups/join-utils.d.ts +60 -0
- package/lib/groups/join-utils.js +262 -0
- package/lib/groups/ledger-affected-events.d.ts +11 -6
- package/lib/groups/ledger-affected-events.js +15 -26
- package/lib/groups/ledger-commit-fold.d.ts +107 -0
- package/lib/groups/ledger-commit-fold.js +167 -0
- package/lib/groups/ledger-ingest.d.ts +124 -0
- package/lib/groups/ledger-ingest.js +212 -0
- package/lib/groups/ledger.d.ts +44 -0
- package/lib/groups/ledger.js +30 -0
- package/lib/groups/manager.d.ts +202 -19
- package/lib/groups/manager.js +623 -106
- package/lib/groups/mls-codec.d.ts +27 -6
- package/lib/groups/mls-codec.js +30 -15
- package/lib/groups/mls-encryptor.js +9 -6
- package/lib/groups/mls-group-handle.d.ts +37 -2
- package/lib/groups/mls-group-handle.js +12 -4
- package/lib/groups/mls-receive-errors.d.ts +0 -2
- package/lib/groups/mls-receive-errors.js +3 -6
- package/lib/groups/mls-state.d.ts +5 -3
- package/lib/groups/mls-state.js +27 -4
- package/lib/groups/peer-presence.d.ts +90 -0
- package/lib/groups/peer-presence.js +295 -0
- package/lib/groups/peer-selection.d.ts +37 -0
- package/lib/groups/peer-selection.js +72 -0
- package/lib/groups/roster-projection.d.ts +53 -0
- package/lib/groups/roster-projection.js +65 -0
- package/lib/groups/store-received-grant.d.ts +4 -3
- package/lib/groups/store-received-grant.js +80 -35
- package/lib/groups/store-received-revocation.d.ts +16 -13
- package/lib/groups/store-received-revocation.js +23 -22
- package/lib/hub/http-client.d.ts +10 -27
- package/lib/hub/http-client.js +2 -19
- package/lib/hub/hub-like.d.ts +57 -6
- package/lib/hub/hub-like.js +539 -52
- package/lib/hub/loopback-log-hub.d.ts +20 -0
- package/lib/hub/loopback-log-hub.js +178 -0
- package/lib/hub/manager.d.ts +25 -40
- package/lib/hub/manager.js +51 -94
- package/lib/hub/peer-scoped-hub-view.d.ts +35 -0
- package/lib/hub/peer-scoped-hub-view.js +92 -0
- package/lib/hub/sender-scoped-hub-view.d.ts +41 -0
- package/lib/hub/sender-scoped-hub-view.js +59 -0
- package/lib/hub/server-did.d.ts +67 -0
- package/lib/hub/server-did.js +131 -0
- package/lib/hub/wiring.d.ts +71 -14
- package/lib/hub/wiring.js +80 -23
- package/lib/index.d.ts +48 -13
- package/lib/index.js +257 -86
- package/lib/peer/connection-registry.d.ts +45 -0
- package/lib/peer/connection-registry.js +52 -0
- package/lib/peer/handlers.d.ts +69 -0
- package/lib/peer/handlers.js +110 -0
- package/lib/protocol.d.ts +213 -0
- package/lib/protocol.js +268 -0
- package/lib/schema.d.ts +2 -1
- package/lib/schema.js +805 -131
- package/lib/sync/access-default-sender.d.ts +23 -0
- package/lib/sync/access-default-sender.js +80 -0
- package/lib/sync/authorize.d.ts +57 -3
- package/lib/sync/authorize.js +45 -4
- package/lib/sync/broadcast-queue.d.ts +3 -4
- package/lib/sync/broadcast-queue.js +2 -3
- package/lib/sync/catalog-scope.d.ts +8 -7
- package/lib/sync/catalog-scope.js +55 -39
- package/lib/sync/errors.d.ts +9 -0
- package/lib/sync/errors.js +10 -0
- package/lib/sync/forwarder.d.ts +13 -19
- package/lib/sync/forwarder.js +13 -19
- package/lib/sync/handlers.js +54 -14
- package/lib/sync/hub-tunnel-sync-listener.d.ts +16 -6
- package/lib/sync/hub-tunnel-sync-listener.js +126 -37
- package/lib/sync/hub-tunnel-sync-provider.d.ts +31 -11
- package/lib/sync/hub-tunnel-sync-provider.js +68 -19
- package/lib/sync/merkle-channel.d.ts +9 -1
- package/lib/sync/merkle-channel.js +0 -0
- package/lib/sync/peer-registry.d.ts +14 -10
- package/lib/sync/peer-registry.js +0 -25
- package/lib/sync/sync-client.d.ts +11 -1
- package/lib/sync/sync-client.js +72 -10
- package/lib/sync/sync-manager.d.ts +22 -4
- package/lib/sync/sync-manager.js +39 -10
- package/lib/sync/tunnel-listeners.d.ts +52 -0
- package/lib/sync/tunnel-listeners.js +175 -0
- package/lib/sync/tunnel-observability.d.ts +25 -0
- package/lib/sync/tunnel-observability.js +63 -0
- package/lib/sync/tunnel-topics.d.ts +20 -0
- package/lib/sync/tunnel-topics.js +28 -0
- package/lib/types.d.ts +699 -28
- package/lib/types.js +0 -1
- package/lib/util/handler-error.d.ts +16 -0
- package/lib/util/handler-error.js +64 -0
- package/lib/util/mutex.d.ts +14 -1
- package/lib/util/mutex.js +37 -3
- package/package.json +59 -52
- package/lib/groups/ledger-entry.d.ts +0 -37
- package/lib/groups/ledger-entry.js +0 -56
- package/lib/groups/ledger-fold.d.ts +0 -56
- package/lib/groups/ledger-fold.js +0 -53
- package/lib/hub/did-observing-transport.d.ts +0 -75
- package/lib/hub/did-observing-transport.js +0 -126
- package/lib/hub/errors.d.ts +0 -24
- package/lib/hub/errors.js +0 -33
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { HLC } from '@kubun/hlc';
|
|
2
|
+
import type { Logger } from '@kubun/logger';
|
|
3
|
+
import type { Circle, CircleMember, Group, P2PStoreAPI } from '@kubun/store-p2p';
|
|
4
|
+
import type { GroupAnchor, LedgerLogEntry } from '@kumiai/mls';
|
|
5
|
+
import { type VerifiedLedgerEntry } from './ledger.js';
|
|
6
|
+
/**
|
|
7
|
+
* The local-event descriptor for an applied control-ledger entry. The ingest
|
|
8
|
+
* captures it after reprojecting, while the store still holds the post-state, so
|
|
9
|
+
* a caller can emit the matching local event for the change without re-reading
|
|
10
|
+
* or re-verifying. Each variant carries the projected row plus the
|
|
11
|
+
* create-vs-change / add-vs-remove discriminators the ingest derived by
|
|
12
|
+
* comparing the pre-state it read before reprojecting.
|
|
13
|
+
*/
|
|
14
|
+
export type LedgerAffected = {
|
|
15
|
+
entry: 'circle.def';
|
|
16
|
+
row: Circle;
|
|
17
|
+
/** True when an active circle row existed before this entry (changed, not created). */
|
|
18
|
+
preExisted: boolean;
|
|
19
|
+
/** True when the entry's catalog set differs from the pre-state's (catalogs changed). */
|
|
20
|
+
catalogsChanged: boolean;
|
|
21
|
+
} | {
|
|
22
|
+
entry: 'circle.def.removed';
|
|
23
|
+
row: Circle;
|
|
24
|
+
} | {
|
|
25
|
+
entry: 'circle.member';
|
|
26
|
+
row: CircleMember;
|
|
27
|
+
} | {
|
|
28
|
+
entry: 'circle.member.removed';
|
|
29
|
+
row: CircleMember;
|
|
30
|
+
} | {
|
|
31
|
+
entry: 'group.settings';
|
|
32
|
+
row: Group;
|
|
33
|
+
};
|
|
34
|
+
export type AffectedRow = {
|
|
35
|
+
kind: 'ledger';
|
|
36
|
+
ledger: LedgerAffected;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Whether a group is governed by a genesis anchor. The control gate must treat
|
|
40
|
+
* three cases differently:
|
|
41
|
+
* - `anchored`: the MLS state was read and carries a genesis anchor → ledger authority.
|
|
42
|
+
* - `unanchored`: the MLS state was read and carries no anchor (a group created
|
|
43
|
+
* before the anchor, or external) → legacy role-column authority.
|
|
44
|
+
* - `unknown`: the anchor could not be read at all (e.g. the handle was evicted,
|
|
45
|
+
* or a teardown race) → fail closed. We must NOT fall back to the role-column
|
|
46
|
+
* gate here: an anchored group whose anchor we momentarily can't read would
|
|
47
|
+
* otherwise be authorized by its forgeable `role` column — exactly the
|
|
48
|
+
* cross-peer bypass the ledger closes.
|
|
49
|
+
*/
|
|
50
|
+
type AnchorResolution = {
|
|
51
|
+
status: 'anchored';
|
|
52
|
+
anchor: GroupAnchor;
|
|
53
|
+
ledger: ReadonlyArray<LedgerLogEntry>;
|
|
54
|
+
} | {
|
|
55
|
+
status: 'unanchored';
|
|
56
|
+
} | {
|
|
57
|
+
status: 'unknown';
|
|
58
|
+
};
|
|
59
|
+
/** Inputs the ledger ingest needs — a strict subset of the receive params. */
|
|
60
|
+
export type LedgerIngestParams = {
|
|
61
|
+
p2pStore: P2PStoreAPI;
|
|
62
|
+
/**
|
|
63
|
+
* Device-wide monotonic clock. On a newly-appended entry the receiver merges
|
|
64
|
+
* the entry's HLC into this clock so its own later entries sort strictly after
|
|
65
|
+
* observed ones. Optional: callers that never receive ledger entries (and
|
|
66
|
+
* existing tests) omit it and the merge is skipped.
|
|
67
|
+
*/
|
|
68
|
+
hlc?: HLC;
|
|
69
|
+
/**
|
|
70
|
+
* Resolve a group's genesis anchor, or `null` when none is available yet. An
|
|
71
|
+
* entry is always stored durably, but the projections can only be built once
|
|
72
|
+
* the authenticated epoch-0 root is known; a `null` anchor defers projection
|
|
73
|
+
* without dropping the entry.
|
|
74
|
+
*/
|
|
75
|
+
getGroupAnchor?: (groupID: string) => Promise<GroupAnchor | null>;
|
|
76
|
+
/**
|
|
77
|
+
* Read the group's control ledger — the handle's own log, in enactment order.
|
|
78
|
+
* Every projection folds from it, so a group whose ledger cannot be read is
|
|
79
|
+
* not projectable and the entry's projection defers (it stays durable).
|
|
80
|
+
*
|
|
81
|
+
* A resolver rather than an array because the receive path is handed a group
|
|
82
|
+
* id, not a handle. Callers that already hold one under the registry's mutex
|
|
83
|
+
* pass a closure over it rather than re-acquiring the lock.
|
|
84
|
+
*/
|
|
85
|
+
getGroupLedger?: (groupID: string) => Promise<ReadonlyArray<LedgerLogEntry>>;
|
|
86
|
+
/** Optional logger — warns on per-entry apply failures. */
|
|
87
|
+
logger?: Logger;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Resolve a group's anchor without ever throwing into the receive loop. A
|
|
91
|
+
* missing `getGroupAnchor` (callers that never project) is treated as
|
|
92
|
+
* `unanchored`; a read that throws is `unknown` (fail closed), distinct from a
|
|
93
|
+
* read that succeeds with no anchor (`unanchored`).
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolveGroupAnchor(params: {
|
|
96
|
+
getGroupAnchor?: (groupID: string) => Promise<GroupAnchor | null>;
|
|
97
|
+
getGroupLedger?: (groupID: string) => Promise<ReadonlyArray<LedgerLogEntry>>;
|
|
98
|
+
}, groupID: string): Promise<AnchorResolution>;
|
|
99
|
+
/** The outcome of folding one verified entry into the projections. */
|
|
100
|
+
export type LedgerIngestResult = {
|
|
101
|
+
/**
|
|
102
|
+
* True when the entry was folded. Always true today — the entry is already in
|
|
103
|
+
* the handle's ledger by the time this runs, and this step only projects.
|
|
104
|
+
* Kept so a caller that folds a batch can still report a count.
|
|
105
|
+
*/
|
|
106
|
+
applied: boolean;
|
|
107
|
+
/** The projected row + discriminators, when this entry moved a projection. */
|
|
108
|
+
ledger?: LedgerAffected;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Fold ONE control-ledger entry a commit enacted into the projections that read
|
|
112
|
+
* it, and merge its clock.
|
|
113
|
+
*
|
|
114
|
+
* The entry itself needs no storing: it is already in the handle's ledger, put
|
|
115
|
+
* there by the commit that enacted it, and every projection folds from there.
|
|
116
|
+
* What is left is the device-local consequence — the observed HLC, the
|
|
117
|
+
* materialized rows, and the descriptor the caller announces from.
|
|
118
|
+
*
|
|
119
|
+
* The caller owns authentication. An entry a commit surfaced was already
|
|
120
|
+
* notarized by the handle that accepted the commit, so it is not re-verified
|
|
121
|
+
* here; this function trusts `verified` and never re-checks the signature.
|
|
122
|
+
*/
|
|
123
|
+
export declare function ingestVerifiedLedgerEntry(params: LedgerIngestParams, verified: VerifiedLedgerEntry, groupID: string): Promise<LedgerIngestResult>;
|
|
124
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { HLC } from '@kubun/hlc';
|
|
2
|
+
import { ADMIN_ROLE_ENTRY_TYPE } from './admin-roster.js';
|
|
3
|
+
import { reprojectCircleDefs, reprojectCircleMembers, reprojectGroupSettings } from './circle-projection.js';
|
|
4
|
+
import { CIRCLE_DEF_ENTRY_TYPE, CIRCLE_MEMBER_ENTRY_TYPE, decodeCircleMemberSubject, GROUP_SETTINGS_ENTRY_TYPE } from './circle-reducers.js';
|
|
5
|
+
import { entryOrd } from './ledger.js';
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a group's anchor without ever throwing into the receive loop. A
|
|
8
|
+
* missing `getGroupAnchor` (callers that never project) is treated as
|
|
9
|
+
* `unanchored`; a read that throws is `unknown` (fail closed), distinct from a
|
|
10
|
+
* read that succeeds with no anchor (`unanchored`).
|
|
11
|
+
*/ export async function resolveGroupAnchor(params, groupID) {
|
|
12
|
+
if (params.getGroupAnchor == null) {
|
|
13
|
+
return {
|
|
14
|
+
status: 'unanchored'
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
let anchor;
|
|
18
|
+
try {
|
|
19
|
+
anchor = await params.getGroupAnchor(groupID);
|
|
20
|
+
} catch {
|
|
21
|
+
return {
|
|
22
|
+
status: 'unknown'
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (anchor == null) {
|
|
26
|
+
return {
|
|
27
|
+
status: 'unanchored'
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// The ledger is resolved with the anchor, not beside it: both are read off the
|
|
31
|
+
// same handle, and every projection needs both. Resolving them together is what
|
|
32
|
+
// makes an unreadable ledger indistinguishable from an unreadable anchor —
|
|
33
|
+
// `unknown`, fail closed — instead of projecting an empty fold over real rows.
|
|
34
|
+
if (params.getGroupLedger == null) {
|
|
35
|
+
return {
|
|
36
|
+
status: 'unknown'
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return {
|
|
41
|
+
status: 'anchored',
|
|
42
|
+
anchor,
|
|
43
|
+
ledger: await params.getGroupLedger(groupID)
|
|
44
|
+
};
|
|
45
|
+
} catch {
|
|
46
|
+
return {
|
|
47
|
+
status: 'unknown'
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Fold ONE control-ledger entry a commit enacted into the projections that read
|
|
53
|
+
* it, and merge its clock.
|
|
54
|
+
*
|
|
55
|
+
* The entry itself needs no storing: it is already in the handle's ledger, put
|
|
56
|
+
* there by the commit that enacted it, and every projection folds from there.
|
|
57
|
+
* What is left is the device-local consequence — the observed HLC, the
|
|
58
|
+
* materialized rows, and the descriptor the caller announces from.
|
|
59
|
+
*
|
|
60
|
+
* The caller owns authentication. An entry a commit surfaced was already
|
|
61
|
+
* notarized by the handle that accepted the commit, so it is not re-verified
|
|
62
|
+
* here; this function trusts `verified` and never re-checks the signature.
|
|
63
|
+
*/ export async function ingestVerifiedLedgerEntry(params, verified, groupID) {
|
|
64
|
+
const { p2pStore } = params;
|
|
65
|
+
// An entry `@kumiai/mls` minted itself — the invitee's role entry, signed
|
|
66
|
+
// inside `createInvite` — carries no `ord`, so there is no clock to merge.
|
|
67
|
+
const ord = entryOrd(verified.entry);
|
|
68
|
+
if (params.hlc != null && ord != null) {
|
|
69
|
+
try {
|
|
70
|
+
params.hlc.receive(HLC.parse(ord));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
params.logger?.warn('ledger entry HLC merge failed', {
|
|
73
|
+
groupID,
|
|
74
|
+
hlc: ord,
|
|
75
|
+
error
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// The overlay can only be projected once the authenticated epoch-0 root is
|
|
80
|
+
// known. Defer projection when there is no anchor or it can't be read right
|
|
81
|
+
// now — the entry is safe in the handle's ledger either way, and a later entry
|
|
82
|
+
// or a reproject heals the overlay.
|
|
83
|
+
const resolution = await resolveGroupAnchor(params, groupID);
|
|
84
|
+
if (resolution.status !== 'anchored') {
|
|
85
|
+
params.logger?.debug('ledger entry role projection deferred', {
|
|
86
|
+
groupID,
|
|
87
|
+
type: verified.entry.type,
|
|
88
|
+
reason: resolution.status === 'unknown' ? 'anchor unavailable' : 'no genesis anchor'
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
applied: true
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const { anchor, ledger } = resolution;
|
|
95
|
+
// Local-event descriptor for this applied entry — populated by the projected
|
|
96
|
+
// control branches below so a caller can emit the same local event a producer
|
|
97
|
+
// fires. The pre-state each branch reads BEFORE reprojecting decides
|
|
98
|
+
// create-vs-change / add-vs-remove; the post-state is the projected row the
|
|
99
|
+
// event carries.
|
|
100
|
+
let ledgerAffected;
|
|
101
|
+
// Route by entry type to the matching projection, each of which reprojects the
|
|
102
|
+
// whole ledger unconditionally. An entry is judged against the ledger prefix
|
|
103
|
+
// through its own slot, so a full fold reaches the same answer an incremental
|
|
104
|
+
// one would and there is no cached-projection-is-stale case to detect — the
|
|
105
|
+
// watermark and incremental appliers this once needed are gone. An unprojected
|
|
106
|
+
// type falls through; the durable append above is the whole of its effect.
|
|
107
|
+
switch(verified.entry.type){
|
|
108
|
+
case ADMIN_ROLE_ENTRY_TYPE:
|
|
109
|
+
{
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case CIRCLE_DEF_ENTRY_TYPE:
|
|
113
|
+
{
|
|
114
|
+
const defEntry = verified;
|
|
115
|
+
// Pre-state (including a tombstoned row) read before reprojecting: an
|
|
116
|
+
// active row before this entry means the circle existed (a change, not a
|
|
117
|
+
// create); its `catalog_ids` is the baseline for catalogs-changed.
|
|
118
|
+
const before = await p2pStore.getCircle(defEntry.entry.subject, {
|
|
119
|
+
includeRemoved: true
|
|
120
|
+
});
|
|
121
|
+
const preActive = before != null && before.removed_at_hlc == null;
|
|
122
|
+
await reprojectCircleDefs({
|
|
123
|
+
store: p2pStore,
|
|
124
|
+
groupID,
|
|
125
|
+
anchor,
|
|
126
|
+
ledger,
|
|
127
|
+
logger: params.logger
|
|
128
|
+
});
|
|
129
|
+
// Post-state decides which local event to emit. A now-tombstoned row maps
|
|
130
|
+
// to a delete; an active row maps to create (no active pre-state) or
|
|
131
|
+
// change (active pre-state). A null post-state (a non-admin entry the
|
|
132
|
+
// projection dropped, or a tombstone with no prior row) emits nothing.
|
|
133
|
+
const after = await p2pStore.getCircle(defEntry.entry.subject, {
|
|
134
|
+
includeRemoved: true
|
|
135
|
+
});
|
|
136
|
+
if (after != null) {
|
|
137
|
+
if (after.removed_at_hlc != null) {
|
|
138
|
+
ledgerAffected = {
|
|
139
|
+
entry: 'circle.def.removed',
|
|
140
|
+
row: after
|
|
141
|
+
};
|
|
142
|
+
} else {
|
|
143
|
+
const catalogsChanged = preActive && before != null && JSON.stringify(before.catalog_ids) !== JSON.stringify(after.catalog_ids);
|
|
144
|
+
ledgerAffected = {
|
|
145
|
+
entry: 'circle.def',
|
|
146
|
+
row: after,
|
|
147
|
+
preExisted: preActive,
|
|
148
|
+
catalogsChanged
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case CIRCLE_MEMBER_ENTRY_TYPE:
|
|
155
|
+
{
|
|
156
|
+
const memberEntry = verified;
|
|
157
|
+
const decoded = decodeCircleMemberSubject(memberEntry.entry.subject);
|
|
158
|
+
await reprojectCircleMembers({
|
|
159
|
+
store: p2pStore,
|
|
160
|
+
groupID,
|
|
161
|
+
anchor,
|
|
162
|
+
ledger,
|
|
163
|
+
logger: params.logger
|
|
164
|
+
});
|
|
165
|
+
// Post-state decides the local event: a now-tombstoned row maps to a
|
|
166
|
+
// remove, an active row to an add (an add and a role-change both map to
|
|
167
|
+
// `circleMemberAdded` — there is no distinct role-change event). A null
|
|
168
|
+
// post-state (a malformed subject, a non-admin entry the projection
|
|
169
|
+
// dropped, or a tombstone with no prior row) emits nothing.
|
|
170
|
+
if (decoded != null) {
|
|
171
|
+
const after = await p2pStore.getCircleMember(decoded.circleID, decoded.memberDID);
|
|
172
|
+
if (after != null) {
|
|
173
|
+
ledgerAffected = after.removed_at_hlc != null ? {
|
|
174
|
+
entry: 'circle.member.removed',
|
|
175
|
+
row: after
|
|
176
|
+
} : {
|
|
177
|
+
entry: 'circle.member',
|
|
178
|
+
row: after
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
case GROUP_SETTINGS_ENTRY_TYPE:
|
|
185
|
+
{
|
|
186
|
+
await reprojectGroupSettings({
|
|
187
|
+
store: p2pStore,
|
|
188
|
+
groupID,
|
|
189
|
+
anchor,
|
|
190
|
+
ledger,
|
|
191
|
+
logger: params.logger
|
|
192
|
+
});
|
|
193
|
+
// Group settings are partial-merge with no tombstone and no
|
|
194
|
+
// create-vs-change distinction — the producer always emits
|
|
195
|
+
// `groupDataChanged` — so the post-state group row alone is the event.
|
|
196
|
+
const after = await p2pStore.getGroup(groupID);
|
|
197
|
+
if (after != null) {
|
|
198
|
+
ledgerAffected = {
|
|
199
|
+
entry: 'group.settings',
|
|
200
|
+
row: after
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return ledgerAffected != null ? {
|
|
207
|
+
applied: true,
|
|
208
|
+
ledger: ledgerAffected
|
|
209
|
+
} : {
|
|
210
|
+
applied: true
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type FoldDrop, type FoldInput, type GroupAnchor, type LedgerReducer } from '@kumiai/mls';
|
|
2
|
+
export { type FoldDrop, type FoldInput, type LedgerEntry, type LedgerReducer, ledgerEntryDigest, signLedgerEntry, type VerifiedLedgerEntry, verifyLedgerEntry, } from '@kumiai/mls';
|
|
3
|
+
/**
|
|
4
|
+
* A fold input paired with the slot it occupies in the group's control ledger.
|
|
5
|
+
*
|
|
6
|
+
* The position, not the entry, is the identity that matters for ordering. The
|
|
7
|
+
* handle's log records what each commit ENACTED rather than a set of claims, so
|
|
8
|
+
* it can hold the same token twice — a repeat is the only way to express a
|
|
9
|
+
* demotion back to a previously-held role — and two such entries share both
|
|
10
|
+
* their `ord` and their content-addressed `entryID`. Nothing derived from the
|
|
11
|
+
* entry can tell those two apart; only the slot can.
|
|
12
|
+
*/
|
|
13
|
+
export type PositionedFoldInput<TValue> = FoldInput<TValue> & {
|
|
14
|
+
/** Index in the handle's ledger, in enactment order. */
|
|
15
|
+
position: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The HLC kubun signed into an entry, or `null` when the entry carries none.
|
|
19
|
+
*
|
|
20
|
+
* DESCRIPTIVE ONLY: nothing orders or authorizes by it. An entry minted inside
|
|
21
|
+
* `createInvite` has no `ord` at all — kumiai orders by the epoch chain — so
|
|
22
|
+
* absence is normal and must stay distinguishable from a real clock.
|
|
23
|
+
*/
|
|
24
|
+
export declare function entryOrd(entry: {
|
|
25
|
+
ord?: string;
|
|
26
|
+
}): string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Fold a group's control ledger into a reducer's projection.
|
|
29
|
+
*
|
|
30
|
+
* Entries fold in the order given, which is the order the handle's log holds
|
|
31
|
+
* them: enactment order, assigned by the authenticated MLS epoch chain. Every
|
|
32
|
+
* peer holding the same ledger holds the same sequence, so the fold is
|
|
33
|
+
* deterministic without kubun imposing an order of its own.
|
|
34
|
+
*
|
|
35
|
+
* Kubun used to sort by the signed `ord` — an order the author asserts, and so
|
|
36
|
+
* one the author can backdate: a demoted admin could date an entry into the
|
|
37
|
+
* window where it still held the role. A position is assigned by the commit
|
|
38
|
+
* chain and cannot be claimed.
|
|
39
|
+
*
|
|
40
|
+
* Authority is NOT checked here: the caller filters unauthorized entries out
|
|
41
|
+
* first, because the reducer's `verifyAuthority` sees only the entry and the
|
|
42
|
+
* prefix an entry is judged against is a property of its slot.
|
|
43
|
+
*/
|
|
44
|
+
export declare function foldLedgerEntries<TValue, TState>(entries: Array<FoldInput<TValue>>, anchor: GroupAnchor, reducer: LedgerReducer<TValue, TState>, onDrop?: (drop: FoldDrop) => void): TState;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { foldLedger } from '@kumiai/mls';
|
|
2
|
+
export { ledgerEntryDigest, signLedgerEntry, verifyLedgerEntry } from '@kumiai/mls';
|
|
3
|
+
/**
|
|
4
|
+
* The HLC kubun signed into an entry, or `null` when the entry carries none.
|
|
5
|
+
*
|
|
6
|
+
* DESCRIPTIVE ONLY: nothing orders or authorizes by it. An entry minted inside
|
|
7
|
+
* `createInvite` has no `ord` at all — kumiai orders by the epoch chain — so
|
|
8
|
+
* absence is normal and must stay distinguishable from a real clock.
|
|
9
|
+
*/ export function entryOrd(entry) {
|
|
10
|
+
return entry.ord ?? null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Fold a group's control ledger into a reducer's projection.
|
|
14
|
+
*
|
|
15
|
+
* Entries fold in the order given, which is the order the handle's log holds
|
|
16
|
+
* them: enactment order, assigned by the authenticated MLS epoch chain. Every
|
|
17
|
+
* peer holding the same ledger holds the same sequence, so the fold is
|
|
18
|
+
* deterministic without kubun imposing an order of its own.
|
|
19
|
+
*
|
|
20
|
+
* Kubun used to sort by the signed `ord` — an order the author asserts, and so
|
|
21
|
+
* one the author can backdate: a demoted admin could date an entry into the
|
|
22
|
+
* window where it still held the role. A position is assigned by the commit
|
|
23
|
+
* chain and cannot be claimed.
|
|
24
|
+
*
|
|
25
|
+
* Authority is NOT checked here: the caller filters unauthorized entries out
|
|
26
|
+
* first, because the reducer's `verifyAuthority` sees only the entry and the
|
|
27
|
+
* prefix an entry is judged against is a property of its slot.
|
|
28
|
+
*/ export function foldLedgerEntries(entries, anchor, reducer, onDrop) {
|
|
29
|
+
return foldLedger(entries, anchor, reducer, onDrop);
|
|
30
|
+
}
|