fleetmesh-node 0.2.0__py3-none-any.whl
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.
- fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
- fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
- fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
- fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
- fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
- fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
- node/RELEASE +1 -0
- node/__init__.py +35 -0
- node/app/__init__.py +49 -0
- node/app/commands.py +265 -0
- node/app/config.py +131 -0
- node/app/control.py +670 -0
- node/app/core.py +867 -0
- node/app/plugins.py +76 -0
- node/app/router.py +83 -0
- node/app/socket.py +431 -0
- node/app/systemd.py +36 -0
- node/app/views.py +204 -0
- node/channels/__init__.py +378 -0
- node/channels/bus.py +331 -0
- node/channels/journal.py +46 -0
- node/commands/__init__.py +140 -0
- node/commands/adapter.py +210 -0
- node/commands/base.py +278 -0
- node/commands/cluster_cmds.py +183 -0
- node/commands/compute_cmds.py +401 -0
- node/commands/core.py +474 -0
- node/commands/gossip_cmds.py +106 -0
- node/commands/nsite_cmds.py +141 -0
- node/commands/peering.py +402 -0
- node/commands/plugin_cmds.py +135 -0
- node/commands/sidecar_cmds.py +120 -0
- node/commands/sync.py +388 -0
- node/commands/transport_cmds.py +451 -0
- node/commands/video_cmds.py +99 -0
- node/compartment/__init__.py +124 -0
- node/compartment/base.py +868 -0
- node/compartment/command.py +458 -0
- node/compartment/context.py +163 -0
- node/compartment/every.py +108 -0
- node/compartment/handler.py +131 -0
- node/compartment/lifecycle.py +460 -0
- node/compartment/lint.py +388 -0
- node/compartment/need.py +158 -0
- node/compartment/problem.py +521 -0
- node/compartment/refusals.py +120 -0
- node/compartment/registry.py +790 -0
- node/compartment/schema.py +290 -0
- node/compartment/sidecar.py +499 -0
- node/compartment/stores.py +662 -0
- node/compartment/wire.py +225 -0
- node/compartment/worker.py +61 -0
- node/config/__init__.py +43 -0
- node/config/base.py +116 -0
- node/config/change.py +441 -0
- node/config/changestore.py +201 -0
- node/config/compartments.py +1144 -0
- node/config/node.py +318 -0
- node/config/parameter.py +563 -0
- node/config/protocols.py +274 -0
- node/config/records.py +171 -0
- node/examples/__init__.py +1 -0
- node/examples/demo.py +226 -0
- node/examples/follower.py +46 -0
- node/interfaces.py +235 -0
- node/keystore/__init__.py +27 -0
- node/keystore/core.py +103 -0
- node/keystore/enclave.py +688 -0
- node/meshnode.py +111 -0
- node/metrics/__init__.py +396 -0
- node/metrics/adapters.py +102 -0
- node/metrics/face.py +110 -0
- node/planes/__init__.py +21 -0
- node/planes/identity/__init__.py +20 -0
- node/planes/identity/announce/__init__.py +9 -0
- node/planes/identity/announce/compartment.py +447 -0
- node/planes/identity/announce/types.py +26 -0
- node/planes/identity/discovery/__init__.py +9 -0
- node/planes/identity/discovery/compartment.py +522 -0
- node/planes/identity/discovery/types.py +13 -0
- node/planes/identity/linked/__init__.py +1 -0
- node/planes/identity/linked/compartment.py +270 -0
- node/planes/identity/linked/types.py +25 -0
- node/planes/identity/presence/__init__.py +7 -0
- node/planes/identity/presence/compartment.py +409 -0
- node/planes/identity/presence/types.py +14 -0
- node/planes/operations/__init__.py +13 -0
- node/planes/operations/scheduler/__init__.py +9 -0
- node/planes/operations/scheduler/compartment.py +353 -0
- node/planes/operations/scheduler/types.py +21 -0
- node/planes/operations/updates/__init__.py +17 -0
- node/planes/operations/updates/core.py +310 -0
- node/planes/operations/updates/store.py +79 -0
- node/planes/operations/updates/types.py +20 -0
- node/planes/peering/__init__.py +28 -0
- node/planes/peering/budget/__init__.py +43 -0
- node/planes/peering/budget/chain.py +43 -0
- node/planes/peering/budget/core.py +537 -0
- node/planes/peering/budget/evaluation.py +80 -0
- node/planes/peering/budget/meter.py +100 -0
- node/planes/peering/budget/receipts.py +439 -0
- node/planes/peering/budget/shape.py +101 -0
- node/planes/peering/budget/tick.py +110 -0
- node/planes/peering/budget/types.py +67 -0
- node/planes/peering/budget/windows.py +346 -0
- node/planes/peering/capacity/__init__.py +7 -0
- node/planes/peering/capacity/compartment.py +342 -0
- node/planes/peering/capacity/types.py +24 -0
- node/planes/peering/peering/__init__.py +25 -0
- node/planes/peering/peering/core.py +464 -0
- node/planes/peering/peering/envelope.py +142 -0
- node/planes/peering/peering/handshake.py +198 -0
- node/planes/peering/peering/shape.py +80 -0
- node/planes/peering/peering/types.py +39 -0
- node/planes/peering/reporter/__init__.py +42 -0
- node/planes/peering/reporter/constants.py +29 -0
- node/planes/peering/reporter/core.py +299 -0
- node/planes/peering/reporter/finding.py +74 -0
- node/planes/peering/reporter/findings.py +171 -0
- node/planes/peering/reporter/publish.py +285 -0
- node/planes/peering/reporter/reading.py +193 -0
- node/planes/peering/reporter/shape.py +86 -0
- node/planes/peering/reporter/types.py +50 -0
- node/planes/peering/strain/__init__.py +9 -0
- node/planes/peering/strain/compartment.py +204 -0
- node/planes/peering/strain/types.py +21 -0
- node/planes/peering/trust/__init__.py +9 -0
- node/planes/peering/trust/compartment.py +239 -0
- node/planes/peering/trust/types.py +30 -0
- node/planes/services/__init__.py +33 -0
- node/planes/services/compute/__init__.py +160 -0
- node/planes/services/compute/common.py +134 -0
- node/planes/services/compute/compartment.py +681 -0
- node/planes/services/compute/executors/__init__.py +121 -0
- node/planes/services/compute/executors/agent.py +840 -0
- node/planes/services/compute/executors/bacalhau.py +605 -0
- node/planes/services/compute/executors/base.py +127 -0
- node/planes/services/compute/executors/harness.py +439 -0
- node/planes/services/compute/executors/mcp.py +454 -0
- node/planes/services/compute/executors/media.py +898 -0
- node/planes/services/compute/executors/nondet.py +283 -0
- node/planes/services/compute/executors/providers.py +499 -0
- node/planes/services/compute/executors/registry.py +140 -0
- node/planes/services/compute/executors/wasm.py +244 -0
- node/planes/services/compute/executors/zk.py +604 -0
- node/planes/services/compute/jobs.py +227 -0
- node/planes/services/compute/load.py +180 -0
- node/planes/services/compute/market.py +433 -0
- node/planes/services/compute/sandbox.py +282 -0
- node/planes/services/compute/types.py +84 -0
- node/planes/services/compute_wire/__init__.py +58 -0
- node/planes/services/compute_wire/compartment.py +256 -0
- node/planes/services/compute_wire/messages.py +320 -0
- node/planes/services/compute_wire/sealing.py +147 -0
- node/planes/services/compute_wire/session.py +572 -0
- node/planes/services/compute_wire/types.py +49 -0
- node/planes/services/compute_wire/verify.py +331 -0
- node/planes/services/dvm/__init__.py +24 -0
- node/planes/services/dvm/gateway.py +526 -0
- node/planes/services/dvm/protocol.py +35 -0
- node/planes/services/dvm/translate.py +15 -0
- node/planes/services/dvm/types.py +25 -0
- node/planes/services/gateway/__init__.py +40 -0
- node/planes/services/gateway/settle.py +279 -0
- node/planes/services/gateway/translate.py +644 -0
- node/planes/services/gossip/__init__.py +22 -0
- node/planes/services/gossip/core.py +929 -0
- node/planes/services/gossip/envelope.py +154 -0
- node/planes/services/gossip/mesh.py +127 -0
- node/planes/services/gossip/types.py +50 -0
- node/planes/services/nsite/__init__.py +7 -0
- node/planes/services/nsite/client.py +62 -0
- node/planes/services/nsite/compartment.py +752 -0
- node/planes/services/nsite/types.py +44 -0
- node/planes/services/paidfetch/__init__.py +7 -0
- node/planes/services/paidfetch/compartment.py +862 -0
- node/planes/services/paidfetch/types.py +99 -0
- node/planes/services/settle/__init__.py +27 -0
- node/planes/services/settle/compartment.py +384 -0
- node/planes/services/settle/engine.py +456 -0
- node/planes/services/settle/gateways.py +541 -0
- node/planes/services/settle/payment.py +512 -0
- node/planes/services/settle/store.py +74 -0
- node/planes/services/settle/types.py +58 -0
- node/planes/services/sidecars/__init__.py +8 -0
- node/planes/services/sidecars/compartment.py +461 -0
- node/planes/services/sidecars/manifest.py +304 -0
- node/planes/services/sidecars/manifests/10-grasp.json +37 -0
- node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
- node/planes/services/sidecars/manifests/30-magpie.json +28 -0
- node/planes/services/sidecars/types.py +43 -0
- node/planes/services/video/__init__.py +106 -0
- node/planes/services/video/barter.py +407 -0
- node/planes/services/video/cli.py +158 -0
- node/planes/services/video/compartment.py +932 -0
- node/planes/services/video/ingest.py +524 -0
- node/planes/services/video/ledger.py +705 -0
- node/planes/services/video/packager.py +847 -0
- node/planes/services/video/player.py +456 -0
- node/planes/services/video/timed_segments.py +124 -0
- node/planes/services/video/transit.py +478 -0
- node/planes/store/__init__.py +24 -0
- node/planes/store/blob_replication/__init__.py +7 -0
- node/planes/store/blob_replication/compartment.py +496 -0
- node/planes/store/blob_replication/types.py +35 -0
- node/planes/store/blob_wire/__init__.py +48 -0
- node/planes/store/blob_wire/client.py +57 -0
- node/planes/store/blob_wire/core.py +359 -0
- node/planes/store/blob_wire/intake.py +74 -0
- node/planes/store/blob_wire/messages.py +85 -0
- node/planes/store/projected/__init__.py +7 -0
- node/planes/store/projected/compartment.py +258 -0
- node/planes/store/projected/types.py +29 -0
- node/planes/store/retention/__init__.py +7 -0
- node/planes/store/retention/compartment.py +214 -0
- node/planes/store/retention/types.py +12 -0
- node/planes/store/sync/__init__.py +35 -0
- node/planes/store/sync/core.py +311 -0
- node/planes/store/sync/envelope.py +106 -0
- node/planes/store/sync/handover.py +206 -0
- node/planes/store/sync/initiator.py +128 -0
- node/planes/store/sync/responder.py +114 -0
- node/planes/store/sync/rules.py +179 -0
- node/planes/store/sync/shape.py +80 -0
- node/planes/store/sync/types.py +51 -0
- node/planes/transport/__init__.py +23 -0
- node/planes/transport/bridge/__init__.py +13 -0
- node/planes/transport/bridge/compartment.py +785 -0
- node/planes/transport/bridge/ratelimit.py +44 -0
- node/planes/transport/bridge/types.py +43 -0
- node/planes/transport/lan/__init__.py +9 -0
- node/planes/transport/lan/compartment.py +446 -0
- node/planes/transport/lan/types.py +25 -0
- node/planes/transport/link/__init__.py +7 -0
- node/planes/transport/link/compartment.py +257 -0
- node/planes/transport/link/types.py +27 -0
- node/planes/transport/mediation/__init__.py +45 -0
- node/planes/transport/mediation/compartment.py +238 -0
- node/planes/transport/mediation/leaf.py +107 -0
- node/planes/transport/mediation/messages.py +302 -0
- node/planes/transport/mediation/service.py +233 -0
- node/planes/transport/mediation/store.py +37 -0
- node/planes/transport/mediation/types.py +20 -0
- node/planes/transport/p2p/__init__.py +7 -0
- node/planes/transport/p2p/compartment.py +349 -0
- node/planes/transport/p2p/types.py +22 -0
- node/protocols/__init__.py +277 -0
- node/protocols/activitypub/__init__.py +56 -0
- node/protocols/activitypub/actor.py +230 -0
- node/protocols/activitypub/federation.py +490 -0
- node/protocols/activitypub/media.py +145 -0
- node/protocols/activitypub/models.py +286 -0
- node/protocols/activitypub/package.py +271 -0
- node/protocols/activitypub/store.py +564 -0
- node/protocols/arcade/__init__.py +26 -0
- node/protocols/atproto/__init__.py +13 -0
- node/protocols/atproto/firehose.py +126 -0
- node/protocols/atproto/metering.py +165 -0
- node/protocols/atproto/package.py +155 -0
- node/protocols/atproto/repo.py +250 -0
- node/protocols/atproto/xrpc.py +285 -0
- node/protocols/base.py +109 -0
- node/protocols/circles/__init__.py +29 -0
- node/protocols/dm/__init__.py +50 -0
- node/protocols/grasp/__init__.py +22 -0
- node/protocols/isotime.py +15 -0
- node/protocols/nostr/__init__.py +51 -0
- node/protocols/nostr/blobstore.py +383 -0
- node/protocols/nostr/blossom.py +627 -0
- node/protocols/nostr/client.py +377 -0
- node/protocols/nostr/descriptor.py +211 -0
- node/protocols/nostr/eventstore.py +304 -0
- node/protocols/nostr/package.py +182 -0
- node/protocols/nostr/peers.py +109 -0
- node/protocols/nostr/relayserver.py +656 -0
- node/protocols/p2panda/__init__.py +22 -0
- node/protocols/p2panda/identity.py +19 -0
- node/protocols/p2panda/package.py +236 -0
- node/protocols/p2panda/rung.py +152 -0
- node/protocols/p2panda/store.py +273 -0
- node/protocols/rhio/__init__.py +33 -0
- node/protocols/rhio/package.py +275 -0
- node/protocols/rhio/rung.py +136 -0
- node/protocols/rhio/store.py +1423 -0
- node/protocols/ssb/muxrpc.py +256 -0
- node/protocols/ssb/package.py +267 -0
- node/protocols/textile/__init__.py +111 -0
- node/protocols/textile/transducers.py +259 -0
- node/release.py +119 -0
- node/storage/__init__.py +88 -0
- node/storage/accounts.py +305 -0
- node/storage/adapter.py +161 -0
- node/storage/base.py +385 -0
- node/storage/dialect.py +209 -0
- node/storage/grants.py +317 -0
- node/storage/locator.py +150 -0
- node/storage/postgres.py +294 -0
- node/storage/projected.py +953 -0
- node/storage/sharding.py +448 -0
- node/storage/standing.py +568 -0
- node/testnet/__init__.py +9 -0
- node/testnet/cluster.py +731 -0
- node/testnet/fips_daemon.py +230 -0
- node/transport/__init__.py +94 -0
- node/transport/const.py +110 -0
- node/transport/didcomm_inbound.py +251 -0
- node/transport/dnstxt.py +169 -0
- node/transport/fips_rung/__init__.py +67 -0
- node/transport/fips_rung/core.py +666 -0
- node/transport/fips_rung/listener.py +96 -0
- node/transport/fips_rung/types.py +12 -0
- node/transport/host.py +334 -0
- node/transport/iroh_rung/__init__.py +29 -0
- node/transport/iroh_rung/core.py +477 -0
- node/transport/iroh_rung/quic.py +942 -0
- node/transport/iroh_rung/types.py +22 -0
- node/transport/ladder.py +145 -0
- node/transport/lineproto.py +49 -0
- node/transport/mdns.py +303 -0
- node/transport/netfetch.py +283 -0
- node/transport/relay_rung/__init__.py +23 -0
- node/transport/relay_rung/cooldown.py +87 -0
- node/transport/relay_rung/core.py +138 -0
- node/transport/relay_rung/inbound.py +161 -0
- node/transport/relay_rung/ledger.py +104 -0
- node/transport/relay_rung/listen.py +121 -0
- node/transport/relay_rung/outbound.py +181 -0
- node/transport/relay_rung/shape.py +71 -0
- node/transport/relay_rung/types.py +58 -0
- node/transport/relay_rung/wire.py +63 -0
- node/transport/router.py +761 -0
- node/transport/rung.py +276 -0
- node/transport/ssb_rung.py +166 -0
- node/transport/ws.py +502 -0
- sidecar/fleetmesh_announce.py +338 -0
- sidecar/fleetmesh_p2p.py +373 -0
- sidecar/fleetmesh_rhio.py +352 -0
- sidecar/fleetmesh_ssb.py +603 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""The relay links this node holds open, as a compartment.
|
|
2
|
+
|
|
3
|
+
The transport keeps the sockets and counts what it drops. Nothing watched it:
|
|
4
|
+
the tick read `transport.health` directly to decide what to say to the journal,
|
|
5
|
+
and `health()` read six more of its counters by name. A subscription that
|
|
6
|
+
dropped was therefore visible in two hand-written places and in no refusal
|
|
7
|
+
channel at all, so `meshnode refusals` - the one view that answers "what has
|
|
8
|
+
this node been saying no to" - could not show a relay that had been refusing
|
|
9
|
+
this node for an hour.
|
|
10
|
+
|
|
11
|
+
This compartment owns what the journal has already been told, so it says a
|
|
12
|
+
transition once and not once a tick, and it records the drop as a refusal like
|
|
13
|
+
every other no.
|
|
14
|
+
|
|
15
|
+
It also records the transport's verdicts on stored messages. A kind 1059 the
|
|
16
|
+
relay accepted and never served used to be indistinguishable from one that
|
|
17
|
+
arrived; now every stored message asks for an acknowledgment, and a peer that
|
|
18
|
+
has spoken since and not acknowledged is a `stored: unacknowledged` refusal
|
|
19
|
+
here, with the message type and the relays it went through, so `meshnode
|
|
20
|
+
refusals` can show the day the control plane went quiet.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any, Callable, Optional
|
|
25
|
+
|
|
26
|
+
from node.compartment import stores as st
|
|
27
|
+
from node.channels import Channel, NOTE, TROUBLE
|
|
28
|
+
from node.compartment import Command, Compartment, Need
|
|
29
|
+
from node.metrics import COUNTER, Metric
|
|
30
|
+
from node.interfaces import AwaitingAckRow
|
|
31
|
+
from node.transport.ladder import Fault
|
|
32
|
+
from node.transport.relay_rung.cooldown import Cooling
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
from node.config import TransportConfig
|
|
36
|
+
from .types import LinkState
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Link(Compartment):
|
|
40
|
+
|
|
41
|
+
name: str = 'link'
|
|
42
|
+
reads: tuple[str, ...] = ('transport',)
|
|
43
|
+
config_cls = TransportConfig
|
|
44
|
+
# strain is constructed before link so a reconnect can be recorded into it,
|
|
45
|
+
# but ticks after link (the journal first); it is bound, not required.
|
|
46
|
+
requires: tuple[str, ...] = ()
|
|
47
|
+
NEEDS = (
|
|
48
|
+
Need('transport'),
|
|
49
|
+
Need('strain', late=True),
|
|
50
|
+
)
|
|
51
|
+
COMMANDS = (
|
|
52
|
+
Command(
|
|
53
|
+
key='show',
|
|
54
|
+
name='Link',
|
|
55
|
+
description='every relay subscription, what it has dropped, what is waiting to be '
|
|
56
|
+
'read, and the stored messages still waiting to be acknowledged',
|
|
57
|
+
tags=('link', 'status'),
|
|
58
|
+
permission='link/show',
|
|
59
|
+
run='state',
|
|
60
|
+
reply='{relays, dropped, recent_faults, publish_refused_in_window, cooling, inbox, '
|
|
61
|
+
'wraps_in, wraps_seen_again, unfinished_envelopes, awaiting_ack, acked, '
|
|
62
|
+
'acks_in, acks_out, unacknowledged}',
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
STORES = (
|
|
66
|
+
st.Store(key='_up', description='last reported connection state per relay url',
|
|
67
|
+
kind=st.TABLE, shape=bool, bound=1024, keyed_by='url', evict=st.LRU),
|
|
68
|
+
st.Store(key='_said', description='last reported unreachable reason per relay url',
|
|
69
|
+
kind=st.TABLE, bound=1024, keyed_by='url', evict=st.LRU),
|
|
70
|
+
st.Store(key='_ack', description='last reported subscription-acknowledged state per relay url',
|
|
71
|
+
kind=st.TABLE, shape=bool, bound=1024, keyed_by='url', evict=st.LRU),
|
|
72
|
+
)
|
|
73
|
+
CHANNELS = (
|
|
74
|
+
Channel('relay_up', 'Relay subscription connected', say='relay {url} subscription up'),
|
|
75
|
+
Channel('relay_down', 'Relay subscription disconnected', level=TROUBLE,
|
|
76
|
+
say='relay {url} subscription down ({reason}), reconnecting'),
|
|
77
|
+
Channel('relay_unreachable', 'Relay not reachable', level=TROUBLE,
|
|
78
|
+
say='relay {url} not reachable ({reason}), retrying'),
|
|
79
|
+
Channel('unacknowledged', 'Stored message was never acknowledged', level=NOTE,
|
|
80
|
+
say='stored {peer} {type} never acknowledged; the peer has spoken since via {relays}'),
|
|
81
|
+
Channel('relay_deaf', 'Relay holds the socket but never acknowledged the subscription',
|
|
82
|
+
level=TROUBLE,
|
|
83
|
+
say='relay {url} took the subscription and never acknowledged it ({reason})'),
|
|
84
|
+
Channel('relay_acked', 'Relay acknowledged the subscription',
|
|
85
|
+
say='relay {url} acknowledged the subscription'),
|
|
86
|
+
)
|
|
87
|
+
METRICS = (
|
|
88
|
+
Metric(name='link_relays_connected', description='Times a relay connection came up.',
|
|
89
|
+
kind=COUNTER, counts='relay_up', tags=('link', 'relay')),
|
|
90
|
+
Metric(name='link_relays_dropped', description='Times a relay connection dropped.',
|
|
91
|
+
kind=COUNTER, counts='relay_down', tags=('link', 'relay')),
|
|
92
|
+
Metric(name='link_relays_unreachable', description='Times a relay was unreachable.',
|
|
93
|
+
kind=COUNTER, counts='relay_unreachable', tags=('link', 'relay')),
|
|
94
|
+
Metric(name='link_unacknowledged', description='Times a stored message was unacknowledged.',
|
|
95
|
+
kind=COUNTER, counts='unacknowledged', tags=('link', 'transport')),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def bind(self, strain: Any) -> None:
|
|
99
|
+
"""The ledger a reconnect is recorded into. Bound after both exist
|
|
100
|
+
because strain ticks after link and the assembler builds in tick order."""
|
|
101
|
+
self.strain = strain
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
transport: Any,
|
|
106
|
+
strain: Optional[Any] = None,
|
|
107
|
+
clock: Optional[Callable[[], int]] = None,
|
|
108
|
+
node: Optional[Any] = None,
|
|
109
|
+
) -> None:
|
|
110
|
+
super().__init__(node, clock)
|
|
111
|
+
self.transport: Any = transport
|
|
112
|
+
self.strain = strain
|
|
113
|
+
self._up: st.Table[str, bool] # url -> the connected state the journal was last told
|
|
114
|
+
self._said: st.Table[str, Optional[str]] # url -> the last unreachable reason the journal was told
|
|
115
|
+
self._ack: st.Table[str, bool] # url -> the acknowledged state the journal was last told
|
|
116
|
+
|
|
117
|
+
def tick(self, now: int) -> list[tuple[Any, ...]]:
|
|
118
|
+
"""Say what changed, once each way it changed.
|
|
119
|
+
|
|
120
|
+
A relay that is down stays down, and saying so every thirty seconds
|
|
121
|
+
buries everything else in the journal. A transition is news; a state
|
|
122
|
+
is not.
|
|
123
|
+
"""
|
|
124
|
+
for url, h in self.transport.health.items():
|
|
125
|
+
prev = self._up.get(url)
|
|
126
|
+
if h['connected'] and prev is not True:
|
|
127
|
+
self.say('relay_up', url=url)
|
|
128
|
+
elif not h['connected'] and prev is True:
|
|
129
|
+
self.say('relay_down', url=url, reason=h['last_error'] or 'no reason')
|
|
130
|
+
self.refusals.refuse('subscription', 'dropped', subject=url,
|
|
131
|
+
detail={'error': h['last_error']}, at=now)
|
|
132
|
+
self._said[url] = h['last_error'] # the next tick must not say the same reason again
|
|
133
|
+
if self.strain is not None:
|
|
134
|
+
self.strain.record('reconnect', transport=url)
|
|
135
|
+
elif not h['connected'] and h['last_error'] and self._said.get(url) != h['last_error']:
|
|
136
|
+
self.say('relay_unreachable', url=url, reason=h['last_error'])
|
|
137
|
+
self._said[url] = h['last_error']
|
|
138
|
+
self.refusals.refuse('subscription', 'not reachable', subject=url,
|
|
139
|
+
detail={'error': h['last_error']}, at=now)
|
|
140
|
+
# `connected` is the socket; `confirmed` is the relay's own EOSE.
|
|
141
|
+
# A relay that holds the socket and never acknowledges carries no
|
|
142
|
+
# events to us, so the inbox is deaf - and every branch above keys
|
|
143
|
+
# off `connected`, which is True, so the journal said 'subscription
|
|
144
|
+
# up' and nothing further (audit I13). Said on transition, like
|
|
145
|
+
# `connected`; trouble() is what covers a run that is never
|
|
146
|
+
# acknowledged at all, because that run has no transition to say.
|
|
147
|
+
if h['connected']:
|
|
148
|
+
was = self._ack.get(url)
|
|
149
|
+
acked = bool(h.get('confirmed'))
|
|
150
|
+
if acked and was is not True:
|
|
151
|
+
self.say('relay_acked', url=url)
|
|
152
|
+
elif not acked and was is True:
|
|
153
|
+
self.say('relay_deaf', url=url, reason=h['last_error'] or 'no EOSE')
|
|
154
|
+
self._ack[url] = acked
|
|
155
|
+
self._up[url] = h['connected']
|
|
156
|
+
out: list[tuple[Any, ...]] = []
|
|
157
|
+
self.transport.sweep(now)
|
|
158
|
+
for v in self.transport.take_unacked():
|
|
159
|
+
if v['verdict'] != 'unacknowledged':
|
|
160
|
+
continue # expired: nothing could arrive any more, and nobody was heard
|
|
161
|
+
self.say('unacknowledged', peer=v['peer'][:16], type=v['type'], relays=', '.join(v['relays']) or 'no relay')
|
|
162
|
+
self.refusals.refuse('stored', 'unacknowledged', subject=v['peer'],
|
|
163
|
+
detail={'type': v['type'], 'sent_at': v['at'], 'heard_at': v['heard_at'],
|
|
164
|
+
'relays': v['relays'], 'mid': v['mid'][:16]}, at=now)
|
|
165
|
+
out.append(('unacknowledged', v['peer'], v['type']))
|
|
166
|
+
return out
|
|
167
|
+
|
|
168
|
+
FAULTS_SHOWN: int = 20
|
|
169
|
+
|
|
170
|
+
def _faults(self, since: Optional[int] = None) -> list[Fault]:
|
|
171
|
+
recent = getattr(self.transport, 'recent_faults', None)
|
|
172
|
+
if recent is None: # a transport without a fault log: nothing to read
|
|
173
|
+
return []
|
|
174
|
+
return list(recent(since))
|
|
175
|
+
|
|
176
|
+
def _cooling(self) -> dict[str, Cooling]:
|
|
177
|
+
cd = self.transport.cooldown
|
|
178
|
+
return cd.state() if cd is not None else {}
|
|
179
|
+
|
|
180
|
+
def publish_refused(self, now: Optional[int] = None) -> list[Fault]:
|
|
181
|
+
"""Wraps a relay refused within TROUBLE_WINDOW: the fault the OK
|
|
182
|
+
reports and nothing else shows, because a refused wrap is counted
|
|
183
|
+
as sent by the caller that published it."""
|
|
184
|
+
at = self.clock() if now is None else now
|
|
185
|
+
return [f for f in self._faults(at - self.TROUBLE_WINDOW)
|
|
186
|
+
if f['reason'].split(':', 1)[0] == 'publish refused']
|
|
187
|
+
|
|
188
|
+
def state(self) -> LinkState:
|
|
189
|
+
t = self.transport
|
|
190
|
+
return LinkState(
|
|
191
|
+
relays={url: dict(h) for url, h in t.health.items()}, # type: ignore[misc]
|
|
192
|
+
rungs=dict(t.rung_health()) if hasattr(t, 'rung_health') else {},
|
|
193
|
+
dropped=dict(t.drop_counts),
|
|
194
|
+
recent_faults=list(reversed(self._faults()))[:self.FAULTS_SHOWN],
|
|
195
|
+
publish_refused_in_window=len(self.publish_refused()),
|
|
196
|
+
cooling=self._cooling(),
|
|
197
|
+
inbox=t.inbox.qsize(),
|
|
198
|
+
wraps_in={str(k): v for k, v in t.wraps_in.items()},
|
|
199
|
+
wraps_seen_again={str(k): v for k, v in t.wraps_seen_again.items()},
|
|
200
|
+
unfinished_envelopes=t.unfinished(),
|
|
201
|
+
awaiting_ack=[AwaitingAckRow(peer=e['peer'][:16], type=e['type'], at=e['at'], relays=e['relays'])
|
|
202
|
+
for e in list(t.awaiting.values())],
|
|
203
|
+
acked=dict(t.acked),
|
|
204
|
+
acks_in=t.acks_in,
|
|
205
|
+
acks_out=t.acks_out,
|
|
206
|
+
unacknowledged=[AwaitingAckRow(peer=v['peer'][:16], type=v['type'], at=v['at'],
|
|
207
|
+
heard_at=v['heard_at'], relays=v['relays'])
|
|
208
|
+
for v in list(t.unacked) if v['verdict'] == 'unacknowledged'],
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def notes(self) -> list[str]:
|
|
212
|
+
cooling = self._cooling()
|
|
213
|
+
if not cooling:
|
|
214
|
+
return []
|
|
215
|
+
# Not wrong in the node, but a relay it is not publishing to; an
|
|
216
|
+
# operator who sees a quiet relay should know it was asked to stop.
|
|
217
|
+
return ['%d relay(s) cooling after a rate-limit: %s'
|
|
218
|
+
% (len(cooling), '; '.join('%s until %d (%s)' % (u, c['until'], c['why'])
|
|
219
|
+
for u, c in sorted(cooling.items())))]
|
|
220
|
+
|
|
221
|
+
def trouble(self) -> list[str]:
|
|
222
|
+
out: list[str] = []
|
|
223
|
+
down = sorted(u for u, h in self.transport.health.items() if not h['connected'])
|
|
224
|
+
if down:
|
|
225
|
+
out.append('relay subscription down: %s' % ', '.join(down))
|
|
226
|
+
# The deaf inbox. `wait_listening` counts `confirmed`, and app start
|
|
227
|
+
# warns once on a timeout justifying itself with "`link` will keep
|
|
228
|
+
# saying so" - which it did not, because every row here read
|
|
229
|
+
# `connected` instead. A relay that holds the socket and never sends
|
|
230
|
+
# EOSE is connected, unconfirmed, quiet, and green in health and
|
|
231
|
+
# `meshctl watch`, while live wraps sent to it are lost: kind 21059 is
|
|
232
|
+
# ephemeral, so no relay keeps a copy (audit I13).
|
|
233
|
+
deaf = sorted((u, h['last_error']) for u, h in self.transport.health.items()
|
|
234
|
+
if h['connected'] and not h.get('confirmed'))
|
|
235
|
+
if deaf:
|
|
236
|
+
out.append('%d relay subscription(s) connected but never acknowledged: %s'
|
|
237
|
+
% (len(deaf), '; '.join('%s (%s)' % (u, e or 'no EOSE') for u, e in deaf)))
|
|
238
|
+
refused = self.publish_refused()
|
|
239
|
+
if refused:
|
|
240
|
+
# A relay that takes the subscription and refuses every wrap
|
|
241
|
+
# looks healthy in every other row: connected, confirmed, quiet.
|
|
242
|
+
by_relay: dict[str, str] = {}
|
|
243
|
+
for f in refused:
|
|
244
|
+
rest = f['reason'].split(':', 1)[1].strip() # 'wss://relay reason text'
|
|
245
|
+
url, _, why = rest.partition(' ')
|
|
246
|
+
by_relay.setdefault(url, why)
|
|
247
|
+
out.append('%d wrap(s) refused by relays in the last %ds: %s'
|
|
248
|
+
% (len(refused), self.TROUBLE_WINDOW,
|
|
249
|
+
'; '.join('%s (%s)' % (u, w) for u, w in sorted(by_relay.items()))))
|
|
250
|
+
recent = self.refusals.within(self.TROUBLE_WINDOW, self.clock(), kind='stored')
|
|
251
|
+
if recent:
|
|
252
|
+
# The one fault the publish result cannot show: what was said to
|
|
253
|
+
# the peer never reached it, and the peer was there to hear it.
|
|
254
|
+
by = sorted({'%s %s' % ((r.subject or '')[:16], r.detail.get('type', '')) for r in recent})
|
|
255
|
+
out.append('%d stored message(s) never acknowledged by a peer that has spoken since: %s'
|
|
256
|
+
% (len(recent), '; '.join(by)))
|
|
257
|
+
return out
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Types and records for the Link compartment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Mapping, TypedDict
|
|
5
|
+
|
|
6
|
+
from node.interfaces import AwaitingAckRow, RelayHealthRow
|
|
7
|
+
from node.transport.ladder import Fault
|
|
8
|
+
from node.transport.relay_rung.cooldown import Cooling
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LinkState(TypedDict):
|
|
12
|
+
"""What `meshnode link show` and the health report say about the relays."""
|
|
13
|
+
relays: dict[str, RelayHealthRow]
|
|
14
|
+
rungs: dict[str, Mapping[str, Any]] # every transport rung's own report, by rung name (nostr, iroh, ...)
|
|
15
|
+
dropped: dict[str, int] # fault family -> count, ever across the ladder
|
|
16
|
+
recent_faults: list[Fault] # the newest first, FAULTS_SHOWN of them, with reasons
|
|
17
|
+
publish_refused_in_window: int # wraps a relay refused within TROUBLE_WINDOW
|
|
18
|
+
cooling: dict[str, Cooling] # relays held out of publishing after a rate-limit, until when
|
|
19
|
+
inbox: int
|
|
20
|
+
wraps_in: dict[str, int]
|
|
21
|
+
wraps_seen_again: dict[str, int]
|
|
22
|
+
unfinished_envelopes: int
|
|
23
|
+
awaiting_ack: list[AwaitingAckRow]
|
|
24
|
+
acked: dict[str, int]
|
|
25
|
+
acks_in: int
|
|
26
|
+
acks_out: int
|
|
27
|
+
unacknowledged: list[AwaitingAckRow]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Mobile Leaf & DIDComm Mediation (`routing/2.0` and `coordinate-mediation/2.0`).
|
|
2
|
+
|
|
3
|
+
Spec reference: spec.html section "Mediation is how a phone gets an address" and
|
|
4
|
+
"Cold-Start Mediator Discovery via NIP-66" (lines 3313-3348).
|
|
5
|
+
|
|
6
|
+
Covers:
|
|
7
|
+
1. DIDComm `coordinate-mediation/2.0` protocol (mediate-request, mediate-grant, mediate-deny, keylist-update).
|
|
8
|
+
2. DIDComm `routing/2.0` protocol (forward messages wrapping inner encrypted envelopes).
|
|
9
|
+
3. DIDComm `messagepickup/2.0` protocol (status-request, delivery-request, delivery, messages-received).
|
|
10
|
+
4. `MediatorService`: Anchor mediator buffering store-and-forward envelopes for offline/mobile leaves.
|
|
11
|
+
5. `MobileLeafClient`: Mobile leaf lifecycle, dual-mediation, and NIP-66 cold-start mediator discovery.
|
|
12
|
+
6. `Mediation` Compartment: Wire integration with NodeApp.
|
|
13
|
+
|
|
14
|
+
Laid out as a package by those seams: messages.py (the wire vocabulary),
|
|
15
|
+
store.py, service.py (the anchor), leaf.py (the phone), compartment.py (the
|
|
16
|
+
compartment). Everything the module used to export is re-exported here.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from .leaf import MobileLeafClient
|
|
21
|
+
from .messages import (KEYLIST_QUERY, KEYLIST_RESPONSE, KEYLIST_UPDATE, KEYLIST_UPDATE_RESPONSE,
|
|
22
|
+
MEDIATE_DENY, MEDIATE_GRANT, MEDIATE_REQUEST, PICKUP_DELIVERY,
|
|
23
|
+
PICKUP_DELIVERY_REQUEST, PICKUP_LIMIT_DEFAULT, PICKUP_LIMIT_MAX, PICKUP_LIMIT_MIN,
|
|
24
|
+
PICKUP_MESSAGES_RECEIVED, PICKUP_STATUS, PICKUP_STATUS_REQUEST, PIURIS,
|
|
25
|
+
ROUTING_FORWARD, clamp_pickup_limit, leaf_of, make_forward, make_keylist_update,
|
|
26
|
+
make_keylist_update_response, make_mediate_deny, make_mediate_grant,
|
|
27
|
+
make_mediate_request, make_pickup_delivery, make_pickup_delivery_request,
|
|
28
|
+
make_pickup_messages_received, make_pickup_status, make_pickup_status_request)
|
|
29
|
+
from .service import MediatorService
|
|
30
|
+
from .store import MediationStore
|
|
31
|
+
from .compartment import Mediation
|
|
32
|
+
from .types import DeliveredMessage, MediationGrantInfo
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
'KEYLIST_QUERY', 'KEYLIST_RESPONSE', 'KEYLIST_UPDATE', 'KEYLIST_UPDATE_RESPONSE',
|
|
36
|
+
'MEDIATE_DENY', 'MEDIATE_GRANT', 'MEDIATE_REQUEST', 'PICKUP_DELIVERY',
|
|
37
|
+
'PICKUP_DELIVERY_REQUEST', 'PICKUP_LIMIT_DEFAULT', 'PICKUP_LIMIT_MAX', 'PICKUP_LIMIT_MIN',
|
|
38
|
+
'PICKUP_MESSAGES_RECEIVED', 'PICKUP_STATUS', 'PICKUP_STATUS_REQUEST', 'PIURIS', 'ROUTING_FORWARD',
|
|
39
|
+
'clamp_pickup_limit', 'leaf_of', 'make_forward', 'make_keylist_update', 'make_keylist_update_response',
|
|
40
|
+
'make_mediate_deny', 'make_mediate_grant', 'make_mediate_request', 'make_pickup_delivery',
|
|
41
|
+
'make_pickup_delivery_request', 'make_pickup_messages_received', 'make_pickup_status',
|
|
42
|
+
'make_pickup_status_request',
|
|
43
|
+
'MediationStore', 'MediatorService', 'MobileLeafClient', 'Mediation',
|
|
44
|
+
'DeliveredMessage', 'MediationGrantInfo',
|
|
45
|
+
]
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""The Mediation compartment: the operator commands and the inbound door
|
|
2
|
+
for the three mediation PIURIs, acting for the proven sender."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Callable, Mapping, Optional
|
|
7
|
+
|
|
8
|
+
from node import channels as ch_mod
|
|
9
|
+
from node import metrics as mx_mod
|
|
10
|
+
from node.compartment import stores as st
|
|
11
|
+
from node.compartment import Command, Handler, Need, Outbound, WireProtocol
|
|
12
|
+
from node.config import MediationConfig
|
|
13
|
+
from node.config.parameter import Parameter
|
|
14
|
+
from .messages import (KEYLIST_UPDATE, MEDIATE_REQUEST, PICKUP_DELIVERY_REQUEST, PICKUP_MESSAGES_RECEIVED,
|
|
15
|
+
PIURIS, ROUTING_FORWARD, make_mediate_request, make_pickup_delivery_request)
|
|
16
|
+
from .service import MediatorService
|
|
17
|
+
from .store import MediationStore
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Mediation(WireProtocol):
|
|
21
|
+
"""The mediation compartment of meshnode (routing/2.0 & coordinate-mediation/2.0)."""
|
|
22
|
+
|
|
23
|
+
name = 'mediation'
|
|
24
|
+
piuris: tuple[str, ...] = tuple(PIURIS)
|
|
25
|
+
OUTBOUND = (
|
|
26
|
+
Outbound('mediate-grant', doc='grant coordinate-mediation admission and routing keys to leaf'),
|
|
27
|
+
Outbound('mediate-deny', doc='deny coordinate-mediation admission to leaf'),
|
|
28
|
+
Outbound('keylist-update-response', doc='acknowledge leaf routing keys updates'),
|
|
29
|
+
Outbound('delivery', doc='drain queued messages to leaf'),
|
|
30
|
+
Outbound('status', doc='deliver queue status to leaf'),
|
|
31
|
+
)
|
|
32
|
+
reads = ('mediation', 'endpoints', 'relays')
|
|
33
|
+
config_cls = MediationConfig
|
|
34
|
+
SCHEMA = MediationStore.SCHEMA
|
|
35
|
+
STORES = (
|
|
36
|
+
st.Store(key='handled', description='messages handled by protocol message type',
|
|
37
|
+
kind=st.TABLE, shape=int, bound=64, keyed_by='msg_type', evict=st.LRU),
|
|
38
|
+
)
|
|
39
|
+
CHANNELS = (
|
|
40
|
+
ch_mod.Channel('message_forwarded', 'Mediation forward message queued for leaf',
|
|
41
|
+
level=ch_mod.INFO, say='forward message queued for recipient {recipient}',
|
|
42
|
+
tags=('mediation', 'forward')),
|
|
43
|
+
ch_mod.Channel('forward_refused', 'Mediation forward message refused or dropped',
|
|
44
|
+
level=ch_mod.TROUBLE, say='forward message refused for recipient {recipient}',
|
|
45
|
+
tags=('mediation', 'forward')),
|
|
46
|
+
ch_mod.Channel('mediation_granted', 'Mediation admission granted to leaf',
|
|
47
|
+
level=ch_mod.INFO, say='mediation admission granted to {leaf}',
|
|
48
|
+
tags=('mediation', 'admission')),
|
|
49
|
+
ch_mod.Channel('messages_drained', 'Queued messages picked up by leaf',
|
|
50
|
+
level=ch_mod.INFO, say='picked up {count} messages for {leaf}',
|
|
51
|
+
tags=('mediation', 'pickup')),
|
|
52
|
+
ch_mod.Channel('sender_unauthenticated', 'Mediation message sender not authenticated',
|
|
53
|
+
level=ch_mod.TROUBLE, say='mediation request refused: sender not authenticated',
|
|
54
|
+
tags=('mediation', 'security')),
|
|
55
|
+
)
|
|
56
|
+
METRICS = (
|
|
57
|
+
mx_mod.Metric(name='mediation_messages_forwarded', description='Messages buffered for mediated leaves.',
|
|
58
|
+
kind=mx_mod.COUNTER, counts='message_forwarded', tags=('mediation', 'forward')),
|
|
59
|
+
mx_mod.Metric(name='mediation_forward_refused', description='Forward messages refused or dropped.',
|
|
60
|
+
kind=mx_mod.COUNTER, counts='forward_refused', tags=('mediation', 'forward')),
|
|
61
|
+
mx_mod.Metric(name='mediation_admissions_granted', description='Leaves admitted into mediation.',
|
|
62
|
+
kind=mx_mod.COUNTER, counts='mediation_granted', tags=('mediation', 'admission')),
|
|
63
|
+
mx_mod.Metric(name='mediation_messages_drained', description='Batches of messages drained by leaves.',
|
|
64
|
+
kind=mx_mod.COUNTER, counts='messages_drained', tags=('mediation', 'pickup')),
|
|
65
|
+
mx_mod.Metric(name='mediation_unauthenticated', description='Mediation messages from unauthenticated senders.',
|
|
66
|
+
kind=mx_mod.COUNTER, counts='sender_unauthenticated', tags=('mediation', 'security')),
|
|
67
|
+
)
|
|
68
|
+
HANDLERS = (
|
|
69
|
+
Handler('forward', run='handle_forward', doc='buffer inbound routing/2.0 forward envelope for mobile leaf'),
|
|
70
|
+
Handler('mediate-request', run='handle_mediate_request', writes=('mediation',), doc='grant coordinate-mediation admission'),
|
|
71
|
+
Handler('keylist-update', run='handle_keylist_update', writes=('mediation',), doc='update leaf routing keys'),
|
|
72
|
+
Handler('delivery-request', run='handle_pickup_delivery_request', reads=('mediation',), doc='drain queued messages for leaf'),
|
|
73
|
+
Handler('messages-received', run='handle_pickup_messages_received', writes=('mediation',), doc='acknowledge and purge delivered messages'),
|
|
74
|
+
)
|
|
75
|
+
COMMANDS = (
|
|
76
|
+
Command(
|
|
77
|
+
key='show',
|
|
78
|
+
name='Mediation Status',
|
|
79
|
+
description='display active mediated leaves and queue depth',
|
|
80
|
+
tags=('mediation', 'status'),
|
|
81
|
+
permission='mediation/show',
|
|
82
|
+
run='show_cmd',
|
|
83
|
+
reply='dict',
|
|
84
|
+
),
|
|
85
|
+
Command(
|
|
86
|
+
key='grant',
|
|
87
|
+
name='Grant Mediation',
|
|
88
|
+
description='admit leaf into mediation and grant routing keys',
|
|
89
|
+
tags=('mediation', 'grant'),
|
|
90
|
+
parameters=(
|
|
91
|
+
Parameter(key='leaf', name='leaf', description='did of the leaf to admit', type=str, default='', required=True),
|
|
92
|
+
),
|
|
93
|
+
permission='mediation/grant',
|
|
94
|
+
run='grant_cmd',
|
|
95
|
+
reply='dict',
|
|
96
|
+
),
|
|
97
|
+
Command(
|
|
98
|
+
key='forward',
|
|
99
|
+
name='Forward Message',
|
|
100
|
+
description='buffer inbound routing/2.0 forward envelope for mobile leaf',
|
|
101
|
+
tags=('mediation', 'forward'),
|
|
102
|
+
parameters=(
|
|
103
|
+
Parameter(key='message', name='message', description='forward envelope (dict or json string)', type=object, default=dict, required=True),
|
|
104
|
+
),
|
|
105
|
+
permission='mediation/forward',
|
|
106
|
+
run='forward_cmd',
|
|
107
|
+
reply='bool',
|
|
108
|
+
),
|
|
109
|
+
Command(
|
|
110
|
+
key='pickup',
|
|
111
|
+
name='Pickup Messages',
|
|
112
|
+
description='drain queued forwarded envelopes for mobile leaf',
|
|
113
|
+
tags=('mediation', 'pickup'),
|
|
114
|
+
parameters=(
|
|
115
|
+
Parameter(key='leaf', name='leaf', description='did of the leaf to drain for', type=str, default='', required=True),
|
|
116
|
+
Parameter(key='limit', name='limit', description='max envelopes to drain', type=int, default=10, required=False),
|
|
117
|
+
),
|
|
118
|
+
permission='mediation/pickup',
|
|
119
|
+
run='pickup_cmd',
|
|
120
|
+
reply='dict',
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
DB: str = 'mediation.sqlite'
|
|
125
|
+
NEEDS = (
|
|
126
|
+
Need('db_path', path='DB'),
|
|
127
|
+
Need('mediator_did', 'did'),
|
|
128
|
+
Need('transport', 'transport'),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def __init__(self, node: Any = None, db_path: Optional[str] = None, mediator_did: Optional[str] = None,
|
|
132
|
+
clock: Optional[Callable[[], int]] = None, transport: Optional[Any] = None,
|
|
133
|
+
db: Optional[Any] = None) -> None:
|
|
134
|
+
"""Built by `from_context` in a node. The bare `Mediation()` and
|
|
135
|
+
`Mediation(node=fake)` forms the tests use still work: with no path
|
|
136
|
+
the store is in memory, and with no DID the one on `node.ks` is used
|
|
137
|
+
if there is one."""
|
|
138
|
+
if db is None and db_path is not None:
|
|
139
|
+
from node.storage.adapter import SqliteAdapter
|
|
140
|
+
db = SqliteAdapter(db_path, schema=self.SCHEMA, clock=clock)
|
|
141
|
+
super().__init__(node, clock, db=db)
|
|
142
|
+
# The transport a reply goes back over; off the fake node in the suites.
|
|
143
|
+
self.transport: Optional[Any] = transport if transport is not None else getattr(node, 'transport', None)
|
|
144
|
+
self.handled: st.Table[str, int]
|
|
145
|
+
if mediator_did is None:
|
|
146
|
+
ks = getattr(node, 'ks', None) if node is not None else None
|
|
147
|
+
actual_did = 'did:nostr:%s' % ks.node_pk if ks is not None else ''
|
|
148
|
+
else:
|
|
149
|
+
actual_did = str(mediator_did)
|
|
150
|
+
if db_path is not None:
|
|
151
|
+
self.service: MediatorService = MediatorService(db_path=db_path, mediator_did=actual_did, clock=clock, db=self.db)
|
|
152
|
+
else:
|
|
153
|
+
self.service = MediatorService(mediator_did=actual_did, clock=clock, db=self.db)
|
|
154
|
+
|
|
155
|
+
def show_cmd(self) -> Mapping[str, Any]:
|
|
156
|
+
return {
|
|
157
|
+
'mediator_did': self.service.mediator_did,
|
|
158
|
+
'routing_keys': self.service.routing_keys,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
def grant_cmd(self, args: Mapping[str, Any]) -> Any:
|
|
162
|
+
req = make_mediate_request(from_did=str(args['leaf']), to_did=self.service.mediator_did)
|
|
163
|
+
return self.service.handle_mediate_request(req)
|
|
164
|
+
|
|
165
|
+
def forward_cmd(self, args: Mapping[str, Any]) -> bool:
|
|
166
|
+
raw = args['message']
|
|
167
|
+
msg = raw if isinstance(raw, dict) else json.loads(raw)
|
|
168
|
+
return bool(self.service.handle_forward(msg))
|
|
169
|
+
|
|
170
|
+
def pickup_cmd(self, args: Mapping[str, Any]) -> Any:
|
|
171
|
+
pick = make_pickup_delivery_request(
|
|
172
|
+
from_did=str(args['leaf']),
|
|
173
|
+
to_did=self.service.mediator_did,
|
|
174
|
+
limit=int(args.get('limit') or 10),
|
|
175
|
+
)
|
|
176
|
+
return self.service.handle_pickup_delivery_request(pick)
|
|
177
|
+
|
|
178
|
+
# ---- the wire ---------------------------------------------------------
|
|
179
|
+
def handle_forward(self, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]:
|
|
180
|
+
msg = getattr(inbound, 'message', inbound)
|
|
181
|
+
if not isinstance(msg, dict) or not isinstance(sender, str) or not sender:
|
|
182
|
+
return None
|
|
183
|
+
queued = self.service.handle_forward(msg)
|
|
184
|
+
self._count(ROUTING_FORWARD)
|
|
185
|
+
recipient = str(msg.get('body', {}).get('next', ''))[:24]
|
|
186
|
+
if queued:
|
|
187
|
+
self.say('message_forwarded', recipient=recipient)
|
|
188
|
+
else:
|
|
189
|
+
self.say('forward_refused', recipient=recipient)
|
|
190
|
+
return ('mediation forward', 'queued' if queued else 'refused', recipient)
|
|
191
|
+
|
|
192
|
+
def _handle_authenticated(self, inbound: Any, sender_pk: str, mtype: str, action: Callable[..., Any]) -> Optional[tuple[Any, ...]]:
|
|
193
|
+
msg = getattr(inbound, 'message', inbound)
|
|
194
|
+
if not isinstance(msg, dict) or not isinstance(sender_pk, str) or not sender_pk:
|
|
195
|
+
return None
|
|
196
|
+
if not getattr(inbound, 'authenticated', False):
|
|
197
|
+
self._count('unauthenticated')
|
|
198
|
+
self.say('sender_unauthenticated')
|
|
199
|
+
return ('mediation refused', mtype.rsplit('/', 1)[-1], 'sender not authenticated')
|
|
200
|
+
sender = 'did:nostr:' + sender_pk
|
|
201
|
+
try:
|
|
202
|
+
reply = action(msg, sender=sender)
|
|
203
|
+
except PermissionError as e:
|
|
204
|
+
self._count('impersonation')
|
|
205
|
+
return ('mediation refused', mtype.rsplit('/', 1)[-1], str(e))
|
|
206
|
+
self._count(mtype)
|
|
207
|
+
if mtype == MEDIATE_REQUEST and isinstance(reply, dict) and reply.get('type', '').endswith('mediate-grant'):
|
|
208
|
+
self.say('mediation_granted', leaf=sender)
|
|
209
|
+
elif mtype == PICKUP_DELIVERY_REQUEST and isinstance(reply, dict):
|
|
210
|
+
msgs = reply.get('body', {}).get('messages', [])
|
|
211
|
+
self.say('messages_drained', count=len(msgs), leaf=sender)
|
|
212
|
+
if mtype == PICKUP_MESSAGES_RECEIVED:
|
|
213
|
+
return ('mediation received', sender_pk[:16], reply)
|
|
214
|
+
if self.transport is not None and isinstance(reply, dict):
|
|
215
|
+
self.wire_client.send(sender_pk, reply)
|
|
216
|
+
return ('mediation', mtype.rsplit('/', 1)[-1], sender_pk[:16], str(reply.get('type', ''))[-30:] if isinstance(reply, dict) else '')
|
|
217
|
+
|
|
218
|
+
def handle_mediate_request(self, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]:
|
|
219
|
+
return self._handle_authenticated(inbound, sender, MEDIATE_REQUEST, self.service.handle_mediate_request)
|
|
220
|
+
|
|
221
|
+
def handle_keylist_update(self, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]:
|
|
222
|
+
return self._handle_authenticated(inbound, sender, KEYLIST_UPDATE, self.service.handle_keylist_update)
|
|
223
|
+
|
|
224
|
+
def handle_pickup_delivery_request(self, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]:
|
|
225
|
+
return self._handle_authenticated(inbound, sender, PICKUP_DELIVERY_REQUEST, self.service.handle_pickup_delivery_request)
|
|
226
|
+
|
|
227
|
+
def handle_pickup_messages_received(self, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]:
|
|
228
|
+
return self._handle_authenticated(inbound, sender, PICKUP_MESSAGES_RECEIVED, self.service.handle_messages_received)
|
|
229
|
+
|
|
230
|
+
def _count(self, key: str) -> None:
|
|
231
|
+
self.handled[key] = self.handled.get(key, 0) + 1
|
|
232
|
+
|
|
233
|
+
def stop(self) -> None:
|
|
234
|
+
"""Safely close mediation persistence and worker state on shutdown."""
|
|
235
|
+
if hasattr(self, 'service') and self.service:
|
|
236
|
+
self.service.close()
|
|
237
|
+
super().stop()
|
|
238
|
+
|