fedipod-server 0.29.0 → 0.31.0

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 (444) hide show
  1. package/README.md +16 -6
  2. package/dist/claims.d.ts +2 -0
  3. package/dist/claims.js +13 -4
  4. package/dist/door.d.ts +4 -0
  5. package/dist/door.js +31 -0
  6. package/dist/fedipod.d.ts +15 -0
  7. package/dist/fedipod.js +13 -0
  8. package/dist/front.d.ts +3 -0
  9. package/dist/front.js +92 -0
  10. package/dist/handler.d.ts +36 -101
  11. package/dist/handler.js +93 -644
  12. package/dist/handler.jsonld +2 -30
  13. package/dist/identities.d.ts +71 -0
  14. package/dist/identities.js +236 -0
  15. package/dist/mounts.d.ts +43 -0
  16. package/dist/mounts.js +123 -0
  17. package/dist/opt-in.d.ts +46 -0
  18. package/dist/opt-in.js +176 -0
  19. package/dist/private-read.d.ts +4 -0
  20. package/dist/private-read.js +34 -0
  21. package/package.json +12 -20
  22. package/lib/client/c2s.mjs +0 -636
  23. package/lib/client/localapi.mjs +0 -87
  24. package/lib/client/masto/accounts.mjs +0 -310
  25. package/lib/client/masto/body.mjs +0 -156
  26. package/lib/client/masto/index.mjs +0 -206
  27. package/lib/client/masto/instance.mjs +0 -112
  28. package/lib/client/masto/media.mjs +0 -79
  29. package/lib/client/masto/oauth.mjs +0 -656
  30. package/lib/client/masto/render.mjs +0 -518
  31. package/lib/client/masto/statuses.mjs +0 -372
  32. package/lib/client/masto/timelines.mjs +0 -438
  33. package/lib/client/oidc-auth.mjs +0 -62
  34. package/lib/client/streaming.mjs +0 -154
  35. package/lib/client/webpush.mjs +0 -98
  36. package/lib/connections/acctfeed.mjs +0 -257
  37. package/lib/connections/atproto.mjs +0 -309
  38. package/lib/connections/bskyfeed.mjs +0 -243
  39. package/lib/connections/bskygroup.mjs +0 -162
  40. package/lib/connections/fediacct.mjs +0 -253
  41. package/lib/connections/import.mjs +0 -357
  42. package/lib/connections/tagfeed.mjs +0 -212
  43. package/lib/connections/vault.mjs +0 -114
  44. package/lib/core/as2.mjs +0 -206
  45. package/lib/core/contexts/activitystreams.json +0 -379
  46. package/lib/core/contexts/anno.json +0 -126
  47. package/lib/core/contexts/did-v1.json +0 -57
  48. package/lib/core/contexts/fep-5711.json +0 -36
  49. package/lib/core/contexts/gotosocial.json +0 -86
  50. package/lib/core/contexts/identity-v1.json +0 -152
  51. package/lib/core/contexts/index.mjs +0 -63
  52. package/lib/core/contexts/join-lemmy.json +0 -33
  53. package/lib/core/contexts/joinmastodon.json +0 -28
  54. package/lib/core/contexts/litepub-0.1.json +0 -60
  55. package/lib/core/contexts/map.json +0 -19
  56. package/lib/core/contexts/miscellany.json +0 -19
  57. package/lib/core/contexts/quotes.json +0 -18
  58. package/lib/core/contexts/schemaorg.json +0 -8845
  59. package/lib/core/contexts/security-data-integrity-v1.json +0 -78
  60. package/lib/core/contexts/security-data-integrity-v2.json +0 -81
  61. package/lib/core/contexts/security-multikey-v1.json +0 -35
  62. package/lib/core/contexts/security-v1.json +0 -74
  63. package/lib/core/contexts/webfinger.json +0 -10
  64. package/lib/core/deliver.mjs +0 -387
  65. package/lib/core/graphview.mjs +0 -327
  66. package/lib/core/intake/activities.mjs +0 -649
  67. package/lib/core/intake/activity.mjs +0 -318
  68. package/lib/core/intake/channel.mjs +0 -144
  69. package/lib/core/intake/group.mjs +0 -258
  70. package/lib/core/intake/index.mjs +0 -732
  71. package/lib/core/intake/notes.mjs +0 -367
  72. package/lib/core/intake/verify.mjs +0 -142
  73. package/lib/core/keys.mjs +0 -153
  74. package/lib/core/lease.mjs +0 -206
  75. package/lib/core/place.mjs +0 -82
  76. package/lib/core/polls.mjs +0 -105
  77. package/lib/core/profile-page.mjs +0 -104
  78. package/lib/core/proof.mjs +0 -79
  79. package/lib/core/publisher/collections.mjs +0 -272
  80. package/lib/core/publisher/index.mjs +0 -522
  81. package/lib/core/publisher/notes.mjs +0 -519
  82. package/lib/core/publisher/own.mjs +0 -143
  83. package/lib/core/publisher/questions.mjs +0 -241
  84. package/lib/core/publisher/restore.mjs +0 -224
  85. package/lib/core/shapes/activitystreams.ttl +0 -157
  86. package/lib/core/shapes/index.mjs +0 -132
  87. package/lib/core/shapes/shapes-text.mjs +0 -13
  88. package/lib/core/social.mjs +0 -561
  89. package/lib/core/storage.mjs +0 -176
  90. package/lib/core/store.mjs +0 -578
  91. package/lib/core/wire-quotes.mjs +0 -71
  92. package/lib/core/wire.mjs +0 -925
  93. package/lib/device/account.mjs +0 -87
  94. package/lib/device/admin/index.mjs +0 -13
  95. package/lib/device/admin/origins.mjs +0 -35
  96. package/lib/device/admin/routes/connections.mjs +0 -144
  97. package/lib/device/admin/routes/gateway.mjs +0 -199
  98. package/lib/device/admin/routes/lifecycle.mjs +0 -200
  99. package/lib/device/admin/routes/owner.mjs +0 -322
  100. package/lib/device/admin/routes/setup.mjs +0 -428
  101. package/lib/device/admin/routes/social.mjs +0 -188
  102. package/lib/device/admin/server.mjs +0 -95
  103. package/lib/device/admin/static.mjs +0 -247
  104. package/lib/device/admin/surface.mjs +0 -315
  105. package/lib/device/certs.mjs +0 -281
  106. package/lib/device/cli/commands/account.mjs +0 -586
  107. package/lib/device/cli/commands/run.mjs +0 -278
  108. package/lib/device/cli/commands/service.mjs +0 -221
  109. package/lib/device/cli/commands/setup.mjs +0 -489
  110. package/lib/device/cli/commands/state.mjs +0 -561
  111. package/lib/device/cli/context.mjs +0 -288
  112. package/lib/device/export-collections.mjs +0 -161
  113. package/lib/device/gateway-move.mjs +0 -58
  114. package/lib/device/home.mjs +0 -161
  115. package/lib/device/migrate.mjs +0 -115
  116. package/lib/device/ports.mjs +0 -27
  117. package/lib/device/remote.mjs +0 -54
  118. package/lib/device/setup.mjs +0 -496
  119. package/lib/device/update.mjs +0 -96
  120. package/lib/gateway/caches.mjs +0 -36
  121. package/lib/gateway/directory.mjs +0 -90
  122. package/lib/gateway/front-core.mjs +0 -966
  123. package/lib/gateway/gateway-core.mjs +0 -244
  124. package/lib/gateway/headers.mjs +0 -49
  125. package/lib/gateway/httpsig.mjs +0 -159
  126. package/lib/gateway/notices.mjs +0 -84
  127. package/lib/gateway/quiet.mjs +0 -189
  128. package/lib/gateway/relay-extras.mjs +0 -89
  129. package/lib/gateway/token-claims.mjs +0 -16
  130. package/lib/pod/README.md +0 -61
  131. package/lib/pod/actor.mjs +0 -96
  132. package/lib/pod/collection.mjs +0 -77
  133. package/lib/pod/containers.mjs +0 -140
  134. package/lib/pod/discovery.mjs +0 -46
  135. package/lib/pod/featured.mjs +0 -18
  136. package/lib/pod/followers.mjs +0 -24
  137. package/lib/pod/following.mjs +0 -10
  138. package/lib/pod/http.mjs +0 -74
  139. package/lib/pod/inbox.mjs +0 -166
  140. package/lib/pod/links.mjs +0 -35
  141. package/lib/pod/location.mjs +0 -52
  142. package/lib/pod/media.mjs +0 -27
  143. package/lib/pod/notes.mjs +0 -78
  144. package/lib/pod/notifications.mjs +0 -89
  145. package/lib/pod/outbox.mjs +0 -18
  146. package/lib/pod/package.json +0 -19
  147. package/lib/pod/policy.mjs +0 -58
  148. package/lib/pod/private.mjs +0 -19
  149. package/lib/pod/root.mjs +0 -104
  150. package/lib/pod/state.mjs +0 -40
  151. package/lib/pod/transport.mjs +0 -674
  152. package/lib/pod/type-index.mjs +0 -101
  153. package/lib/pod/urls.mjs +0 -91
  154. package/lib/server/embed.mjs +0 -424
  155. package/lib/session/README.md +0 -137
  156. package/lib/session/demo.html +0 -94
  157. package/lib/session/fedi-account.mjs +0 -417
  158. package/lib/session/fedi-login.mjs +0 -123
  159. package/lib/session/oidc-session.mjs +0 -233
  160. package/lib/session/package.json +0 -49
  161. package/lib/shared/guard.mjs +0 -213
  162. package/lib/shared/links.mjs +0 -9
  163. package/lib/shared/safefetch.mjs +0 -235
  164. package/lib/shared/ua.mjs +0 -29
  165. package/phanpy/dist/404.html +0 -40
  166. package/phanpy/dist/_headers +0 -2
  167. package/phanpy/dist/apple-touch-icon.png +0 -0
  168. package/phanpy/dist/assets/ICONS-DIPJeU0C.js +0 -2
  169. package/phanpy/dist/assets/ICONS-DIPJeU0C.js.map +0 -1
  170. package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js +0 -183
  171. package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
  172. package/phanpy/dist/assets/Temml-CCjoacWt.woff2 +0 -0
  173. package/phanpy/dist/assets/__vite-browser-external-C7iut881.js +0 -2
  174. package/phanpy/dist/assets/__vite-browser-external-C7iut881.js.map +0 -1
  175. package/phanpy/dist/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  176. package/phanpy/dist/assets/catch-up-O5q5C75Z.png +0 -0
  177. package/phanpy/dist/assets/chunk-aKtaBQYM.js +0 -1
  178. package/phanpy/dist/assets/compose-CFEx7wb4.js +0 -2
  179. package/phanpy/dist/assets/compose-CFEx7wb4.js.map +0 -1
  180. package/phanpy/dist/assets/compose-ZJN5xSb4.js +0 -35
  181. package/phanpy/dist/assets/compose-ZJN5xSb4.js.map +0 -1
  182. package/phanpy/dist/assets/debug.module-YFQgwnIr.js +0 -52
  183. package/phanpy/dist/assets/debug.module-YFQgwnIr.js.map +0 -1
  184. package/phanpy/dist/assets/dom-p1YcJ5Rw.js +0 -2
  185. package/phanpy/dist/assets/dom-p1YcJ5Rw.js.map +0 -1
  186. package/phanpy/dist/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  187. package/phanpy/dist/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  188. package/phanpy/dist/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  189. package/phanpy/dist/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  190. package/phanpy/dist/assets/icons/add-circle-line-WD0mwORD.js +0 -1
  191. package/phanpy/dist/assets/icons/alert-line-CpFUMf9s.js +0 -1
  192. package/phanpy/dist/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
  193. package/phanpy/dist/assets/icons/announcement-line-5nwaJFJm.js +0 -1
  194. package/phanpy/dist/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
  195. package/phanpy/dist/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
  196. package/phanpy/dist/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
  197. package/phanpy/dist/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
  198. package/phanpy/dist/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
  199. package/phanpy/dist/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
  200. package/phanpy/dist/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
  201. package/phanpy/dist/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
  202. package/phanpy/dist/assets/icons/at-line-CF66Bznk.js +0 -1
  203. package/phanpy/dist/assets/icons/attachment-line-BY8It-l9.js +0 -1
  204. package/phanpy/dist/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
  205. package/phanpy/dist/assets/icons/board-line-C0UWXyA6.js +0 -1
  206. package/phanpy/dist/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
  207. package/phanpy/dist/assets/icons/building-5-line-Dd59MzbO.js +0 -1
  208. package/phanpy/dist/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
  209. package/phanpy/dist/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
  210. package/phanpy/dist/assets/icons/calendar-month-line-vommts2C.js +0 -1
  211. package/phanpy/dist/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
  212. package/phanpy/dist/assets/icons/camera-line-Cv69MEei.js +0 -1
  213. package/phanpy/dist/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
  214. package/phanpy/dist/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
  215. package/phanpy/dist/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
  216. package/phanpy/dist/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
  217. package/phanpy/dist/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
  218. package/phanpy/dist/assets/icons/clipboard-line-BBp364gJ.js +0 -1
  219. package/phanpy/dist/assets/icons/close-circle-line-CICdiuGz.js +0 -1
  220. package/phanpy/dist/assets/icons/close-line-DYrFWcjv.js +0 -1
  221. package/phanpy/dist/assets/icons/cloud-line-tiQAbah8.js +0 -1
  222. package/phanpy/dist/assets/icons/code-line-fAl8iiV3.js +0 -1
  223. package/phanpy/dist/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
  224. package/phanpy/dist/assets/icons/copy-2-line-CLW69yF5.js +0 -1
  225. package/phanpy/dist/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
  226. package/phanpy/dist/assets/icons/document-3-line-DBGNhAg2.js +0 -1
  227. package/phanpy/dist/assets/icons/document-line-C7mb9sb7.js +0 -1
  228. package/phanpy/dist/assets/icons/down-line-BGqSSThd.js +0 -1
  229. package/phanpy/dist/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
  230. package/phanpy/dist/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
  231. package/phanpy/dist/assets/icons/exit-line-Dc07BS-x.js +0 -1
  232. package/phanpy/dist/assets/icons/external-link-line-BvpSkmjO.js +0 -1
  233. package/phanpy/dist/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
  234. package/phanpy/dist/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
  235. package/phanpy/dist/assets/icons/filter-2-line-B_mR2usy.js +0 -1
  236. package/phanpy/dist/assets/icons/filter-line-C_nm-3cU.js +0 -1
  237. package/phanpy/dist/assets/icons/flag-1-line-C2HB05I0.js +0 -1
  238. package/phanpy/dist/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
  239. package/phanpy/dist/assets/icons/formula-line-DLrt2d6j.js +0 -1
  240. package/phanpy/dist/assets/icons/grid-line-CgUmEikf.js +0 -1
  241. package/phanpy/dist/assets/icons/group-line-C5fnX0wU.js +0 -1
  242. package/phanpy/dist/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
  243. package/phanpy/dist/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
  244. package/phanpy/dist/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
  245. package/phanpy/dist/assets/icons/heart-line-CJ7K47J4.js +0 -1
  246. package/phanpy/dist/assets/icons/history-2-line-kWWvFzqv.js +0 -1
  247. package/phanpy/dist/assets/icons/history-line-bGQiwbTH.js +0 -1
  248. package/phanpy/dist/assets/icons/home-3-line-fRU1zeLG.js +0 -1
  249. package/phanpy/dist/assets/icons/information-line-iNk6-sOY.js +0 -1
  250. package/phanpy/dist/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
  251. package/phanpy/dist/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
  252. package/phanpy/dist/assets/icons/layout-5-line-B_znt5wO.js +0 -1
  253. package/phanpy/dist/assets/icons/left-line-eUw0tya4.js +0 -1
  254. package/phanpy/dist/assets/icons/lightning-line-DaKoDdix.js +0 -1
  255. package/phanpy/dist/assets/icons/link-2-line-DCqLmMA7.js +0 -1
  256. package/phanpy/dist/assets/icons/list-check-line-BAxosBh9.js +0 -1
  257. package/phanpy/dist/assets/icons/lock-line-CPoaXeUW.js +0 -1
  258. package/phanpy/dist/assets/icons/mail-line-B0P8aa1L.js +0 -1
  259. package/phanpy/dist/assets/icons/moon-line-CEqNAToP.js +0 -1
  260. package/phanpy/dist/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
  261. package/phanpy/dist/assets/icons/more-3-line-BIiVjYTz.js +0 -1
  262. package/phanpy/dist/assets/icons/notification-line-BZZq2Gtu.js +0 -1
  263. package/phanpy/dist/assets/icons/pencil-line-CKh8-A1v.js +0 -1
  264. package/phanpy/dist/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
  265. package/phanpy/dist/assets/icons/pin-line-DpTolyhs.js +0 -1
  266. package/phanpy/dist/assets/icons/play-fill-B0-G_8lX.js +0 -1
  267. package/phanpy/dist/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
  268. package/phanpy/dist/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
  269. package/phanpy/dist/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
  270. package/phanpy/dist/assets/icons/radar-line-CKyroTth.js +0 -1
  271. package/phanpy/dist/assets/icons/react-line-CoIKhhtK.js +0 -1
  272. package/phanpy/dist/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
  273. package/phanpy/dist/assets/icons/right-line-B3KWJaqc.js +0 -1
  274. package/phanpy/dist/assets/icons/rocket-line-4KZl537D.js +0 -1
  275. package/phanpy/dist/assets/icons/round-fill-C69NTrZ0.js +0 -1
  276. package/phanpy/dist/assets/icons/round-line-B9k_1uo-.js +0 -1
  277. package/phanpy/dist/assets/icons/route-line-B28ReHcE.js +0 -1
  278. package/phanpy/dist/assets/icons/rows-4-line-CrBniagM.js +0 -1
  279. package/phanpy/dist/assets/icons/scan-line-DSf74YdP.js +0 -1
  280. package/phanpy/dist/assets/icons/search-2-line-B6geIU1z.js +0 -1
  281. package/phanpy/dist/assets/icons/settings-3-line-azJwK47w.js +0 -1
  282. package/phanpy/dist/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
  283. package/phanpy/dist/assets/icons/share-2-line-D5MxWjrm.js +0 -1
  284. package/phanpy/dist/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
  285. package/phanpy/dist/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
  286. package/phanpy/dist/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
  287. package/phanpy/dist/assets/icons/time-line-BF3eet1v.js +0 -1
  288. package/phanpy/dist/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
  289. package/phanpy/dist/assets/icons/translate-line-BFWol2Ce.js +0 -1
  290. package/phanpy/dist/assets/icons/unlock-line-X_x5vwv3.js +0 -1
  291. package/phanpy/dist/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
  292. package/phanpy/dist/assets/icons/user-4-line-BVSynZCp.js +0 -1
  293. package/phanpy/dist/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
  294. package/phanpy/dist/assets/icons/user-add-line-BbXdHG4z.js +0 -1
  295. package/phanpy/dist/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
  296. package/phanpy/dist/assets/icons/user-follow-line-BXux0wSd.js +0 -1
  297. package/phanpy/dist/assets/icons/user-star-line-DdYlpHLU.js +0 -1
  298. package/phanpy/dist/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
  299. package/phanpy/dist/assets/icons/user-x-line-DIDgNlPH.js +0 -1
  300. package/phanpy/dist/assets/icons/volume-line-Bs390KQt.js +0 -1
  301. package/phanpy/dist/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
  302. package/phanpy/dist/assets/icons/walk-line-yNXfB-md.js +0 -1
  303. package/phanpy/dist/assets/icons/world-2-line-CenkGIwj.js +0 -1
  304. package/phanpy/dist/assets/icons/zoom-in-line-CaENUphf.js +0 -1
  305. package/phanpy/dist/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
  306. package/phanpy/dist/assets/instances-BNPptnn-.json +0 -333
  307. package/phanpy/dist/assets/locales/ar-SA-CfheRbCu.js +0 -1
  308. package/phanpy/dist/assets/locales/ca-ES-DHRW86Tz.js +0 -1
  309. package/phanpy/dist/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
  310. package/phanpy/dist/assets/locales/de-DE-ByRPMRFy.js +0 -1
  311. package/phanpy/dist/assets/locales/eo-UY-CPC7wLCS.js +0 -1
  312. package/phanpy/dist/assets/locales/es-ES-dJa0Fd9t.js +0 -1
  313. package/phanpy/dist/assets/locales/eu-ES-C3djlPR2.js +0 -1
  314. package/phanpy/dist/assets/locales/fa-IR-DTT490Ck.js +0 -1
  315. package/phanpy/dist/assets/locales/fi-FI-B23MCPuz.js +0 -1
  316. package/phanpy/dist/assets/locales/fr-FR-BcaQsO5C.js +0 -1
  317. package/phanpy/dist/assets/locales/gl-ES-BR-CVKBN.js +0 -1
  318. package/phanpy/dist/assets/locales/he-IL-CfheRbCu.js +0 -1
  319. package/phanpy/dist/assets/locales/hu-HU-CfheRbCu.js +0 -1
  320. package/phanpy/dist/assets/locales/it-IT-C1136UFV.js +0 -1
  321. package/phanpy/dist/assets/locales/ja-JP-CRZmRAhD.js +0 -1
  322. package/phanpy/dist/assets/locales/kab-Cz8xH_TT.js +0 -1
  323. package/phanpy/dist/assets/locales/ko-KR-CG9UiY4o.js +0 -1
  324. package/phanpy/dist/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
  325. package/phanpy/dist/assets/locales/nb-NO-CfheRbCu.js +0 -1
  326. package/phanpy/dist/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
  327. package/phanpy/dist/assets/locales/oc-FR-CfheRbCu.js +0 -1
  328. package/phanpy/dist/assets/locales/pl-PL-DkwGXf6P.js +0 -1
  329. package/phanpy/dist/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
  330. package/phanpy/dist/assets/locales/pt-BR-nGL0HdFS.js +0 -1
  331. package/phanpy/dist/assets/locales/pt-PT-Cp17aj2z.js +0 -1
  332. package/phanpy/dist/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
  333. package/phanpy/dist/assets/locales/th-TH-CfheRbCu.js +0 -1
  334. package/phanpy/dist/assets/locales/tok-CfheRbCu.js +0 -1
  335. package/phanpy/dist/assets/locales/tr-TR-CfheRbCu.js +0 -1
  336. package/phanpy/dist/assets/locales/uk-UA-C5E3dFBf.js +0 -1
  337. package/phanpy/dist/assets/locales/zh-CN-BHJi7_et.js +0 -1
  338. package/phanpy/dist/assets/locales/zh-TW-CfheRbCu.js +0 -1
  339. package/phanpy/dist/assets/main-BdqNbG-a.js +0 -26
  340. package/phanpy/dist/assets/main-BdqNbG-a.js.map +0 -1
  341. package/phanpy/dist/assets/mock-home-DGhR__He.js +0 -2
  342. package/phanpy/dist/assets/mock-home-DGhR__He.js.map +0 -1
  343. package/phanpy/dist/assets/multi-column-ROck0NVt.jpg +0 -0
  344. package/phanpy/dist/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  345. package/phanpy/dist/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  346. package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js +0 -2
  347. package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js.map +0 -1
  348. package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js +0 -20
  349. package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js.map +0 -1
  350. package/phanpy/dist/assets/style-B1iLPTPR.css +0 -2
  351. package/phanpy/dist/assets/temml-DuyIQMei.js +0 -97
  352. package/phanpy/dist/assets/temml-DuyIQMei.js.map +0 -1
  353. package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js +0 -2
  354. package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
  355. package/phanpy/dist/assets/year-in-posts-B2lywqdx.png +0 -0
  356. package/phanpy/dist/assets/year-in-posts-DJv_star.js +0 -3
  357. package/phanpy/dist/assets/year-in-posts-DJv_star.js.map +0 -1
  358. package/phanpy/dist/compose/index.html +0 -20
  359. package/phanpy/dist/favicon.ico +0 -0
  360. package/phanpy/dist/index.html +0 -73
  361. package/phanpy/dist/logo-192.png +0 -0
  362. package/phanpy/dist/logo-512.png +0 -0
  363. package/phanpy/dist/logo-badge-72.png +0 -0
  364. package/phanpy/dist/logo-maskable-512.png +0 -0
  365. package/phanpy/dist/logo-monochrome-512.png +0 -0
  366. package/phanpy/dist/logo-monochrome-maskable-512.png +0 -0
  367. package/phanpy/dist/manifest.webmanifest +0 -1
  368. package/phanpy/dist/og-image-2.jpg +0 -0
  369. package/phanpy/dist/og-image.png +0 -0
  370. package/phanpy/dist/robots.txt +0 -2
  371. package/phanpy/dist/sw.js +0 -2
  372. package/phanpy/dist/sw.js.map +0 -1
  373. package/phanpy/dist/version.json +0 -1
  374. package/run-agent.mjs +0 -899
  375. package/vendor/gate.cjs +0 -138
  376. package/vendor/idp-grant.cjs +0 -394
  377. package/vendor/jfetch.cjs +0 -16
  378. package/web/admin/actors.js +0 -147
  379. package/web/admin/bar.css +0 -126
  380. package/web/admin/bar.js +0 -99
  381. package/web/admin/client/client.js +0 -66
  382. package/web/admin/client/index.html +0 -69
  383. package/web/admin/client-pick.js +0 -79
  384. package/web/admin/common.js +0 -23
  385. package/web/admin/connections.js +0 -112
  386. package/web/admin/gateway.js +0 -133
  387. package/web/admin/group.js +0 -258
  388. package/web/admin/index.html +0 -612
  389. package/web/admin/notices-bar.js +0 -90
  390. package/web/admin/oauth-signin.mjs +0 -37
  391. package/web/admin/record.js +0 -393
  392. package/web/admin/setup/index.html +0 -257
  393. package/web/admin/setup/setup.js +0 -339
  394. package/web/admin/tokens.css +0 -68
  395. package/web/admin/upkeep.js +0 -177
  396. package/web/admin/window.css +0 -55
  397. package/web/admin/window.js +0 -136
  398. package/web/app/README.md +0 -81
  399. package/web/app/admin-facade.mjs +0 -588
  400. package/web/app/agent.mjs +0 -672
  401. package/web/app/atproto-browser.mjs +0 -86
  402. package/web/app/authorize_interaction.html +0 -56
  403. package/web/app/authorize_interaction.js +0 -69
  404. package/web/app/boot.mjs +0 -616
  405. package/web/app/deliver-relay.mjs +0 -129
  406. package/web/app/dist/boot.js +0 -34562
  407. package/web/app/dist/boot.js.map +0 -7
  408. package/web/app/dist/sw.js +0 -73456
  409. package/web/app/dist/sw.js.map +0 -7
  410. package/web/app/fediacct-browser.mjs +0 -146
  411. package/web/app/gateway-move.mjs +0 -60
  412. package/web/app/idb-kv.mjs +0 -35
  413. package/web/app/index.html +0 -258
  414. package/web/app/keys-browser.mjs +0 -71
  415. package/web/app/keystore.mjs +0 -82
  416. package/web/app/oidc-session.mjs +0 -6
  417. package/web/app/pod-auth.mjs +0 -248
  418. package/web/app/pod-remote.mjs +0 -84
  419. package/web/app/shims/_globals.mjs +0 -2
  420. package/web/app/shims/fedify-sig.mjs +0 -77
  421. package/web/app/shims/node-crypto.mjs +0 -113
  422. package/web/app/shims/node-fs.mjs +0 -17
  423. package/web/app/shims/node-path.mjs +0 -19
  424. package/web/app/shims/node-url.mjs +0 -7
  425. package/web/app/shims/prelude.js +0 -58
  426. package/web/app/shims/safefetch.mjs +0 -58
  427. package/web/app/shims/shapes-text.mjs +0 -8
  428. package/web/app/shims/web-push.mjs +0 -14
  429. package/web/app/signup.mjs +0 -357
  430. package/web/app/sw-src.mjs +0 -220
  431. package/web/app/update.js +0 -70
  432. package/web/front/admin.html +0 -55
  433. package/web/front/admin.js +0 -89
  434. package/web/front/install.sh +0 -69
  435. package/web/front/new-account.html +0 -35
  436. package/web/front/new-account.js +0 -13
  437. package/web/front/notices.html +0 -69
  438. package/web/front/notices.js +0 -107
  439. package/web/front/run.html +0 -64
  440. package/web/front/run.js +0 -221
  441. package/web/front/solid-oidc-client.js +0 -6
  442. package/web/icons/forum.svg +0 -11
  443. package/web/icons/gateway.svg +0 -14
  444. package/web/icons/local.svg +0 -11
