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,107 @@
|
|
|
1
|
+
"""MobileLeafClient: the leaf side. Cold-start mediator discovery over
|
|
2
|
+
kind 11801 descriptors under the operator-independence rule, the
|
|
3
|
+
mediate-request it sends, the grant it records, and the forward it wraps
|
|
4
|
+
an inner envelope in."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Mapping, Sequence
|
|
11
|
+
|
|
12
|
+
from fleetmesh_core.wire.interfaces import NostrEvent
|
|
13
|
+
from fleetmesh_core.wire.wire_types import ForwardBody, Message
|
|
14
|
+
from node import keystore
|
|
15
|
+
from .messages import make_forward, make_mediate_request
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from .types import MediationGrantInfo
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MobileLeafClient:
|
|
22
|
+
"""Mobile Leaf client implementing mediation negotiation and cold-start discovery."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, ks: keystore.Keystore) -> None:
|
|
25
|
+
self.ks: keystore.Keystore = ks
|
|
26
|
+
self.did: str = f"did:nostr:{ks.node_pk}"
|
|
27
|
+
self.mediators: dict[str, MediationGrantInfo] = {} # mediator_did -> grant info
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def discover_mediators(
|
|
31
|
+
descriptors: Sequence[NostrEvent],
|
|
32
|
+
required_count: int = 2,
|
|
33
|
+
require_distinct_operators: bool = True,
|
|
34
|
+
) -> list[NostrEvent]:
|
|
35
|
+
"""Select independent mediator anchors per 3-Layer Operator Independence Model.
|
|
36
|
+
|
|
37
|
+
Spec reference: spec.html lines 3332-3348 (Cold-Start Mediator Discovery via NIP-66).
|
|
38
|
+
Filters for:
|
|
39
|
+
1. Descriptor carries ['c', 'mediator'] capability hint.
|
|
40
|
+
2. Carries WebSocket endpoint (wss:// or ws://).
|
|
41
|
+
3. Selected anchors have distinct 'operator' pubkeys.
|
|
42
|
+
"""
|
|
43
|
+
candidates: list[NostrEvent] = []
|
|
44
|
+
for ev in descriptors:
|
|
45
|
+
if ev.get('kind') != 11801:
|
|
46
|
+
continue
|
|
47
|
+
tags = ev.get('tags', [])
|
|
48
|
+
tag_map = {t[0]: [row[1:] for row in tags if row and row[0] == t[0]] for t in tags if t}
|
|
49
|
+
|
|
50
|
+
# Check mediator capability
|
|
51
|
+
c_tags = [c[0] for c in tag_map.get('c', []) if c]
|
|
52
|
+
if 'mediator' not in c_tags:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
# Check endpoint
|
|
56
|
+
eps = tag_map.get('endpoint', [])
|
|
57
|
+
has_ws = any(e and e[0] in ('wss', 'ws') for e in eps)
|
|
58
|
+
if not has_ws:
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
candidates.append(ev)
|
|
62
|
+
|
|
63
|
+
# Select independent anchors by operator
|
|
64
|
+
selected: list[NostrEvent] = []
|
|
65
|
+
seen_operators: set[str] = set()
|
|
66
|
+
|
|
67
|
+
for cand in candidates:
|
|
68
|
+
# The comprehension above builds a different map (tag -> rows), so
|
|
69
|
+
# this one gets its own name rather than reusing `tags`.
|
|
70
|
+
first_values = {t[0]: t[1] for t in cand.get('tags', []) if len(t) > 1}
|
|
71
|
+
op = first_values.get('operator') or cand.get('pubkey')
|
|
72
|
+
|
|
73
|
+
if require_distinct_operators and op in seen_operators:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
seen_operators.add(op)
|
|
77
|
+
selected.append(cand)
|
|
78
|
+
if len(selected) >= required_count:
|
|
79
|
+
break
|
|
80
|
+
|
|
81
|
+
return selected
|
|
82
|
+
|
|
83
|
+
def build_mediate_request(self, mediator_did: str) -> Message[Mapping[str, Any]]:
|
|
84
|
+
return make_mediate_request(self.did, mediator_did)
|
|
85
|
+
|
|
86
|
+
def record_mediation_grant(self, grant_msg: Mapping[str, Any]) -> None:
|
|
87
|
+
med_did = grant_msg.get('from', '')
|
|
88
|
+
body = grant_msg.get('body', {})
|
|
89
|
+
self.mediators[med_did] = {
|
|
90
|
+
'routing_keys': body.get('routing_keys', []),
|
|
91
|
+
'endpoint': body.get('endpoint'),
|
|
92
|
+
'granted_at': int(time.time()),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def wrap_forward(self, inner_envelope_json: str, mediator_did: str, recipient_leaf_did: str) -> Message[ForwardBody]:
|
|
96
|
+
"""Wrap an inner encrypted payload into a routing/2.0 forward message."""
|
|
97
|
+
attachment = {
|
|
98
|
+
'id': 'att-' + os.urandom(8).hex(),
|
|
99
|
+
'data': {'json': json.loads(inner_envelope_json) if isinstance(inner_envelope_json, str) else inner_envelope_json},
|
|
100
|
+
}
|
|
101
|
+
return make_forward(
|
|
102
|
+
from_did=self.did,
|
|
103
|
+
to_did=mediator_did,
|
|
104
|
+
next_recipient=recipient_leaf_did,
|
|
105
|
+
attachments=[attachment],
|
|
106
|
+
)
|
|
107
|
+
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""The wire: PIURIs, bounds, the two guards every handler shares, and the
|
|
2
|
+
constructors for every coordinate-mediation/2.0, routing/2.0 and
|
|
3
|
+
messagepickup/2.0 message this node sends.
|
|
4
|
+
|
|
5
|
+
Nothing here touches storage or the node; it is the vocabulary the service
|
|
6
|
+
and the leaf client both speak.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from typing import Any, Mapping, Optional, Sequence
|
|
12
|
+
|
|
13
|
+
from fleetmesh_core.wire.wire_types import (
|
|
14
|
+
ForwardBody, KeylistUpdateBody, KeylistUpdateResponseBody, MediateDenyBody, MediateGrantBody,
|
|
15
|
+
Message, PickupDeliveryBody, PickupDeliveryRequestBody, PickupMessagesReceivedBody, PickupStatusBody,
|
|
16
|
+
PickupStatusRequestBody,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Protocol PIURIs
|
|
20
|
+
MEDIATE_REQUEST = 'https://didcomm.org/coordinate-mediation/2.0/mediate-request'
|
|
21
|
+
MEDIATE_GRANT = 'https://didcomm.org/coordinate-mediation/2.0/mediate-grant'
|
|
22
|
+
MEDIATE_DENY = 'https://didcomm.org/coordinate-mediation/2.0/mediate-deny'
|
|
23
|
+
KEYLIST_UPDATE = 'https://didcomm.org/coordinate-mediation/2.0/keylist-update'
|
|
24
|
+
KEYLIST_UPDATE_RESPONSE = 'https://didcomm.org/coordinate-mediation/2.0/keylist-update-response'
|
|
25
|
+
KEYLIST_QUERY = 'https://didcomm.org/coordinate-mediation/2.0/keylist-query'
|
|
26
|
+
KEYLIST_RESPONSE = 'https://didcomm.org/coordinate-mediation/2.0/keylist-response'
|
|
27
|
+
|
|
28
|
+
ROUTING_FORWARD = 'https://didcomm.org/routing/2.0/forward'
|
|
29
|
+
|
|
30
|
+
PICKUP_STATUS_REQUEST = 'https://didcomm.org/messagepickup/2.0/status-request'
|
|
31
|
+
PICKUP_STATUS = 'https://didcomm.org/messagepickup/2.0/status'
|
|
32
|
+
PICKUP_DELIVERY_REQUEST = 'https://didcomm.org/messagepickup/2.0/delivery-request'
|
|
33
|
+
PICKUP_DELIVERY = 'https://didcomm.org/messagepickup/2.0/delivery'
|
|
34
|
+
PICKUP_MESSAGES_RECEIVED = 'https://didcomm.org/messagepickup/2.0/messages-received'
|
|
35
|
+
|
|
36
|
+
# The prefixes the Mediation subsystem takes off the ProtocolRouter.
|
|
37
|
+
PIURIS = (
|
|
38
|
+
'https://didcomm.org/coordinate-mediation/2.0',
|
|
39
|
+
'https://didcomm.org/routing/2.0',
|
|
40
|
+
'https://didcomm.org/messagepickup/2.0',
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Bounds on one pickup delivery-request. `limit` is the leaf's own number and
|
|
44
|
+
# was taken as sent: 0 or a negative number is a LIMIT SQLite reads as "no
|
|
45
|
+
# limit", and a huge one is the whole inbox serialised into one reply. One
|
|
46
|
+
# drain per request, between one message and this many.
|
|
47
|
+
PICKUP_LIMIT_MIN: int = 1
|
|
48
|
+
PICKUP_LIMIT_MAX: int = 100
|
|
49
|
+
PICKUP_LIMIT_DEFAULT: int = 10
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def clamp_pickup_limit(raw: Any) -> int:
|
|
53
|
+
"""`limit` from a delivery-request as an int in [PICKUP_LIMIT_MIN, PICKUP_LIMIT_MAX]."""
|
|
54
|
+
try:
|
|
55
|
+
n = int(raw)
|
|
56
|
+
except (TypeError, ValueError):
|
|
57
|
+
n = PICKUP_LIMIT_DEFAULT
|
|
58
|
+
if isinstance(raw, bool):
|
|
59
|
+
n = PICKUP_LIMIT_DEFAULT
|
|
60
|
+
return max(PICKUP_LIMIT_MIN, min(PICKUP_LIMIT_MAX, n))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def leaf_of(msg: Mapping[str, Any], sender: Optional[str]) -> str:
|
|
64
|
+
"""The DID a mediation message is acting for.
|
|
65
|
+
|
|
66
|
+
`msg['from']` is a plaintext field the sender wrote. On its own it is a
|
|
67
|
+
claim, and every mediation handler used it as the identity: a leaf that
|
|
68
|
+
wrote another leaf's DID into `from` drained that leaf's inbox and
|
|
69
|
+
rewrote its keylist. The authenticated identity is the authcrypt `skid`
|
|
70
|
+
of the envelope the message arrived in, which the transport layer hands
|
|
71
|
+
down as `sender`. When it is given, `from` must agree with it; when it is
|
|
72
|
+
not (an operator command, a test), `from` is all there is and is used.
|
|
73
|
+
"""
|
|
74
|
+
claimed = msg.get('from', '') or ''
|
|
75
|
+
if sender:
|
|
76
|
+
if claimed and claimed != sender:
|
|
77
|
+
raise PermissionError('message from %s claims to be from %s' % (sender, claimed))
|
|
78
|
+
return sender
|
|
79
|
+
return claimed
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# Message Constructors
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
def make_mediate_request(
|
|
87
|
+
from_did: str,
|
|
88
|
+
to_did: str,
|
|
89
|
+
thid: Optional[str] = None,
|
|
90
|
+
) -> Message[Mapping[str, Any]]:
|
|
91
|
+
msg_id = 'med-req-' + os.urandom(8).hex()
|
|
92
|
+
msg: Message[Mapping[str, Any]] = {
|
|
93
|
+
'id': msg_id,
|
|
94
|
+
'type': MEDIATE_REQUEST,
|
|
95
|
+
'from': from_did,
|
|
96
|
+
'to': [to_did],
|
|
97
|
+
'thid': thid or msg_id,
|
|
98
|
+
'body': {},
|
|
99
|
+
}
|
|
100
|
+
return msg
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def make_mediate_grant(
|
|
104
|
+
from_did: str,
|
|
105
|
+
to_did: str,
|
|
106
|
+
routing_keys: Sequence[str],
|
|
107
|
+
endpoint: Optional[str] = None,
|
|
108
|
+
thid: Optional[str] = None,
|
|
109
|
+
) -> Message[MediateGrantBody]:
|
|
110
|
+
msg_id = 'med-grant-' + os.urandom(8).hex()
|
|
111
|
+
body: MediateGrantBody = {'routing_keys': list(routing_keys)}
|
|
112
|
+
if endpoint:
|
|
113
|
+
body['endpoint'] = endpoint
|
|
114
|
+
msg: Message[MediateGrantBody] = {
|
|
115
|
+
'id': msg_id,
|
|
116
|
+
'type': MEDIATE_GRANT,
|
|
117
|
+
'from': from_did,
|
|
118
|
+
'to': [to_did],
|
|
119
|
+
'thid': thid or msg_id,
|
|
120
|
+
'body': body,
|
|
121
|
+
}
|
|
122
|
+
return msg
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def make_mediate_deny(
|
|
126
|
+
from_did: str,
|
|
127
|
+
to_did: str,
|
|
128
|
+
reason: str = 'capacity',
|
|
129
|
+
thid: Optional[str] = None,
|
|
130
|
+
) -> Message[MediateDenyBody]:
|
|
131
|
+
msg_id = 'med-deny-' + os.urandom(8).hex()
|
|
132
|
+
msg: Message[MediateDenyBody] = {
|
|
133
|
+
'id': msg_id,
|
|
134
|
+
'type': MEDIATE_DENY,
|
|
135
|
+
'from': from_did,
|
|
136
|
+
'to': [to_did],
|
|
137
|
+
'thid': thid or msg_id,
|
|
138
|
+
'body': {'reason': reason},
|
|
139
|
+
}
|
|
140
|
+
return msg
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def make_keylist_update(
|
|
144
|
+
from_did: str,
|
|
145
|
+
to_did: str,
|
|
146
|
+
updates: Sequence[Mapping[str, str]],
|
|
147
|
+
thid: Optional[str] = None,
|
|
148
|
+
) -> Message[KeylistUpdateBody]:
|
|
149
|
+
msg_id = 'kl-upd-' + os.urandom(8).hex()
|
|
150
|
+
msg: Message[KeylistUpdateBody] = {
|
|
151
|
+
'id': msg_id,
|
|
152
|
+
'type': KEYLIST_UPDATE,
|
|
153
|
+
'from': from_did,
|
|
154
|
+
'to': [to_did],
|
|
155
|
+
'thid': thid or msg_id,
|
|
156
|
+
'body': {'updates': list(updates)},
|
|
157
|
+
}
|
|
158
|
+
return msg
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def make_keylist_update_response(
|
|
162
|
+
from_did: str,
|
|
163
|
+
to_did: str,
|
|
164
|
+
updated: Sequence[Mapping[str, str]],
|
|
165
|
+
thid: Optional[str] = None,
|
|
166
|
+
) -> Message[KeylistUpdateResponseBody]:
|
|
167
|
+
msg_id = 'kl-resp-' + os.urandom(8).hex()
|
|
168
|
+
msg: Message[KeylistUpdateResponseBody] = {
|
|
169
|
+
'id': msg_id,
|
|
170
|
+
'type': KEYLIST_UPDATE_RESPONSE,
|
|
171
|
+
'from': from_did,
|
|
172
|
+
'to': [to_did],
|
|
173
|
+
'thid': thid or msg_id,
|
|
174
|
+
'body': {'updated': list(updated)},
|
|
175
|
+
}
|
|
176
|
+
return msg
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def make_forward(
|
|
180
|
+
from_did: str,
|
|
181
|
+
to_did: str,
|
|
182
|
+
next_recipient: str,
|
|
183
|
+
attachments: Sequence[Mapping[str, Any]],
|
|
184
|
+
thid: Optional[str] = None,
|
|
185
|
+
) -> Message[ForwardBody]:
|
|
186
|
+
"""DIDComm routing/2.0 forward message wrapping an inner encrypted envelope."""
|
|
187
|
+
msg_id = 'fwd-' + os.urandom(8).hex()
|
|
188
|
+
msg: Message[ForwardBody] = {
|
|
189
|
+
'id': msg_id,
|
|
190
|
+
'type': ROUTING_FORWARD,
|
|
191
|
+
'from': from_did,
|
|
192
|
+
'to': [to_did],
|
|
193
|
+
'thid': thid or msg_id,
|
|
194
|
+
'body': {'next': next_recipient},
|
|
195
|
+
'attachments': list(attachments),
|
|
196
|
+
}
|
|
197
|
+
return msg
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def make_pickup_status_request(
|
|
201
|
+
from_did: str,
|
|
202
|
+
to_did: str,
|
|
203
|
+
recipient_key: Optional[str] = None,
|
|
204
|
+
thid: Optional[str] = None,
|
|
205
|
+
) -> Message[PickupStatusRequestBody]:
|
|
206
|
+
msg_id = 'pck-req-' + os.urandom(8).hex()
|
|
207
|
+
body: PickupStatusRequestBody = {}
|
|
208
|
+
if recipient_key:
|
|
209
|
+
body['recipient_key'] = recipient_key
|
|
210
|
+
msg: Message[PickupStatusRequestBody] = {
|
|
211
|
+
'id': msg_id,
|
|
212
|
+
'type': PICKUP_STATUS_REQUEST,
|
|
213
|
+
'from': from_did,
|
|
214
|
+
'to': [to_did],
|
|
215
|
+
'thid': thid or msg_id,
|
|
216
|
+
'body': body,
|
|
217
|
+
}
|
|
218
|
+
return msg
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def make_pickup_status(
|
|
222
|
+
from_did: str,
|
|
223
|
+
to_did: str,
|
|
224
|
+
message_count: int = 0,
|
|
225
|
+
recipient_key: Optional[str] = None,
|
|
226
|
+
thid: Optional[str] = None,
|
|
227
|
+
) -> Message[PickupStatusBody]:
|
|
228
|
+
msg_id = 'pck-stat-' + os.urandom(8).hex()
|
|
229
|
+
body: PickupStatusBody = {'message_count': message_count}
|
|
230
|
+
if recipient_key:
|
|
231
|
+
body['recipient_key'] = recipient_key
|
|
232
|
+
msg: Message[PickupStatusBody] = {
|
|
233
|
+
'id': msg_id,
|
|
234
|
+
'type': PICKUP_STATUS,
|
|
235
|
+
'from': from_did,
|
|
236
|
+
'to': [to_did],
|
|
237
|
+
'thid': thid or msg_id,
|
|
238
|
+
'body': body,
|
|
239
|
+
}
|
|
240
|
+
return msg
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def make_pickup_delivery_request(
|
|
244
|
+
from_did: str,
|
|
245
|
+
to_did: str,
|
|
246
|
+
limit: int = 10,
|
|
247
|
+
recipient_key: Optional[str] = None,
|
|
248
|
+
thid: Optional[str] = None,
|
|
249
|
+
) -> Message[PickupDeliveryRequestBody]:
|
|
250
|
+
msg_id = 'pck-del-req-' + os.urandom(8).hex()
|
|
251
|
+
body: PickupDeliveryRequestBody = {'limit': limit}
|
|
252
|
+
if recipient_key:
|
|
253
|
+
body['recipient_key'] = recipient_key
|
|
254
|
+
msg: Message[PickupDeliveryRequestBody] = {
|
|
255
|
+
'id': msg_id,
|
|
256
|
+
'type': PICKUP_DELIVERY_REQUEST,
|
|
257
|
+
'from': from_did,
|
|
258
|
+
'to': [to_did],
|
|
259
|
+
'thid': thid or msg_id,
|
|
260
|
+
'body': body,
|
|
261
|
+
}
|
|
262
|
+
return msg
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def make_pickup_delivery(
|
|
266
|
+
from_did: str,
|
|
267
|
+
to_did: str,
|
|
268
|
+
messages: Sequence[Mapping[str, Any]],
|
|
269
|
+
recipient_key: Optional[str] = None,
|
|
270
|
+
thid: Optional[str] = None,
|
|
271
|
+
) -> Message[PickupDeliveryBody]:
|
|
272
|
+
msg_id = 'pck-del-' + os.urandom(8).hex()
|
|
273
|
+
body: PickupDeliveryBody = {'messages': list(messages)}
|
|
274
|
+
if recipient_key:
|
|
275
|
+
body['recipient_key'] = recipient_key
|
|
276
|
+
msg: Message[PickupDeliveryBody] = {
|
|
277
|
+
'id': msg_id,
|
|
278
|
+
'type': PICKUP_DELIVERY,
|
|
279
|
+
'from': from_did,
|
|
280
|
+
'to': [to_did],
|
|
281
|
+
'thid': thid or msg_id,
|
|
282
|
+
'body': body,
|
|
283
|
+
}
|
|
284
|
+
return msg
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def make_pickup_messages_received(
|
|
288
|
+
from_did: str,
|
|
289
|
+
to_did: str,
|
|
290
|
+
message_id_list: Sequence[str],
|
|
291
|
+
thid: Optional[str] = None,
|
|
292
|
+
) -> Message[PickupMessagesReceivedBody]:
|
|
293
|
+
msg_id = 'pck-ack-' + os.urandom(8).hex()
|
|
294
|
+
msg: Message[PickupMessagesReceivedBody] = {
|
|
295
|
+
'id': msg_id,
|
|
296
|
+
'type': PICKUP_MESSAGES_RECEIVED,
|
|
297
|
+
'from': from_did,
|
|
298
|
+
'to': [to_did],
|
|
299
|
+
'thid': thid or msg_id,
|
|
300
|
+
'body': {'message_id_list': list(message_id_list)},
|
|
301
|
+
}
|
|
302
|
+
return msg
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""MediatorService: the anchor side. Admits leaves, keeps their keylists,
|
|
2
|
+
buffers forwarded envelopes and drains them on pickup, bounded.
|
|
3
|
+
|
|
4
|
+
Every handler identifies the leaf by `leaf_of(msg, sender)` - the proven
|
|
5
|
+
envelope sender, with `from` allowed only to agree - and acts only on that
|
|
6
|
+
leaf's own keys and inbox. See the docstrings on `_keys_of` and
|
|
7
|
+
`handle_messages_received` for the two cross-leaf reads that rule closed.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from typing import Any, Callable, Mapping, Optional, Sequence
|
|
16
|
+
|
|
17
|
+
from fleetmesh_core.wire.wire_types import KeylistUpdateResponseBody, MediateDenyBody, MediateGrantBody, Message, PickupDeliveryBody
|
|
18
|
+
from .messages import (PICKUP_LIMIT_DEFAULT, ROUTING_FORWARD, clamp_pickup_limit, leaf_of,
|
|
19
|
+
make_keylist_update_response, make_mediate_deny, make_mediate_grant,
|
|
20
|
+
make_pickup_delivery)
|
|
21
|
+
from .store import MediationStore
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
from .types import DeliveredMessage
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MediatorService:
|
|
28
|
+
"""Anchor-side mediation and store-and-forward buffer for mobile leaves."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
db_path: str = ':memory:',
|
|
33
|
+
mediator_did: str = '',
|
|
34
|
+
routing_keys: Optional[Sequence[str]] = None,
|
|
35
|
+
clock: Optional[Callable[[], int]] = None,
|
|
36
|
+
max_buffered_messages: int = 1000,
|
|
37
|
+
db: Optional[Any] = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
self.db_path: str = db_path
|
|
40
|
+
self.mediator_did: str = mediator_did
|
|
41
|
+
self.routing_keys: list[str] = list(routing_keys or ([mediator_did + '#agree'] if mediator_did else []))
|
|
42
|
+
self.clock: Callable[[], int] = clock or (lambda: int(time.time()))
|
|
43
|
+
self.max_buffered_messages: int = max_buffered_messages
|
|
44
|
+
self._lock: threading.RLock = threading.RLock()
|
|
45
|
+
self.db: Any = db if db is not None else MediationStore(db_path)
|
|
46
|
+
|
|
47
|
+
def close(self) -> None:
|
|
48
|
+
"""Safely close underlying SQLite persistence."""
|
|
49
|
+
self.db.close()
|
|
50
|
+
|
|
51
|
+
def handle_mediate_request(self, msg: Mapping[str, Any], sender: Optional[str] = None) -> Message[MediateGrantBody] | Message[MediateDenyBody]:
|
|
52
|
+
"""Admit leaf into mediation and grant routing keys.
|
|
53
|
+
|
|
54
|
+
`sender` is the authenticated envelope sender (see `leaf_of`)."""
|
|
55
|
+
leaf_did = leaf_of(msg, sender)
|
|
56
|
+
if not leaf_did:
|
|
57
|
+
return make_mediate_deny(self.mediator_did, leaf_did, reason='missing from DID', thid=msg.get('id'))
|
|
58
|
+
|
|
59
|
+
now = self.clock()
|
|
60
|
+
with self.db.transaction() as cur:
|
|
61
|
+
cur.execute('''
|
|
62
|
+
INSERT OR REPLACE INTO leaves (leaf_did, admitted_at, status)
|
|
63
|
+
VALUES (?, ?, 'active')
|
|
64
|
+
''', (leaf_did, now))
|
|
65
|
+
# Automatically register leaf_did as initial recipient_key
|
|
66
|
+
cur.execute('''
|
|
67
|
+
INSERT OR REPLACE INTO keylist (recipient_key, leaf_did, registered_at)
|
|
68
|
+
VALUES (?, ?, ?)
|
|
69
|
+
''', (leaf_did, leaf_did, now))
|
|
70
|
+
|
|
71
|
+
return make_mediate_grant(
|
|
72
|
+
from_did=self.mediator_did,
|
|
73
|
+
to_did=leaf_did,
|
|
74
|
+
routing_keys=self.routing_keys,
|
|
75
|
+
thid=msg.get('id'),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def handle_keylist_update(self, msg: Mapping[str, Any], sender: Optional[str] = None) -> Message[KeylistUpdateResponseBody]:
|
|
79
|
+
"""Update registered recipient keys for a leaf."""
|
|
80
|
+
leaf_did = leaf_of(msg, sender)
|
|
81
|
+
updates = msg.get('body', {}).get('updates', [])
|
|
82
|
+
updated_out: list[dict[str, str]] = []
|
|
83
|
+
now = self.clock()
|
|
84
|
+
|
|
85
|
+
with self.db.transaction() as cur:
|
|
86
|
+
for u in updates:
|
|
87
|
+
key = u.get('recipient_key')
|
|
88
|
+
action = u.get('action')
|
|
89
|
+
if not key:
|
|
90
|
+
continue
|
|
91
|
+
if action == 'add':
|
|
92
|
+
cur.execute('''
|
|
93
|
+
INSERT OR REPLACE INTO keylist (recipient_key, leaf_did, registered_at)
|
|
94
|
+
VALUES (?, ?, ?)
|
|
95
|
+
''', (key, leaf_did, now))
|
|
96
|
+
updated_out.append({'recipient_key': key, 'action': 'add', 'result': 'success'})
|
|
97
|
+
elif action == 'remove':
|
|
98
|
+
cur_res = cur.execute('''
|
|
99
|
+
DELETE FROM keylist WHERE recipient_key = ? AND leaf_did = ?
|
|
100
|
+
''', (key, leaf_did))
|
|
101
|
+
res = 'success' if cur_res.rowcount > 0 else 'no_change'
|
|
102
|
+
updated_out.append({'recipient_key': key, 'action': 'remove', 'result': res})
|
|
103
|
+
|
|
104
|
+
return make_keylist_update_response(
|
|
105
|
+
from_did=self.mediator_did,
|
|
106
|
+
to_did=leaf_did,
|
|
107
|
+
updated=updated_out,
|
|
108
|
+
thid=msg.get('id'),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def handle_forward(self, msg: Mapping[str, Any]) -> bool:
|
|
112
|
+
"""Buffer inbound forward envelope for delivery to leaf."""
|
|
113
|
+
if msg.get('type') != ROUTING_FORWARD:
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
next_recipient = msg.get('body', {}).get('next', '')
|
|
117
|
+
if not next_recipient:
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
# Find registered leaf and verify queue bound
|
|
121
|
+
with self.db.transaction() as cur:
|
|
122
|
+
cur.execute('SELECT leaf_did FROM keylist WHERE recipient_key = ?', (next_recipient,))
|
|
123
|
+
row = cur.fetchone()
|
|
124
|
+
if not row:
|
|
125
|
+
# Check if next_recipient is an admitted leaf_did directly
|
|
126
|
+
cur.execute('SELECT leaf_did FROM leaves WHERE leaf_did = ?', (next_recipient,))
|
|
127
|
+
leaf_row = cur.fetchone()
|
|
128
|
+
if not leaf_row:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
# Bound queue to avoid memory/storage exhaustion
|
|
132
|
+
cur.execute('SELECT COUNT(*) FROM inbox WHERE recipient_key = ? AND delivered = 0', (next_recipient,))
|
|
133
|
+
count_row = cur.fetchone()
|
|
134
|
+
current_count = int(count_row[0] or 0) if count_row else 0
|
|
135
|
+
if current_count >= self.max_buffered_messages:
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
# Store forwarded envelope attachments in inbox
|
|
139
|
+
attachments = msg.get('attachments', [])
|
|
140
|
+
payload_str = json.dumps(attachments, sort_keys=True)
|
|
141
|
+
msg_id = msg.get('id') or ('fwd-store-' + os.urandom(8).hex())
|
|
142
|
+
now = self.clock()
|
|
143
|
+
|
|
144
|
+
cur.execute('''
|
|
145
|
+
INSERT OR REPLACE INTO inbox (msg_id, recipient_key, sender_did, payload, enqueued_at, delivered)
|
|
146
|
+
VALUES (?, ?, ?, ?, ?, 0)
|
|
147
|
+
''', (msg_id, next_recipient, msg.get('from', ''), payload_str, now))
|
|
148
|
+
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
def get_queue_status(self, recipient_key: str) -> int:
|
|
152
|
+
row = self.db._one('SELECT COUNT(*) FROM inbox WHERE recipient_key = ? AND delivered = 0', (recipient_key,))
|
|
153
|
+
return int(row[0] or 0) if row else 0
|
|
154
|
+
|
|
155
|
+
def _keys_of(self, leaf_did: str) -> list[str]:
|
|
156
|
+
"""Every recipient_key registered to this leaf, and nothing else.
|
|
157
|
+
|
|
158
|
+
The leaf's own DID is here only because mediate-request registered it.
|
|
159
|
+
A `recipient_key` a leaf merely names in a request is NOT added, which
|
|
160
|
+
is what let one leaf ask for another leaf's messages."""
|
|
161
|
+
return [r[0] for r in self.db._all('SELECT recipient_key FROM keylist WHERE leaf_did = ?', (leaf_did,))]
|
|
162
|
+
|
|
163
|
+
def handle_pickup_delivery_request(self, msg: Mapping[str, Any], sender: Optional[str] = None) -> Message[PickupDeliveryBody]:
|
|
164
|
+
"""Drain queued forwarded envelopes for the requesting leaf."""
|
|
165
|
+
leaf_did = leaf_of(msg, sender)
|
|
166
|
+
limit = clamp_pickup_limit(msg.get('body', {}).get('limit', PICKUP_LIMIT_DEFAULT))
|
|
167
|
+
target_key = msg.get('body', {}).get('recipient_key') or leaf_did
|
|
168
|
+
|
|
169
|
+
messages_out: list[DeliveredMessage] = []
|
|
170
|
+
with self.db._lock:
|
|
171
|
+
keys = self._keys_of(leaf_did)
|
|
172
|
+
if target_key in keys:
|
|
173
|
+
keys = [target_key] # a named key narrows the drain to that key
|
|
174
|
+
if not keys:
|
|
175
|
+
return make_pickup_delivery(from_did=self.mediator_did, to_did=leaf_did, messages=[],
|
|
176
|
+
recipient_key=target_key, thid=msg.get('id'))
|
|
177
|
+
|
|
178
|
+
placeholders = ','.join('?' for _ in keys)
|
|
179
|
+
rows = self.db._all(f'''
|
|
180
|
+
SELECT msg_id, recipient_key, sender_did, payload, enqueued_at
|
|
181
|
+
FROM inbox WHERE recipient_key IN ({placeholders}) AND delivered = 0
|
|
182
|
+
ORDER BY enqueued_at ASC LIMIT ?
|
|
183
|
+
''', (*keys, limit))
|
|
184
|
+
|
|
185
|
+
for mid, rk, sender, payload_str, enq in rows:
|
|
186
|
+
try:
|
|
187
|
+
attachments = json.loads(payload_str)
|
|
188
|
+
except Exception:
|
|
189
|
+
attachments = []
|
|
190
|
+
messages_out.append({
|
|
191
|
+
'id': mid,
|
|
192
|
+
'recipient_key': rk,
|
|
193
|
+
'sender': sender,
|
|
194
|
+
'attachments': attachments,
|
|
195
|
+
'enqueued_at': enq,
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
return make_pickup_delivery(
|
|
199
|
+
from_did=self.mediator_did,
|
|
200
|
+
to_did=leaf_did,
|
|
201
|
+
messages=messages_out,
|
|
202
|
+
recipient_key=target_key,
|
|
203
|
+
thid=msg.get('id'),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def handle_messages_received(self, msg: Mapping[str, Any], sender: Optional[str] = None) -> int:
|
|
207
|
+
"""Mark picked-up messages as delivered and purge.
|
|
208
|
+
|
|
209
|
+
Only messages addressed to one of THIS leaf's registered keys are
|
|
210
|
+
purged. The DELETE was by msg_id alone, so a leaf that learned or
|
|
211
|
+
guessed another leaf's message ids could destroy that leaf's mail."""
|
|
212
|
+
leaf_did = leaf_of(msg, sender)
|
|
213
|
+
mids = [m for m in msg.get('body', {}).get('message_id_list', []) if isinstance(m, str)]
|
|
214
|
+
if not mids:
|
|
215
|
+
return 0
|
|
216
|
+
|
|
217
|
+
with self.db.transaction() as cur:
|
|
218
|
+
keys = [r[0] for r in cur.execute('SELECT recipient_key FROM keylist WHERE leaf_did = ?',
|
|
219
|
+
(leaf_did,)).fetchall()]
|
|
220
|
+
if not keys:
|
|
221
|
+
return 0
|
|
222
|
+
purged = 0
|
|
223
|
+
# Chunked so a long ack cannot exceed SQLite's bound-variable limit.
|
|
224
|
+
step = 400
|
|
225
|
+
for i in range(0, len(mids), step):
|
|
226
|
+
chunk = mids[i:i + step]
|
|
227
|
+
cur_del = cur.execute(f'''
|
|
228
|
+
DELETE FROM inbox WHERE msg_id IN ({','.join('?' for _ in chunk)})
|
|
229
|
+
AND recipient_key IN ({','.join('?' for _ in keys)})
|
|
230
|
+
''', (*chunk, *keys))
|
|
231
|
+
purged += cur_del.rowcount
|
|
232
|
+
return purged
|
|
233
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""SQLite persistence for mediation: admitted leaves, their keylists, and
|
|
2
|
+
the buffered inbox a leaf drains on pickup."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from node.compartment.schema import Column, Index, Table
|
|
6
|
+
from node.storage import SqliteStore
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MediationStore(SqliteStore):
|
|
10
|
+
"""Thread-safe SQLite store for mediation leaves, keylists, and buffered inboxes."""
|
|
11
|
+
|
|
12
|
+
SCHEMA = (
|
|
13
|
+
Table('leaves', (
|
|
14
|
+
Column('leaf_did', 'TEXT', primary_key=True),
|
|
15
|
+
Column('admitted_at', 'INTEGER', not_null=True),
|
|
16
|
+
Column('status', 'TEXT', not_null=True, default="'active'"),
|
|
17
|
+
)),
|
|
18
|
+
Table('keylist', (
|
|
19
|
+
Column('recipient_key', 'TEXT', primary_key=True),
|
|
20
|
+
Column('leaf_did', 'TEXT', not_null=True),
|
|
21
|
+
Column('registered_at', 'INTEGER', not_null=True),
|
|
22
|
+
), indices=(
|
|
23
|
+
Index('idx_keylist_leaf', ('leaf_did',)),
|
|
24
|
+
)),
|
|
25
|
+
Table('inbox', (
|
|
26
|
+
Column('msg_id', 'TEXT', primary_key=True),
|
|
27
|
+
Column('recipient_key', 'TEXT', not_null=True),
|
|
28
|
+
Column('sender_did', 'TEXT'),
|
|
29
|
+
Column('payload', 'TEXT', not_null=True),
|
|
30
|
+
Column('enqueued_at', 'INTEGER', not_null=True),
|
|
31
|
+
Column('delivered', 'INTEGER', not_null=True, default=0),
|
|
32
|
+
), indices=(
|
|
33
|
+
Index('idx_inbox_recip', ('recipient_key', 'delivered')),
|
|
34
|
+
)),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|