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,154 @@
1
+ """gossip/1.0 wire vocabulary: PIURI, message types, and the GossipError.
2
+
3
+ Separate from core.py because the vocabulary is what validate.py, the tests,
4
+ and the subsystem all need, and it has no dependency on the mesh/seen-store
5
+ machinery that acts on it.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import uuid
10
+ from typing import Any, Mapping, Optional, Sequence
11
+
12
+ from fleetmesh_core.wire.wire_types import GossipGraftBody, GossipIdsBody, GossipPruneBody, GossipPublishBody, Message
13
+
14
+ PIURI: str = 'https://fleetmesh.org/gossip/1.0'
15
+ GRAFT_TYPE: str = PIURI + '/graft'
16
+ PRUNE_TYPE: str = PIURI + '/prune'
17
+ IHAVE_TYPE: str = PIURI + '/ihave'
18
+ IWANT_TYPE: str = PIURI + '/iwant'
19
+ PUBLISH_TYPE: str = PIURI + '/publish'
20
+
21
+ MESSAGES: tuple[str, ...] = ('graft', 'prune', 'ihave', 'iwant', 'publish')
22
+
23
+ # Target mesh degree per topic, the gossipsub D parameter. A node grafts up to
24
+ # this many peers per topic and regrafts on tick when it falls below.
25
+ DEFAULT_MESH_DEGREE: int = 6
26
+
27
+ MEMBERSHIP_KIND: int = 30804
28
+
29
+
30
+ class GossipError(Exception):
31
+ """Raised when a gossip/1.0 wire message or mesh transition is invalid."""
32
+
33
+
34
+ def make_graft(
35
+ from_did: str,
36
+ to_did: str,
37
+ topic: str,
38
+ membership_id: str,
39
+ created_time: Optional[int] = None,
40
+ msg_id: Optional[str] = None,
41
+ ) -> Message[GossipGraftBody]:
42
+ """Construct a DIDComm gossip/1.0/graft message.
43
+
44
+ `membership_id` is the id of the kind 30804 membership event the sender
45
+ holds naming its own pubkey for this topic: the receiver checks it names
46
+ the sender before adding them to its mesh, rather than trusting the ask.
47
+ """
48
+ msg: Message[GossipGraftBody] = {
49
+ 'id': msg_id or str(uuid.uuid4()),
50
+ 'type': GRAFT_TYPE,
51
+ 'from': from_did,
52
+ 'to': [to_did],
53
+ 'body': GossipGraftBody(topic=topic, membership=membership_id),
54
+ }
55
+ if created_time is not None:
56
+ msg['created_time'] = int(created_time)
57
+ return msg
58
+
59
+
60
+ def make_prune(
61
+ from_did: str,
62
+ to_did: str,
63
+ topic: str,
64
+ reason: str = 'degree',
65
+ created_time: Optional[int] = None,
66
+ msg_id: Optional[str] = None,
67
+ ) -> Message[GossipPruneBody]:
68
+ """Construct a DIDComm gossip/1.0/prune message."""
69
+ msg: Message[GossipPruneBody] = {
70
+ 'id': msg_id or str(uuid.uuid4()),
71
+ 'type': PRUNE_TYPE,
72
+ 'from': from_did,
73
+ 'to': [to_did],
74
+ 'body': GossipPruneBody(topic=topic, reason=reason),
75
+ }
76
+ if created_time is not None:
77
+ msg['created_time'] = int(created_time)
78
+ return msg
79
+
80
+
81
+ def make_ihave(
82
+ from_did: str,
83
+ to_did: str,
84
+ topic: str,
85
+ ids: Sequence[str],
86
+ created_time: Optional[int] = None,
87
+ msg_id: Optional[str] = None,
88
+ ) -> Message[GossipIdsBody]:
89
+ """Construct a DIDComm gossip/1.0/ihave message: ids only, no payload."""
90
+ msg: Message[GossipIdsBody] = {
91
+ 'id': msg_id or str(uuid.uuid4()),
92
+ 'type': IHAVE_TYPE,
93
+ 'from': from_did,
94
+ 'to': [to_did],
95
+ 'body': GossipIdsBody(topic=topic, ids=list(ids)),
96
+ }
97
+ if created_time is not None:
98
+ msg['created_time'] = int(created_time)
99
+ return msg
100
+
101
+
102
+ def make_iwant(
103
+ from_did: str,
104
+ to_did: str,
105
+ topic: str,
106
+ ids: Sequence[str],
107
+ created_time: Optional[int] = None,
108
+ msg_id: Optional[str] = None,
109
+ ) -> Message[GossipIdsBody]:
110
+ """Construct a DIDComm gossip/1.0/iwant message: ids being asked for."""
111
+ msg: Message[GossipIdsBody] = {
112
+ 'id': msg_id or str(uuid.uuid4()),
113
+ 'type': IWANT_TYPE,
114
+ 'from': from_did,
115
+ 'to': [to_did],
116
+ 'body': GossipIdsBody(topic=topic, ids=list(ids)),
117
+ }
118
+ if created_time is not None:
119
+ msg['created_time'] = int(created_time)
120
+ return msg
121
+
122
+
123
+ def make_publish(
124
+ from_did: str,
125
+ to_did: str,
126
+ topic: str,
127
+ event_id: str,
128
+ envelope: Mapping[str, Any],
129
+ created_time: Optional[int] = None,
130
+ msg_id: Optional[str] = None,
131
+ ) -> Message[GossipPublishBody]:
132
+ """Construct a DIDComm gossip/1.0/publish message.
133
+
134
+ `envelope` is the already-sealed NIP-59 seal+wrap this node floods: gossip
135
+ never sees plaintext, only the per-member sealed bytes keyed by `event_id`
136
+ (the wrap's own id, which is what the seen-id store dedupes on).
137
+ """
138
+ msg: Message[GossipPublishBody] = {
139
+ 'id': msg_id or str(uuid.uuid4()),
140
+ 'type': PUBLISH_TYPE,
141
+ 'from': from_did,
142
+ 'to': [to_did],
143
+ 'body': GossipPublishBody(topic=topic, event_id=event_id, envelope=dict(envelope)),
144
+ }
145
+ if created_time is not None:
146
+ msg['created_time'] = int(created_time)
147
+ return msg
148
+
149
+
150
+ __all__ = [
151
+ 'PIURI', 'GRAFT_TYPE', 'PRUNE_TYPE', 'IHAVE_TYPE', 'IWANT_TYPE', 'PUBLISH_TYPE',
152
+ 'MESSAGES', 'DEFAULT_MESH_DEGREE', 'MEMBERSHIP_KIND', 'GossipError',
153
+ 'make_graft', 'make_prune', 'make_ihave', 'make_iwant', 'make_publish',
154
+ ]
@@ -0,0 +1,127 @@
1
+ """The seen-id store: sqlite-backed, bounded by TTL and size, durable across
2
+ restarts.
3
+
4
+ A node that forgets an id the moment it restarts re-floods everything it has
5
+ already seen back out to its whole mesh, which is indistinguishable from an
6
+ attacker replaying a topic's history at every peer. Durability is what keeps
7
+ a restart from being an amplification event. Unbounded growth is the other
8
+ failure: a long-lived topic would otherwise make this table the largest
9
+ thing on disk for a compartment whose whole job is "have I seen this once
10
+ already" - so entries expire (SEEN_TTL_SECONDS) and the table is swept to
11
+ MAX_SEEN_ROWS on insert pressure, oldest first by `first_seen_at`, which is the
12
+ LRU reading of when a topic first carried an event.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import time
18
+ from typing import Any, Mapping, Optional
19
+
20
+ from node.compartment.schema import Column, Index, Table
21
+ from node.storage import SqliteStore
22
+
23
+ DEFAULT_SEEN_TTL_SECONDS: int = 24 * 3600
24
+ DEFAULT_MAX_SEEN_ROWS: int = 200_000
25
+
26
+
27
+ class SeenStore(SqliteStore):
28
+ """(topic, event_id) -> first_seen_at, for gossip loop prevention."""
29
+
30
+ SCHEMA = (
31
+ Table('seen', (
32
+ Column('topic', 'TEXT', not_null=True),
33
+ Column('event_id', 'TEXT', not_null=True),
34
+ Column('first_seen_at', 'INTEGER', not_null=True),
35
+ ), primary_key=('topic', 'event_id'), indices=(
36
+ Index('seen_at', ('first_seen_at',)),
37
+ )),
38
+ )
39
+
40
+ def __init__(self, path: str = ':memory:', ttl_seconds: int = DEFAULT_SEEN_TTL_SECONDS,
41
+ max_rows: int = DEFAULT_MAX_SEEN_ROWS, **kw: Any) -> None:
42
+ super().__init__(path, **kw)
43
+ self.ttl_seconds: int = ttl_seconds
44
+ self.max_rows: int = max_rows
45
+
46
+ def seen(self, topic: str, event_id: str) -> bool:
47
+ return self._one('SELECT 1 FROM seen WHERE topic=? AND event_id=?', (topic, event_id)) is not None
48
+
49
+ def mark(self, topic: str, event_id: str, now: Optional[int] = None) -> bool:
50
+ """Record (topic, event_id) as seen. Returns True if this was new."""
51
+ at = int(now if now is not None else time.time())
52
+ with self._lock:
53
+ try:
54
+ self.execute('INSERT INTO seen VALUES (?,?,?)', (topic, event_id, at))
55
+ self.db.commit()
56
+ except Exception:
57
+ # Already present: the INSERT on the primary key failed. Roll
58
+ # the implicit transaction back, or this connection holds a
59
+ # write lock on the file until the next commit.
60
+ self.db.rollback()
61
+ return False
62
+ n = self._one('SELECT COUNT(*) FROM seen')
63
+ if n and n[0] > self.max_rows:
64
+ self._sweep_oldest(n[0] - self.max_rows)
65
+ return True
66
+
67
+ def _sweep_oldest(self, excess: int) -> None:
68
+ with self._lock:
69
+ # Named by the declared key, not by SQLite's implicit rowid: the
70
+ # ordering was always `first_seen_at`, and rowid was only how the
71
+ # selected rows were identified. `(topic, event_id)` is the table's
72
+ # own PRIMARY KEY and says the same thing on any backend.
73
+ self.execute(
74
+ 'DELETE FROM seen WHERE (topic, event_id) IN '
75
+ '(SELECT topic, event_id FROM seen ORDER BY first_seen_at ASC LIMIT ?)', (excess,))
76
+ self.db.commit()
77
+
78
+ def expire(self, now: Optional[int] = None) -> int:
79
+ """Drop rows older than the TTL. Returns the count removed."""
80
+ cut = int(now if now is not None else time.time()) - self.ttl_seconds
81
+ with self._lock:
82
+ cur = self.execute('DELETE FROM seen WHERE first_seen_at < ?', (cut,))
83
+ self.db.commit()
84
+ return cur.rowcount if cur.rowcount is not None else 0
85
+
86
+ def count(self, topic: Optional[str] = None) -> int:
87
+ if topic is None:
88
+ row = self._one('SELECT COUNT(*) FROM seen')
89
+ else:
90
+ row = self._one('SELECT COUNT(*) FROM seen WHERE topic=?', (topic,))
91
+ return int(row[0]) if row else 0
92
+
93
+
94
+ class RosterStore(SqliteStore):
95
+ """topic -> the current admin-signed kind 30804 roster, durable across restarts.
96
+
97
+ A topic the operator created and then lost on the next restart is a topic
98
+ that silently stopped gossiping: no peer would graft to a node with no
99
+ membership event, and nothing would say why. The roster is the whole of
100
+ what makes a node a member, so it lives on disk next to the seen-ids.
101
+ """
102
+
103
+ SCHEMA = (
104
+ Table('rosters', (
105
+ Column('topic', 'TEXT', primary_key=True),
106
+ Column('event', 'TEXT', not_null=True),
107
+ Column('created_at', 'INTEGER', not_null=True),
108
+ )),
109
+ )
110
+
111
+ def put(self, topic: str, event: Mapping[str, Any]) -> None:
112
+ with self._lock:
113
+ self.execute('INSERT OR REPLACE INTO rosters VALUES (?,?,?)',
114
+ (topic, json.dumps(event, separators=(',', ':'), sort_keys=True),
115
+ int(event.get('created_at', 0))))
116
+ self.db.commit()
117
+
118
+ def delete(self, topic: str) -> None:
119
+ with self._lock:
120
+ self.execute('DELETE FROM rosters WHERE topic=?', (topic,))
121
+ self.db.commit()
122
+
123
+ def all(self) -> dict[str, Mapping[str, Any]]:
124
+ return {topic: json.loads(raw) for topic, raw in self._all('SELECT topic, event FROM rosters')}
125
+
126
+
127
+ __all__ = ['SeenStore', 'RosterStore', 'DEFAULT_SEEN_TTL_SECONDS', 'DEFAULT_MAX_SEEN_ROWS']
@@ -0,0 +1,50 @@
1
+ """Types and records for the Gossip compartment."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Mapping, Optional, TypedDict
5
+
6
+
7
+ class GossipTotals(TypedDict):
8
+ """Every gossip event this node took part in, counted since it started."""
9
+ published: int
10
+ delivered: int
11
+ offered: int
12
+ wanted: int
13
+ served: int
14
+ auto_admitted: int
15
+
16
+
17
+ class TopicView(TypedDict):
18
+ topic: str
19
+ admin: Optional[str]
20
+ members: list[str]
21
+ mesh: list[str]
22
+ expires: int
23
+ roster_id: Optional[str]
24
+
25
+
26
+ class TopicCreationResult(TypedDict):
27
+ topic: str
28
+ event: Mapping[str, Any]
29
+ relays: dict[str, list[Any]]
30
+
31
+
32
+ class OriginateResult(TypedDict):
33
+ topic: str
34
+ event_id: str
35
+ sent_to: list[str]
36
+ mesh: int
37
+
38
+
39
+ class GossipStateReport(TypedDict):
40
+ topics: list[str]
41
+ mesh: dict[str, int]
42
+ seen: int
43
+ published: int
44
+ auto_admitted: int
45
+ delivered: int
46
+ offered: int
47
+ wanted: int
48
+ served: int
49
+ mcache: int
50
+ refused: int
@@ -0,0 +1,7 @@
1
+ """nsite: the node as a NIP-5A static-site host, reachable from the plain web.
2
+
3
+ One-module compartment: everything is in compartment.py.
4
+ """
5
+ from .compartment import NsiteGateway
6
+
7
+ __all__ = ['NsiteGateway']
@@ -0,0 +1,62 @@
1
+ """nsite client-side publishing and site walking workflows."""
2
+ from __future__ import annotations
3
+
4
+ import mimetypes
5
+ import os
6
+ from typing import Callable, Sequence
7
+
8
+ MAX_FILES = 5000
9
+
10
+
11
+ def walk_site_files(root: str, max_files: int = MAX_FILES) -> list[tuple[str, str]]:
12
+ """Walk local directory for static site files.
13
+
14
+ Returns a list of (rel_path, full_path) tuples.
15
+ Raises ValueError if invalid directory, empty, or exceeds max_files.
16
+ """
17
+ root = os.path.abspath(root)
18
+ if not os.path.isdir(root):
19
+ raise ValueError('%s is not a directory' % root)
20
+ out: list[tuple[str, str]] = []
21
+ for dirpath, dirnames, filenames in os.walk(root):
22
+ dirnames[:] = sorted(d for d in dirnames if not d.startswith('.'))
23
+ for f in sorted(filenames):
24
+ if f.startswith('.'):
25
+ continue
26
+ full = os.path.join(dirpath, f)
27
+ rel = '/' + os.path.relpath(full, root).replace(os.sep, '/')
28
+ out.append((rel, full))
29
+ if not out:
30
+ raise ValueError('%s holds no files' % root)
31
+ if len(out) > max_files:
32
+ raise ValueError('%d files; a site here is at most %d' % (len(out), max_files))
33
+ return out
34
+
35
+
36
+ def upload_site_blobs(
37
+ files: Sequence[tuple[str, str]],
38
+ face_http_url: str,
39
+ node_sk: str,
40
+ progress: Callable[[str, str, int], None] | None = None,
41
+ ) -> tuple[dict[str, str], int]:
42
+ """Upload site files to local Blossom face.
43
+
44
+ Returns (paths_dict, total_bytes) where paths_dict maps rel_path -> sha256.
45
+ """
46
+ from node.protocols.nostr import blossom as blossom_mod
47
+
48
+ paths: dict[str, str] = {}
49
+ total = 0
50
+ for rel, full in files:
51
+ with open(full, 'rb') as fh:
52
+ data = fh.read()
53
+ ctype = mimetypes.guess_type(rel)[0] or 'application/octet-stream'
54
+ desc = blossom_mod.BlossomClient.upload_blob(
55
+ face_http_url, data, content_type=ctype, signer_sk=node_sk, allow_private=True
56
+ )
57
+ h = str(desc.get('sha256', '')).lower()
58
+ paths[rel] = h
59
+ total += len(data)
60
+ if progress is not None:
61
+ progress(rel, h, len(data))
62
+ return paths, total