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.
Files changed (338) hide show
  1. fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
  2. fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
  3. fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
  4. fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
  5. fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
  6. fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
  7. node/RELEASE +1 -0
  8. node/__init__.py +35 -0
  9. node/app/__init__.py +49 -0
  10. node/app/commands.py +265 -0
  11. node/app/config.py +131 -0
  12. node/app/control.py +670 -0
  13. node/app/core.py +867 -0
  14. node/app/plugins.py +76 -0
  15. node/app/router.py +83 -0
  16. node/app/socket.py +431 -0
  17. node/app/systemd.py +36 -0
  18. node/app/views.py +204 -0
  19. node/channels/__init__.py +378 -0
  20. node/channels/bus.py +331 -0
  21. node/channels/journal.py +46 -0
  22. node/commands/__init__.py +140 -0
  23. node/commands/adapter.py +210 -0
  24. node/commands/base.py +278 -0
  25. node/commands/cluster_cmds.py +183 -0
  26. node/commands/compute_cmds.py +401 -0
  27. node/commands/core.py +474 -0
  28. node/commands/gossip_cmds.py +106 -0
  29. node/commands/nsite_cmds.py +141 -0
  30. node/commands/peering.py +402 -0
  31. node/commands/plugin_cmds.py +135 -0
  32. node/commands/sidecar_cmds.py +120 -0
  33. node/commands/sync.py +388 -0
  34. node/commands/transport_cmds.py +451 -0
  35. node/commands/video_cmds.py +99 -0
  36. node/compartment/__init__.py +124 -0
  37. node/compartment/base.py +868 -0
  38. node/compartment/command.py +458 -0
  39. node/compartment/context.py +163 -0
  40. node/compartment/every.py +108 -0
  41. node/compartment/handler.py +131 -0
  42. node/compartment/lifecycle.py +460 -0
  43. node/compartment/lint.py +388 -0
  44. node/compartment/need.py +158 -0
  45. node/compartment/problem.py +521 -0
  46. node/compartment/refusals.py +120 -0
  47. node/compartment/registry.py +790 -0
  48. node/compartment/schema.py +290 -0
  49. node/compartment/sidecar.py +499 -0
  50. node/compartment/stores.py +662 -0
  51. node/compartment/wire.py +225 -0
  52. node/compartment/worker.py +61 -0
  53. node/config/__init__.py +43 -0
  54. node/config/base.py +116 -0
  55. node/config/change.py +441 -0
  56. node/config/changestore.py +201 -0
  57. node/config/compartments.py +1144 -0
  58. node/config/node.py +318 -0
  59. node/config/parameter.py +563 -0
  60. node/config/protocols.py +274 -0
  61. node/config/records.py +171 -0
  62. node/examples/__init__.py +1 -0
  63. node/examples/demo.py +226 -0
  64. node/examples/follower.py +46 -0
  65. node/interfaces.py +235 -0
  66. node/keystore/__init__.py +27 -0
  67. node/keystore/core.py +103 -0
  68. node/keystore/enclave.py +688 -0
  69. node/meshnode.py +111 -0
  70. node/metrics/__init__.py +396 -0
  71. node/metrics/adapters.py +102 -0
  72. node/metrics/face.py +110 -0
  73. node/planes/__init__.py +21 -0
  74. node/planes/identity/__init__.py +20 -0
  75. node/planes/identity/announce/__init__.py +9 -0
  76. node/planes/identity/announce/compartment.py +447 -0
  77. node/planes/identity/announce/types.py +26 -0
  78. node/planes/identity/discovery/__init__.py +9 -0
  79. node/planes/identity/discovery/compartment.py +522 -0
  80. node/planes/identity/discovery/types.py +13 -0
  81. node/planes/identity/linked/__init__.py +1 -0
  82. node/planes/identity/linked/compartment.py +270 -0
  83. node/planes/identity/linked/types.py +25 -0
  84. node/planes/identity/presence/__init__.py +7 -0
  85. node/planes/identity/presence/compartment.py +409 -0
  86. node/planes/identity/presence/types.py +14 -0
  87. node/planes/operations/__init__.py +13 -0
  88. node/planes/operations/scheduler/__init__.py +9 -0
  89. node/planes/operations/scheduler/compartment.py +353 -0
  90. node/planes/operations/scheduler/types.py +21 -0
  91. node/planes/operations/updates/__init__.py +17 -0
  92. node/planes/operations/updates/core.py +310 -0
  93. node/planes/operations/updates/store.py +79 -0
  94. node/planes/operations/updates/types.py +20 -0
  95. node/planes/peering/__init__.py +28 -0
  96. node/planes/peering/budget/__init__.py +43 -0
  97. node/planes/peering/budget/chain.py +43 -0
  98. node/planes/peering/budget/core.py +537 -0
  99. node/planes/peering/budget/evaluation.py +80 -0
  100. node/planes/peering/budget/meter.py +100 -0
  101. node/planes/peering/budget/receipts.py +439 -0
  102. node/planes/peering/budget/shape.py +101 -0
  103. node/planes/peering/budget/tick.py +110 -0
  104. node/planes/peering/budget/types.py +67 -0
  105. node/planes/peering/budget/windows.py +346 -0
  106. node/planes/peering/capacity/__init__.py +7 -0
  107. node/planes/peering/capacity/compartment.py +342 -0
  108. node/planes/peering/capacity/types.py +24 -0
  109. node/planes/peering/peering/__init__.py +25 -0
  110. node/planes/peering/peering/core.py +464 -0
  111. node/planes/peering/peering/envelope.py +142 -0
  112. node/planes/peering/peering/handshake.py +198 -0
  113. node/planes/peering/peering/shape.py +80 -0
  114. node/planes/peering/peering/types.py +39 -0
  115. node/planes/peering/reporter/__init__.py +42 -0
  116. node/planes/peering/reporter/constants.py +29 -0
  117. node/planes/peering/reporter/core.py +299 -0
  118. node/planes/peering/reporter/finding.py +74 -0
  119. node/planes/peering/reporter/findings.py +171 -0
  120. node/planes/peering/reporter/publish.py +285 -0
  121. node/planes/peering/reporter/reading.py +193 -0
  122. node/planes/peering/reporter/shape.py +86 -0
  123. node/planes/peering/reporter/types.py +50 -0
  124. node/planes/peering/strain/__init__.py +9 -0
  125. node/planes/peering/strain/compartment.py +204 -0
  126. node/planes/peering/strain/types.py +21 -0
  127. node/planes/peering/trust/__init__.py +9 -0
  128. node/planes/peering/trust/compartment.py +239 -0
  129. node/planes/peering/trust/types.py +30 -0
  130. node/planes/services/__init__.py +33 -0
  131. node/planes/services/compute/__init__.py +160 -0
  132. node/planes/services/compute/common.py +134 -0
  133. node/planes/services/compute/compartment.py +681 -0
  134. node/planes/services/compute/executors/__init__.py +121 -0
  135. node/planes/services/compute/executors/agent.py +840 -0
  136. node/planes/services/compute/executors/bacalhau.py +605 -0
  137. node/planes/services/compute/executors/base.py +127 -0
  138. node/planes/services/compute/executors/harness.py +439 -0
  139. node/planes/services/compute/executors/mcp.py +454 -0
  140. node/planes/services/compute/executors/media.py +898 -0
  141. node/planes/services/compute/executors/nondet.py +283 -0
  142. node/planes/services/compute/executors/providers.py +499 -0
  143. node/planes/services/compute/executors/registry.py +140 -0
  144. node/planes/services/compute/executors/wasm.py +244 -0
  145. node/planes/services/compute/executors/zk.py +604 -0
  146. node/planes/services/compute/jobs.py +227 -0
  147. node/planes/services/compute/load.py +180 -0
  148. node/planes/services/compute/market.py +433 -0
  149. node/planes/services/compute/sandbox.py +282 -0
  150. node/planes/services/compute/types.py +84 -0
  151. node/planes/services/compute_wire/__init__.py +58 -0
  152. node/planes/services/compute_wire/compartment.py +256 -0
  153. node/planes/services/compute_wire/messages.py +320 -0
  154. node/planes/services/compute_wire/sealing.py +147 -0
  155. node/planes/services/compute_wire/session.py +572 -0
  156. node/planes/services/compute_wire/types.py +49 -0
  157. node/planes/services/compute_wire/verify.py +331 -0
  158. node/planes/services/dvm/__init__.py +24 -0
  159. node/planes/services/dvm/gateway.py +526 -0
  160. node/planes/services/dvm/protocol.py +35 -0
  161. node/planes/services/dvm/translate.py +15 -0
  162. node/planes/services/dvm/types.py +25 -0
  163. node/planes/services/gateway/__init__.py +40 -0
  164. node/planes/services/gateway/settle.py +279 -0
  165. node/planes/services/gateway/translate.py +644 -0
  166. node/planes/services/gossip/__init__.py +22 -0
  167. node/planes/services/gossip/core.py +929 -0
  168. node/planes/services/gossip/envelope.py +154 -0
  169. node/planes/services/gossip/mesh.py +127 -0
  170. node/planes/services/gossip/types.py +50 -0
  171. node/planes/services/nsite/__init__.py +7 -0
  172. node/planes/services/nsite/client.py +62 -0
  173. node/planes/services/nsite/compartment.py +752 -0
  174. node/planes/services/nsite/types.py +44 -0
  175. node/planes/services/paidfetch/__init__.py +7 -0
  176. node/planes/services/paidfetch/compartment.py +862 -0
  177. node/planes/services/paidfetch/types.py +99 -0
  178. node/planes/services/settle/__init__.py +27 -0
  179. node/planes/services/settle/compartment.py +384 -0
  180. node/planes/services/settle/engine.py +456 -0
  181. node/planes/services/settle/gateways.py +541 -0
  182. node/planes/services/settle/payment.py +512 -0
  183. node/planes/services/settle/store.py +74 -0
  184. node/planes/services/settle/types.py +58 -0
  185. node/planes/services/sidecars/__init__.py +8 -0
  186. node/planes/services/sidecars/compartment.py +461 -0
  187. node/planes/services/sidecars/manifest.py +304 -0
  188. node/planes/services/sidecars/manifests/10-grasp.json +37 -0
  189. node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
  190. node/planes/services/sidecars/manifests/30-magpie.json +28 -0
  191. node/planes/services/sidecars/types.py +43 -0
  192. node/planes/services/video/__init__.py +106 -0
  193. node/planes/services/video/barter.py +407 -0
  194. node/planes/services/video/cli.py +158 -0
  195. node/planes/services/video/compartment.py +932 -0
  196. node/planes/services/video/ingest.py +524 -0
  197. node/planes/services/video/ledger.py +705 -0
  198. node/planes/services/video/packager.py +847 -0
  199. node/planes/services/video/player.py +456 -0
  200. node/planes/services/video/timed_segments.py +124 -0
  201. node/planes/services/video/transit.py +478 -0
  202. node/planes/store/__init__.py +24 -0
  203. node/planes/store/blob_replication/__init__.py +7 -0
  204. node/planes/store/blob_replication/compartment.py +496 -0
  205. node/planes/store/blob_replication/types.py +35 -0
  206. node/planes/store/blob_wire/__init__.py +48 -0
  207. node/planes/store/blob_wire/client.py +57 -0
  208. node/planes/store/blob_wire/core.py +359 -0
  209. node/planes/store/blob_wire/intake.py +74 -0
  210. node/planes/store/blob_wire/messages.py +85 -0
  211. node/planes/store/projected/__init__.py +7 -0
  212. node/planes/store/projected/compartment.py +258 -0
  213. node/planes/store/projected/types.py +29 -0
  214. node/planes/store/retention/__init__.py +7 -0
  215. node/planes/store/retention/compartment.py +214 -0
  216. node/planes/store/retention/types.py +12 -0
  217. node/planes/store/sync/__init__.py +35 -0
  218. node/planes/store/sync/core.py +311 -0
  219. node/planes/store/sync/envelope.py +106 -0
  220. node/planes/store/sync/handover.py +206 -0
  221. node/planes/store/sync/initiator.py +128 -0
  222. node/planes/store/sync/responder.py +114 -0
  223. node/planes/store/sync/rules.py +179 -0
  224. node/planes/store/sync/shape.py +80 -0
  225. node/planes/store/sync/types.py +51 -0
  226. node/planes/transport/__init__.py +23 -0
  227. node/planes/transport/bridge/__init__.py +13 -0
  228. node/planes/transport/bridge/compartment.py +785 -0
  229. node/planes/transport/bridge/ratelimit.py +44 -0
  230. node/planes/transport/bridge/types.py +43 -0
  231. node/planes/transport/lan/__init__.py +9 -0
  232. node/planes/transport/lan/compartment.py +446 -0
  233. node/planes/transport/lan/types.py +25 -0
  234. node/planes/transport/link/__init__.py +7 -0
  235. node/planes/transport/link/compartment.py +257 -0
  236. node/planes/transport/link/types.py +27 -0
  237. node/planes/transport/mediation/__init__.py +45 -0
  238. node/planes/transport/mediation/compartment.py +238 -0
  239. node/planes/transport/mediation/leaf.py +107 -0
  240. node/planes/transport/mediation/messages.py +302 -0
  241. node/planes/transport/mediation/service.py +233 -0
  242. node/planes/transport/mediation/store.py +37 -0
  243. node/planes/transport/mediation/types.py +20 -0
  244. node/planes/transport/p2p/__init__.py +7 -0
  245. node/planes/transport/p2p/compartment.py +349 -0
  246. node/planes/transport/p2p/types.py +22 -0
  247. node/protocols/__init__.py +277 -0
  248. node/protocols/activitypub/__init__.py +56 -0
  249. node/protocols/activitypub/actor.py +230 -0
  250. node/protocols/activitypub/federation.py +490 -0
  251. node/protocols/activitypub/media.py +145 -0
  252. node/protocols/activitypub/models.py +286 -0
  253. node/protocols/activitypub/package.py +271 -0
  254. node/protocols/activitypub/store.py +564 -0
  255. node/protocols/arcade/__init__.py +26 -0
  256. node/protocols/atproto/__init__.py +13 -0
  257. node/protocols/atproto/firehose.py +126 -0
  258. node/protocols/atproto/metering.py +165 -0
  259. node/protocols/atproto/package.py +155 -0
  260. node/protocols/atproto/repo.py +250 -0
  261. node/protocols/atproto/xrpc.py +285 -0
  262. node/protocols/base.py +109 -0
  263. node/protocols/circles/__init__.py +29 -0
  264. node/protocols/dm/__init__.py +50 -0
  265. node/protocols/grasp/__init__.py +22 -0
  266. node/protocols/isotime.py +15 -0
  267. node/protocols/nostr/__init__.py +51 -0
  268. node/protocols/nostr/blobstore.py +383 -0
  269. node/protocols/nostr/blossom.py +627 -0
  270. node/protocols/nostr/client.py +377 -0
  271. node/protocols/nostr/descriptor.py +211 -0
  272. node/protocols/nostr/eventstore.py +304 -0
  273. node/protocols/nostr/package.py +182 -0
  274. node/protocols/nostr/peers.py +109 -0
  275. node/protocols/nostr/relayserver.py +656 -0
  276. node/protocols/p2panda/__init__.py +22 -0
  277. node/protocols/p2panda/identity.py +19 -0
  278. node/protocols/p2panda/package.py +236 -0
  279. node/protocols/p2panda/rung.py +152 -0
  280. node/protocols/p2panda/store.py +273 -0
  281. node/protocols/rhio/__init__.py +33 -0
  282. node/protocols/rhio/package.py +275 -0
  283. node/protocols/rhio/rung.py +136 -0
  284. node/protocols/rhio/store.py +1423 -0
  285. node/protocols/ssb/muxrpc.py +256 -0
  286. node/protocols/ssb/package.py +267 -0
  287. node/protocols/textile/__init__.py +111 -0
  288. node/protocols/textile/transducers.py +259 -0
  289. node/release.py +119 -0
  290. node/storage/__init__.py +88 -0
  291. node/storage/accounts.py +305 -0
  292. node/storage/adapter.py +161 -0
  293. node/storage/base.py +385 -0
  294. node/storage/dialect.py +209 -0
  295. node/storage/grants.py +317 -0
  296. node/storage/locator.py +150 -0
  297. node/storage/postgres.py +294 -0
  298. node/storage/projected.py +953 -0
  299. node/storage/sharding.py +448 -0
  300. node/storage/standing.py +568 -0
  301. node/testnet/__init__.py +9 -0
  302. node/testnet/cluster.py +731 -0
  303. node/testnet/fips_daemon.py +230 -0
  304. node/transport/__init__.py +94 -0
  305. node/transport/const.py +110 -0
  306. node/transport/didcomm_inbound.py +251 -0
  307. node/transport/dnstxt.py +169 -0
  308. node/transport/fips_rung/__init__.py +67 -0
  309. node/transport/fips_rung/core.py +666 -0
  310. node/transport/fips_rung/listener.py +96 -0
  311. node/transport/fips_rung/types.py +12 -0
  312. node/transport/host.py +334 -0
  313. node/transport/iroh_rung/__init__.py +29 -0
  314. node/transport/iroh_rung/core.py +477 -0
  315. node/transport/iroh_rung/quic.py +942 -0
  316. node/transport/iroh_rung/types.py +22 -0
  317. node/transport/ladder.py +145 -0
  318. node/transport/lineproto.py +49 -0
  319. node/transport/mdns.py +303 -0
  320. node/transport/netfetch.py +283 -0
  321. node/transport/relay_rung/__init__.py +23 -0
  322. node/transport/relay_rung/cooldown.py +87 -0
  323. node/transport/relay_rung/core.py +138 -0
  324. node/transport/relay_rung/inbound.py +161 -0
  325. node/transport/relay_rung/ledger.py +104 -0
  326. node/transport/relay_rung/listen.py +121 -0
  327. node/transport/relay_rung/outbound.py +181 -0
  328. node/transport/relay_rung/shape.py +71 -0
  329. node/transport/relay_rung/types.py +58 -0
  330. node/transport/relay_rung/wire.py +63 -0
  331. node/transport/router.py +761 -0
  332. node/transport/rung.py +276 -0
  333. node/transport/ssb_rung.py +166 -0
  334. node/transport/ws.py +502 -0
  335. sidecar/fleetmesh_announce.py +338 -0
  336. sidecar/fleetmesh_p2p.py +373 -0
  337. sidecar/fleetmesh_rhio.py +352 -0
  338. sidecar/fleetmesh_ssb.py +603 -0
