fedipod-server 0.21.0 → 0.28.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 (332) hide show
  1. package/README.md +5 -4
  2. package/dist/claims.d.ts +3 -1
  3. package/dist/claims.js +20 -7
  4. package/dist/components/context.jsonld +6 -0
  5. package/dist/handler.d.ts +29 -1
  6. package/dist/handler.js +89 -4
  7. package/dist/handler.jsonld +36 -1
  8. package/lib/client/c2s.mjs +100 -3
  9. package/lib/client/masto/accounts.mjs +44 -4
  10. package/lib/client/masto/body.mjs +90 -3
  11. package/lib/client/masto/index.mjs +5 -1
  12. package/lib/client/masto/instance.mjs +11 -3
  13. package/lib/client/masto/media.mjs +3 -57
  14. package/lib/client/masto/oauth.mjs +66 -9
  15. package/lib/client/masto/render.mjs +62 -5
  16. package/lib/client/masto/statuses.mjs +25 -1
  17. package/lib/client/masto/timelines.mjs +128 -6
  18. package/lib/connections/tagfeed.mjs +42 -5
  19. package/lib/core/as2.mjs +36 -5
  20. package/lib/core/contexts/index.mjs +14 -0
  21. package/lib/core/contexts/litepub-0.1.json +60 -0
  22. package/lib/core/contexts/map.json +3 -1
  23. package/lib/core/contexts/quotes.json +18 -0
  24. package/lib/core/graphview.mjs +41 -3
  25. package/lib/core/intake/activities.mjs +136 -2
  26. package/lib/core/intake/activity.mjs +79 -1
  27. package/lib/core/intake/group.mjs +37 -3
  28. package/lib/core/intake/index.mjs +32 -2
  29. package/lib/core/intake/notes.mjs +60 -3
  30. package/lib/core/lease.mjs +12 -2
  31. package/lib/core/profile-page.mjs +104 -0
  32. package/lib/core/publisher/collections.mjs +6 -1
  33. package/lib/core/publisher/index.mjs +35 -17
  34. package/lib/core/publisher/notes.mjs +104 -3
  35. package/lib/core/shapes/activitystreams.ttl +28 -0
  36. package/lib/core/shapes/index.mjs +26 -1
  37. package/lib/core/wire-quotes.mjs +71 -0
  38. package/lib/core/wire.mjs +59 -103
  39. package/lib/device/admin/routes/lifecycle.mjs +9 -0
  40. package/lib/device/admin/static.mjs +4 -1
  41. package/lib/device/admin/surface.mjs +26 -2
  42. package/lib/device/certs.mjs +18 -2
  43. package/lib/device/cli/commands/setup.mjs +6 -2
  44. package/lib/device/cli/commands/state.mjs +3 -1
  45. package/lib/device/gateway-move.mjs +58 -0
  46. package/lib/device/migrate.mjs +3 -1
  47. package/lib/device/setup.mjs +67 -7
  48. package/lib/gateway/front-core.mjs +307 -36
  49. package/lib/gateway/gateway-core.mjs +15 -6
  50. package/lib/gateway/notices.mjs +84 -0
  51. package/lib/gateway/quiet.mjs +184 -0
  52. package/lib/pod/root.mjs +6 -3
  53. package/lib/pod/state.mjs +8 -7
  54. package/lib/pod/transport.mjs +5 -1
  55. package/lib/session/README.md +123 -0
  56. package/lib/session/demo.html +94 -0
  57. package/lib/session/fedi-account.mjs +346 -0
  58. package/lib/session/fedi-login.mjs +123 -0
  59. package/lib/session/oidc-session.mjs +233 -0
  60. package/lib/session/package.json +41 -0
  61. package/package.json +1 -1
  62. package/run-agent.mjs +28 -0
  63. package/vendor/gate.cjs +6 -3
  64. package/web/admin/bar.css +66 -0
  65. package/web/admin/bar.js +38 -0
  66. package/web/admin/client/index.html +31 -3
  67. package/web/admin/client-pick.js +79 -0
  68. package/web/admin/gateway.js +22 -0
  69. package/web/admin/index.html +54 -13
  70. package/web/admin/notices-bar.js +90 -0
  71. package/web/admin/oauth-signin.mjs +37 -0
  72. package/web/admin/record.js +13 -1
  73. package/web/admin/setup/index.html +31 -3
  74. package/web/admin/tokens.css +15 -3
  75. package/web/admin/upkeep.js +4 -5
  76. package/web/app/README.md +14 -10
  77. package/web/app/admin-facade.mjs +51 -12
  78. package/web/app/agent.mjs +157 -19
  79. package/web/app/authorize_interaction.html +56 -0
  80. package/web/app/authorize_interaction.js +69 -0
  81. package/web/app/boot.mjs +233 -95
  82. package/web/app/dist/boot.js +477 -569
  83. package/web/app/dist/boot.js.map +4 -4
  84. package/web/app/dist/sw.js +1861 -532
  85. package/web/app/dist/sw.js.map +4 -4
  86. package/web/app/gateway-move.mjs +60 -0
  87. package/web/app/index.html +40 -49
  88. package/web/app/keys-browser.mjs +12 -15
  89. package/web/app/keystore.mjs +19 -36
  90. package/web/app/oidc-session.mjs +5 -188
  91. package/web/app/signup.mjs +172 -132
  92. package/web/app/sw-src.mjs +1 -1
  93. package/web/app/update.js +66 -0
  94. package/web/front/admin.html +5 -3
  95. package/web/front/new-account.html +2 -2
  96. package/web/front/notices.html +69 -0
  97. package/web/front/notices.js +107 -0
  98. package/web/front/run.html +15 -8
  99. package/web/front/run.js +124 -36
  100. package/web/icons/forum.svg +11 -0
  101. package/web/icons/gateway.svg +14 -0
  102. package/web/icons/local.svg +11 -0
  103. package/web/app/index.html~ +0 -152
  104. package/web/app/site/_headers +0 -9
  105. package/web/app/site/_redirects +0 -22
  106. package/web/app/site/admin/actors.js +0 -145
  107. package/web/app/site/admin/bar.css +0 -60
  108. package/web/app/site/admin/bar.js +0 -61
  109. package/web/app/site/admin/client/client.js +0 -66
  110. package/web/app/site/admin/client/index.html +0 -43
  111. package/web/app/site/admin/common.js +0 -23
  112. package/web/app/site/admin/connections.js +0 -112
  113. package/web/app/site/admin/gateway.js +0 -111
  114. package/web/app/site/admin/group.js +0 -258
  115. package/web/app/site/admin/index.html +0 -564
  116. package/web/app/site/admin/record.js +0 -378
  117. package/web/app/site/admin/setup/index.html +0 -215
  118. package/web/app/site/admin/setup/setup.js +0 -326
  119. package/web/app/site/admin/tokens.css +0 -56
  120. package/web/app/site/admin/upkeep.js +0 -178
  121. package/web/app/site/admin/window.css +0 -55
  122. package/web/app/site/admin/window.js +0 -136
  123. package/web/app/site/app/404.html +0 -40
  124. package/web/app/site/app/_headers +0 -2
  125. package/web/app/site/app/apple-touch-icon.png +0 -0
  126. package/web/app/site/app/assets/ICONS-DIPJeU0C.js +0 -2
  127. package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +0 -1
  128. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +0 -183
  129. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
  130. package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
  131. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +0 -2
  132. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +0 -1
  133. package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  134. package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
  135. package/web/app/site/app/assets/chunk-aKtaBQYM.js +0 -1
  136. package/web/app/site/app/assets/compose-CFEx7wb4.js +0 -2
  137. package/web/app/site/app/assets/compose-CFEx7wb4.js.map +0 -1
  138. package/web/app/site/app/assets/compose-ZJN5xSb4.js +0 -35
  139. package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +0 -1
  140. package/web/app/site/app/assets/debug.module-YFQgwnIr.js +0 -52
  141. package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +0 -1
  142. package/web/app/site/app/assets/dom-p1YcJ5Rw.js +0 -2
  143. package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +0 -1
  144. package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  145. package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  146. package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  147. package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  148. package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +0 -1
  149. package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +0 -1
  150. package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
  151. package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +0 -1
  152. package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
  153. package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
  154. package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
  155. package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
  156. package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
  157. package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
  158. package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
  159. package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
  160. package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +0 -1
  161. package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +0 -1
  162. package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
  163. package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +0 -1
  164. package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
  165. package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +0 -1
  166. package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
  167. package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
  168. package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +0 -1
  169. package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
  170. package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +0 -1
  171. package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
  172. package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
  173. package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
  174. package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
  175. package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
  176. package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +0 -1
  177. package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +0 -1
  178. package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +0 -1
  179. package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +0 -1
  180. package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +0 -1
  181. package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
  182. package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +0 -1
  183. package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
  184. package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +0 -1
  185. package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +0 -1
  186. package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +0 -1
  187. package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
  188. package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
  189. package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +0 -1
  190. package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +0 -1
  191. package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
  192. package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
  193. package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +0 -1
  194. package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +0 -1
  195. package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +0 -1
  196. package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
  197. package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +0 -1
  198. package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +0 -1
  199. package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +0 -1
  200. package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
  201. package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
  202. package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
  203. package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +0 -1
  204. package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +0 -1
  205. package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +0 -1
  206. package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +0 -1
  207. package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +0 -1
  208. package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
  209. package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
  210. package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +0 -1
  211. package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +0 -1
  212. package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +0 -1
  213. package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +0 -1
  214. package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +0 -1
  215. package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +0 -1
  216. package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +0 -1
  217. package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +0 -1
  218. package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
  219. package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +0 -1
  220. package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +0 -1
  221. package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +0 -1
  222. package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
  223. package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +0 -1
  224. package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +0 -1
  225. package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
  226. package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
  227. package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
  228. package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +0 -1
  229. package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +0 -1
  230. package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
  231. package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +0 -1
  232. package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +0 -1
  233. package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +0 -1
  234. package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +0 -1
  235. package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +0 -1
  236. package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +0 -1
  237. package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +0 -1
  238. package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +0 -1
  239. package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +0 -1
  240. package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
  241. package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +0 -1
  242. package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
  243. package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
  244. package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
  245. package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +0 -1
  246. package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
  247. package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +0 -1
  248. package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +0 -1
  249. package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
  250. package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +0 -1
  251. package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
  252. package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +0 -1
  253. package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
  254. package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +0 -1
  255. package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +0 -1
  256. package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
  257. package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +0 -1
  258. package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +0 -1
  259. package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
  260. package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +0 -1
  261. package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +0 -1
  262. package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +0 -1
  263. package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
  264. package/web/app/site/app/assets/instances-BNPptnn-.json +0 -333
  265. package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +0 -1
  266. package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +0 -1
  267. package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
  268. package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +0 -1
  269. package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +0 -1
  270. package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +0 -1
  271. package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +0 -1
  272. package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +0 -1
  273. package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +0 -1
  274. package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +0 -1
  275. package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +0 -1
  276. package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +0 -1
  277. package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +0 -1
  278. package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +0 -1
  279. package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +0 -1
  280. package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +0 -1
  281. package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +0 -1
  282. package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
  283. package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +0 -1
  284. package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
  285. package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +0 -1
  286. package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +0 -1
  287. package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
  288. package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +0 -1
  289. package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +0 -1
  290. package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
  291. package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +0 -1
  292. package/web/app/site/app/assets/locales/tok-CfheRbCu.js +0 -1
  293. package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +0 -1
  294. package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +0 -1
  295. package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +0 -1
  296. package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +0 -1
  297. package/web/app/site/app/assets/main-BdqNbG-a.js +0 -26
  298. package/web/app/site/app/assets/main-BdqNbG-a.js.map +0 -1
  299. package/web/app/site/app/assets/mock-home-DGhR__He.js +0 -2
  300. package/web/app/site/app/assets/mock-home-DGhR__He.js.map +0 -1
  301. package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
  302. package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  303. package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  304. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +0 -2
  305. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +0 -1
  306. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +0 -20
  307. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +0 -1
  308. package/web/app/site/app/assets/style-B1iLPTPR.css +0 -2
  309. package/web/app/site/app/assets/temml-DuyIQMei.js +0 -97
  310. package/web/app/site/app/assets/temml-DuyIQMei.js.map +0 -1
  311. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +0 -2
  312. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
  313. package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
  314. package/web/app/site/app/assets/year-in-posts-DJv_star.js +0 -3
  315. package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +0 -1
  316. package/web/app/site/app/compose/index.html +0 -20
  317. package/web/app/site/app/favicon.ico +0 -0
  318. package/web/app/site/app/index.html +0 -73
  319. package/web/app/site/app/logo-192.png +0 -0
  320. package/web/app/site/app/logo-512.png +0 -0
  321. package/web/app/site/app/logo-badge-72.png +0 -0
  322. package/web/app/site/app/logo-maskable-512.png +0 -0
  323. package/web/app/site/app/logo-monochrome-512.png +0 -0
  324. package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
  325. package/web/app/site/app/manifest.webmanifest +0 -1
  326. package/web/app/site/app/og-image-2.jpg +0 -0
  327. package/web/app/site/app/og-image.png +0 -0
  328. package/web/app/site/app/robots.txt +0 -2
  329. package/web/app/site/app/version.json +0 -1
  330. package/web/app/site/boot.js +0 -34277
  331. package/web/app/site/index.html +0 -251
  332. package/web/app/site/sw.js +0 -71331
