@optimystic/db-core 0.25.1 → 0.27.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 (104) hide show
  1. package/dist/src/cohort-topic/membership/verifier.d.ts +8 -0
  2. package/dist/src/cohort-topic/membership/verifier.d.ts.map +1 -1
  3. package/dist/src/cohort-topic/membership/verifier.js +17 -0
  4. package/dist/src/cohort-topic/membership/verifier.js.map +1 -1
  5. package/dist/src/cohort-topic/promotion.d.ts +25 -0
  6. package/dist/src/cohort-topic/promotion.d.ts.map +1 -1
  7. package/dist/src/cohort-topic/promotion.js +58 -11
  8. package/dist/src/cohort-topic/promotion.js.map +1 -1
  9. package/dist/src/cohort-topic/wire/primitives.d.ts +6 -0
  10. package/dist/src/cohort-topic/wire/primitives.d.ts.map +1 -1
  11. package/dist/src/cohort-topic/wire/primitives.js +6 -0
  12. package/dist/src/cohort-topic/wire/primitives.js.map +1 -1
  13. package/dist/src/cohort-topic/wire/types.d.ts +5 -0
  14. package/dist/src/cohort-topic/wire/types.d.ts.map +1 -1
  15. package/dist/src/cohort-topic/wire/validate.d.ts.map +1 -1
  16. package/dist/src/cohort-topic/wire/validate.js +10 -19
  17. package/dist/src/cohort-topic/wire/validate.js.map +1 -1
  18. package/dist/src/collection/collection.d.ts +8 -1
  19. package/dist/src/collection/collection.d.ts.map +1 -1
  20. package/dist/src/collection/collection.js +19 -3
  21. package/dist/src/collection/collection.js.map +1 -1
  22. package/dist/src/matchmaking/wire.d.ts +2 -2
  23. package/dist/src/matchmaking/wire.d.ts.map +1 -1
  24. package/dist/src/matchmaking/wire.js +3 -3
  25. package/dist/src/matchmaking/wire.js.map +1 -1
  26. package/dist/src/network/struct.d.ts +5 -5
  27. package/dist/src/network/struct.d.ts.map +1 -1
  28. package/dist/src/testing/index.d.ts +1 -0
  29. package/dist/src/testing/index.d.ts.map +1 -1
  30. package/dist/src/testing/index.js +1 -0
  31. package/dist/src/testing/index.js.map +1 -1
  32. package/dist/src/testing/refresh-probe.d.ts +31 -0
  33. package/dist/src/testing/refresh-probe.d.ts.map +1 -0
  34. package/dist/src/testing/refresh-probe.js +39 -0
  35. package/dist/src/testing/refresh-probe.js.map +1 -0
  36. package/dist/src/testing/test-transactor.d.ts +58 -5
  37. package/dist/src/testing/test-transactor.d.ts.map +1 -1
  38. package/dist/src/testing/test-transactor.js +87 -4
  39. package/dist/src/testing/test-transactor.js.map +1 -1
  40. package/dist/src/transaction/coordinator.d.ts +90 -6
  41. package/dist/src/transaction/coordinator.d.ts.map +1 -1
  42. package/dist/src/transaction/coordinator.js +311 -90
  43. package/dist/src/transaction/coordinator.js.map +1 -1
  44. package/dist/src/transaction/errors.d.ts +42 -0
  45. package/dist/src/transaction/errors.d.ts.map +1 -1
  46. package/dist/src/transaction/errors.js +50 -0
  47. package/dist/src/transaction/errors.js.map +1 -1
  48. package/dist/src/transaction/index.d.ts +1 -1
  49. package/dist/src/transaction/index.d.ts.map +1 -1
  50. package/dist/src/transaction/index.js +1 -1
  51. package/dist/src/transaction/index.js.map +1 -1
  52. package/dist/src/transaction/session.d.ts +7 -3
  53. package/dist/src/transaction/session.d.ts.map +1 -1
  54. package/dist/src/transaction/session.js +7 -3
  55. package/dist/src/transaction/session.js.map +1 -1
  56. package/dist/src/transaction/transaction.d.ts +12 -2
  57. package/dist/src/transaction/transaction.d.ts.map +1 -1
  58. package/dist/src/transaction/transaction.js.map +1 -1
  59. package/dist/src/transactor/network-transactor.d.ts +1 -1
  60. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  61. package/dist/src/transactor/network-transactor.js +6 -26
  62. package/dist/src/transactor/network-transactor.js.map +1 -1
  63. package/dist/src/transactor/transactor-source.d.ts +4 -6
  64. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  65. package/dist/src/transactor/transactor-source.js +4 -6
  66. package/dist/src/transactor/transactor-source.js.map +1 -1
  67. package/dist/src/transform/atomic.d.ts.map +1 -1
  68. package/dist/src/transform/atomic.js +7 -0
  69. package/dist/src/transform/atomic.js.map +1 -1
  70. package/dist/src/transform/base-pins.d.ts +48 -0
  71. package/dist/src/transform/base-pins.d.ts.map +1 -0
  72. package/dist/src/transform/base-pins.js +65 -0
  73. package/dist/src/transform/base-pins.js.map +1 -0
  74. package/dist/src/transform/digest.d.ts +4 -2
  75. package/dist/src/transform/digest.d.ts.map +1 -1
  76. package/dist/src/transform/digest.js +14 -19
  77. package/dist/src/transform/digest.js.map +1 -1
  78. package/dist/src/transform/tracker.d.ts +31 -3
  79. package/dist/src/transform/tracker.d.ts.map +1 -1
  80. package/dist/src/transform/tracker.js +105 -13
  81. package/dist/src/transform/tracker.js.map +1 -1
  82. package/package.json +1 -1
  83. package/src/cohort-topic/membership/verifier.ts +487 -461
  84. package/src/cohort-topic/promotion.ts +86 -14
  85. package/src/cohort-topic/wire/primitives.ts +195 -188
  86. package/src/cohort-topic/wire/types.ts +480 -475
  87. package/src/cohort-topic/wire/validate.ts +504 -512
  88. package/src/collection/collection.ts +19 -3
  89. package/src/matchmaking/wire.ts +605 -603
  90. package/src/network/struct.ts +5 -5
  91. package/src/testing/index.ts +1 -0
  92. package/src/testing/refresh-probe.ts +52 -0
  93. package/src/testing/test-transactor.ts +100 -4
  94. package/src/transaction/coordinator.ts +312 -99
  95. package/src/transaction/errors.ts +141 -91
  96. package/src/transaction/index.ts +1 -1
  97. package/src/transaction/session.ts +7 -3
  98. package/src/transaction/transaction.ts +12 -2
  99. package/src/transactor/network-transactor.ts +1025 -1045
  100. package/src/transactor/transactor-source.ts +170 -172
  101. package/src/transform/atomic.ts +6 -0
  102. package/src/transform/base-pins.ts +83 -0
  103. package/src/transform/digest.ts +14 -19
  104. package/src/transform/tracker.ts +108 -15
