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,564 @@
1
+ """ActivityPub SQLite persistent store.
2
+
3
+ Standard library only. Manages persistence for:
4
+ - Local and remote actors (URI, public key, endpoints, JSON-LD profile).
5
+ - Inbound (inbox) and outbound (outbox) activities.
6
+ - ActivityStreams objects (Notes, Articles, Media, etc.).
7
+ - Follower and Following relational graphs.
8
+ - ActivityStreams OrderedCollection paging.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ from typing import Any, Mapping, Optional, Sequence, Union
15
+
16
+ from fleetmesh_core.conformance.errors import FleetmeshError
17
+ from node.compartment.schema import Column, Index, Table
18
+ from node.storage import SqliteStore
19
+ from node.protocols.activitypub.actor import Actor
20
+ from node.protocols.activitypub.models import AS_CONTEXT, Activity, Note
21
+
22
+ __all__ = [
23
+ 'ActivityPubStoreError',
24
+ 'ActivityPubStore',
25
+ ]
26
+
27
+
28
+ class ActivityPubStoreError(FleetmeshError):
29
+ """Base error for ActivityPub storage operations."""
30
+
31
+
32
+ from node.protocols.isotime import now_iso as _now_iso # noqa: E402
33
+
34
+
35
+ class ActivityPubStore(SqliteStore):
36
+ """Storage for ActivityPub actor profiles, activities and the follow graph.
37
+
38
+ A `SqliteStore` rather than a hand-rolled connection: the base owns the
39
+ connection, the lock, the per-store schema version, transaction control and
40
+ the dialect that rewrites placeholders and upserts for another backend.
41
+ This store opened its own connection, ran its own PRAGMAs and declared its
42
+ schema as a SQL string, so it was SQLite-only and invisible to the
43
+ `Locator` - see docs/storage-backends.md.
44
+
45
+ The six indexes were declared `published DESC`. `Index` takes no direction,
46
+ and none is needed: both backends can scan an ascending index backwards, so
47
+ `(direction, published)` still serves `WHERE direction = ? ORDER BY
48
+ published DESC`.
49
+ """
50
+
51
+ SCHEMA = (
52
+ Table('ap_actors', (
53
+ Column('uri', 'TEXT', primary_key=True),
54
+ Column('type', 'TEXT', not_null=True),
55
+ Column('username', 'TEXT'),
56
+ Column('public_key_pem', 'TEXT'),
57
+ Column('inbox', 'TEXT'),
58
+ Column('outbox', 'TEXT'),
59
+ Column('data_json', 'TEXT', not_null=True),
60
+ Column('updated_at', 'TEXT', not_null=True),
61
+ ), indices=(
62
+ Index('idx_ap_actors_username', ('username',)),
63
+ )),
64
+ Table('ap_activities', (
65
+ Column('id', 'TEXT', primary_key=True),
66
+ Column('type', 'TEXT', not_null=True),
67
+ Column('actor', 'TEXT', not_null=True),
68
+ Column('target', 'TEXT'),
69
+ Column('published', 'TEXT', not_null=True),
70
+ Column('direction', 'TEXT', not_null=True),
71
+ Column('raw_json', 'TEXT', not_null=True),
72
+ ), indices=(
73
+ Index('idx_ap_act_dir', ('direction', 'published')),
74
+ Index('idx_ap_act_actor_dir', ('actor', 'direction', 'published')),
75
+ )),
76
+ Table('ap_objects', (
77
+ Column('id', 'TEXT', primary_key=True),
78
+ Column('type', 'TEXT', not_null=True),
79
+ Column('attributed_to', 'TEXT'),
80
+ Column('published', 'TEXT'),
81
+ Column('content', 'TEXT'),
82
+ Column('raw_json', 'TEXT', not_null=True),
83
+ ), indices=(
84
+ Index('idx_ap_obj_attr', ('attributed_to', 'published')),
85
+ )),
86
+ Table('ap_followers', (
87
+ Column('actor_uri', 'TEXT', not_null=True),
88
+ Column('follower_uri', 'TEXT', not_null=True),
89
+ Column('followed_at', 'TEXT', not_null=True),
90
+ ), primary_key=('actor_uri', 'follower_uri'), indices=(
91
+ Index('idx_ap_followers_actor', ('actor_uri',)),
92
+ )),
93
+ Table('ap_following', (
94
+ Column('actor_uri', 'TEXT', not_null=True),
95
+ Column('target_uri', 'TEXT', not_null=True),
96
+ Column('followed_at', 'TEXT', not_null=True),
97
+ ), primary_key=('actor_uri', 'target_uri'), indices=(
98
+ Index('idx_ap_following_actor', ('actor_uri',)),
99
+ )),
100
+ )
101
+
102
+ def __init__(self, db_path: str = ":memory:"):
103
+ if db_path != ":memory:":
104
+ dirname = os.path.dirname(db_path)
105
+ if dirname:
106
+ os.makedirs(dirname, exist_ok=True)
107
+ super().__init__(db_path)
108
+
109
+ def close(self) -> None:
110
+ super().close()
111
+
112
+ # -------------------------------------------------------------------------
113
+ # Actor Management
114
+ # -------------------------------------------------------------------------
115
+
116
+ def upsert_actor(self, actor: Union[Actor, Mapping[str, Any]]) -> None:
117
+ """Persist or update an Actor document."""
118
+ if isinstance(actor, Actor):
119
+ doc = actor.to_jsonld()
120
+ elif isinstance(actor, (dict, Mapping)):
121
+ doc = dict(actor)
122
+ else:
123
+ raise ActivityPubStoreError("actor must be an Actor instance or dict")
124
+
125
+ uri = doc.get('id')
126
+ if not uri or not isinstance(uri, str):
127
+ raise ActivityPubStoreError("actor document missing 'id' URI")
128
+
129
+ act_type = doc.get('type', 'Person')
130
+ username = doc.get('preferredUsername')
131
+ inbox = doc.get('inbox')
132
+ outbox = doc.get('outbox')
133
+
134
+ pk_pem = None
135
+ pk_obj = doc.get('publicKey')
136
+ if isinstance(pk_obj, dict):
137
+ pk_pem = pk_obj.get('publicKeyPem')
138
+
139
+ data_json = json.dumps(doc, sort_keys=True)
140
+ updated_at = _now_iso()
141
+
142
+ with self._lock:
143
+ with self.transaction() as _cur:
144
+ _cur.execute(
145
+ """
146
+ INSERT INTO ap_actors (uri, type, username, public_key_pem, inbox, outbox, data_json, updated_at)
147
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
148
+ ON CONFLICT(uri) DO UPDATE SET
149
+ type=excluded.type,
150
+ username=excluded.username,
151
+ public_key_pem=excluded.public_key_pem,
152
+ inbox=excluded.inbox,
153
+ outbox=excluded.outbox,
154
+ data_json=excluded.data_json,
155
+ updated_at=excluded.updated_at
156
+ """,
157
+ (uri, act_type, username, pk_pem, inbox, outbox, data_json, updated_at),
158
+ )
159
+
160
+ def get_actor(self, uri: str) -> Optional[Mapping[str, Any]]:
161
+ """Retrieve an Actor document by its URI."""
162
+ with self._lock:
163
+ row = self._one(
164
+ "SELECT data_json FROM ap_actors WHERE uri = ?",
165
+ (uri,),
166
+ )
167
+ if not row:
168
+ return None
169
+ return json.loads(row[0])
170
+
171
+ def get_actor_by_username(self, username: str) -> Optional[Mapping[str, Any]]:
172
+ """Retrieve an Actor document by preferredUsername."""
173
+ with self._lock:
174
+ row = self._one(
175
+ "SELECT data_json FROM ap_actors WHERE LOWER(username) = LOWER(?)",
176
+ (username,),
177
+ )
178
+ if not row:
179
+ return None
180
+ return json.loads(row[0])
181
+
182
+ def get_actor_public_key_pem(self, uri: str) -> Optional[str]:
183
+ """Directly query an actor's public key PEM string."""
184
+ with self._lock:
185
+ row = self._one(
186
+ "SELECT public_key_pem FROM ap_actors WHERE uri = ?",
187
+ (uri,),
188
+ )
189
+ if not row or not row[0]:
190
+ return None
191
+ return row[0]
192
+
193
+ # -------------------------------------------------------------------------
194
+ # Object Management (Notes, Articles, etc.)
195
+ # -------------------------------------------------------------------------
196
+
197
+ def save_object(self, obj: Union[Note, Mapping[str, Any]]) -> None:
198
+ """Persist or update an ActivityStreams object."""
199
+ if isinstance(obj, Note):
200
+ doc = obj.to_dict()
201
+ elif isinstance(obj, (dict, Mapping)):
202
+ doc = dict(obj)
203
+ else:
204
+ raise ActivityPubStoreError("object must be a Note or dict")
205
+
206
+ obj_id = doc.get('id')
207
+ if not obj_id or not isinstance(obj_id, str):
208
+ raise ActivityPubStoreError("object missing 'id'")
209
+
210
+ obj_type = doc.get('type', 'Object')
211
+ attributed_to = doc.get('attributedTo')
212
+ published = doc.get('published') or _now_iso()
213
+ content = doc.get('content')
214
+ raw_json = json.dumps(doc, sort_keys=True)
215
+
216
+ with self._lock:
217
+ with self.transaction() as _cur:
218
+ _cur.execute(
219
+ """
220
+ INSERT INTO ap_objects (id, type, attributed_to, published, content, raw_json)
221
+ VALUES (?, ?, ?, ?, ?, ?)
222
+ ON CONFLICT(id) DO UPDATE SET
223
+ type=excluded.type,
224
+ attributed_to=excluded.attributed_to,
225
+ published=excluded.published,
226
+ content=excluded.content,
227
+ raw_json=excluded.raw_json
228
+ """,
229
+ (obj_id, obj_type, attributed_to, published, content, raw_json),
230
+ )
231
+
232
+ def get_object(self, id: str) -> Optional[Mapping[str, Any]]:
233
+ """Retrieve an ActivityStreams object by ID."""
234
+ with self._lock:
235
+ row = self._one(
236
+ "SELECT raw_json FROM ap_objects WHERE id = ?",
237
+ (id,),
238
+ )
239
+ if not row:
240
+ return None
241
+ return json.loads(row[0])
242
+
243
+ # -------------------------------------------------------------------------
244
+ # Activity Management (Inbox / Outbox)
245
+ # -------------------------------------------------------------------------
246
+
247
+ def record_activity(
248
+ self,
249
+ activity: Union[Activity, Mapping[str, Any]],
250
+ direction: str,
251
+ ) -> None:
252
+ """Record an inbound ('inbox') or outbound ('outbox') activity.
253
+
254
+ If the activity contains an embedded object dictionary, it will automatically
255
+ be persisted to the objects table.
256
+ """
257
+ if direction not in ('inbox', 'outbox'):
258
+ raise ActivityPubStoreError("direction must be 'inbox' or 'outbox'")
259
+
260
+ if isinstance(activity, Activity):
261
+ doc = activity.to_dict()
262
+ elif isinstance(activity, dict):
263
+ doc = dict(activity)
264
+ else:
265
+ raise ActivityPubStoreError("activity must be an Activity or dict")
266
+
267
+ act_id = doc.get('id')
268
+ if not act_id or not isinstance(act_id, str):
269
+ raise ActivityPubStoreError("activity missing 'id'")
270
+
271
+ act_type = doc.get('type')
272
+ if not act_type or not isinstance(act_type, str):
273
+ raise ActivityPubStoreError("activity missing 'type'")
274
+
275
+ actor = doc.get('actor')
276
+ if not actor or not isinstance(actor, str):
277
+ raise ActivityPubStoreError("activity missing 'actor'")
278
+
279
+ target = None
280
+ obj = doc.get('object')
281
+ if isinstance(obj, dict):
282
+ target = obj.get('id')
283
+ # Auto-save embedded object
284
+ try:
285
+ self.save_object(obj)
286
+ except Exception:
287
+ pass
288
+ elif isinstance(obj, str):
289
+ target = obj
290
+
291
+ published = doc.get('published') or _now_iso()
292
+ raw_json = json.dumps(doc, sort_keys=True)
293
+
294
+ with self._lock:
295
+ with self.transaction() as _cur:
296
+ _cur.execute(
297
+ """
298
+ INSERT INTO ap_activities (id, type, actor, target, published, direction, raw_json)
299
+ VALUES (?, ?, ?, ?, ?, ?, ?)
300
+ ON CONFLICT(id) DO UPDATE SET
301
+ type=excluded.type,
302
+ actor=excluded.actor,
303
+ target=excluded.target,
304
+ published=excluded.published,
305
+ direction=excluded.direction,
306
+ raw_json=excluded.raw_json
307
+ """,
308
+ (act_id, act_type, actor, target, published, direction, raw_json),
309
+ )
310
+
311
+ def get_activity(self, id: str) -> Optional[Mapping[str, Any]]:
312
+ """Retrieve an activity by ID."""
313
+ with self._lock:
314
+ row = self._one(
315
+ "SELECT raw_json FROM ap_activities WHERE id = ?",
316
+ (id,),
317
+ )
318
+ if not row:
319
+ return None
320
+ return json.loads(row[0])
321
+
322
+ def list_inbox(
323
+ self,
324
+ actor_uri: Optional[str] = None,
325
+ limit: int = 50,
326
+ offset: int = 0,
327
+ ) -> list[Mapping[str, Any]]:
328
+ """List incoming activities, optionally filtered by actor."""
329
+ with self._lock:
330
+ if actor_uri:
331
+ rows = self._all(
332
+ """
333
+ SELECT raw_json FROM ap_activities
334
+ WHERE direction = 'inbox' AND actor = ?
335
+ ORDER BY published DESC, id DESC
336
+ LIMIT ? OFFSET ?
337
+ """,
338
+ (actor_uri, limit, offset),
339
+ )
340
+ else:
341
+ rows = self._all(
342
+ """
343
+ SELECT raw_json FROM ap_activities
344
+ WHERE direction = 'inbox'
345
+ ORDER BY published DESC, id DESC
346
+ LIMIT ? OFFSET ?
347
+ """,
348
+ (limit, offset),
349
+ )
350
+ return [json.loads(r[0]) for r in rows]
351
+
352
+ def list_outbox(
353
+ self,
354
+ actor_uri: Optional[str] = None,
355
+ limit: int = 50,
356
+ offset: int = 0,
357
+ ) -> list[Mapping[str, Any]]:
358
+ """List outgoing activities, optionally filtered by actor."""
359
+ with self._lock:
360
+ if actor_uri:
361
+ rows = self._all(
362
+ """
363
+ SELECT raw_json FROM ap_activities
364
+ WHERE direction = 'outbox' AND actor = ?
365
+ ORDER BY published DESC, id DESC
366
+ LIMIT ? OFFSET ?
367
+ """,
368
+ (actor_uri, limit, offset),
369
+ )
370
+ else:
371
+ rows = self._all(
372
+ """
373
+ SELECT raw_json FROM ap_activities
374
+ WHERE direction = 'outbox'
375
+ ORDER BY published DESC, id DESC
376
+ LIMIT ? OFFSET ?
377
+ """,
378
+ (limit, offset),
379
+ )
380
+ return [json.loads(r[0]) for r in rows]
381
+
382
+ def count_inbox(self, actor_uri: Optional[str] = None) -> int:
383
+ """Count incoming activities."""
384
+ with self._lock:
385
+ if actor_uri:
386
+ row = self._one(
387
+ "SELECT COUNT(*) FROM ap_activities WHERE direction = 'inbox' AND actor = ?",
388
+ (actor_uri,),
389
+ )
390
+ else:
391
+ row = self._one(
392
+ "SELECT COUNT(*) FROM ap_activities WHERE direction = 'inbox'"
393
+ )
394
+ return int(row[0]) if row else 0
395
+
396
+ def count_outbox(self, actor_uri: Optional[str] = None) -> int:
397
+ """Count outgoing activities."""
398
+ with self._lock:
399
+ if actor_uri:
400
+ row = self._one(
401
+ "SELECT COUNT(*) FROM ap_activities WHERE direction = 'outbox' AND actor = ?",
402
+ (actor_uri,),
403
+ )
404
+ else:
405
+ row = self._one(
406
+ "SELECT COUNT(*) FROM ap_activities WHERE direction = 'outbox'"
407
+ )
408
+ return int(row[0]) if row else 0
409
+
410
+ # -------------------------------------------------------------------------
411
+ # Social Graph (Followers & Following)
412
+ # -------------------------------------------------------------------------
413
+
414
+ def add_follower(
415
+ self,
416
+ actor_uri: str,
417
+ follower_uri: str,
418
+ followed_at: Optional[str] = None,
419
+ ) -> None:
420
+ """Record a follower for an actor."""
421
+ ts = followed_at or _now_iso()
422
+ with self._lock:
423
+ with self.transaction() as _cur:
424
+ _cur.execute(
425
+ """
426
+ INSERT INTO ap_followers (actor_uri, follower_uri, followed_at)
427
+ VALUES (?, ?, ?)
428
+ ON CONFLICT(actor_uri, follower_uri) DO NOTHING
429
+ """,
430
+ (actor_uri, follower_uri, ts),
431
+ )
432
+
433
+ def remove_follower(self, actor_uri: str, follower_uri: str) -> None:
434
+ """Remove a follower relationship."""
435
+ with self._lock:
436
+ with self.transaction() as _cur:
437
+ _cur.execute(
438
+ "DELETE FROM ap_followers WHERE actor_uri = ? AND follower_uri = ?",
439
+ (actor_uri, follower_uri),
440
+ )
441
+
442
+ def get_followers(
443
+ self,
444
+ actor_uri: str,
445
+ limit: int = 100,
446
+ offset: int = 0,
447
+ ) -> list[str]:
448
+ """List follower URIs for an actor."""
449
+ with self._lock:
450
+ rows = self._all(
451
+ """
452
+ SELECT follower_uri FROM ap_followers
453
+ WHERE actor_uri = ?
454
+ ORDER BY followed_at DESC
455
+ LIMIT ? OFFSET ?
456
+ """,
457
+ (actor_uri, limit, offset),
458
+ )
459
+ return [r[0] for r in rows]
460
+
461
+ def count_followers(self, actor_uri: str) -> int:
462
+ """Return the total number of followers for an actor."""
463
+ with self._lock:
464
+ row = self._one(
465
+ "SELECT COUNT(*) FROM ap_followers WHERE actor_uri = ?",
466
+ (actor_uri,),
467
+ )
468
+ return int(row[0]) if row else 0
469
+
470
+ def add_following(
471
+ self,
472
+ actor_uri: str,
473
+ target_uri: str,
474
+ followed_at: Optional[str] = None,
475
+ ) -> None:
476
+ """Record an actor following a remote target."""
477
+ ts = followed_at or _now_iso()
478
+ with self._lock:
479
+ with self.transaction() as _cur:
480
+ _cur.execute(
481
+ """
482
+ INSERT INTO ap_following (actor_uri, target_uri, followed_at)
483
+ VALUES (?, ?, ?)
484
+ ON CONFLICT(actor_uri, target_uri) DO NOTHING
485
+ """,
486
+ (actor_uri, target_uri, ts),
487
+ )
488
+
489
+ def remove_following(self, actor_uri: str, target_uri: str) -> None:
490
+ """Remove a following relationship."""
491
+ with self._lock:
492
+ with self.transaction() as _cur:
493
+ _cur.execute(
494
+ "DELETE FROM ap_following WHERE actor_uri = ? AND target_uri = ?",
495
+ (actor_uri, target_uri),
496
+ )
497
+
498
+ def get_following(
499
+ self,
500
+ actor_uri: str,
501
+ limit: int = 100,
502
+ offset: int = 0,
503
+ ) -> list[str]:
504
+ """List target URIs that an actor follows."""
505
+ with self._lock:
506
+ rows = self._all(
507
+ """
508
+ SELECT target_uri FROM ap_following
509
+ WHERE actor_uri = ?
510
+ ORDER BY followed_at DESC
511
+ LIMIT ? OFFSET ?
512
+ """,
513
+ (actor_uri, limit, offset),
514
+ )
515
+ return [r[0] for r in rows]
516
+
517
+ def count_following(self, actor_uri: str) -> int:
518
+ """Return the total number of actors that an actor follows."""
519
+ with self._lock:
520
+ row = self._one(
521
+ "SELECT COUNT(*) FROM ap_following WHERE actor_uri = ?",
522
+ (actor_uri,),
523
+ )
524
+ return int(row[0]) if row else 0
525
+
526
+ # -------------------------------------------------------------------------
527
+ # OrderedCollection Serialization
528
+ # -------------------------------------------------------------------------
529
+
530
+ @staticmethod
531
+ def as_ordered_collection(
532
+ items: Sequence[Any],
533
+ total_items: int,
534
+ id_url: str,
535
+ page: Optional[int] = None,
536
+ page_size: int = 50,
537
+ ) -> Mapping[str, Any]:
538
+ """Format items as an ActivityStreams 2.0 OrderedCollection or OrderedCollectionPage."""
539
+ if page is None:
540
+ doc = {
541
+ "@context": AS_CONTEXT,
542
+ "id": id_url,
543
+ "type": "OrderedCollection",
544
+ "totalItems": total_items,
545
+ "first": f"{id_url}?page=1",
546
+ }
547
+ if total_items > 0:
548
+ last_page = max(1, (total_items + page_size - 1) // page_size)
549
+ doc["last"] = f"{id_url}?page={last_page}"
550
+ return doc
551
+
552
+ page_doc = {
553
+ "@context": AS_CONTEXT,
554
+ "id": f"{id_url}?page={page}",
555
+ "type": "OrderedCollectionPage",
556
+ "partOf": id_url,
557
+ "totalItems": total_items,
558
+ "orderedItems": list(items),
559
+ }
560
+ if page > 1:
561
+ page_doc["prev"] = f"{id_url}?page={page - 1}"
562
+ if (page * page_size) < total_items:
563
+ page_doc["next"] = f"{id_url}?page={page + 1}"
564
+ return page_doc
@@ -0,0 +1,26 @@
1
+ """FleetMesh Node Arcade Protocol Package (`node.protocols.arcade`)."""
2
+ from __future__ import annotations
3
+
4
+ # The stores and route handlers live in the fleetmesh-protocols package
5
+ # (fleetmesh_protocols.arcade); this package re-exports them so node code and
6
+ # the protocol router keep their import paths.
7
+
8
+ from fleetmesh_protocols.arcade.routes import HttpResult, JSON_CT, handle_arcade_http
9
+ from fleetmesh_protocols.arcade.store import (
10
+ ArcadeError,
11
+ MAX_DESKTOP_JSON_BYTES,
12
+ MAX_RECORD_JSON_BYTES,
13
+ TOMBSTONE_TTL_SEC,
14
+ StandaloneArcadeStore,
15
+ )
16
+
17
+ __all__ = [
18
+ "ArcadeError",
19
+ "MAX_DESKTOP_JSON_BYTES",
20
+ "MAX_RECORD_JSON_BYTES",
21
+ "TOMBSTONE_TTL_SEC",
22
+ "StandaloneArcadeStore",
23
+ "HttpResult",
24
+ "JSON_CT",
25
+ "handle_arcade_http",
26
+ ]
@@ -0,0 +1,13 @@
1
+ """AT Protocol (atproto) package for Fleetmesh.
2
+
3
+ Supports:
4
+ - Repositories, MST storage, and CAR export/import.
5
+ - XRPC HTTP server and Firehose subscribeRepos WebSocket stream.
6
+ - Cross-protocol mapping between Nostr, ActivityPub, and ATProto records.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from .package import AtprotoProtocolPackage
11
+ from .repo import Repo
12
+
13
+ __all__ = ['AtprotoProtocolPackage', 'Repo']