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,304 @@
1
+ """The node's event store: the reference SqliteStore, safe to share across threads.
2
+
3
+ The relay face serves every connection on its own thread and the sync
4
+ sessions run on others, all against one store. sqlite3 connections refuse
5
+ use from a thread other than the one that opened them unless told otherwise,
6
+ and even then two threads must not interleave statements. So: one
7
+ connection opened for any thread, and one lock around every primitive the
8
+ reference defines. The reference's own semantics - what supersedes what,
9
+ tombstones, ranges, fingerprints - are inherited untouched.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from typing import Any, Callable, Iterable, Optional, TypedDict
14
+
15
+ from node.compartment.schema import Column, Index, Table
16
+ from node.interfaces import NostrEvent
17
+ from node.storage import SqliteStore
18
+ from fleetmesh_core.storage import store
19
+
20
+
21
+ class BridgeCursorRecord(TypedDict):
22
+ relay: str
23
+ last_reconciled_at: int
24
+ last_event_at: int
25
+ events_reconciled: int
26
+
27
+
28
+ class NodeStore(SqliteStore, store.SqliteStore):
29
+ LOCAL: str = 'local'
30
+ MESH: str = 'mesh'
31
+
32
+ # The reference's own tables first, verbatim, then this node's additions.
33
+ # The connection, PRAGMAs and lock come from BaseSqliteStore; the
34
+ # reference's query semantics - what supersedes what, tombstones, ranges,
35
+ # fingerprints - come from store.SqliteStore, whose __init__ is bypassed
36
+ # because the base already opened the connection and built the schema.
37
+ SCHEMA = (
38
+ Table('events', (
39
+ Column('id', 'TEXT', primary_key=True),
40
+ Column('pubkey', 'TEXT'),
41
+ Column('created_at', 'INTEGER'),
42
+ Column('kind', 'INTEGER'),
43
+ Column('json', 'TEXT'),
44
+ ), indices=(
45
+ Index('events_created_at', ('created_at', 'id')),
46
+ # Supersession and deletion ask for one author's events, and
47
+ # asked it of the whole table until this existed.
48
+ Index('events_author_kind', ('pubkey', 'kind')),
49
+ Index('events_kind_created_at', ('kind', 'created_at', 'id')),
50
+ )),
51
+ Table('tombstones', (
52
+ Column('key', 'TEXT', not_null=True),
53
+ Column('deleter', 'TEXT', not_null=True),
54
+ ), primary_key=('key', 'deleter')),
55
+ # Where each event came from. Not a protocol field and not in the
56
+ # reference: the specification only says what a node may *claim*,
57
+ # and forbids asserting completeness over a shard it holds only
58
+ # through mesh replication, which is how third-party replication
59
+ # would launder unverified peer data into an authoritative record.
60
+ # Knowing which is which is this implementation's own problem.
61
+ Table('origin', (
62
+ Column('id', 'TEXT', primary_key=True),
63
+ Column('origin', 'TEXT', not_null=True),
64
+ )),
65
+ # Single-letter tags, which is the set NIP-01 indexes. A client
66
+ # asking for its own gift wraps decoded every wrap in the store
67
+ # without this.
68
+ Table('tags', (
69
+ Column('id', 'TEXT', not_null=True),
70
+ Column('name', 'TEXT', not_null=True),
71
+ Column('value', 'TEXT', not_null=True),
72
+ ), primary_key=('id', 'name', 'value'), indices=(
73
+ Index('tags_name_value', ('name', 'value')),
74
+ ), without_rowid=True),
75
+ # Persistent checkpoints for external relay bridge reconciliation.
76
+ Table('bridge_cursor', (
77
+ Column('relay', 'TEXT', primary_key=True),
78
+ Column('last_reconciled_at', 'INTEGER'),
79
+ Column('last_event_at', 'INTEGER'),
80
+ Column('events_reconciled', 'INTEGER', default=0),
81
+ )),
82
+ )
83
+
84
+ def __init__(self, path: str = ':memory:', **kw: Any) -> None:
85
+ SqliteStore.__init__(self, path, **kw)
86
+ # After the lock exists and not before it. _index_tags goes through
87
+ # _locked like every other writer, so backfilling above this line
88
+ # raised AttributeError on the way up and took every node down at once
89
+ # - which is the same mistake, in the same shape, as the Reporter that
90
+ # read its account store four lines above the ledger it filled.
91
+ self._backfill_tags()
92
+ self.origin_swept: int = self._sweep_origin()
93
+
94
+ def _sweep_origin(self) -> int:
95
+ """Drop provenance rows for events this store no longer holds.
96
+
97
+ `_drop` used to leave them: every superseded replaceable event and
98
+ every NIP-09 deletion left its `origin` row behind - measured at two
99
+ events held against seven rows - and nothing ever read or removed
100
+ them (audit 2026-10-15, B3). `_drop` clears its own row now, which
101
+ stops that source. One remains by design: sync records a delivered
102
+ event's origin before deciding whether to keep it (`sync/initiator`),
103
+ so a delivery that is refused leaves a row too. Hence on every open,
104
+ not once: after the first it removes only those, in one indexed
105
+ anti-join, before anything else can write.
106
+ """
107
+ with self._lock:
108
+ cur = self.execute('DELETE FROM origin WHERE id NOT IN (SELECT id FROM events)')
109
+ self.db.commit()
110
+ return max(0, int(cur.rowcount or 0))
111
+
112
+ @staticmethod
113
+ def _locked(name: str) -> Callable[..., Any]:
114
+ def method(self: Any, *a: Any, **k: Any) -> Any:
115
+ with self._lock:
116
+ return getattr(store.SqliteStore, name)(self, *a, **k)
117
+ method.__name__ = name
118
+ return method
119
+
120
+ _put = _locked('_put')
121
+
122
+ def _drop(self, event_id: str) -> None:
123
+ """The reference's drop, plus this node's own provenance row.
124
+
125
+ Called by supersession and by a NIP-09 deletion, inside the one
126
+ transaction `insert_event` holds (`_atomic` joins it), so the event,
127
+ its tags and its origin go together or not at all. `forget` already
128
+ removed all three; this is the other way an event leaves (B3).
129
+ """
130
+ with self._lock, self._atomic():
131
+ store.SqliteStore._drop(self, event_id)
132
+ self.execute('DELETE FROM origin WHERE id = ?', (event_id,))
133
+
134
+ def _all(self, q: Optional[str] = None, args: Any = ()) -> list[Any]:
135
+ if q is not None:
136
+ return SqliteStore._all(self, q, args)
137
+ with self._lock:
138
+ return store.SqliteStore._all(self)
139
+ _get = _locked('_get')
140
+ # The narrowing reads go through the lock like every other primitive: the
141
+ # relay face serves each connection on its own thread and the sync sessions
142
+ # run on others, and two threads must not interleave statements on one
143
+ # connection whatever the statement is.
144
+ _narrow = _locked('_narrow')
145
+ # Held across the read rather than around a fetchall: the point of it is
146
+ # that it stops early, and a fetchall would put the whole match set back
147
+ # in memory as the rows it was written to avoid parsing.
148
+ newest_matching = _locked('newest_matching')
149
+ _index_tags = _locked('_index_tags')
150
+ _by_author_kind = _locked('_by_author_kind')
151
+ _by_author = _locked('_by_author')
152
+ _tombstone = _locked('_tombstone')
153
+ _tombstoned_by = _locked('_tombstoned_by')
154
+ # insert_event reads then writes; hold the lock across the whole decision.
155
+ insert_event = _locked('insert_event')
156
+
157
+ def by_kind_before(self, kind: int, cutoff: int, cap: Optional[int] = None,
158
+ after: Optional[tuple[int, str]] = None) -> list[NostrEvent]:
159
+ """Events of one kind dated before a cutoff, oldest first, one page.
160
+
161
+ For retention, which is the only caller. The index is on
162
+ (created_at, id), so this reads a range rather than the table.
163
+
164
+ `cap` and `after` are what keep the sweep from being the thing that
165
+ kills the node: a policy set for the first time on a face holding days
166
+ of gift wraps has every one of them past the cutoff at once, and this
167
+ used to answer with all of them parsed. `after` is the (created_at, id)
168
+ of the last row of the previous page, and the cursor rather than an
169
+ offset because the sweep is deleting as it walks - and because an event
170
+ held back inside a claimed shard stays, so a page that re-read what it
171
+ had refused would never end.
172
+ """
173
+ import json as _json
174
+ where: list[str] = ['kind = ?', 'created_at < ?']
175
+ args: list[int | str] = [int(kind), int(cutoff)]
176
+ if after is not None:
177
+ where.append('(created_at, id) > (?, ?)')
178
+ args.extend([int(after[0]), str(after[1])])
179
+ sql = ('SELECT json FROM events WHERE ' + ' AND '.join(where)
180
+ + ' ORDER BY created_at, id')
181
+ if cap is not None:
182
+ sql += ' LIMIT ?'
183
+ args.append(int(cap))
184
+ with self._lock:
185
+ rows = self.execute(sql, args).fetchall()
186
+ return [_json.loads(r[0]) for r in rows]
187
+
188
+ def count(self) -> int:
189
+ """How many events this node holds, as one number from the index.
190
+
191
+ Written into status.json on every tick, and until this existed it was
192
+ `len(self._all())`: the whole store parsed into dicts to be thrown
193
+ away, so the node's peak heap was the size of its store and the
194
+ allocator kept it. On the public face that was 600 MB of a 1 GB box
195
+ for a number that a COUNT answers without reading a row.
196
+ """
197
+ with self._lock:
198
+ return int(self.execute('SELECT COUNT(*) FROM events').fetchone()[0])
199
+
200
+ def forget(self, event_id: str) -> bool:
201
+ """Drop one event because this node stopped keeping it. NOT a deletion.
202
+
203
+ A NIP-09 kind 5 tombstones an id so a later re-offer cannot resurrect
204
+ it, because there the author said the thing should not exist. Retention
205
+ says only that THIS node stopped holding it, which is a fact about a
206
+ disk and not about the event: a peer may hand it back tomorrow and be
207
+ right to. So the row goes and no tombstone is written.
208
+ """
209
+ with self._lock:
210
+ cur = self.execute('DELETE FROM events WHERE id = ?', (event_id,))
211
+ self.execute('DELETE FROM origin WHERE id = ?', (event_id,))
212
+ self.execute('DELETE FROM tags WHERE id = ?', (event_id,))
213
+ self.db.commit()
214
+ return cur.rowcount > 0
215
+
216
+ def note_origin(self, event_id: str, origin: str) -> None:
217
+ """Record how one event reached this node. First writer wins: an event
218
+ this node already held locally is not reclassified because a peer later
219
+ sent it too."""
220
+ if origin not in (self.LOCAL, self.MESH):
221
+ raise ValueError('origin is local or mesh')
222
+ with self._lock:
223
+ self.execute('INSERT OR IGNORE INTO origin VALUES (?,?)', (event_id, origin))
224
+ self.db.commit()
225
+
226
+ def origin_of(self, event_id: str) -> Optional[str]:
227
+ with self._lock:
228
+ row = self.execute('SELECT origin FROM origin WHERE id = ?', (event_id,)).fetchone()
229
+ return row[0] if row else None
230
+
231
+ def any_replicated(self, event_ids: Iterable[str]) -> bool:
232
+ """True if any of these arrived through the mesh, or is of unknown origin.
233
+
234
+ Unknown counts as replicated. A claim of completeness is a falsifiable
235
+ assertion about data this node stands behind, and an event it cannot
236
+ account for the arrival of is not data it can stand behind.
237
+ """
238
+ ids = list(event_ids)
239
+ if not ids:
240
+ return False
241
+ with self._lock:
242
+ known: dict[str, str] = {}
243
+ # Chunked: a have/need may carry validate.MAX_IDS ids, past what
244
+ # SQLite binds in one statement.
245
+ for chunk in store.chunked(ids):
246
+ sql, vals = store.in_clause('id', chunk)
247
+ known.update((r[0], r[1]) for r in self.execute(
248
+ 'SELECT id, origin FROM origin WHERE ' + sql, vals))
249
+ return any(known.get(i) != self.LOCAL for i in ids)
250
+
251
+ def get_bridge_cursor(self, relay_url: str) -> Optional[BridgeCursorRecord]:
252
+ with self._lock:
253
+ cur = self.db.cursor()
254
+ cur.execute('SELECT last_reconciled_at, last_event_at, events_reconciled FROM bridge_cursor WHERE relay = ?', (relay_url,))
255
+ row = cur.fetchone()
256
+ if row is None:
257
+ return None
258
+ return {
259
+ 'relay': relay_url,
260
+ 'last_reconciled_at': row[0],
261
+ 'last_event_at': row[1],
262
+ 'events_reconciled': row[2],
263
+ }
264
+
265
+ def set_bridge_cursor(
266
+ self,
267
+ relay_url: str,
268
+ last_reconciled_at: Optional[int] = None,
269
+ last_event_at: Optional[int] = None,
270
+ count_increment: int = 0,
271
+ ) -> None:
272
+ with self._lock:
273
+ cur = self.db.cursor()
274
+ cur.execute('SELECT last_reconciled_at, last_event_at, events_reconciled FROM bridge_cursor WHERE relay = ?', (relay_url,))
275
+ row = cur.fetchone()
276
+ if row is None:
277
+ cur.execute(
278
+ 'INSERT INTO bridge_cursor (relay, last_reconciled_at, last_event_at, events_reconciled) VALUES (?, ?, ?, ?)',
279
+ (relay_url, last_reconciled_at or 0, last_event_at or 0, count_increment)
280
+ )
281
+ else:
282
+ new_reconciled = last_reconciled_at if last_reconciled_at is not None else row[0]
283
+ new_event_at = max(row[1] or 0, last_event_at or 0)
284
+ new_count = (row[2] or 0) + count_increment
285
+ cur.execute(
286
+ 'UPDATE bridge_cursor SET last_reconciled_at = ?, last_event_at = ?, events_reconciled = ? WHERE relay = ?',
287
+ (new_reconciled, new_event_at, new_count, relay_url)
288
+ )
289
+ self.db.commit()
290
+
291
+ def all_bridge_cursors(self) -> list[BridgeCursorRecord]:
292
+ with self._lock:
293
+ cur = self.db.cursor()
294
+ cur.execute('SELECT relay, last_reconciled_at, last_event_at, events_reconciled FROM bridge_cursor ORDER BY relay')
295
+ rows = cur.fetchall()
296
+ return [
297
+ {
298
+ 'relay': r[0],
299
+ 'last_reconciled_at': r[1],
300
+ 'last_event_at': r[2],
301
+ 'events_reconciled': r[3],
302
+ }
303
+ for r in rows
304
+ ]
@@ -0,0 +1,182 @@
1
+ """fleetmesh-protocol-nostr: Decoupled Nostr & Blossom protocol package.
2
+
3
+ Conforms to ProtocolPackage (fleetmesh_core.wire.interfaces.ProtocolPackage).
4
+ Wraps RelayServer and BlossomServer, allowing them to be mounted onto
5
+ ProtocolHostRouter or operated with zero listening overhead when disabled.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from fleetmesh_core.wire import spelling # noqa: E402
10
+
11
+ from typing import Any, Callable, Mapping, Optional
12
+
13
+ from fleetmesh_core.wire.interfaces import (
14
+ ProtocolHealth,
15
+ ProtocolHostRouter,
16
+ ProtocolIdentityAdapter,
17
+ )
18
+ from . import relayserver
19
+
20
+
21
+ class NostrIdentityAdapter:
22
+ """Identity and assertion adapter for Nostr (secp256k1 / BIP-340)."""
23
+
24
+ def __init__(self, keystore: Any) -> None:
25
+ self.ks = keystore
26
+
27
+ def resolve_id(self, uri: str) -> Optional[str]:
28
+ # Handle npub1... or hex
29
+ if uri.startswith('npub1'):
30
+ try:
31
+ from fleetmesh_core.crypto import bech32
32
+ raw = bech32.bech32_decode('npub', uri)
33
+ if raw is not None:
34
+ return raw.hex()
35
+ except Exception:
36
+ pass
37
+ elif spelling.is_hex(uri, 64, upper=True):
38
+ return uri.lower()
39
+ return None
40
+
41
+ def sign_assertion(self, payload: bytes) -> bytes:
42
+ import hashlib
43
+ from fleetmesh_core.crypto import crypto
44
+ msg32 = hashlib.sha256(payload).digest() if len(payload) != 32 else payload
45
+ sk = getattr(self.ks, 'node_sk', None)
46
+ sk_hex = sk.hex() if isinstance(sk, (bytes, bytearray)) else str(sk or '')
47
+ sig = crypto.schnorr_sign(msg32, sk_hex)
48
+ return bytes.fromhex(sig)
49
+
50
+ def verify_assertion(self, payload: bytes, signature: bytes, pubkey: str) -> bool:
51
+ import hashlib
52
+ from fleetmesh_core.crypto import crypto
53
+ msg32 = hashlib.sha256(payload).digest() if len(payload) != 32 else payload
54
+ try:
55
+ return crypto.schnorr_verify(msg32, pubkey, signature.hex())
56
+ except Exception:
57
+ return False
58
+
59
+
60
+ class NostrProtocolPackage:
61
+ """The Nostr & Blossom protocol suite for Fleetmesh."""
62
+
63
+ name: str = 'nostr'
64
+ version: str = '1.0.0'
65
+ standards: tuple[str, ...] = (
66
+ 'NIP-01', 'NIP-11', 'NIP-40', 'NIP-42', 'NIP-44', 'NIP-59', 'NIP-77',
67
+ 'BUD-01', 'BUD-02', 'BUD-04',
68
+ )
69
+
70
+ def __init__(
71
+ self,
72
+ backend: Optional[Any] = None,
73
+ blob_store: Optional[Any] = None,
74
+ grants: Optional[Any] = None,
75
+ node: Optional[Any] = None,
76
+ clock: Optional[Callable[[], int]] = None,
77
+ name: str = 'meshnode',
78
+ pubkey: Optional[str] = None,
79
+ ) -> None:
80
+ self.backend = backend
81
+ self.blob_store = blob_store
82
+ self.grants = grants
83
+ self.node = node
84
+ self.clock = clock
85
+ self.node_name = name
86
+ self.pubkey = pubkey
87
+ self.config: Mapping[str, Any] = {}
88
+ self.relay_server: Optional[relayserver.RelayServer] = None
89
+ self._host: Optional[ProtocolHostRouter] = None
90
+ self._enabled: bool = True
91
+
92
+ def configure(self, config: Mapping[str, Any]) -> None:
93
+ self.config = dict(config)
94
+ self._enabled = self.config.get('enabled', True)
95
+
96
+ def identity_adapter(self, keystore: Any) -> Optional[ProtocolIdentityAdapter]:
97
+ if keystore is None:
98
+ return None
99
+ return NostrIdentityAdapter(keystore)
100
+
101
+ def mount_ingress(self, host: ProtocolHostRouter) -> None:
102
+ if not self._enabled:
103
+ return
104
+ self._host = host
105
+ if self.relay_server is None:
106
+ self.relay_server = relayserver.RelayServer(
107
+ backend=self.backend,
108
+ blob_store=self.blob_store,
109
+ grants=self.grants,
110
+ node=self.node,
111
+ clock=self.clock,
112
+ name=self.node_name,
113
+ pubkey=self.pubkey,
114
+ future_seconds=self.config.get('future_seconds', 900),
115
+ require_auth=self.config.get('require_auth', False),
116
+ host_router=host,
117
+ )
118
+ self.relay_server.mount_ingress(host, owner='nostr')
119
+
120
+ def transport_rung(self, context: Any) -> Optional[Any]:
121
+ # Outbound RelayRung for TransportRouter
122
+ if not self._enabled:
123
+ return None
124
+ from node.transport.relay_rung import RelayRung
125
+ from node.transport.rung import RelayRungAdapter
126
+ if context is not None and hasattr(context, 'nostr'):
127
+ return RelayRungAdapter(context)
128
+ if context is not None and hasattr(context, 'ks') and hasattr(context, 'peers'):
129
+ return RelayRung(
130
+ keystore=context.ks,
131
+ peers=context.peers,
132
+ relay_urls=self.config.get('relays', ()),
133
+ clock=context.clock,
134
+ )
135
+ return None
136
+
137
+ def compartments(self) -> tuple[type[Any], ...]:
138
+ # Background compartments associated with Nostr
139
+ return ()
140
+
141
+ def start(self) -> None:
142
+ if not self._enabled:
143
+ return
144
+ if self.relay_server is not None:
145
+ self.relay_server.start()
146
+
147
+ def stop(self) -> None:
148
+ if self.relay_server is not None:
149
+ self.relay_server.stop()
150
+
151
+ def health(self) -> ProtocolHealth:
152
+ if not self._enabled:
153
+ return ProtocolHealth(
154
+ healthy=True,
155
+ status="disabled",
156
+ connections_active=0,
157
+ messages_in=0,
158
+ messages_out=0,
159
+ errors=(),
160
+ )
161
+ if self.relay_server is not None:
162
+ return ProtocolHealth(
163
+ healthy=True,
164
+ status="serving",
165
+ connections_active=len(getattr(self.relay_server, '_conns', {})),
166
+ messages_in=self.relay_server.accepted,
167
+ messages_out=0,
168
+ errors=(),
169
+ )
170
+ return ProtocolHealth(
171
+ healthy=True,
172
+ status="configured",
173
+ connections_active=0,
174
+ messages_in=0,
175
+ messages_out=0,
176
+ errors=(),
177
+ )
178
+
179
+ def descriptor_capabilities(self) -> tuple[tuple[str, str], ...]:
180
+ if not self._enabled:
181
+ return ()
182
+ return (('c', 'relay'), ('c', 'negentropy'))
@@ -0,0 +1,109 @@
1
+ """What this node knows about other nodes: their live descriptors.
2
+
3
+ A peer is addressed by its node pubkey. From its descriptor come the
4
+ agreement key an envelope is encrypted to, the class, the protocols it
5
+ implements and the endpoints. Descriptors are cached and refreshed from
6
+ relays when missing or expired, and every one goes through
7
+ fleetmesh_core.identity.validate_descriptor before it is believed.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import threading
12
+ import time
13
+ from typing import Callable, Optional, Sequence
14
+
15
+ from cryptography.hazmat.primitives.asymmetric import x25519
16
+ from fleetmesh_core.crypto import keys # noqa: E402
17
+ from fleetmesh_core.identity import identity # noqa: E402
18
+ from node.interfaces import NostrEvent # noqa: E402
19
+ from . import descriptor # noqa: E402
20
+
21
+
22
+ def agreement_key(desc: NostrEvent) -> Optional[x25519.X25519PublicKey]:
23
+ """The X25519 public key a descriptor's agreement Multikey names, or None."""
24
+ for t in desc.get('tags', []):
25
+ if t and t[0] == 'key' and len(t) > 2 and t[1] == 'agreement':
26
+ kind, raw = keys.parse_multikey(t[2])
27
+ if kind == 'agreement':
28
+ return keys.agreement_public(raw)
29
+ return None
30
+
31
+
32
+ from fleetmesh_core.identity.did import node_of_did
33
+
34
+ __all__ = ['Peers', 'agreement_key']
35
+
36
+
37
+ class Peers:
38
+ # A descriptor that is not on the relays is asked for again on every
39
+ # message its key sends, and the asking happens on the transport's
40
+ # listener thread with the relay timeout in front of it. A stranger with
41
+ # no descriptor could hold that thread for fifteen seconds per wrap. A
42
+ # miss is remembered for this long before the relays are asked again.
43
+ MISS_TTL: int = 60
44
+
45
+ def __init__(
46
+ self,
47
+ relay_urls: Sequence[str] = (),
48
+ clock: Optional[Callable[[], int]] = None,
49
+ timeout: float = 15.0,
50
+ resolver: Optional[Callable[[str, Sequence[str], Optional[int], float], Optional[NostrEvent]]] = None,
51
+ ) -> None:
52
+ self.relays: list[str] = list(relay_urls)
53
+ self.clock: Callable[[], int] = clock or (lambda: int(time.time()))
54
+ self.timeout: float = timeout
55
+ # How a descriptor not held is looked up; given so a test can count
56
+ # the lookups without touching the descriptor module.
57
+ self.resolver = resolver or (lambda pk, urls, now, timeout: descriptor.resolve(pk, urls, now=now, timeout=timeout))
58
+ self._cache: dict[str, NostrEvent] = {}
59
+ self._missing: dict[str, int] = {} # pubkey -> when a failed resolve may be retried
60
+ self._lock: threading.Lock = threading.Lock()
61
+
62
+ def learn(self, desc: NostrEvent) -> bool:
63
+ """Accept a descriptor if it is genuine, validates and is newer than what we hold."""
64
+ if not descriptor.genuine(desc) or identity.validate_descriptor(desc, now=self.clock(), digest=descriptor.DIGEST):
65
+ return False
66
+ with self._lock:
67
+ have = self._cache.get(desc['pubkey'])
68
+ if have is None or desc['created_at'] > have['created_at']:
69
+ self._cache[desc['pubkey']] = desc
70
+ self._missing.pop(desc['pubkey'], None)
71
+ return True
72
+ return False
73
+
74
+ def descriptor(self, pubkey: str, refresh: bool = False) -> Optional[NostrEvent]:
75
+ now = self.clock()
76
+ with self._lock:
77
+ d = self._cache.get(pubkey)
78
+ if d is not None and not refresh and not identity.validate_descriptor(d, now=now, digest=descriptor.DIGEST):
79
+ return d
80
+ with self._lock:
81
+ until = self._missing.get(pubkey)
82
+ if until is not None and now < until and not refresh:
83
+ return None
84
+ if self.relays:
85
+ fresh = self.resolver(pubkey, self.relays, now, self.timeout)
86
+ if fresh is not None:
87
+ self.learn(fresh)
88
+ return fresh
89
+ with self._lock:
90
+ self._missing[pubkey] = now + self.MISS_TTL
91
+ return None
92
+
93
+ def agreement(self, pubkey: str) -> Optional[x25519.X25519PublicKey]:
94
+ d = self.descriptor(pubkey)
95
+ return agreement_key(d) if d is not None else None
96
+
97
+ def resolve_sender(self, skid: str) -> Optional[x25519.X25519PublicKey]:
98
+ """For didcomm.decrypt: the static agreement key a skid names, if the
99
+ descriptor behind it is live. The kid must be the #agree fragment."""
100
+ if not skid.endswith('#agree'):
101
+ return None
102
+ try:
103
+ return self.agreement(node_of_did(skid))
104
+ except ValueError:
105
+ return None
106
+
107
+ def known(self) -> list[str]:
108
+ with self._lock:
109
+ return sorted(self._cache)