package/lib/core/wire.mjs CHANGED
@@ -3,6 +3,10 @@
3
3
  // protocol documents, not our RDF.
4
4
 
5
5
  export const AS_CTX = 'https://www.w3.org/ns/activitystreams';
6
+ // Lemmy's own context, held locally (lib/core/contexts/join-lemmy.json). It
7
+ // names the three terms a Lemmy reader looks for on a community and its
8
+ // posts: postingRestrictedToMods, stickied, commentsEnabled.
9
+ export const LEMMY_CTX = 'https://join-lemmy.org/context.json';
6
10
  export const SEC_CTX = 'https://w3id.org/security/v1';
7
11
  export const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
8
12
 
@@ -80,7 +84,8 @@ export const assertionKeyId = (urls) => urls.actor + '#ed25519-key';
80
84
  export function actorDoc({ urls, handle, name, publicKeyPem, assertionKey = null, movedTo = null, kind = 'person',
81
85
  approveJoins = false, summary = null, icon = null, image = null, fields = [],
82
86
  webId = null, aliases = [], moderators = null, pendingFollowers = null, pendingFollowing = null,
83
- blocked = null, inbox = null, outbox = null, oauthAuthorize = null, oauthToken = null }) {
87
+ blocked = null, inbox = null, outbox = null, oauthAuthorize = null, oauthToken = null,
88
+ postingRestrictedToMods = null }) {
84
89
  // manuallyApprovesFollowers is NOT in the base AS2 context, so it is declared
85
90
  // inline exactly as Mastodon declares it — and only when we actually use it.
86
91
  // It is what makes a client show "Request to follow" rather than "Follow" and
@@ -118,13 +123,18 @@ export function actorDoc({ urls, handle, name, publicKeyPem, assertionKey = null
118
123
  // old accounts elsewhere whose servers check for their own URL here before
119
124
  // they will send a Move. Declared inline exactly as Mastodon declares it.
120
125
  if (webId || aliases.length) context.push({ alsoKnownAs: { '@id': 'as:alsoKnownAs', '@type': '@id' } });
126
+ // A community says whether only its moderators may open posts in it, in
127
+ // Lemmy's term, declared through Lemmy's context. Groups only.
128
+ if (kind === 'group' && postingRestrictedToMods !== null) context.push(LEMMY_CTX);
121
129
  if (fields.length) {
122
130
  context.push({ schema: 'http://schema.org#', PropertyValue: 'schema:PropertyValue', value: 'schema:value' });
123
131
  }
124
132
  return {
125
133
  '@context': context,
126
134
  id: urls.actor,
127
- type: kind === 'group' ? 'Group' : 'Person',
135
+ // A group is a community; an application is a service that speaks for a
136
+ // site rather than a person — the actor a forum publishes for itself.
137
+ type: kind === 'group' ? 'Group' : kind === 'application' ? 'Application' : 'Person',
128
138
  ...(approveJoins ? { manuallyApprovesFollowers: true } : {}),
129
139
  ...(assertionKey ? { assertionMethod: [{
130
140
  id: assertionKeyId(urls), type: 'Multikey', controller: urls.actor,
@@ -132,6 +142,7 @@ export function actorDoc({ urls, handle, name, publicKeyPem, assertionKey = null
132
142
  }] } : {}),
133
143
  ...(movedTo ? { movedTo } : {}),
134
144
  ...(webId || aliases.length ? { alsoKnownAs: [...(webId ? [webId] : []), ...aliases] } : {}),
145
+ ...(kind === 'group' && postingRestrictedToMods !== null ? { postingRestrictedToMods: !!postingRestrictedToMods } : {}),
135
146
  preferredUsername: handle,
136
147
  name: name || handle,
137
148
  // The bio, and the avatar. For a group, `summary` is where what the group
@@ -196,102 +207,7 @@ export function actorDoc({ urls, handle, name, publicKeyPem, assertionKey = null
196
207
  // the actor document. It says who this is and how to follow — the handle to
197
208
  // paste, and the remote-follow control that hands the visitor to their own
198
209
  // server's follow screen.
199
- // `fields` are the profile's name/value rows, `joined` an ISO date, `pinned`
200
- // the pinned posts as { content (sanitized HTML), published, url }.
201
- export function profilePageHtml({ name, address, summary = null, icon = null, image = null,
202
- fields = [], joined = null, pinned = [], kind = 'person' }) {
203
- const esc = (s) => String(s ?? '').replace(/[&<>"]/g, c => ({ ...HTML_ESCAPES, '"': '&quot;' }[c]));
204
- const what = kind === 'group' ? 'a group' : 'an account';
205
- const month = (iso) => {
206
- const d = new Date(iso);
207
- return Number.isNaN(d.getTime()) ? null
208
- : d.toLocaleDateString('en', { month: 'long', year: 'numeric', timeZone: 'UTC' });
209
- };
210
- const day = (iso) => {
211
- const d = new Date(iso);
212
- return Number.isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 10);
213
- };
214
- const fieldRows = fields.filter(f => f?.name && f?.value !== undefined && f?.value !== null && String(f.value) !== '');
215
- const joinedText = joined ? month(joined) : null;
216
- const pins = pinned.filter(x => x && typeof x.content === 'string');
217
- return `<!doctype html>
218
- <html lang="en">
219
- <head>
220
- <meta charset="utf-8">
221
- <meta name="viewport" content="width=device-width, initial-scale=1">
222
- <title>${esc(name)} (${esc(address)})</title>
223
- <style>
224
- :root { color-scheme: light dark; }
225
- body { font: 112.5%/1.5 system-ui, sans-serif; max-width: 34rem; margin: 3rem auto; padding: 0 1rem; }
226
- img.header { width: 100%; max-height: 12rem; object-fit: cover; border-radius: 1rem; }
227
- img.avatar { width: 6rem; height: 6rem; border-radius: 1rem; object-fit: cover; margin-top: 1rem; }
228
- h1 { margin: .5rem 0 0; }
229
- .address { font-size: 1.1rem; user-select: all; }
230
- dl.fields { display: grid; grid-template-columns: max-content 1fr; gap: .4rem 1rem; margin: 1.5rem 0; }
231
- dl.fields dt { font-weight: 600; }
232
- dl.fields dd { margin: 0; overflow-wrap: anywhere; }
233
- .joined { margin: 1rem 0; }
234
- section.pinned h2 { font-size: 1.1rem; margin: 2rem 0 .5rem; }
235
- article.pin { border: 1px solid #8884; border-radius: 1rem; padding: 1rem 1.2rem; margin: 1rem 0; }
236
- article.pin p:last-child { margin-bottom: 0; }
237
- article.pin .when { font-size: 1rem; }
238
- form { margin-top: 2rem; }
239
- label { display: block; margin-bottom: .3rem; }
240
- input { font: inherit; padding: .5rem; width: 14rem; max-width: 100%; }
241
- button { font: inherit; padding: .5rem 1rem; }
242
- .hint { color: #666; font-size: 1rem; }
243
- .err { color: #b00020; }
244
- .err:empty { display: none; }
245
- @media (prefers-color-scheme: dark) {
246
- .hint { color: #aaa; }
247
- .err { color: #ff8a80; }
248
- }
249
- </style>
250
- </head>
251
- <body>
252
- <main>
253
- ${image ? `<img class="header" src="${esc(image)}" alt="">` : ''}
254
- ${icon ? `<img class="avatar" src="${esc(icon)}" alt="">` : ''}
255
- <h1>${esc(name)}</h1>
256
- <p class="address">${esc(address)}</p>
257
- ${summary ? `<div>${summary}</div>` : ''}
258
- ${fieldRows.length ? `<dl class="fields">
259
- ${fieldRows.map(f => `<dt>${esc(f.name)}</dt><dd>${esc(f.value)}</dd>`).join('\n')}
260
- </dl>` : ''}
261
- ${joinedText ? `<p class="joined">Joined ${esc(joinedText)}</p>` : ''}
262
- ${pins.length ? `<section class="pinned">
263
- <h2>Pinned</h2>
264
- ${pins.map(x => `<article class="pin">
265
- ${x.content}
266
- <p class="when">${x.url ? `<a href="${esc(x.url)}">${esc(day(x.published))}</a>` : esc(day(x.published))}</p>
267
- </article>`).join('\n')}
268
- </section>` : ''}
269
- <p>This is ${what} on the Fediverse. To follow it, paste the address above
270
- into the search box of Mastodon or any Fediverse app — or use the form.</p>
271
- <form id="follow">
272
- <label for="server">your server</label>
273
- <input id="server" type="text" placeholder="mastodon.social" autocomplete="off"
274
- required aria-describedby="follow-err">
275
- <button type="submit">Follow</button>
276
- <p class="err" id="follow-err" role="alert"></p>
277
- </form>
278
- <p class="hint">The form sends you to your own server's follow screen.</p>
279
- </main>
280
- <script>
281
- document.getElementById('follow').addEventListener('submit', (ev) => {
282
- ev.preventDefault();
283
- const s = document.getElementById('server').value.trim().replace(/^https?:\\/\\//, '').replace(/\\/.*$/, '');
284
- const err = document.getElementById('follow-err');
285
- const addr = document.querySelector('.address').textContent.trim();
286
- if (!s) { err.textContent = 'Enter your server, e.g. mastodon.social'; return; }
287
- err.textContent = '';
288
- location.href = 'https://' + s + '/authorize_interaction?uri=' + encodeURIComponent(addr);
289
- });
290
- </script>
291
- </body>
292
- </html>
293
- `;
294
- }
210
+ export { profilePageHtml } from './profile-page.mjs';
295
211
 
296
212
  // Retirement. An abandoned pod keeps accepting deliveries into a container
297
213
  // nobody will ever drain, and no remote server has any way to learn that. A
@@ -692,18 +608,47 @@ export function addressing(urls, visibility, who = []) {
692
608
  }[visibility] || { to: [PUBLIC], cc: [urls.followers, ...who] };
693
609
  }
694
610
 
611
+ // The quote terms, declared exactly as Mastodon declares them on a post:
612
+ // FEP-044f's `quote` and `quoteAuthorization`, and the two older spellings
613
+ // (Fedibird's quoteUri, Misskey's _misskey_quote) that older servers read.
614
+ export const QUOTE_CTX = {
615
+ quote: { '@id': 'https://w3id.org/fep/044f#quote', '@type': '@id' },
616
+ quoteUri: { '@id': 'http://fedibird.com/ns#quoteUri', '@type': '@id' },
617
+ _misskey_quote: { '@id': 'https://misskey-hub.net/ns#_misskey_quote', '@type': '@id' },
618
+ quoteAuthorization: { '@id': 'https://w3id.org/fep/044f#quoteAuthorization', '@type': '@id' },
619
+ };
620
+ // The interaction policy terms, GoToSocial's, declared as Mastodon declares them.
621
+ export const POLICY_CTX = {
622
+ gts: 'https://gotosocial.org/ns#',
623
+ interactionPolicy: { '@id': 'gts:interactionPolicy', '@type': '@id' },
624
+ canQuote: { '@id': 'gts:canQuote', '@type': '@id' },
625
+ automaticApproval: { '@id': 'gts:automaticApproval', '@type': '@id' },
626
+ manualApproval: { '@id': 'gts:manualApproval', '@type': '@id' },
627
+ };
628
+
629
+ // Who may quote a post: anyone, for a post the world can read; nobody, for a
630
+ // followers-only or direct one. Said on every note, because a server that
631
+ // finds no policy offers no quote button.
632
+ export function quotePolicy(visibility) {
633
+ const open = visibility === 'public' || visibility === 'unlisted';
634
+ return { canQuote: { automaticApproval: open ? [PUBLIC] : [], manualApproval: [] } };
635
+ }
636
+
695
637
  // `also`: actors addressed without being mentioned — the parent's author on
696
638
  // a reply. In cc but not in tag, so their server receives the post and does
697
639
  // not notify them. A direct post names its audience in the text alone.
640
+ // `quote`: the post this one quotes, `{ id, authorization }` — the
641
+ // authorization is the quoted author's QuoteAuthorization, once granted.
698
642
  export function noteDoc({ urls, slug, content, published, inReplyTo, attachments, mentions = [],
699
- visibility = 'public', summary = null, sensitive = false, updated = null, container = null, also = [] }) {
643
+ visibility = 'public', summary = null, sensitive = false, updated = null, container = null, also = [],
644
+ quote = null }) {
700
645
  const id = (container || urls.notes) + slug;
701
646
  const who = mentions.map(m => m.actor);
702
647
  const extra = visibility === 'direct' ? [] : also.filter(a => a && !who.includes(a));
703
648
  const addressed = addressing(urls, visibility, [...who, ...extra]);
704
649
  const hashtags = hashtagsIn(content).map(name => ({ name, href: tagHref(urls, name) }));
705
650
  const note = {
706
- '@context': AS_CTX,
651
+ '@context': [AS_CTX, { ...POLICY_CTX, ...(quote?.id ? QUOTE_CTX : {}) }],
707
652
  id, type: 'Note',
708
653
  attributedTo: urls.actor,
709
654
  content: contentHtml(content, mentions, hashtags),
@@ -711,7 +656,15 @@ export function noteDoc({ urls, slug, content, published, inReplyTo, attachments
711
656
  to: addressed.to,
712
657
  cc: addressed.cc,
713
658
  replies: repliesId(id),
659
+ interactionPolicy: quotePolicy(visibility),
714
660
  };
661
+ if (quote?.id) {
662
+ // All three spellings name the same post, as Mastodon writes them.
663
+ note.quote = quote.id;
664
+ note.quoteUri = quote.id;
665
+ note._misskey_quote = quote.id;
666
+ if (quote.authorization) note.quoteAuthorization = quote.authorization;
667
+ }
715
668
  if (summary) note.summary = summary; // the content warning
716
669
  // A content warning is what a receiver hides behind; media marked
717
670
  // sensitive without one is hidden the same way.
@@ -759,7 +712,7 @@ export function questionDoc({ options = [], multiple = false, endTime = null,
759
712
  if (endTime) question.endTime = endTime;
760
713
  if (closed) question.closed = closed;
761
714
  if (votersCount !== null && votersCount !== undefined) {
762
- question['@context'] = [AS_CTX, {
715
+ question['@context'] = [...[].concat(note['@context']), {
763
716
  toot: 'http://joinmastodon.org/ns#',
764
717
  votersCount: {
765
718
  '@id': 'toot:votersCount',
@@ -875,10 +828,13 @@ export function updateActorActivity({ urls, actor, serial, published = new Date(
875
828
  // An edit: same Note id, `updated` stamped, so receivers replace their copy
876
829
  // and show it as edited. The activity id carries the stamp — each edit is a
877
830
  // new activity, and a replay of an old one is already seen.
878
- export function updateActivity(note, urls) {
831
+ // `serial` names the activity when the note is not being edited — a quote
832
+ // authorization arriving on it changes what the note says about its quote,
833
+ // not what it says.
834
+ export function updateActivity(note, urls, { serial = null } = {}) {
879
835
  return {
880
836
  '@context': AS_CTX,
881
- id: note.id + '#update-' + String(note.updated || '').replace(/[^0-9TZ]/g, ''),
837
+ id: note.id + '#update-' + (note.updated ? String(note.updated).replace(/[^0-9TZ]/g, '') : 'q' + (serial ?? Date.now())),
882
838
  type: 'Update',
883
839
  actor: urls.actor,
884
840
  to: note.to, cc: note.cc,
@@ -14,6 +14,15 @@ export async function post(p, body, ctx, req, res) { // eslint-disable-line no
14
14
  // lease exists to keep to a single agent. A viewer must claim it first
15
15
  // and give up if it cannot — the facade has refused viewer writes since
16
16
  // multi-device landed, and these two were simply missed.
17
+ // The account back in this agent's hands. A viewer publishes nothing and
18
+ // drains nothing, so a lease left behind by something that is gone leaves
19
+ // the account silent with nothing in the interface to say so.
20
+ case '/takeover': {
21
+ if (!await agent.requestTakeover?.()) {
22
+ return json(res, 503, { error: 'the lease could not be taken — the pod refused the write' });
23
+ }
24
+ return json(res, 200, { ok: true, mode: agent.status().mode });
25
+ }
17
26
  case '/drain': {
18
27
  if (!await agent.requestTakeover?.()) {
19
28
  return json(res, 503, { error: 'another agent is active for this pod — it is doing the draining' });
@@ -52,7 +52,10 @@ const UI_DIR = path.join(projectRoot, 'ui'); // extra client dists: ui/<na
52
52
  // One surface, /admin/, with setup as its first section: /admin/setup/ is the
53
53
  // first run, /admin/ is the record, and there is room for the rest.
54
54
  const WEB_DIR = path.join(projectRoot, 'web');
55
- const WEB_MOUNTS = ['admin'];
55
+ // `icons` alongside `admin`: the favicons the agent's own pages name. One
56
+ // file each, no authority behind them, and the same jail as everything else
57
+ // under web/.
58
+ const WEB_MOUNTS = ['admin', 'icons'];
56
59
  export const webMount = (pathname) => {
57
60
  const seg = decodeURIComponent(pathname).replace(/^\/+/, '').split('/')[0];
58
61
  return WEB_MOUNTS.includes(seg) ? seg : null;
@@ -6,6 +6,8 @@
6
6
  // served off disk, and the dispatch to the routes modules beside it. Each of
7
7
  // those exports get() and post() over the same ctx.
8
8
 
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
9
11
  import { MastoApi } from '../../client/masto/index.mjs';
10
12
  import { C2S } from '../../client/c2s.mjs';
11
13
  import { makeC2sAuth } from '../../client/oidc-auth.mjs';
@@ -75,15 +77,27 @@ const ROUTES = [owner, setup, lifecycle, gateway, social, connections];
75
77
  // meaning inside a server that is not one — and moves the gate: a pod that is
76
78
  // a fediverse instance must let strangers reach /api and /oauth, so the gate
77
79
  // guards the operator's door (basePath) instead of the whole surface.
80
+ const SESSION_FILES = {
81
+ '/oauth/session/signin.mjs': 'web/admin/oauth-signin.mjs',
82
+ '/oauth/session/fedi-login.mjs': 'lib/session/fedi-login.mjs',
83
+ '/oauth/session/oidc-session.mjs': 'lib/session/oidc-session.mjs',
84
+ };
85
+
78
86
  export function buildAdminSurface({ agent, gate, allowed, log = console.log,
79
87
  port = null, handle = null, embedded = false, basePath = '/', mount = '',
80
88
  publicOrigin = null, scheme = null,
81
89
  versionOnDisk = () => localVersion(projectRoot) }) {
82
90
  const json = (res, status, obj) => sendJson(res, status, obj, allowed);
83
- const masto = new MastoApi({ agent, log, allowed, scheme, embedded, mount });
91
+ const masto = new MastoApi({ agent, log, allowed, scheme, embedded, mount,
92
+ throughDoor: (req) => Boolean(gate?.upgradeOk?.(req)) });
93
+ // A client's sign-in can be proved with the owner's pod sign-in instead of a
94
+ // password: the same verification the post door uses, and the owner it names.
95
+ masto.podAuth = makeC2sAuth({ agent, log, scheme, mount });
96
+ masto.ownerWebId = () => agent.remote?.webId || null;
84
97
  // The spec's own write API (§6), beside the facade. Its bearer fallback is
85
98
  // the facade's token, so the two surfaces share one notion of the operator.
86
- const c2s = new C2S({ agent, log, auth: makeC2sAuth({ agent, masto, log, scheme, mount }) });
99
+ const c2s = new C2S({ agent, log, scheme, mount,
100
+ auth: makeC2sAuth({ agent, masto, log, scheme, mount }) });
87
101
  const streaming = new Streaming({ masto, log, allowed, gate, gateOptional: embedded });
88
102
  // Asked per request, not once here: startAdmin runs before connect, so the
89
103
  // kind is not known yet at mount time.
@@ -171,6 +185,16 @@ export function buildAdminSurface({ agent, gate, allowed, log = console.log,
171
185
  if (p === '/ap/outbox' || p === '/ap/actor' || p === '/ap/inbox') {
172
186
  if (await c2s.handle(req, res, p, url)) return;
173
187
  }
188
+ // The sign-in page's script and the library it signs in with, outside
189
+ // the door like the sign-in itself. A fixed list; nothing in the request
190
+ // names a file.
191
+ const sessionFile = SESSION_FILES[p];
192
+ if (sessionFile) {
193
+ let body;
194
+ try { body = fs.readFileSync(path.join(projectRoot, sessionFile)); } catch { return json(res, 404, { error: 'not here' }); }
195
+ res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-cache' });
196
+ res.end(body); return;
197
+ }
174
198
  // Where a client looks first to find out how to sign in (RFC 8414), and
175
199
  // in front of the door for the same reason C2S is: a client that has to
176
200
  // be handed a secret before it can ask how to sign in cannot set itself
@@ -71,7 +71,16 @@ function nameConstraintsExtension() {
71
71
  return { id: '2.5.29.30', critical: true, value: der.getBytes() };
72
72
  }
73
73
 
74
- function makeCert({ subjectCN, issuer = null, issuerKeyPem = null, isCA = false, days, extraDns = [] }) {
74
+ // The issuer's own key identifier, for the certificate it signs to name. A
75
+ // verifier that finds several CAs under one name — this machine's browser
76
+ // store has held dozens of "FediPod Local CA" from scratch agents — picks by
77
+ // this, and without it picks by name, tries the wrong key and fails.
78
+ const issuerKeyId = (issuerCertPem) => {
79
+ const ext = forge.pki.certificateFromPem(issuerCertPem).getExtension('subjectKeyIdentifier');
80
+ return ext?.subjectKeyIdentifier ? forge.util.hexToBytes(ext.subjectKeyIdentifier) : null;
81
+ };
82
+
83
+ function makeCert({ subjectCN, issuer = null, issuerKeyPem = null, issuerCertPem = null, isCA = false, days, extraDns = [] }) {
75
84
  const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 });
76
85
  const cert = forge.pki.createCertificate();
77
86
  cert.publicKey = keys.publicKey;
@@ -89,6 +98,9 @@ function makeCert({ subjectCN, issuer = null, issuerKeyPem = null, isCA = false,
89
98
  : [{ name: 'basicConstraints', cA: false },
90
99
  { name: 'keyUsage', digitalSignature: true, keyEncipherment: true },
91
100
  { name: 'extKeyUsage', serverAuth: true },
101
+ { name: 'subjectKeyIdentifier' },
102
+ ...(issuerCertPem && issuerKeyId(issuerCertPem)
103
+ ? [{ name: 'authorityKeyIdentifier', keyIdentifier: issuerKeyId(issuerCertPem) }] : []),
92
104
  { name: 'subjectAltName',
93
105
  altNames: [...SANS, ...extraDns.map(v => ({ type: 2, value: v }))] }]);
94
106
  const signingKey = issuerKeyPem ? forge.pki.privateKeyFromPem(issuerKeyPem) : keys.privateKey;
@@ -131,6 +143,7 @@ export function ensureLocalTls(dir, { log = () => {}, names = [] } = {}) {
131
143
  const trust = !!(caKey && caCert);
132
144
  const isStale = (k, c) => !k || !c || daysLeft(c) < REMINT_DAYS || !coversLocalhost(c)
133
145
  || (trust && !issuedBy(c, caCert))
146
+ || (trust && !namesIssuerKey(c))
134
147
  || names.some(n => !certDnsNames(c).includes(n));
135
148
  let key = readPem(dir, FILES.key);
136
149
  let cert = readPem(dir, FILES.cert);
@@ -165,7 +178,7 @@ export function ensureLocalTls(dir, { log = () => {}, names = [] } = {}) {
165
178
  .filter(n => n !== 'localhost' && n !== '*.localhost');
166
179
  const minted = makeCert({
167
180
  subjectCN: 'localhost',
168
- ...(trust ? { issuer: caSubject(caCert), issuerKeyPem: caKey } : {}),
181
+ ...(trust ? { issuer: caSubject(caCert), issuerKeyPem: caKey, issuerCertPem: caCert } : {}),
169
182
  days: LEAF_DAYS,
170
183
  extraDns: carried,
171
184
  });
@@ -181,6 +194,9 @@ export function ensureLocalTls(dir, { log = () => {}, names = [] } = {}) {
181
194
  return { key, cert, trust, dir, expiresInDays: Math.floor(daysLeft(cert)) };
182
195
  }
183
196
 
197
+ const namesIssuerKey = (certPem) => {
198
+ try { return !!forge.pki.certificateFromPem(certPem).getExtension('authorityKeyIdentifier'); } catch { return false; }
199
+ };
184
200
  const caSubject = (caCertPem) => forge.pki.certificateFromPem(caCertPem).subject.attributes
185
201
  .map(a => ({ name: a.name, value: a.value }));
186
202
  const issuedBy = (certPem, caCertPem) => {
@@ -52,8 +52,12 @@ if (!newAccount) {
52
52
  const { resourceExists } = await import(new URL('../../../../lib/pod/root.mjs', import.meta.url));
53
53
  const { apUrls, DEFAULT_ROOT: DR } = await import(new URL('../../../../lib/core/wire.mjs', import.meta.url));
54
54
  if (await resourceExists(fetch, apUrls(pod, DR).actor)) {
55
- console.error('The pod already hosts a FediPod account. If you want a second account, put it on a different pod.');
56
- process.exit(2);
55
+ if (String(flag('address') || 'pod').toLowerCase() !== 'front') {
56
+ console.error('The pod already hosts a FediPod account. If you want a second account, put it on a different pod.');
57
+ console.error('To move an account whose address is at another gateway to this one, add --address front.');
58
+ process.exit(2);
59
+ }
60
+ console.log('The pod already holds an account; if its address is at another gateway, setup moves it here.');
57
61
  }
58
62
  }
59
63
 
@@ -289,7 +289,9 @@ for (const { name, dir } of homes) {
289
289
  // The kind lives in pod state, so it is only knowable from the live probe —
290
290
  // a stopped identity honestly shows nothing rather than a guess.
291
291
  rows.push({ name, pod: pod ? new URL(pod).host : '(no credential)', port: port || '—',
292
- kind: live?.kind === 'group' ? 'group' : live ? 'person' : '—',
292
+ // A home can hold a forum, which says so itself; anything else that
293
+ // answers is a person unless it says group.
294
+ kind: live?.kind === 'group' ? 'group' : live?.kind === 'forum' ? 'forum' : live ? 'person' : '—',
293
295
  state: live ? `${live.mode}${live.podRequests ? ` · ${live.podRequests.perMinuteNow}/min` : ''}` : 'not running' });
294
296
  }
295
297
 
@@ -0,0 +1,58 @@
1
+ // gateway-move.mjs — the second half of a DeviceAgent moving an address from
2
+ // one gateway to another (lib/device/setup.mjs does the first, when a pod
3
+ // that already holds an account at another gateway is set up with an address
4
+ // at this one). The browser build has the same two halves in
5
+ // web/app/gateway-move.mjs; this is the Node one, which signs and delivers
6
+ // itself instead of going through a relay.
7
+ //
8
+ // 1. The old gateway is told the address moved (POST /api/move, proved with
9
+ // the pod session as attach was). From then on it serves the old actor
10
+ // as a moved stub — same key, old id, `movedTo` the new one.
11
+ // 2. A Move goes to every follower, FROM the old actor, signed under the
12
+ // old key id: the only signature a follower's server can verify against
13
+ // the old actor. Same key material; only the key id differs.
14
+ //
15
+ // Runs when the agent starts acting, and is safe to run again: a move left
16
+ // pending (the old gateway unreachable) is tried at the next start.
17
+ import { Deliverer } from '../core/deliver.mjs';
18
+ import { moveActivity } from '../core/wire.mjs';
19
+
20
+ export async function completeGatewayMove(agent, { makeDeliverer = (o) => new Deliverer(o) } = {}) {
21
+ const cfg = agent.store.getConfig();
22
+ const mv = cfg?.movedFrom;
23
+ if (!mv || mv.completedAt) return null;
24
+ const log = agent.log || (() => {});
25
+ const newActor = agent.urls.actor;
26
+
27
+ // 1. the old gateway
28
+ let told;
29
+ try {
30
+ told = await agent.remote.session.fetch(`${mv.gateway}/api/move`, {
31
+ method: 'POST', headers: { 'content-type': 'application/json' },
32
+ body: JSON.stringify({ handle: mv.handle, movedTo: newActor }),
33
+ });
34
+ } catch (e) { told = { status: 0, statusText: e.message }; }
35
+ if (told.status !== 200) {
36
+ log(`gateway move: ${mv.gateway} did not mark @${mv.handle} as moved (${told.status || told.statusText}) — will retry at the next start`);
37
+ return { told: false };
38
+ }
39
+
40
+ // 2. the Move, from the old address
41
+ const contacts = agent.store.getContacts();
42
+ const inboxes = [...new Set(contacts.followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean))];
43
+ const activity = moveActivity({ actor: mv.actor }, newActor, Date.now());
44
+ const sender = makeDeliverer({
45
+ store: agent.store, rsaPrivate: agent.deliverer.rsaPrivate,
46
+ keyId: `${mv.actor}#main-key`, actorId: mv.actor, log, passive: true,
47
+ });
48
+ let sent = 0; let failed = 0;
49
+ for (const inbox of inboxes) {
50
+ try { await sender.deliverNow(inbox, activity); sent++; }
51
+ catch (e) { failed++; log(`gateway move: Move to ${inbox} failed: ${e.message}`); }
52
+ }
53
+ const done = { ...mv, completedAt: new Date().toISOString(), moveSent: sent, moveFailed: failed };
54
+ agent.store.setConfig({ ...agent.store.getConfig(), movedFrom: done });
55
+ await agent.store.flush?.();
56
+ log(`moved from ${mv.actor} to ${newActor}: Move sent to ${sent} inbox(es)${failed ? `, ${failed} failed` : ''}`);
57
+ return done;
58
+ }
@@ -27,7 +27,9 @@ export const layoutOf = (cred) => Number(cred?.layout) || 0;
27
27
  // deliberately, with `--private-root`, has said where they want it; a migration
28
28
  // that overrode that would be moving data on its own initiative.
29
29
  export function needsStateMove(cred) {
30
- return !cred?.privateRoot;
30
+ // `stateOnPod` is a move-in from a browser account (setup.mjs): the state
31
+ // is on the pod because that is where the account already lives.
32
+ return !cred?.privateRoot && !cred?.stateOnPod;
31
33
  }
32
34
 
33
35
  export function pendingSteps(cred) {
@@ -21,6 +21,7 @@ import { rootOf, recordLastUsed, writeJsonAtomic } from './home.mjs';
21
21
  import { insecureUrlReason } from '../shared/safefetch.mjs';
22
22
  import { resourceExists } from '../pod/root.mjs';
23
23
  import { CURRENT_LAYOUT, isCurrent } from './migrate.mjs';
24
+ import { completeGatewayMove } from './gateway-move.mjs';
24
25
 
25
26
  const SOLID = $rdf.Namespace('http://www.w3.org/ns/solid/terms#');
26
27
 
@@ -125,6 +126,18 @@ export function frontedAddress({ pod, shape }) {
125
126
  return pathPod || shape === 'front';
126
127
  }
127
128
 
129
+ // The account a pod already holds, read as its owner: the config and the key
130
+ // record in its state container. Both null when there is none, or when the
131
+ // credential is not the owner's. Injected for tests.
132
+ export async function readPodAccount({ home, credential, pod, root, log = () => {} }) {
133
+ const remote = new RemotePod(credential, { home, log });
134
+ await remote.warmup?.();
135
+ const urls = apUrls(pod, root || DEFAULT_ROOT);
136
+ const config = await remote.getJson(urls.state + 'config.json').catch(() => null);
137
+ const keys = await remote.getJson(urls.state + 'keys.json').catch(() => null);
138
+ return { config, keys };
139
+ }
140
+
128
141
  // Take an address at a Gateway: attach the pod to it, fronted, and return the
129
142
  // gateway config bootstrap writes and connect reads. The pod session proves
130
143
  // the pod — no password reaches the Gateway. Injected for tests.
@@ -231,8 +244,13 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
231
244
  // several writes, and that pod is a server somebody runs. `state --to` moves
232
245
  // it afterwards; starting on the pod and moving later is strictly worse,
233
246
  // because the copy left behind was on the pod the whole time.
234
- const privateRoot = answers.privateRoot
247
+ let privateRoot = answers.privateRoot
235
248
  || pathToFileURL(path.join(home, 'private')).href + '/';
249
+ // A pod that already holds an account at ANOTHER gateway, set up with an
250
+ // address at this one: not a second account but the same one moving in.
251
+ // Its state and key are on the pod, and stay there.
252
+ let movingIn = false;
253
+ let movedFrom = null;
236
254
  // Everything that leaves this function — notes, errors, the log — goes
237
255
  // through here. Nothing today echoes a password back, but /setup/progress is
238
256
  // polled repeatedly and would re-serve one forever if anything ever did.
@@ -266,10 +284,15 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
266
284
  // reachable, and no silent 401 later on a pod whose profile is empty.
267
285
  const usable = await checkPod(pod);
268
286
  if (!usable.ok) throw new Error(usable.error);
269
- if (await (deps.resourceExists || resourceExists)(deps.fetch || fetch, apUrls(pod, root || DEFAULT_ROOT).actor)) {
270
- throw new Error('The pod already hosts a FediPod account. If you want a second account, put it on a different pod.');
287
+ const held = await (deps.resourceExists || resourceExists)(deps.fetch || fetch, apUrls(pod, root || DEFAULT_ROOT).actor);
288
+ if (held && !frontedAddress({ pod, shape })) {
289
+ throw new Error('The pod already hosts a FediPod account. If you want a second account, put it on a different pod. '
290
+ + 'To move an account whose address is at another gateway to this one, ask for an address at this gateway.');
271
291
  }
272
- skip('account', 'using the pod you already have');
292
+ if (held && kind === 'group') throw new Error('The pod already hosts a FediPod account, and a group cannot move between gateways.');
293
+ movingIn = held;
294
+ if (movingIn) { privateRoot = null; }
295
+ skip('account', movingIn ? 'the pod already holds the account — its address moves here' : 'using the pod you already have');
273
296
  }
274
297
 
275
298
  // --- credential: the point of no return, and the durability boundary ---
@@ -295,12 +318,14 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
295
318
  remotePod: pod.endsWith('/') ? pod : pod + '/',
296
319
  createdAt: new Date().toISOString(),
297
320
  ...(root ? { root } : {}),
298
- ...(keys === 'pod' ? { keysMode: 'pod' } : {}),
299
- // Where the private half lives. Per-machine, like keysMode.
321
+ ...(keys === 'pod' || movingIn ? { keysMode: 'pod' } : {}),
322
+ // Where the private half lives. Per-machine, like keysMode. A move-in
323
+ // keeps it on the pod, where the account already is, and says so.
300
324
  ...(privateRoot ? { privateRoot } : {}),
325
+ ...(movingIn ? { stateOnPod: true } : {}),
301
326
  // What shape this install is, so `upgrade` can tell an old one from a
302
327
  // new one without guessing from the fields.
303
- ...(isCurrent({ privateRoot }) ? { layout: CURRENT_LAYOUT } : {}),
328
+ ...(isCurrent({ privateRoot, stateOnPod: movingIn }) ? { layout: CURRENT_LAYOUT } : {}),
304
329
  };
305
330
  fs.mkdirSync(home, { recursive: true, mode: 0o700 });
306
331
  writeJsonAtomic(credPath, rec);
@@ -316,6 +341,24 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
316
341
  done('credential', credPath);
317
342
  }
318
343
 
344
+ // --- a move-in: the account on the pod, read with the credential just
345
+ // minted, has to be one whose address lives at another gateway ---
346
+ if (movingIn) {
347
+ const cred = JSON.parse(fs.readFileSync(credPath, 'utf8'));
348
+ const account = await (deps.readPodAccount || readPodAccount)({ home, credential: cred, pod, root: root || DEFAULT_ROOT, log });
349
+ if (!account?.config) throw new Error('the pod holds an account, but its record could not be read with this credential — is this account the pod\'s owner?');
350
+ const frontActor = account.config.gateway?.frontActor || null;
351
+ const gwHost = new URL(gatewayOrigin).host;
352
+ if (!frontActor) throw new Error('The pod already hosts a FediPod account whose address lives on the pod: nothing to move. Sign in to it instead.');
353
+ if (new URL(frontActor).host === gwHost) throw new Error(`The pod already hosts a FediPod account at ${gwHost}: sign in to it instead of setting up again.`);
354
+ if ((account.config.kind || 'person') === 'group') throw new Error('a group cannot move between gateways');
355
+ if (!account.keys?.rsa || account.keys.ct) {
356
+ throw new Error(`this account's key is still under a password from an earlier version — sign in once at ${new URL(frontActor).host} first, then set up here`);
357
+ }
358
+ movedFrom = { actor: frontActor, gateway: new URL(frontActor).origin, handle: account.config.handle, at: new Date().toISOString() };
359
+ log(`moving @${account.config.handle}@${new URL(frontActor).host} here, as @${handle}@${gwHost}`);
360
+ }
361
+
319
362
  // --- take an address at the Gateway, if this pod needs one or asked for
320
363
  // one --- before bootstrap, so the config it writes carries the Gateway
321
364
  // ids and the key connect mints below is stamped to the Gateway actor from
@@ -346,6 +389,17 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
346
389
  if (!ready.ok) throw new Error(ready.error);
347
390
  }
348
391
  await agent.bootstrap({ handle, name: name || handle, root, kind, approveJoins, summary, icon, gateway: gatewayCfg });
392
+ if (movingIn && movedFrom) {
393
+ // The old actor becomes an alias, which is what a server checks on the
394
+ // new actor before it honours a Move; the Move itself is pending until
395
+ // the new actor is published (completeGatewayMove, below and at every
396
+ // start). The key stays; its stamp follows the actor.
397
+ const cfg = agent.store.getConfig() || {};
398
+ agent.store.setConfig({ ...cfg, aliases: [...new Set([...(cfg.aliases || []), movedFrom.actor])], movedFrom });
399
+ const keyRec = agent.store.read?.('keys.json', null);
400
+ if (keyRec?.mintedFor && gatewayCfg?.frontActor) agent.store.write?.('keys.json', { ...keyRec, mintedFor: gatewayCfg.frontActor });
401
+ await agent.store.flush();
402
+ }
349
403
  done('bootstrap');
350
404
 
351
405
  begin('connect');
@@ -360,6 +414,12 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
360
414
  }
361
415
  const published = await agent.publisher.publishProfile();
362
416
  await agent.store.flush();
417
+ if (movingIn) {
418
+ // Tell the old gateway and the followers. Not fatal: left pending, it
419
+ // is tried again when the agent next starts acting.
420
+ const moved = await (deps.completeGatewayMove || completeGatewayMove)(agent).catch((e) => { log(`gateway move: ${e.message}`); return null; });
421
+ if (moved?.completedAt) log(`Move sent from ${movedFrom.actor} to ${moved.moveSent} inbox(es)`);
422
+ }
363
423
  done('publish');
364
424
 
365
425
  // --- and say plainly whether the world can actually see it ---