@@ -1,475 +1,480 @@
1
- /**
2
- * Cohort-topic substrate — V1 wire message types.
3
- *
4
- * Transcribed from `docs/cohort-topic.md` §Wire formats. These interfaces are the canonical
5
- * on-the-wire shapes for the cohort-topic RPC surface; the codec in `./codec.ts` serializes
6
- * them as length-prefixed UTF-8 JSON.
7
- *
8
- * Conventions (mandated by §Wire formats):
9
- * - Every byte-typed field is carried as a base64url string (no padding) — never raw bytes.
10
- * - Every timestamp is unix milliseconds.
11
- * - `appPayload` is an opaque base64url string carrying application-defined bytes. The wire layer
12
- * never interprets it; reactivity / matchmaking define their own structures and serialize them
13
- * into this slot.
14
- */
15
-
16
- /** Registration request. Walks toward the root via FRET `RouteAndMaybeAct`. */
17
- export interface RegisterV1 {
18
- v: 1;
19
- /** Topic id, 32 bytes, base64url. */
20
- topicId: string;
21
- /** Tier, 0..3. */
22
- tier: number;
23
- /** Current walk position `d`. */
24
- treeTier: number;
25
- /** Participant's ring coord, 32 bytes, base64url. */
26
- participantCoord: string;
27
- /** Registration TTL in ms (default 90000; Edge 60000). */
28
- ttl: number;
29
- /** True on a root cold-start request. */
30
- bootstrap?: boolean;
31
- /**
32
- * Follow-on cold-start request: set on the **dedicated re-issue** the participant sends after a
33
- * `Promoted` redirect target answered {@link RegisterResult} `no_state` (the redirect points at a
34
- * tier-`(d+1)` child cohort that does not exist yet). It tells that cold child "I am here because
35
- * your parent redirected me — instantiate", which drives the cold-start gate
36
- * (`shouldInstantiate({ followOn, … })`) exactly as `bootstrap` drives it at the root. Constraints:
37
- * always `treeTier >= 1` (a follow-on is a deeper-than-root growth point, never the root); mutually
38
- * exclusive with {@link bootstrap} and {@link probe} (the walk sets at most one). Because a wire flag is
39
- * participant-forgeable, safety does NOT come from its provenance: a `followOn: true` register is gated
40
- * by the **same** `bootstrapEvidence` policy a `bootstrap: true` cold-root register passes (§Anti-DoS),
41
- * so it carries the identical proof in {@link bootstrapEvidence} and pays the identical anti-abuse cost.
42
- * Covered by `signature` (appended to `registerSigningPayload`) so a MITM cannot strip or flip it.
43
- */
44
- followOn?: boolean;
45
- /**
46
- * Read-only lookup probe: classify + return the cohort snapshot without admitting (no record, no
47
- * arrival, no promotion trigger, no topic-budget touch, and **never** a cold-start instantiation /
48
- * `bootstrap`). A probe walks to the responsible cohort exactly as a register does and resolves the
49
- * same {@link RegisterReplyV1} (`no_state` cold, `promoted` redirected, `accepted` snapshot served),
50
- * but the terminal member action is the read-only {@link RegisterReplyV1} classify rather than an
51
- * admission. Drives {@link import("../service.js").CohortTopicService.lookup}. Mutually exclusive with
52
- * `bootstrap` (the walk never sets `bootstrap` on a probe).
53
- */
54
- probe?: boolean;
55
- /** Opaque application-defined bytes, base64url. */
56
- appPayload?: string;
57
- /**
58
- * Cold-start bootstrap-evidence envelope, base64url (a `BootstrapEvidenceEnvelopeV1` — see
59
- * `../antidos/bootstrap-evidence-envelope.js`). Present on a `bootstrap: true` root register **or** a
60
- * `followOn: true` deeper-tier re-issue — both are cold-start requests gated by the identical evidence
61
- * policy (§Anti-DoS), so both carry this proof; carries the tier-dependent proof a cold cohort demands
62
- * (proof-of-work / reputation endorsement / signed parent reference — §Anti-DoS). This is a
63
- * **dedicated** field, NOT `appPayload`: the cohort
64
- * copies `appPayload` verbatim into the registration's `appState` and replicates it cluster-wide,
65
- * whereas the bootstrap evidence is parsed-and-checked by the substrate, **covered by `signature`**
66
- * (so a MITM cannot strip or swap it), and never stored as appState. Empty string is treated as
67
- * absent (it normalizes to the same signed-image placeholder as an absent field).
68
- */
69
- bootstrapEvidence?: string;
70
- /** Unix ms. */
71
- timestamp: number;
72
- /** 16 random bytes, base64url. */
73
- correlationId: string;
74
- /** Participant peer-key signature, base64url. */
75
- signature: string;
76
- }
77
-
78
- export type RegisterResult =
79
- | "accepted"
80
- | "no_state"
81
- | "promoted"
82
- | "unwilling_member"
83
- | "unwilling_cohort";
84
-
85
- /** Registration reply. Field presence is keyed by {@link RegisterResult}. */
86
- export interface RegisterReplyV1 {
87
- v: 1;
88
- result: RegisterResult;
89
- // accepted:
90
- /** PeerId. */
91
- primary?: string;
92
- /** PeerIds, 1-2. */
93
- backups?: string[];
94
- /** 32 bytes, base64url. */
95
- cohortEpoch?: string;
96
- /** Full cohort PeerIds, for client cache. */
97
- cohortMembers?: string[];
98
- /** Present on `accepted` and `promoted` only. */
99
- topicTraffic?: TopicTrafficV1;
100
- // promoted:
101
- /** `d+1` typically; may leap. */
102
- targetTier?: number;
103
- // unwilling_member:
104
- /** PeerIds within the same cohort to try. */
105
- candidateMembers?: string[];
106
- // unwilling_cohort:
107
- retryAfterMs?: number;
108
- /** Human-readable, optional. */
109
- reason?: string;
110
- }
111
-
112
- /** Coarse traffic barometer attached to accepted/promoted replies. */
113
- export interface TopicTrafficV1 {
114
- windowSeconds: number;
115
- arrivalsPerMin: number;
116
- queriesPerMin: number;
117
- directParticipants: number;
118
- childCohortCount: number;
119
- }
120
-
121
- /** Registration renewal (ping). */
122
- export interface RenewV1 {
123
- v: 1;
124
- topicId: string;
125
- /** PeerId. */
126
- participantId: string;
127
- /** Matches the original {@link RegisterV1}. */
128
- correlationId: string;
129
- timestamp: number;
130
- /**
131
- * True on a crash-failover re-attach — the participant attests it could not reach its primary and
132
- * asks the contacted backup to promote itself. Absent/false on a normal ping. Signed (part of the
133
- * renew body) so a member can trust the attestation and a MITM cannot flip a ping into a promotion.
134
- */
135
- reattach?: boolean;
136
- /**
137
- * True on a withdraw tombstone — the participant attests it is leaving; the holder evicts the
138
- * record immediately and gossips the eviction (freeing the cohort slot rather than holding it for a
139
- * full TTL). Absent/false on a ping or reattach. Signed (part of the renew body) so a third party
140
- * cannot evict someone else's registration. Mutually exclusive with {@link reattach} (the participant
141
- * never sets both; on a malformed frame that sets both, the cohort side honors `withdraw`).
142
- */
143
- withdraw?: boolean;
144
- signature: string;
145
- }
146
-
147
- /** Renewal reply. */
148
- export interface RenewReplyV1 {
149
- v: 1;
150
- /**
151
- * `withdrawn` answers a {@link RenewV1.withdraw} tombstone the holder accepted (record evicted +
152
- * gossiped); the participant ignores it (it is leaving), but the distinct result lets the cohort
153
- * side branch unambiguously (re-touch the topic budget down, never count an arrival).
154
- */
155
- result: "ok" | "unknown_registration" | "primary_moved" | "withdrawn";
156
- // primary_moved:
157
- newPrimary?: string;
158
- newBackups?: string[];
159
- cohortEpoch?: string;
160
- }
161
-
162
- /**
163
- * Child cohort → parent cohort link: "I am the tier-`d` cohort at {@link childCohortCoord}; record me as
164
- * your child." A freshly cold-started deeper forwarder sends this to its tier-`(d 1)` parent (routed the
165
- * same way a participant register is, over `routeAndAct` to the parent coord) so the parent authenticates,
166
- * records the child, and acks replacing the interim plain participant-register the child used to send.
167
- */
168
- export interface ChildLinkV1 {
169
- v: 1;
170
- /** Topic id, 32 bytes, base64url. */
171
- topicId: string;
172
- /**
173
- * The child cohort's served coord `coord_d(childParticipantCoord, topicId)`, 32 bytes base64url — the
174
- * coord the parent verifies the threshold signature against (it looks up the child cohort's
175
- * {@link MembershipCertV1} at this coord).
176
- */
177
- childCohortCoord: string;
178
- /**
179
- * A representative participant coord in the child's prefix-shard (the seed the child engine was
180
- * instantiated at). Lets the parent *deterministically bind the parent-child relationship*: it recomputes
181
- * `coord_childTier(childParticipantCoord, topicId) == childCohortCoord` AND
182
- * `coord_(childTier-1)(childParticipantCoord, topicId) == this parent's served coord`. Any participant
183
- * sharing the child's `d·log₂F`-bit prefix yields the same pair, so it is a representative, not an
184
- * identity. 32 bytes base64url.
185
- */
186
- childParticipantCoord: string;
187
- /** Child tree tier `d` (always ≥ 1 — the root has no parent to link to). Parent serves `d − 1`. */
188
- childTier: number;
189
- /** Op capacity tier T0..T3 (stamped so a real parent validates the frame's tier). */
190
- tier: number;
191
- /** Unix ms; the parent's per-child freshness/ordering key (strictly-newer wins). */
192
- effectiveAt: number;
193
- /** Child cohort threshold signature over `childLinkSigningPayload`, base64url. Empty in the key-less interim. */
194
- thresholdSig: string;
195
- /** Signing members' PeerIds, `>= minSigs`, base64url. Empty in the key-less interim. */
196
- signers: string[];
197
- /** Child cohort epoch, 32 bytes base64url (the epoch the threshold sig was collected under). */
198
- cohortEpoch: string;
199
- }
200
-
201
- /** Parent → child ack. `linked` flips the child `awaiting_parent → serving`; `rejected` keeps it awaiting. */
202
- export interface ChildLinkReplyV1 {
203
- v: 1;
204
- result: "linked" | "rejected";
205
- /** Human-readable, optional. */
206
- reason?: string;
207
- }
208
-
209
- /** Threshold-signed promotion notice. */
210
- export interface PromotionNoticeV1 {
211
- v: 1;
212
- topicId: string;
213
- fromTier: number;
214
- /** Typically `fromTier + 1`. */
215
- toTier: number;
216
- /**
217
- * The served coord `coord_d(participantCoord, topicId)` the deciding cohort sits at, 32 bytes, base64url
218
- * the same value as that cohort's {@link MembershipCertV1.cohortCoord}, which the threshold signature
219
- * verifies against. The receiver routes the notice to the local engine for exactly this coord rather than
220
- * scanning for the first engine matching `(topic, tier)`, so a node serving several sibling cohorts for
221
- * one `(topic, tier)` (possible at `d ≥ 1`, where each cohort has its own served coord) applies the notice
222
- * to the cohort that actually produced it. Covered by the threshold signature rewriting it breaks verification.
223
- */
224
- cohortCoord: string;
225
- /** Unix ms. */
226
- effectiveAt: number;
227
- /** Cohort threshold signature, base64url. */
228
- thresholdSig: string;
229
- /** PeerIds, `>= minSigs`. */
230
- signers: string[];
231
- cohortEpoch: string;
232
- }
233
-
234
- /** Threshold-signed demotion notice. */
235
- export interface DemotionNoticeV1 {
236
- v: 1;
237
- topicId: string;
238
- tier: number;
239
- /** 32 bytes, base64url. */
240
- parentCohortCoord: string;
241
- /**
242
- * The served coord `coord_d(participantCoord, topicId)` the deciding cohort sits at, 32 bytes, base64url —
243
- * the same served-coord concept as {@link PromotionNoticeV1.cohortCoord}. Distinct from
244
- * {@link parentCohortCoord} (the tier-`(d − 1)` parent this demotion hands off to): `cohortCoord` names the
245
- * *demoting* cohort and is what the receiver routes + verifies by. Covered by the threshold signature.
246
- */
247
- cohortCoord: string;
248
- effectiveAt: number;
249
- thresholdSig: string;
250
- signers: string[];
251
- cohortEpoch: string;
252
- }
253
-
254
- /**
255
- * A registration record carried in cohort gossip for cross-member replication (so any member can
256
- * fail over to serving it). Byte fields are base64url; mirrors the local `RegistrationRecord`.
257
- */
258
- export interface GossipRecordV1 {
259
- /** Topic id, 32 bytes, base64url. */
260
- topicId: string;
261
- /** Registering participant peer id, base64url. */
262
- participantId: string;
263
- /** Tier 0..3. */
264
- tier: number;
265
- /** Assigned primary peer id, base64url. */
266
- primary: string;
267
- /** 1..2 warm-failover peer ids, base64url. */
268
- backups: string[];
269
- /** Unix ms the registration first attached. */
270
- attachedAt: number;
271
- /** Unix ms of the most recent ping/touch (the convergence key — newest wins). */
272
- lastPing: number;
273
- /** Registration TTL in ms. */
274
- ttl: number;
275
- /** Opaque application-defined per-registration state, base64url. */
276
- appState?: string;
277
- }
278
-
279
- /** Reference to a single registration in an eviction delta. */
280
- export interface GossipRecordRefV1 {
281
- /** Topic id, 32 bytes, base64url. */
282
- topicId: string;
283
- /** Participant peer id, base64url. */
284
- participantId: string;
285
- /**
286
- * Unix ms of the evicted record's most-recent ping (mirrors {@link GossipRecordV1.lastPing}). The
287
- * receiver's freshness guard for the delete: a held record newer than this stamp is a re-registration
288
- * that arrived after the eviction was produced, so the stale eviction is ignored rather than deleting
289
- * the fresh record. Required every real eviction has this on hand from the held record.
290
- */
291
- lastPing: number;
292
- }
293
-
294
- /**
295
- * A child-cohort link/unlink advertised in cohort gossip, for cross-member convergence of the child set.
296
- *
297
- * FRET routes a {@link ChildLinkV1} to a **single** parent member, so only that member's child registry
298
- * records the child; siblings would read `childCohortCount == 0`. Gossiping the child **set** (a converged
299
- * union) closes that gap: a `childLinks` ref replicates a recorded child to every parent member, and a
300
- * `childUnlinks` ref replicates a released (demoted) child. Merge is **last-writer-wins by
301
- * {@link effectiveAt}** per `(topicId, childCohortCoord)` a link and a later unlink converge regardless of
302
- * arrival order, and re-delivery is idempotent. The child set is keyed by child coord, not the parent's
303
- * epoch, so a parent membership rotation never drops it.
304
- */
305
- export interface ChildLinkRefV1 {
306
- /** Topic id, 32 bytes, base64url. */
307
- topicId: string;
308
- /** The child cohort's served coord, 32 bytes, base64url. */
309
- childCohortCoord: string;
310
- /** Unix ms — the link/unlink effectiveAt; last-writer-wins per `(topicId, childCohortCoord)`. */
311
- effectiveAt: number;
312
- }
313
-
314
- /** Intra-cohort gossip: willingness vector, load barometer, exact per-topic summaries, record deltas. */
315
- export interface CohortGossipV1 {
316
- v: 1;
317
- /** PeerId. */
318
- fromMember: string;
319
- /**
320
- * The cohort coord this gossip is for, 32 bytes, base64url — the inbound routing key. A node serving
321
- * many cohorts fans a delivered frame to every coord engine's bus; each bus merges only the gossip
322
- * naming its own coord, so a gossip for one cohort never pollutes a sibling cohort's store/view.
323
- */
324
- coord: string;
325
- cohortEpoch: string;
326
- /**
327
- * The tree tier `d` the originating cohort sits at (the coord encodes the tier-shard, but a coord is a
328
- * hash and cannot be inverted to recover `d`). Carried so a cold sibling that instantiates its engine
329
- * off a co-member's frame (§Cold-start instantiation) adopts the right tier. Always well-defined: the
330
- * only members that originate a frame already know their `treeTier`, and every member of a coord shares
331
- * one `treeTier` by construction. Covered by {@link signature} so it cannot be spoofed.
332
- */
333
- treeTier: number;
334
- /** 4 bits T0..T3, hex. */
335
- willingnessBits: string;
336
- /** 4 entries, 0..7 per tier. */
337
- loadBuckets: number[];
338
- /** Cohort-wide observation window for the rate fields in `topicSummaries`. */
339
- windowSeconds: number;
340
- topicSummaries: CohortTopicSummary[];
341
- /**
342
- * Registration records this member is advertising (fresh or touched), for cross-member
343
- * replication. Absent when this gossip carries no record changes. Merge is last-writer-wins by
344
- * {@link GossipRecordV1.lastPing}.
345
- */
346
- records?: GossipRecordV1[];
347
- /** Registrations this member evicted (stale), so all members converge on the active set. */
348
- evicted?: GossipRecordRefV1[];
349
- /**
350
- * Child cohorts this member freshly recorded (links), replicated so every parent member converges on the
351
- * same child set. Absent when this gossip carries no child-link changes. Merge is last-writer-wins by
352
- * {@link ChildLinkRefV1.effectiveAt} per `(topicId, childCohortCoord)`; a link sets the child linked.
353
- */
354
- childLinks?: ChildLinkRefV1[];
355
- /**
356
- * Child cohorts this member released (a demoted child unrecorded), replicated so every parent member drops
357
- * it and can shrink in turn. Absent when this gossip carries no child-unlink changes. Same last-writer-wins
358
- * merge as {@link childLinks}; an unlink sets the child unlinked. A link@t1 then unlink@t2 (t2 > t1)
359
- * converges to unlinked regardless of arrival order.
360
- */
361
- childUnlinks?: ChildLinkRefV1[];
362
- timestamp: number;
363
- signature: string;
364
- }
365
-
366
- /** Per-topic summary inside {@link CohortGossipV1}; counts are exact, intra-cohort only. */
367
- export interface CohortTopicSummary {
368
- topicId: string;
369
- tier: number;
370
- /** Exact, intra-cohort only. */
371
- directParticipants: number;
372
- /** Exact, fresh + renewals over the gossip window. */
373
- arrivalsPerMin: number;
374
- queriesPerMin: number;
375
- promoted: boolean;
376
- childCohortCount: number;
377
- }
378
-
379
- /**
380
- * What a {@link SignRequestV1} asks a cohort member to endorse — drives the signer's endorsement policy.
381
- *
382
- * - `membership` / `promotion` / `demotion` — the requester and endorser must share the **current**
383
- * cohort + epoch around `coord`.
384
- * - `rotation` an epoch hand-off: the endorser attests it was a member of the cohort at the **prior**
385
- * epoch carried as `cohortEpoch` (the predecessor cohort threshold-signs the successor cert's payload),
386
- * so the gate checks *prior*-epoch membership rather than current. See `cohort-topic-trust-anchor-rotation-production`.
387
- * - `childlink` — a child cohort signs a {@link ChildLinkV1} over its **own** served coord at its current
388
- * epoch (identical endorsement shape to `promotion`/`demotion`: share the current cohort + epoch around
389
- * `coord`); the parent verifies it against the child cohort's cert before recording the child.
390
- */
391
- export type SignKind = "membership" | "promotion" | "demotion" | "rotation" | "childlink";
392
-
393
- /**
394
- * Intra-cohort sign request (`/optimystic/cohort-topic/1.0.0/sign`). A member assembling a `k − x`
395
- * threshold signature dials each cohort member with this; the member endorses by Ed25519-signing the
396
- * **exact** `payload` bytes (already canonicalized by the requester via `sig/payloads.ts`), so signer
397
- * and verifier never re-canonicalize independently. `coord`/`cohortEpoch` scope the endorsement
398
- * (the member checks it shares that cohort/epoch); `kind` selects the endorsement policy.
399
- */
400
- export interface SignRequestV1 {
401
- v: 1;
402
- kind: SignKind;
403
- /** Cohort coord the signature is for, 32 bytes, base64url. */
404
- coord: string;
405
- /** Cohort epoch the requester is collecting under, 32 bytes, base64url. */
406
- cohortEpoch: string;
407
- /** The already-canonical signing bytes (the requester's `sig/payloads.ts` image), base64url. */
408
- payload: string;
409
- }
410
-
411
- /** A member's endorsement of a {@link SignRequestV1}: its peer-key signature over the request payload. */
412
- export interface SignReplyOkV1 {
413
- v: 1;
414
- /** The endorsing member's dialable id (UTF-8 peer-id string), base64url. */
415
- signer: string;
416
- /** Ed25519 signature over the request `payload`, base64url (64 bytes decoded). */
417
- signature: string;
418
- }
419
-
420
- /** A member's refusal to endorse a {@link SignRequestV1} (not a cohort member, epoch mismatch, …). */
421
- export interface SignReplyRefusedV1 {
422
- v: 1;
423
- refused: true;
424
- reason: string;
425
- }
426
-
427
- /** Reply to a {@link SignRequestV1}: an endorsement ({@link SignReplyOkV1}) or a refusal ({@link SignReplyRefusedV1}). */
428
- export type SignReplyV1 = SignReplyOkV1 | SignReplyRefusedV1;
429
-
430
- /** Membership certificate for a cohort. */
431
- export interface MembershipCertV1 {
432
- v: 1;
433
- /** 32 bytes, base64url. */
434
- cohortCoord: string;
435
- cohortEpoch: string;
436
- /** PeerIds, sorted ascending, length `k`. */
437
- members: string[];
438
- /** Unix ms. */
439
- stabilizedAt: number;
440
- thresholdSig: string;
441
- signers: string[];
442
- /** Optional FRET stabilization proof, base64url. */
443
- fretAttestation?: string;
444
- /**
445
- * Rotation attestation (epoch rotation). The three fields below are an all-or-nothing group: a cert
446
- * either carries a full attestation (all three present) or none (all absent). They let a successor
447
- * cohort inherit trust from its predecessor: the predecessor cohort threshold-signs **this** cert's
448
- * `membershipCertSigningPayload`, so a verifier holding a trusted predecessor at {@link prevEpoch} can
449
- * confirm the rotation is legitimate (the prior cohort signed off) rather than a forgery. The
450
- * attestation is **not** part of `membershipCertSigningPayload` (it signs *over* that payload), so the
451
- * signed image is unchanged and legacy certs (no rotation fields) still decode.
452
- */
453
- /** Predecessor cohort epoch this cert rotates from (32 bytes, base64url). Present only on a rotation. */
454
- prevEpoch?: string;
455
- /** Predecessor cohort's threshold signature over THIS cert's `membershipCertSigningPayload`, base64url. */
456
- rotationSig?: string;
457
- /** Predecessor cohort signers (PeerIds, base64url) that produced {@link rotationSig}; `>= minSigs`. */
458
- rotationSigners?: string[];
459
- }
460
-
461
- /** Discriminated union over every V1 message carried by the cohort-topic protocols. */
462
- export type CohortMessageV1 =
463
- | RegisterV1
464
- | RegisterReplyV1
465
- | RenewV1
466
- | RenewReplyV1
467
- | ChildLinkV1
468
- | ChildLinkReplyV1
469
- | PromotionNoticeV1
470
- | DemotionNoticeV1
471
- | CohortGossipV1
472
- | MembershipCertV1
473
- | SignRequestV1
474
- | SignReplyOkV1
475
- | SignReplyRefusedV1;
1
+ /**
2
+ * Cohort-topic substrate — V1 wire message types.
3
+ *
4
+ * Transcribed from `docs/cohort-topic.md` §Wire formats. These interfaces are the canonical
5
+ * on-the-wire shapes for the cohort-topic RPC surface; the codec in `./codec.ts` serializes
6
+ * them as length-prefixed UTF-8 JSON.
7
+ *
8
+ * Conventions (mandated by §Wire formats):
9
+ * - Every byte-typed field is carried as a base64url string (no padding) — never raw bytes.
10
+ * - Every timestamp is unix milliseconds.
11
+ * - `appPayload` is an opaque base64url string carrying application-defined bytes. The wire layer
12
+ * never interprets it; reactivity / matchmaking define their own structures and serialize them
13
+ * into this slot.
14
+ */
15
+
16
+ /** Registration request. Walks toward the root via FRET `RouteAndMaybeAct`. */
17
+ export interface RegisterV1 {
18
+ v: 1;
19
+ /** Topic id, 32 bytes, base64url. */
20
+ topicId: string;
21
+ /** Tier, 0..3. */
22
+ tier: number;
23
+ /** Current walk position `d`. */
24
+ treeTier: number;
25
+ /** Participant's ring coord, 32 bytes, base64url. */
26
+ participantCoord: string;
27
+ /** Registration TTL in ms (default 90000; Edge 60000). */
28
+ ttl: number;
29
+ /** True on a root cold-start request. */
30
+ bootstrap?: boolean;
31
+ /**
32
+ * Follow-on cold-start request: set on the **dedicated re-issue** the participant sends after a
33
+ * `Promoted` redirect target answered {@link RegisterResult} `no_state` (the redirect points at a
34
+ * tier-`(d+1)` child cohort that does not exist yet). It tells that cold child "I am here because
35
+ * your parent redirected me — instantiate", which drives the cold-start gate
36
+ * (`shouldInstantiate({ followOn, … })`) exactly as `bootstrap` drives it at the root. Constraints:
37
+ * always `treeTier >= 1` (a follow-on is a deeper-than-root growth point, never the root); mutually
38
+ * exclusive with {@link bootstrap} and {@link probe} (the walk sets at most one). Because a wire flag is
39
+ * participant-forgeable, safety does NOT come from its provenance: a `followOn: true` register is gated
40
+ * by the **same** `bootstrapEvidence` policy a `bootstrap: true` cold-root register passes (§Anti-DoS),
41
+ * so it carries the identical proof in {@link bootstrapEvidence} and pays the identical anti-abuse cost.
42
+ * Covered by `signature` (appended to `registerSigningPayload`) so a MITM cannot strip or flip it.
43
+ */
44
+ followOn?: boolean;
45
+ /**
46
+ * Read-only lookup probe: classify + return the cohort snapshot without admitting (no record, no
47
+ * arrival, no promotion trigger, no topic-budget touch, and **never** a cold-start instantiation /
48
+ * `bootstrap`). A probe walks to the responsible cohort exactly as a register does and resolves the
49
+ * same {@link RegisterReplyV1} (`no_state` cold, `promoted` redirected, `accepted` snapshot served),
50
+ * but the terminal member action is the read-only {@link RegisterReplyV1} classify rather than an
51
+ * admission. Drives {@link import("../service.js").CohortTopicService.lookup}. Mutually exclusive with
52
+ * `bootstrap` (the walk never sets `bootstrap` on a probe).
53
+ */
54
+ probe?: boolean;
55
+ /** Opaque application-defined bytes, base64url. */
56
+ appPayload?: string;
57
+ /**
58
+ * Cold-start bootstrap-evidence envelope, base64url (a `BootstrapEvidenceEnvelopeV1` — see
59
+ * `../antidos/bootstrap-evidence-envelope.js`). Present on a `bootstrap: true` root register **or** a
60
+ * `followOn: true` deeper-tier re-issue — both are cold-start requests gated by the identical evidence
61
+ * policy (§Anti-DoS), so both carry this proof; carries the tier-dependent proof a cold cohort demands
62
+ * (proof-of-work / reputation endorsement / signed parent reference — §Anti-DoS). This is a
63
+ * **dedicated** field, NOT `appPayload`: the cohort
64
+ * copies `appPayload` verbatim into the registration's `appState` and replicates it cluster-wide,
65
+ * whereas the bootstrap evidence is parsed-and-checked by the substrate, **covered by `signature`**
66
+ * (so a MITM cannot strip or swap it), and never stored as appState. Empty string is treated as
67
+ * absent (it normalizes to the same signed-image placeholder as an absent field).
68
+ */
69
+ bootstrapEvidence?: string;
70
+ /** Unix ms. */
71
+ timestamp: number;
72
+ /** 16 random bytes, base64url. */
73
+ correlationId: string;
74
+ /** Participant peer-key signature, base64url. */
75
+ signature: string;
76
+ }
77
+
78
+ export type RegisterResult =
79
+ | "accepted"
80
+ | "no_state"
81
+ | "promoted"
82
+ | "unwilling_member"
83
+ | "unwilling_cohort";
84
+
85
+ /** Registration reply. Field presence is keyed by {@link RegisterResult}. */
86
+ export interface RegisterReplyV1 {
87
+ v: 1;
88
+ result: RegisterResult;
89
+ // accepted:
90
+ /** PeerId. */
91
+ primary?: string;
92
+ /** PeerIds, 1-2. */
93
+ backups?: string[];
94
+ /** 32 bytes, base64url. */
95
+ cohortEpoch?: string;
96
+ /** Full cohort PeerIds, for client cache. */
97
+ cohortMembers?: string[];
98
+ /** Present on `accepted` and `promoted` only. */
99
+ topicTraffic?: TopicTrafficV1;
100
+ // promoted:
101
+ /** `d+1` typically; may leap. */
102
+ targetTier?: number;
103
+ // unwilling_member:
104
+ /** PeerIds within the same cohort to try. */
105
+ candidateMembers?: string[];
106
+ // unwilling_cohort:
107
+ retryAfterMs?: number;
108
+ /** Human-readable, optional. */
109
+ reason?: string;
110
+ }
111
+
112
+ /** Coarse traffic barometer attached to accepted/promoted replies. */
113
+ export interface TopicTrafficV1 {
114
+ windowSeconds: number;
115
+ arrivalsPerMin: number;
116
+ queriesPerMin: number;
117
+ directParticipants: number;
118
+ childCohortCount: number;
119
+ }
120
+
121
+ /** Registration renewal (ping). */
122
+ export interface RenewV1 {
123
+ v: 1;
124
+ topicId: string;
125
+ /** PeerId. */
126
+ participantId: string;
127
+ /** Matches the original {@link RegisterV1}. */
128
+ correlationId: string;
129
+ timestamp: number;
130
+ /**
131
+ * True on a crash-failover re-attach — the participant attests it could not reach its primary and
132
+ * asks the contacted backup to promote itself. Absent/false on a normal ping. Signed (part of the
133
+ * renew body) so a member can trust the attestation and a MITM cannot flip a ping into a promotion.
134
+ */
135
+ reattach?: boolean;
136
+ /**
137
+ * True on a withdraw tombstone — the participant attests it is leaving; the holder evicts the
138
+ * record immediately and gossips the eviction (freeing the cohort slot rather than holding it for a
139
+ * full TTL). Absent/false on a ping or reattach. Signed (part of the renew body) so a third party
140
+ * cannot evict someone else's registration. Mutually exclusive with {@link reattach} (the participant
141
+ * never sets both; on a malformed frame that sets both, the cohort side honors `withdraw`).
142
+ */
143
+ withdraw?: boolean;
144
+ signature: string;
145
+ }
146
+
147
+ /** Renewal reply. */
148
+ export interface RenewReplyV1 {
149
+ v: 1;
150
+ /**
151
+ * `withdrawn` answers a {@link RenewV1.withdraw} tombstone the holder accepted (record evicted +
152
+ * gossiped); the participant ignores it (it is leaving), but the distinct result lets the cohort
153
+ * side branch unambiguously (re-touch the topic budget down, never count an arrival).
154
+ */
155
+ result: "ok" | "unknown_registration" | "primary_moved" | "withdrawn";
156
+ // primary_moved:
157
+ newPrimary?: string;
158
+ newBackups?: string[];
159
+ /** 32 bytes, base64url. */
160
+ cohortEpoch?: string;
161
+ }
162
+
163
+ /**
164
+ * Child cohort parent cohort link: "I am the tier-`d` cohort at {@link childCohortCoord}; record me as
165
+ * your child." A freshly cold-started deeper forwarder sends this to its tier-`(d − 1)` parent (routed the
166
+ * same way a participant register is, over `routeAndAct` to the parent coord) so the parent authenticates,
167
+ * records the child, and acks — replacing the interim plain participant-register the child used to send.
168
+ */
169
+ export interface ChildLinkV1 {
170
+ v: 1;
171
+ /** Topic id, 32 bytes, base64url. */
172
+ topicId: string;
173
+ /**
174
+ * The child cohort's served coord `coord_d(childParticipantCoord, topicId)`, 32 bytes base64url the
175
+ * coord the parent verifies the threshold signature against (it looks up the child cohort's
176
+ * {@link MembershipCertV1} at this coord).
177
+ */
178
+ childCohortCoord: string;
179
+ /**
180
+ * A representative participant coord in the child's prefix-shard (the seed the child engine was
181
+ * instantiated at). Lets the parent *deterministically bind the parent-child relationship*: it recomputes
182
+ * `coord_childTier(childParticipantCoord, topicId) == childCohortCoord` AND
183
+ * `coord_(childTier-1)(childParticipantCoord, topicId) == this parent's served coord`. Any participant
184
+ * sharing the child's `d·log₂F`-bit prefix yields the same pair, so it is a representative, not an
185
+ * identity. 32 bytes base64url.
186
+ */
187
+ childParticipantCoord: string;
188
+ /** Child tree tier `d` (always ≥ 1 — the root has no parent to link to). Parent serves `d − 1`. */
189
+ childTier: number;
190
+ /** Op capacity tier T0..T3 (stamped so a real parent validates the frame's tier). */
191
+ tier: number;
192
+ /** Unix ms; the parent's per-child freshness/ordering key (strictly-newer wins). */
193
+ effectiveAt: number;
194
+ /** Child cohort threshold signature over `childLinkSigningPayload`, base64url. Empty in the key-less interim. */
195
+ thresholdSig: string;
196
+ /** Signing members' PeerIds, `>= minSigs`, base64url. Empty in the key-less interim. */
197
+ signers: string[];
198
+ /** Child cohort epoch, 32 bytes base64url (the epoch the threshold sig was collected under). */
199
+ cohortEpoch: string;
200
+ }
201
+
202
+ /** Parent child ack. `linked` flips the child `awaiting_parent → serving`; `rejected` keeps it awaiting. */
203
+ export interface ChildLinkReplyV1 {
204
+ v: 1;
205
+ result: "linked" | "rejected";
206
+ /** Human-readable, optional. */
207
+ reason?: string;
208
+ }
209
+
210
+ /** Threshold-signed promotion notice. */
211
+ export interface PromotionNoticeV1 {
212
+ v: 1;
213
+ topicId: string;
214
+ fromTier: number;
215
+ /** Typically `fromTier + 1`. */
216
+ toTier: number;
217
+ /**
218
+ * The served coord `coord_d(participantCoord, topicId)` the deciding cohort sits at, 32 bytes, base64url
219
+ * the same value as that cohort's {@link MembershipCertV1.cohortCoord}, which the threshold signature
220
+ * verifies against. The receiver routes the notice to the local engine for exactly this coord rather than
221
+ * scanning for the first engine matching `(topic, tier)`, so a node serving several sibling cohorts for
222
+ * one `(topic, tier)` (possible at `d 1`, where each cohort has its own served coord) applies the notice
223
+ * to the cohort that actually produced it. Covered by the threshold signature — rewriting it breaks verification.
224
+ */
225
+ cohortCoord: string;
226
+ /** Unix ms. */
227
+ effectiveAt: number;
228
+ /** Cohort threshold signature, base64url. */
229
+ thresholdSig: string;
230
+ /** PeerIds, `>= minSigs`. */
231
+ signers: string[];
232
+ /** 32 bytes, base64url. */
233
+ cohortEpoch: string;
234
+ }
235
+
236
+ /** Threshold-signed demotion notice. */
237
+ export interface DemotionNoticeV1 {
238
+ v: 1;
239
+ topicId: string;
240
+ tier: number;
241
+ /** 32 bytes, base64url. */
242
+ parentCohortCoord: string;
243
+ /**
244
+ * The served coord `coord_d(participantCoord, topicId)` the deciding cohort sits at, 32 bytes, base64url
245
+ * the same served-coord concept as {@link PromotionNoticeV1.cohortCoord}. Distinct from
246
+ * {@link parentCohortCoord} (the tier-`(d − 1)` parent this demotion hands off to): `cohortCoord` names the
247
+ * *demoting* cohort and is what the receiver routes + verifies by. Covered by the threshold signature.
248
+ */
249
+ cohortCoord: string;
250
+ effectiveAt: number;
251
+ thresholdSig: string;
252
+ signers: string[];
253
+ /** 32 bytes, base64url. */
254
+ cohortEpoch: string;
255
+ }
256
+
257
+ /**
258
+ * A registration record carried in cohort gossip for cross-member replication (so any member can
259
+ * fail over to serving it). Byte fields are base64url; mirrors the local `RegistrationRecord`.
260
+ */
261
+ export interface GossipRecordV1 {
262
+ /** Topic id, 32 bytes, base64url. */
263
+ topicId: string;
264
+ /** Registering participant peer id, base64url. */
265
+ participantId: string;
266
+ /** Tier 0..3. */
267
+ tier: number;
268
+ /** Assigned primary peer id, base64url. */
269
+ primary: string;
270
+ /** 1..2 warm-failover peer ids, base64url. */
271
+ backups: string[];
272
+ /** Unix ms the registration first attached. */
273
+ attachedAt: number;
274
+ /** Unix ms of the most recent ping/touch (the convergence key — newest wins). */
275
+ lastPing: number;
276
+ /** Registration TTL in ms. */
277
+ ttl: number;
278
+ /** Opaque application-defined per-registration state, base64url. */
279
+ appState?: string;
280
+ }
281
+
282
+ /** Reference to a single registration in an eviction delta. */
283
+ export interface GossipRecordRefV1 {
284
+ /** Topic id, 32 bytes, base64url. */
285
+ topicId: string;
286
+ /** Participant peer id, base64url. */
287
+ participantId: string;
288
+ /**
289
+ * Unix ms of the evicted record's most-recent ping (mirrors {@link GossipRecordV1.lastPing}). The
290
+ * receiver's freshness guard for the delete: a held record newer than this stamp is a re-registration
291
+ * that arrived after the eviction was produced, so the stale eviction is ignored rather than deleting
292
+ * the fresh record. Required — every real eviction has this on hand from the held record.
293
+ */
294
+ lastPing: number;
295
+ }
296
+
297
+ /**
298
+ * A child-cohort link/unlink advertised in cohort gossip, for cross-member convergence of the child set.
299
+ *
300
+ * FRET routes a {@link ChildLinkV1} to a **single** parent member, so only that member's child registry
301
+ * records the child; siblings would read `childCohortCount == 0`. Gossiping the child **set** (a converged
302
+ * union) closes that gap: a `childLinks` ref replicates a recorded child to every parent member, and a
303
+ * `childUnlinks` ref replicates a released (demoted) child. Merge is **last-writer-wins by
304
+ * {@link effectiveAt}** per `(topicId, childCohortCoord)` — a link and a later unlink converge regardless of
305
+ * arrival order, and re-delivery is idempotent. The child set is keyed by child coord, not the parent's
306
+ * epoch, so a parent membership rotation never drops it.
307
+ */
308
+ export interface ChildLinkRefV1 {
309
+ /** Topic id, 32 bytes, base64url. */
310
+ topicId: string;
311
+ /** The child cohort's served coord, 32 bytes, base64url. */
312
+ childCohortCoord: string;
313
+ /** Unix ms — the link/unlink effectiveAt; last-writer-wins per `(topicId, childCohortCoord)`. */
314
+ effectiveAt: number;
315
+ }
316
+
317
+ /** Intra-cohort gossip: willingness vector, load barometer, exact per-topic summaries, record deltas. */
318
+ export interface CohortGossipV1 {
319
+ v: 1;
320
+ /** PeerId. */
321
+ fromMember: string;
322
+ /**
323
+ * The cohort coord this gossip is for, 32 bytes, base64url — the inbound routing key. A node serving
324
+ * many cohorts fans a delivered frame to every coord engine's bus; each bus merges only the gossip
325
+ * naming its own coord, so a gossip for one cohort never pollutes a sibling cohort's store/view.
326
+ */
327
+ coord: string;
328
+ /** 32 bytes, base64url. */
329
+ cohortEpoch: string;
330
+ /**
331
+ * The tree tier `d` the originating cohort sits at (the coord encodes the tier-shard, but a coord is a
332
+ * hash and cannot be inverted to recover `d`). Carried so a cold sibling that instantiates its engine
333
+ * off a co-member's frame (§Cold-start instantiation) adopts the right tier. Always well-defined: the
334
+ * only members that originate a frame already know their `treeTier`, and every member of a coord shares
335
+ * one `treeTier` by construction. Covered by {@link signature} so it cannot be spoofed.
336
+ */
337
+ treeTier: number;
338
+ /** 4 bits T0..T3, hex. */
339
+ willingnessBits: string;
340
+ /** 4 entries, 0..7 per tier. */
341
+ loadBuckets: number[];
342
+ /** Cohort-wide observation window for the rate fields in `topicSummaries`. */
343
+ windowSeconds: number;
344
+ topicSummaries: CohortTopicSummary[];
345
+ /**
346
+ * Registration records this member is advertising (fresh or touched), for cross-member
347
+ * replication. Absent when this gossip carries no record changes. Merge is last-writer-wins by
348
+ * {@link GossipRecordV1.lastPing}.
349
+ */
350
+ records?: GossipRecordV1[];
351
+ /** Registrations this member evicted (stale), so all members converge on the active set. */
352
+ evicted?: GossipRecordRefV1[];
353
+ /**
354
+ * Child cohorts this member freshly recorded (links), replicated so every parent member converges on the
355
+ * same child set. Absent when this gossip carries no child-link changes. Merge is last-writer-wins by
356
+ * {@link ChildLinkRefV1.effectiveAt} per `(topicId, childCohortCoord)`; a link sets the child linked.
357
+ */
358
+ childLinks?: ChildLinkRefV1[];
359
+ /**
360
+ * Child cohorts this member released (a demoted child unrecorded), replicated so every parent member drops
361
+ * it and can shrink in turn. Absent when this gossip carries no child-unlink changes. Same last-writer-wins
362
+ * merge as {@link childLinks}; an unlink sets the child unlinked. A link@t1 then unlink@t2 (t2 > t1)
363
+ * converges to unlinked regardless of arrival order.
364
+ */
365
+ childUnlinks?: ChildLinkRefV1[];
366
+ timestamp: number;
367
+ signature: string;
368
+ }
369
+
370
+ /** Per-topic summary inside {@link CohortGossipV1}; counts are exact, intra-cohort only. */
371
+ export interface CohortTopicSummary {
372
+ topicId: string;
373
+ tier: number;
374
+ /** Exact, intra-cohort only. */
375
+ directParticipants: number;
376
+ /** Exact, fresh + renewals over the gossip window. */
377
+ arrivalsPerMin: number;
378
+ queriesPerMin: number;
379
+ promoted: boolean;
380
+ childCohortCount: number;
381
+ }
382
+
383
+ /**
384
+ * What a {@link SignRequestV1} asks a cohort member to endorse drives the signer's endorsement policy.
385
+ *
386
+ * - `membership` / `promotion` / `demotion` — the requester and endorser must share the **current**
387
+ * cohort + epoch around `coord`.
388
+ * - `rotation` — an epoch hand-off: the endorser attests it was a member of the cohort at the **prior**
389
+ * epoch carried as `cohortEpoch` (the predecessor cohort threshold-signs the successor cert's payload),
390
+ * so the gate checks *prior*-epoch membership rather than current. See `cohort-topic-trust-anchor-rotation-production`.
391
+ * - `childlink` a child cohort signs a {@link ChildLinkV1} over its **own** served coord at its current
392
+ * epoch (identical endorsement shape to `promotion`/`demotion`: share the current cohort + epoch around
393
+ * `coord`); the parent verifies it against the child cohort's cert before recording the child.
394
+ */
395
+ export type SignKind = "membership" | "promotion" | "demotion" | "rotation" | "childlink";
396
+
397
+ /**
398
+ * Intra-cohort sign request (`/optimystic/cohort-topic/1.0.0/sign`). A member assembling a `k − x`
399
+ * threshold signature dials each cohort member with this; the member endorses by Ed25519-signing the
400
+ * **exact** `payload` bytes (already canonicalized by the requester via `sig/payloads.ts`), so signer
401
+ * and verifier never re-canonicalize independently. `coord`/`cohortEpoch` scope the endorsement
402
+ * (the member checks it shares that cohort/epoch); `kind` selects the endorsement policy.
403
+ */
404
+ export interface SignRequestV1 {
405
+ v: 1;
406
+ kind: SignKind;
407
+ /** Cohort coord the signature is for, 32 bytes, base64url. */
408
+ coord: string;
409
+ /** Cohort epoch the requester is collecting under, 32 bytes, base64url. */
410
+ cohortEpoch: string;
411
+ /** The already-canonical signing bytes (the requester's `sig/payloads.ts` image), base64url. */
412
+ payload: string;
413
+ }
414
+
415
+ /** A member's endorsement of a {@link SignRequestV1}: its peer-key signature over the request payload. */
416
+ export interface SignReplyOkV1 {
417
+ v: 1;
418
+ /** The endorsing member's dialable id (UTF-8 peer-id string), base64url. */
419
+ signer: string;
420
+ /** Ed25519 signature over the request `payload`, base64url (64 bytes decoded). */
421
+ signature: string;
422
+ }
423
+
424
+ /** A member's refusal to endorse a {@link SignRequestV1} (not a cohort member, epoch mismatch, …). */
425
+ export interface SignReplyRefusedV1 {
426
+ v: 1;
427
+ refused: true;
428
+ reason: string;
429
+ }
430
+
431
+ /** Reply to a {@link SignRequestV1}: an endorsement ({@link SignReplyOkV1}) or a refusal ({@link SignReplyRefusedV1}). */
432
+ export type SignReplyV1 = SignReplyOkV1 | SignReplyRefusedV1;
433
+
434
+ /** Membership certificate for a cohort. */
435
+ export interface MembershipCertV1 {
436
+ v: 1;
437
+ /** 32 bytes, base64url. */
438
+ cohortCoord: string;
439
+ /** 32 bytes, base64url. */
440
+ cohortEpoch: string;
441
+ /** PeerIds, sorted ascending, length `k`. */
442
+ members: string[];
443
+ /** Unix ms. */
444
+ stabilizedAt: number;
445
+ thresholdSig: string;
446
+ signers: string[];
447
+ /** Optional FRET stabilization proof, base64url. */
448
+ fretAttestation?: string;
449
+ /**
450
+ * Rotation attestation (epoch rotation). The three fields below are an all-or-nothing group: a cert
451
+ * either carries a full attestation (all three present) or none (all absent). They let a successor
452
+ * cohort inherit trust from its predecessor: the predecessor cohort threshold-signs **this** cert's
453
+ * `membershipCertSigningPayload`, so a verifier holding a trusted predecessor at {@link prevEpoch} can
454
+ * confirm the rotation is legitimate (the prior cohort signed off) rather than a forgery. The
455
+ * attestation is **not** part of `membershipCertSigningPayload` (it signs *over* that payload), so the
456
+ * signed image is unchanged and legacy certs (no rotation fields) still decode.
457
+ */
458
+ /** Predecessor cohort epoch this cert rotates from (32 bytes, base64url). Present only on a rotation. */
459
+ prevEpoch?: string;
460
+ /** Predecessor cohort's threshold signature over THIS cert's `membershipCertSigningPayload`, base64url. */
461
+ rotationSig?: string;
462
+ /** Predecessor cohort signers (PeerIds, base64url) that produced {@link rotationSig}; `>= minSigs`. */
463
+ rotationSigners?: string[];
464
+ }
465
+
466
+ /** Discriminated union over every V1 message carried by the cohort-topic protocols. */
467
+ export type CohortMessageV1 =
468
+ | RegisterV1
469
+ | RegisterReplyV1
470
+ | RenewV1
471
+ | RenewReplyV1
472
+ | ChildLinkV1
473
+ | ChildLinkReplyV1
474
+ | PromotionNoticeV1
475
+ | DemotionNoticeV1
476
+ | CohortGossipV1
477
+ | MembershipCertV1
478
+ | SignRequestV1
479
+ | SignReplyOkV1
480
+ | SignReplyRefusedV1;