fedipod 0.8.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 (276) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/architecture.svg +89 -0
  4. package/bin/fedipod.mjs +2303 -0
  5. package/cli.md +243 -0
  6. package/groups.md +85 -0
  7. package/gui.md +99 -0
  8. package/lib/account.mjs +87 -0
  9. package/lib/admin.mjs +1606 -0
  10. package/lib/atproto.mjs +310 -0
  11. package/lib/bskyfeed.mjs +166 -0
  12. package/lib/bskygroup.mjs +162 -0
  13. package/lib/c2s.mjs +305 -0
  14. package/lib/certs.mjs +265 -0
  15. package/lib/deliver.mjs +249 -0
  16. package/lib/directory.mjs +90 -0
  17. package/lib/embed.mjs +214 -0
  18. package/lib/export-collections.mjs +161 -0
  19. package/lib/front-core.mjs +322 -0
  20. package/lib/gateway-core.mjs +115 -0
  21. package/lib/guard.mjs +213 -0
  22. package/lib/home.mjs +161 -0
  23. package/lib/httpsig.mjs +109 -0
  24. package/lib/import.mjs +357 -0
  25. package/lib/intake.mjs +1710 -0
  26. package/lib/keys.mjs +139 -0
  27. package/lib/lease.mjs +190 -0
  28. package/lib/localapi.mjs +87 -0
  29. package/lib/mastoapi.mjs +1588 -0
  30. package/lib/migrate.mjs +127 -0
  31. package/lib/oidc-auth.mjs +53 -0
  32. package/lib/podrdf.mjs +132 -0
  33. package/lib/ports.mjs +27 -0
  34. package/lib/publisher.mjs +1003 -0
  35. package/lib/remote.mjs +303 -0
  36. package/lib/safefetch.mjs +235 -0
  37. package/lib/setup.mjs +262 -0
  38. package/lib/social.mjs +505 -0
  39. package/lib/storage.mjs +170 -0
  40. package/lib/store.mjs +470 -0
  41. package/lib/streaming.mjs +154 -0
  42. package/lib/tagfeed.mjs +158 -0
  43. package/lib/ua.mjs +29 -0
  44. package/lib/update.mjs +91 -0
  45. package/lib/webpush.mjs +98 -0
  46. package/lib/wire.mjs +762 -0
  47. package/package.json +60 -0
  48. package/phanpy/dist/404.html +40 -0
  49. package/phanpy/dist/_headers +2 -0
  50. package/phanpy/dist/apple-touch-icon.png +0 -0
  51. package/phanpy/dist/assets/ICONS-DIPJeU0C.js +2 -0
  52. package/phanpy/dist/assets/ICONS-DIPJeU0C.js.map +1 -0
  53. package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js +183 -0
  54. package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js.map +1 -0
  55. package/phanpy/dist/assets/Temml-CCjoacWt.woff2 +0 -0
  56. package/phanpy/dist/assets/__vite-browser-external-C7iut881.js +2 -0
  57. package/phanpy/dist/assets/__vite-browser-external-C7iut881.js.map +1 -0
  58. package/phanpy/dist/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  59. package/phanpy/dist/assets/catch-up-O5q5C75Z.png +0 -0
  60. package/phanpy/dist/assets/chunk-aKtaBQYM.js +1 -0
  61. package/phanpy/dist/assets/compose-CFEx7wb4.js +2 -0
  62. package/phanpy/dist/assets/compose-CFEx7wb4.js.map +1 -0
  63. package/phanpy/dist/assets/compose-ZJN5xSb4.js +35 -0
  64. package/phanpy/dist/assets/compose-ZJN5xSb4.js.map +1 -0
  65. package/phanpy/dist/assets/debug.module-YFQgwnIr.js +52 -0
  66. package/phanpy/dist/assets/debug.module-YFQgwnIr.js.map +1 -0
  67. package/phanpy/dist/assets/dom-p1YcJ5Rw.js +2 -0
  68. package/phanpy/dist/assets/dom-p1YcJ5Rw.js.map +1 -0
  69. package/phanpy/dist/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  70. package/phanpy/dist/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  71. package/phanpy/dist/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  72. package/phanpy/dist/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  73. package/phanpy/dist/assets/icons/add-circle-line-WD0mwORD.js +1 -0
  74. package/phanpy/dist/assets/icons/alert-line-CpFUMf9s.js +1 -0
  75. package/phanpy/dist/assets/icons/android-2-line-Bzxnrd3S.js +1 -0
  76. package/phanpy/dist/assets/icons/announcement-line-5nwaJFJm.js +1 -0
  77. package/phanpy/dist/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +1 -0
  78. package/phanpy/dist/assets/icons/arrow-down-line-B8uzXzq6.js +1 -0
  79. package/phanpy/dist/assets/icons/arrow-left-line-CGrGT9C-.js +1 -0
  80. package/phanpy/dist/assets/icons/arrow-right-line-CE-HERfX.js +1 -0
  81. package/phanpy/dist/assets/icons/arrow-to-up-line-CvdQjmcy.js +1 -0
  82. package/phanpy/dist/assets/icons/arrow-up-circle-line-DEparSNF.js +1 -0
  83. package/phanpy/dist/assets/icons/arrow-up-line-C7yBeugc.js +1 -0
  84. package/phanpy/dist/assets/icons/arrows-right-line-BtbXmJGP.js +1 -0
  85. package/phanpy/dist/assets/icons/at-line-CF66Bznk.js +1 -0
  86. package/phanpy/dist/assets/icons/attachment-line-BY8It-l9.js +1 -0
  87. package/phanpy/dist/assets/icons/blockquote-line-BS6pU_hm.js +1 -0
  88. package/phanpy/dist/assets/icons/board-line-C0UWXyA6.js +1 -0
  89. package/phanpy/dist/assets/icons/bookmark-line-1CtX5JLF.js +1 -0
  90. package/phanpy/dist/assets/icons/building-5-line-Dd59MzbO.js +1 -0
  91. package/phanpy/dist/assets/icons/bus-2-line-6_dvA0GG.js +1 -0
  92. package/phanpy/dist/assets/icons/calendar-day-line-Crpl_fmu.js +1 -0
  93. package/phanpy/dist/assets/icons/calendar-month-line-vommts2C.js +1 -0
  94. package/phanpy/dist/assets/icons/calendar-time-add-line-BU_DTlQk.js +1 -0
  95. package/phanpy/dist/assets/icons/camera-line-Cv69MEei.js +1 -0
  96. package/phanpy/dist/assets/icons/celebrate-line-B8DvIoi0.js +1 -0
  97. package/phanpy/dist/assets/icons/chart-bar-line-FK-QOmtF.js +1 -0
  98. package/phanpy/dist/assets/icons/chart-line-line-BGcJAFWw.js +1 -0
  99. package/phanpy/dist/assets/icons/chat-3-line-YFK2-hwz.js +1 -0
  100. package/phanpy/dist/assets/icons/check-circle-line-DsAxFr5-.js +1 -0
  101. package/phanpy/dist/assets/icons/clipboard-line-BBp364gJ.js +1 -0
  102. package/phanpy/dist/assets/icons/close-circle-line-CICdiuGz.js +1 -0
  103. package/phanpy/dist/assets/icons/close-line-DYrFWcjv.js +1 -0
  104. package/phanpy/dist/assets/icons/cloud-line-tiQAbah8.js +1 -0
  105. package/phanpy/dist/assets/icons/code-line-fAl8iiV3.js +1 -0
  106. package/phanpy/dist/assets/icons/comment-2-line-CR3g7WeB.js +1 -0
  107. package/phanpy/dist/assets/icons/copy-2-line-CLW69yF5.js +1 -0
  108. package/phanpy/dist/assets/icons/delete-2-line-B8xmbr4n.js +1 -0
  109. package/phanpy/dist/assets/icons/document-3-line-DBGNhAg2.js +1 -0
  110. package/phanpy/dist/assets/icons/document-line-C7mb9sb7.js +1 -0
  111. package/phanpy/dist/assets/icons/down-line-BGqSSThd.js +1 -0
  112. package/phanpy/dist/assets/icons/edit-4-line-DcgGaEhx.js +1 -0
  113. package/phanpy/dist/assets/icons/emoji-2-line-D8YgnM-y.js +1 -0
  114. package/phanpy/dist/assets/icons/exit-line-Dc07BS-x.js +1 -0
  115. package/phanpy/dist/assets/icons/external-link-line-BvpSkmjO.js +1 -0
  116. package/phanpy/dist/assets/icons/eye-2-line-CiWXx-f_.js +1 -0
  117. package/phanpy/dist/assets/icons/eye-close-line-BgTaSJ4P.js +1 -0
  118. package/phanpy/dist/assets/icons/filter-2-line-B_mR2usy.js +1 -0
  119. package/phanpy/dist/assets/icons/filter-line-C_nm-3cU.js +1 -0
  120. package/phanpy/dist/assets/icons/flag-1-line-C2HB05I0.js +1 -0
  121. package/phanpy/dist/assets/icons/forbid-circle-line-D3LTT9yd.js +1 -0
  122. package/phanpy/dist/assets/icons/formula-line-DLrt2d6j.js +1 -0
  123. package/phanpy/dist/assets/icons/grid-line-CgUmEikf.js +1 -0
  124. package/phanpy/dist/assets/icons/group-line-C5fnX0wU.js +1 -0
  125. package/phanpy/dist/assets/icons/hand-finger-2-line-BGgxbuL9.js +1 -0
  126. package/phanpy/dist/assets/icons/hashtag-line-CyTEEXBb.js +1 -0
  127. package/phanpy/dist/assets/icons/heart-crack-line-BZddbO2C.js +1 -0
  128. package/phanpy/dist/assets/icons/heart-line-CJ7K47J4.js +1 -0
  129. package/phanpy/dist/assets/icons/history-2-line-kWWvFzqv.js +1 -0
  130. package/phanpy/dist/assets/icons/history-line-bGQiwbTH.js +1 -0
  131. package/phanpy/dist/assets/icons/home-3-line-fRU1zeLG.js +1 -0
  132. package/phanpy/dist/assets/icons/information-line-iNk6-sOY.js +1 -0
  133. package/phanpy/dist/assets/icons/keyboard-line-DsCyuQNz.js +1 -0
  134. package/phanpy/dist/assets/icons/layout-4-line-CZkOX52Z.js +1 -0
  135. package/phanpy/dist/assets/icons/layout-5-line-B_znt5wO.js +1 -0
  136. package/phanpy/dist/assets/icons/left-line-eUw0tya4.js +1 -0
  137. package/phanpy/dist/assets/icons/lightning-line-DaKoDdix.js +1 -0
  138. package/phanpy/dist/assets/icons/link-2-line-DCqLmMA7.js +1 -0
  139. package/phanpy/dist/assets/icons/list-check-line-BAxosBh9.js +1 -0
  140. package/phanpy/dist/assets/icons/lock-line-CPoaXeUW.js +1 -0
  141. package/phanpy/dist/assets/icons/mail-line-B0P8aa1L.js +1 -0
  142. package/phanpy/dist/assets/icons/moon-line-CEqNAToP.js +1 -0
  143. package/phanpy/dist/assets/icons/more-1-fill-Dw2e5xab.js +1 -0
  144. package/phanpy/dist/assets/icons/more-3-line-BIiVjYTz.js +1 -0
  145. package/phanpy/dist/assets/icons/notification-line-BZZq2Gtu.js +1 -0
  146. package/phanpy/dist/assets/icons/pencil-line-CKh8-A1v.js +1 -0
  147. package/phanpy/dist/assets/icons/photo-album-line-Bnfg2u40.js +1 -0
  148. package/phanpy/dist/assets/icons/pin-line-DpTolyhs.js +1 -0
  149. package/phanpy/dist/assets/icons/play-fill-B0-G_8lX.js +1 -0
  150. package/phanpy/dist/assets/icons/qrcode-2-line-B6SPCaHn.js +1 -0
  151. package/phanpy/dist/assets/icons/quill-pen-line-B6LUrnCP.js +1 -0
  152. package/phanpy/dist/assets/icons/quote-left-fill-hQZ1cj_6.js +1 -0
  153. package/phanpy/dist/assets/icons/radar-line-CKyroTth.js +1 -0
  154. package/phanpy/dist/assets/icons/react-line-CoIKhhtK.js +1 -0
  155. package/phanpy/dist/assets/icons/refresh-2-line-D0mTF97S.js +1 -0
  156. package/phanpy/dist/assets/icons/right-line-B3KWJaqc.js +1 -0
  157. package/phanpy/dist/assets/icons/rocket-line-4KZl537D.js +1 -0
  158. package/phanpy/dist/assets/icons/round-fill-C69NTrZ0.js +1 -0
  159. package/phanpy/dist/assets/icons/round-line-B9k_1uo-.js +1 -0
  160. package/phanpy/dist/assets/icons/route-line-B28ReHcE.js +1 -0
  161. package/phanpy/dist/assets/icons/rows-4-line-CrBniagM.js +1 -0
  162. package/phanpy/dist/assets/icons/scan-line-DSf74YdP.js +1 -0
  163. package/phanpy/dist/assets/icons/search-2-line-B6geIU1z.js +1 -0
  164. package/phanpy/dist/assets/icons/settings-3-line-azJwK47w.js +1 -0
  165. package/phanpy/dist/assets/icons/settings-6-line-C_5zc5YR.js +1 -0
  166. package/phanpy/dist/assets/icons/share-2-line-D5MxWjrm.js +1 -0
  167. package/phanpy/dist/assets/icons/share-forward-line-Cp61fZzZ.js +1 -0
  168. package/phanpy/dist/assets/icons/sparkles-2-line-DYYyXTaW.js +1 -0
  169. package/phanpy/dist/assets/icons/sparkles-line-C2hY1s6I.js +1 -0
  170. package/phanpy/dist/assets/icons/time-line-BF3eet1v.js +1 -0
  171. package/phanpy/dist/assets/icons/transfer-4-line-oisfvTBw.js +1 -0
  172. package/phanpy/dist/assets/icons/translate-line-BFWol2Ce.js +1 -0
  173. package/phanpy/dist/assets/icons/unlock-line-X_x5vwv3.js +1 -0
  174. package/phanpy/dist/assets/icons/upload-3-line-DgzwUJeW.js +1 -0
  175. package/phanpy/dist/assets/icons/user-4-line-BVSynZCp.js +1 -0
  176. package/phanpy/dist/assets/icons/user-add-2-line-BlGS5Q8q.js +1 -0
  177. package/phanpy/dist/assets/icons/user-add-line-BbXdHG4z.js +1 -0
  178. package/phanpy/dist/assets/icons/user-edit-line-BFAHOV0X.js +1 -0
  179. package/phanpy/dist/assets/icons/user-follow-line-BXux0wSd.js +1 -0
  180. package/phanpy/dist/assets/icons/user-star-line-DdYlpHLU.js +1 -0
  181. package/phanpy/dist/assets/icons/user-warning-line-Cj43j0_E.js +1 -0
  182. package/phanpy/dist/assets/icons/user-x-line-DIDgNlPH.js +1 -0
  183. package/phanpy/dist/assets/icons/volume-line-Bs390KQt.js +1 -0
  184. package/phanpy/dist/assets/icons/volume-mute-line-DzlzIqFZ.js +1 -0
  185. package/phanpy/dist/assets/icons/walk-line-yNXfB-md.js +1 -0
  186. package/phanpy/dist/assets/icons/world-2-line-CenkGIwj.js +1 -0
  187. package/phanpy/dist/assets/icons/zoom-in-line-CaENUphf.js +1 -0
  188. package/phanpy/dist/assets/icons/zoom-out-line-BDNk0EWo.js +1 -0
  189. package/phanpy/dist/assets/instances-BNPptnn-.json +333 -0
  190. package/phanpy/dist/assets/locales/ar-SA-CfheRbCu.js +1 -0
  191. package/phanpy/dist/assets/locales/ca-ES-DHRW86Tz.js +1 -0
  192. package/phanpy/dist/assets/locales/cs-CZ-CKoqEuA1.js +1 -0
  193. package/phanpy/dist/assets/locales/de-DE-ByRPMRFy.js +1 -0
  194. package/phanpy/dist/assets/locales/eo-UY-CPC7wLCS.js +1 -0
  195. package/phanpy/dist/assets/locales/es-ES-dJa0Fd9t.js +1 -0
  196. package/phanpy/dist/assets/locales/eu-ES-C3djlPR2.js +1 -0
  197. package/phanpy/dist/assets/locales/fa-IR-DTT490Ck.js +1 -0
  198. package/phanpy/dist/assets/locales/fi-FI-B23MCPuz.js +1 -0
  199. package/phanpy/dist/assets/locales/fr-FR-BcaQsO5C.js +1 -0
  200. package/phanpy/dist/assets/locales/gl-ES-BR-CVKBN.js +1 -0
  201. package/phanpy/dist/assets/locales/he-IL-CfheRbCu.js +1 -0
  202. package/phanpy/dist/assets/locales/hu-HU-CfheRbCu.js +1 -0
  203. package/phanpy/dist/assets/locales/it-IT-C1136UFV.js +1 -0
  204. package/phanpy/dist/assets/locales/ja-JP-CRZmRAhD.js +1 -0
  205. package/phanpy/dist/assets/locales/kab-Cz8xH_TT.js +1 -0
  206. package/phanpy/dist/assets/locales/ko-KR-CG9UiY4o.js +1 -0
  207. package/phanpy/dist/assets/locales/lt-LT-CJcI6sHZ.js +1 -0
  208. package/phanpy/dist/assets/locales/nb-NO-CfheRbCu.js +1 -0
  209. package/phanpy/dist/assets/locales/nl-NL-DxN4Yc1t.js +1 -0
  210. package/phanpy/dist/assets/locales/oc-FR-CfheRbCu.js +1 -0
  211. package/phanpy/dist/assets/locales/pl-PL-DkwGXf6P.js +1 -0
  212. package/phanpy/dist/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +1 -0
  213. package/phanpy/dist/assets/locales/pt-BR-nGL0HdFS.js +1 -0
  214. package/phanpy/dist/assets/locales/pt-PT-Cp17aj2z.js +1 -0
  215. package/phanpy/dist/assets/locales/ru-RU-0Y8cIoaK.js +1 -0
  216. package/phanpy/dist/assets/locales/th-TH-CfheRbCu.js +1 -0
  217. package/phanpy/dist/assets/locales/tok-CfheRbCu.js +1 -0
  218. package/phanpy/dist/assets/locales/tr-TR-CfheRbCu.js +1 -0
  219. package/phanpy/dist/assets/locales/uk-UA-C5E3dFBf.js +1 -0
  220. package/phanpy/dist/assets/locales/zh-CN-BHJi7_et.js +1 -0
  221. package/phanpy/dist/assets/locales/zh-TW-CfheRbCu.js +1 -0
  222. package/phanpy/dist/assets/main-BdqNbG-a.js +26 -0
  223. package/phanpy/dist/assets/main-BdqNbG-a.js.map +1 -0
  224. package/phanpy/dist/assets/mock-home-DGhR__He.js +2 -0
  225. package/phanpy/dist/assets/mock-home-DGhR__He.js.map +1 -0
  226. package/phanpy/dist/assets/multi-column-ROck0NVt.jpg +0 -0
  227. package/phanpy/dist/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  228. package/phanpy/dist/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  229. package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js +2 -0
  230. package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js.map +1 -0
  231. package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js +20 -0
  232. package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js.map +1 -0
  233. package/phanpy/dist/assets/style-B1iLPTPR.css +2 -0
  234. package/phanpy/dist/assets/temml-DuyIQMei.js +97 -0
  235. package/phanpy/dist/assets/temml-DuyIQMei.js.map +1 -0
  236. package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js +2 -0
  237. package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js.map +1 -0
  238. package/phanpy/dist/assets/year-in-posts-B2lywqdx.png +0 -0
  239. package/phanpy/dist/assets/year-in-posts-DJv_star.js +3 -0
  240. package/phanpy/dist/assets/year-in-posts-DJv_star.js.map +1 -0
  241. package/phanpy/dist/compose/index.html +20 -0
  242. package/phanpy/dist/favicon.ico +0 -0
  243. package/phanpy/dist/index.html +73 -0
  244. package/phanpy/dist/logo-192.png +0 -0
  245. package/phanpy/dist/logo-512.png +0 -0
  246. package/phanpy/dist/logo-badge-72.png +0 -0
  247. package/phanpy/dist/logo-maskable-512.png +0 -0
  248. package/phanpy/dist/logo-monochrome-512.png +0 -0
  249. package/phanpy/dist/logo-monochrome-maskable-512.png +0 -0
  250. package/phanpy/dist/manifest.webmanifest +1 -0
  251. package/phanpy/dist/og-image-2.jpg +0 -0
  252. package/phanpy/dist/og-image.png +0 -0
  253. package/phanpy/dist/robots.txt +2 -0
  254. package/phanpy/dist/sw.js +25 -0
  255. package/phanpy/dist/version.json +1 -0
  256. package/run-agent.mjs +805 -0
  257. package/scripts/build-dist.mjs +37 -0
  258. package/scripts/verify-ui.mjs +60 -0
  259. package/ui/README.md +23 -0
  260. package/vendor/gate.cjs +132 -0
  261. package/vendor/idp-grant.cjs +394 -0
  262. package/vendor/jfetch.cjs +16 -0
  263. package/web/admin/admin.js +958 -0
  264. package/web/admin/bar.css +64 -0
  265. package/web/admin/bar.js +61 -0
  266. package/web/admin/client/client.js +66 -0
  267. package/web/admin/client/index.html +42 -0
  268. package/web/admin/index.html +493 -0
  269. package/web/admin/setup/index.html +172 -0
  270. package/web/admin/setup/setup.js +268 -0
  271. package/web/admin/window.css +55 -0
  272. package/web/admin/window.js +136 -0
  273. package/web/front/install.sh +69 -0
  274. package/web/front/new-account.html +424 -0
  275. package/web/front/run.html +138 -0
  276. package/web/front/solid-client-authn.bundle.js +2 -0
