@kubun/plugin-p2p 0.10.1 → 0.12.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.
Files changed (201) hide show
  1. package/lib/context/delegation.js +130 -1
  2. package/lib/context/group.js +1646 -1
  3. package/lib/context/hub.js +151 -1
  4. package/lib/context/join.js +115 -1
  5. package/lib/context/peer.d.ts +92 -0
  6. package/lib/context/peer.js +1216 -0
  7. package/lib/context/require-admin.d.ts +57 -0
  8. package/lib/context/require-admin.js +57 -0
  9. package/lib/context/sync.d.ts +12 -0
  10. package/lib/context/sync.js +252 -1
  11. package/lib/context/types.d.ts +58 -22
  12. package/lib/context/types.js +48 -1
  13. package/lib/groups/access-default-token.d.ts +44 -0
  14. package/lib/groups/access-default-token.js +103 -0
  15. package/lib/groups/admin-roster.d.ts +55 -0
  16. package/lib/groups/admin-roster.js +69 -0
  17. package/lib/groups/anchor-store.d.ts +15 -0
  18. package/lib/groups/anchor-store.js +37 -0
  19. package/lib/groups/app-cursor-store.d.ts +17 -0
  20. package/lib/groups/app-cursor-store.js +34 -0
  21. package/lib/groups/broadcast-codec.js +6 -1
  22. package/lib/groups/broadcast.d.ts +238 -106
  23. package/lib/groups/broadcast.js +702 -1
  24. package/lib/groups/catalog-token.d.ts +50 -0
  25. package/lib/groups/catalog-token.js +96 -0
  26. package/lib/groups/circle-projection.d.ts +90 -0
  27. package/lib/groups/circle-projection.js +202 -0
  28. package/lib/groups/circle-reducers.d.ts +139 -0
  29. package/lib/groups/circle-reducers.js +128 -0
  30. package/lib/groups/commit-adoption.d.ts +77 -0
  31. package/lib/groups/commit-adoption.js +108 -0
  32. package/lib/groups/commit-journal.d.ts +14 -0
  33. package/lib/groups/commit-journal.js +65 -0
  34. package/lib/groups/control-request.d.ts +66 -0
  35. package/lib/groups/control-request.js +101 -0
  36. package/lib/groups/events.d.ts +19 -2
  37. package/lib/groups/events.js +10 -1
  38. package/lib/groups/group-anchor.d.ts +24 -0
  39. package/lib/groups/group-anchor.js +22 -0
  40. package/lib/groups/group-crypto.d.ts +42 -0
  41. package/lib/groups/group-crypto.js +159 -0
  42. package/lib/groups/group-handle-registry.d.ts +138 -9
  43. package/lib/groups/group-handle-registry.js +533 -1
  44. package/lib/groups/group-handlers.d.ts +30 -0
  45. package/lib/groups/group-handlers.js +218 -0
  46. package/lib/groups/group-health-monitor.d.ts +11 -0
  47. package/lib/groups/group-health-monitor.js +139 -1
  48. package/lib/groups/group-mls.d.ts +66 -0
  49. package/lib/groups/group-mls.js +696 -0
  50. package/lib/groups/group-peer-manager.d.ts +301 -0
  51. package/lib/groups/group-peer-manager.js +1336 -0
  52. package/lib/groups/group-protocols.d.ts +754 -0
  53. package/lib/groups/group-protocols.js +448 -0
  54. package/lib/groups/invite-payload.d.ts +49 -1
  55. package/lib/groups/invite-payload.js +26 -1
  56. package/lib/groups/join-request-identity.d.ts +21 -0
  57. package/lib/groups/join-request-identity.js +43 -0
  58. package/lib/groups/join-utils.d.ts +61 -0
  59. package/lib/groups/join-utils.js +366 -1
  60. package/lib/groups/ledger-affected-events.d.ts +20 -0
  61. package/lib/groups/ledger-affected-events.js +85 -0
  62. package/lib/groups/ledger-commit-fold.d.ts +107 -0
  63. package/lib/groups/ledger-commit-fold.js +167 -0
  64. package/lib/groups/ledger-ingest.d.ts +124 -0
  65. package/lib/groups/ledger-ingest.js +212 -0
  66. package/lib/groups/ledger.d.ts +44 -0
  67. package/lib/groups/ledger.js +30 -0
  68. package/lib/groups/manager.d.ts +217 -85
  69. package/lib/groups/manager.js +798 -1
  70. package/lib/groups/mls-codec.d.ts +28 -7
  71. package/lib/groups/mls-codec.js +33 -1
  72. package/lib/groups/mls-encryptor.d.ts +2 -2
  73. package/lib/groups/mls-encryptor.js +33 -1
  74. package/lib/groups/mls-group-handle.d.ts +37 -2
  75. package/lib/groups/mls-group-handle.js +18 -1
  76. package/lib/groups/mls-json.js +17 -1
  77. package/lib/groups/mls-receive-errors.d.ts +27 -0
  78. package/lib/groups/mls-receive-errors.js +39 -0
  79. package/lib/groups/mls-state.d.ts +6 -4
  80. package/lib/groups/mls-state.js +79 -1
  81. package/lib/groups/peer-presence.d.ts +90 -0
  82. package/lib/groups/peer-presence.js +295 -0
  83. package/lib/groups/peer-selection.d.ts +37 -0
  84. package/lib/groups/peer-selection.js +72 -0
  85. package/lib/groups/roster-projection.d.ts +53 -0
  86. package/lib/groups/roster-projection.js +65 -0
  87. package/lib/groups/store-received-grant.d.ts +7 -3
  88. package/lib/groups/store-received-grant.js +161 -1
  89. package/lib/groups/store-received-revocation.d.ts +19 -16
  90. package/lib/groups/store-received-revocation.js +83 -1
  91. package/lib/hub/ack-backoff.d.ts +22 -0
  92. package/lib/hub/ack-backoff.js +88 -0
  93. package/lib/hub/http-client.d.ts +18 -10
  94. package/lib/hub/http-client.js +15 -1
  95. package/lib/hub/hub-like.d.ts +75 -0
  96. package/lib/hub/hub-like.js +1022 -0
  97. package/lib/hub/loopback-log-hub.d.ts +20 -0
  98. package/lib/hub/loopback-log-hub.js +178 -0
  99. package/lib/hub/manager.d.ts +25 -40
  100. package/lib/hub/manager.js +155 -1
  101. package/lib/hub/peer-scoped-hub-view.d.ts +35 -0
  102. package/lib/hub/peer-scoped-hub-view.js +92 -0
  103. package/lib/hub/sender-scoped-hub-view.d.ts +41 -0
  104. package/lib/hub/sender-scoped-hub-view.js +59 -0
  105. package/lib/hub/server-did.d.ts +67 -0
  106. package/lib/hub/server-did.js +131 -0
  107. package/lib/hub/wiring.d.ts +77 -51
  108. package/lib/hub/wiring.js +189 -1
  109. package/lib/index.d.ts +58 -22
  110. package/lib/index.js +563 -1
  111. package/lib/peer/connection-registry.d.ts +45 -0
  112. package/lib/peer/connection-registry.js +52 -0
  113. package/lib/peer/handlers.d.ts +69 -0
  114. package/lib/peer/handlers.js +110 -0
  115. package/lib/protocol.d.ts +214 -1
  116. package/lib/protocol.js +519 -1
  117. package/lib/schema.d.ts +3 -1
  118. package/lib/schema.js +1150 -39
  119. package/lib/sync/access-default-sender.d.ts +23 -0
  120. package/lib/sync/access-default-sender.js +80 -0
  121. package/lib/sync/authorize.d.ts +58 -4
  122. package/lib/sync/authorize.js +114 -1
  123. package/lib/sync/broadcast-queue.d.ts +3 -4
  124. package/lib/sync/broadcast-queue.js +182 -1
  125. package/lib/sync/broadcast-sender.js +179 -1
  126. package/lib/sync/catalog-scope.d.ts +8 -7
  127. package/lib/sync/catalog-scope.js +84 -1
  128. package/lib/sync/errors.d.ts +12 -2
  129. package/lib/sync/errors.js +29 -1
  130. package/lib/sync/forwarder.d.ts +13 -19
  131. package/lib/sync/forwarder.js +271 -1
  132. package/lib/sync/handlers.js +200 -1
  133. package/lib/sync/hub-tunnel-sync-listener.d.ts +19 -5
  134. package/lib/sync/hub-tunnel-sync-listener.js +261 -1
  135. package/lib/sync/hub-tunnel-sync-provider.d.ts +34 -14
  136. package/lib/sync/hub-tunnel-sync-provider.js +113 -1
  137. package/lib/sync/merkle-apply.d.ts +5 -0
  138. package/lib/sync/merkle-apply.js +194 -1
  139. package/lib/sync/merkle-channel.d.ts +11 -3
  140. package/lib/sync/merkle-channel.js +0 -0
  141. package/lib/sync/merkle-tree.js +120 -1
  142. package/lib/sync/peer-registry.d.ts +15 -11
  143. package/lib/sync/peer-registry.js +82 -1
  144. package/lib/sync/receive-access-gate.js +24 -1
  145. package/lib/sync/scope-resolver.js +41 -1
  146. package/lib/sync/sync-client.d.ts +14 -4
  147. package/lib/sync/sync-client.js +254 -1
  148. package/lib/sync/sync-manager.d.ts +24 -6
  149. package/lib/sync/sync-manager.js +301 -1
  150. package/lib/sync/tunnel-listeners.d.ts +52 -0
  151. package/lib/sync/tunnel-listeners.js +175 -0
  152. package/lib/sync/tunnel-observability.d.ts +25 -0
  153. package/lib/sync/tunnel-observability.js +63 -0
  154. package/lib/sync/tunnel-topics.d.ts +20 -0
  155. package/lib/sync/tunnel-topics.js +28 -0
  156. package/lib/types.d.ts +704 -22
  157. package/lib/types.js +1 -1
  158. package/lib/util/handler-error.d.ts +16 -0
  159. package/lib/util/handler-error.js +64 -0
  160. package/lib/util/mutex.d.ts +14 -1
  161. package/lib/util/mutex.js +44 -1
  162. package/lib/utils.js +21 -1
  163. package/package.json +58 -47
  164. package/lib/groups/broadcast-service.d.ts +0 -263
  165. package/lib/groups/broadcast-service.js +0 -1
  166. package/lib/groups/rejoin-codec.d.ts +0 -14
  167. package/lib/groups/rejoin-codec.js +0 -1
  168. package/lib/groups/wire-frame.d.ts +0 -34
  169. package/lib/groups/wire-frame.js +0 -1
  170. package/lib/hub/circle-catchup-requester.d.ts +0 -58
  171. package/lib/hub/circle-catchup-requester.js +0 -1
  172. package/lib/hub/circle-catchup-responder.d.ts +0 -50
  173. package/lib/hub/circle-catchup-responder.js +0 -1
  174. package/lib/hub/connection-pool.d.ts +0 -43
  175. package/lib/hub/connection-pool.js +0 -1
  176. package/lib/hub/did-observing-transport.d.ts +0 -64
  177. package/lib/hub/did-observing-transport.js +0 -1
  178. package/lib/hub/epoch-stale-detector.d.ts +0 -18
  179. package/lib/hub/epoch-stale-detector.js +0 -1
  180. package/lib/hub/errors.d.ts +0 -30
  181. package/lib/hub/errors.js +0 -1
  182. package/lib/hub/forward-remote-broadcast.d.ts +0 -15
  183. package/lib/hub/forward-remote-broadcast.js +0 -1
  184. package/lib/hub/group-channel.d.ts +0 -89
  185. package/lib/hub/group-channel.js +0 -1
  186. package/lib/hub/hub-connection.d.ts +0 -96
  187. package/lib/hub/hub-connection.js +0 -1
  188. package/lib/hub/receive-handler.d.ts +0 -51
  189. package/lib/hub/receive-handler.js +0 -1
  190. package/lib/hub/rejoin-manager.d.ts +0 -78
  191. package/lib/hub/rejoin-manager.js +0 -1
  192. package/lib/hub/rejoin-responder.d.ts +0 -32
  193. package/lib/hub/rejoin-responder.js +0 -1
  194. package/lib/hub/relay-manager.d.ts +0 -142
  195. package/lib/hub/relay-manager.js +0 -1
  196. package/lib/hub/send-handler.d.ts +0 -40
  197. package/lib/hub/send-handler.js +0 -1
  198. package/lib/hub/tunnel-inbox.d.ts +0 -20
  199. package/lib/hub/tunnel-inbox.js +0 -1
  200. package/lib/hub/wait-for-gate.d.ts +0 -14
  201. package/lib/hub/wait-for-gate.js +0 -1
