@serfab/cadre-core 1.17.0 → 1.19.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.
- package/README.md +3 -2
- package/dist/cadre-invite-protocol.d.ts +35 -13
- package/dist/cadre-invite-protocol.js +90 -25
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-node.d.ts +586 -255
- package/dist/cadre-node.js +1182 -566
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-cohort.d.ts +66 -9
- package/dist/control-cohort.js +67 -9
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +451 -193
- package/dist/control-database.js +701 -359
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +46 -43
- package/dist/control-formation-recorder.js +77 -78
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +6 -0
- package/dist/control-protocol-guard.js +14 -2
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +48 -31
- package/dist/control-schema-version.js +67 -45
- package/dist/control-schema-version.js.map +1 -1
- package/dist/control-schema.d.ts +8 -8
- package/dist/control-schema.js +16 -16
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +42 -7
- package/dist/control-write-retry.js +87 -19
- package/dist/control-write-retry.js.map +1 -1
- package/dist/enrolled-machine-store.d.ts +3 -3
- package/dist/enrolled-machine-store.js +3 -3
- package/dist/formation-token.d.ts +48 -0
- package/dist/formation-token.js +26 -0
- package/dist/formation-token.js.map +1 -0
- package/dist/index.d.ts +15 -14
- package/dist/index.js +17 -14
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +26 -9
- package/dist/joined-strand-store.js +63 -24
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/link-budget.d.ts +9 -7
- package/dist/link-budget.js +9 -7
- package/dist/link-budget.js.map +1 -1
- package/dist/member-redemption.d.ts +15 -9
- package/dist/member-redemption.js +13 -8
- package/dist/member-redemption.js.map +1 -1
- package/dist/membership-view.d.ts +91 -0
- package/dist/membership-view.js +110 -0
- package/dist/membership-view.js.map +1 -0
- package/dist/owner-grant-protocol.d.ts +11 -7
- package/dist/owner-grant-protocol.js +31 -17
- package/dist/owner-grant-protocol.js.map +1 -1
- package/dist/peer-addr-book.d.ts +10 -1
- package/dist/peer-addr-book.js +4 -3
- package/dist/peer-addr-book.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +96 -30
- package/dist/peer-join-backfill.js +272 -128
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/pending-join-runner.d.ts +48 -18
- package/dist/pending-join-runner.js +74 -56
- package/dist/pending-join-runner.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +47 -40
- package/dist/seed-bootstrap.js +66 -61
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/self-address-update.d.ts +13 -0
- package/dist/self-address-update.js +19 -0
- package/dist/self-address-update.js.map +1 -0
- package/dist/strand-database.d.ts +23 -2
- package/dist/strand-database.js +26 -6
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +176 -89
- package/dist/strand-first-sync-gate.js +288 -122
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-deadlines.d.ts +9 -9
- package/dist/strand-formation-deadlines.js +9 -9
- package/dist/strand-formation-manager.d.ts +23 -17
- package/dist/strand-formation-manager.js +46 -44
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +5 -8
- package/dist/strand-formation-protocol.js +101 -16
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +26 -17
- package/dist/strand-instance-manager.js +119 -50
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +16 -13
- package/dist/strand-membership-reconciler.js +28 -22
- package/dist/strand-membership-reconciler.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +40 -28
- package/dist/strand-membership-writer.js +212 -153
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.js +5 -2
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-revocation-enforcer.d.ts +134 -60
- package/dist/strand-revocation-enforcer.js +144 -71
- package/dist/strand-revocation-enforcer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +50 -39
- package/dist/strand-solicitation.js +10 -3
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-watcher.d.ts +80 -13
- package/dist/strand-watcher.js +152 -48
- package/dist/strand-watcher.js.map +1 -1
- package/dist/table-change-watch.d.ts +63 -0
- package/dist/table-change-watch.js +209 -0
- package/dist/table-change-watch.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +4 -3
- package/dist/timeout-scheduler.js +4 -3
- package/dist/timeout-scheduler.js.map +1 -1
- package/dist/types.d.ts +90 -49
- package/dist/types.js +4 -4
- package/dist/types.js.map +1 -1
- package/package.json +8 -8
- package/src/cadre-invite-protocol.ts +119 -31
- package/src/cadre-node.ts +1289 -588
- package/src/control-cohort.ts +86 -9
- package/src/control-database.ts +848 -403
- package/src/control-formation-recorder.ts +359 -353
- package/src/control-protocol-guard.ts +16 -2
- package/src/control-schema-version.ts +101 -51
- package/src/control-schema.ts +16 -16
- package/src/control-write-retry.ts +91 -19
- package/src/delegate-admission.ts +351 -351
- package/src/enrolled-machine-store.ts +3 -3
- package/src/formation-token.ts +63 -0
- package/src/index.ts +30 -18
- package/src/joined-strand-store.ts +69 -24
- package/src/link-budget.ts +9 -7
- package/src/member-redemption.ts +20 -13
- package/src/membership-view.ts +144 -0
- package/src/owner-grant-protocol.ts +33 -18
- package/src/peer-addr-book.ts +4 -3
- package/src/peer-join-backfill.ts +961 -761
- package/src/pending-join-runner.ts +76 -53
- package/src/seed-bootstrap.ts +72 -67
- package/src/self-address-update.ts +19 -0
- package/src/strand-database.ts +352 -322
- package/src/strand-first-sync-gate.ts +334 -134
- package/src/strand-formation-deadlines.ts +227 -227
- package/src/strand-formation-manager.ts +731 -725
- package/src/strand-formation-protocol.ts +122 -24
- package/src/strand-instance-manager.ts +2092 -2026
- package/src/strand-membership-reconciler.ts +31 -25
- package/src/strand-membership-writer.ts +2093 -2019
- package/src/strand-move.ts +5 -2
- package/src/strand-revocation-enforcer.ts +740 -654
- package/src/strand-solicitation.ts +61 -34
- package/src/strand-watcher.ts +175 -56
- package/src/table-change-watch.ts +285 -0
- package/src/timeout-scheduler.ts +29 -28
- package/src/types.ts +89 -51
|
@@ -1,654 +1,740 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Strand revoked-peer enforcement: a per-strand deny gate that refuses the
|
|
3
|
-
* network nodes of a REMOVED party, so removal means "we stop talking to you"
|
|
4
|
-
* rather than only deleting a row.
|
|
5
|
-
*
|
|
6
|
-
* ## The deny set is revocation-keyed, never membership-keyed
|
|
7
|
-
*
|
|
8
|
-
* A strand node maintains a materialized in-memory set of revoked peer ids:
|
|
9
|
-
* every `Strand.MemberPeer.PeerId` whose `MemberKey` has no live `Strand.Member`
|
|
10
|
-
* row. `MemberPeer.MemberExists` runs on insert, so an orphaned binding can only
|
|
11
|
-
* mean "this member existed and was removed" — it is exactly the durable,
|
|
12
|
-
* replicated record of a removed party's machines.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* gate
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*/
|
|
170
|
-
export
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
this.
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Strand revoked-peer enforcement: a per-strand deny gate that refuses the
|
|
3
|
+
* network nodes of a REMOVED party, so removal means "we stop talking to you"
|
|
4
|
+
* rather than only deleting a row.
|
|
5
|
+
*
|
|
6
|
+
* ## The deny set is revocation-keyed, never membership-keyed
|
|
7
|
+
*
|
|
8
|
+
* A strand node maintains a materialized in-memory set of revoked peer ids:
|
|
9
|
+
* every `Strand.MemberPeer.PeerId` whose `MemberKey` has no live `Strand.Member`
|
|
10
|
+
* row. `MemberPeer.MemberExists` runs on insert, so an orphaned binding can only
|
|
11
|
+
* mean "this member existed and was removed" — it is exactly the durable,
|
|
12
|
+
* replicated record of a removed party's machines. That holds on the network
|
|
13
|
+
* only because every writer commits a party's `Member` row before its
|
|
14
|
+
* `MemberPeer` row, as separate commits (the membership reconciler does, and
|
|
15
|
+
* `registerMemberPeer` requires an existing member): Optimystic can save a
|
|
16
|
+
* multi-collection transaction's tables separately, and remaining members can
|
|
17
|
+
* apply them in either order. The one exception is the founder's bootstrap,
|
|
18
|
+
* which writes its own `Member` and `MemberPeer` in one transaction; that is
|
|
19
|
+
* acceptable because it commits before any other party has joined, so no other
|
|
20
|
+
* party's enforcer is reading yet. Everything NOT in the set is admitted.
|
|
21
|
+
* Deliberate consequences:
|
|
22
|
+
*
|
|
23
|
+
* - **Open strands are untouched.** `Member`/`MemberPeer` carry `OnlyClosed`
|
|
24
|
+
* checks, so an open strand has no rows and the set is empty — and
|
|
25
|
+
* `StrandInstanceManager` additionally skips arming this enforcer entirely for
|
|
26
|
+
* `Type !== 'c'`, so an open strand neither subscribes nor reads.
|
|
27
|
+
* - **A joining party mid-admission is admitted** (it has no orphaned binding).
|
|
28
|
+
* An allowlist ("only peers bound to live members") was considered and
|
|
29
|
+
* rejected: a joiner's first membership writes must travel through the very
|
|
30
|
+
* cohort an allowlist would close to it — the same mutual-denial trap the
|
|
31
|
+
* control network's gater doc describes (`membership-connection-gater.ts`),
|
|
32
|
+
* with no enrollment window to carve out. A deny-list on positive evidence
|
|
33
|
+
* has no such trap.
|
|
34
|
+
* - **Stale-view failure direction: fail-open.** A remaining member that has not
|
|
35
|
+
* yet replicated the revocation keeps serving the removed party until the
|
|
36
|
+
* tombstoned state arrives — bounded by replication, consistent with
|
|
37
|
+
* "revocation is forward-looking". The opposite error (denying a legitimate
|
|
38
|
+
* member) is only reachable for a revoked-then-re-admitted party at a node
|
|
39
|
+
* that saw the revoke but not the re-add; the re-add gives the member key a
|
|
40
|
+
* live `Member` row again, which removes its peers from the deny set as the
|
|
41
|
+
* row replicates — self-healing, and the re-adding manager's own node updates
|
|
42
|
+
* first, so the heal never depends on talking to the peer being wrongly denied.
|
|
43
|
+
* - **Orphaned `MemberPeer` rows are load-bearing**: they ARE the deny record —
|
|
44
|
+
* the `Strand.Revocation` tombstone keeps only the stamp, not the peer id — so
|
|
45
|
+
* nothing here (or anywhere) tombstones or auto-cleans them at revocation.
|
|
46
|
+
* The manager-cleanup path (`removeMemberPeer`'s manager branch) stays as the
|
|
47
|
+
* only way to clear a wrongly-registered binding, and clearing an orphan
|
|
48
|
+
* forgets the denial (the NOTE at that branch says so).
|
|
49
|
+
*
|
|
50
|
+
* ## Removal cuts LIVE sessions too — the refresh IS the teardown seam
|
|
51
|
+
*
|
|
52
|
+
* Refusing NEW connections only half-honors "no longer be answered to or
|
|
53
|
+
* communicated with": a long-lived strand connection may never re-dial, so
|
|
54
|
+
* without teardown a removed party keeps whatever session it already holds.
|
|
55
|
+
* Teardown rides the snapshot refresh itself, which runs when the membership
|
|
56
|
+
* tables change (see "When the deny set is refreshed"). `refresh()` is public
|
|
57
|
+
* for a caller that wants to await the cut
|
|
58
|
+
* (`StrandInstanceManager.refreshRevocationEnforcement` →
|
|
59
|
+
* `CadreNode.refreshRevocationEnforcement`): it resolves only after the sweep.
|
|
60
|
+
*
|
|
61
|
+
* The sweep is driven by MEMBERSHIP in the deny set, never by an
|
|
62
|
+
* entered-the-set diff: every refresh hangs up every currently-connected
|
|
63
|
+
* revoked peer, so a resume, a crashed refresh, or a connection established
|
|
64
|
+
* between two refreshes cannot leave a session standing (a diff would be a
|
|
65
|
+
* strict subset of this, so none is kept). A peer with no open connection is
|
|
66
|
+
* simply not swept, and a failed `hangUp` is logged, never thrown, and retried
|
|
67
|
+
* by a refresh {@link REVOCATION_REFRESH_RETRY_MS} later. `hangUp` closes DIRECT and RELAYED connections alike, and
|
|
68
|
+
* on a strand node running the relay server it also drops any reservation the
|
|
69
|
+
* revoked peer holds there (a reservation dies with its connection — see the
|
|
70
|
+
* relay-seam doc in `membership-connection-gater.ts`).
|
|
71
|
+
*
|
|
72
|
+
* Teardown is also what stops OUTBOUND traffic. The gate is one-directional —
|
|
73
|
+
* `authorizeStream` judges streams the revoked peer opens to US — so nothing
|
|
74
|
+
* else prevents this node from pushing over an already-open connection, which
|
|
75
|
+
* `PeerJoinBackfill` does for that connection's whole life. Closing the
|
|
76
|
+
* connection stops both directions at once, which is why no separate outbound
|
|
77
|
+
* gate exists.
|
|
78
|
+
*
|
|
79
|
+
* If this node's OWN peer id is in the set, it was itself removed (or it left —
|
|
80
|
+
* a voluntary departure files the same tombstone). Nothing is torn down for
|
|
81
|
+
* that: the other side's gate already refuses us, and every other revoked peer
|
|
82
|
+
* is still swept. Instead `onSelfRevoked` fires ONCE per entry into the set
|
|
83
|
+
* (re-armed if the party is re-admitted, so a second removal signals again),
|
|
84
|
+
* which `CadreNode` surfaces as `strand:revoked` so the embedding app decides
|
|
85
|
+
* what to do — this module never stops or leaves a strand on its own.
|
|
86
|
+
* Best-effort by nature: a removed party learns only if the tombstone
|
|
87
|
+
* replicated to it before the cut.
|
|
88
|
+
*
|
|
89
|
+
* ## When the deny set is refreshed
|
|
90
|
+
*
|
|
91
|
+
* Every refresh runs on one table change subscription (`table-change-watch.ts`),
|
|
92
|
+
* single-flight, so no two scheduled refreshes overlap:
|
|
93
|
+
*
|
|
94
|
+
* - **On a change** to `Strand.Member` or `Strand.MemberPeer`. App writes
|
|
95
|
+
* (`revokeMember`, `leaveStrand`) commit on the same Quereus `Database` the
|
|
96
|
+
* subscription is on, so a local removal is acted on within the coalescing
|
|
97
|
+
* window; a replicated one when this node's storage takes it in.
|
|
98
|
+
* - **Once at start**, when the strand's database is published.
|
|
99
|
+
* - **On a backstop** ({@link DEFAULT_REVOCATION_BACKSTOP_MS} after the last
|
|
100
|
+
* refresh settled), for a change that raised no event.
|
|
101
|
+
* - **After a failed refresh**, {@link REVOCATION_REFRESH_RETRY_MS} later. The
|
|
102
|
+
* first successful read also arms the plugin's bridge for replicated changes
|
|
103
|
+
* (the NOTE on `watchTables`), so until one succeeds only local commits wake it.
|
|
104
|
+
* - **After each successful refresh on a wide strand** — more distinct bound peer ids than
|
|
105
|
+
* the cluster size — {@link REVOCATION_WIDE_STRAND_REFRESH_MS} later, because
|
|
106
|
+
* this node may be outside a membership block's cohort and so hear no change.
|
|
107
|
+
*
|
|
108
|
+
* ## What transfers from the control gater (`membership-connection-gater.ts`)
|
|
109
|
+
*
|
|
110
|
+
* Transfers: the two-layer shape (fail-closed per-stream gate as PRIMARY, a
|
|
111
|
+
* connection-level layer on top), the synchronous in-memory
|
|
112
|
+
* snapshot judged by the stream gate (a live DB read inside a gate deadlocks —
|
|
113
|
+
* the same argument as the control node's `authorizeInboundControlStream`), the
|
|
114
|
+
* fail-open-on-error/timeout posture for the connection hooks
|
|
115
|
+
* (`decideWithinDeadline`, reused as-is), and the "compose over the caller's
|
|
116
|
+
* gater, every base hook preserved" mechanics.
|
|
117
|
+
*
|
|
118
|
+
* Does NOT transfer — provisional admission and the relay-reservation seam
|
|
119
|
+
* (`'admit-provisionally'`, its deadline, the unauthorized budget). That
|
|
120
|
+
* machinery exists because the control gate judges on ABSENCE of placement,
|
|
121
|
+
* where a member whose row is in flight, or a device whose invitation row has
|
|
122
|
+
* not arrived, is indistinguishable from an outsider, and a wrong reservation
|
|
123
|
+
* deny is unrecoverable. This gate denies on POSITIVE revocation evidence, so a
|
|
124
|
+
* deny is never that kind of wrong answer; the one stale-wrong case (a missed
|
|
125
|
+
* re-add) heals via replication as above. Every hook here is therefore a plain
|
|
126
|
+
* "deny iff revoked" — no budget, no provisional verdict, no deadline.
|
|
127
|
+
*
|
|
128
|
+
* Does NOT transfer — the bring-up quiet period. A strand node's DB bring-up
|
|
129
|
+
* NEEDS its cross-party cohort connections; this enforcer simply starts with an
|
|
130
|
+
* empty snapshot (admit everything) until its first successful read — the same
|
|
131
|
+
* fail-open posture as everywhere else here.
|
|
132
|
+
*
|
|
133
|
+
* Does NOT transfer — every stranger carve-out (enrollment window, formation
|
|
134
|
+
* window, delegate grants). None of those protocols exist on a strand node.
|
|
135
|
+
*
|
|
136
|
+
* Unlike the control stream gate, no empty-snapshot carve-out is needed: an
|
|
137
|
+
* empty deny set means "nothing revoked", which correctly admits everyone — the
|
|
138
|
+
* deny-list shape makes the cold-start case and the steady case the same code
|
|
139
|
+
* path.
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
import debug from 'debug';
|
|
143
|
+
import type { ConnectionGater, PeerId, MultiaddrConnection } from '@libp2p/interface';
|
|
144
|
+
import type { Database } from '@quereus/quereus';
|
|
145
|
+
import { decideWithinDeadline } from './membership-connection-gater.js';
|
|
146
|
+
import { ADMISSION_DECISION_TIMEOUT_MS } from './link-budget.js';
|
|
147
|
+
import { scheduleRuns, type TableChangeWatch, type TableChangeWatchOptions } from './table-change-watch.js';
|
|
148
|
+
import type { TimeoutScheduler } from './timeout-scheduler.js';
|
|
149
|
+
|
|
150
|
+
const log = debug('sereus:cadre:strand-revocation');
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Default longest wait between two deny-set refreshes when no membership change
|
|
154
|
+
* is heard. Overridable via {@link StrandRevocationEnforcementConfig.backstopMs}.
|
|
155
|
+
*
|
|
156
|
+
* NOTE: the backstop covers two paths the change subscription does not: a
|
|
157
|
+
* machine that was offline when the removal was committed (it normally takes
|
|
158
|
+
* the blocks in by join backfill or read repair on reconnect, which raise the
|
|
159
|
+
* change too), and any change that reaches storage without an event. The case
|
|
160
|
+
* of a node outside a membership block's cohort is covered by
|
|
161
|
+
* {@link REVOCATION_WIDE_STRAND_REFRESH_MS}, not by this.
|
|
162
|
+
*/
|
|
163
|
+
export const DEFAULT_REVOCATION_BACKSTOP_MS = 5 * 60_000;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* How soon a failed refresh is retried. Also how soon a replicated removal is
|
|
167
|
+
* heard after a first read that failed, since that read is what arms the
|
|
168
|
+
* plugin's replicated-change bridge for the two tables.
|
|
169
|
+
*/
|
|
170
|
+
export const REVOCATION_REFRESH_RETRY_MS = 30_000;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* How soon a strand with more distinct bound peer ids than its cluster size is
|
|
174
|
+
* refreshed again after a successful refresh. On such a strand this node may be
|
|
175
|
+
* outside a membership block's cohort, so it takes in no storage event for a
|
|
176
|
+
* removal (`docs/architecture.md` → "Table change subscriptions").
|
|
177
|
+
*/
|
|
178
|
+
export const REVOCATION_WIDE_STRAND_REFRESH_MS = 30_000;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Embedder tuning for the strand revoked-peer gate, threaded from
|
|
182
|
+
* `CadreNodeConfig.strandRevocationEnforcement` to
|
|
183
|
+
* `StartStrandConfig.revocationEnforcement` the way the backfill knob is.
|
|
184
|
+
*/
|
|
185
|
+
export interface StrandRevocationEnforcementConfig {
|
|
186
|
+
/**
|
|
187
|
+
* Default true. False disarms the gate entirely for this node's strands —
|
|
188
|
+
* the pre-existing behaviour (a removed party's peers keep being served).
|
|
189
|
+
*/
|
|
190
|
+
enabled?: boolean;
|
|
191
|
+
/** Longest wait between two refreshes with no change heard, ms. Default {@link DEFAULT_REVOCATION_BACKSTOP_MS}. */
|
|
192
|
+
backstopMs?: number;
|
|
193
|
+
/**
|
|
194
|
+
* Default true: the enforcer schedules its own refreshes — on a change to
|
|
195
|
+
* `Strand.Member` or `Strand.MemberPeer`, after a failed read or hang-up, and
|
|
196
|
+
* on a wide strand's follow-up. False schedules none of those, so refreshes
|
|
197
|
+
* run only at start, on the backstop, and on `refreshRevocationEnforcement`.
|
|
198
|
+
* For test scenarios that need a machine's cuts to be the ones the test drove;
|
|
199
|
+
* a production node leaves it on.
|
|
200
|
+
*/
|
|
201
|
+
autoRefresh?: boolean;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** One `Strand.MemberPeer` row, as the deny-set derivation consumes it. */
|
|
205
|
+
export interface StrandMemberPeerBinding {
|
|
206
|
+
memberKey: string;
|
|
207
|
+
peerId: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The membership rows one deny-set derivation reads. */
|
|
211
|
+
export interface StrandRevocationRows {
|
|
212
|
+
/** Every live `Strand.Member.Key`. */
|
|
213
|
+
memberKeys: ReadonlySet<string>;
|
|
214
|
+
/** Every `Strand.MemberPeer` row (live-membered and orphaned alike). */
|
|
215
|
+
bindings: readonly StrandMemberPeerBinding[];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The minimum of a libp2p node the teardown sweep needs: who WE are, what is
|
|
220
|
+
* open, and how to close it. Deliberately not the node itself, so the sweep
|
|
221
|
+
* unit-tests against a three-property stub; `Libp2p` (hence
|
|
222
|
+
* `Libp2pNodeWithRepo`) satisfies it structurally, which is what
|
|
223
|
+
* `StrandInstanceManager` passes.
|
|
224
|
+
*/
|
|
225
|
+
export interface StrandRevocationNetwork {
|
|
226
|
+
/** This node's own peer id — how the enforcer recognizes ITSELF in the deny set. */
|
|
227
|
+
readonly peerId: PeerId;
|
|
228
|
+
/** Every open connection. Only `remotePeer` is read — and reused as the hangUp target. */
|
|
229
|
+
getConnections(): readonly { readonly remotePeer: PeerId }[];
|
|
230
|
+
/** Close every connection to `peer`: direct, relayed, and any reservation riding one. */
|
|
231
|
+
hangUp(peer: PeerId): Promise<void>;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface StrandRevocationEnforcerDeps {
|
|
235
|
+
/** Log tag naming which strand this enforcer serves (the strand id). */
|
|
236
|
+
label: string;
|
|
237
|
+
/**
|
|
238
|
+
* Read the membership rows the deny set is derived from. Injectable so the
|
|
239
|
+
* enforcer unit-tests without a strand database; production wires
|
|
240
|
+
* {@link readStrandRevocationRows} over the strand's live DB. A throw or
|
|
241
|
+
* rejection KEEPS the previous snapshot (never clears it) — the control
|
|
242
|
+
* model, `refreshAuthorizedControlPeers`.
|
|
243
|
+
*/
|
|
244
|
+
readRows: () => Promise<StrandRevocationRows>;
|
|
245
|
+
/**
|
|
246
|
+
* Handle on the strand's libp2p node for the teardown sweep, read LAZILY on
|
|
247
|
+
* every sweep — never captured. It has to be: the enforcer is constructed
|
|
248
|
+
* BEFORE the node (the node's options embed the enforcer's predicates), and
|
|
249
|
+
* the node is dropped again on quiesce. Omit it, or return undefined, and the
|
|
250
|
+
* gate still refuses new connections — only teardown of OPEN ones is skipped.
|
|
251
|
+
*/
|
|
252
|
+
getNetwork?: () => StrandRevocationNetwork | undefined;
|
|
253
|
+
/**
|
|
254
|
+
* Called ONCE each time this node's own peer id ENTERS the deny set — i.e.
|
|
255
|
+
* this node's party was removed from the strand, or left it. Not re-fired
|
|
256
|
+
* while it stays there; re-armed if the party is re-admitted, so a second
|
|
257
|
+
* removal signals again. Wired by `StrandInstanceManager` through to
|
|
258
|
+
* `CadreNode`'s `strand:revoked` event. A resume rebuilds the enforcer, so a
|
|
259
|
+
* still-revoked strand also legitimately re-fires once per resume. A throw is
|
|
260
|
+
* logged and swallowed — an embedder's handler must not derail the sweep.
|
|
261
|
+
*/
|
|
262
|
+
onSelfRevoked?: () => void;
|
|
263
|
+
/**
|
|
264
|
+
* Subscribe `onChange` to changes in `Strand.Member` and `Strand.MemberPeer`
|
|
265
|
+
* (`StrandDatabase.watchTables`). Absent, or throwing: refreshes come from
|
|
266
|
+
* start, the backstop, retries and wide-strand follow-ups only (unit tests).
|
|
267
|
+
*/
|
|
268
|
+
watchChanges?(onChange: () => Promise<void>, options: TableChangeWatchOptions): TableChangeWatch;
|
|
269
|
+
/**
|
|
270
|
+
* The strand's cluster size. A snapshot with more distinct bound peer ids than
|
|
271
|
+
* this schedules {@link REVOCATION_WIDE_STRAND_REFRESH_MS}; absent, never.
|
|
272
|
+
*/
|
|
273
|
+
cohortBreadth?: number;
|
|
274
|
+
/** Timer seam for the watch; omit for real (unref'd) timeouts. */
|
|
275
|
+
scheduler?: TimeoutScheduler;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Every `Strand.MemberPeer` binding — live-membered and orphaned alike. */
|
|
279
|
+
async function scanBindings(db: Database): Promise<StrandMemberPeerBinding[]> {
|
|
280
|
+
const bindings: StrandMemberPeerBinding[] = [];
|
|
281
|
+
for await (const row of db.eval('select MemberKey, PeerId from Strand.MemberPeer')) {
|
|
282
|
+
bindings.push({ memberKey: row.MemberKey as string, peerId: row.PeerId as string });
|
|
283
|
+
}
|
|
284
|
+
return bindings;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Every live `Strand.Member.Key`. */
|
|
288
|
+
async function scanMemberKeys(db: Database): Promise<Set<string>> {
|
|
289
|
+
const memberKeys = new Set<string>();
|
|
290
|
+
for await (const row of db.eval('select Key from Strand.Member')) {
|
|
291
|
+
memberKeys.add(row.Key as string);
|
|
292
|
+
}
|
|
293
|
+
return memberKeys;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Read the deny-set inputs from a strand database: full-scan
|
|
298
|
+
* `Strand.MemberPeer` and `Strand.Member`, to be joined in JavaScript by the
|
|
299
|
+
* enforcer. Scan-and-filter, never seek — the composite-PK point-lookup
|
|
300
|
+
* unreliability documented at `scanMemberPeers` in `strand-membership-writer.ts`
|
|
301
|
+
* applies, so correctness depends only on each scan returning a superset of the
|
|
302
|
+
* live rows.
|
|
303
|
+
*
|
|
304
|
+
* **Scan order is load-bearing, and it is BINDINGS FIRST.** The two scans are
|
|
305
|
+
* separate reads with nothing holding them to one snapshot, so a write that
|
|
306
|
+
* lands between them is seen by the second scan and not the first. A member
|
|
307
|
+
* joins as `Member` row then `MemberPeer` row (`MemberExists` forces that
|
|
308
|
+
* order; the module doc says why it holds on the network), so reading `Member`
|
|
309
|
+
* first would let a join that commits mid-read produce a binding whose member
|
|
310
|
+
* key is absent from the key set — a brand-new member classified as REVOKED,
|
|
311
|
+
* the one fail-CLOSED outcome this module exists to avoid. Reading bindings
|
|
312
|
+
* first inverts the skew: the new binding is simply not in the older bindings
|
|
313
|
+
* snapshot, so the joiner is unclassified and admitted, and a revocation that
|
|
314
|
+
* lands mid-read is still seen (the key is missing from the NEWER key scan).
|
|
315
|
+
* Every skew window then resolves fail-open, matching the module doc.
|
|
316
|
+
*
|
|
317
|
+
* NOTE: two whole-table scans per refresh (on each membership change and each
|
|
318
|
+
* backstop, per closed strand). Fine at strand scale (a handful of members and bindings); if
|
|
319
|
+
* membership churn ever makes these tables large, the fix is a reliable
|
|
320
|
+
* filtered read — which needs the networked point-lookup gap closed
|
|
321
|
+
* (`debt-composite-pk-point-lookup-unreliable-untracked`) — not a bigger scan.
|
|
322
|
+
*/
|
|
323
|
+
export async function readStrandRevocationRows(db: Database): Promise<StrandRevocationRows> {
|
|
324
|
+
const bindings = await scanBindings(db);
|
|
325
|
+
const memberKeys = await scanMemberKeys(db);
|
|
326
|
+
return { memberKeys, bindings };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The per-strand revoked-peer deny set and the synchronous predicates over it.
|
|
331
|
+
*
|
|
332
|
+
* Lifecycle: created in `StrandInstanceManager.buildStrandRuntime` (closed
|
|
333
|
+
* strands only), started when `publishDatabase` hands the strand's database to
|
|
334
|
+
* the app, stopped and dropped in `releaseRuntime` — so quiesce → resume
|
|
335
|
+
* rebuilds it with a fresh snapshot. The snapshot starts EMPTY (admit
|
|
336
|
+
* everything) and repopulates on the first successful read; a revoked peer
|
|
337
|
+
* therefore gets a short admit window per resume at the connection level.
|
|
338
|
+
* Accepted: fail-open posture, and resume is never blocked on a refresh.
|
|
339
|
+
*/
|
|
340
|
+
export class StrandRevocationEnforcer {
|
|
341
|
+
/** The materialized deny set — peer ids with an orphaned binding and no live one. */
|
|
342
|
+
private revokedPeerIds: ReadonlySet<string> = new Set();
|
|
343
|
+
private readonly backstopMs: number;
|
|
344
|
+
private readonly autoRefresh: boolean;
|
|
345
|
+
/** Tail of the refresh chain — what serializes refreshes (no two reads in flight). */
|
|
346
|
+
private refreshTail: Promise<void> = Promise.resolve();
|
|
347
|
+
/** Every scheduled refresh runs on it; present between {@link start} and {@link stop}. */
|
|
348
|
+
private watch: TableChangeWatch | undefined;
|
|
349
|
+
private started = false;
|
|
350
|
+
private stopped = false;
|
|
351
|
+
/** Latch for the one-shot self-revocation signal — see {@link StrandRevocationEnforcerDeps.onSelfRevoked}. */
|
|
352
|
+
private selfRevokedSignaled = false;
|
|
353
|
+
|
|
354
|
+
constructor(
|
|
355
|
+
private readonly deps: StrandRevocationEnforcerDeps,
|
|
356
|
+
config?: StrandRevocationEnforcementConfig
|
|
357
|
+
) {
|
|
358
|
+
this.backstopMs = config?.backstopMs ?? DEFAULT_REVOCATION_BACKSTOP_MS;
|
|
359
|
+
this.autoRefresh = config?.autoRefresh ?? true;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Number of peers currently denied — test/diagnostic surface. */
|
|
363
|
+
get revokedCount(): number {
|
|
364
|
+
return this.revokedPeerIds.size;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Subscribe to membership changes and run one refresh at once (not awaited —
|
|
369
|
+
* bring-up never blocks on it); the module doc's "When the deny set is
|
|
370
|
+
* refreshed" has the full schedule. Call once the strand's database is
|
|
371
|
+
* published. A second call, or one after {@link stop}, is a no-op.
|
|
372
|
+
*/
|
|
373
|
+
start(): void {
|
|
374
|
+
if (this.started || this.stopped) return;
|
|
375
|
+
this.started = true;
|
|
376
|
+
this.watch = this.subscribe();
|
|
377
|
+
this.watch.trigger(0);
|
|
378
|
+
log('[%s] revocation enforcer started (backstop %dms)', this.deps.label, this.backstopMs);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Stop scheduling refreshes; a read already in flight completes but its result is discarded. */
|
|
382
|
+
stop(): void {
|
|
383
|
+
if (this.stopped) return;
|
|
384
|
+
this.stopped = true;
|
|
385
|
+
this.watch?.dispose();
|
|
386
|
+
this.watch = undefined;
|
|
387
|
+
log('[%s] revocation enforcer stopped', this.deps.label);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Refresh the deny set now. Never rejects. Serialized: concurrent calls chain,
|
|
392
|
+
* so no two reads overlap, and each explicit call gets a read that STARTS
|
|
393
|
+
* after the call (an on-demand refresh after a revocation commit observes that
|
|
394
|
+
* commit). A failed or slow read keeps the previous snapshot — never clears it.
|
|
395
|
+
*/
|
|
396
|
+
refresh(): Promise<void> {
|
|
397
|
+
const run = this.refreshTail.then(() => this.doRefresh());
|
|
398
|
+
this.refreshTail = run;
|
|
399
|
+
return run;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Is `remotePeerId` positively revoked in the current snapshot? Synchronous by
|
|
404
|
+
* design — this is judged inside libp2p gates where an await against the DB
|
|
405
|
+
* would deadlock (the control stream gate's argument).
|
|
406
|
+
*/
|
|
407
|
+
isRevoked(remotePeerId: string): boolean {
|
|
408
|
+
return this.revokedPeerIds.has(remotePeerId);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* The fail-closed PRIMARY layer: the per-stream predicate wired as
|
|
413
|
+
* `createLibp2pNode({ authorizeInboundStream })`, gating all four
|
|
414
|
+
* `/optimystic/strand-<id>/{repo,cluster,sync,block-transfer}` protocols.
|
|
415
|
+
* Also what catches a revoked peer arriving over a RELAYED connection, or
|
|
416
|
+
* over a connection admitted before the revocation replicated here.
|
|
417
|
+
*/
|
|
418
|
+
authorizeStream(remotePeerId: string, _protocol: string): boolean {
|
|
419
|
+
return !this.isRevoked(remotePeerId);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* The watch every scheduled refresh runs on: the deps' change subscription, or
|
|
424
|
+
* a timer-only one when changes are not watched or the subscription throws.
|
|
425
|
+
*
|
|
426
|
+
* NOTE: a refresh can wake itself once. Its read may start a read-repair sync
|
|
427
|
+
* (a held block read more than Optimystic's `readRepairWindowMs`, 10 s, after
|
|
428
|
+
* its last check), and that sync's landing raises a change. The follow-up
|
|
429
|
+
* refresh runs within the coalescing window plus one read — well inside the
|
|
430
|
+
* read-repair window — so it starts no second sync and the chain stops there.
|
|
431
|
+
* If idle sync counts on a strand do not fall with this subscription, suspect
|
|
432
|
+
* this loop first.
|
|
433
|
+
*/
|
|
434
|
+
private subscribe(): TableChangeWatch {
|
|
435
|
+
const onChange = (): Promise<void> => this.refresh();
|
|
436
|
+
const options: TableChangeWatchOptions = {
|
|
437
|
+
label: `strand-revocation:${this.deps.label}`,
|
|
438
|
+
backstopMs: this.backstopMs,
|
|
439
|
+
scheduler: this.deps.scheduler,
|
|
440
|
+
};
|
|
441
|
+
if (this.autoRefresh && this.deps.watchChanges) {
|
|
442
|
+
try {
|
|
443
|
+
return this.deps.watchChanges(onChange, options);
|
|
444
|
+
} catch (error) {
|
|
445
|
+
log('[%s] membership change subscription failed — refreshing on the backstop and retries only: %o', this.deps.label, error);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return scheduleRuns(onChange, options);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* One serialized snapshot rebuild; contains every failure (contract: never rejects).
|
|
453
|
+
*
|
|
454
|
+
* NOTE: nothing here carries a deadline — a `readRows` or a `hangUp` that never
|
|
455
|
+
* settles wedges the refresh chain, which freezes the deny set (the watch runs
|
|
456
|
+
* one refresh at a time, and every later `refresh()` chains behind the stuck one).
|
|
457
|
+
* Both are bounded in practice (Quereus reads settle; libp2p's connection close
|
|
458
|
+
* has its own timeout). If a strand is ever seen with a stale deny set and no
|
|
459
|
+
* refresh log line, wrap both awaits in a timeout rather than hunting further.
|
|
460
|
+
*/
|
|
461
|
+
private async doRefresh(): Promise<void> {
|
|
462
|
+
if (this.stopped) return;
|
|
463
|
+
try {
|
|
464
|
+
const rows = await this.deps.readRows();
|
|
465
|
+
if (this.stopped) return;
|
|
466
|
+
this.revokedPeerIds = deriveRevokedPeerIds(rows);
|
|
467
|
+
log('[%s] deny set refreshed: %d revoked peer(s)', this.deps.label, this.revokedPeerIds.size);
|
|
468
|
+
this.followUpWideStrand(rows);
|
|
469
|
+
// Awaited inside the refresh, so an on-demand refresh() resolves only once
|
|
470
|
+
// the cut has been made — that is what makes "revoke, then refresh" an
|
|
471
|
+
// immediate teardown for the caller rather than a scheduled one.
|
|
472
|
+
await this.tearDownRevoked(this.revokedPeerIds);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
log('[%s] deny-set refresh failed — keeping previous snapshot: %o', this.deps.label, error);
|
|
475
|
+
this.scheduleFollowUp(REVOCATION_REFRESH_RETRY_MS, 'refresh failed');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Arm one more refresh `delayMs` from now, unless {@link StrandRevocationEnforcementConfig.autoRefresh} is off or the enforcer is not running. */
|
|
480
|
+
private scheduleFollowUp(delayMs: number, reason: string): void {
|
|
481
|
+
if (!this.autoRefresh || !this.watch) return;
|
|
482
|
+
log('[%s] %s — refreshing again in %dms', this.deps.label, reason, delayMs);
|
|
483
|
+
this.watch.trigger(delayMs);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Schedule {@link REVOCATION_WIDE_STRAND_REFRESH_MS} when `rows` bind more distinct peers than the cluster holds. */
|
|
487
|
+
private followUpWideStrand(rows: StrandRevocationRows): void {
|
|
488
|
+
const breadth = this.deps.cohortBreadth;
|
|
489
|
+
if (breadth === undefined) return;
|
|
490
|
+
const boundPeers = new Set(rows.bindings.map((binding) => binding.peerId)).size;
|
|
491
|
+
if (boundPeers > breadth) {
|
|
492
|
+
this.scheduleFollowUp(REVOCATION_WIDE_STRAND_REFRESH_MS, `${boundPeers} bound peers on a cluster of ${breadth}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Close what is already OPEN to the peers of `revoked` — the teardown half of
|
|
498
|
+
* enforcement (module doc: "Removal cuts LIVE sessions too"). Never rejects:
|
|
499
|
+
* every failure is contained, so the refresh chain survives, and a failed
|
|
500
|
+
* `hangUp` schedules a retry.
|
|
501
|
+
*
|
|
502
|
+
* `revoked` is passed IN rather than read off the field, so the sweep judges
|
|
503
|
+
* the exact snapshot its own refresh produced; likewise the connection list is
|
|
504
|
+
* enumerated once, before any await, so a peer re-admitted mid-sweep is at
|
|
505
|
+
* worst hung up once and reconnects.
|
|
506
|
+
*
|
|
507
|
+
* NOTE: dropping the revoked peer from the strand's Optimystic cohort needs no
|
|
508
|
+
* separate action — machines quiesce strands routinely and the remaining cohort
|
|
509
|
+
* keeps committing, so an unreachable cohort peer is already tolerated, and
|
|
510
|
+
* `denyDialPeer` makes the exclusion fast (a local refusal, not a dial timeout).
|
|
511
|
+
* Two residual facts, both accepted: the removed node keeps every block it had
|
|
512
|
+
* already replicated (revocation is forward-looking), and a block whose only
|
|
513
|
+
* holders were the removed party's machines can be unavailable to the rest
|
|
514
|
+
* until repair.
|
|
515
|
+
*
|
|
516
|
+
* NOTE: post-removal commit latency is real and MEASURED, not hypothetical. On
|
|
517
|
+
* the two-parties-by-two-machines fixture in
|
|
518
|
+
* `integration-tests/src/scenarios/strand-removal-cuts-network.integration.ts`
|
|
519
|
+
* the remaining cohort's FIRST write after a cut fails repeatedly with
|
|
520
|
+
* `BlockUnavailableError: … (peers-unreachable)` before it commits — three or
|
|
521
|
+
* four failures, 3.5-4.3 s, over four runs — because the cohort still listed
|
|
522
|
+
* the removed party's now unreachable machines and had to downsize to the
|
|
523
|
+
* live holders first. It
|
|
524
|
+
* recovers on its own, so this is a latency cost, not a defect, and it is
|
|
525
|
+
* stated for app authors in `docs/strands.md` → Removing Members. If it ever
|
|
526
|
+
* stops recovering — a write that never commits, or `cluster-fetch:no-quorum` —
|
|
527
|
+
* that is an upstream (Optimystic) conversation, not a workaround here.
|
|
528
|
+
*/
|
|
529
|
+
private async tearDownRevoked(revoked: ReadonlySet<string>): Promise<void> {
|
|
530
|
+
if (this.stopped) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const network = this.resolveNetwork();
|
|
534
|
+
if (!network) {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const selfPeerId = network.peerId.toString();
|
|
538
|
+
// Judged on EVERY pass, including the empty-set one: leaving the set is what
|
|
539
|
+
// re-arms the signal (see trackSelfRevoked). Being revoked ourselves tears
|
|
540
|
+
// nothing down on our own behalf — the other side's gate already refuses us —
|
|
541
|
+
// so the sweep below still runs for every OTHER revoked peer in the pass.
|
|
542
|
+
this.trackSelfRevoked(revoked.has(selfPeerId));
|
|
543
|
+
if (revoked.size === 0) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
for (const peer of connectedRevokedPeers(network, revoked, selfPeerId, this.deps.label)) {
|
|
547
|
+
await this.hangUpRevoked(network, peer);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** The lazily-read network handle; an accessor that throws costs this pass only. */
|
|
552
|
+
private resolveNetwork(): StrandRevocationNetwork | undefined {
|
|
553
|
+
try {
|
|
554
|
+
return this.deps.getNetwork?.();
|
|
555
|
+
} catch (error) {
|
|
556
|
+
log('[%s] network handle unavailable — teardown skipped this pass: %o', this.deps.label, error);
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** One hangUp, contained: a stopping node or an already-closed connection is not an error here. */
|
|
562
|
+
private async hangUpRevoked(network: StrandRevocationNetwork, peer: PeerId): Promise<void> {
|
|
563
|
+
try {
|
|
564
|
+
await network.hangUp(peer);
|
|
565
|
+
log('[%s] hung up revoked peer %s', this.deps.label, peer.toString());
|
|
566
|
+
} catch (error) {
|
|
567
|
+
log('[%s] hangUp of revoked peer %s failed: %o', this.deps.label, peer.toString(), error);
|
|
568
|
+
this.scheduleFollowUp(REVOCATION_REFRESH_RETRY_MS, 'hangUp failed');
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Latch {@link StrandRevocationEnforcerDeps.onSelfRevoked} to the TRANSITION
|
|
574
|
+
* into the revoked state: fired once on entry, and re-armed the moment this
|
|
575
|
+
* node is out of the set again — a manager can re-admit a party it removed
|
|
576
|
+
* (a fresh `Member` row makes the orphaned bindings live again), and a second
|
|
577
|
+
* removal has to be reported like the first.
|
|
578
|
+
*/
|
|
579
|
+
private trackSelfRevoked(selfRevoked: boolean): void {
|
|
580
|
+
if (!selfRevoked) {
|
|
581
|
+
this.selfRevokedSignaled = false;
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (this.selfRevokedSignaled) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
this.selfRevokedSignaled = true;
|
|
588
|
+
log('[%s] THIS node is revoked from the strand (removed or left) — signalling', this.deps.label);
|
|
589
|
+
try {
|
|
590
|
+
this.deps.onSelfRevoked?.();
|
|
591
|
+
} catch (error) {
|
|
592
|
+
log('[%s] onSelfRevoked handler threw: %o', this.deps.label, error);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* The distinct peer ids of `network`'s open connections that are in `revoked`,
|
|
599
|
+
* as the `PeerId` objects `hangUp` takes — reusing each connection's own
|
|
600
|
+
* `remotePeer` rather than parsing the string back, so no peer-id codec is
|
|
601
|
+
* needed here. Self is excluded (a node holds no connection to itself, and
|
|
602
|
+
* hanging one up would be nonsense if it did), and a peer connected twice — say
|
|
603
|
+
* one direct and one relayed — yields ONE hangUp, which closes both. A throw
|
|
604
|
+
* mid-enumeration is logged and keeps whatever was collected before it: a
|
|
605
|
+
* partial sweep beats none, and the next refresh re-enumerates anyway.
|
|
606
|
+
*/
|
|
607
|
+
function connectedRevokedPeers(
|
|
608
|
+
network: StrandRevocationNetwork,
|
|
609
|
+
revoked: ReadonlySet<string>,
|
|
610
|
+
selfPeerId: string,
|
|
611
|
+
label: string
|
|
612
|
+
): PeerId[] {
|
|
613
|
+
const targets = new Map<string, PeerId>();
|
|
614
|
+
try {
|
|
615
|
+
for (const connection of network.getConnections()) {
|
|
616
|
+
const remotePeerId = connection.remotePeer.toString();
|
|
617
|
+
if (remotePeerId !== selfPeerId && revoked.has(remotePeerId)) {
|
|
618
|
+
targets.set(remotePeerId, connection.remotePeer);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
} catch (error) {
|
|
622
|
+
log('[%s] connection enumeration failed — teardown skipped this pass: %o', label, error);
|
|
623
|
+
}
|
|
624
|
+
return [...targets.values()];
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Derive the deny set: a peer id is revoked iff it has at least one ORPHANED
|
|
629
|
+
* binding (a `MemberPeer` row whose member key has no live `Member` row) AND no
|
|
630
|
+
* live-membered binding. The second conjunct is what lets a peer id bound to
|
|
631
|
+
* TWO member keys — revoked under one, live under the other — keep serving the
|
|
632
|
+
* live member: a machine serving a live member must not be cut. Per-binding,
|
|
633
|
+
* not per-member, so every machine of a multi-device removed party lands in
|
|
634
|
+
* the set.
|
|
635
|
+
*/
|
|
636
|
+
function deriveRevokedPeerIds(rows: StrandRevocationRows): Set<string> {
|
|
637
|
+
const orphaned = new Set<string>();
|
|
638
|
+
const live = new Set<string>();
|
|
639
|
+
for (const binding of rows.bindings) {
|
|
640
|
+
(rows.memberKeys.has(binding.memberKey) ? live : orphaned).add(binding.peerId);
|
|
641
|
+
}
|
|
642
|
+
for (const peerId of live) {
|
|
643
|
+
orphaned.delete(peerId);
|
|
644
|
+
}
|
|
645
|
+
return orphaned;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** The one capability the gater composition needs from the enforcer. */
|
|
649
|
+
export type StrandRevocationJudge = Pick<StrandRevocationEnforcer, 'isRevoked'>;
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Build a closed strand's connection gater: the caller-supplied gater (if any)
|
|
653
|
+
* with revoked-peer denial composed onto three hooks. The opportunistic
|
|
654
|
+
* SECONDARY layer — the per-stream gate above is the fail-closed primary.
|
|
655
|
+
*
|
|
656
|
+
* - `denyInboundEncryptedConnection`: deny iff revoked, so a removed party's
|
|
657
|
+
* node is refused before any protocol negotiation.
|
|
658
|
+
* - `denyDialPeer`: deny iff revoked — the owner ruling covers "communicated
|
|
659
|
+
* with", not just "answered to", and refusing the dial locally is instant,
|
|
660
|
+
* which keeps Optimystic from wasting a dial timeout on a peer this node
|
|
661
|
+
* will not talk to anyway.
|
|
662
|
+
* - `denyInboundRelayReservation`: deny iff revoked — matters when the strand
|
|
663
|
+
* node runs the relay server (the storage-profile default), where admitting
|
|
664
|
+
* the reservation would let a removed party keep using this node's
|
|
665
|
+
* forwarding capacity.
|
|
666
|
+
*
|
|
667
|
+
* Composition semantics: every hook of `base` is preserved as-is; on the three
|
|
668
|
+
* composed hooks a deny from EITHER the base gater or the revocation check
|
|
669
|
+
* denies, and an error (or a decision slower than `decisionTimeoutMs`) in
|
|
670
|
+
* either is fail-open — the base error admits through to the revocation check,
|
|
671
|
+
* a revocation-check error admits outright, and the fail-closed stream gate
|
|
672
|
+
* still stands behind both. The deadline is belt-and-braces: the snapshot read
|
|
673
|
+
* is synchronous.
|
|
674
|
+
*
|
|
675
|
+
* NOTE: accepted tradeoff — swallowing a BASE hook's throw diverges from
|
|
676
|
+
* `createMembershipConnectionGater`, which lets it propagate to libp2p (a
|
|
677
|
+
* fail-closed outcome there). Uniform fail-open was chosen for this module so
|
|
678
|
+
* every hook has one failure direction, and it was the shape the plan specified;
|
|
679
|
+
* the cost is that an embedder gater which throws is silently admitted here
|
|
680
|
+
* while the same gater passed raw to an OPEN strand would refuse. Loud in the
|
|
681
|
+
* log either way. Revisit if an embedder ever ships a gater whose throw is a
|
|
682
|
+
* meaningful deny, or if the two gaters are unified.
|
|
683
|
+
*
|
|
684
|
+
* NOTE: `base` is spread, so a gater passed as a CLASS INSTANCE would lose its
|
|
685
|
+
* prototype methods — same caveat as `createMembershipConnectionGater`; every
|
|
686
|
+
* caller in this repo supplies a plain object (the RN/web permissive gaters,
|
|
687
|
+
* e.g. `{ denyDialMultiaddr: () => false }`, keep working unchanged).
|
|
688
|
+
*/
|
|
689
|
+
export function createRevocationConnectionGater(
|
|
690
|
+
enforcer: StrandRevocationJudge,
|
|
691
|
+
base?: ConnectionGater,
|
|
692
|
+
decisionTimeoutMs: number = ADMISSION_DECISION_TIMEOUT_MS
|
|
693
|
+
): ConnectionGater {
|
|
694
|
+
/** The base gater's verdict, fail-open: an error or absence is "not denied". */
|
|
695
|
+
const baseDenies = async (decide: () => Promise<boolean> | boolean | undefined, hook: string): Promise<boolean> => {
|
|
696
|
+
try {
|
|
697
|
+
return await decide() === true;
|
|
698
|
+
} catch (error) {
|
|
699
|
+
log('base gater %s threw — treating as not-denied (fail-open): %o', hook, error);
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
/** The revocation verdict, bounded and fail-open. */
|
|
704
|
+
const revokedDenies = async (peerId: PeerId, hook: string): Promise<boolean> => {
|
|
705
|
+
const remotePeerId = peerId.toString();
|
|
706
|
+
try {
|
|
707
|
+
const revoked = await decideWithinDeadline(
|
|
708
|
+
() => enforcer.isRevoked(remotePeerId), false, decisionTimeoutMs, `${hook}(${remotePeerId})`
|
|
709
|
+
);
|
|
710
|
+
if (revoked) {
|
|
711
|
+
log('%s: %s is revoked — denying', hook, remotePeerId);
|
|
712
|
+
}
|
|
713
|
+
return revoked;
|
|
714
|
+
} catch (error) {
|
|
715
|
+
log('%s revocation check threw for %s — admitting (fail-open; stream gate decides): %o', hook, remotePeerId, error);
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
return {
|
|
720
|
+
...base,
|
|
721
|
+
denyDialPeer: async (peerId: PeerId): Promise<boolean> => {
|
|
722
|
+
if (await baseDenies(() => base?.denyDialPeer?.(peerId), 'denyDialPeer')) {
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
return revokedDenies(peerId, 'denyDialPeer');
|
|
726
|
+
},
|
|
727
|
+
denyInboundEncryptedConnection: async (peerId: PeerId, maConn: MultiaddrConnection): Promise<boolean> => {
|
|
728
|
+
if (await baseDenies(() => base?.denyInboundEncryptedConnection?.(peerId, maConn), 'denyInboundEncryptedConnection')) {
|
|
729
|
+
return true;
|
|
730
|
+
}
|
|
731
|
+
return revokedDenies(peerId, 'denyInboundEncryptedConnection');
|
|
732
|
+
},
|
|
733
|
+
denyInboundRelayReservation: async (peerId: PeerId): Promise<boolean> => {
|
|
734
|
+
if (await baseDenies(() => base?.denyInboundRelayReservation?.(peerId), 'denyInboundRelayReservation')) {
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
return revokedDenies(peerId, 'denyInboundRelayReservation');
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
}
|