@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,50 @@
1
+ import { type OwnIdentity } from '@kokuin/token';
2
+ import type { CatalogRecord } from '@kubun/protocol';
3
+ type SetPayload = {
4
+ op: 'set';
5
+ catalog: CatalogRecord;
6
+ };
7
+ type UpdatePayload = {
8
+ op: 'update';
9
+ catalogID: string;
10
+ update: {
11
+ name?: string;
12
+ description?: string;
13
+ filterCriteria?: CatalogRecord['filterCriteria'];
14
+ };
15
+ hlc: string;
16
+ };
17
+ type RemovePayload = {
18
+ op: 'remove';
19
+ catalogID: string;
20
+ hlc: string;
21
+ };
22
+ /**
23
+ * The verified claim behind a catalog control broadcast. `issuer` is the
24
+ * authenticated author (the verified token `iss`). Every authoritative field
25
+ * comes from the signed payload, so a receiver never trusts the surrounding
26
+ * plaintext frame. Sender-bound: the receiver applies a set only when the
27
+ * catalog's `ownerDID` equals this issuer, and an update/remove only when the
28
+ * locally-stored catalog is owned by this issuer.
29
+ */
30
+ export type VerifiedCatalog = {
31
+ issuer: string;
32
+ claim: SetPayload;
33
+ } | {
34
+ issuer: string;
35
+ claim: UpdatePayload;
36
+ } | {
37
+ issuer: string;
38
+ claim: RemovePayload;
39
+ };
40
+ export declare function signCatalogSet(identity: OwnIdentity, catalog: CatalogRecord): Promise<string>;
41
+ export declare function signCatalogUpdate(identity: OwnIdentity, claim: Omit<UpdatePayload, 'op'>): Promise<string>;
42
+ export declare function signCatalogRemove(identity: OwnIdentity, claim: Omit<RemovePayload, 'op'>): Promise<string>;
43
+ /**
44
+ * Verify a signed catalog token and extract its claim. Returns `null` (never
45
+ * throws) when the token is unparseable, unsigned (`alg: 'none'`), or
46
+ * structurally malformed — an attacker cannot forge an `iss` this way. The
47
+ * issuer is the normalized verified `iss`.
48
+ */
49
+ export declare function verifyCatalog(token: string): Promise<VerifiedCatalog | null>;
50
+ export {};
@@ -0,0 +1,96 @@
1
+ import { isVerifiedToken, normalizeDID, stringifyToken, verifyToken } from '@kokuin/token';
2
+ export async function signCatalogSet(identity, catalog) {
3
+ const signed = await identity.signToken({
4
+ op: 'set',
5
+ catalog
6
+ }, {
7
+ embedLongForm: true
8
+ });
9
+ return stringifyToken(signed);
10
+ }
11
+ export async function signCatalogUpdate(identity, claim) {
12
+ const signed = await identity.signToken({
13
+ op: 'update',
14
+ ...claim
15
+ }, {
16
+ embedLongForm: true
17
+ });
18
+ return stringifyToken(signed);
19
+ }
20
+ export async function signCatalogRemove(identity, claim) {
21
+ const signed = await identity.signToken({
22
+ op: 'remove',
23
+ ...claim
24
+ }, {
25
+ embedLongForm: true
26
+ });
27
+ return stringifyToken(signed);
28
+ }
29
+ function isCatalogRecord(value) {
30
+ if (value == null || typeof value !== 'object') {
31
+ return false;
32
+ }
33
+ const record = value;
34
+ return typeof record.id === 'string' && typeof record.ownerDID === 'string' && typeof record.name === 'string' && typeof record.hlc === 'string';
35
+ }
36
+ /**
37
+ * Verify a signed catalog token and extract its claim. Returns `null` (never
38
+ * throws) when the token is unparseable, unsigned (`alg: 'none'`), or
39
+ * structurally malformed — an attacker cannot forge an `iss` this way. The
40
+ * issuer is the normalized verified `iss`.
41
+ */ export async function verifyCatalog(token) {
42
+ let verified;
43
+ try {
44
+ verified = await verifyToken(token);
45
+ } catch {
46
+ return null;
47
+ }
48
+ if (!isVerifiedToken(verified)) {
49
+ return null;
50
+ }
51
+ const { iss, op } = verified.payload;
52
+ const issuer = normalizeDID(iss);
53
+ if (op === 'set') {
54
+ const { catalog } = verified.payload;
55
+ if (!isCatalogRecord(catalog)) {
56
+ return null;
57
+ }
58
+ return {
59
+ issuer,
60
+ claim: {
61
+ op: 'set',
62
+ catalog
63
+ }
64
+ };
65
+ }
66
+ if (op === 'update') {
67
+ const { catalogID, update, hlc } = verified.payload;
68
+ if (typeof catalogID !== 'string' || typeof hlc !== 'string' || update == null) {
69
+ return null;
70
+ }
71
+ return {
72
+ issuer,
73
+ claim: {
74
+ op: 'update',
75
+ catalogID,
76
+ update: update,
77
+ hlc
78
+ }
79
+ };
80
+ }
81
+ if (op === 'remove') {
82
+ const { catalogID, hlc } = verified.payload;
83
+ if (typeof catalogID !== 'string' || typeof hlc !== 'string') {
84
+ return null;
85
+ }
86
+ return {
87
+ issuer,
88
+ claim: {
89
+ op: 'remove',
90
+ catalogID,
91
+ hlc
92
+ }
93
+ };
94
+ }
95
+ return null;
96
+ }
@@ -0,0 +1,90 @@
1
+ import type { Logger } from '@kubun/logger';
2
+ import type { P2PStoreAPI } from '@kubun/store-p2p';
3
+ import type { GroupAnchor, LedgerLogEntry } from '@kumiai/mls';
4
+ import { type CircleDefValue, type CircleMemberValue, type GroupSettingsValue, type IsAdminAtPosition } from './circle-reducers.js';
5
+ import { type PositionedFoldInput } from './ledger.js';
6
+ export declare const selectCircleMemberEntries: (ledger: ReadonlyArray<LedgerLogEntry>) => PositionedFoldInput<CircleMemberValue>[];
7
+ /**
8
+ * One projected circle row as the fold decides it. `hlc` is the winning active
9
+ * entry's HLC (the row's `hlc` column); `removedAtHLC` is the winning tombstone
10
+ * HLC, or null when the circle is active. A removed circle retains its last
11
+ * active definition so the tombstone row carries real columns — mirroring the
12
+ * LWW store where `markCircleRemoved` only sets `removed_at_hlc` and leaves the
13
+ * name/description intact.
14
+ */
15
+ export type CircleDefProjectionRow = {
16
+ name: string;
17
+ description: string;
18
+ catalogIDs: Array<string>;
19
+ hlc: string;
20
+ removedAtHLC: string | null;
21
+ };
22
+ /**
23
+ * Fold the circle-definition ledger into a per-circle projection covering ALL
24
+ * subjects, active and removed — the active reducer drops removed circles, but a
25
+ * projection must keep a tombstone row for them. Replays in the ledger's own
26
+ * enactment order under the same admin-authority gate as the reducer: an active entry sets the
27
+ * definition and clears any tombstone (re-add), a tombstone marks the row
28
+ * removed while keeping its last active value. A circle whose only entry is a
29
+ * tombstone (no prior active value) produces no row, matching the LWW store's
30
+ * ignore-if-absent removal.
31
+ */
32
+ export declare function projectCircleDefs(entries: Array<PositionedFoldInput<CircleDefValue>>, isAdmin: IsAdminAtPosition): Map<string, CircleDefProjectionRow>;
33
+ /** One projected circle-member row, keyed by the composite ledger subject. */
34
+ export type CircleMemberProjectionRow = {
35
+ circleID: string;
36
+ memberDID: string;
37
+ role: string;
38
+ hlc: string;
39
+ removedAtHLC: string | null;
40
+ };
41
+ /**
42
+ * Fold the circle-member ledger into a per-subject projection covering ALL
43
+ * subjects, active and removed. Same structure as {@link projectCircleDefs}: an
44
+ * active entry sets the role and clears any tombstone, a tombstone marks the row
45
+ * removed while keeping its last active role. A malformed subject is skipped, as
46
+ * is a tombstone with no prior active value. Gates each entry on the same
47
+ * admin-authority check as the reducer's fold — an entry from a non-admin issuer
48
+ * never projects.
49
+ */
50
+ export declare function projectCircleMembers(entries: Array<PositionedFoldInput<CircleMemberValue>>, isAdmin: IsAdminAtPosition): Map<string, CircleMemberProjectionRow>;
51
+ /**
52
+ * Rebuild a circle/settings projection from the whole ledger.
53
+ *
54
+ * The only projection path there is: an entry is judged against the ledger prefix
55
+ * through its own slot, so folding the whole log is the same answer as folding it
56
+ * incrementally, and there is no cached-projection-is-stale case to detect.
57
+ * Authority is the admin fold (`adminAuthorityFromEntries`), so every projection
58
+ * honors one gate. Each `reconcile*` write is unconditional, so the row converges
59
+ * to exactly the fold's decision regardless of prior contents.
60
+ */
61
+ export type ReprojectCircleParams = {
62
+ store: P2PStoreAPI;
63
+ groupID: string;
64
+ anchor: GroupAnchor;
65
+ /**
66
+ * The group's control ledger, in the handle's enactment order. Passed in rather
67
+ * than read: these projections run where the handle is already open, and every
68
+ * entry they fold is one the handle verified on the way in.
69
+ */
70
+ ledger: ReadonlyArray<LedgerLogEntry>;
71
+ logger?: Logger;
72
+ };
73
+ export declare function reprojectCircleDefs(params: ReprojectCircleParams): Promise<void>;
74
+ export declare function reprojectCircleMembers(params: ReprojectCircleParams): Promise<void>;
75
+ /**
76
+ * Reproject group settings onto the group row. Settings are PARTIAL per-field
77
+ * (no tombstone), so the committed reducer's active map IS the full projection:
78
+ * fold it under the admin gate, then write the merged fields via the
79
+ * unconditional `updateGroup`. A field no entry ever set stays at the group's
80
+ * original value (creation-time name/description).
81
+ */
82
+ /**
83
+ * Fold the group's settings ledger under the admin gate and return the merged
84
+ * per-field value, or `undefined` when no authorized settings entry has been
85
+ * folded. This is the same fold {@link reprojectGroupSettings} materializes,
86
+ * exposed so a producer can read the CURRENT folded designation before authoring
87
+ * — what a device projects and what it checks before writing cannot then drift.
88
+ */
89
+ export declare function foldGroupSettings(ledger: ReadonlyArray<LedgerLogEntry>, groupID: string, anchor: GroupAnchor): GroupSettingsValue | undefined;
90
+ export declare function reprojectGroupSettings(params: ReprojectCircleParams): Promise<void>;
@@ -0,0 +1,202 @@
1
+ import { selectRoleEntries } from './admin-roster.js';
2
+ import { adminAuthorityFromEntries, authorizedEntries, CIRCLE_DEF_ENTRY_TYPE, CIRCLE_MEMBER_ENTRY_TYPE, createGroupSettingsReducer, decodeCircleMemberSubject, GROUP_SETTINGS_ENTRY_TYPE } from './circle-reducers.js';
3
+ import { entryOrd, foldLedgerEntries } from './ledger.js';
4
+ function isRemovedDef(value) {
5
+ return 'removed' in value && value.removed === true;
6
+ }
7
+ function isRemovedMember(value) {
8
+ return 'removed' in value && value.removed === true;
9
+ }
10
+ /**
11
+ * Build a selector for one ledger type, carrying each entry's slot in the WHOLE
12
+ * ledger so its authority can be judged against the prefix that precedes it.
13
+ *
14
+ * Entries come from the handle's own log, and every door into it verifies —
15
+ * `GroupHandle.applyLedgerEntries`, `bootstrapLedger` and an accepted commit
16
+ * each re-verify the tokens they admit — so these are pure filters over an
17
+ * in-memory array: no re-verification, and nothing to memoize. They replaced
18
+ * loaders that re-verified every stored `signed_token` on each call, which was
19
+ * needed only because the SQL row they read could not vouch for itself.
20
+ *
21
+ * Selecting by `type` is what licenses the cast: the log is typed over an unknown
22
+ * claim value, and the type is the claim about what that value is.
23
+ */ function createEntrySelector(entryType) {
24
+ return (ledger)=>{
25
+ const entries = [];
26
+ for (const [position, held] of ledger.entries()){
27
+ if (held.verified.entry.type !== entryType) {
28
+ continue;
29
+ }
30
+ entries.push({
31
+ verified: held.verified,
32
+ entryID: held.entryID,
33
+ position
34
+ });
35
+ }
36
+ return entries;
37
+ };
38
+ }
39
+ const selectCircleDefEntries = createEntrySelector(CIRCLE_DEF_ENTRY_TYPE);
40
+ export const selectCircleMemberEntries = createEntrySelector(CIRCLE_MEMBER_ENTRY_TYPE);
41
+ const selectGroupSettingsEntries = createEntrySelector(GROUP_SETTINGS_ENTRY_TYPE);
42
+ /**
43
+ * Fold the circle-definition ledger into a per-circle projection covering ALL
44
+ * subjects, active and removed — the active reducer drops removed circles, but a
45
+ * projection must keep a tombstone row for them. Replays in the ledger's own
46
+ * enactment order under the same admin-authority gate as the reducer: an active entry sets the
47
+ * definition and clears any tombstone (re-add), a tombstone marks the row
48
+ * removed while keeping its last active value. A circle whose only entry is a
49
+ * tombstone (no prior active value) produces no row, matching the LWW store's
50
+ * ignore-if-absent removal.
51
+ */ export function projectCircleDefs(entries, isAdmin) {
52
+ const rows = new Map();
53
+ for (const { verified } of authorizedEntries(entries, isAdmin)){
54
+ const { subject, value } = verified.entry;
55
+ const hlc = entryOrd(verified.entry);
56
+ // Kubun stamps `ord` on every circle entry it signs; one without is foreign
57
+ // or malformed. Skipping is fail-closed — projecting it would have to invent
58
+ // an HLC, and on a tombstone a `null` `removed_at_hlc` reads as ACTIVE and
59
+ // silently revives the row.
60
+ if (hlc == null) continue;
61
+ if (isRemovedDef(value)) {
62
+ const existing = rows.get(subject);
63
+ if (existing == null) continue;
64
+ rows.set(subject, {
65
+ ...existing,
66
+ removedAtHLC: hlc
67
+ });
68
+ } else {
69
+ rows.set(subject, {
70
+ name: value.name,
71
+ description: value.description,
72
+ catalogIDs: value.catalogIDs,
73
+ hlc,
74
+ removedAtHLC: null
75
+ });
76
+ }
77
+ }
78
+ return rows;
79
+ }
80
+ /**
81
+ * Fold the circle-member ledger into a per-subject projection covering ALL
82
+ * subjects, active and removed. Same structure as {@link projectCircleDefs}: an
83
+ * active entry sets the role and clears any tombstone, a tombstone marks the row
84
+ * removed while keeping its last active role. A malformed subject is skipped, as
85
+ * is a tombstone with no prior active value. Gates each entry on the same
86
+ * admin-authority check as the reducer's fold — an entry from a non-admin issuer
87
+ * never projects.
88
+ */ export function projectCircleMembers(entries, isAdmin) {
89
+ const rows = new Map();
90
+ for (const { verified } of authorizedEntries(entries, isAdmin)){
91
+ const { subject, value } = verified.entry;
92
+ const hlc = entryOrd(verified.entry);
93
+ // See {@link projectCircleDefs}: no `ord` means foreign or malformed, and a
94
+ // null tombstone HLC would read as active.
95
+ if (hlc == null) continue;
96
+ if (isRemovedMember(value)) {
97
+ const existing = rows.get(subject);
98
+ if (existing == null) continue;
99
+ rows.set(subject, {
100
+ ...existing,
101
+ removedAtHLC: hlc
102
+ });
103
+ } else {
104
+ const decoded = decodeCircleMemberSubject(subject);
105
+ if (decoded == null) continue;
106
+ rows.set(subject, {
107
+ circleID: decoded.circleID,
108
+ memberDID: decoded.memberDID,
109
+ role: value.role,
110
+ hlc,
111
+ removedAtHLC: null
112
+ });
113
+ }
114
+ }
115
+ return rows;
116
+ }
117
+ function adminAuthorityFor(params) {
118
+ return adminAuthorityFromEntries(selectRoleEntries(params.ledger), params.anchor, params.groupID);
119
+ }
120
+ /**
121
+ * The group's CURRENT open-circle set, folded from the settings ledger under the
122
+ * supplied admin authority. `open` is a derived cache of this set on each circle
123
+ * row, so both the settings→circles and circles←settings reprojection paths
124
+ * derive it from the one authoritative fold and converge regardless of which
125
+ * entry (the `circle.def` or the `group.settings` opening it) arrives first.
126
+ */ function foldOpenCircleIDs(ledger, groupID, anchor, isAdmin) {
127
+ const entries = selectGroupSettingsEntries(ledger);
128
+ const { settings } = foldLedgerEntries(authorizedEntries(entries, isAdmin), anchor, createGroupSettingsReducer());
129
+ return new Set(settings.get(groupID)?.openCircleIDs ?? []);
130
+ }
131
+ export async function reprojectCircleDefs(params) {
132
+ const { store, groupID, anchor, ledger } = params;
133
+ const isAdmin = adminAuthorityFor(params);
134
+ const entries = selectCircleDefEntries(ledger);
135
+ const openIDs = foldOpenCircleIDs(ledger, groupID, anchor, isAdmin);
136
+ const rows = projectCircleDefs(entries, isAdmin);
137
+ for (const [circleID, row] of rows){
138
+ await store.reconcileCircleProjection({
139
+ id: circleID,
140
+ groupID,
141
+ name: row.name,
142
+ description: row.description,
143
+ catalogIDs: row.catalogIDs,
144
+ hlc: row.hlc,
145
+ removedAtHLC: row.removedAtHLC,
146
+ open: openIDs.has(circleID)
147
+ });
148
+ }
149
+ }
150
+ export async function reprojectCircleMembers(params) {
151
+ const { store, ledger } = params;
152
+ const isAdmin = adminAuthorityFor(params);
153
+ const entries = selectCircleMemberEntries(ledger);
154
+ const rows = projectCircleMembers(entries, isAdmin);
155
+ for (const row of rows.values()){
156
+ await store.reconcileCircleMemberProjection({
157
+ circleID: row.circleID,
158
+ memberDID: row.memberDID,
159
+ role: row.role,
160
+ hlc: row.hlc,
161
+ removedAtHLC: row.removedAtHLC
162
+ });
163
+ }
164
+ }
165
+ /**
166
+ * Reproject group settings onto the group row. Settings are PARTIAL per-field
167
+ * (no tombstone), so the committed reducer's active map IS the full projection:
168
+ * fold it under the admin gate, then write the merged fields via the
169
+ * unconditional `updateGroup`. A field no entry ever set stays at the group's
170
+ * original value (creation-time name/description).
171
+ */ /**
172
+ * Fold the group's settings ledger under the admin gate and return the merged
173
+ * per-field value, or `undefined` when no authorized settings entry has been
174
+ * folded. This is the same fold {@link reprojectGroupSettings} materializes,
175
+ * exposed so a producer can read the CURRENT folded designation before authoring
176
+ * — what a device projects and what it checks before writing cannot then drift.
177
+ */ export function foldGroupSettings(ledger, groupID, anchor) {
178
+ const isAdmin = adminAuthorityFromEntries(selectRoleEntries(ledger), anchor, groupID);
179
+ const { settings } = foldLedgerEntries(authorizedEntries(selectGroupSettingsEntries(ledger), isAdmin), anchor, createGroupSettingsReducer());
180
+ return settings.get(groupID);
181
+ }
182
+ export async function reprojectGroupSettings(params) {
183
+ const { store, groupID, anchor, ledger } = params;
184
+ const merged = foldGroupSettings(ledger, groupID, anchor);
185
+ if (merged != null) {
186
+ // name/description live on the group row directly.
187
+ await store.updateGroup(groupID, {
188
+ name: merged.name,
189
+ description: merged.description
190
+ });
191
+ }
192
+ // `openCircleIDs` is the authoritative open set; project it onto each circle's
193
+ // `open` flag (settings → circles). Runs unconditionally so a fold with no open
194
+ // circles closes every circle in the group.
195
+ await store.reconcileCircleOpenFlags(groupID, merged?.openCircleIDs ?? []);
196
+ // `commitHub` is the group's consensus commit-hub designation; materialize it
197
+ // onto the local `is_commit_hub` flag so the fold, not each device's first
198
+ // hub-bind, decides which bound hub the group commits through. A fold carrying
199
+ // no designation is a no-op here, leaving the local auto-designated flag in
200
+ // place until an admin authors one.
201
+ await store.reconcileCommitHub(groupID, merged?.commitHub);
202
+ }
@@ -0,0 +1,139 @@
1
+ import type { GroupAnchor } from '@kumiai/mls';
2
+ import { type AdminRoleValue } from './admin-roster.js';
3
+ import type { LedgerReducer, PositionedFoldInput } from './ledger.js';
4
+ /**
5
+ * The fold's authority gate: was `issuer` a group admin when the entry at
6
+ * `position` was enacted?
7
+ *
8
+ * The LEDGER admin-fold ONLY — membership and MLS-removal are enforced outside the
9
+ * projection, never conjoined here, so the fold stays synchronous and every peer
10
+ * folding the same ledger converges. Applied by {@link authorizedEntries} BEFORE
11
+ * the fold, not inside a reducer's `verifyAuthority`: that hook sees only the
12
+ * entry, and the prefix an entry is judged against is a property of its slot.
13
+ */
14
+ export type IsAdminAtPosition = (issuer: string, position: number) => boolean;
15
+ /**
16
+ * Drop the entries whose issuer lacked admin authority at their own slot, keeping
17
+ * the rest in ledger order.
18
+ *
19
+ * INCLUSIVE compare against the ledger prefix through the entry's own position: a
20
+ * grant authorizes from its own slot onward, a demotion stops authorizing from its
21
+ * own slot onward.
22
+ */
23
+ export declare function authorizedEntries<TValue>(entries: Array<PositionedFoldInput<TValue>>, isAdmin: IsAdminAtPosition): Array<PositionedFoldInput<TValue>>;
24
+ /**
25
+ * Ledger entry `type` for a circle-membership claim. Every control-ledger type
26
+ * kubun mints lives under `kubun.*` — that namespace is kubun's, and the split
27
+ * between "kubun projects this" and "the MLS library interprets this" is drawn
28
+ * on it. Namespaces the library reserves for itself are its own to name and
29
+ * rename, so kubun stays out of them and never restates them.
30
+ */
31
+ export declare const CIRCLE_MEMBER_ENTRY_TYPE = "kubun.circle.member";
32
+ /**
33
+ * The claim carried by a circle-member entry. The active form names the
34
+ * member's role within the circle; the tombstone form removes them. Both are
35
+ * whole-value: the last entry for a subject in the ledger replaces the projected
36
+ * state outright, so the value alone decides active-with-role vs removed. `role`
37
+ * mirrors the store's free-form role column (no fixed enum), so it stays a
38
+ * string rather than inventing a circle-specific role type.
39
+ */
40
+ export type CircleMemberValue = {
41
+ role: string;
42
+ } | {
43
+ removed: true;
44
+ };
45
+ /**
46
+ * Deterministic composite subject for a circle member. The signed entry's
47
+ * `subject` encodes both the circle and the member, so the ledger's `subject`
48
+ * column uniquely identifies the pair and per-member LWW ordering + dedup work
49
+ * without consulting the value.
50
+ *
51
+ * Unambiguous because the member is a normalized DID (`did:method:...`), which
52
+ * never contains the `/member:` separator: every reachable separator therefore
53
+ * sits between the circle id and the DID, and the decoder splits on the last
54
+ * one. The labeled `circle:` / `/member:` prefixes also keep the encoding
55
+ * distinct from any other reducer's subject scheme.
56
+ */
57
+ export declare function encodeCircleMemberSubject(circleID: string, memberDID: string): string;
58
+ /** Tolerant inverse of {@link encodeCircleMemberSubject}: null on a malformed subject. */
59
+ export declare function decodeCircleMemberSubject(subject: string): {
60
+ circleID: string;
61
+ memberDID: string;
62
+ } | null;
63
+ /** Ledger entry `type` for a circle definition (create / update / delete). */
64
+ export declare const CIRCLE_DEF_ENTRY_TYPE = "kubun.circle.def";
65
+ /**
66
+ * The claim carried by a circle-definition entry. The active form is the whole
67
+ * circle definition; the tombstone form deletes it. Both are whole-value: the
68
+ * last entry for a circle in the ledger replaces the projected definition outright,
69
+ * so create and update share one shape and the value alone decides present vs
70
+ * deleted.
71
+ */
72
+ export type CircleDefValue = {
73
+ name: string;
74
+ description: string;
75
+ catalogIDs: Array<string>;
76
+ } | {
77
+ removed: true;
78
+ };
79
+ /**
80
+ * Ledger entry `type` for a group-settings claim. Namespaced under `kubun.*`
81
+ * like every kubun control-ledger type — a bare `group.settings` sits in the
82
+ * namespace the MLS library reserves, so a commit carrying it is refused whole.
83
+ */
84
+ export declare const GROUP_SETTINGS_ENTRY_TYPE = "kubun.group.settings";
85
+ /**
86
+ * The claim carried by a group-settings entry. FIELD-WISE LWW, no tombstone: an
87
+ * omitted field leaves the prior value untouched, while `''` (or an empty list) is
88
+ * a REAL value that clears it.
89
+ *
90
+ * `commitHub` is the group's consensus commit-hub designation — the shared hub
91
+ * identity, NOT a local hub-row id. Folding it as group state is what stops two
92
+ * devices forking the group by each committing through their own first-bound hub.
93
+ */
94
+ export type GroupSettingsValue = {
95
+ name?: string;
96
+ description?: string;
97
+ openCircleIDs?: Array<string>;
98
+ commitHub?: {
99
+ url: string;
100
+ serverDID?: string;
101
+ };
102
+ };
103
+ /** Folded group settings, keyed by groupID (the entry subject). */
104
+ export type GroupSettingsState = {
105
+ settings: Map<string, GroupSettingsValue>;
106
+ };
107
+ /**
108
+ * Per-field last-writer-wins group settings. The signed entry's `subject` is the
109
+ * groupID directly, so no decode is needed. Replayed in the ledger's own
110
+ * enactment order, so for each field the LAST entry that SETS that field wins
111
+ * naturally.
112
+ *
113
+ * Merges PER FIELD rather than replacing the whole value the way `circle.def` /
114
+ * `circle.member` do, because these fields are touched in isolation: a name-only
115
+ * update must not clobber a previously-set description. Presence is
116
+ * `value.field !== undefined`, not `'field' in value`, so an explicit `undefined`
117
+ * means no change while `''` or an empty list CLEARS the field.
118
+ *
119
+ * Unauthorized entries never reach here — see {@link authorizedEntries}.
120
+ */
121
+ export declare function createGroupSettingsReducer(): LedgerReducer<GroupSettingsValue, GroupSettingsState>;
122
+ /**
123
+ * Build an {@link IsAdminAtPosition} predicate from the group's admin-role ledger
124
+ * entries. It re-folds the admin sub-ledger through each queried slot, so the
125
+ * answer is rotation-aware (was-admin-then, not is-admin-now): a key granted
126
+ * admin at slot n authorizes entries from slot n onward, and a later demotion
127
+ * stops authorizing from its own slot onward. The compare is inclusive
128
+ * (`position <= throughPosition`), which matters only for a role entry judging
129
+ * itself — a circle entry never shares a slot with the grant that authorizes it.
130
+ *
131
+ * Callers select the role entries once (via `selectRoleEntries`) and reuse the
132
+ * returned predicate across the circle / def / settings folds.
133
+ *
134
+ * LEDGER-ADMIN ONLY — it does not check the issuer's MLS membership, so an admin
135
+ * removed from the MLS group whose admin role was never revoked on the ledger still
136
+ * has its entries folded in. Closing that needs removal to write a ledger
137
+ * revocation atomically with the roster delta; out of scope here.
138
+ */
139
+ export declare function adminAuthorityFromEntries(adminEntries: Array<PositionedFoldInput<AdminRoleValue>>, anchor: GroupAnchor, groupID: string): IsAdminAtPosition;