package/run-agent.mjs DELETED
@@ -1,899 +0,0 @@
1
- // run-agent.mjs — fedipod: a standalone single-actor ActivityPub
2
- // agent. The remote pod is a RELAY: it serves the public wire face
3
- // (/fedipod/ap/) and buffers inbound mail in a public-append inbox
4
- // while this process is off, and it keeps one private document, the lease,
5
- // because a lock only one machine can reach coordinates nothing.
6
- //
7
- // Everything else private is on THIS machine — the RDF truth and the
8
- // operational state, in a directory beside the credential and the signing key.
9
- // So a machine holding only the credential file does NOT resume the actor: it
10
- // resumes the identity with an empty timeline, contacts, blocklist and
11
- // notifications, and `fedipod rebuild` recovers the posts the pod still
12
- // carries. `privateRoot` absent in credential.json is the pre-2026-08-03
13
- // layout and still means both trees are on the pod; `fedipod upgrade`
14
- // says so and `state --all` moves them.
15
- //
16
- // node run-agent.mjs # or: bin/fedipod.mjs run
17
- //
18
- // Env: AP_HOME (credential dir + local log, default ~/.fedipod, or
19
- // ~/.activitypod on an install that predates the rename — see lib/home.mjs),
20
- // AP_PORT (UI/API/admin, default 8030),
21
- // AP_GATE_TOKEN (optional loopback gate; absent → open, loopback-only).
22
- //
23
- // Until `bin/fedipod.mjs setup` has minted a credential the agent idles:
24
- // UI + admin up, no federation.
25
-
26
- import fs from 'node:fs';
27
- import os from 'node:os';
28
- import path from 'node:path';
29
- import { pathToFileURL } from 'node:url';
30
-
31
- import { PodStore } from './lib/core/store.mjs';
32
- import { apRoot, rootOf, writeJsonAtomic } from './lib/device/home.mjs';
33
- import { ensureTrustedTls } from './lib/device/certs.mjs';
34
- import { storageFor } from './lib/core/storage.mjs';
35
- import { resolveKeys } from './lib/core/keys.mjs';
36
- import { completeGatewayMove } from './lib/device/gateway-move.mjs';
37
- import { RemotePod } from './lib/device/remote.mjs';
38
- import { Deliverer } from './lib/core/deliver.mjs';
39
- import { Publisher } from './lib/core/publisher/index.mjs';
40
- import { Intake } from './lib/core/intake/index.mjs';
41
- import { C2S } from './lib/client/c2s.mjs';
42
- import { TagFeed } from './lib/connections/tagfeed.mjs';
43
- import { ImportWorker } from './lib/connections/import.mjs';
44
- import { Atproto } from './lib/connections/atproto.mjs';
45
- import { FediAccounts } from './lib/connections/fediacct.mjs';
46
- import { vaultsFor } from './lib/connections/vault.mjs';
47
- import { AcctFeed } from './lib/connections/acctfeed.mjs';
48
- import { BskyFeed } from './lib/connections/bskyfeed.mjs';
49
- import { BskyGroup } from './lib/connections/bskygroup.mjs';
50
- import { Lease } from './lib/core/lease.mjs';
51
- import { startAdmin } from './lib/device/admin/index.mjs';
52
- import { exposureProblem, hostLabel } from './lib/shared/guard.mjs';
53
- import { pendingSteps } from './lib/device/migrate.mjs';
54
- import { apUrls, assertionKeyId , publicHandle } from './lib/core/wire.mjs';
55
- import { followActor, unfollowActor, resolveHandle } from './lib/core/social.mjs';
56
- import * as podInbox from './lib/pod/inbox.mjs';
57
-
58
- export class Agent {
59
- constructor({ home, log }) {
60
- this.home = home;
61
- this.log = log;
62
- this.logRing = [];
63
- this.store = new PodStore({ log });
64
- }
65
-
66
- readCredential() {
67
- try { return JSON.parse(fs.readFileSync(path.join(this.home, 'credential.json'), 'utf8')); }
68
- catch { return null; }
69
- }
70
-
71
- configured() { return !!this.remote && !!this.store.getConfig(); }
72
-
73
- // Where the private half lives. `privateRoot` in the credential file names a
74
- // container — by default a plain directory beside the credential, and a pod
75
- // on this machine if you move it there. Absent means on the pod, so an
76
- // existing install is untouched.
77
- privateUrls(cred, urls = this.urls) {
78
- if (!cred.privateRoot) {
79
- return { state: urls.state, archive: urls.home + 'inbox-archive/', elsewhere: false };
80
- }
81
- const base = cred.privateRoot.endsWith('/') ? cred.privateRoot : cred.privateRoot + '/';
82
- return { state: base + 'ap-state/', archive: base + 'inbox-archive/', elsewhere: true };
83
- }
84
-
85
- // A container to keep the private half in. On the pod it is reached with the
86
- // credential; a local pod plainly, with a token header when it is gated
87
- // (dk's is); a directory is not reached over anything at all.
88
- privateStorage(cred, which, urls = this.urls) {
89
- const url = this.privateUrls(cred, urls)[which];
90
- if (!cred.privateRoot) return storageFor(url, (u, i) => this.remote.fetch(u, i));
91
- const token = process.env.AP_STATE_TOKEN || '';
92
- return storageFor(url, (u, i) => fetch(u, token
93
- ? { ...i, headers: { ...(i?.headers || {}), 'x-dk-token': token } }
94
- : i));
95
- }
96
-
97
- logLines(n = 100) { return this.logRing.slice(-n); }
98
-
99
- status() {
100
- const cfg = this.store.getConfig();
101
- const contacts = this.store.getContacts();
102
- return {
103
- configured: this.configured(),
104
- mode: !this.configured() ? 'unconfigured' : this.viewer ? 'viewer' : 'active',
105
- kind: cfg?.kind || 'person',
106
- handle: cfg?.handle || null,
107
- actor: this.urls?.actor || null,
108
- followers: contacts.followers.length,
109
- following: contacts.following.length,
110
- queue: this.store.getQueue().length,
111
- deadLetters: this.store.getDeadLetters().length,
112
- blockedDomains: this.store.getBlocklist().domains.length,
113
- push: this.intake?.wsState || 'n/a',
114
- // Measured by the last sweep from the listing it already fetched, so
115
- // asking costs nothing. What the admin page prompts on.
116
- inbox: this.intake?.inboxStats || null,
117
- lastDrain: this.intake?.lastDrain || null,
118
- tagfeed: this.tagfeed
119
- ? { ...this.tagfeed.config(), lastSweep: this.tagfeed.lastSweep, lastAdded: this.tagfeed.lastAdded }
120
- : null,
121
- atproto: this.atproto?.status() || null,
122
- // Anyone asking whether this agent is hammering their server can read the
123
- // answer here instead of in their access log.
124
- podRequests: this.remote?.stats?.() || null,
125
- update: this.updateInfo || null,
126
- inboxCooldownFor: this.intake?.drainCooldownUntil
127
- ? Math.max(0, Math.round((this.intake.drainCooldownUntil - Date.now()) / 1000)) : 0,
128
- stateSkipped: this.store.lastSkipped || [],
129
- };
130
- }
131
-
132
- // First-run provisioning, called by the setup CLI after the credential file
133
- // is written: containers, owner-only ACLs on the private trees, config into
134
- // pod state. publishProfile (via connect) handles the public wire ACLs.
135
- async bootstrap({ handle, name, root, kind, approveJoins = false, summary, icon, gateway }) {
136
- const cred = this.readCredential();
137
- if (!cred) throw new Error('no credential — run setup first');
138
- if (!this.remote) {
139
- this.remote = new RemotePod(cred, { log: this.log, home: this.home });
140
- await this.remote.warmup();
141
- }
142
- this.urls = apUrls(cred.remotePod, root);
143
- const priv = this.privateUrls(cred);
144
- const stateStore = this.privateStorage(cred, 'state');
145
- // The pod's own ap-state/ is provisioned either way: even when the rest of
146
- // the private half lives elsewhere, lease.json stays here, because a lease
147
- // in a pod only one machine can reach coordinates nothing.
148
- await this.remote.putJson(this.urls.state + '.keep', { keep: true }, 'application/json');
149
- await this.remote.setAcl(this.urls.home, []);
150
- await this.remote.setAcl(this.urls.state, []);
151
- if (priv.elsewhere) {
152
- const r = await stateStore.write('.keep', '{"keep":true}\n', 'application/json');
153
- if (!r.ok) throw new Error(`private store ${stateStore.base}.keep → ${r.why}`);
154
- this.log(`private state lives at ${cred.privateRoot} (${stateStore.kind})`);
155
- }
156
- this.store.attach(stateStore);
157
- // Re-running setup must not destroy state set afterwards (the UI
158
- // password, above all), so load what is there and merge into it.
159
- await this.store.load().catch(() => {});
160
- const existing = this.store.getConfig() || {};
161
- this.store.setConfig({
162
- ...existing,
163
- remotePod: cred.remotePod, handle, name: name || existing.name || handle,
164
- issuer: cred.issuerOrigin, ...(root ? { root } : {}),
165
- ...(kind ? { kind } : {}),
166
- ...(approveJoins ? { approveJoins: true } : {}),
167
- ...(summary ? { summary } : {}), ...(icon ? { icon } : {}),
168
- // Set before the first publish, so the actor never advertises the pod
169
- // inbox only to rename it a moment later.
170
- ...(gateway ? { gateway } : {}),
171
- });
172
- await this.store.flush();
173
- }
174
-
175
- // Cache the handle in agent.json, merging so the port survives. Nothing
176
- // outside this process can read pod state, so this file is the only place a
177
- // sibling — or the next start, before it connects — can learn the name to
178
- // build this identity's origin from.
179
- recordHandle(handle) {
180
- if (!handle || !this.home) return;
181
- const file = path.join(this.home, 'agent.json');
182
- let rec = {};
183
- try { rec = JSON.parse(fs.readFileSync(file, 'utf8')) || {}; } catch { /* first run */ }
184
- if (rec.handle === handle) return;
185
- try { writeJsonAtomic(file, { ...rec, handle }, { mode: 0o644 }); }
186
- catch { /* the agent still runs; its links are just bare */ }
187
- }
188
-
189
- // Bring federation up from the credential file + pod state. `name` (from
190
- // `run --name "…"`) updates the display name other servers show, without
191
- // the collateral of re-running setup.
192
- // `act: false` reads the identity and stops. Everything a confirmation prompt
193
- // needs — the handle, the host, the follow counts — is in pod state that is
194
- // loaded by then, while acquiring the lease, draining the inbox, subscribing a
195
- // notification channel, probing the ACLs and sweeping the tag feed are all
196
- // things a command you are about to decline should never have spent. The
197
- // command calls connect() again, without the flag, if you say yes.
198
- async connect({ name = null, repair = true, act = true } = {}) {
199
- const cred = this.readCredential();
200
- if (!cred) return false;
201
- if (!this.remote) {
202
- this.remote = new RemotePod(cred, { log: this.log, home: this.home });
203
- await this.remote.warmup();
204
- }
205
- const probeUrls = apUrls(cred.remotePod, cred.root);
206
- // Load until it actually succeeds: attaching is not the same as having
207
- // read the state, and a retry that skipped the load would see an empty
208
- // cache and wrongly conclude the agent was never set up.
209
- if (!this.store.storage) this.store.attach(this.privateStorage(cred, 'state', probeUrls));
210
- if (!this.stateLoaded) {
211
- await this.store.load();
212
- this.stateLoaded = true;
213
- }
214
- let config = this.store.getConfig();
215
- if (!config) { this.log('credential present but pod state empty — run setup'); return false; }
216
- // The store had to be attached from the credential, because only the config
217
- // it holds says where the state really lives. If the two disagree,
218
- // everything past here reads one tree and writes another.
219
- // Only meaningful when the state follows the pod. With privateRoot set it
220
- // is pinned by configuration and a moved pod does not move it.
221
- if (!cred.privateRoot && apUrls(config.remotePod, config.root).state !== probeUrls.state) {
222
- const moved = apUrls(config.remotePod, config.root).state;
223
- this.log(`state tree moved (${probeUrls.state} → ${moved}) — reattaching`);
224
- this.store.attach(storageFor(moved, (u, i) => this.remote.fetch(u, i)));
225
- await this.store.load();
226
- config = this.store.getConfig();
227
- if (!config) { this.log('no state at the pod its own config names — run setup'); return false; }
228
- }
229
- // Resurrecting a tombstoned actor would contradict the Delete every server
230
- // has already acted on, so a retired identity stays retired.
231
- if (config.retiredAt) {
232
- this.log(`this actor was retired on ${config.retiredAt} — run setup for a new identity`);
233
- return false;
234
- }
235
- // A rename is a merge into the existing config, never a rewrite — the
236
- // UI password and anything else set later must survive it.
237
- if (name && name !== config.name) {
238
- this.store.setConfig({ ...config, name });
239
- config = this.store.getConfig();
240
- this.renamed = true; // republish the actor once connected
241
- this.log(`display name set to "${name}"`);
242
- }
243
- // A fronted identity (config.gateway.frontActor) advertises its ids on a
244
- // shared domain; RemotePod gets the map so writes still land on the pod.
245
- const publicBase = config.gateway?.frontActor
246
- ? config.gateway.frontActor.replace(/ap\/actor\/?$/, '') : null;
247
- this.urls = apUrls(config.remotePod, config.root, { publicBase });
248
- if (this.urls.toPod) this.remote.setUrlMap(this.urls.toPod);
249
- // The agent answers at <handle>.localhost too, so each identity on a
250
- // machine gets a browser origin of its own. Known only now: the admin
251
- // server was listening before any of this was read.
252
- this.authorities?.setHandle(config.handle);
253
- // And write it down beside the port. Everything that links to this identity
254
- // from OUTSIDE the process builds the origin from agent.json — the Actors
255
- // list, a sibling's page, the next start — and an identity set up before the
256
- // handle was recorded has only a port there, so every one of those links
257
- // came out bare. Pod state is the authority; this is the cache of it.
258
- this.recordHandle(config.handle);
259
-
260
- // Said once, where the config is finally known. Exposure without a gate
261
- // token is refused before the socket opens (startAgent), so reaching here
262
- // means the token is set and the whole surface is behind it. A UI password
263
- // is the second, per-person half: without one, anyone who has the token
264
- // gets a client bearer for the asking.
265
- // An install made before the shape changed goes on working, which is why
266
- // nothing here refuses — but it should not go on SILENTLY. The whole reason
267
- // this is said out loud is that bbba587 changed a default and every install
268
- // that already existed kept the old layout with nothing to show for it.
269
- const pending = pendingSteps(cred);
270
- for (const step of pending) {
271
- this.log(`this identity is on an older layout: ${step.what} (${step.why}). `
272
- + 'Run `fedipod upgrade` to see what is pending.');
273
- }
274
- // The record page says the same thing on its software row — the log is
275
- // read by nobody who lives in the GUI.
276
- this.pendingUpgrade = pending.map(s => s.what);
277
-
278
- if (process.env.AP_ALLOWED_HOSTS && !config.uiPassword) {
279
- this.log('WARNING: AP_ALLOWED_HOSTS is set and no UI password is — anyone holding '
280
- + 'AP_GATE_TOKEN can mint a client token. Run `fedipod passwd`.');
281
- }
282
-
283
- if (!act) return true; // reading only — see the note above
284
-
285
- // Exactly one agent may act on a pod (inbox drains are destructive
286
- // reads); later arrivals become read-only viewers of the same state.
287
- // Deliberately the REMOTE pod's ap-state/, never privateRoot's: a lease
288
- // that only one machine can reach is not a lease.
289
- this.lease = new Lease({
290
- url: this.urls.state + 'lease.json',
291
- fetchImpl: (u, i) => this.remote.fetch(u, i), log: this.log,
292
- });
293
- this.viewer = !(await this.lease.acquire());
294
-
295
- // Keys are LOCAL by default (the pod host never holds them); `setup
296
- // --keys pod` opts into sharing them through the pod so several devices
297
- // can sign as the same actor. Per-machine choice, so it rides in the
298
- // credential file rather than pod state.
299
- const keys = await resolveKeys(this.store, {
300
- localDir: cred.keysMode === 'pod' ? null : this.home,
301
- rotate: !!cred.rotateKeyOnce,
302
- actorId: this.urls.actor,
303
- log: this.log,
304
- // Consulted only when no key material exists anywhere: if the actor
305
- // already publishes a key, minting a new one would break federation.
306
- actorHasKey: async () => {
307
- const doc = await this.remote.getJson(this.urls.actor).catch(() => null);
308
- return !!doc?.publicKey?.publicKeyPem;
309
- },
310
- });
311
- if (cred.rotateKeyOnce) { // one-shot flag
312
- const { rotateKeyOnce, ...rest } = cred;
313
- writeJsonAtomic(path.join(this.home, 'credential.json'), rest);
314
- }
315
- // connect() can run more than once — connectWithRetry retries after a throw,
316
- // and the CLI connects for real after its confirmation. Deliverer arms a
317
- // 60s queue timer in its constructor and Intake owns a poll timer and a
318
- // websocket, so replacing them without stopping the old ones leaves both
319
- // ticking for the life of the process.
320
- this.intake?.stop();
321
- this.deliverer?.stop();
322
- this.tagfeed?.stop();
323
- clearInterval(this.schedTimer);
324
- this.publisher?.stopPolls();
325
- this.deliverer = new Deliverer({
326
- store: this.store, rsaPrivate: keys.rsaPrivate, keyId: this.urls.actor + '#main-key',
327
- actorId: this.urls.actor, edPrivate: keys.edPrivate,
328
- proofKeyId: assertionKeyId(this.urls),
329
- log: this.log, passive: this.viewer,
330
- onGone: () => this.publisher.publishCollections({ followers: true }),
331
- });
332
- this.publisher = new Publisher({
333
- config, remote: this.remote, store: this.store,
334
- deliverer: this.deliverer, publicKeyPem: keys.rsaPublicPem,
335
- assertionKey: keys.edPublicMultibase, log: this.log,
336
- resolveMention: (h) => resolveHandle(this, h),
337
- resolveActor: (u) => this.intake.fetchAP(u),
338
- // Inside a pod server the client surface answers on the pod's own
339
- // origin, so it can be advertised. Standalone it is on loopback, and
340
- // naming it in a world-readable actor would send clients nowhere.
341
- clientOrigin: this.embedded ? this.urls.base : null,
342
- });
343
- // Intake is constructed even for viewers — its signed fetchAP powers
344
- // search/deref; start() (draining) is active-only.
345
- // The dispatcher the admin surface also builds; this one is for what the
346
- // Gateway's outbox door took on the owner's behalf and the drain finds.
347
- this.c2s = new C2S({ agent: this, log: this.log });
348
- this.intake = new Intake({
349
- config, urls: this.urls, remote: this.remote, store: this.store,
350
- deliverer: this.deliverer, publisher: this.publisher, log: this.log, lease: this.lease,
351
- archive: this.privateStorage(cred, 'archive'),
352
- push: !this.embedded, pollSeconds: this.pollSeconds || null,
353
- ownerPost: (a, o) => this.c2s.dispatch(a, o),
354
- });
355
- // The CSV-import worker: paced, resumable, armed only while active.
356
- this.importer?.stop();
357
- this.importer = new ImportWorker({ agent: this, log: this.log });
358
- // Where the credentials for accounts held on OTHER servers are kept.
359
- const vaults = vaultsFor({ embedded: this.embedded, home: this.home, store: this.store });
360
- // The Bluesky connection, when one exists. Stamped to this actor; a
361
- // credential connected for another identity is treated as absent.
362
- this.atproto = new Atproto({
363
- localDir: this.home, actorId: this.urls.actor, log: this.log, vault: vaults.bluesky,
364
- });
365
- this.publisher.atproto = this.atproto;
366
- // Fediverse accounts the owner holds elsewhere. Same custody rules as the
367
- // Bluesky credential, and the same absence when stamped for someone else.
368
- this.fediaccts = new FediAccounts({
369
- localDir: this.home, actorId: this.urls.actor, log: this.log,
370
- vault: vaults.fedi, apps: vaults.apps,
371
- });
372
- this.acctfeed?.stop();
373
- this.acctfeed = null;
374
- this.bskyfeed?.stop();
375
- this.bskyfeed = null;
376
- this.bskygroup = null;
377
- this.intake.bskyGroup = null;
378
- if (this.viewer) {
379
- this.startViewer();
380
- this.log(this.lease.denied === 'unreadable'
381
- ? `the pod cannot be read, so the lease is unknown — viewing as @${config.handle} (read-only)`
382
- : `another agent is active for this pod — viewing as @${config.handle} (read-only)`);
383
- return true;
384
- }
385
- await this.startActive({ repair });
386
- return true;
387
- }
388
-
389
- // Claim the lease from whoever holds it and start acting. For the case where
390
- // the holder is gone but its lease has not expired — a crash, or a one-shot
391
- // command that exited without releasing — the only alternative is waiting out
392
- // the TTL, which is five minutes.
393
- async takeOver() {
394
- if (!this.viewer) return false;
395
- if (!await this.lease.takeover()) return false;
396
- this.log('took the lease over');
397
- await this.startActive({ promoted: true });
398
- return true;
399
- }
400
-
401
- // Mint a replacement signing key and republish the actor that advertises it.
402
- // These are one operation: a rotation without the republish leaves remote
403
- // servers verifying against a key we no longer hold, which fails every
404
- // delivery silently. The live deliverer and publisher are updated too, so the
405
- // running agent signs with the new key from here on.
406
- async rotateKey() {
407
- const cred = this.readCredential();
408
- const before = this.publisher.publicKeyPem;
409
- const keys = await resolveKeys(this.store, {
410
- localDir: cred.keysMode === 'pod' ? null : this.home,
411
- rotate: true,
412
- actorId: this.urls.actor,
413
- log: this.log,
414
- });
415
- this.publisher.publicKeyPem = keys.rsaPublicPem;
416
- this.publisher.assertionKey = keys.edPublicMultibase;
417
- this.deliverer.rsaPrivate = keys.rsaPrivate;
418
- this.deliverer.edPrivate = keys.edPrivate;
419
- await this.publisher.publishProfile();
420
- return { changed: before !== keys.rsaPublicPem, publicKeyPem: keys.rsaPublicPem };
421
- }
422
-
423
- // Park: as quiet as a pod can be while keeping its name, and revivable.
424
- // The following list is snapshotted FIRST — unfollowing is what stops the
425
- // traffic at source, but it also destroys the only record of who was being
426
- // followed, and "until I want to revive it" needs that record.
427
- async park({ unfollow = unfollowActor } = {}) {
428
- const following = await this._snapshotFollowing();
429
- const r = await this.quiesce({ unfollow });
430
- this.log(`parked: ${r.unfollowed} unfollow(s) recorded for revival, inbox closed`);
431
- return { ...r, snapshot: following.length };
432
- }
433
-
434
- // Undo a park: re-open the inbox, then re-follow everyone from the snapshot.
435
- // Each Follow needs the far end to Accept, so this is a request, not a
436
- // restoration — some will not come back, which is the nature of the thing.
437
- async revive({ follow = followActor } = {}) {
438
- const parked = this.store.read('parked.json', null);
439
- await this.publisher.openInbox();
440
- let refollowed = 0;
441
- for (const f of parked?.following || []) {
442
- try { await follow(this, f.actor); refollowed++; }
443
- catch (e) { this.log(`re-follow ${f.actor} failed: ${e.message}`); }
444
- }
445
- if (parked) this.store.remove('parked.json').catch(() => {});
446
- await this.store.flush();
447
- this.log(`revived: inbox open, ${refollowed}/${parked?.following?.length || 0} follow(s) re-sent`);
448
- return { refollowed, of: parked?.following?.length || 0, parkedAt: parked?.parkedAt || null };
449
- }
450
-
451
- // The follow graph, written down before it is torn down. Both going quiet
452
- // and moving away unfollow everyone, and the record page offers "active"
453
- // afterwards for either — so both have to leave something to come back from,
454
- // or setting it back re-follows nobody and says so only in a count.
455
- async _snapshotFollowing() {
456
- const following = this.store.getContacts().following;
457
- this.store.write('parked.json', {
458
- parkedAt: new Date().toISOString(),
459
- following: following.map(f => ({ actor: f.actor, handle: f.handle || null })),
460
- });
461
- await this.store.flush();
462
- return following;
463
- }
464
-
465
- // Keep the handle, take no more mail. Unfollowing is what actually stops the
466
- // volume — every account you follow pushes its posts into your inbox — and
467
- // closing the inbox handles the rest, which no follow graph can gate:
468
- // stranger mentions, new follow requests, outright spam.
469
- // `unfollow` is injectable so this is testable without a live pod.
470
- async quiesce({ unfollow = unfollowActor } = {}) {
471
- const following = this.store.getContacts().following.map(f => f.actor);
472
- let unfollowed = 0;
473
- for (const actor of following) {
474
- try { await unfollow(this, actor); unfollowed++; }
475
- catch (e) { this.log(`unfollow ${actor} failed: ${e.message}`); }
476
- }
477
- const quiescedAt = await this.publisher.closeInbox();
478
- this.log(`quiesced: unfollowed ${unfollowed}/${following.length}, inbox closed`);
479
- return { unfollowed, following: following.length, quiescedAt };
480
- }
481
-
482
- // Same, plus the fediverse-native redirect: followers are migrated to the
483
- // target by their own servers, and the old handle keeps resolving.
484
- async moveTo(target, { unfollow = unfollowActor } = {}) {
485
- const moved = await this.publisher.publishMove(target);
486
- const snapshot = await this._snapshotFollowing();
487
- const quiesced = await this.quiesce({ unfollow });
488
- return { ...moved, ...quiesced, snapshot: snapshot.length };
489
- }
490
-
491
- // True when it had to republish. Authenticated read: the question here is
492
- // whether the document EXISTS, not whether the world can see it —
493
- // verifyPublicSurface answers that one.
494
- async ensureActorPublished() {
495
- const doc = await this.remote.getJson(this.urls.actor);
496
- if (doc?.id) return false;
497
- this.log('actor document missing from the pod — republishing');
498
- await this.publisher.publishProfile({ force: true }); // the digest cannot know this
499
- return true;
500
- }
501
-
502
- // The featured collection was once written only on the first pin, so an
503
- // account from before then has none, and every server that shows the
504
- // profile is refused and asks again. One authenticated GET; the empty
505
- // collection goes up when the pod lacks it. True when it had to.
506
- async ensureFeaturedPublished() {
507
- const doc = await this.remote.getJson(this.urls.featured);
508
- if (doc?.id) return false;
509
- this.log('featured collection missing from the pod — publishing it');
510
- await this.publisher.publishFeatured();
511
- return true;
512
- }
513
-
514
- // The inbox's door must be open to the world (or to the gateway alone, in
515
- // locked mode) or nothing anyone sends ever lands. The permission is written
516
- // at setup, and a container made again later — a root move, a pod rebuilt —
517
- // came back without it: the group's inbox on fp2 refused every delivery for
518
- // nine days while its agent ran on, healthy-looking. One write per start,
519
- // and it is idempotent.
520
- async ensureInboxOpen() {
521
- const g = this.store.getConfig()?.gateway;
522
- const posture = g?.mode === 'locked' && g.webId ? { gatewayWebId: g.webId } : 'open';
523
- await podInbox.setPosture(this.remote, this.urls, posture);
524
- return posture;
525
- }
526
-
527
- // Read-only mode: refresh the state cache periodically, and take over the
528
- // moment the active agent's lease frees.
529
- startViewer() {
530
- this.viewer = true;
531
- // Five minutes, not one: with revalidation a quiet refresh is a single 304,
532
- // but a viewer still has no reason to ask twelve times an hour.
533
- this.refreshTimer = setInterval(async () => {
534
- try {
535
- if (this.viewer && await this.lease.acquire()) {
536
- this.log('lease freed — promoting to ACTIVE');
537
- await this.startActive({ promoted: true });
538
- return;
539
- }
540
- if (this.viewer) await this.store.load();
541
- } catch (e) { this.log(`viewer refresh: ${e.message}`); }
542
- }, Math.round(5 * 60_000 * (0.85 + Math.random() * 0.3)));
543
- this.refreshTimer.unref?.();
544
- }
545
-
546
- // The acting half: lease renewal, inbox drain, tag feed, delivery queue.
547
- // Called at connect when the lease is ours, or on viewer promotion.
548
- async startActive({ repair = true, promoted = false } = {}) {
549
- this.viewer = false;
550
- clearInterval(this.refreshTimer);
551
- if (promoted) await this.refreshBeforeActing();
552
- // Own posts the outbox names and the timeline index lacks come back here,
553
- // before anything acts on the index.
554
- await this.publisher.healStatuses().catch(e => this.log(`healing the timeline index: ${e.message}`));
555
- // Likes made before the liked list existed become its first entries.
556
- try { this.publisher.backfillLiked(); } catch (e) { this.log(`liked list: ${e.message}`); }
557
- this.lease.onLost = () => this.demote();
558
- this.lease.startRenewal();
559
- this.deliverer.startQueue();
560
- // Parked: no draining (the inbox is closed anyway) and no tag feed, so
561
- // starting the agent by accident does not undo the quiet.
562
- if (this.store.getConfig()?.quiescedAt) {
563
- this.log('parked — not draining or polling; run `fedipod revive` to resume');
564
- return; // renewal is already running, above
565
- }
566
- await this.intake.start();
567
- // An address that moved here from another gateway, whose move was left
568
- // pending (the old gateway could not be told): try again now that this
569
- // agent acts and the actor is published.
570
- completeGatewayMove(this).catch(e => this.log(`gateway move: ${e.message}`));
571
- // Reused, not replaced: demote() stops this instance but does not clear it,
572
- // so constructing a new one over the top orphaned the old chain beyond the
573
- // reach of any later stop().
574
- this.tagfeed ||= new TagFeed({ store: this.store, intake: this.intake, log: this.log });
575
- this.tagfeed.start();
576
- this.startBsky();
577
- this.startAccts();
578
- // Scheduled posts: a 30s sweep publishes what has come due. The entry is
579
- // removed before publishing, so a slow publish cannot double-post; a
580
- // failed one is dropped with its reason in the log.
581
- clearInterval(this.schedTimer);
582
- this.schedTimer = setInterval(() => {
583
- const due = this.store.getScheduled().filter(e => Date.parse(e.scheduledAt) <= Date.now());
584
- for (const e of due) {
585
- this.store.setScheduled(this.store.getScheduled().filter(x => x.id !== e.id));
586
- this.publisher.publishNote(e.params.status, {
587
- inReplyTo: e.params.inReplyTo, attachments: e.params.attachments,
588
- visibility: e.params.visibility, spoilerText: e.params.spoilerText,
589
- }).then(() => this.log(`scheduled post published (${e.id})`))
590
- .catch(err => this.log(`scheduled post ${e.id} failed: ${err.message} — dropped`));
591
- }
592
- // A poll whose time is up is shut on the same sweep: it stops taking
593
- // answers here, and everyone holding it is told once.
594
- this.publisher.closeDuePolls()
595
- .catch(err => this.log(`closing polls: ${err.message}`));
596
- }, 30_000);
597
- this.schedTimer.unref();
598
- // A CSV import interrupted by a restart or a handoff picks back up here.
599
- this.importer?.resume();
600
- // The identity the fediverse actually sees: a fronted actor's name and
601
- // host are the front's, not the pod's.
602
- this.log(`federating as @${publicHandle(this.store.getConfig())}@${new URL(this.urls.actor).host}`);
603
- if (this.renamed) {
604
- // The display name lives in the actor document, so a rename only
605
- // reaches other servers once that is republished.
606
- this.renamed = false;
607
- this.publisher.publishProfile()
608
- .then(() => this.log('actor document republished with the new display name'))
609
- .catch(e => this.log(`republish after rename failed: ${e.message}`));
610
- }
611
- // bootstrap writes the owner-only ACLs once, at setup, and nothing else
612
- // ever returns to them — so check on every start that the private trees
613
- // really are private, and repair them if not. Off the critical path.
614
- this.publisher.ensurePrivateAcls()
615
- .catch(e => this.log(`private-ACL check failed: ${e.message}`));
616
- // A publish that died half-way leaves an actor nobody can fetch while
617
- // everything here looks healthy — one GET to find out, and republishing
618
- // is idempotent.
619
- // Skipped during setup, which publishes the profile itself a moment later:
620
- // running both meant every first run wrote the whole wire face twice.
621
- if (repair) {
622
- this.ensureActorPublished()
623
- .catch(e => this.log(`actor check failed: ${e.message}`));
624
- this.ensureFeaturedPublished()
625
- .catch(e => this.log(`featured check failed: ${e.message}`));
626
- this.ensureInboxOpen()
627
- .catch(e => this.log(`inbox door check failed: ${e.message}`));
628
- // The human page, rewritten only when what it shows has changed: one
629
- // local digest compare, and a write the first time after an upgrade.
630
- this.publisher.publishProfilePage()
631
- .catch(e => this.log(`profile page: ${e.message}`));
632
- }
633
- this.seedFollowNotifications().catch(e => this.log(`notification seeding failed: ${e.message}`));
634
- }
635
-
636
- // Re-read state before acting on what we hold. A viewer's cache is kept
637
- // current by revalidation against the CONTAINER, whose ETag vouches for its
638
- // children existing rather than for their contents — so a peer rewriting
639
- // contacts.json or statuses.json in place moves nothing this agent would
640
- // notice. Only on promotion, never on a timer, and only documents that really
641
- // changed come back with a body.
642
- async refreshBeforeActing() {
643
- if (!this.store.storage) return;
644
- await this.store.load({ force: true })
645
- .catch(e => this.log(`state refresh on promotion: ${e.message}`));
646
- }
647
-
648
- // Another device claimed the lease (its user acted there) — stand down to
649
- // viewer so exactly one agent keeps draining.
650
- // The Bluesky mirror poll, only ever running when an account is connected.
651
- // Reused, not replaced, for the same orphaned-timer reason as tagfeed.
652
- startBsky() {
653
- if (this.viewer || !this.atproto?.connected()) return;
654
- if (this.store.getConfig()?.quiescedAt) return;
655
- // A group's account is the group's presence on Bluesky: follows are joins,
656
- // mentions are submissions, and both ride the notification hook.
657
- if (this.store.getConfig()?.kind === 'group') {
658
- this.bskygroup ||= new BskyGroup({
659
- store: this.store, atproto: this.atproto, intake: this.intake,
660
- publisher: this.publisher, log: this.log,
661
- });
662
- this.intake.bskyGroup = this.bskygroup;
663
- }
664
- this.bskyfeed ||= new BskyFeed({
665
- store: this.store, atproto: this.atproto, log: this.log,
666
- onNotification: async (n) => {
667
- if (!this.bskygroup) return;
668
- if (n.reason === 'follow') await this.bskygroup.onFollow(n.author);
669
- else await this.bskygroup.onMention(n);
670
- },
671
- });
672
- this.bskyfeed.start();
673
- }
674
-
675
- stopBsky() { this.bskyfeed?.stop(); }
676
-
677
- // The connected fediverse accounts' poll, only ever running when at least
678
- // one is connected. Reused, not replaced, for the same orphaned-timer reason
679
- // as tagfeed.
680
- startAccts() {
681
- if (this.viewer || !this.fediaccts?.connected()) return;
682
- if (this.store.getConfig()?.quiescedAt) return;
683
- this.acctfeed ||= new AcctFeed({
684
- store: this.store, accounts: this.fediaccts, log: this.log,
685
- });
686
- this.acctfeed.start();
687
- }
688
-
689
- stopAccts() { this.acctfeed?.stop(); }
690
-
691
- // Connecting or disconnecting an account changes what there is to poll, and
692
- // the last one going leaves a timer with nothing to ask.
693
- restartAccts() {
694
- this.stopAccts();
695
- this.startAccts();
696
- }
697
-
698
- demote() {
699
- if (this.viewer) return;
700
- this.log('another device took over — demoting to viewer');
701
- this.intake?.stop();
702
- this.tagfeed?.stop();
703
- this.bskyfeed?.stop();
704
- this.acctfeed?.stop();
705
- this.deliverer?.stop();
706
- this.importer?.stop();
707
- clearInterval(this.schedTimer);
708
- this.publisher?.stopPolls();
709
- // The lease too: standing down means standing down. Left renewing, a viewer
710
- // keeps writing to the pod on the active agent's behalf and can win the
711
- // lease back on a conditional PUT it had no business making.
712
- this.lease?.stopRenewal();
713
- this.startViewer();
714
- }
715
-
716
- // A user acting on this viewer outranks the idle active agent elsewhere:
717
- // claim the lease now (writes are safe immediately — publishing never
718
- // touches the inbox), but hold the destructive inbox drain until the old
719
- // active has seen the loss at its next renewal and demoted.
720
- async requestTakeover() {
721
- if (!this.viewer) return true;
722
- if (!(await this.lease.takeover())) return false;
723
- this.log('taking over from the other device (user action here)');
724
- this.viewer = false;
725
- clearInterval(this.refreshTimer);
726
- this.lease.onLost = () => this.demote();
727
- this.lease.startRenewal();
728
- this.deliverer.startQueue();
729
- setTimeout(async () => {
730
- if (this.viewer) return; // lost it again in the meantime
731
- try {
732
- await this.refreshBeforeActing();
733
- await this.intake.start();
734
- this.tagfeed ||= new TagFeed({ store: this.store, intake: this.intake, log: this.log });
735
- this.tagfeed.start();
736
- this.startBsky();
737
- this.startAccts();
738
- // A stranded CSV import resumes here too — startActive is not on the
739
- // takeover path, so without this the rows sat pending until a restart.
740
- this.importer?.resume();
741
- this.log('takeover complete — draining resumed on this device');
742
- } catch (e) { this.log(`takeover drain start: ${e.message}`); }
743
- }, 35_000).unref?.(); // > one renewal interval: old agent has demoted
744
- return true;
745
- }
746
-
747
- // A first run inherits followers without ever having seen them arrive, so
748
- // the notifications list starts empty where it should start full.
749
- async seedFollowNotifications() {
750
- if (this.store.has('notifications.json')) return;
751
- for (const f of this.store.getContacts().followers) {
752
- this.store.addNotification({ type: 'follow', actor: f.actor });
753
- }
754
- }
755
- }
756
-
757
- // Two megabytes is a few weeks of an ordinary agent's chatter, and small
758
- // enough that reading the whole thing is still reasonable.
759
- const LOG_MAX_BYTES = 2 * 1024 * 1024;
760
-
761
- export async function startAgent({
762
- home = process.env.AP_HOME || apRoot(),
763
- port = Number(process.env.AP_PORT) || 8030,
764
- gateToken = process.env.AP_GATE_TOKEN || '',
765
- name = null,
766
- takeover = false,
767
- handle = null,
768
- } = {}) {
769
- fs.mkdirSync(home, { recursive: true, mode: 0o700 });
770
- // A home can hold a FORUM rather than a person or a group: same profiles
771
- // directory, same unit, same startup — a different thing to run. Its
772
- // credential says so, and running the agent over it would publish a person
773
- // where a forum lives.
774
- const { isForumHome, runForum } = await import('./packages/fedipod-bb/src/run.mjs');
775
- if (isForumHome(home)) {
776
- console.log(`[bb:${port}] this home holds a forum — running it`);
777
- return runForum({ home, port, log: (...a) => console.log(`[bb:${port}]`, ...a) });
778
- }
779
- // connect() sets this from pod state a moment later, but the browser may
780
- // already be opening — seed it from what setup recorded so the named origin
781
- // works from the first request.
782
- // agent.json is how everything else finds this agent afterwards — `profiles`,
783
- // `stop`, the Actors list. The CLI used to be the only thing that wrote it, so
784
- // an agent spawned any other way (the admin page starts them now) ran
785
- // invisibly on a port nobody could look up. Merge, so `handle` survives.
786
- const agentJson = path.join(home, 'agent.json');
787
- let recorded = {};
788
- try { recorded = JSON.parse(fs.readFileSync(agentJson, 'utf8')) || {}; } catch { /* first run */ }
789
- if (!handle) handle = recorded.handle || null;
790
- if (recorded.port !== port) {
791
- try {
792
- fs.mkdirSync(home, { recursive: true, mode: 0o700 });
793
- writeJsonAtomic(agentJson, { ...recorded, port }, { mode: 0o644 });
794
- } catch { /* the agent still runs; it is just harder to find */ }
795
- }
796
- const logFile = path.join(home, 'agent.log');
797
- const agent = new Agent({ home, log: () => {} });
798
- // The port on every line. Agents sharing a home share agent.log, and without
799
- // it there is no way to tell which one wrote what — a viewer's startup reads
800
- // as the active one's work.
801
- const log = (...a) => {
802
- const line = `${new Date().toISOString()} :${port} ${a.join(' ')}`;
803
- console.log(`[ap:${port}]`, ...a);
804
- agent.logRing.push(line);
805
- if (agent.logRing.length > 500) agent.logRing.shift();
806
- try {
807
- // Rotated, because nothing else was ever going to. This file is appended
808
- // to on every drain, every delivery and every lease event, for the life
809
- // of an agent that is meant to run for months — on a phone under Termux
810
- // especially, an unbounded log is the thing that fills the disk. One
811
- // previous generation is kept; a crash investigation rarely wants two.
812
- if (fs.statSync(logFile).size > LOG_MAX_BYTES) fs.renameSync(logFile, logFile + '.1');
813
- } catch { /* no log yet, or a rename we cannot do — appending still works */ }
814
- try { fs.appendFileSync(logFile, line + '\n'); } catch { /* logging must never throw */ }
815
- };
816
- agent.log = log;
817
- agent.store.log = log;
818
- // Before the socket, not after: an agent that must not be reachable at that
819
- // address must never have answered there. This used to be a warning said at
820
- // connect time, which is both too late (the server has been listening for
821
- // seconds) and too narrow (it only ever described the OAuth login).
822
- const exposure = exposureProblem({ allowedHosts: process.env.AP_ALLOWED_HOSTS, gateToken });
823
- if (exposure) {
824
- log(`refusing to start:\n${exposure}`);
825
- process.exit(2);
826
- }
827
- // https and nothing else, on the one port this agent was given: a
828
- // per-machine certificate (never packaged — see lib/certs.mjs), covering
829
- // every identity on this root. A certificate problem stops the start rather
830
- // than quietly serving the UI and the API in the clear.
831
- let tls = null;
832
- try {
833
- tls = ensureTrustedTls(path.join(rootOf(agent.home), 'certs'),
834
- { log, names: hostLabel(handle) ? [`${hostLabel(handle)}.localhost`] : [] });
835
- } catch (e) {
836
- log(`refusing to start: certificate setup failed (${e.message})`);
837
- log('the agent serves https only — fix the certificate, or run `fedipod https --trust`');
838
- process.exit(2);
839
- }
840
- startAdmin({ port, gateToken, agent, log, handle, tls });
841
-
842
- // Is a newer FediPod published? Once at boot and daily after; the answer
843
- // rides /status and the record page offers the update.
844
- const updateTick = async () => {
845
- const { checkLatest } = await import('./lib/device/update.mjs');
846
- const u = await checkLatest();
847
- if (u) {
848
- agent.updateInfo = u;
849
- if (u.available) log(`FediPod ${u.latest} is available (running ${u.current}) — update from the record page or \`fedipod update\``);
850
- }
851
- };
852
- void updateTick();
853
- setInterval(updateTick, 24 * 3600e3).unref?.();
854
-
855
- // The pod (or its issuer) can be briefly unreachable — a 504 from the
856
- // token endpoint, or no network yet at boot under install-service. Keep
857
- // retrying with backoff instead of sitting unconfigured until someone
858
- // notices; the UI stays up throughout.
859
- const connectWithRetry = async () => {
860
- for (let attempt = 1; ; attempt++) {
861
- try {
862
- const up = await agent.connect({ name });
863
- if (up) {
864
- // A lease whose holder is gone but not expired would otherwise leave
865
- // us read-only for the whole TTL.
866
- if (takeover && agent.viewer) await agent.takeOver();
867
- return;
868
- }
869
- log('unconfigured — run `bin/fedipod.mjs setup` to begin');
870
- return; // no credential: retrying won't help
871
- } catch (e) {
872
- // Caps at an hour, not ten minutes: a pod that has refused for an hour
873
- // is not going to be helped by asking six times more per hour, and an
874
- // agent left running for days should not be a fixture in its logs.
875
- // Jittered so restarts of several agents do not line up.
876
- const base = Math.min(30 * 2 ** (attempt - 1), 3600);
877
- const wait = Math.round(base * (0.8 + Math.random() * 0.4));
878
- log(`connect failed (attempt ${attempt}): ${e.message} — retrying in ${wait}s`);
879
- await new Promise(r => setTimeout(r, wait * 1000));
880
- }
881
- }
882
- };
883
- connectWithRetry();
884
- const shutdown = () => {
885
- setTimeout(() => process.exit(0), 1500).unref(); // never hang a stop on a slow pod
886
- try { fs.rmSync(path.join(home, 'agent.pid'), { force: true }); } catch {}
887
- Promise.allSettled([
888
- agent.store.flush(),
889
- agent.viewer ? Promise.resolve() : agent.lease?.release(),
890
- ]).finally(() => process.exit(0));
891
- };
892
- process.on('SIGTERM', shutdown);
893
- process.on('SIGINT', shutdown);
894
- return agent;
895
- }
896
-
897
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
898
- startAgent();
899
- }