@optimystic/db-core 0.13.4 → 0.14.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/dist/src/cluster/structs.d.ts +29 -0
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cohort-topic/addressing.d.ts +53 -0
- package/dist/src/cohort-topic/addressing.d.ts.map +1 -0
- package/dist/src/cohort-topic/addressing.js +95 -0
- package/dist/src/cohort-topic/addressing.js.map +1 -0
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.d.ts +105 -0
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.d.ts.map +1 -0
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.js +197 -0
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.js.map +1 -0
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.d.ts +50 -0
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.d.ts.map +1 -0
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.js +62 -0
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.js.map +1 -0
- package/dist/src/cohort-topic/antidos/index.d.ts +6 -0
- package/dist/src/cohort-topic/antidos/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/antidos/index.js +6 -0
- package/dist/src/cohort-topic/antidos/index.js.map +1 -0
- package/dist/src/cohort-topic/antidos/rate-limiter.d.ts +95 -0
- package/dist/src/cohort-topic/antidos/rate-limiter.d.ts.map +1 -0
- package/dist/src/cohort-topic/antidos/rate-limiter.js +146 -0
- package/dist/src/cohort-topic/antidos/rate-limiter.js.map +1 -0
- package/dist/src/cohort-topic/antidos/replay-guard.d.ts +37 -0
- package/dist/src/cohort-topic/antidos/replay-guard.d.ts.map +1 -0
- package/dist/src/cohort-topic/antidos/replay-guard.js +78 -0
- package/dist/src/cohort-topic/antidos/replay-guard.js.map +1 -0
- package/dist/src/cohort-topic/antidos/topic-budget.d.ts +50 -0
- package/dist/src/cohort-topic/antidos/topic-budget.d.ts.map +1 -0
- package/dist/src/cohort-topic/antidos/topic-budget.js +95 -0
- package/dist/src/cohort-topic/antidos/topic-budget.js.map +1 -0
- package/dist/src/cohort-topic/antiflood/index.d.ts +3 -0
- package/dist/src/cohort-topic/antiflood/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/antiflood/index.js +3 -0
- package/dist/src/cohort-topic/antiflood/index.js.map +1 -0
- package/dist/src/cohort-topic/antiflood/invariants.d.ts +70 -0
- package/dist/src/cohort-topic/antiflood/invariants.d.ts.map +1 -0
- package/dist/src/cohort-topic/antiflood/invariants.js +86 -0
- package/dist/src/cohort-topic/antiflood/invariants.js.map +1 -0
- package/dist/src/cohort-topic/antiflood/jitter.d.ts +65 -0
- package/dist/src/cohort-topic/antiflood/jitter.d.ts.map +1 -0
- package/dist/src/cohort-topic/antiflood/jitter.js +75 -0
- package/dist/src/cohort-topic/antiflood/jitter.js.map +1 -0
- package/dist/src/cohort-topic/coldstart.d.ts +117 -0
- package/dist/src/cohort-topic/coldstart.d.ts.map +1 -0
- package/dist/src/cohort-topic/coldstart.js +127 -0
- package/dist/src/cohort-topic/coldstart.js.map +1 -0
- package/dist/src/cohort-topic/dmax.d.ts +44 -0
- package/dist/src/cohort-topic/dmax.d.ts.map +1 -0
- package/dist/src/cohort-topic/dmax.js +70 -0
- package/dist/src/cohort-topic/dmax.js.map +1 -0
- package/dist/src/cohort-topic/gossip/bus.d.ts +85 -0
- package/dist/src/cohort-topic/gossip/bus.d.ts.map +1 -0
- package/dist/src/cohort-topic/gossip/bus.js +146 -0
- package/dist/src/cohort-topic/gossip/bus.js.map +1 -0
- package/dist/src/cohort-topic/gossip/index.d.ts +4 -0
- package/dist/src/cohort-topic/gossip/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/gossip/index.js +4 -0
- package/dist/src/cohort-topic/gossip/index.js.map +1 -0
- package/dist/src/cohort-topic/gossip/records.d.ts +12 -0
- package/dist/src/cohort-topic/gossip/records.d.ts.map +1 -0
- package/dist/src/cohort-topic/gossip/records.js +41 -0
- package/dist/src/cohort-topic/gossip/records.js.map +1 -0
- package/dist/src/cohort-topic/gossip/view.d.ts +39 -0
- package/dist/src/cohort-topic/gossip/view.d.ts.map +1 -0
- package/dist/src/cohort-topic/gossip/view.js +30 -0
- package/dist/src/cohort-topic/gossip/view.js.map +1 -0
- package/dist/src/cohort-topic/index.d.ts +21 -0
- package/dist/src/cohort-topic/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/index.js +21 -0
- package/dist/src/cohort-topic/index.js.map +1 -0
- package/dist/src/cohort-topic/load/barometer.d.ts +78 -0
- package/dist/src/cohort-topic/load/barometer.d.ts.map +1 -0
- package/dist/src/cohort-topic/load/barometer.js +93 -0
- package/dist/src/cohort-topic/load/barometer.js.map +1 -0
- package/dist/src/cohort-topic/load/index.d.ts +2 -0
- package/dist/src/cohort-topic/load/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/load/index.js +2 -0
- package/dist/src/cohort-topic/load/index.js.map +1 -0
- package/dist/src/cohort-topic/member-engine.d.ts +134 -0
- package/dist/src/cohort-topic/member-engine.d.ts.map +1 -0
- package/dist/src/cohort-topic/member-engine.js +264 -0
- package/dist/src/cohort-topic/member-engine.js.map +1 -0
- package/dist/src/cohort-topic/membership/index.d.ts +4 -0
- package/dist/src/cohort-topic/membership/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/membership/index.js +4 -0
- package/dist/src/cohort-topic/membership/index.js.map +1 -0
- package/dist/src/cohort-topic/membership/publisher.d.ts +82 -0
- package/dist/src/cohort-topic/membership/publisher.d.ts.map +1 -0
- package/dist/src/cohort-topic/membership/publisher.js +107 -0
- package/dist/src/cohort-topic/membership/publisher.js.map +1 -0
- package/dist/src/cohort-topic/membership/source.d.ts +31 -0
- package/dist/src/cohort-topic/membership/source.d.ts.map +1 -0
- package/dist/src/cohort-topic/membership/source.js +32 -0
- package/dist/src/cohort-topic/membership/source.js.map +1 -0
- package/dist/src/cohort-topic/membership/verifier.d.ts +96 -0
- package/dist/src/cohort-topic/membership/verifier.d.ts.map +1 -0
- package/dist/src/cohort-topic/membership/verifier.js +250 -0
- package/dist/src/cohort-topic/membership/verifier.js.map +1 -0
- package/dist/src/cohort-topic/ports.d.ts +149 -0
- package/dist/src/cohort-topic/ports.d.ts.map +1 -0
- package/dist/src/cohort-topic/ports.js +25 -0
- package/dist/src/cohort-topic/ports.js.map +1 -0
- package/dist/src/cohort-topic/promotion.d.ts +135 -0
- package/dist/src/cohort-topic/promotion.d.ts.map +1 -0
- package/dist/src/cohort-topic/promotion.js +269 -0
- package/dist/src/cohort-topic/promotion.js.map +1 -0
- package/dist/src/cohort-topic/registration/bytes.d.ts +16 -0
- package/dist/src/cohort-topic/registration/bytes.d.ts.map +1 -0
- package/dist/src/cohort-topic/registration/bytes.js +32 -0
- package/dist/src/cohort-topic/registration/bytes.js.map +1 -0
- package/dist/src/cohort-topic/registration/handoff.d.ts +69 -0
- package/dist/src/cohort-topic/registration/handoff.d.ts.map +1 -0
- package/dist/src/cohort-topic/registration/handoff.js +98 -0
- package/dist/src/cohort-topic/registration/handoff.js.map +1 -0
- package/dist/src/cohort-topic/registration/index.d.ts +7 -0
- package/dist/src/cohort-topic/registration/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/registration/index.js +7 -0
- package/dist/src/cohort-topic/registration/index.js.map +1 -0
- package/dist/src/cohort-topic/registration/renewal.d.ts +115 -0
- package/dist/src/cohort-topic/registration/renewal.d.ts.map +1 -0
- package/dist/src/cohort-topic/registration/renewal.js +306 -0
- package/dist/src/cohort-topic/registration/renewal.js.map +1 -0
- package/dist/src/cohort-topic/registration/sharding.d.ts +31 -0
- package/dist/src/cohort-topic/registration/sharding.d.ts.map +1 -0
- package/dist/src/cohort-topic/registration/sharding.js +48 -0
- package/dist/src/cohort-topic/registration/sharding.js.map +1 -0
- package/dist/src/cohort-topic/registration/store.d.ts +13 -0
- package/dist/src/cohort-topic/registration/store.d.ts.map +1 -0
- package/dist/src/cohort-topic/registration/store.js +72 -0
- package/dist/src/cohort-topic/registration/store.js.map +1 -0
- package/dist/src/cohort-topic/registration/types.d.ts +71 -0
- package/dist/src/cohort-topic/registration/types.d.ts.map +1 -0
- package/dist/src/cohort-topic/registration/types.js +27 -0
- package/dist/src/cohort-topic/registration/types.js.map +1 -0
- package/dist/src/cohort-topic/ring-hash.d.ts +27 -0
- package/dist/src/cohort-topic/ring-hash.d.ts.map +1 -0
- package/dist/src/cohort-topic/ring-hash.js +45 -0
- package/dist/src/cohort-topic/ring-hash.js.map +1 -0
- package/dist/src/cohort-topic/service.d.ts +157 -0
- package/dist/src/cohort-topic/service.d.ts.map +1 -0
- package/dist/src/cohort-topic/service.js +237 -0
- package/dist/src/cohort-topic/service.js.map +1 -0
- package/dist/src/cohort-topic/sig/index.d.ts +3 -0
- package/dist/src/cohort-topic/sig/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/sig/index.js +3 -0
- package/dist/src/cohort-topic/sig/index.js.map +1 -0
- package/dist/src/cohort-topic/sig/payloads.d.ts +25 -0
- package/dist/src/cohort-topic/sig/payloads.d.ts.map +1 -0
- package/dist/src/cohort-topic/sig/payloads.js +25 -0
- package/dist/src/cohort-topic/sig/payloads.js.map +1 -0
- package/dist/src/cohort-topic/sig/threshold.d.ts +33 -0
- package/dist/src/cohort-topic/sig/threshold.d.ts.map +1 -0
- package/dist/src/cohort-topic/sig/threshold.js +51 -0
- package/dist/src/cohort-topic/sig/threshold.js.map +1 -0
- package/dist/src/cohort-topic/tiers.d.ts +58 -0
- package/dist/src/cohort-topic/tiers.d.ts.map +1 -0
- package/dist/src/cohort-topic/tiers.js +49 -0
- package/dist/src/cohort-topic/tiers.js.map +1 -0
- package/dist/src/cohort-topic/traffic.d.ts +90 -0
- package/dist/src/cohort-topic/traffic.d.ts.map +1 -0
- package/dist/src/cohort-topic/traffic.js +146 -0
- package/dist/src/cohort-topic/traffic.js.map +1 -0
- package/dist/src/cohort-topic/walk.d.ts +123 -0
- package/dist/src/cohort-topic/walk.d.ts.map +1 -0
- package/dist/src/cohort-topic/walk.js +150 -0
- package/dist/src/cohort-topic/walk.js.map +1 -0
- package/dist/src/cohort-topic/willingness.d.ts +123 -0
- package/dist/src/cohort-topic/willingness.d.ts.map +1 -0
- package/dist/src/cohort-topic/willingness.js +143 -0
- package/dist/src/cohort-topic/willingness.js.map +1 -0
- package/dist/src/cohort-topic/wire/codec.d.ts +42 -0
- package/dist/src/cohort-topic/wire/codec.d.ts.map +1 -0
- package/dist/src/cohort-topic/wire/codec.js +163 -0
- package/dist/src/cohort-topic/wire/codec.js.map +1 -0
- package/dist/src/cohort-topic/wire/index.d.ts +6 -0
- package/dist/src/cohort-topic/wire/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/wire/index.js +6 -0
- package/dist/src/cohort-topic/wire/index.js.map +1 -0
- package/dist/src/cohort-topic/wire/payloads.d.ts +38 -0
- package/dist/src/cohort-topic/wire/payloads.d.ts.map +1 -0
- package/dist/src/cohort-topic/wire/payloads.js +102 -0
- package/dist/src/cohort-topic/wire/payloads.js.map +1 -0
- package/dist/src/cohort-topic/wire/types.d.ts +304 -0
- package/dist/src/cohort-topic/wire/types.d.ts.map +1 -0
- package/dist/src/cohort-topic/wire/types.js +16 -0
- package/dist/src/cohort-topic/wire/types.js.map +1 -0
- package/dist/src/cohort-topic/wire/validate.d.ts +25 -0
- package/dist/src/cohort-topic/wire/validate.d.ts.map +1 -0
- package/dist/src/cohort-topic/wire/validate.js +427 -0
- package/dist/src/cohort-topic/wire/validate.js.map +1 -0
- package/dist/src/collection/collection.d.ts +58 -1
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +76 -0
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +58 -2
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +59 -2
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/log/log.d.ts +42 -4
- package/dist/src/log/log.d.ts.map +1 -1
- package/dist/src/log/log.js +71 -3
- package/dist/src/log/log.js.map +1 -1
- package/dist/src/log/struct.d.ts +122 -2
- package/dist/src/log/struct.d.ts.map +1 -1
- package/dist/src/log/struct.js.map +1 -1
- package/dist/src/matchmaking/capability-filter.d.ts +24 -0
- package/dist/src/matchmaking/capability-filter.d.ts.map +1 -0
- package/dist/src/matchmaking/capability-filter.js +37 -0
- package/dist/src/matchmaking/capability-filter.js.map +1 -0
- package/dist/src/matchmaking/config.d.ts +77 -0
- package/dist/src/matchmaking/config.d.ts.map +1 -0
- package/dist/src/matchmaking/config.js +69 -0
- package/dist/src/matchmaking/config.js.map +1 -0
- package/dist/src/matchmaking/index.d.ts +21 -0
- package/dist/src/matchmaking/index.d.ts.map +1 -0
- package/dist/src/matchmaking/index.js +21 -0
- package/dist/src/matchmaking/index.js.map +1 -0
- package/dist/src/matchmaking/multi-cohort-seeker.d.ts +133 -0
- package/dist/src/matchmaking/multi-cohort-seeker.d.ts.map +1 -0
- package/dist/src/matchmaking/multi-cohort-seeker.js +142 -0
- package/dist/src/matchmaking/multi-cohort-seeker.js.map +1 -0
- package/dist/src/matchmaking/provider.d.ts +70 -0
- package/dist/src/matchmaking/provider.d.ts.map +1 -0
- package/dist/src/matchmaking/provider.js +93 -0
- package/dist/src/matchmaking/provider.js.map +1 -0
- package/dist/src/matchmaking/query-eval.d.ts +53 -0
- package/dist/src/matchmaking/query-eval.d.ts.map +1 -0
- package/dist/src/matchmaking/query-eval.js +69 -0
- package/dist/src/matchmaking/query-eval.js.map +1 -0
- package/dist/src/matchmaking/seeker-walk.d.ts +95 -0
- package/dist/src/matchmaking/seeker-walk.d.ts.map +1 -0
- package/dist/src/matchmaking/seeker-walk.js +75 -0
- package/dist/src/matchmaking/seeker-walk.js.map +1 -0
- package/dist/src/matchmaking/seeker.d.ts +51 -0
- package/dist/src/matchmaking/seeker.d.ts.map +1 -0
- package/dist/src/matchmaking/seeker.js +62 -0
- package/dist/src/matchmaking/seeker.js.map +1 -0
- package/dist/src/matchmaking/topic-anchor.d.ts +54 -0
- package/dist/src/matchmaking/topic-anchor.d.ts.map +1 -0
- package/dist/src/matchmaking/topic-anchor.js +67 -0
- package/dist/src/matchmaking/topic-anchor.js.map +1 -0
- package/dist/src/matchmaking/voting-quorum.d.ts +233 -0
- package/dist/src/matchmaking/voting-quorum.d.ts.map +1 -0
- package/dist/src/matchmaking/voting-quorum.js +238 -0
- package/dist/src/matchmaking/voting-quorum.js.map +1 -0
- package/dist/src/matchmaking/wire.d.ts +259 -0
- package/dist/src/matchmaking/wire.d.ts.map +1 -0
- package/dist/src/matchmaking/wire.js +484 -0
- package/dist/src/matchmaking/wire.js.map +1 -0
- package/dist/src/network/i-repo.d.ts +19 -0
- package/dist/src/network/i-repo.d.ts.map +1 -1
- package/dist/src/network/repo-protocol.d.ts +3 -1
- package/dist/src/network/repo-protocol.d.ts.map +1 -1
- package/dist/src/network/struct.d.ts +23 -1
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/reactivity/backfill.d.ts +118 -0
- package/dist/src/reactivity/backfill.d.ts.map +1 -0
- package/dist/src/reactivity/backfill.js +137 -0
- package/dist/src/reactivity/backfill.js.map +1 -0
- package/dist/src/reactivity/backpressure.d.ts +99 -0
- package/dist/src/reactivity/backpressure.d.ts.map +1 -0
- package/dist/src/reactivity/backpressure.js +153 -0
- package/dist/src/reactivity/backpressure.js.map +1 -0
- package/dist/src/reactivity/checkpoint.d.ts +165 -0
- package/dist/src/reactivity/checkpoint.d.ts.map +1 -0
- package/dist/src/reactivity/checkpoint.js +232 -0
- package/dist/src/reactivity/checkpoint.js.map +1 -0
- package/dist/src/reactivity/config.d.ts +131 -0
- package/dist/src/reactivity/config.d.ts.map +1 -0
- package/dist/src/reactivity/config.js +135 -0
- package/dist/src/reactivity/config.js.map +1 -0
- package/dist/src/reactivity/dedupe.d.ts +51 -0
- package/dist/src/reactivity/dedupe.d.ts.map +1 -0
- package/dist/src/reactivity/dedupe.js +90 -0
- package/dist/src/reactivity/dedupe.js.map +1 -0
- package/dist/src/reactivity/forwarder.d.ts +50 -0
- package/dist/src/reactivity/forwarder.d.ts.map +1 -0
- package/dist/src/reactivity/forwarder.js +52 -0
- package/dist/src/reactivity/forwarder.js.map +1 -0
- package/dist/src/reactivity/index.d.ts +34 -0
- package/dist/src/reactivity/index.d.ts.map +1 -0
- package/dist/src/reactivity/index.js +34 -0
- package/dist/src/reactivity/index.js.map +1 -0
- package/dist/src/reactivity/notification.d.ts +72 -0
- package/dist/src/reactivity/notification.d.ts.map +1 -0
- package/dist/src/reactivity/notification.js +92 -0
- package/dist/src/reactivity/notification.js.map +1 -0
- package/dist/src/reactivity/policy.d.ts +59 -0
- package/dist/src/reactivity/policy.d.ts.map +1 -0
- package/dist/src/reactivity/policy.js +64 -0
- package/dist/src/reactivity/policy.js.map +1 -0
- package/dist/src/reactivity/push-state.d.ts +140 -0
- package/dist/src/reactivity/push-state.d.ts.map +1 -0
- package/dist/src/reactivity/push-state.js +270 -0
- package/dist/src/reactivity/push-state.js.map +1 -0
- package/dist/src/reactivity/recover.d.ts +75 -0
- package/dist/src/reactivity/recover.d.ts.map +1 -0
- package/dist/src/reactivity/recover.js +105 -0
- package/dist/src/reactivity/recover.js.map +1 -0
- package/dist/src/reactivity/replay-buffer.d.ts +57 -0
- package/dist/src/reactivity/replay-buffer.d.ts.map +1 -0
- package/dist/src/reactivity/replay-buffer.js +88 -0
- package/dist/src/reactivity/replay-buffer.js.map +1 -0
- package/dist/src/reactivity/resume.d.ts +214 -0
- package/dist/src/reactivity/resume.d.ts.map +1 -0
- package/dist/src/reactivity/resume.js +358 -0
- package/dist/src/reactivity/resume.js.map +1 -0
- package/dist/src/reactivity/rotation.d.ts +283 -0
- package/dist/src/reactivity/rotation.d.ts.map +1 -0
- package/dist/src/reactivity/rotation.js +265 -0
- package/dist/src/reactivity/rotation.js.map +1 -0
- package/dist/src/reactivity/subscriber.d.ts +66 -0
- package/dist/src/reactivity/subscriber.d.ts.map +1 -0
- package/dist/src/reactivity/subscriber.js +75 -0
- package/dist/src/reactivity/subscriber.js.map +1 -0
- package/dist/src/reactivity/subscription.d.ts +52 -0
- package/dist/src/reactivity/subscription.d.ts.map +1 -0
- package/dist/src/reactivity/subscription.js +28 -0
- package/dist/src/reactivity/subscription.js.map +1 -0
- package/dist/src/reactivity/topic-anchor.d.ts +46 -0
- package/dist/src/reactivity/topic-anchor.d.ts.map +1 -0
- package/dist/src/reactivity/topic-anchor.js +61 -0
- package/dist/src/reactivity/topic-anchor.js.map +1 -0
- package/dist/src/reactivity/verify.d.ts +41 -0
- package/dist/src/reactivity/verify.d.ts.map +1 -0
- package/dist/src/reactivity/verify.js +48 -0
- package/dist/src/reactivity/verify.js.map +1 -0
- package/dist/src/reactivity/wire-validate.d.ts +28 -0
- package/dist/src/reactivity/wire-validate.d.ts.map +1 -0
- package/dist/src/reactivity/wire-validate.js +79 -0
- package/dist/src/reactivity/wire-validate.js.map +1 -0
- package/dist/src/reactivity/wire.d.ts +102 -0
- package/dist/src/reactivity/wire.d.ts.map +1 -0
- package/dist/src/reactivity/wire.js +129 -0
- package/dist/src/reactivity/wire.js.map +1 -0
- package/dist/src/transaction/coordinator.d.ts +9 -5
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +48 -25
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transactor/change-notifier.d.ts +74 -0
- package/dist/src/transactor/change-notifier.d.ts.map +1 -0
- package/dist/src/transactor/change-notifier.js +4 -0
- package/dist/src/transactor/change-notifier.js.map +1 -0
- package/dist/src/transactor/index.d.ts +1 -0
- package/dist/src/transactor/index.d.ts.map +1 -1
- package/dist/src/transactor/index.js +1 -0
- package/dist/src/transactor/index.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts +44 -2
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +132 -17
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/package.json +3 -2
- package/src/cluster/structs.ts +86 -57
- package/src/cohort-topic/addressing.ts +112 -0
- package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +253 -0
- package/src/cohort-topic/antidos/bootstrap-evidence.ts +94 -0
- package/src/cohort-topic/antidos/index.ts +5 -0
- package/src/cohort-topic/antidos/rate-limiter.ts +210 -0
- package/src/cohort-topic/antidos/replay-guard.ts +109 -0
- package/src/cohort-topic/antidos/topic-budget.ts +145 -0
- package/src/cohort-topic/antiflood/index.ts +2 -0
- package/src/cohort-topic/antiflood/invariants.ts +108 -0
- package/src/cohort-topic/antiflood/jitter.ts +117 -0
- package/src/cohort-topic/coldstart.ts +204 -0
- package/src/cohort-topic/dmax.ts +88 -0
- package/src/cohort-topic/gossip/bus.ts +217 -0
- package/src/cohort-topic/gossip/index.ts +3 -0
- package/src/cohort-topic/gossip/records.ts +45 -0
- package/src/cohort-topic/gossip/view.ts +66 -0
- package/src/cohort-topic/index.ts +20 -0
- package/src/cohort-topic/load/barometer.ts +134 -0
- package/src/cohort-topic/load/index.ts +1 -0
- package/src/cohort-topic/member-engine.ts +409 -0
- package/src/cohort-topic/membership/index.ts +3 -0
- package/src/cohort-topic/membership/publisher.ts +171 -0
- package/src/cohort-topic/membership/source.ts +41 -0
- package/src/cohort-topic/membership/verifier.ts +345 -0
- package/src/cohort-topic/ports.ts +157 -0
- package/src/cohort-topic/promotion.ts +384 -0
- package/src/cohort-topic/registration/bytes.ts +37 -0
- package/src/cohort-topic/registration/handoff.ts +154 -0
- package/src/cohort-topic/registration/index.ts +6 -0
- package/src/cohort-topic/registration/renewal.ts +423 -0
- package/src/cohort-topic/registration/sharding.ts +61 -0
- package/src/cohort-topic/registration/store.ts +81 -0
- package/src/cohort-topic/registration/types.ts +76 -0
- package/src/cohort-topic/ring-hash.ts +50 -0
- package/src/cohort-topic/service.ts +390 -0
- package/src/cohort-topic/sig/index.ts +2 -0
- package/src/cohort-topic/sig/payloads.ts +39 -0
- package/src/cohort-topic/sig/threshold.ts +64 -0
- package/src/cohort-topic/tiers.ts +74 -0
- package/src/cohort-topic/traffic.ts +211 -0
- package/src/cohort-topic/walk.ts +255 -0
- package/src/cohort-topic/willingness.ts +237 -0
- package/src/cohort-topic/wire/codec.ts +204 -0
- package/src/cohort-topic/wire/index.ts +16 -0
- package/src/cohort-topic/wire/payloads.ts +117 -0
- package/src/cohort-topic/wire/types.ts +343 -0
- package/src/cohort-topic/wire/validate.ts +478 -0
- package/src/collection/collection.ts +93 -1
- package/src/collections/tree/tree.ts +84 -3
- package/src/index.ts +3 -0
- package/src/log/log.ts +86 -7
- package/src/log/struct.ts +126 -2
- package/src/matchmaking/capability-filter.ts +45 -0
- package/src/matchmaking/config.ts +98 -0
- package/src/matchmaking/index.ts +21 -0
- package/src/matchmaking/multi-cohort-seeker.ts +234 -0
- package/src/matchmaking/provider.ts +123 -0
- package/src/matchmaking/query-eval.ts +105 -0
- package/src/matchmaking/seeker-walk.ts +127 -0
- package/src/matchmaking/seeker.ts +86 -0
- package/src/matchmaking/topic-anchor.ts +90 -0
- package/src/matchmaking/voting-quorum.ts +394 -0
- package/src/matchmaking/wire.ts +680 -0
- package/src/network/i-repo.ts +19 -0
- package/src/network/repo-protocol.ts +3 -2
- package/src/network/struct.ts +24 -1
- package/src/reactivity/backfill.ts +220 -0
- package/src/reactivity/backpressure.ts +191 -0
- package/src/reactivity/checkpoint.ts +308 -0
- package/src/reactivity/config.ts +172 -0
- package/src/reactivity/dedupe.ts +132 -0
- package/src/reactivity/forwarder.ts +87 -0
- package/src/reactivity/index.ts +34 -0
- package/src/reactivity/notification.ts +123 -0
- package/src/reactivity/policy.ts +79 -0
- package/src/reactivity/push-state.ts +343 -0
- package/src/reactivity/recover.ts +153 -0
- package/src/reactivity/replay-buffer.ts +141 -0
- package/src/reactivity/resume.ts +549 -0
- package/src/reactivity/rotation.ts +415 -0
- package/src/reactivity/subscriber.ts +132 -0
- package/src/reactivity/subscription.ts +66 -0
- package/src/reactivity/topic-anchor.ts +71 -0
- package/src/reactivity/verify.ts +73 -0
- package/src/reactivity/wire-validate.ts +87 -0
- package/src/reactivity/wire.ts +224 -0
- package/src/transaction/coordinator.ts +53 -27
- package/src/transactor/change-notifier.ts +80 -0
- package/src/transactor/index.ts +1 -0
- package/src/transactor/network-transactor.ts +162 -19
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — tier addressing (`coord_d`).
|
|
3
|
+
*
|
|
4
|
+
* Transcribed from `docs/cohort-topic.md` §Tier addressing:
|
|
5
|
+
*
|
|
6
|
+
* ```
|
|
7
|
+
* coord_0(_, topicId) = H(0x00 ‖ topicId)
|
|
8
|
+
* coord_d(P, topicId) = H(d ‖ prefix(P, d·log₂F) ‖ topicId) for d ≥ 1
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* `H` is the injected {@link IRingHash} (db-core's own SHA-256 truncated to the ring width — **not**
|
|
12
|
+
* a FRET import). `prefix(P, n)` is the `n` most-significant bits of peer id `P`, left-padded if
|
|
13
|
+
* shorter. `F` is the fan-out (default 16, `log₂F = 4`); tier `d` has exactly `F^d` coordinates.
|
|
14
|
+
*
|
|
15
|
+
* The db-p2p binding is responsible for ensuring the coord byte layout (ring width) matches FRET's
|
|
16
|
+
* `RING_BITS` so the routing keys produced here line up with FRET's ring on the wire.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { IRingHash, RingCoord } from "./ports.js";
|
|
20
|
+
|
|
21
|
+
/** Tier-addressing surface — derives the ring coordinate for tier `d` of a topic. */
|
|
22
|
+
export interface TierAddressing {
|
|
23
|
+
/** Fan-out per tier (default 16). */
|
|
24
|
+
readonly F: number;
|
|
25
|
+
/** Tier-0 root coordinate: `H(0x00 ‖ topicId)`. Peer-independent. */
|
|
26
|
+
coord0(topicId: Uint8Array): RingCoord;
|
|
27
|
+
/** Tier-`d` coordinate for `d ≥ 1`: `H(d ‖ prefix(peerId, d·log₂F) ‖ topicId)`. */
|
|
28
|
+
coordD(d: number, peerId: Uint8Array, topicId: Uint8Array): RingCoord;
|
|
29
|
+
/** Dispatches `d === 0` to {@link coord0}, otherwise to {@link coordD}. */
|
|
30
|
+
coord(d: number, peerId: Uint8Array, topicId: Uint8Array): RingCoord;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Default fan-out per tier (`log₂16 = 4`). */
|
|
34
|
+
export const DEFAULT_FANOUT = 16;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* `prefix(P, n)` — the `n` most-significant bits of `P`, packed MSB-first into `⌈n/8⌉` bytes with
|
|
38
|
+
* the trailing partial byte's unused low bits zeroed. If `P` carries fewer than `n` bits it is
|
|
39
|
+
* **left-padded**: the high `(n − bits(P))` positions are zero and `P`'s bits occupy the low end, so
|
|
40
|
+
* the result is always exactly `n` bits wide. `n === 0` yields an empty array (the tier-0 case).
|
|
41
|
+
*
|
|
42
|
+
* Exported for tests; not part of the public substrate surface.
|
|
43
|
+
*/
|
|
44
|
+
export function prefixBits(peerId: Uint8Array, n: number): Uint8Array {
|
|
45
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
46
|
+
throw new RangeError(`prefix bit count must be a non-negative integer, got ${n}`);
|
|
47
|
+
}
|
|
48
|
+
const outBytes = Math.ceil(n / 8);
|
|
49
|
+
const out = new Uint8Array(outBytes);
|
|
50
|
+
const available = peerId.length * 8;
|
|
51
|
+
const pad = Math.max(0, n - available);
|
|
52
|
+
for (let j = 0; j < n; j++) {
|
|
53
|
+
// Result bit j (MSB-first). High `pad` positions are zero (left-pad); the rest map to P.
|
|
54
|
+
if (j < pad) continue;
|
|
55
|
+
const srcBit = j - pad;
|
|
56
|
+
const srcByte = peerId[srcBit >>> 3]!;
|
|
57
|
+
const bit = (srcByte >>> (7 - (srcBit & 7))) & 1;
|
|
58
|
+
if (bit) out[j >>> 3]! |= 1 << (7 - (j & 7));
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Implements {@link TierAddressing} over an injected {@link IRingHash}. */
|
|
64
|
+
export class HashTierAddressing implements TierAddressing {
|
|
65
|
+
public readonly F: number;
|
|
66
|
+
private readonly log2F: number;
|
|
67
|
+
|
|
68
|
+
constructor(private readonly hash: IRingHash, F: number = DEFAULT_FANOUT) {
|
|
69
|
+
if (!Number.isInteger(F) || F < 2) {
|
|
70
|
+
throw new RangeError(`fan-out F must be an integer ≥ 2, got ${F}`);
|
|
71
|
+
}
|
|
72
|
+
const log2F = Math.log2(F);
|
|
73
|
+
if (!Number.isInteger(log2F)) {
|
|
74
|
+
throw new RangeError(`fan-out F must be a power of two, got ${F}`);
|
|
75
|
+
}
|
|
76
|
+
this.F = F;
|
|
77
|
+
this.log2F = log2F;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
coord0(topicId: Uint8Array): RingCoord {
|
|
81
|
+
// H(0x00 ‖ topicId)
|
|
82
|
+
const input = new Uint8Array(1 + topicId.length);
|
|
83
|
+
input[0] = 0x00;
|
|
84
|
+
input.set(topicId, 1);
|
|
85
|
+
return this.hash.H(input);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
coordD(d: number, peerId: Uint8Array, topicId: Uint8Array): RingCoord {
|
|
89
|
+
if (!Number.isInteger(d) || d < 1) {
|
|
90
|
+
throw new RangeError(`coordD requires an integer tier d ≥ 1, got ${d}`);
|
|
91
|
+
}
|
|
92
|
+
if (d > 255) {
|
|
93
|
+
throw new RangeError(`tier d must fit in one byte (≤ 255), got ${d}`);
|
|
94
|
+
}
|
|
95
|
+
// H(d ‖ prefix(peerId, d·log₂F) ‖ topicId)
|
|
96
|
+
const prefix = prefixBits(peerId, d * this.log2F);
|
|
97
|
+
const input = new Uint8Array(1 + prefix.length + topicId.length);
|
|
98
|
+
input[0] = d;
|
|
99
|
+
input.set(prefix, 1);
|
|
100
|
+
input.set(topicId, 1 + prefix.length);
|
|
101
|
+
return this.hash.H(input);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
coord(d: number, peerId: Uint8Array, topicId: Uint8Array): RingCoord {
|
|
105
|
+
return d === 0 ? this.coord0(topicId) : this.coordD(d, peerId, topicId);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Convenience factory mirroring the db-p2p adapter construction style. */
|
|
110
|
+
export function createTierAddressing(hash: IRingHash, F: number = DEFAULT_FANOUT): TierAddressing {
|
|
111
|
+
return new HashTierAddressing(hash, F);
|
|
112
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — bootstrap-evidence envelope (anti-DoS, crypto-free).
|
|
3
|
+
*
|
|
4
|
+
* The on-the-wire structure a participant attaches to a cold-start `bootstrap: true` register, and the
|
|
5
|
+
* byte-level canonicalization both sides agree on. This module owns only the **format** — the versioned
|
|
6
|
+
* envelope, the canonical anti-replay bound image, and the proof-of-work puzzle's preimage/difficulty.
|
|
7
|
+
* It embeds **no cryptography**: the actual PoW hashing, reputation-signature checks, and parent-topic
|
|
8
|
+
* verification live in db-p2p (which binds the node's `RingHash` and peer-key crypto). The sibling
|
|
9
|
+
* discipline of `wire/payloads.ts` / `sig/payloads.ts` — explicitly-ordered arrays, deterministic UTF-8
|
|
10
|
+
* JSON, base64url without padding — so a participant who *mints* evidence and a cohort that *verifies*
|
|
11
|
+
* it never re-canonicalize bytes independently.
|
|
12
|
+
*
|
|
13
|
+
* The envelope rides in the dedicated, signature-covered {@link RegisterV1.bootstrapEvidence} field
|
|
14
|
+
* (not `appPayload`). A verifier reads only the kind its tier accepts; an absent kind, a malformed
|
|
15
|
+
* envelope, or a wrong/future version all surface as "this kind not offered" (the parse is **total** —
|
|
16
|
+
* like `verifyPeerSig`, any decode error yields `undefined`, never a throw), so a verifier fails its
|
|
17
|
+
* check (→ `unwilling_cohort`) rather than crashing on attacker-supplied input.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { b64urlToBytes, bytesToB64url } from "../wire/codec.js";
|
|
21
|
+
import type { RegisterV1 } from "../wire/types.js";
|
|
22
|
+
|
|
23
|
+
const utf8 = new TextEncoder();
|
|
24
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
25
|
+
|
|
26
|
+
/** Proof-of-work evidence (T2/T3 path). `nonce` is base64url, bound via {@link powPreimage}. */
|
|
27
|
+
export interface PowEvidenceV1 {
|
|
28
|
+
/** Base64url nonce; `hash(powPreimage(reg, nonce))` must satisfy {@link meetsDifficulty}. */
|
|
29
|
+
nonce: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Signed parent-topic reference (all tiers). Both base64url; `sig` is over {@link parentRefSigningImage}. */
|
|
33
|
+
export interface ParentRefEvidenceV1 {
|
|
34
|
+
/** The committed parent topic id, base64url. */
|
|
35
|
+
parentTopicId: string;
|
|
36
|
+
/** Participant peer-key signature over {@link parentRefSigningImage} (the bound tuple extended with `parentTopicId`), base64url. */
|
|
37
|
+
sig: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Reputation endorsement (T2/T3 path). `referee` is the endorsing peer-id-string bytes (base64url) and
|
|
42
|
+
* `sig` is that referee's peer-key signature over the bound image. The referee MAY equal the
|
|
43
|
+
* participant (a reputable participant self-vouches).
|
|
44
|
+
*/
|
|
45
|
+
export interface ReputationEvidenceV1 {
|
|
46
|
+
/** Endorsing peer-id-string bytes, base64url. */
|
|
47
|
+
referee: string;
|
|
48
|
+
/** Referee's peer-key signature over the bound image, base64url. */
|
|
49
|
+
sig: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** V1 bootstrap-evidence envelope, base64url-encoded into {@link RegisterV1.bootstrapEvidence}. */
|
|
53
|
+
export interface BootstrapEvidenceEnvelopeV1 {
|
|
54
|
+
v: 1;
|
|
55
|
+
/** Proof-of-work (T2/T3 path). Absent → no PoW offered. */
|
|
56
|
+
pow?: PowEvidenceV1;
|
|
57
|
+
/** Signed parent-topic reference (all tiers). Absent → none offered. */
|
|
58
|
+
parentRef?: ParentRefEvidenceV1;
|
|
59
|
+
/** Reputation endorsement (T2/T3 path). Absent → none offered. */
|
|
60
|
+
reputation?: ReputationEvidenceV1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The four register fields every bootstrap-evidence kind is bound to (the anti-replay tuple). The full
|
|
65
|
+
* {@link RegisterV1} satisfies this, so a verifier passes its decoded register directly; the
|
|
66
|
+
* participant-side builder passes just these fields (it has no full register yet).
|
|
67
|
+
*/
|
|
68
|
+
export type BootstrapBoundFields = Pick<RegisterV1, "topicId" | "tier" | "participantCoord" | "timestamp">;
|
|
69
|
+
|
|
70
|
+
/** Default PoW difficulty: leading zero bits required. ~2^bits hashes to mint, one hash to verify. */
|
|
71
|
+
export const DEFAULT_POW_DIFFICULTY_BITS = 20;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The canonical bytes every bootstrap-evidence kind is bound to: `(topicId, tier, participantCoord,
|
|
75
|
+
* timestamp)`. `topicId`/`participantCoord` are bound as their base64url wire strings verbatim (matching
|
|
76
|
+
* `sig/payloads.ts`) so signer and verifier never re-canonicalize bytes independently. Binding all four
|
|
77
|
+
* means evidence minted for one (topic, tier, peer, time) cannot be replayed for another; binding
|
|
78
|
+
* `timestamp` additionally bounds a captured proof's reuse window, since the register replay guard drops
|
|
79
|
+
* a `timestamp` older than its acceptance window.
|
|
80
|
+
*/
|
|
81
|
+
export function bootstrapBoundImage(reg: BootstrapBoundFields): Uint8Array {
|
|
82
|
+
return utf8.encode(JSON.stringify([
|
|
83
|
+
"BootstrapEvidenceV1",
|
|
84
|
+
reg.topicId,
|
|
85
|
+
reg.tier,
|
|
86
|
+
reg.participantCoord,
|
|
87
|
+
reg.timestamp,
|
|
88
|
+
]));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The canonical bytes a **signed parent-topic reference** binds: the {@link bootstrapBoundImage} tuple
|
|
93
|
+
* `(topicId, tier, participantCoord, timestamp)` **extended with the referenced `parentTopicId`**, under a
|
|
94
|
+
* distinct discriminator tag (`"BootstrapParentRefV1"`). Extending the image with `parentTopicId` means a
|
|
95
|
+
* reference minted for one `(topic, tier, peer, time, parent)` cannot be lifted onto another register —
|
|
96
|
+
* including onto a register naming a *different* parent; the distinct tag keeps it from colliding with a
|
|
97
|
+
* {@link bootstrapBoundImage} reputation/PoW signature (domain separation). `topicId`/`participantCoord`/
|
|
98
|
+
* `parentTopicId` are bound as their base64url wire strings verbatim, like {@link bootstrapBoundImage}, so
|
|
99
|
+
* the participant who *signs* the reference and the cohort that *verifies* it never re-canonicalize bytes.
|
|
100
|
+
*/
|
|
101
|
+
export function parentRefSigningImage(reg: BootstrapBoundFields, parentTopicId: string): Uint8Array {
|
|
102
|
+
return utf8.encode(JSON.stringify([
|
|
103
|
+
"BootstrapParentRefV1",
|
|
104
|
+
reg.topicId,
|
|
105
|
+
reg.tier,
|
|
106
|
+
reg.participantCoord,
|
|
107
|
+
reg.timestamp,
|
|
108
|
+
parentTopicId,
|
|
109
|
+
]));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The proof-of-work hash preimage: {@link bootstrapBoundImage} concatenated with `nonce`. db-p2p hashes
|
|
114
|
+
* this (via the node's `RingHash`) and checks the digest against {@link meetsDifficulty}. Bound to the
|
|
115
|
+
* register tuple ⇒ no cross-topic / cross-peer replay; cheap to verify (one hash + bit check), tunably
|
|
116
|
+
* costly to produce.
|
|
117
|
+
*/
|
|
118
|
+
export function powPreimage(reg: BootstrapBoundFields, nonce: Uint8Array): Uint8Array {
|
|
119
|
+
const image = bootstrapBoundImage(reg);
|
|
120
|
+
const out = new Uint8Array(image.length + nonce.length);
|
|
121
|
+
out.set(image, 0);
|
|
122
|
+
out.set(nonce, image.length);
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* True iff the first `bits` most-significant bits of `hash` are zero (the PoW difficulty target). Bits
|
|
128
|
+
* are read MSB-first per byte (mirroring `addressing.ts`'s `prefixBits`). `bits = 0` is trivially met
|
|
129
|
+
* (lets a config disable PoW cost for tests); a non-finite `bits` is a guard-failure (never met); a
|
|
130
|
+
* negative `bits` clamps to 0; `bits` larger than `hash.length * 8` requires every bit of `hash` to be
|
|
131
|
+
* zero and so is effectively unsatisfiable for a random hash.
|
|
132
|
+
*/
|
|
133
|
+
export function meetsDifficulty(hash: Uint8Array, bits: number): boolean {
|
|
134
|
+
if (!Number.isFinite(bits)) {
|
|
135
|
+
return false; // NaN / ±Infinity → defensive guard, never satisfiable
|
|
136
|
+
}
|
|
137
|
+
let remaining = Math.floor(bits);
|
|
138
|
+
if (remaining <= 0) {
|
|
139
|
+
return true; // 0 (or clamped-negative) leading zero bits is trivially met
|
|
140
|
+
}
|
|
141
|
+
for (let i = 0; i < hash.length && remaining > 0; i++) {
|
|
142
|
+
const byte = hash[i]!;
|
|
143
|
+
if (remaining >= 8) {
|
|
144
|
+
if (byte !== 0) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
remaining -= 8;
|
|
148
|
+
} else {
|
|
149
|
+
// Check only the top `remaining` MSBs of this partial final byte (MSB-first).
|
|
150
|
+
const mask = (0xff << (8 - remaining)) & 0xff;
|
|
151
|
+
if ((byte & mask) !== 0) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
remaining = 0;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Exhausted the hash before satisfying `bits` (oversize target) → unsatisfiable.
|
|
158
|
+
return remaining === 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Serialize an envelope to the base64url JSON string carried in {@link RegisterV1.bootstrapEvidence}. */
|
|
162
|
+
export function serializeBootstrapEvidenceEnvelope(env: BootstrapEvidenceEnvelopeV1): string {
|
|
163
|
+
// Rebuild in a fixed field order so serialization is a pure function of the logical content
|
|
164
|
+
// (serialize∘parse is stable regardless of how a caller ordered the source object's keys).
|
|
165
|
+
const out: BootstrapEvidenceEnvelopeV1 = { v: 1 };
|
|
166
|
+
if (env.pow !== undefined) {
|
|
167
|
+
out.pow = { nonce: env.pow.nonce };
|
|
168
|
+
}
|
|
169
|
+
if (env.parentRef !== undefined) {
|
|
170
|
+
out.parentRef = { parentTopicId: env.parentRef.parentTopicId, sig: env.parentRef.sig };
|
|
171
|
+
}
|
|
172
|
+
if (env.reputation !== undefined) {
|
|
173
|
+
out.reputation = { referee: env.reputation.referee, sig: env.reputation.sig };
|
|
174
|
+
}
|
|
175
|
+
return bytesToB64url(utf8.encode(JSON.stringify(out)));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Decode {@link RegisterV1.bootstrapEvidence} (base64url → JSON), structurally validate it, and return
|
|
180
|
+
* the {@link BootstrapEvidenceEnvelopeV1}. **Total**: returns `undefined` on an absent/empty field, a
|
|
181
|
+
* non-base64url or non-JSON body, a wrong/future `v`, or a structurally-invalid kind — never throws. A
|
|
182
|
+
* verifier treats `undefined` as "this kind not offered" and fails its check (fails closed).
|
|
183
|
+
*/
|
|
184
|
+
export function parseBootstrapEvidenceEnvelope(reg: Pick<RegisterV1, "bootstrapEvidence">): BootstrapEvidenceEnvelopeV1 | undefined {
|
|
185
|
+
const raw = reg.bootstrapEvidence;
|
|
186
|
+
if (raw === undefined || raw === "") {
|
|
187
|
+
return undefined; // not offered
|
|
188
|
+
}
|
|
189
|
+
let parsed: unknown;
|
|
190
|
+
try {
|
|
191
|
+
parsed = JSON.parse(utf8Decoder.decode(b64urlToBytes(raw)));
|
|
192
|
+
} catch {
|
|
193
|
+
return undefined; // not base64url / not UTF-8 / not JSON → fail closed
|
|
194
|
+
}
|
|
195
|
+
return narrowEnvelope(parsed);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Structurally narrow an already-parsed value to a v1 envelope, or `undefined`. */
|
|
199
|
+
function narrowEnvelope(value: unknown): BootstrapEvidenceEnvelopeV1 | undefined {
|
|
200
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
const obj = value as Record<string, unknown>;
|
|
204
|
+
if (obj["v"] !== 1) {
|
|
205
|
+
return undefined; // wrong / future version → fail closed under the v1 reader
|
|
206
|
+
}
|
|
207
|
+
const out: BootstrapEvidenceEnvelopeV1 = { v: 1 };
|
|
208
|
+
|
|
209
|
+
if (obj["pow"] !== undefined) {
|
|
210
|
+
const nonce = b64urlSubfield(obj["pow"], "nonce");
|
|
211
|
+
if (nonce === undefined) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
out.pow = { nonce };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (obj["parentRef"] !== undefined) {
|
|
218
|
+
const parentTopicId = b64urlSubfield(obj["parentRef"], "parentTopicId");
|
|
219
|
+
const sig = b64urlSubfield(obj["parentRef"], "sig");
|
|
220
|
+
if (parentTopicId === undefined || sig === undefined) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
out.parentRef = { parentTopicId, sig };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (obj["reputation"] !== undefined) {
|
|
227
|
+
const referee = b64urlSubfield(obj["reputation"], "referee");
|
|
228
|
+
const sig = b64urlSubfield(obj["reputation"], "sig");
|
|
229
|
+
if (referee === undefined || sig === undefined) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
out.reputation = { referee, sig };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** A required base64url sub-field: a non-empty string that decodes cleanly, else `undefined`. */
|
|
239
|
+
function b64urlSubfield(container: unknown, key: string): string | undefined {
|
|
240
|
+
if (typeof container !== "object" || container === null || Array.isArray(container)) {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
const value = (container as Record<string, unknown>)[key];
|
|
244
|
+
if (typeof value !== "string" || value === "") {
|
|
245
|
+
return undefined; // missing, non-string, or empty → treat as absent
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
b64urlToBytes(value); // structural: must decode as base64url
|
|
249
|
+
} catch {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — bootstrap evidence policy (anti-DoS).
|
|
3
|
+
*
|
|
4
|
+
* Transcribed from `docs/cohort-topic.md` §Anti-DoS bullet 4. A cold root that accepts a
|
|
5
|
+
* `bootstrap: true` registration must demand evidence, so a cold topic cannot be instantiated for
|
|
6
|
+
* free by an attacker. The accepted evidence is **tier-dependent**:
|
|
7
|
+
*
|
|
8
|
+
* - **T0 / T1** — generally no proof-of-work, because these tiers correspond to committed work; a
|
|
9
|
+
* *signed reference to a committed parent topic that does exist* is sufficient.
|
|
10
|
+
* - **T2 / T3** — a **proof-of-work** OR a **reputation-score signature** (a signature from a peer
|
|
11
|
+
* with sufficient reputation, see `architecture.md` §Reputation) OR a **signed parent-topic
|
|
12
|
+
* reference**.
|
|
13
|
+
*
|
|
14
|
+
* This module is the *policy* — which evidence kinds satisfy which tier. The cryptographic checks
|
|
15
|
+
* themselves (verifying a PoW, a reputation signature, a parent-topic reference) are injected, keeping
|
|
16
|
+
* db-core free of any specific PoW/reputation scheme (the same injection discipline as the threshold
|
|
17
|
+
* crypto port). The evidence travels in the registration's dedicated, signature-covered
|
|
18
|
+
* `bootstrapEvidence` field — a versioned `BootstrapEvidenceEnvelopeV1`
|
|
19
|
+
* (`./bootstrap-evidence-envelope.js`), parsed crypto-free here and checked by the injected verifiers;
|
|
20
|
+
* NOT the opaque `appPayload` slot (which the cohort copies verbatim into the registration's appState).
|
|
21
|
+
* A registration that is not a bootstrap needs no evidence and is admitted.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { RegisterV1 } from "../wire/types.js";
|
|
25
|
+
import { createLogger } from "../../logger.js";
|
|
26
|
+
|
|
27
|
+
const log = createLogger("cohort-topic:antidos");
|
|
28
|
+
|
|
29
|
+
/** Tiers at/below this need no proof-of-work — a signed parent reference suffices (T0/T1). */
|
|
30
|
+
export const DEFAULT_MAX_NO_POW_TIER = 1;
|
|
31
|
+
|
|
32
|
+
export interface BootstrapEvidenceDeps {
|
|
33
|
+
/** Verify a proof-of-work attached to `reg` (T2/T3 path). Absent → PoW is never satisfiable. */
|
|
34
|
+
verifyPoW?: (reg: RegisterV1) => boolean;
|
|
35
|
+
/** Verify a sufficient-reputation signature on `reg` (T2/T3 path). Absent → never satisfiable. */
|
|
36
|
+
verifyReputation?: (reg: RegisterV1) => boolean;
|
|
37
|
+
/** Verify a signed reference to an existing committed parent topic (all tiers). Absent → never satisfiable. */
|
|
38
|
+
verifyParentReference?: (reg: RegisterV1) => boolean;
|
|
39
|
+
config?: BootstrapEvidenceConfig;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface BootstrapEvidenceConfig {
|
|
43
|
+
/** Highest tier exempt from proof-of-work (T0/T1 → 1). Default {@link DEFAULT_MAX_NO_POW_TIER}. */
|
|
44
|
+
maxNoPowTier?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Tier-dependent bootstrap-evidence gate for cold-root registrations. */
|
|
48
|
+
export interface BootstrapEvidence {
|
|
49
|
+
/**
|
|
50
|
+
* Whether `reg` carries acceptable bootstrap evidence for `tier`. A non-bootstrap registration is
|
|
51
|
+
* always acceptable (nothing to prove). For a bootstrap: T0/T1 require a signed parent reference;
|
|
52
|
+
* T2/T3 accept proof-of-work OR a reputation signature OR a signed parent reference.
|
|
53
|
+
*/
|
|
54
|
+
verify(reg: RegisterV1, tier: number): boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class TieredBootstrapEvidence implements BootstrapEvidence {
|
|
58
|
+
private readonly maxNoPowTier: number;
|
|
59
|
+
private readonly verifyPoW: (reg: RegisterV1) => boolean;
|
|
60
|
+
private readonly verifyReputation: (reg: RegisterV1) => boolean;
|
|
61
|
+
private readonly verifyParentReference: (reg: RegisterV1) => boolean;
|
|
62
|
+
|
|
63
|
+
constructor(deps: BootstrapEvidenceDeps = {}) {
|
|
64
|
+
this.maxNoPowTier = deps.config?.maxNoPowTier ?? DEFAULT_MAX_NO_POW_TIER;
|
|
65
|
+
this.verifyPoW = deps.verifyPoW ?? ((): boolean => false);
|
|
66
|
+
this.verifyReputation = deps.verifyReputation ?? ((): boolean => false);
|
|
67
|
+
this.verifyParentReference = deps.verifyParentReference ?? ((): boolean => false);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
verify(reg: RegisterV1, tier: number): boolean {
|
|
71
|
+
if (reg.bootstrap !== true) {
|
|
72
|
+
return true; // only a cold-root bootstrap must carry evidence
|
|
73
|
+
}
|
|
74
|
+
if (tier <= this.maxNoPowTier) {
|
|
75
|
+
// T0/T1: a signed reference to a committed parent topic is sufficient (no PoW expected).
|
|
76
|
+
const ok = this.verifyParentReference(reg);
|
|
77
|
+
if (!ok) {
|
|
78
|
+
log("bootstrap-evidence reject: tier=%d needs a signed parent reference", tier);
|
|
79
|
+
}
|
|
80
|
+
return ok;
|
|
81
|
+
}
|
|
82
|
+
// T2/T3: any one of PoW, a reputation signature, or a signed parent reference.
|
|
83
|
+
const ok = this.verifyPoW(reg) || this.verifyReputation(reg) || this.verifyParentReference(reg);
|
|
84
|
+
if (!ok) {
|
|
85
|
+
log("bootstrap-evidence reject: tier=%d needs PoW / reputation / parent reference", tier);
|
|
86
|
+
}
|
|
87
|
+
return ok;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Build a {@link BootstrapEvidence} policy over the injected (db-p2p-supplied) evidence verifiers. */
|
|
92
|
+
export function createBootstrapEvidence(deps: BootstrapEvidenceDeps = {}): BootstrapEvidence {
|
|
93
|
+
return new TieredBootstrapEvidence(deps);
|
|
94
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — per-peer registration rate limiter (anti-DoS).
|
|
3
|
+
*
|
|
4
|
+
* Transcribed from `docs/cohort-topic.md` §Anti-DoS bullet 1. A cohort member tracks inbound
|
|
5
|
+
* `RegisterV1` rate per `(sourcePeerId, topicId)` and refuses a source that exceeds
|
|
6
|
+
* `register_rate_per_peer` (default 4 / min). An over-rate registration draws `UnwillingCohort` with
|
|
7
|
+
* an **exponential** `retryAfter`, so a peer hammering one topic at one cohort backs off geometrically
|
|
8
|
+
* — the same capped-doubling curve the willingness back-off uses ({@link backoffRetryMs}).
|
|
9
|
+
*
|
|
10
|
+
* The limiter is a sliding-window counter: it keeps the accept timestamps inside the trailing
|
|
11
|
+
* `windowMs` for each key, admits while the window holds `< ratePerWindow`, and on rejection advances
|
|
12
|
+
* a per-key strike counter that indexes the back-off curve. A run of strikes decays once the source
|
|
13
|
+
* has been quiet for a full window (its window empties and the strike counter resets), so a
|
|
14
|
+
* well-behaved peer is never permanently penalized. Each key's accept history is trimmed to the live
|
|
15
|
+
* window whenever that key is checked, so a key's footprint stays `O(ratePerWindow)`.
|
|
16
|
+
*
|
|
17
|
+
* The `states` map is bounded two complementary ways so a long-running host cannot leak memory and a
|
|
18
|
+
* flood of attacker-chosen keys cannot exhaust it:
|
|
19
|
+
*
|
|
20
|
+
* - **Hard LRU cap (`maxKeys`)** — enforced inline in {@link SlidingWindowRateLimiter.check}. Every
|
|
21
|
+
* check moves its key to the most-recently-checked end (the {@link import("../../utility/lru-map.js").LruMap}
|
|
22
|
+
* delete-then-set trick over `Map` insertion order); when a *new* key would exceed `maxKeys` the
|
|
23
|
+
* least-recently-checked keys are evicted until within cap. Recency is refreshed on **every** check,
|
|
24
|
+
* rejects included, so a source mid-attack (accumulated `strikes` driving back-off) stays at the hot
|
|
25
|
+
* end and is never the eviction victim — only genuinely idle keys age to the cold end.
|
|
26
|
+
* - **Idle-TTL sweep (`idleTtlMs`)** — {@link SlidingWindowRateLimiter.sweep} drops keys not checked
|
|
27
|
+
* within `idleTtlMs`. Driver-called on the host's gossip cadence, it reclaims steady-state footprint
|
|
28
|
+
* proportional to *active* keys.
|
|
29
|
+
*
|
|
30
|
+
* Eviction is penalty-free when a key is dropped only after a full window of quiet: the window logic
|
|
31
|
+
* already forgives a source quiet for a full window (its accepts age out and `strikes` resets), so
|
|
32
|
+
* dropping such a key and re-allocating a fresh `{ accepts:[now], strikes:0 }` on its return is
|
|
33
|
+
* observationally identical to keeping it — just an earlier reclaim. This holds for the LRU cap (a
|
|
34
|
+
* mid-attack key is refreshed to the hot end on every check, so only genuinely idle keys are evicted)
|
|
35
|
+
* and for the default `idleTtlMs` (`== windowMs`). A configured `idleTtlMs < windowMs` reclaims more
|
|
36
|
+
* aggressively: `sweep` may then drop a key whose accepts have *not* fully aged out, forgiving its
|
|
37
|
+
* accumulated `strikes` sooner than the window alone would — an accepted footprint/strike-accounting
|
|
38
|
+
* tradeoff, not an identity. The substrate does not defend against unbounded Sybil key creation (that is FRET's /
|
|
39
|
+
* the reputation subsystem's concern; see §Anti-DoS closing note).
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { recordKey } from "../registration/bytes.js";
|
|
43
|
+
import { backoffRetryMs, type BackoffConfig, DEFAULT_BACKOFF_CONFIG } from "../willingness.js";
|
|
44
|
+
import { createLogger } from "../../logger.js";
|
|
45
|
+
|
|
46
|
+
const log = createLogger("cohort-topic:antidos");
|
|
47
|
+
|
|
48
|
+
/** Default per-peer-per-topic acceptance ceiling per window — `register_rate_per_peer` (4 / min). */
|
|
49
|
+
export const DEFAULT_REGISTER_RATE_PER_PEER = 4;
|
|
50
|
+
/** Default sliding window the ceiling applies over (ms). */
|
|
51
|
+
export const DEFAULT_RATE_WINDOW_MS = 60_000;
|
|
52
|
+
/** Default hard cap on tracked `(peer, topic)` keys; the least-recently-checked are evicted beyond this. */
|
|
53
|
+
export const DEFAULT_RATE_LIMITER_MAX_KEYS = 100_000;
|
|
54
|
+
/** Default idle threshold for {@link RegisterRateLimiter.sweep} — one quiet window makes a key evictable. */
|
|
55
|
+
export const DEFAULT_RATE_LIMITER_IDLE_TTL_MS = DEFAULT_RATE_WINDOW_MS;
|
|
56
|
+
|
|
57
|
+
export interface RegisterRateLimiterConfig {
|
|
58
|
+
/** Accepts permitted per window per `(peer, topic)`. Default {@link DEFAULT_REGISTER_RATE_PER_PEER}. */
|
|
59
|
+
ratePerWindow?: number;
|
|
60
|
+
/** Sliding-window length (ms). Default {@link DEFAULT_RATE_WINDOW_MS}. */
|
|
61
|
+
windowMs?: number;
|
|
62
|
+
/** Exponential `retryAfter` curve for over-rate sources. Default {@link DEFAULT_BACKOFF_CONFIG}. */
|
|
63
|
+
backoff?: BackoffConfig;
|
|
64
|
+
/** Hard cap on tracked `(peer, topic)` keys; least-recently-checked evicted beyond this. Default {@link DEFAULT_RATE_LIMITER_MAX_KEYS}. */
|
|
65
|
+
maxKeys?: number;
|
|
66
|
+
/**
|
|
67
|
+
* A key not checked within this many ms is evictable by {@link RegisterRateLimiter.sweep}. Default
|
|
68
|
+
* {@link DEFAULT_RATE_LIMITER_IDLE_TTL_MS} (`== windowMs`). Keep `>= windowMs` to preserve the
|
|
69
|
+
* penalty-free invariant; a smaller value reclaims sooner but may forgive an idle source's strikes
|
|
70
|
+
* before its window would (see the class doc comment).
|
|
71
|
+
*/
|
|
72
|
+
idleTtlMs?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Outcome of a rate check: admitted, or refused with the back-off the caller puts in `UnwillingCohort`. */
|
|
76
|
+
export type RateCheckResult = { ok: true } | { ok: false; retryAfterMs: number };
|
|
77
|
+
|
|
78
|
+
/** Per-peer-per-topic inbound `RegisterV1` rate limiter. */
|
|
79
|
+
export interface RegisterRateLimiter {
|
|
80
|
+
/**
|
|
81
|
+
* Classify a register from `peerId` for `topicId` at `now`. Records the accept on `{ ok: true }`;
|
|
82
|
+
* an over-rate source gets `{ ok: false, retryAfterMs }` with exponential back-off and the strike
|
|
83
|
+
* is *not* recorded as an accept (so back-off cannot itself fill the window). Every call — accept
|
|
84
|
+
* **or** reject — refreshes the key's recency, so an actively-hammering source is never evicted by
|
|
85
|
+
* the LRU cap (which would reset its back-off escalation).
|
|
86
|
+
*/
|
|
87
|
+
check(peerId: Uint8Array, topicId: Uint8Array, now: number): RateCheckResult;
|
|
88
|
+
/**
|
|
89
|
+
* Evict keys idle (not checked) for `>= idleTtlMs`. Returns the number evicted. Driver-called on
|
|
90
|
+
* the host's gossip cadence to reclaim steady-state footprint; the hard `maxKeys` LRU cap bounds
|
|
91
|
+
* worst-case footprint even without it.
|
|
92
|
+
*/
|
|
93
|
+
sweep(now: number): number;
|
|
94
|
+
/** Tracked `(peer, topic)` key count (test/diagnostic introspection). */
|
|
95
|
+
readonly size: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Sliding-window state for one `(peer, topic)` key. */
|
|
99
|
+
interface WindowState {
|
|
100
|
+
/** Accept timestamps inside the trailing window, ascending. */
|
|
101
|
+
accepts: number[];
|
|
102
|
+
/** Consecutive over-rate strikes since the last accept — indexes the back-off curve. */
|
|
103
|
+
strikes: number;
|
|
104
|
+
/** `now` of the most recent check (accept OR reject) — LRU recency + idle-TTL key for {@link RegisterRateLimiter.sweep}. */
|
|
105
|
+
lastSeen: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class SlidingWindowRateLimiter implements RegisterRateLimiter {
|
|
109
|
+
private readonly states = new Map<string, WindowState>();
|
|
110
|
+
private readonly ratePerWindow: number;
|
|
111
|
+
private readonly windowMs: number;
|
|
112
|
+
private readonly backoff: BackoffConfig;
|
|
113
|
+
private readonly maxKeys: number;
|
|
114
|
+
private readonly idleTtlMs: number;
|
|
115
|
+
|
|
116
|
+
constructor(config: RegisterRateLimiterConfig = {}) {
|
|
117
|
+
this.ratePerWindow = config.ratePerWindow ?? DEFAULT_REGISTER_RATE_PER_PEER;
|
|
118
|
+
if (!Number.isInteger(this.ratePerWindow) || this.ratePerWindow <= 0) {
|
|
119
|
+
throw new RangeError(`ratePerWindow must be a positive integer, got ${this.ratePerWindow}`);
|
|
120
|
+
}
|
|
121
|
+
this.windowMs = config.windowMs ?? DEFAULT_RATE_WINDOW_MS;
|
|
122
|
+
if (!(this.windowMs > 0)) {
|
|
123
|
+
throw new RangeError(`windowMs must be > 0, got ${this.windowMs}`);
|
|
124
|
+
}
|
|
125
|
+
this.backoff = config.backoff ?? DEFAULT_BACKOFF_CONFIG;
|
|
126
|
+
this.maxKeys = config.maxKeys ?? DEFAULT_RATE_LIMITER_MAX_KEYS;
|
|
127
|
+
if (!Number.isInteger(this.maxKeys) || this.maxKeys <= 0) {
|
|
128
|
+
throw new RangeError(`maxKeys must be a positive integer, got ${this.maxKeys}`);
|
|
129
|
+
}
|
|
130
|
+
this.idleTtlMs = config.idleTtlMs ?? DEFAULT_RATE_LIMITER_IDLE_TTL_MS;
|
|
131
|
+
if (!(this.idleTtlMs > 0)) {
|
|
132
|
+
throw new RangeError(`idleTtlMs must be > 0, got ${this.idleTtlMs}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
check(peerId: Uint8Array, topicId: Uint8Array, now: number): RateCheckResult {
|
|
137
|
+
const key = recordKey(topicId, peerId);
|
|
138
|
+
const state = this.states.get(key);
|
|
139
|
+
const cutoff = now - this.windowMs;
|
|
140
|
+
|
|
141
|
+
if (state === undefined) {
|
|
142
|
+
// New key: enforce the hard cap by evicting the least-recently-checked keys (oldest by
|
|
143
|
+
// `Map` insertion order) until this insertion stays within `maxKeys`. The evicted keys
|
|
144
|
+
// are by construction the coldest — an actively-checked key is refreshed to the hot end
|
|
145
|
+
// below and so is never the victim.
|
|
146
|
+
while (this.states.size >= this.maxKeys) {
|
|
147
|
+
const oldest = this.states.keys().next().value;
|
|
148
|
+
if (oldest === undefined) break;
|
|
149
|
+
this.states.delete(oldest);
|
|
150
|
+
}
|
|
151
|
+
this.states.set(key, { accepts: [now], strikes: 0, lastSeen: now });
|
|
152
|
+
return { ok: true };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Refresh LRU recency on every check (accept OR reject): delete-then-set moves this key to the
|
|
156
|
+
// most-recently-checked end of the `Map`, and `lastSeen = now` is the idle-TTL key sweep() reads.
|
|
157
|
+
// A source mid-attack stays at the hot end here even on a reject, so the cap never evicts (and
|
|
158
|
+
// thus never resets the back-off of) an active attacker.
|
|
159
|
+
this.states.delete(key);
|
|
160
|
+
this.states.set(key, state);
|
|
161
|
+
state.lastSeen = now;
|
|
162
|
+
|
|
163
|
+
// Drop accepts that have aged out of the trailing window.
|
|
164
|
+
let live = 0;
|
|
165
|
+
for (const t of state.accepts) {
|
|
166
|
+
if (t > cutoff) {
|
|
167
|
+
state.accepts[live++] = t;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
state.accepts.length = live;
|
|
171
|
+
|
|
172
|
+
if (live === 0) {
|
|
173
|
+
// The source has been quiet for a full window — forgive accumulated strikes.
|
|
174
|
+
state.strikes = 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (live < this.ratePerWindow) {
|
|
178
|
+
state.accepts.push(now);
|
|
179
|
+
state.strikes = 0;
|
|
180
|
+
return { ok: true };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Over rate: exponential back-off indexed by the strike count, then advance the strike.
|
|
184
|
+
const retryAfterMs = backoffRetryMs(state.strikes, this.backoff);
|
|
185
|
+
state.strikes++;
|
|
186
|
+
log("rate-limit reject window=%d/%d strikes=%d retryAfter=%d", live, this.ratePerWindow, state.strikes, retryAfterMs);
|
|
187
|
+
return { ok: false, retryAfterMs };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
sweep(now: number): number {
|
|
191
|
+
let evicted = 0;
|
|
192
|
+
// Deleting the current key during `Map` iteration is well-defined and does not skip entries.
|
|
193
|
+
for (const [key, state] of this.states) {
|
|
194
|
+
if (now - state.lastSeen >= this.idleTtlMs) {
|
|
195
|
+
this.states.delete(key);
|
|
196
|
+
evicted++;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return evicted;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
get size(): number {
|
|
203
|
+
return this.states.size;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Build a {@link RegisterRateLimiter} over the configured ceiling, window, and back-off curve. */
|
|
208
|
+
export function createRegisterRateLimiter(config: RegisterRateLimiterConfig = {}): RegisterRateLimiter {
|
|
209
|
+
return new SlidingWindowRateLimiter(config);
|
|
210
|
+
}
|