@@ -0,0 +1,251 @@
1
+ """Opening an inbound DIDComm envelope: one place, every transport.
2
+
3
+ Four paths carry a JWE into this node - a gift wrap off a relay, an iroh
4
+ stream, an iroh QUIC stream, and a FIPS native flow - and until this module
5
+ each of them opened the envelope its own way. Three of the four were wrong in
6
+ the same direction: they took the plaintext's `from` as the sender's name
7
+ without anything that proved it. A frame that was never encrypted, written by
8
+ anyone who could reach the socket, arrived as a message from whoever it named,
9
+ and on two of the paths it arrived marked authenticated. The transport that
10
+ got it right was the relay one, which is the oldest, and its rule is the rule
11
+ here: the names must agree, and a name nobody proved is not a name.
12
+
13
+ What proves a sender is the ECDH-1PU agreement. didcomm.decrypt only succeeds
14
+ under authcrypt if the static key `skid` names took part in the key
15
+ agreement, so a decrypted authcrypt message was written by the holder of that
16
+ key. The plaintext's `from` is then required to name the same node, and on
17
+ the relay path the seal's signer too. Anoncrypt has no skid and therefore no
18
+ sender: the message is delivered with `authenticated=False` and its `from` is
19
+ recorded as a claim, which is what first contact is.
20
+
21
+ Anything that is not a JWE is refused. There is no plaintext path, not for
22
+ discovery and not for ping: a transport that wants an unauthenticated
23
+ protocol runs it beside DIDComm, not inside the queue that request() waits
24
+ on. The reason strings are the ones node/transport/ has always recorded, so an
25
+ operator who learned to read `dropped` reads the same words from every rung.
26
+
27
+ Replay. A JWE is a fixed string of bytes, and a direct rung (fips flow, iroh
28
+ stream, libp2p stream) carries it as-is: anyone who captured one can write
29
+ it to the socket again, and the agreement proves exactly what it proved the
30
+ first time - the holder of the key wrote this message, once. The relay rung
31
+ has always deduplicated by wrap id; the direct rungs had nothing. So each
32
+ direct rung holds an InboundGuard and hands it to open_inbound, which
33
+ refuses a message id it has already opened and a `created_time` outside the
34
+ skew window the schema tolerates. A message with no `created_time` is still
35
+ accepted (the id check alone covers it); the gift-wrap rung passes no guard
36
+ because a stored wrap is legitimately delivered days late and the wrap id
37
+ is already its replay key.
38
+ """
39
+ from __future__ import annotations
40
+
41
+ import collections
42
+ import json
43
+ import threading
44
+ import time
45
+ from typing import Any, Optional
46
+
47
+ from fleetmesh_core.identity import didcomm
48
+ from fleetmesh_core.wire import schema_files
49
+ from fleetmesh_core.wire.wire_types import DIDCommMessage
50
+ from fleetmesh_core.identity.did import node_of_did
51
+
52
+ # A DIDComm message must fit in one wrap fragment set; a transport that has
53
+ # no smaller framing limit of its own still refuses anything past this
54
+ # before it is parsed, so a peer cannot make json.loads build a giant object.
55
+ MAX_ENVELOPE_BYTES: int = 16 * 1024 * 1024
56
+
57
+ # How far a direct-rung message's `created_time` may sit from this node's
58
+ # clock, either way: the schema's one tolerance for judging a peer by time,
59
+ # read through ref like every other constant. (It used to be a path
60
+ # computed from __file__ with a silent 300s fallback; moving this module
61
+ # one directory down turned the window from 60s to 300s and nothing but
62
+ # the test noticed. A constant the schema owns is not read by path.)
63
+ FRESH_WINDOW_SECONDS: int = int(schema_files.load_constants()['grant']['max_clock_skew_seconds'])
64
+ # How many message ids a guard remembers. Past this the oldest is forgotten,
65
+ # which is safe because anything that old is also outside the fresh window.
66
+ SEEN_IDS_MAX: int = 4096
67
+
68
+
69
+ class InboundGuard:
70
+ """Per-rung replay and freshness state: a bounded id-LRU and a clock window.
71
+
72
+ One per transport instance (never module-level: two nodes in one
73
+ process must not share what they have seen). `refused` counts by reason
74
+ so an operator can tell a replaying peer from a drifting clock.
75
+ """
76
+
77
+ def __init__(self, window: int = FRESH_WINDOW_SECONDS, capacity: int = SEEN_IDS_MAX) -> None:
78
+ self.window: int = int(window)
79
+ self.capacity: int = int(capacity)
80
+ self._seen: collections.OrderedDict[str, None] = collections.OrderedDict()
81
+ self._lock = threading.Lock()
82
+ self.refused: collections.Counter[str] = collections.Counter()
83
+
84
+ def stale(self, created_time: Any, now: int) -> Optional[str]:
85
+ """None if `created_time` is absent or inside the window, else why not."""
86
+ if created_time is None:
87
+ return None
88
+ if isinstance(created_time, bool) or not isinstance(created_time, (int, float)):
89
+ self.refused['created_time'] += 1
90
+ return 'created_time is not a number'
91
+ delta = int(created_time) - int(now)
92
+ if abs(delta) > self.window:
93
+ self.refused['stale'] += 1
94
+ return 'stale: created_time is %ds %s this node\'s clock (window %ds)' % (
95
+ abs(delta), 'ahead of' if delta > 0 else 'behind', self.window)
96
+ return None
97
+
98
+ def first_time(self, message_id: str) -> bool:
99
+ """True and remembered if this id is new; False if it has been opened before."""
100
+ with self._lock:
101
+ if message_id in self._seen:
102
+ self._seen.move_to_end(message_id)
103
+ self.refused['replay'] += 1
104
+ return False
105
+ self._seen[message_id] = None
106
+ while len(self._seen) > self.capacity:
107
+ self._seen.popitem(last=False)
108
+ return True
109
+
110
+
111
+ class Inbound:
112
+ """One delivered message: the plaintext, who sent it, and how it was sealed."""
113
+
114
+ def __init__(
115
+ self,
116
+ message: DIDCommMessage,
117
+ sender_pk: str,
118
+ authenticated: bool,
119
+ wrap_id: str,
120
+ ) -> None:
121
+ self.message: DIDCommMessage = message
122
+ self.sender: str = sender_pk
123
+ self.authenticated: bool = authenticated # False for anoncrypt (first contact)
124
+ self.wrap_id: str = wrap_id
125
+
126
+ @property
127
+ def thid(self) -> Optional[str]:
128
+ return self.message.get('thid') or self.message.get('id')
129
+
130
+
131
+ def foreign(payload: Any) -> str:
132
+ """Why a decrypted payload is not one of ours, in a few words.
133
+
134
+ A protocol that shares kind 21059 says whose message it is the same way
135
+ DIDComm does, in a `type`, so that is what is named when there is one and
136
+ it is the counted half of the reason. Nothing else about the payload is
137
+ reported: it decrypted under this node's key, so it may be somebody's
138
+ private traffic that merely arrived at the wrong daemon.
139
+ """
140
+ if isinstance(payload, dict):
141
+ kind = payload.get('type')
142
+ if isinstance(kind, str) and kind.strip():
143
+ return 'another protocol: %s' % kind.strip()[:80]
144
+ members = ', '.join(sorted(str(k)[:16] for k in list(payload)[:6]))
145
+ return 'payload is not a JWE: an object carrying %s' % (members or 'nothing')
146
+ return 'payload is not a JWE: %s' % type(payload).__name__
147
+
148
+
149
+ def is_jwe(payload: Any) -> bool:
150
+ """The shape didcomm.decrypt will look at. Anything else is not tried."""
151
+ return isinstance(payload, dict) and 'protected' in payload and 'recipients' in payload
152
+
153
+
154
+ def _pubkey_or_none(name: Any) -> Optional[str]:
155
+ """did:nostr:<pk>[#frag] -> pk, or None. Never raises: a malformed name
156
+ from the wire is a reason to drop, not a way to end a reader thread."""
157
+ if not isinstance(name, str):
158
+ return None
159
+ try:
160
+ return node_of_did(name)
161
+ except (ValueError, AttributeError):
162
+ return None
163
+
164
+
165
+ def open_inbound(
166
+ payload: Any,
167
+ keystore: Any,
168
+ peers: Any,
169
+ wrap_id: str,
170
+ seal_pk: Optional[str] = None,
171
+ guard: Optional[InboundGuard] = None,
172
+ now: Optional[int] = None,
173
+ ) -> tuple[Optional[Inbound], Optional[str]]:
174
+ """Open one inbound envelope. Returns (Inbound, None) or (None, reason).
175
+
176
+ payload: the bytes off the wire, or the object a transport already parsed
177
+ (the relay path parses before it can tell a fragment from a whole).
178
+ seal_pk: on the relay path, the key that signed the seal, which every
179
+ other name must then equal. Transports without a seal pass None and the
180
+ agreement between `skid` and `from` is what is checked.
181
+ guard: a direct rung's InboundGuard. When given, a message id already
182
+ opened through it is refused as a replay, and a `created_time` outside
183
+ the guard's window of `now` (the rung's clock; time.time() if None) is
184
+ refused as stale. The relay rung passes none: its wrap id is the key.
185
+
186
+ Exactly one of the pair is None, so a caller writes
187
+ inbound, why = open_inbound(...)
188
+ if inbound is None: self._drop(why, wrap_id); return
189
+ and the reason it records is the same on every rung.
190
+ """
191
+ if isinstance(payload, (bytes, bytearray, memoryview)):
192
+ raw = bytes(payload)
193
+ if len(raw) > MAX_ENVELOPE_BYTES:
194
+ return None, 'envelope: %d bytes is larger than this node opens' % len(raw)
195
+ try:
196
+ payload = json.loads(raw.decode('utf-8'))
197
+ except (ValueError, UnicodeDecodeError):
198
+ return None, 'payload is not a JWE: not JSON'
199
+ if not is_jwe(payload):
200
+ # Named apart from malformed traffic when it can be, because on a
201
+ # shared kind this is somebody else's healthy protocol. On a direct
202
+ # transport it is somebody sending plaintext, which is refused for
203
+ # the reason in the module docstring: nothing about it is proven.
204
+ return None, foreign(payload)
205
+ try:
206
+ message, skid = didcomm.decrypt(payload, keystore.kid('agreement'), keystore.agreement_priv,
207
+ resolve_sender=peers.resolve_sender)
208
+ except didcomm.EnvelopeError as e:
209
+ return None, 'envelope: %s' % e
210
+ except Exception as e: # a resolver that raised, a header of the wrong type
211
+ return None, 'envelope: %s: %s' % (type(e).__name__, e)
212
+ if (not isinstance(message, dict) or not isinstance(message.get('type'), str)
213
+ or not isinstance(message.get('id'), str)):
214
+ return None, 'plaintext is not a DIDComm message'
215
+ claimed = _pubkey_or_none(message.get('from', ''))
216
+ if claimed is None:
217
+ return None, 'from is not a did:nostr'
218
+ signer: Optional[str] = None
219
+ if skid is not None:
220
+ signer = _pubkey_or_none(skid)
221
+ if signer is None:
222
+ return None, 'skid is not a did:nostr'
223
+ # The names must agree. With a seal there are three; without one, the
224
+ # skid the agreement proved is the anchor and `from` must match it.
225
+ if seal_pk is not None:
226
+ if claimed != seal_pk:
227
+ return None, 'from %s does not match the seal signer %s' % (claimed[:8], seal_pk[:8])
228
+ if signer is not None and signer != seal_pk:
229
+ return None, 'skid does not match the seal signer'
230
+ sender = seal_pk
231
+ elif signer is not None:
232
+ if claimed != signer:
233
+ return None, 'from %s does not match the skid %s' % (claimed[:8], signer[:8])
234
+ sender = signer
235
+ else:
236
+ # Anoncrypt on a transport with no seal: nobody proved anything, so
237
+ # what is recorded as the sender is the claim, and it says so.
238
+ sender = claimed
239
+ raw_thid: Any = message.get('thid')
240
+ if raw_thid is not None and not isinstance(raw_thid, str):
241
+ return None, 'thid is not a string'
242
+ if guard is not None:
243
+ # Freshness first, then the id: a stale message is refused without
244
+ # being remembered, so a flood of old captures cannot push live ids
245
+ # out of the LRU.
246
+ why = guard.stale(message.get('created_time'), int(time.time()) if now is None else now)
247
+ if why is not None:
248
+ return None, why
249
+ if not guard.first_time(message['id']):
250
+ return None, 'replay: message %s was already opened' % message['id'][:32]
251
+ return Inbound(message, sender, authenticated=signer is not None, wrap_id=wrap_id), None
@@ -0,0 +1,169 @@
1
+ """A minimal DNS TXT lookup, because the standard library has none.
2
+
3
+ The nsite gateway binds a custom domain to a site through a TXT record at
4
+ `_nsite.<domain>`. `socket` can resolve A/AAAA, not TXT, and pulling in a
5
+ DNS library for one record type is not worth a dependency: this is a single
6
+ UDP query (RFC 1035, with the RFC 1123 TCP fallback when the answer is
7
+ truncated), to the resolvers `/etc/resolv.conf` names, following CNAMEs in
8
+ the answer section.
9
+
10
+ Returned strings are the TXT character-strings of one record joined, one
11
+ entry per record, decoded as UTF-8 with errors replaced. The caller decides
12
+ what a value means.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import random
18
+ import socket
19
+ import struct
20
+ from typing import Callable, Optional, Sequence
21
+
22
+ TYPE_TXT = 16
23
+ TYPE_CNAME = 5
24
+ CLASS_IN = 1
25
+ MAX_LABEL = 63
26
+ MAX_NAME = 253
27
+
28
+ Resolver = Callable[[str], list[str]]
29
+
30
+
31
+ class DnsError(Exception):
32
+ """The lookup could not be completed (network, malformed answer)."""
33
+
34
+
35
+ def system_nameservers(path: str = '/etc/resolv.conf') -> list[str]:
36
+ out: list[str] = []
37
+ try:
38
+ with open(path, encoding='utf-8', errors='replace') as f:
39
+ for line in f:
40
+ parts = line.split()
41
+ if len(parts) >= 2 and parts[0] == 'nameserver':
42
+ out.append(parts[1])
43
+ except OSError:
44
+ pass
45
+ return out or ['127.0.0.53', '1.1.1.1']
46
+
47
+
48
+ def _encode_name(name: str) -> bytes:
49
+ name = name.rstrip('.')
50
+ if len(name) > MAX_NAME:
51
+ raise DnsError('name too long')
52
+ out = b''
53
+ for label in name.split('.'):
54
+ raw = label.encode('idna') if label else b''
55
+ if not raw or len(raw) > MAX_LABEL:
56
+ raise DnsError('bad label in %r' % name)
57
+ out += bytes([len(raw)]) + raw
58
+ return out + b'\x00'
59
+
60
+
61
+ def _read_name(msg: bytes, off: int, depth: int = 0) -> tuple[str, int]:
62
+ labels: list[str] = []
63
+ jumped_to: Optional[int] = None
64
+ while True:
65
+ if off >= len(msg):
66
+ raise DnsError('truncated name')
67
+ ln = msg[off]
68
+ if ln & 0xC0 == 0xC0:
69
+ if depth > 8:
70
+ raise DnsError('pointer loop')
71
+ ptr = struct.unpack('!H', msg[off:off + 2])[0] & 0x3FFF
72
+ name, _ = _read_name(msg, ptr, depth + 1)
73
+ labels.append(name)
74
+ return '.'.join(x for x in labels if x), (jumped_to if jumped_to is not None else off + 2)
75
+ off += 1
76
+ if ln == 0:
77
+ return '.'.join(labels), off
78
+ labels.append(msg[off:off + ln].decode('ascii', errors='replace'))
79
+ off += ln
80
+
81
+
82
+ def _query(name: str, server: str, timeout: float, tcp: bool = False) -> bytes:
83
+ qid = random.randrange(0, 1 << 16)
84
+ header = struct.pack('!HHHHHH', qid, 0x0100, 1, 0, 0, 0) # RD set
85
+ q = header + _encode_name(name) + struct.pack('!HH', TYPE_TXT, CLASS_IN)
86
+ fam = socket.AF_INET6 if ':' in server else socket.AF_INET
87
+ if tcp:
88
+ with socket.socket(fam, socket.SOCK_STREAM) as s:
89
+ s.settimeout(timeout)
90
+ s.connect((server, 53))
91
+ s.sendall(struct.pack('!H', len(q)) + q)
92
+ ln = s.recv(2)
93
+ if len(ln) < 2:
94
+ raise DnsError('short tcp answer')
95
+ want = struct.unpack('!H', ln)[0]
96
+ buf = b''
97
+ while len(buf) < want:
98
+ chunk = s.recv(want - len(buf))
99
+ if not chunk:
100
+ break
101
+ buf += chunk
102
+ resp = buf
103
+ else:
104
+ with socket.socket(fam, socket.SOCK_DGRAM) as s:
105
+ s.settimeout(timeout)
106
+ s.sendto(q, (server, 53))
107
+ resp, _ = s.recvfrom(4096)
108
+ if len(resp) < 12 or struct.unpack('!H', resp[:2])[0] != qid:
109
+ raise DnsError('answer id mismatch')
110
+ return resp
111
+
112
+
113
+ def _parse_txt(resp: bytes, name: str) -> list[str]:
114
+ _, flags, qd, an, _, _ = struct.unpack('!HHHHHH', resp[:12])
115
+ rcode = flags & 0xF
116
+ if rcode == 3:
117
+ return [] # NXDOMAIN: no such name, not an error
118
+ if rcode != 0:
119
+ raise DnsError('rcode %d' % rcode)
120
+ if flags & 0x0200:
121
+ raise _Truncated()
122
+ off = 12
123
+ for _ in range(qd):
124
+ _, off = _read_name(resp, off)
125
+ off += 4
126
+ wanted = {name.rstrip('.').lower()}
127
+ out: list[str] = []
128
+ for _ in range(an):
129
+ rname, off = _read_name(resp, off)
130
+ rtype, _, _, rdlen = struct.unpack('!HHIH', resp[off:off + 10])
131
+ off += 10
132
+ rdata = resp[off:off + rdlen]
133
+ off += rdlen
134
+ if rname.lower() not in wanted:
135
+ continue
136
+ if rtype == TYPE_CNAME:
137
+ target, _ = _read_name(resp, off - rdlen)
138
+ wanted.add(target.lower())
139
+ elif rtype == TYPE_TXT:
140
+ i, parts = 0, []
141
+ while i < len(rdata):
142
+ ln = rdata[i]
143
+ parts.append(rdata[i + 1:i + 1 + ln])
144
+ i += 1 + ln
145
+ out.append(b''.join(parts).decode('utf-8', errors='replace'))
146
+ return out
147
+
148
+
149
+ class _Truncated(Exception):
150
+ pass
151
+
152
+
153
+ def lookup_txt(name: str, servers: Optional[Sequence[str]] = None, timeout: float = 3.0) -> list[str]:
154
+ """TXT records at `name`. Empty list = the name has none (incl. NXDOMAIN).
155
+ Raises DnsError when no resolver answered usably."""
156
+ servers = list(servers) if servers else system_nameservers()
157
+ if os.environ.get('FLEETMESH_DNS_SERVERS'):
158
+ servers = os.environ['FLEETMESH_DNS_SERVERS'].split(',')
159
+ last: Optional[Exception] = None
160
+ for server in servers:
161
+ try:
162
+ try:
163
+ return _parse_txt(_query(name, server, timeout), name)
164
+ except _Truncated:
165
+ return _parse_txt(_query(name, server, timeout, tcp=True), name)
166
+ except (OSError, DnsError, struct.error) as e:
167
+ last = e
168
+ continue
169
+ raise DnsError('no resolver answered for %s: %s' % (name, last))
@@ -0,0 +1,67 @@
1
+ """FIPS Native Datagram API transport rung package.
2
+
3
+ Exports the client, framing codecs, reassembly engine, and listener mixin.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from .types import FipsArrivalInfo
8
+ from .core import (
9
+ DEFAULT_MAX_PAYLOAD,
10
+ DEFAULT_SOCKET_PATHS,
11
+ FLAG_ACK_REQUESTED,
12
+ FLAG_COMPRESSED,
13
+ FLAG_DIDCOMM,
14
+ FLAG_FINAL,
15
+ FRAME_MAGIC,
16
+ FRAMING_MAGIC,
17
+ FRAMING_VERSION,
18
+ HEADER_FORMAT,
19
+ HEADER_SIZE,
20
+ MAX_FRAGMENTS,
21
+ PORT_ACCOUNTING,
22
+ PORT_COMPUTE_CANCEL,
23
+ PORT_COMPUTE_MARKET,
24
+ PORT_COMPUTE_STREAM,
25
+ PORT_PEERING,
26
+ PORT_TELEMETRY,
27
+ REASSEMBLY_TIMEOUT_S,
28
+ FipsNativeTransport,
29
+ FipsTransportError,
30
+ Reassembler,
31
+ decode_frame,
32
+ decode_frame_ex,
33
+ encode_frame,
34
+ find_api_socket,
35
+ )
36
+ from .listener import FipsRungMixin
37
+
38
+ __all__ = [
39
+ 'FipsArrivalInfo',
40
+ 'FipsNativeTransport',
41
+ 'FipsRungMixin',
42
+ 'FipsTransportError',
43
+ 'Reassembler',
44
+ 'encode_frame',
45
+ 'decode_frame',
46
+ 'decode_frame_ex',
47
+ 'find_api_socket',
48
+ 'FLAG_FINAL',
49
+ 'FLAG_COMPRESSED',
50
+ 'FLAG_DIDCOMM',
51
+ 'FLAG_ACK_REQUESTED',
52
+ 'FRAMING_MAGIC',
53
+ 'FRAMING_VERSION',
54
+ 'FRAME_MAGIC',
55
+ 'HEADER_FORMAT',
56
+ 'HEADER_SIZE',
57
+ 'PORT_PEERING',
58
+ 'PORT_TELEMETRY',
59
+ 'PORT_ACCOUNTING',
60
+ 'PORT_COMPUTE_MARKET',
61
+ 'PORT_COMPUTE_STREAM',
62
+ 'PORT_COMPUTE_CANCEL',
63
+ 'DEFAULT_SOCKET_PATHS',
64
+ 'DEFAULT_MAX_PAYLOAD',
65
+ 'MAX_FRAGMENTS',
66
+ 'REASSEMBLY_TIMEOUT_S',
67
+ ]