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,141 @@
1
+ """nsite: publish this node's static site and watch the gateway.
2
+
3
+ `publish` is the only command that moves bytes: it walks a directory,
4
+ PUTs each file to the node's own Blossom face (signed with the node key,
5
+ over loopback, so the face's auth switch does not matter), then asks the
6
+ daemon to sign and publish the manifest from the resulting hashes. The
7
+ control socket never carries file bodies.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ from typing import Any, Mapping, Optional, TypedDict
13
+
14
+ from node import keystore
15
+ from node.config.parameter import Parameter
16
+ from node.planes.services.nsite.client import upload_site_blobs, walk_site_files
17
+ from node.planes.services.nsite.compartment import DomainBinding, NsiteGateway, NsiteState, PublishResult
18
+ from .adapter import CommandDispatcher
19
+ from .base import _ask, _live, _status_file, add_command, emit, fail
20
+
21
+
22
+ class DomainsResponse(TypedDict, total=False):
23
+ domains: list[DomainBinding]
24
+
25
+
26
+ def _face_http(a: argparse.Namespace) -> str:
27
+ st = _live(a, 'status') or _status_file(a.dir) or {}
28
+ url = st.get('face') or ''
29
+ if not url.startswith(('ws://', 'wss://')):
30
+ fail('the node is not running or its face is unknown (status has no face url)')
31
+ return 'http' + url[2:]
32
+
33
+
34
+ def _show_status(r: NsiteState) -> None:
35
+ print('nsite gateway:')
36
+ print(' manifests cached: %d' % r.get('manifests_cached', 0))
37
+ print(' domains verified: %d' % r.get('domains_verified', 0))
38
+ print(' bytes cached: %d' % r.get('cached_bytes', 0))
39
+
40
+
41
+ def _fmt_binding(b: Optional[DomainBinding]) -> str:
42
+ if not b:
43
+ return '(none)'
44
+ return '%-32s %-10s %s%s' % (b.get('domain'), b.get('status'), b.get('site_key') or '-',
45
+ (' - ' + (b.get('why') or '')) if b.get('why') else '')
46
+
47
+
48
+ def _show_domains(r: DomainsResponse | Mapping[str, Any]) -> None:
49
+ rows = r.get('domains') or []
50
+ if not rows:
51
+ print('no custom-domain bindings yet')
52
+ return
53
+ for b in rows:
54
+ print(_fmt_binding(b))
55
+
56
+
57
+ def _show_published(r: PublishResult) -> None:
58
+ print('published %d files, %d bytes' % (int(r.get('files', 0)), int(r.get('bytes', 0))))
59
+ print(' manifest %s (kind %s)' % (r.get('manifest'), r.get('kind')))
60
+ print(' version %s' % r.get('aggregate'))
61
+ if r.get('snapshot'):
62
+ print(' snapshot %s' % r['snapshot'])
63
+ for h in r.get('hosts') or []:
64
+ print(' https://%s/' % h)
65
+ if r.get('snapshot_label'):
66
+ print(' (snapshot label %s)' % r['snapshot_label'])
67
+ for p in r.get('published_to') or []:
68
+ print(' relays: %s %s' % ('ok' if p.get('ok') else 'FAILED', p.get('why') or ''))
69
+
70
+
71
+ def cmd_publish(a: argparse.Namespace) -> int:
72
+ if not a.path:
73
+ fail('publish requires --path <directory>')
74
+ try:
75
+ files = walk_site_files(a.path)
76
+ except ValueError as e:
77
+ fail(str(e))
78
+ if '/index.html' not in {p for p, _ in files}:
79
+ print('warning: no /index.html at the root; browsers asking for / get the 404 page')
80
+
81
+ ks = keystore.Keystore.load(a.dir)
82
+ face = _face_http(a)
83
+
84
+ def progress(rel: str, h: str, size: int) -> None:
85
+ if not getattr(a, 'json', False):
86
+ print(' %-40s %s %7d' % (rel, h[:12], size))
87
+
88
+ paths, total = upload_site_blobs(files, face, ks.node_sk, progress=progress)
89
+ res = _ask(
90
+ a.dir,
91
+ 'nsite.publish',
92
+ paths=paths,
93
+ site=a.site,
94
+ snapshot=bool(a.snapshot),
95
+ domains=a.domain_claim or [],
96
+ servers=a.server or [],
97
+ title=a.title,
98
+ timeout=60,
99
+ )
100
+ res['files'] = len(paths)
101
+ res['bytes'] = total
102
+ return emit(a, res, _show_published)
103
+
104
+
105
+ # Build declarative dispatcher from NsiteGateway.COMMANDS
106
+ _cmd_map = {c.key: c for c in NsiteGateway.COMMANDS}
107
+ dispatcher = CommandDispatcher('nsite', default_action='status')
108
+ dispatcher.add_command('status', _cmd_map['sites'], human=_show_status, timeout=10)
109
+ dispatcher.add_command('domains', _cmd_map['domains'], human=_show_domains, timeout=10)
110
+ dispatcher.add_command('verify', _cmd_map['verify'], human=lambda r, a: print(_fmt_binding(r.get('binding'))), timeout=30)
111
+ dispatcher.add_command(
112
+ 'forget',
113
+ _cmd_map['forget'],
114
+ human=lambda r, a: print('forgot %s' % a.domain if r.get('dropped') else 'no binding for %s' % a.domain),
115
+ timeout=30,
116
+ )
117
+ dispatcher.add_custom(
118
+ 'publish',
119
+ handler=cmd_publish,
120
+ parameters=(
121
+ Parameter(key='path', name='path', description='publish: directory to publish', type=str, default=''),
122
+ Parameter(key='site', name='site', description='publish: named-site id (d tag, 1-13 of [a-z0-9-]); default is the root site', type=str, default=''),
123
+ Parameter(key='snapshot', name='snapshot', description='publish: also publish an immutable kind-5128 snapshot', type=bool, default=False),
124
+ Parameter(key='domain_claim', name='domain_claim', description='publish: claim a custom domain in the manifest (repeatable)', type=list, default=list, cli_name='--domain-claim'),
125
+ Parameter(key='server', name='server', description='publish: extra Blossom server URL the blobs are mirrored on (repeatable)', type=list, default=list, cli_name='--server'),
126
+ Parameter(key='title', name='title', description='publish: site title tag', type=str, default=''),
127
+ ),
128
+ )
129
+
130
+
131
+ def cmd_nsite(a: argparse.Namespace) -> int:
132
+ return dispatcher.dispatch(a)
133
+
134
+
135
+ class NsiteCommandProvider:
136
+ """Registers the nsite (NIP-5A static site) commands."""
137
+
138
+ @classmethod
139
+ def register(cls, sub: argparse._SubParsersAction) -> None:
140
+ p = add_command(sub, 'nsite', cmd_nsite, 'host static sites (NIP-5A): publish this node\'s site, manage custom domains', js=True)
141
+ dispatcher.register_arguments(p, action_choices=('status', 'domains', 'verify', 'forget', 'publish'))
@@ -0,0 +1,402 @@
1
+ """Peering management, bilateral grants, accounting windows, and trust tiers."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import time
8
+ from typing import Any, Mapping, cast
9
+
10
+ from node import app as app_mod
11
+ from node.app import socket as control
12
+ from node.storage import accounts as accounts_mod, grants as grants_mod
13
+ from .base import (
14
+ TIERS,
15
+ _ask,
16
+ _grants,
17
+ _live,
18
+ _peer,
19
+ _status_file,
20
+ _table,
21
+ _when,
22
+ add_command,
23
+ emit,
24
+ fail,
25
+ load_json_arg,
26
+ )
27
+
28
+
29
+ def _peers_table(view: list) -> None:
30
+ if not view:
31
+ print('no peers: nothing granted and nothing held')
32
+ return
33
+ rows = []
34
+ for v in view:
35
+ last = v.get('last_evaluated') or {}
36
+ standing = v.get('standing') or {}
37
+ rows.append((v['peer'][:16], (v.get('issued') or {}).get('tier') or '-', (v.get('received') or {}).get('tier') or '-',
38
+ ('%s from %s' % (v['pending']['tier'], v['pending']['from_epoch'])) if v.get('pending') else '-',
39
+ ('%s %s' % (last.get('epoch'), last.get('severity'))) if last else '-',
40
+ ('%d clean / %d' % (standing['clean_windows'], standing['evaluated_windows'])) if standing else '-',
41
+ ','.join(v.get('pulls') or []) or '-',
42
+ ('%d' % len(v['shared'])) if v.get('shared') else '-',
43
+ ('other %s' % v['digest'][:8]) if v.get('digest') else 'same' if v.get('descriptor') else '-'))
44
+ _table(('peer', 'issued', 'received', 'pending', 'last window', 'streak', 'pulling', 'shared', 'schema'), rows)
45
+ for v in view:
46
+ for n in v.get('notices') or []:
47
+ print('%s: %s' % (v['peer'][:16], n))
48
+ if v.get('not_shared'):
49
+ print('%s: not shared with this node: %s' % (v['peer'][:16], ', '.join(v['not_shared'])))
50
+ zeroed = [v for v in view if v.get('zeroed_until')]
51
+ for v in zeroed:
52
+ print('%s is zeroed after S3 until %s UTC' % (v['peer'][:16], time.strftime('%Y-%m-%d %H:%M', time.gmtime(v['zeroed_until']))))
53
+
54
+
55
+ def cmd_peers(a: argparse.Namespace) -> int:
56
+ view = _live(a, 'peers', timeout=15)
57
+ if view is None:
58
+ # From disk: the grants and what the last status said.
59
+ g = _grants(a.dir)
60
+ st: Mapping[str, Any] = {}
61
+ try:
62
+ st = dict(_status_file(a.dir))
63
+ except SystemExit:
64
+ pass
65
+ cfg_peers: dict[str, list[str]] = {}
66
+ try:
67
+ for p in app_mod.load_config(a.dir).get('peers', []):
68
+ cfg_peers.setdefault(p['pubkey'], []).append(p['shard'])
69
+ except (OSError, ValueError):
70
+ pass
71
+ view = []
72
+ for pk in sorted({r['peer'] for r in g.list()}):
73
+ issued, received, pend = g.get(pk, grants_mod.ISSUED), g.get(pk, grants_mod.RECEIVED), g.pending(pk)
74
+ ev = [e for e in st.get('evaluations', []) if pk.startswith(e['peer'])]
75
+ view.append({'peer': pk,
76
+ 'issued': {'tier': issued['payload'].get('tier'), 'status': issued['status']} if issued else None,
77
+ 'received': {'tier': received['payload'].get('tier'), 'status': received['status']} if received else None,
78
+ 'pending': {'tier': pend['payload'].get('tier'), 'from_epoch': pend['decided_epoch'] + 1} if pend else None,
79
+ 'last_evaluated': ev[-1] if ev else None, 'standing': None,
80
+ 'zeroed_until': None, 'pulls': cfg_peers.get(pk, []), 'descriptor': None,
81
+ 'shared': [], 'not_shared': [], 'digest': None, 'notices': []})
82
+ return emit(a, view, _peers_table)
83
+
84
+
85
+ def cmd_grant(a: argparse.Namespace) -> int:
86
+ pk = _peer(a.dir, a.peer)
87
+ out = app_mod.grant_view(_grants(a.dir), pk)
88
+ if a.json:
89
+ return emit(a, out)
90
+ if out['issued'] is None and out['received'] is None:
91
+ fail('no grant either way with %s' % pk[:16])
92
+ out_dict = cast(Mapping[str, Any], out)
93
+ for direction in (grants_mod.ISSUED, grants_mod.RECEIVED):
94
+ row = out_dict.get(direction)
95
+ print('%s %s' % (direction, '(none)' if row is None else ''))
96
+ if row is None:
97
+ continue
98
+ p = row['payload']
99
+ print(' envelope %s tier %s status %s window %ss receipt every %ss'
100
+ % (row['envelope_id'][:16] + '…', p.get('tier'), row['status'], p.get('window'), (p.get('receipt') or ['?'])[0]))
101
+ limits = p.get('limits') or {}
102
+ print(' limits ' + ' '.join('%s %s' % (k, v) for k, v in sorted(limits.items())))
103
+ scope = p.get('scope') or {}
104
+ print(' scope pull %s push %s' % (json.dumps(scope.get('pull')), json.dumps(scope.get('push'))))
105
+ hist = out_dict.get(direction + '_history', [])
106
+ if len(hist) > 1:
107
+ print(' history ' + '; '.join('%s… %s from %s' % (h['envelope_id'][:8], h['tier'], h['effective_epoch']) for h in hist))
108
+ if out['pending']:
109
+ p = out['pending']
110
+ print('waiting tier %s from window %s (%s UTC): %s' % (p['payload'].get('tier'), p['decided_epoch'] + 1,
111
+ _when(p['decided_epoch'] + 1), p['note']))
112
+ if out['seed_applied']:
113
+ print('seed %s, decided at %s' % (out['seed_applied'][0], time.strftime('%Y-%m-%d %H:%M UTC', time.gmtime(out['seed_applied'][1]))
114
+ if out['seed_applied'][1] else 'time zero'))
115
+ return 0
116
+
117
+
118
+ def cmd_windows(a: argparse.Namespace) -> int:
119
+ pk = _peer(a.dir, a.peer)
120
+ path = os.path.join(a.dir, 'accounts.sqlite')
121
+ if not os.path.exists(path):
122
+ fail('no accounts.sqlite yet: nothing has been metered')
123
+ # Through the store, not a bare sqlite3 connection: `windows_view` already
124
+ # prefers `_all` when it is given one, and a store reaches whatever backend
125
+ # is configured. readonly=True keeps exactly what the direct connection
126
+ # gave - no writes, no schema, no WAL writer lock beside a running node.
127
+ acc = accounts_mod.AccountStore(path, readonly=True)
128
+ try:
129
+ out = app_mod.windows_view(acc, pk, a.last)
130
+ finally:
131
+ acc.close()
132
+ rows = []
133
+ for rec in out:
134
+ m, cl = rec['measured'], rec['claimed'] or {}
135
+ rows.append((rec['epoch'], _when(rec['epoch'] + 1), rec['severity'] or 'not evaluated',
136
+ '%s/%s' % (m.get('req', 0), cl.get('req', '-')), '%s/%s' % (m.get('events_in', 0), cl.get('events_in', '-')),
137
+ '%s/%s' % (m.get('bytes_in', 0), cl.get('bytes_in', '-')), rec['receipts_in'], rec['my_receipts_sent']))
138
+ if a.json:
139
+ return emit(a, out)
140
+ if not rows:
141
+ fail('no windows metered with %s' % pk[:16])
142
+ print('windows with %s (measured/claimed, in the issuer frame)' % pk[:16])
143
+ _table(('window', 'closed UTC', 'severity', 'req', 'events_in', 'bytes_in', 'receipts in', 'sent'), rows)
144
+ return 0
145
+
146
+
147
+ def cmd_peer(a: argparse.Namespace) -> int:
148
+ cfg = dict(app_mod.load_config(a.dir))
149
+ pk = _peer(a.dir, a.peer)
150
+ filt = load_json_arg(a.filter, '--filter')
151
+ if not isinstance(filt, dict):
152
+ fail('--filter must be a nostr filter object')
153
+ if a.every < 1:
154
+ fail('--every must be a positive number of seconds')
155
+ entry = {'pubkey': pk, 'shard': a.shard, 'filter': filt, 'pull_every': a.every}
156
+ others = [p for p in cfg.get('peers', []) if (p['pubkey'], p['shard']) != (pk, a.shard)]
157
+ if entry in cfg.get('peers', []):
158
+ # Already exactly this. Saying "will peer" here made every deploy pass
159
+ # restart every node, which on a public mesh is a round of farewells,
160
+ # replays and conn_rate refusals for nothing.
161
+ return emit(a, {'action': 'peer', 'peer': pk, 'shard': a.shard, 'every': a.every,
162
+ 'peers_configured': len(cfg['peers']), 'changed': False},
163
+ lambda _: print('already peered with %s for shard %s; %d peers configured'
164
+ % (pk[:16], a.shard, len(cfg['peers']))))
165
+ cfg['peers'] = others + [entry]
166
+ app_mod.save_config(a.dir, cfg)
167
+ return emit(a, {'action': 'peer', 'peer': pk, 'shard': a.shard, 'every': a.every, 'peers_configured': len(cfg['peers']), 'changed': True},
168
+ lambda _: print('will peer with %s for shard %s on the next %s; %d peers configured'
169
+ % (pk[:16], a.shard, 'tick' if control.running(a.dir) else 'run', len(cfg['peers']))))
170
+
171
+
172
+ def cmd_unpeer(a: argparse.Namespace) -> int:
173
+ cfg = dict(app_mod.load_config(a.dir))
174
+ pk = _peer(a.dir, a.peer)
175
+ before = len(cfg.get('peers', []))
176
+ cfg['peers'] = [p for p in cfg.get('peers', []) if p['pubkey'] != pk or (a.shard and p['shard'] != a.shard)]
177
+ app_mod.save_config(a.dir, cfg)
178
+ n = before - len(cfg['peers'])
179
+ if not n:
180
+ fail('%s was not being pulled from' % pk[:16])
181
+ return emit(a, {'action': 'unpeer', 'peer': pk, 'shard': a.shard, 'shards_removed': n, 'peers_configured': len(cfg['peers'])},
182
+ lambda _: print('stopped pulling %d shard(s) from %s; the grants stay live (`terminate` ends the peering)' % (n, pk[:16])))
183
+
184
+
185
+ def _trust_table(st: dict) -> None:
186
+ decided = st['decided']
187
+ if not decided:
188
+ print('no operator trust decisions recorded')
189
+ return
190
+ print('%-16s %-12s %-12s %-12s %s' % ('peer', 'decided', 'held', 'state', 'when'))
191
+ for pk, d in sorted(decided.items()):
192
+ when = time.strftime('%Y-%m-%d %H:%M', time.gmtime(d['at'])) if d.get('at') else 'initial'
193
+ print('%-16s %-12s %-12s %-12s %s' % (pk[:16], d.get('tier', '-'), str(d.get('held') or '-'), d.get('state', '-'), when))
194
+ print('\nin force: %d pending: %d overridden: %d waiting: %d' % (
195
+ st.get('in_force', 0), len(st.get('pending') or []),
196
+ len(st.get('overridden') or []), len(st.get('waiting') or [])))
197
+
198
+
199
+ def cmd_trust(a: argparse.Namespace) -> int:
200
+ """The one decision that raises a tier, and it is a person's."""
201
+ if not getattr(a, 'tier', None):
202
+ st = _live(a, 'trust')
203
+ if st is None:
204
+ cfg = app_mod.load_config(a.dir)
205
+ trust_cfg = dict(cfg.get('trust') or {})
206
+ st = {
207
+ 'decided': {
208
+ pk[:16]: {
209
+ 'tier': e.get('tier') if isinstance(e, dict) else e,
210
+ 'at': e.get('at', 0) if isinstance(e, dict) else 0,
211
+ 'held': None,
212
+ 'state': 'offline',
213
+ }
214
+ for pk, e in trust_cfg.items()
215
+ },
216
+ 'in_force': 0,
217
+ 'pending': [],
218
+ 'overridden': [],
219
+ 'waiting': [pk[:16] for pk in trust_cfg],
220
+ }
221
+ decided = st.get('decided') or {}
222
+ if getattr(a, 'peer', None):
223
+ pk_filter = _peer(a.dir, a.peer)
224
+ decided = {k: v for k, v in decided.items() if k == pk_filter[:16] or k == pk_filter}
225
+ out = dict(st)
226
+ out['decided'] = decided
227
+ return emit(a, out, _trust_table)
228
+
229
+ if not getattr(a, 'peer', None):
230
+ fail('meshnode trust: error: setting a tier requires a peer')
231
+
232
+ cfg = dict(app_mod.load_config(a.dir))
233
+ pk = _peer(a.dir, a.peer)
234
+ trust = dict(cfg.get('trust') or {})
235
+ trust[pk] = {'tier': a.tier, 'at': int(time.time())}
236
+ cfg['trust'] = trust
237
+ app_mod.save_config(a.dir, cfg)
238
+
239
+ def seeded(_: dict) -> None:
240
+ print('%s is %s' % (pk[:16], a.tier))
241
+ if _grants(a.dir).get(pk, 'issued') is None:
242
+ print(' no grant issued to it yet; it will open at %s when you peer' % a.tier)
243
+ else:
244
+ print(' the live grant will be re-issued at %s at the next window boundary' % a.tier)
245
+ return emit(a, {'peer': pk, 'tier': a.tier, 'at': trust[pk]['at']}, seeded)
246
+
247
+
248
+ def cmd_terminate(a: argparse.Namespace) -> int:
249
+ pk = _peer(a.dir, a.peer)
250
+ r = _ask(a.dir, 'terminate', peer=pk, reason=a.reason)
251
+
252
+ def human(r: dict) -> None:
253
+ told = r.get('told') or {}
254
+ print('terminated the peering with %s (%s); both grants are zero here' % (pk[:16], r.get('reason')))
255
+ if 'error' in told:
256
+ print(' the peer could not be told: %s' % told['error'])
257
+ else:
258
+ print(' told via %s' % ', '.join(u for u, v in told.items() if v[0]) or 'no relay accepted the message')
259
+ if r.get('pulls_removed'):
260
+ print(' %d scheduled pull(s) removed from the config' % r['pulls_removed'])
261
+ return emit(a, r, human)
262
+
263
+
264
+ def _retired(r: dict) -> None:
265
+ print('retiring: %s' % r.get('reason'))
266
+ for d in r.get('did') or []:
267
+ print(' %s' % ' '.join(str(x) for x in d))
268
+ print(' %d handover(s) to %d peer(s); %d claim(s) withdrawn'
269
+ % (r.get('handed', 0), len(r.get('peers') or []), r.get('withdrawn', 0)))
270
+ if r.get('revoked'):
271
+ print(' the node key is revoked: a successor inherits nothing and starts on probation')
272
+ else:
273
+ print(' the key is NOT revoked; this node can be started again')
274
+ print(' the node is stopping, and says farewell to every peer on the way out')
275
+
276
+
277
+ def cmd_retire(a: argparse.Namespace) -> int:
278
+ """Leave the mesh in order. Not what a restart does."""
279
+ r = _ask(a.dir, 'retire', reason=a.reason, revoke=bool(a.revoke), lapses_in=a.lapses_in)
280
+ return emit(a, r, _retired)
281
+
282
+
283
+ def cmd_withdraw(a: argparse.Namespace) -> int:
284
+ pk = _peer(a.dir, a.peer)
285
+ r = _ask(a.dir, 'withdraw', peer=pk, window=a.window, why=a.why)
286
+
287
+ def human(r: dict | list) -> None:
288
+ for row in (r if isinstance(r, list) else [r]):
289
+ if row['was_published']:
290
+ print('withdrew the report about %s for window %s (%s)' % (pk[:16], row['window'], row['report'][:16]))
291
+ print(' the kind 30802 is not deleted; it carries its own expiration and lapses on it')
292
+ else:
293
+ print('withdrew the finding about %s for window %s; it had not been published'
294
+ % (pk[:16], row['window']))
295
+ print(' the subject was told: %s' % row['why'])
296
+ return emit(a, r, human)
297
+
298
+
299
+ def cmd_standing(a: argparse.Namespace) -> int:
300
+ pk = _peer(a.dir, a.peer)
301
+ target = _peer(a.dir, a.target)
302
+ r = _ask(a.dir, 'standing', peer=pk, target=target)
303
+
304
+ def human(r: dict) -> None:
305
+ if r['answer'] == 'decline':
306
+ print('%s declined to say: %s' % (pk[:16], (r['body'] or {}).get('reason')))
307
+ return
308
+ b = r['body']
309
+ print('%s says of %s: tier %s, %s clean window(s) in a row, %s evaluated, %s'
310
+ % (pk[:16], target[:16], b.get('tier'), b.get('clean_windows'), b.get('evaluated_windows'),
311
+ 'has adverse reports' if b.get('has_adverse_reports') else 'no adverse reports'))
312
+ return emit(a, r, human)
313
+
314
+
315
+ def cmd_reports(a: argparse.Namespace) -> int:
316
+ target = _peer(a.dir, a.target)
317
+ r = _ask(a.dir, 'reports', target=target, timeout=60)
318
+
319
+ def human(r: dict) -> None:
320
+ reps = r.get('reports') or []
321
+ if not reps:
322
+ print('no kind 30802 about %s on this node\'s relays' % target[:16])
323
+ return
324
+ _table(('reporter', 'severity', 'admissible', 'weight here', 'published UTC'),
325
+ [(x['reporter'][:16], x['severity'], 'yes' if x['admissible'] else 'no: ' + '; '.join(x['reasons'])[:60],
326
+ x['weight'], time.strftime('%Y-%m-%d %H:%M', time.gmtime(x['created_at']))) for x in reps])
327
+ print('\nthis node\'s view of %s, its own S0 weighed against them: %s' % (target[:16], r.get('view')))
328
+ return emit(a, r, human)
329
+
330
+
331
+ from ..config.parameter import Parameter
332
+ from .adapter import add_parameter_argument
333
+
334
+ PARAM_WIN_LAST = Parameter(key='last', name='last', description='how many, newest last (0 for all)', type=int, default=24)
335
+
336
+ PARAM_PEER_SHARD = Parameter(key='shard', name='shard', description='shard name', type=str, default='', required=True)
337
+ PARAM_PEER_FILTER = Parameter(key='filter', name='filter', description='a nostr filter, JSON or @file', type=str, default='', required=True)
338
+ PARAM_PEER_EVERY = Parameter(
339
+ key='every', name='every',
340
+ description='seconds between pulls. The default is the probation connection ceiling: four an hour. It was 600, which is six, so a peering that used the shipped default overran the grant it was shipped with',
341
+ type=int, default=900,
342
+ )
343
+
344
+ PARAM_UNPEER_SHARD = Parameter(key='shard', name='shard', description='only this shard', type=str, default='')
345
+
346
+ PARAM_TRUST_PEER = Parameter(key='peer', name='peer', description='the peer to seed a tier for (pubkey, did, npub or prefix)', type=str, default='', positional=True, required=False, nargs='?')
347
+ PARAM_TRUST_TIER = Parameter(key='tier', name='tier', description='the tier to seed for this peer', type=str, default=None, choices=TIERS)
348
+
349
+ PARAM_TERM_REASON = Parameter(key='reason', name='reason', description='reason for termination', type=str, default='operator')
350
+
351
+ PARAM_RET_REASON = Parameter(key='reason', name='reason', description='reason for retirement', type=str, default='operator')
352
+ PARAM_RET_REVOKE = Parameter(key='revoke', name='revoke', description='publish a kind 11802 as well: the key is retired for good and a successor inherits nothing', type=bool, default=False)
353
+ PARAM_RET_LAPSES = Parameter(key='lapses_in', name='lapses_in', cli_name='--lapses-in', description='how long the withdrawn claims stay on the relays before they expire', type=int, default=86400)
354
+
355
+ PARAM_WD_WINDOW = Parameter(key='window', name='window', description='the window epoch the finding is about', type=int, default=0, positional=True, required=True)
356
+ PARAM_WD_WHY = Parameter(key='why', name='why', description='what to tell the subject; recorded on the complaint thread', type=str, default=None)
357
+
358
+ PARAM_ST_TARGET = Parameter(key='target', name='target', description='the node asked about: pubkey, did:nostr or prefix', type=str, default='', positional=True, required=True)
359
+
360
+ PARAM_RP_TARGET = Parameter(key='target', name='target', description='pubkey, did:nostr or prefix', type=str, default='', positional=True, required=True)
361
+
362
+
363
+ class PeeringCommandProvider:
364
+ """Registers peer management, trust tier, bilateral grant, and report commands."""
365
+
366
+ @classmethod
367
+ def register(cls, sub: argparse._SubParsersAction) -> None:
368
+ add_command(sub, 'peers', cmd_peers, 'every peer: tiers, pending grant, last window, standing', js=True)
369
+ add_command(sub, 'grant', cmd_grant, 'both grants with a peer, their history, and what waits for the boundary', peer=True, js=True)
370
+
371
+ w = add_command(sub, 'windows', cmd_windows, 'the windows this node priced for a peer', peer=True, js=True)
372
+ add_parameter_argument(w, PARAM_WIN_LAST)
373
+
374
+ p = add_command(sub, 'peer', cmd_peer, 'add a peer and shard to pull from', peer=True, js=True)
375
+ add_parameter_argument(p, PARAM_PEER_SHARD, require_flag=True)
376
+ add_parameter_argument(p, PARAM_PEER_FILTER, require_flag=True)
377
+ add_parameter_argument(p, PARAM_PEER_EVERY)
378
+
379
+ u = add_command(sub, 'unpeer', cmd_unpeer, 'stop pulling from a peer; the grants stay live', peer=True, js=True)
380
+ add_parameter_argument(u, PARAM_UNPEER_SHARD)
381
+
382
+ t = add_command(sub, 'trust', cmd_trust, "inspect or set a peer's tier, which nothing else can raise", js=True)
383
+ add_parameter_argument(t, PARAM_TRUST_PEER)
384
+ add_parameter_argument(t, PARAM_TRUST_TIER)
385
+
386
+ te = add_command(sub, 'terminate', cmd_terminate, 'end a peering: zero both grants and tell the peer (needs the node running)', peer=True, js=True)
387
+ add_parameter_argument(te, PARAM_TERM_REASON)
388
+
389
+ rt = add_command(sub, 'retire', cmd_retire, 'leave the mesh in order: hand over every asserted shard, withdraw the claims, stop', js=True)
390
+ add_parameter_argument(rt, PARAM_RET_REASON)
391
+ add_parameter_argument(rt, PARAM_RET_REVOKE)
392
+ add_parameter_argument(rt, PARAM_RET_LAPSES)
393
+
394
+ wd = add_command(sub, 'withdraw', cmd_withdraw, 'withdraw a finding this node made about one window (needs the node running)', peer=True, js=True)
395
+ add_parameter_argument(wd, PARAM_WD_WINDOW)
396
+ add_parameter_argument(wd, PARAM_WD_WHY)
397
+
398
+ st = add_command(sub, 'standing', cmd_standing, 'ask a peer what it knows about a third node (needs the node running)', peer=True, js=True)
399
+ add_parameter_argument(st, PARAM_ST_TARGET)
400
+
401
+ r = add_command(sub, 'reports', cmd_reports, 'the kind 30802s about a node, weighed by this one (needs the node running)', js=True)
402
+ add_parameter_argument(r, PARAM_RP_TARGET)