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
package/lib/setup.mjs ADDED
@@ -0,0 +1,262 @@
1
+ // setup.mjs — the setup flow itself, lifted out of the CLI so a browser page
2
+ // can drive it. Same order, same decisions as `fedipod setup` always had:
3
+ // create the account and pod (or use one you already have), mint a revocable
4
+ // credential, provision the pod, publish the actor, check it is really public.
5
+ //
6
+ // The caller owns the run record and polls it; runSetup only fills it in. That
7
+ // is deliberate. The credential a CSS server mints is shown once and cannot be
8
+ // recovered, so the durable record of a run has to be the credential file on
9
+ // disk, never an HTTP response a closed tab can take with it.
10
+
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { pathToFileURL } from 'node:url';
14
+
15
+ import { createAccountWithPod as realCreateAccount } from './account.mjs';
16
+ import { mintCredential as realMint } from './remote.mjs';
17
+ import { hashPassword } from './mastoapi.mjs';
18
+ import { webfingerHost } from './wire.mjs';
19
+ import { rootOf, recordLastUsed, writeJsonAtomic } from './home.mjs';
20
+ import { insecureUrlReason } from './safefetch.mjs';
21
+ import { CURRENT_LAYOUT, isCurrent } from './migrate.mjs';
22
+
23
+ export const STEPS = ['account', 'credential', 'bootstrap', 'connect', 'publish', 'verify'];
24
+
25
+ export const credentialPath = (home) => path.join(home, 'credential.json');
26
+ export const hasCredential = (home) => fs.existsSync(credentialPath(home));
27
+
28
+ export function newRun() {
29
+ return {
30
+ phase: 'running',
31
+ startedAt: new Date().toISOString(),
32
+ steps: STEPS.map(key => ({ key, state: 'waiting', note: null })),
33
+ error: null,
34
+ result: null,
35
+ };
36
+ }
37
+
38
+ // What must be answered before a run can start. `resuming` is a run that
39
+ // already has its credential: the account and the pod are settled, and only
40
+ // the actor's own details are still open.
41
+ export function setupInputError(a, resuming = false) {
42
+ if (!a.handle) return 'a handle is required';
43
+ if (a.kind && a.kind !== 'person' && a.kind !== 'group') return 'kind must be person or group';
44
+ if (a.privateRoot) {
45
+ try { new URL(a.privateRoot); } catch { return `"${a.privateRoot}" is not a container address`; }
46
+ // A pod one, at least. A file: private root is a directory and has no
47
+ // transport to secure.
48
+ if (/^https?:/i.test(a.privateRoot)) {
49
+ const bad = insecureUrlReason(a.privateRoot, 'private-data address');
50
+ if (bad) return bad;
51
+ }
52
+ }
53
+ if (a.gateway) {
54
+ if (typeof a.gateway.url !== 'string') return 'a gateway needs its door URL';
55
+ const badGw = insecureUrlReason(a.gateway.url, 'gateway address');
56
+ if (badGw) return badGw;
57
+ }
58
+ if (resuming) return null;
59
+ if (a.mode !== 'new' && a.mode !== 'existing') return 'mode must be "new" or "existing"';
60
+ if (!a.issuer) return 'an identity provider is required';
61
+ // Checked BEFORE the password is asked for, let alone sent: the issuer is
62
+ // where it goes, and the pod is where the credential it buys is used.
63
+ const badIssuer = insecureUrlReason(a.issuer, 'identity provider address');
64
+ if (badIssuer) return badIssuer;
65
+ if (!a.email) return 'an account email is required';
66
+ if (!a.password) return 'the account password is required';
67
+ if (a.mode === 'existing' && !a.pod) return 'a pod address is required';
68
+ const badPod = insecureUrlReason(a.pod, 'pod address');
69
+ if (badPod) return badPod;
70
+ return null;
71
+ }
72
+
73
+ // What the CLI printed before asking "create pod and fediverse account?"
74
+ // (bin/fedipod.mjs, the address preview) — as data, so the page can show
75
+ // the same warnings. Pure: no network, so it can answer while you type.
76
+ export function preflight({ mode, pod, issuer, podName, handle, kind }) {
77
+ const warnings = [];
78
+ if (!handle) return { ok: false, error: 'a handle is required' };
79
+ let issuerHost;
80
+ try { issuerHost = new URL(issuer || 'https://solidcommunity.net').host; }
81
+ catch { return { ok: false, error: `"${issuer}" is not a URL` }; }
82
+
83
+ if (mode === 'new') {
84
+ // No warning about whether the server gives the pod its own subdomain. The
85
+ // run checks what it actually got and fails the account step when a group
86
+ // lands on a shared host, which is the case that matters; saying it up
87
+ // front only made the form noisy.
88
+ return {
89
+ ok: true, mode, handle, kind,
90
+ address: `@${handle}@${podName || handle}.${issuerHost}`,
91
+ webfingerHost: null, resolvable: null, warnings, refusal: null,
92
+ };
93
+ }
94
+
95
+ let podUrl;
96
+ try { podUrl = new URL(pod); }
97
+ catch { return { ok: false, error: `"${pod}" is not a pod address` }; }
98
+ // A handle resolves through <host>/.well-known/webfinger, so it only works
99
+ // when the pod owns the root of its host.
100
+ const wfHost = webfingerHost(podUrl.href);
101
+ let refusal = null;
102
+ if (!wfHost) {
103
+ warnings.push('pod-is-a-path');
104
+ // A person warned about an unresolvable handle is the one who suffers, so
105
+ // that is their call to accept. Nobody could ever find this group, and the
106
+ // people it would fail are not the operator reading the warning.
107
+ if (kind === 'group') refusal = 'group-needs-host-root';
108
+ }
109
+ return {
110
+ ok: !refusal, mode: 'existing', handle, kind,
111
+ address: `@${handle}@${wfHost || podUrl.host}`,
112
+ webfingerHost: wfHost, resolvable: !!wfHost, warnings, refusal,
113
+ };
114
+ }
115
+
116
+ // Fills `run` in as it goes. Never throws: a failure lands in run.error, so a
117
+ // page that polls sees it. `deps` is for tests — the real functions otherwise.
118
+ export async function runSetup({ home, agent, answers, run, deps = {}, log = () => {} }) {
119
+ const createAccount = deps.createAccountWithPod || realCreateAccount;
120
+ const mint = deps.mintCredential || realMint;
121
+
122
+ const at = (key) => run.steps.find(s => s.key === key);
123
+ const begin = (key) => { at(key).state = 'running'; };
124
+ const done = (key, note = null) => { at(key).state = 'ok'; at(key).note = note; };
125
+ const skip = (key, note = null) => { at(key).state = 'skipped'; at(key).note = note; };
126
+
127
+ const {
128
+ mode, issuer, email, password, handle, name, podName,
129
+ kind = 'person', approveJoins = false, summary, icon, keys, uiPassword,
130
+ gateway = null,
131
+ } = answers;
132
+ let { pod, root } = answers;
133
+ let accountWebId = null; // what createAccountWithPod reported, when it ran
134
+ // The private half always starts here, beside the credential and the keys —
135
+ // not on the pod. Every activity you receive would otherwise cost the pod
136
+ // several writes, and that pod is a server somebody runs. `state --to` moves
137
+ // it afterwards; starting on the pod and moving later is strictly worse,
138
+ // because the copy left behind was on the pod the whole time.
139
+ const privateRoot = answers.privateRoot
140
+ || pathToFileURL(path.join(home, 'private')).href + '/';
141
+ // Everything that leaves this function — notes, errors, the log — goes
142
+ // through here. Nothing today echoes a password back, but /setup/progress is
143
+ // polled repeatedly and would re-serve one forever if anything ever did.
144
+ const scrub = (s) => (password ? String(s).split(password).join('••••••') : String(s));
145
+
146
+ const credPath = credentialPath(home);
147
+ const resuming = fs.existsSync(credPath);
148
+
149
+ try {
150
+ // --- account: create it, or use the pod you already have ---
151
+ if (resuming) {
152
+ skip('account', 'the credential is already on disk');
153
+ } else if (mode === 'new') {
154
+ begin('account');
155
+ const made = await createAccount({ issuer, email, password, podName: podName || handle });
156
+ pod = made.pod;
157
+ accountWebId = made.webId || null;
158
+ log(`account + pod created: ${pod}`);
159
+ // The server put the pod on a path rather than its own subdomain, so
160
+ // nobody could ever find this group. A person was warned before we got
161
+ // here and chose to continue; a group cannot.
162
+ if (kind === 'group' && !webfingerHost(pod)) {
163
+ throw new Error(`${issuer} created the pod at ${pod} — a path on a shared host, `
164
+ + 'not a host root. WebFinger is only answered at a host root, so nobody '
165
+ + 'could find this group. The pod exists; no actor was published.');
166
+ }
167
+ done('account', pod);
168
+ } else {
169
+ skip('account', 'using the pod you already have');
170
+ }
171
+
172
+ // --- credential: the point of no return, and the durability boundary ---
173
+ if (resuming) {
174
+ const rec = JSON.parse(fs.readFileSync(credPath, 'utf8'));
175
+ pod = rec.remotePod;
176
+ root = rec.root;
177
+ skip('credential', `already minted — ${credPath}`);
178
+ } else {
179
+ begin('credential');
180
+ // The WebID matters as much as the pod: an account with more than one
181
+ // pod has more than one, and a credential bound to the wrong one 403s
182
+ // every write. Account creation knows which; otherwise the pod's origin
183
+ // picks it out.
184
+ const credential = await mint({
185
+ origin: issuer, email, password, name: 'fedipod',
186
+ webId: accountWebId || undefined, podUrl: pod,
187
+ });
188
+ const rec = {
189
+ ...credential,
190
+ remotePod: pod.endsWith('/') ? pod : pod + '/',
191
+ ...(root ? { root } : {}),
192
+ ...(keys === 'pod' ? { keysMode: 'pod' } : {}),
193
+ // Where the private half lives. Per-machine, like keysMode.
194
+ ...(privateRoot ? { privateRoot } : {}),
195
+ // What shape this install is, so `upgrade` can tell an old one from a
196
+ // new one without guessing from the fields.
197
+ ...(isCurrent({ privateRoot }) ? { layout: CURRENT_LAYOUT } : {}),
198
+ };
199
+ fs.mkdirSync(home, { recursive: true, mode: 0o700 });
200
+ writeJsonAtomic(credPath, rec);
201
+ // The identity you just made is the one you are using, so it becomes what
202
+ // a plain command means. Here rather than earlier: the credential landing
203
+ // is what makes this an identity at all.
204
+ // NOT `root` — that name is already taken by the answers' AP container
205
+ // root, destructured above, and shadowing it is a temporal dead zone.
206
+ const apHomeRoot = rootOf(home);
207
+ if (apHomeRoot !== home) recordLastUsed(apHomeRoot, path.basename(home));
208
+ pod = rec.remotePod;
209
+ log(`credential minted and saved to ${credPath}`);
210
+ done('credential', credPath);
211
+ }
212
+
213
+ // --- provision the pod and bring federation up ---
214
+ begin('bootstrap');
215
+ await agent.bootstrap({ handle, name: name || handle, root, kind, approveJoins, summary, icon, gateway });
216
+ done('bootstrap');
217
+
218
+ begin('connect');
219
+ // Direct, not connectWithRetry: setup wants to hear about a failure now,
220
+ // not back off for an hour with a browser tab waiting on it.
221
+ await agent.connect({ repair: false }); // publish below is the publish
222
+ done('connect');
223
+
224
+ begin('publish');
225
+ if (uiPassword) {
226
+ agent.store.setConfig({ ...agent.store.getConfig(), uiPassword: hashPassword(uiPassword) });
227
+ }
228
+ const published = await agent.publisher.publishProfile();
229
+ await agent.store.flush();
230
+ done('publish');
231
+
232
+ // --- and say plainly whether the world can actually see it ---
233
+ begin('verify');
234
+ const unreachable = published?.unreachable || [];
235
+ const wfHost = webfingerHost(pod);
236
+ done('verify', unreachable.length
237
+ ? `not readable without credentials: ${unreachable.join(', ')}`
238
+ : 'the public surface is reachable');
239
+
240
+ run.result = {
241
+ kind,
242
+ pod,
243
+ handle,
244
+ actor: agent.urls?.actor || null,
245
+ webfingerHost: wfHost,
246
+ resolvable: !!wfHost,
247
+ address: wfHost ? `@${handle}@${wfHost}` : null,
248
+ unreachable,
249
+ };
250
+ run.phase = 'done';
251
+ log(wfHost
252
+ ? `${kind === 'group' ? 'group' : 'actor'} published: @${handle}@${wfHost}`
253
+ : `${kind === 'group' ? 'group' : 'actor'} published, but not reachable as a handle — `
254
+ + `${pod} is not a host root`);
255
+ } catch (e) {
256
+ for (const s of run.steps) if (s.state === 'running') s.state = 'error';
257
+ run.error = scrub(e.message);
258
+ run.phase = 'error';
259
+ log(`setup failed: ${run.error}`);
260
+ }
261
+ return run;
262
+ }