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,285 @@
1
+ """AT Protocol XRPC (XML-less RPC) server and endpoint router.
2
+
3
+ Standard library only. Routes XRPC queries (GET) and procedures (POST)
4
+ under `/xrpc/<nsid>`, handling core ATProto Lexicons:
5
+ - com.atproto.server.describeServer
6
+ - com.atproto.repo.describeRepo
7
+ - com.atproto.repo.getRecord
8
+ - com.atproto.repo.listRecords
9
+ - com.atproto.repo.createRecord
10
+ - com.atproto.repo.deleteRecord
11
+ - com.atproto.sync.getRepo
12
+ - com.atproto.sync.getLatestCommit
13
+ - com.atproto.sync.getBlob
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import re
19
+ import urllib.parse
20
+ from typing import Callable, Mapping, Optional
21
+
22
+ from fleetmesh_core.conformance.errors import FleetmeshError
23
+ from .repo import Repo
24
+
25
+ VALID_COLLECTION_RE = re.compile(r'^[a-zA-Z0-9.-]{1,512}$')
26
+ VALID_RKEY_RE = re.compile(r'^[a-zA-Z0-9._~-]{1,512}$')
27
+
28
+ __all__ = [
29
+ 'XRPCError',
30
+ 'XRPCRouter',
31
+ ]
32
+
33
+
34
+ class XRPCError(FleetmeshError):
35
+ """Represents an XRPC error with HTTP status and Lexicon error string."""
36
+
37
+ def __init__(self, status: int, error: str, message: str):
38
+ super().__init__(f"{error}: {message}")
39
+ self.status = status
40
+ self.error = error
41
+ self.message = message
42
+
43
+ def to_dict(self) -> dict[str, str]:
44
+ return {'error': self.error, 'message': self.message}
45
+
46
+
47
+ class XRPCRouter:
48
+ """Dispatches XRPC queries and procedures to registered Lexicon handlers."""
49
+
50
+ def __init__(
51
+ self,
52
+ repo: Optional[Repo] = None,
53
+ service_domain: str = "node.fleetmesh.local",
54
+ ):
55
+ self.repo = repo
56
+ self.service_domain = service_domain
57
+ self._queries: dict[str, Callable[[dict[str, list[str]], bytes], tuple[int, str, bytes]]] = {}
58
+ self._procedures: dict[str, Callable[[dict[str, list[str]], bytes], tuple[int, str, bytes]]] = {}
59
+ self._register_default_lexicons()
60
+
61
+ def register_query(
62
+ self,
63
+ nsid: str,
64
+ handler: Callable[[dict[str, list[str]], bytes], tuple[int, str, bytes]],
65
+ ) -> None:
66
+ """Register a GET XRPC query handler."""
67
+ self._queries[nsid] = handler
68
+
69
+ def register_procedure(
70
+ self,
71
+ nsid: str,
72
+ handler: Callable[[dict[str, list[str]], bytes], tuple[int, str, bytes]],
73
+ ) -> None:
74
+ """Register a POST XRPC procedure handler."""
75
+ self._procedures[nsid] = handler
76
+
77
+ def _register_default_lexicons(self) -> None:
78
+ self.register_query('com.atproto.server.describeServer', self._describe_server)
79
+ self.register_query('com.atproto.repo.describeRepo', self._describe_repo)
80
+ self.register_query('com.atproto.repo.getRecord', self._get_record)
81
+ self.register_query('com.atproto.repo.listRecords', self._list_records)
82
+ self.register_procedure('com.atproto.repo.createRecord', self._create_record)
83
+ self.register_procedure('com.atproto.repo.deleteRecord', self._delete_record)
84
+ self.register_query('com.atproto.sync.getRepo', self._get_repo_car)
85
+ self.register_query('com.atproto.sync.getLatestCommit', self._get_latest_commit)
86
+ self.register_query('com.atproto.sync.getBlob', self._get_blob)
87
+
88
+ def handle_request(
89
+ self,
90
+ method: str,
91
+ raw_path: str,
92
+ body: bytes = b"",
93
+ ) -> tuple[int, Mapping[str, str], bytes]:
94
+ """Dispatch HTTP request to XRPC handler.
95
+
96
+ Returns (status_code, headers_dict, body_bytes).
97
+ """
98
+ parsed = urllib.parse.urlparse(raw_path)
99
+ path = parsed.path
100
+ if not path.startswith('/xrpc/'):
101
+ return 404, {'Content-Type': 'application/json'}, json.dumps({'error': 'NotFound', 'message': 'Not an XRPC endpoint'}).encode()
102
+
103
+ nsid = path[len('/xrpc/'):].strip('/')
104
+ params = urllib.parse.parse_qs(parsed.query)
105
+
106
+ try:
107
+ if method == 'GET':
108
+ if nsid not in self._queries:
109
+ return 404, {'Content-Type': 'application/json'}, json.dumps({'error': 'MethodNotImplemented', 'message': f"Query {nsid} not implemented"}).encode()
110
+ status, content_type, resp_body = self._queries[nsid](params, body)
111
+ return status, {'Content-Type': content_type}, resp_body
112
+ elif method == 'POST':
113
+ if nsid not in self._procedures:
114
+ return 404, {'Content-Type': 'application/json'}, json.dumps({'error': 'MethodNotImplemented', 'message': f"Procedure {nsid} not implemented"}).encode()
115
+ status, content_type, resp_body = self._procedures[nsid](params, body)
116
+ return status, {'Content-Type': content_type}, resp_body
117
+ else:
118
+ return 405, {'Allow': 'GET, POST', 'Content-Type': 'application/json'}, json.dumps({'error': 'MethodNotAllowed', 'message': f"HTTP {method} not allowed"}).encode()
119
+ except XRPCError as e:
120
+ return e.status, {'Content-Type': 'application/json'}, json.dumps(e.to_dict()).encode()
121
+ except Exception as e:
122
+ return 500, {'Content-Type': 'application/json'}, json.dumps({'error': 'InternalServerError', 'message': str(e)}).encode()
123
+
124
+ # --- Core Lexicon Handlers ---
125
+
126
+ def _describe_server(self, params: dict[str, list[str]], body: bytes) -> tuple[int, str, bytes]:
127
+ data = {
128
+ 'availableUserDomains': [self.service_domain],
129
+ 'inviteCodeRequired': False,
130
+ 'links': {
131
+ 'privacyPolicy': f"https://{self.service_domain}/privacy",
132
+ 'termsOfService': f"https://{self.service_domain}/tos",
133
+ },
134
+ }
135
+ return 200, 'application/json', json.dumps(data).encode()
136
+
137
+ def _describe_repo(self, params: dict[str, list[str]], body: bytes) -> tuple[int, str, bytes]:
138
+ if self.repo is None:
139
+ raise XRPCError(404, 'RepoNotFound', 'No repository hosted on this node')
140
+ repo_param = params.get('repo', [self.repo.did])[0]
141
+ if repo_param != self.repo.did:
142
+ raise XRPCError(404, 'RepoNotFound', f"Repo {repo_param} not found")
143
+
144
+ records = self.repo.list_records()
145
+ collections = sorted(list(set(c for c, _, _, _ in records)))
146
+ data = {
147
+ 'did': self.repo.did,
148
+ 'handle': self.service_domain,
149
+ 'didDoc': None,
150
+ 'collections': collections,
151
+ 'handleIsCorrect': True,
152
+ }
153
+ return 200, 'application/json', json.dumps(data).encode()
154
+
155
+ def _get_record(self, params: dict[str, list[str]], body: bytes) -> tuple[int, str, bytes]:
156
+ repo_val = params.get('repo', [''])[0]
157
+ col_val = params.get('collection', [''])[0]
158
+ rkey_val = params.get('rkey', [''])[0]
159
+
160
+ if not col_val or not rkey_val:
161
+ raise XRPCError(400, 'InvalidRequest', 'Missing required params: collection and rkey')
162
+ if not VALID_COLLECTION_RE.match(col_val):
163
+ raise XRPCError(400, 'InvalidRequest', f"Malformed collection format: {col_val}")
164
+ if not VALID_RKEY_RE.match(rkey_val):
165
+ raise XRPCError(400, 'InvalidRequest', f"Malformed rkey format: {rkey_val}")
166
+
167
+ if self.repo is None:
168
+ raise XRPCError(404, 'RepoNotFound', 'No repository hosted on this node')
169
+ if repo_val and repo_val != self.repo.did:
170
+ raise XRPCError(404, 'RepoNotFound', f"Unknown repo: {repo_val}")
171
+
172
+ record_info = self.repo.get_record(col_val, rkey_val)
173
+ if record_info is None:
174
+ raise XRPCError(404, 'RecordNotFound', f"Record {col_val}/{rkey_val} not found")
175
+
176
+ rec_cid, rec_doc = record_info
177
+ out = {
178
+ 'uri': f"at://{self.repo.did}/{col_val}/{rkey_val}",
179
+ 'cid': rec_cid.cid_str,
180
+ 'value': rec_doc,
181
+ }
182
+ return 200, 'application/json', json.dumps(out).encode()
183
+
184
+ def _list_records(self, params: dict[str, list[str]], body: bytes) -> tuple[int, str, bytes]:
185
+ col_val = params.get('collection', [''])[0]
186
+ if not col_val:
187
+ raise XRPCError(400, 'InvalidRequest', 'Missing required param: collection')
188
+ if not VALID_COLLECTION_RE.match(col_val):
189
+ raise XRPCError(400, 'InvalidRequest', f"Malformed collection format: {col_val}")
190
+
191
+ if self.repo is None:
192
+ raise XRPCError(404, 'RepoNotFound', 'No repository hosted on this node')
193
+
194
+ limit = 50
195
+ if 'limit' in params:
196
+ try:
197
+ limit = min(100, max(1, int(params['limit'][0])))
198
+ except ValueError:
199
+ pass
200
+
201
+ records = self.repo.list_records(col_val)[:limit]
202
+ out_records = []
203
+ for col, rkey, cid, doc in records:
204
+ out_records.append({
205
+ 'uri': f"at://{self.repo.did}/{col}/{rkey}",
206
+ 'cid': cid.cid_str,
207
+ 'value': doc,
208
+ })
209
+ return 200, 'application/json', json.dumps({'records': out_records}).encode()
210
+
211
+ def _create_record(self, params: dict[str, list[str]], body: bytes) -> tuple[int, str, bytes]:
212
+ try:
213
+ req_data = json.loads(body.decode('utf-8'))
214
+ except Exception:
215
+ raise XRPCError(400, 'InvalidJson', 'Invalid JSON body')
216
+
217
+ col = req_data.get('collection')
218
+ rec = req_data.get('record')
219
+ rkey = req_data.get('rkey')
220
+ if not col or not rec:
221
+ raise XRPCError(400, 'InvalidRequest', 'Missing collection or record in body')
222
+ if not VALID_COLLECTION_RE.match(col):
223
+ raise XRPCError(400, 'InvalidRequest', f"Malformed collection format: {col}")
224
+ if rkey is not None and not VALID_RKEY_RE.match(rkey):
225
+ raise XRPCError(400, 'InvalidRequest', f"Malformed rkey format: {rkey}")
226
+
227
+ if self.repo is None:
228
+ raise XRPCError(404, 'RepoNotFound', 'No repository hosted on this node')
229
+
230
+ assigned_rkey, rec_cid, commit_cid = self.repo.put_record(col, rec, rkey=rkey)
231
+ out = {
232
+ 'uri': f"at://{self.repo.did}/{col}/{assigned_rkey}",
233
+ 'cid': rec_cid.cid_str,
234
+ 'commit': {'cid': commit_cid.cid_str, 'rev': self.repo.head_commit_cid.cid_str if self.repo.head_commit_cid else ''},
235
+ }
236
+ return 200, 'application/json', json.dumps(out).encode()
237
+
238
+ def _delete_record(self, params: dict[str, list[str]], body: bytes) -> tuple[int, str, bytes]:
239
+ try:
240
+ req_data = json.loads(body.decode('utf-8'))
241
+ except Exception:
242
+ raise XRPCError(400, 'InvalidJson', 'Invalid JSON body')
243
+
244
+ col = req_data.get('collection')
245
+ rkey = req_data.get('rkey')
246
+ if not col or not rkey:
247
+ raise XRPCError(400, 'InvalidRequest', 'Missing collection or rkey in body')
248
+ if not VALID_COLLECTION_RE.match(col):
249
+ raise XRPCError(400, 'InvalidRequest', f"Malformed collection format: {col}")
250
+ if not VALID_RKEY_RE.match(rkey):
251
+ raise XRPCError(400, 'InvalidRequest', f"Malformed rkey format: {rkey}")
252
+
253
+ if self.repo is None:
254
+ raise XRPCError(404, 'RepoNotFound', 'No repository hosted on this node')
255
+
256
+ commit_cid = self.repo.delete_record(col, rkey)
257
+ out = {'commit': {'cid': commit_cid.cid_str}}
258
+ return 200, 'application/json', json.dumps(out).encode()
259
+
260
+ def _get_repo_car(self, params: dict[str, list[str]], body: bytes) -> tuple[int, str, bytes]:
261
+ if self.repo is None:
262
+ raise XRPCError(404, 'RepoNotFound', 'No repository hosted on this node')
263
+ car_bytes = self.repo.export_car()
264
+ return 200, 'application/vnd.ipld.car', car_bytes
265
+
266
+ def _get_latest_commit(self, params: dict[str, list[str]], body: bytes) -> tuple[int, str, bytes]:
267
+ if self.repo is None or self.repo.head_commit_cid is None:
268
+ raise XRPCError(404, 'RepoNotFound', 'Repository has no commits')
269
+ out = {
270
+ 'cid': self.repo.head_commit_cid.cid_str,
271
+ 'rev': self.repo.head_commit_cid.cid_str,
272
+ }
273
+ return 200, 'application/json', json.dumps(out).encode()
274
+
275
+ def _get_blob(self, params: dict[str, list[str]], body: bytes) -> tuple[int, str, bytes]:
276
+ if self.repo is None:
277
+ raise XRPCError(404, 'RepoNotFound', 'No repository hosted on this node')
278
+ cid_str = params.get('cid', [''])[0]
279
+ if not cid_str:
280
+ raise XRPCError(400, 'InvalidRequest', 'Missing cid param')
281
+
282
+ data = self.repo.get_blob(cid_str)
283
+ if data is None:
284
+ raise XRPCError(404, 'BlobNotFound', f"Blob {cid_str} not found")
285
+ return 200, 'application/octet-stream', data
node/protocols/base.py ADDED
@@ -0,0 +1,109 @@
1
+ """Shared base classes for Fleetmesh protocol adapters."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Optional, TypeVar
5
+
6
+ from cryptography.exceptions import InvalidSignature
7
+ from cryptography.hazmat.primitives.asymmetric import ed25519
8
+
9
+ from fleetmesh_core.crypto import keys
10
+ from fleetmesh_core.wire import spelling
11
+ from fleetmesh_core.wire.interfaces import ProtocolIdentityAdapter
12
+
13
+ __all__ = [
14
+ 'BaseProtocolPackage',
15
+ 'Ed25519IdentityAdapter',
16
+ ]
17
+
18
+ T = TypeVar('T', bound='Ed25519IdentityAdapter')
19
+
20
+
21
+ class BaseProtocolPackage:
22
+ """Shared base class for Fleetmesh protocol packages."""
23
+
24
+ store: Any = None
25
+
26
+ def stop(self) -> None:
27
+ """Close any attached protocol store when stopping the package."""
28
+ store_obj = getattr(self, 'store', None)
29
+ if store_obj is not None and hasattr(store_obj, 'close'):
30
+ store_obj.close()
31
+
32
+
33
+ class Ed25519IdentityAdapter(ProtocolIdentityAdapter):
34
+ """Shared Ed25519 identity and assertion adapter for protocol packages."""
35
+
36
+ uri_prefixes: tuple[str, ...] = ()
37
+
38
+ def __init__(
39
+ self,
40
+ private_key: Any = None,
41
+ public_key_hex: Optional[str] = None,
42
+ ) -> None:
43
+ self.private_key = private_key
44
+ self._private_key = private_key
45
+ if private_key is not None and hasattr(private_key, 'public_key'):
46
+ self._public_key_hex = private_key.public_key().public_bytes_raw().hex()
47
+ else:
48
+ self._public_key_hex = public_key_hex.lower() if public_key_hex else ""
49
+
50
+ @property
51
+ def author_id(self) -> str:
52
+ """The canonical 64-character lowercase hex author identifier."""
53
+ return self._public_key_hex
54
+
55
+ def resolve_id(self, uri: str) -> Optional[str]:
56
+ """Resolve a protocol URI or raw 64-char hex public key to a canonical ID."""
57
+ clean = uri.strip()
58
+ for prefix in self.uri_prefixes:
59
+ if clean.startswith(prefix):
60
+ clean = clean[len(prefix):]
61
+ break
62
+ if spelling.is_hex(clean, 64, upper=True):
63
+ return clean.lower()
64
+ return None
65
+
66
+ def sign_assertion(self, payload: bytes) -> bytes:
67
+ """Sign payload bytes using the configured Ed25519 private key."""
68
+ key = self.private_key if self.private_key is not None else self._private_key
69
+ if key is None:
70
+ raise RuntimeError("Private key not configured for signing assertions")
71
+ if hasattr(key, 'sign'):
72
+ return key.sign(payload)
73
+ k = ed25519.Ed25519PrivateKey.from_private_bytes(bytes(key))
74
+ return k.sign(payload)
75
+
76
+ @staticmethod
77
+ def _verify_ed25519_bytes(payload: bytes, signature: bytes, raw_pub: bytes) -> bool:
78
+ try:
79
+ verifier = ed25519.Ed25519PublicKey.from_public_bytes(raw_pub)
80
+ verifier.verify(signature, payload)
81
+ return True
82
+ except (InvalidSignature, ValueError, TypeError):
83
+ return False
84
+
85
+ def verify_assertion(self, payload: bytes, signature: bytes, pubkey: str) -> bool:
86
+ """Verify an assertion signature against an Ed25519 public key."""
87
+ resolved = self.resolve_id(pubkey)
88
+ if resolved is None:
89
+ return False
90
+ try:
91
+ raw_pub = bytes.fromhex(resolved)
92
+ except ValueError:
93
+ return False
94
+ return self._verify_ed25519_bytes(payload, signature, raw_pub)
95
+
96
+ @classmethod
97
+ def from_keystore(cls: type[T], keystore: Any) -> T:
98
+ """Construct adapter from a Fleetmesh Keystore instance."""
99
+ if hasattr(keystore, 'transport_seed') and keystore.transport_seed:
100
+ priv, _ = keys.transport_keypair(keystore.transport_seed)
101
+ return cls(private_key=priv)
102
+ if hasattr(keystore, 'transport_priv') and keystore.transport_priv is not None:
103
+ return cls(private_key=keystore.transport_priv)
104
+ if hasattr(keystore, 'ed25519_sk') and keystore.ed25519_sk is not None:
105
+ return cls(private_key=keystore.ed25519_sk)
106
+ if hasattr(keystore, 'transport_pub') and keystore.transport_pub:
107
+ raw_pub = keys.raw_public(keystore.transport_pub)
108
+ return cls(public_key_hex=raw_pub.hex())
109
+ return cls()
@@ -0,0 +1,29 @@
1
+ """FleetMesh Node Circles Protocol Package (`node.protocols.circles`)."""
2
+ from __future__ import annotations
3
+
4
+ # The stores and route handlers live in the fleetmesh-protocols package
5
+ # (fleetmesh_protocols.circles); this package re-exports them so node code and
6
+ # the protocol router keep their import paths.
7
+
8
+ from fleetmesh_protocols.circles.persistence import load_circles, persist_circle
9
+ from fleetmesh_protocols.circles.store import (
10
+ apply_circle_media_privacy,
11
+ circle_access,
12
+ handle_circles_post,
13
+ is_circle_member,
14
+ visible_circle,
15
+ visible_circles,
16
+ visible_invites,
17
+ )
18
+
19
+ __all__ = [
20
+ "load_circles",
21
+ "persist_circle",
22
+ "apply_circle_media_privacy",
23
+ "circle_access",
24
+ "handle_circles_post",
25
+ "is_circle_member",
26
+ "visible_circle",
27
+ "visible_circles",
28
+ "visible_invites",
29
+ ]
@@ -0,0 +1,50 @@
1
+ """FleetMesh Node Direct-Message Mailbox & Spool Package (`node.protocols.dm`)."""
2
+ from __future__ import annotations
3
+
4
+ # The stores and route handlers live in the fleetmesh-protocols package
5
+ # (fleetmesh_protocols.dm); this package re-exports them so node code and
6
+ # the protocol router keep their import paths.
7
+
8
+ from fleetmesh_protocols.dm.mailbox import (
9
+ MAX_SEALED_CONTENT,
10
+ ensure_mailbox,
11
+ get_dm_summary,
12
+ load_dm_mailbox,
13
+ peer_records,
14
+ persist_dm_message,
15
+ persist_dm_view,
16
+ persist_leaf_binding,
17
+ record_message,
18
+ update_view,
19
+ viewer_conversations,
20
+ viewer_envelopes,
21
+ viewer_messages,
22
+ )
23
+ from fleetmesh_protocols.dm.spool import (
24
+ ensure_dm_spool_fields,
25
+ get_active_typing_indicators,
26
+ get_dm_peers,
27
+ handle_dm_post,
28
+ record_typing_indicator,
29
+ )
30
+
31
+ __all__ = [
32
+ "MAX_SEALED_CONTENT",
33
+ "ensure_mailbox",
34
+ "get_dm_summary",
35
+ "load_dm_mailbox",
36
+ "peer_records",
37
+ "persist_dm_message",
38
+ "persist_dm_view",
39
+ "persist_leaf_binding",
40
+ "record_message",
41
+ "update_view",
42
+ "viewer_conversations",
43
+ "viewer_envelopes",
44
+ "viewer_messages",
45
+ "ensure_dm_spool_fields",
46
+ "get_active_typing_indicators",
47
+ "get_dm_peers",
48
+ "handle_dm_post",
49
+ "record_typing_indicator",
50
+ ]
@@ -0,0 +1,22 @@
1
+ """FleetMesh Node GRASP Git Protocol Package (`node.protocols.grasp`)."""
2
+ from __future__ import annotations
3
+
4
+ # The stores and route handlers live in the fleetmesh-protocols package
5
+ # (fleetmesh_protocols.grasp); this package re-exports them so node code and
6
+ # the protocol router keep their import paths.
7
+
8
+ from fleetmesh_protocols.grasp.git_http import _pkt_line, handle_git_smart_http, render_node_portal_html
9
+ from fleetmesh_protocols.grasp.git_store import SEED_EPOCH, StandaloneGraspGitStore, _clean_slug, _commit_time, handle_grasp_api_http
10
+ from fleetmesh_protocols.grasp.seeds import SEEDED_GRASP_REPOS
11
+
12
+ __all__ = [
13
+ "SEEDED_GRASP_REPOS",
14
+ "SEED_EPOCH",
15
+ "StandaloneGraspGitStore",
16
+ "_clean_slug",
17
+ "_commit_time",
18
+ "handle_grasp_api_http",
19
+ "_pkt_line",
20
+ "handle_git_smart_http",
21
+ "render_node_portal_html",
22
+ ]
@@ -0,0 +1,15 @@
1
+ """The timestamp text the protocol stores write: UTC, whole seconds, a Z.
2
+
3
+ Four stores wrote this function out for themselves, byte for byte (audit
4
+ 2026-10-15, D8). It reads the wall clock, as all four did: what it stamps is
5
+ when a row was written, for an operator reading the table, never a protocol
6
+ time another node compares.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import datetime
11
+
12
+
13
+ def now_iso() -> str:
14
+ """Now, as 2026-10-15T09:20:00Z."""
15
+ return datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
@@ -0,0 +1,51 @@
1
+ """Nostr & Blossom protocol package for Fleetmesh.
2
+
3
+ Supports:
4
+ - NIP-01..NIP-77 relay server, event store, and client connections.
5
+ - BUD-01..BUD-04 Blossom blob storage and media servers.
6
+ - Peer descriptor exchange and cryptographic identity assertions.
7
+ - Unified ProtocolPackage ingress and lifecycle management.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from .client import DEAD_AFTER, PING_AFTER, Relay, RelayError, publish_all, query_all
14
+
15
+ __all__ = [
16
+ 'DEAD_AFTER', 'PING_AFTER', 'Relay', 'RelayError', 'publish_all', 'query_all',
17
+ 'RelayServer', 'NodeStore', 'BlossomServer', 'BlossomClient', 'BlobStore', 'Peers',
18
+ 'NostrProtocolPackage', 'NostrIdentityAdapter',
19
+ 'blobstore', 'blossom', 'client', 'descriptor', 'eventstore', 'package', 'peers', 'relayserver',
20
+ ]
21
+
22
+
23
+ def __getattr__(name: str) -> Any:
24
+ if name == 'RelayServer':
25
+ from .relayserver import RelayServer
26
+ return RelayServer
27
+ if name == 'NodeStore':
28
+ from .eventstore import NodeStore
29
+ return NodeStore
30
+ if name == 'BlossomServer':
31
+ from .blossom import BlossomServer
32
+ return BlossomServer
33
+ if name == 'BlossomClient':
34
+ from .blossom import BlossomClient
35
+ return BlossomClient
36
+ if name == 'BlobStore':
37
+ from .blobstore import BlobStore
38
+ return BlobStore
39
+ if name == 'Peers':
40
+ from .peers import Peers
41
+ return Peers
42
+ if name == 'NostrProtocolPackage':
43
+ from .package import NostrProtocolPackage
44
+ return NostrProtocolPackage
45
+ if name == 'NostrIdentityAdapter':
46
+ from .package import NostrIdentityAdapter
47
+ return NostrIdentityAdapter
48
+ if name in ('blobstore', 'blossom', 'client', 'descriptor', 'eventstore', 'package', 'peers', 'relayserver'):
49
+ import importlib
50
+ return importlib.import_module(f'.{name}', __package__)
51
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")