@@ -0,0 +1,127 @@
1
+ // Keeping every identity on this machine at the current shape of an install.
2
+ //
3
+ // bbba587 fixed the DEFAULT: a new install keeps its private half — timeline,
4
+ // contacts, blocklist, notifications, masto-tokens.json — on this machine.
5
+ // It did nothing for the installs that already existed, and `privateRoot`
6
+ // absent still means "on the pod" (run-agent.mjs privateUrls). So an identity
7
+ // set up before that commit still writes every state change to a pod, and
8
+ // still keeps its private data on a server.
9
+ //
10
+ // A new default only ever fixes the installs that do not exist yet. That is the
11
+ // reason this file exists rather than a one-off script: a change to install
12
+ // shape ships as a numbered step HERE, and `fedipod upgrade` runs the
13
+ // pending ones over every identity under the root, not just the one you
14
+ // happened to be in.
15
+
16
+ // Bump when a step is added, and add the step to pendingSteps below.
17
+ export const CURRENT_LAYOUT = 1;
18
+
19
+ // 0 is "never stamped", which is every install made before this file existed.
20
+ // It is not the same as "wrong": an old install may already be in the right
21
+ // shape, and pendingSteps is what decides, per step.
22
+ export const layoutOf = (cred) => Number(cred?.layout) || 0;
23
+
24
+ // Layout 1 — the private half lives on this machine.
25
+ //
26
+ // Only when `privateRoot` is ABSENT. An operator who pointed it somewhere
27
+ // deliberately, with `--private-root`, has said where they want it; a migration
28
+ // that overrode that would be moving data on its own initiative.
29
+ export function needsStateMove(cred) {
30
+ return !cred?.privateRoot;
31
+ }
32
+
33
+ export function pendingSteps(cred) {
34
+ const steps = [];
35
+ if (needsStateMove(cred)) {
36
+ steps.push({
37
+ id: 'state-off-pod',
38
+ what: 'move the private half off the pod, onto this machine',
39
+ why: 'receiving a post costs pod writes, and the timeline lives on a server',
40
+ });
41
+ }
42
+ return steps;
43
+ }
44
+
45
+ // True when there is nothing left to do and the stamp can be written.
46
+ export const isCurrent = (cred) => pendingSteps(cred).length === 0;
47
+
48
+ // The documents on the pod that the state tree consists of — everything the
49
+ // move copied. Deliberately NOT "everything in the container":
50
+ //
51
+ // lease.json stays. It is the single-active-agent lock, and it is on the pod
52
+ // precisely BECAUSE the private half is not: a lease only one machine can
53
+ // reach coordinates nothing. run-agent builds it from urls.state, never from
54
+ // privateRoot, so it goes on being used after the move.
55
+ //
56
+ // .keep and .acl belong to the container, which also stays — the container is
57
+ // what carries the owner-only ACL that lease.json sits behind.
58
+ //
59
+ // RemotePod.delete's deny-list is the backstop under all of this; this list is
60
+ // what we affirmatively claim, and the two disagreeing is a bug worth hearing
61
+ // about rather than a file quietly surviving.
62
+ export const MOVED_STATE_DOCS = new Set([
63
+ 'config.json', 'queue.json', 'blocklist.json', 'contacts.json', 'muted.json',
64
+ 'pending.json', 'requests.json', 'deadletter.json', 'statuses.json',
65
+ 'notifications.json', 'media.json', 'actors.json', 'ids.json', 'published.json',
66
+ 'outbox.json', 'outbox-removed.json', 'parked.json', 'tagfeed.json',
67
+ 'intake-attempts.json', 'inbox-channel.json', 'masto-tokens.json', 'masto-markers.json',
68
+ 'lists.json', 'filters.json', 'scheduled.json', 'webpush.json',
69
+ 'import-state.json',
70
+ // Present only in pod-keys mode; a state move must carry it or the moved
71
+ // store cannot sign.
72
+ 'keys.json',
73
+ ]);
74
+
75
+ export const KEPT_ON_POD = new Set(['lease.json', '.keep']);
76
+
77
+ // What `state --drop-remote` would remove, given a container listing. Split out
78
+ // so the decision can be tested without a pod, and so the caller can show it
79
+ // before doing anything.
80
+ export function classifyRemoteState(urls, base) {
81
+ const drop = [];
82
+ const keep = [];
83
+ for (const u of urls) {
84
+ const name = String(u).slice(String(base).length).replace(/^\/+/, '');
85
+ if (!name || name.includes('/')) { keep.push({ url: u, name, why: 'not a document of this container' }); continue; }
86
+ if (KEPT_ON_POD.has(name)) {
87
+ keep.push({ url: u, name, why: name === 'lease.json' ? 'the lease stays on the pod by design' : 'the container needs it' });
88
+ } else if (MOVED_STATE_DOCS.has(name)) {
89
+ drop.push({ url: u, name });
90
+ } else {
91
+ keep.push({ url: u, name, why: 'not one of ours' });
92
+ }
93
+ }
94
+ return { drop, keep };
95
+ }
96
+
97
+ // Copy the private half — every state document, then the RDF tree — from one
98
+ // pair of storages to the other. Copy and verify only: nothing is repointed and
99
+ // nothing is deleted, so a failure part-way leaves the source authoritative.
100
+ // Used by both `state --to` and the record page's Move private data.
101
+ export async function copyPrivateHalf({ from, to, log = () => {} }) {
102
+ const { PodStore } = await import('./store.mjs');
103
+ const { PodRdf } = await import('./podrdf.mjs');
104
+ const src = new PodStore({ log: () => {} });
105
+ src.attach(from.state);
106
+ await src.load();
107
+ const dst = new PodStore({ log });
108
+ dst.attach(to.state);
109
+ for (const [name, value] of src.cache) dst.write(name, value);
110
+ if (!await dst.commit()) {
111
+ throw new Error('some state documents did not land — nothing was repointed, nothing was deleted');
112
+ }
113
+ const rdfFrom = new PodRdf({ storage: from.fediverse });
114
+ const rdfTo = new PodRdf({ storage: to.fediverse });
115
+ let notes = 0;
116
+ for (const doc of ['settings', 'contacts']) {
117
+ try { await rdfTo.put(rdfTo.fedi + doc, await rdfFrom.get(rdfFrom.fedi + doc)); }
118
+ catch (e) { if (!/ 404$/.test(e.message)) throw e; }
119
+ }
120
+ for (const kind of ['posts', 'timeline']) {
121
+ for (const url of await rdfFrom.listNotes(kind)) {
122
+ await rdfTo.put(`${rdfTo.fedi}${kind}/${url.split('/').pop()}`, await rdfFrom.get(url));
123
+ notes++;
124
+ }
125
+ }
126
+ return { docs: src.cache.size, notes };
127
+ }
@@ -0,0 +1,53 @@
1
+ // oidc-auth.mjs — who may drive the C2S surface. Two proofs are accepted:
2
+ // the Mastodon facade's bearer (minted on this machine, so its holder is the
3
+ // operator by construction), or a Solid-OIDC token — DPoP-bound when the
4
+ // client sends the proof header — verified by the same library the wider
5
+ // Solid world uses. The token alone is not enough: the WebID it names must
6
+ // be THIS identity's owner, an authorization step the token does not carry.
7
+ //
8
+ // The verifier is injected so offline tests stub it, and wrapped so the
9
+ // library (CJS, older jose) can be replaced without touching any caller.
10
+
11
+ export function makeC2sAuth({ agent, masto = null, verifier = null, log = () => {}, scheme = null }) {
12
+ let verify = verifier;
13
+ const loadVerifier = async () => {
14
+ if (!verify) {
15
+ const { createSolidTokenVerifier } = await import('@solid/access-token-verifier');
16
+ verify = createSolidTokenVerifier();
17
+ }
18
+ return verify;
19
+ };
20
+
21
+ return async function authenticate(req, pathname) {
22
+ if (masto?.authed(req)) {
23
+ return { ok: true, webid: agent.remote?.webId || null, via: 'bearer' };
24
+ }
25
+ if (!req.headers.authorization) {
26
+ return { ok: false, status: 401, error: 'authentication required: a Solid-OIDC token (DPoP) or this agent\'s own bearer' };
27
+ }
28
+ let webid;
29
+ try {
30
+ const v = await loadVerifier();
31
+ // The URL the client signed its proof over. The Host header already
32
+ // passed the Authorities firewall, so whichever alias the client used
33
+ // (localhost, 127.0.0.1, the named origin) is one this agent answers on;
34
+ // the scheme is whichever listener the request arrived on.
35
+ const htu = `${scheme ? scheme.replace(/:$/u, '') : req.socket?.encrypted ? 'https' : 'http'
36
+ }://${req.headers.host}${pathname}`;
37
+ ({ webid } = await v(
38
+ req.headers.authorization,
39
+ req.headers.dpop ? { header: req.headers.dpop, method: req.method, url: htu } : undefined,
40
+ ));
41
+ } catch (e) {
42
+ log(`c2s auth: token rejected — ${e.message}`);
43
+ return { ok: false, status: 401, error: `token rejected: ${e.message}` };
44
+ }
45
+ const owner = agent.remote?.webId;
46
+ if (!owner || webid !== owner) {
47
+ // Verified is not authorized: a valid token from ANY WebID must not
48
+ // post as this actor. (The gap jg10's outbox leaves open.)
49
+ return { ok: false, status: 403, error: 'authenticated, but this outbox belongs to its owner alone' };
50
+ }
51
+ return { ok: true, webid, via: 'oidc' };
52
+ };
53
+ }
package/lib/podrdf.mjs ADDED
@@ -0,0 +1,132 @@
1
+ // podrdf.mjs — the RDF source of truth under /activitypods-js/fediverse/:
2
+ // settings, contacts, and one document per note. Same documents and AS2
3
+ // vocabulary as dk's local.mjs (plan: ap-pod-mapping.md).
4
+ //
5
+ // Parsed and serialised by rdflib, never by hand. Escaping, encoding and shape
6
+ // are exactly what a regex gets wrong quietly — a tab inside a literal, a '%'
7
+ // in a slug, a triple laid out in a form the pattern did not anticipate — and
8
+ // a hand-built serialiser writes the documents the hand-built parser reads
9
+ // back, so the two bugs cover for each other. See claude/plans/no-regex-rdf.md.
10
+ //
11
+ // The container it lives in is a Storage (lib/storage.mjs): a pod over HTTP or
12
+ // a directory. Callers still speak in absolute URLs, which is what the rest of
13
+ // the agent has.
14
+
15
+ import * as $rdf from 'rdflib';
16
+
17
+ const AS = $rdf.Namespace('https://www.w3.org/ns/activitystreams#');
18
+ const RDF = $rdf.Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#');
19
+ const XSD = $rdf.Namespace('http://www.w3.org/2001/XMLSchema#');
20
+ const TURTLE = 'text/turtle';
21
+
22
+ export class PodRdf {
23
+ constructor({ storage }) {
24
+ this.storage = storage;
25
+ this.base = storage.base;
26
+ this.fedi = this.base;
27
+ }
28
+
29
+ // Callers hold absolute URLs; the storage works in paths under its base.
30
+ _rel(url) {
31
+ if (!url.startsWith(this.fedi)) throw new Error(`${url} is not under ${this.fedi}`);
32
+ return url.slice(this.fedi.length);
33
+ }
34
+
35
+ async get(url) {
36
+ const r = await this.storage.read(this._rel(url));
37
+ if (!r.ok) throw new Error(`pod GET ${url} → ${r.status}`);
38
+ return r.body;
39
+ }
40
+
41
+ async put(url, body, contentType = TURTLE) {
42
+ const r = await this.storage.write(this._rel(url), body, contentType);
43
+ if (!r.ok) throw new Error(`pod PUT ${url} → ${r.why}`);
44
+ }
45
+
46
+ async delete(url) {
47
+ if (!await this.storage.remove(this._rel(url))) throw new Error(`pod DELETE ${url} failed`);
48
+ }
49
+
50
+ // Child resource URLs of {fedi}{kind}/ (empty when the container is absent).
51
+ async listNotes(kind) {
52
+ const { names } = await this.storage.list(`${kind}/`);
53
+ return names
54
+ // `.<pid>.tmp` is what FileStorage.write leaves if it is interrupted
55
+ // between the write and the rename. It is not a note, and handing one to
56
+ // the parser produces a failure that reads like a corrupt document.
57
+ .filter(n => !n.endsWith('/') && !/\.(acl|meta)$/.test(n) && !/\.\d+\.tmp$/.test(n))
58
+ .map(n => `${this.fedi}${kind}/${n}`);
59
+ }
60
+
61
+ _graph(url, ttl) {
62
+ const g = $rdf.graph();
63
+ $rdf.parse(ttl, g, url, TURTLE);
64
+ return g;
65
+ }
66
+
67
+ // Inverse of writeNote for one resource.
68
+ async readNote(url) {
69
+ const g = this._graph(url, await this.get(url));
70
+ const doc = $rdf.sym(url);
71
+ const iri = (p) => g.any(doc, AS(p), null, doc)?.value;
72
+ const str = (p) => g.any(doc, AS(p), null, doc)?.value;
73
+ const attachments = g.each(doc, AS('attachment'), null, doc).map((a) => {
74
+ const mediaType = g.any(a, AS('mediaType'), null, doc)?.value;
75
+ const description = g.any(a, AS('name'), null, doc)?.value;
76
+ return { url: a.value, mediaType: mediaType || '', ...(description ? { description } : {}) };
77
+ });
78
+ return {
79
+ noteId: iri('url'), actor: iri('attributedTo'),
80
+ published: str('published'), inReplyTo: iri('inReplyTo'), content: str('content'),
81
+ ...(attachments.length ? { attachments } : {}),
82
+ };
83
+ }
84
+
85
+ // Incoming or own post → one RDF resource. kind: 'timeline' | 'posts'
86
+ async writeNote(kind, slug, { noteId, actor, published, content, inReplyTo, attachments }) {
87
+ const url = `${this.fedi}${kind}/${slug}`;
88
+ const doc = $rdf.sym(url);
89
+ const g = $rdf.graph();
90
+ g.add(doc, RDF('type'), AS('Note'), doc);
91
+ g.add(doc, AS('url'), $rdf.sym(noteId), doc);
92
+ g.add(doc, AS('attributedTo'), $rdf.sym(actor), doc);
93
+ // Two-arg with a NamedNode is how rdflib takes a datatype; passing it
94
+ // third silently yields an xsd:string, which would drop ^^xsd:dateTime
95
+ // from every note published from here on.
96
+ if (published) g.add(doc, AS('published'), $rdf.literal(published, XSD('dateTime')), doc);
97
+ if (inReplyTo) g.add(doc, AS('inReplyTo'), $rdf.sym(inReplyTo), doc);
98
+ g.add(doc, AS('content'), $rdf.literal(content || ''), doc);
99
+ for (const a of attachments || []) {
100
+ const at = $rdf.sym(a.url);
101
+ g.add(doc, AS('attachment'), at, doc);
102
+ g.add(at, RDF('type'), AS('Document'), doc);
103
+ if (a.mediaType) g.add(at, AS('mediaType'), $rdf.literal(a.mediaType), doc);
104
+ if (a.description) g.add(at, AS('name'), $rdf.literal(a.description), doc);
105
+ }
106
+ await this.put(url, $rdf.serialize(doc, g, url, TURTLE));
107
+ }
108
+
109
+ // Contacts doc — the followers/following truth, rebuilt whole each change.
110
+ async writeContacts({ followers, following }) {
111
+ const url = this.fedi + 'contacts';
112
+ const doc = $rdf.sym(url);
113
+ const me = $rdf.sym(url + '#me');
114
+ const g = $rdf.graph();
115
+ g.add(me, RDF('type'), AS('Person'), doc);
116
+ for (const f of followers) g.add(me, AS('followers'), $rdf.sym(f.actor), doc);
117
+ for (const f of following) g.add(me, AS('following'), $rdf.sym(f.actor), doc);
118
+ await this.put(url, $rdf.serialize(doc, g, url, TURTLE));
119
+ }
120
+
121
+ // Settings doc — handle + pointer to the public face.
122
+ async writeSettings({ handle, actorUrl }) {
123
+ const url = this.fedi + 'settings';
124
+ const doc = $rdf.sym(url);
125
+ const me = $rdf.sym(url + '#me');
126
+ const g = $rdf.graph();
127
+ g.add(me, RDF('type'), AS('Person'), doc);
128
+ g.add(me, AS('preferredUsername'), $rdf.literal(handle), doc);
129
+ g.add(me, AS('url'), $rdf.sym(actorUrl), doc);
130
+ await this.put(url, $rdf.serialize(doc, g, url, TURTLE));
131
+ }
132
+ }
package/lib/ports.mjs ADDED
@@ -0,0 +1,27 @@
1
+ // ports.mjs — finding a port nothing else is on.
2
+ //
3
+ // This lived twice, in bin/fedipod.mjs and lib/admin.mjs, and the two
4
+ // copies had already drifted: one returned null when it ran out of ports and
5
+ // the other threw. Both spawn agents, so which one you got decided whether a
6
+ // full range produced an error message or a silent undefined.
7
+
8
+ import net from 'node:net';
9
+
10
+ // Bind to find out. Something holding a port without answering HTTP reads as
11
+ // free to a probe that only asks for a response, and the new agent then dies on
12
+ // EADDRINUSE with nobody watching.
13
+ export function portFree(port) {
14
+ return new Promise((resolve) => {
15
+ const probe = net.createServer();
16
+ probe.once('error', () => resolve(false));
17
+ probe.once('listening', () => probe.close(() => resolve(true)));
18
+ probe.listen(port, '127.0.0.1');
19
+ });
20
+ }
21
+
22
+ // null when the whole span is taken. Callers that want an error say so in their
23
+ // own words — the CLI has a person reading it, the admin API has a status code.
24
+ export async function freePortFrom(first, span = 50) {
25
+ for (let p = first; p < first + span; p++) if (await portFree(p)) return p;
26
+ return null;
27
+ }