@@ -0,0 +1,167 @@
1
+ import { readGroupAnchor } from '@kumiai/mls';
2
+ import { ingestVerifiedLedgerEntry } from './ledger-ingest.js';
3
+ /**
4
+ * The namespace kubun owns on the control ledger. Every entry type kubun mints
5
+ * and projects lives under it: `kubun.circle.member`, `kubun.circle.def`,
6
+ * `kubun.group.settings`. Note the last one — it CONTAINS `group.` without
7
+ * starting with it, which is why this stays a prefix test and must never be
8
+ * loosened to a substring test.
9
+ */ const KUBUN_ENTRY_TYPE_PREFIX = 'kubun.';
10
+ /**
11
+ * Whether an entry type is one kubun owns, and therefore one kubun's fold
12
+ * projects. Everything outside `kubun.*` is foreign: the handle consumes it and
13
+ * surfaces nothing to kubun, so nothing folds it.
14
+ *
15
+ * Anchored on KUBUN's namespace, not the one `@kumiai/mls` reserves, so an
16
+ * upstream rename (`group.role` → `kumiai.role`) cannot silently change which
17
+ * entries kubun projects.
18
+ *
19
+ * A producer must write rows only for FOREIGN entries: an entry the fold surfaces
20
+ * is appended and projected by ingest, which reads the row's pre-state to decide
21
+ * which domain event it stands for. A pre-written row reads as pre-existing, so
22
+ * the author alone sees a change event where every other device sees a create.
23
+ *
24
+ * This answers what kubun PROJECTS, not what it must OBSERVE — see
25
+ * {@link entriesRetainedByCommit}, whose answer for the role entry is the opposite.
26
+ */ export function isKubunLedgerEntryType(type) {
27
+ return type.startsWith(KUBUN_ENTRY_TYPE_PREFIX);
28
+ }
29
+ /**
30
+ * The entries a landed commit added to the handle's ledger that kubun does not
31
+ * project but must still OBSERVE. Everything outside `kubun.*`: the handle
32
+ * surfaces none of it, so no kubun fold builds a row from it, yet these entries
33
+ * carry HLCs this device has to merge — a role change stamped by another member
34
+ * is a timestamp this device must sort after.
35
+ *
36
+ * PROJECTS and OBSERVES are different questions — reusing one predicate for both
37
+ * drops the role entry's clock, which reaches this device through no other path.
38
+ *
39
+ * The delta is taken BY INDEX, never by comparing tokens: the handle's ledger is a
40
+ * log, not a set, so a token it already held can legitimately be enacted again.
41
+ * `ledgerLengthBeforeCommit` is the log's length read before `processMessage`.
42
+ */ export function entriesRetainedByCommit(handle, ledgerLengthBeforeCommit) {
43
+ const retained = [];
44
+ const log = handle.ledger;
45
+ for(let index = Math.max(ledgerLengthBeforeCommit, 0); index < log.length; index++){
46
+ const held = log[index];
47
+ if (held == null || isKubunLedgerEntryType(held.verified.entry.type)) {
48
+ continue;
49
+ }
50
+ retained.push({
51
+ verified: held.verified,
52
+ token: held.token
53
+ });
54
+ }
55
+ return retained;
56
+ }
57
+ /**
58
+ * The entries a device's OWN commit would surface, recovered from the
59
+ * post-commit handle's ledger. The sink only fires for a commit a handle
60
+ * PROCESSES — a device merges its own commit instead of processing it, so the
61
+ * local path derives the same list from the tokens it committed, keeping the
62
+ * entries kubun owns and dropping the rest. Both paths then fold identical
63
+ * entries through the same ingest.
64
+ *
65
+ * The receive side surfaces by kumiai's rule (everything kumiai does not reserve
66
+ * for itself) and this side surfaces by kubun's. The two agree on every type
67
+ * kubun commits, which is `kubun.*` plus the role entry kumiai interprets — a
68
+ * commit carrying some third party's namespace would surface remotely and not
69
+ * locally, and kubun mints no such entry.
70
+ */ export function entriesSurfacedByOwnCommit(handle, tokens) {
71
+ const entries = [];
72
+ for (const token of tokens){
73
+ const held = handle.ledger.find((candidate)=>candidate.token === token);
74
+ if (held == null || !isKubunLedgerEntryType(held.verified.entry.type)) {
75
+ continue;
76
+ }
77
+ entries.push(held.verified);
78
+ }
79
+ return entries;
80
+ }
81
+ /**
82
+ * Recover the signed token a surfaced entry was carried by. The handle's ledger
83
+ * log holds `{ token, verified }` pairs, and the entry a commit surfaces IS the
84
+ * `verified` the log stores for it, so the pair is recovered by identity rather
85
+ * than by re-deriving a digest or re-verifying a signature.
86
+ */ function tokenForEntry(handle, entry) {
87
+ for (const held of handle.ledger){
88
+ if (held.verified === entry) {
89
+ return held.token;
90
+ }
91
+ }
92
+ return undefined;
93
+ }
94
+ /**
95
+ * Fold a landed commit's surfaced entries into the same projection ingest a
96
+ * broadcast entry folds through, so the two sources converge on identical rows.
97
+ * Returns one descriptor per entry that moved a projection, for the caller to
98
+ * emit as domain events.
99
+ *
100
+ * NEVER throws. The MLS advance is the load-bearing state and a projection is a
101
+ * cache derivable from the ledger: a fold fault must not roll back a good
102
+ * commit, because a lane that refuses to advance past a frame re-reads that same
103
+ * frame forever. A faulted entry is logged and skipped; the projection heals on
104
+ * the next reproject (an admin entry's arrival rebuilds every dependent, and a
105
+ * ledger catch-up rebuilds all four from the whole ledger). Each entry folds
106
+ * independently, so one fault does not skip the rest.
107
+ */ export async function foldCommittedLedgerEntries(params) {
108
+ const { handle, entries, p2pStore, groupID, logger } = params;
109
+ const retained = params.retained ?? [];
110
+ if (entries.length === 0 && retained.length === 0) {
111
+ return [];
112
+ }
113
+ // Retained first, projected second — the enactment order the handle applied
114
+ // them in, which is the order their clocks were stamped in.
115
+ const pending = [
116
+ ...retained
117
+ ];
118
+ for (const entry of entries){
119
+ const token = tokenForEntry(handle, entry);
120
+ if (token == null) {
121
+ // The ledger log is the only source of an entry's signed token, and an
122
+ // entry the accepting handle surfaced is in it. Nothing to append without
123
+ // one, so log rather than guess.
124
+ logger?.warn('committed ledger entry skipped: no signed token in the handle ledger', {
125
+ groupID,
126
+ type: entry.entry.type
127
+ });
128
+ continue;
129
+ }
130
+ pending.push({
131
+ verified: entry,
132
+ token
133
+ });
134
+ }
135
+ const anchor = readGroupAnchor(handle);
136
+ const affected = [];
137
+ for (const { verified } of pending){
138
+ try {
139
+ const result = await ingestVerifiedLedgerEntry({
140
+ p2pStore,
141
+ ...params.hlc != null ? {
142
+ hlc: params.hlc
143
+ } : {},
144
+ ...anchor != null ? {
145
+ getGroupAnchor: async ()=>anchor
146
+ } : {},
147
+ // The accepting handle is already in hand, so its log is read straight
148
+ // off it. Going back through the registry here would re-acquire a lock
149
+ // this path may already hold.
150
+ getGroupLedger: async ()=>handle.ledger,
151
+ ...logger != null ? {
152
+ logger
153
+ } : {}
154
+ }, verified, groupID);
155
+ if (result.ledger != null) {
156
+ affected.push(result.ledger);
157
+ }
158
+ } catch (error) {
159
+ logger?.warn('committed ledger entry projection failed, skipping', {
160
+ groupID,
161
+ type: verified.entry.type,
162
+ error
163
+ });
164
+ }
165
+ }
166
+ return affected;
167
+ }
@@ -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
+ }