fedipod-server 0.9.0 → 0.11.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 (319) hide show
  1. package/dist/claims.js +1 -1
  2. package/dist/handler.d.ts +1 -1
  3. package/dist/handler.js +35 -2
  4. package/dist/handler.jsonld +1 -1
  5. package/lib/admin.mjs +3 -3
  6. package/lib/c2s.mjs +27 -2
  7. package/lib/deliver.mjs +6 -0
  8. package/lib/embed.mjs +1 -0
  9. package/lib/export-collections.mjs +1 -1
  10. package/lib/front-core.mjs +272 -49
  11. package/lib/gateway-core.mjs +3 -7
  12. package/lib/httpsig.mjs +33 -2
  13. package/lib/import.mjs +1 -1
  14. package/lib/intake.mjs +357 -118
  15. package/lib/links.mjs +6 -32
  16. package/lib/mastoapi.mjs +449 -38
  17. package/lib/migrate.mjs +4 -18
  18. package/lib/pod/README.md +61 -0
  19. package/lib/pod/actor.mjs +96 -0
  20. package/lib/pod/collection.mjs +77 -0
  21. package/lib/pod/containers.mjs +136 -0
  22. package/lib/pod/discovery.mjs +46 -0
  23. package/lib/pod/featured.mjs +18 -0
  24. package/lib/pod/followers.mjs +24 -0
  25. package/lib/pod/following.mjs +10 -0
  26. package/lib/pod/http.mjs +74 -0
  27. package/lib/pod/inbox.mjs +144 -0
  28. package/lib/pod/links.mjs +35 -0
  29. package/lib/pod/media.mjs +27 -0
  30. package/lib/pod/notes.mjs +80 -0
  31. package/lib/pod/notifications.mjs +89 -0
  32. package/lib/pod/outbox.mjs +18 -0
  33. package/lib/pod/package.json +19 -0
  34. package/lib/pod/policy.mjs +58 -0
  35. package/lib/pod/private.mjs +19 -0
  36. package/lib/pod/root.mjs +63 -0
  37. package/lib/pod/state.mjs +39 -0
  38. package/lib/pod/transport.mjs +477 -0
  39. package/lib/pod/urls.mjs +71 -0
  40. package/lib/polls.mjs +105 -0
  41. package/lib/publisher.mjs +357 -188
  42. package/lib/remote.mjs +30 -395
  43. package/lib/social.mjs +20 -9
  44. package/lib/storage.mjs +2 -2
  45. package/lib/store.mjs +8 -1
  46. package/lib/tagfeed.mjs +19 -2
  47. package/lib/wire.mjs +87 -67
  48. package/package.json +1 -1
  49. package/run-agent.mjs +20 -39
  50. package/web/admin/admin.js +141 -74
  51. package/web/admin/bar.css +9 -13
  52. package/web/admin/bar.js +2 -2
  53. package/web/admin/client/client.js +1 -1
  54. package/web/admin/client/index.html +5 -6
  55. package/web/admin/index.html +53 -65
  56. package/web/admin/setup/index.html +2 -1
  57. package/web/admin/setup/setup.js +1 -1
  58. package/web/admin/tokens.css +56 -0
  59. package/web/app/README.md +77 -0
  60. package/web/app/admin-facade.mjs +549 -0
  61. package/web/app/agent.mjs +490 -0
  62. package/web/app/atproto-browser.mjs +86 -0
  63. package/web/app/boot.mjs +340 -0
  64. package/web/app/deliver-relay.mjs +72 -0
  65. package/web/app/dist/boot.js +34067 -0
  66. package/web/app/dist/boot.js.map +7 -0
  67. package/web/app/dist/sw.js +53745 -0
  68. package/web/app/dist/sw.js.map +7 -0
  69. package/web/app/fediacct-browser.mjs +146 -0
  70. package/web/app/idb-kv.mjs +35 -0
  71. package/web/app/index.html +203 -0
  72. package/web/app/index.html~ +152 -0
  73. package/web/app/keys-browser.mjs +46 -0
  74. package/web/app/keystore.mjs +99 -0
  75. package/web/app/oidc-session.mjs +189 -0
  76. package/web/app/pod-auth.mjs +248 -0
  77. package/web/app/pod-remote.mjs +84 -0
  78. package/web/app/shims/_globals.mjs +2 -0
  79. package/web/app/shims/fedify-sig.mjs +77 -0
  80. package/web/app/shims/node-crypto.mjs +113 -0
  81. package/web/app/shims/node-fs.mjs +17 -0
  82. package/web/app/shims/node-path.mjs +19 -0
  83. package/web/app/shims/node-url.mjs +7 -0
  84. package/web/app/shims/prelude.js +58 -0
  85. package/web/app/shims/safefetch.mjs +58 -0
  86. package/web/app/shims/web-push.mjs +14 -0
  87. package/web/app/signup.mjs +248 -0
  88. package/web/app/site/_headers +9 -0
  89. package/web/app/site/_redirects +21 -0
  90. package/web/app/site/admin/admin.js +1181 -0
  91. package/web/app/site/admin/bar.css +60 -0
  92. package/web/app/site/admin/bar.js +61 -0
  93. package/web/app/site/admin/client/client.js +66 -0
  94. package/web/app/site/admin/client/index.html +43 -0
  95. package/web/app/site/admin/index.html +556 -0
  96. package/web/app/site/admin/setup/index.html +191 -0
  97. package/web/app/site/admin/setup/setup.js +308 -0
  98. package/web/app/site/admin/tokens.css +56 -0
  99. package/web/app/site/admin/window.css +55 -0
  100. package/web/app/site/admin/window.js +136 -0
  101. package/web/app/site/app/404.html +40 -0
  102. package/web/app/site/app/_headers +2 -0
  103. package/web/app/site/app/apple-touch-icon.png +0 -0
  104. package/web/app/site/app/assets/ICONS-DIPJeU0C.js +2 -0
  105. package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +1 -0
  106. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +183 -0
  107. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +1 -0
  108. package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
  109. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +2 -0
  110. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +1 -0
  111. package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  112. package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
  113. package/web/app/site/app/assets/chunk-aKtaBQYM.js +1 -0
  114. package/web/app/site/app/assets/compose-CFEx7wb4.js +2 -0
  115. package/web/app/site/app/assets/compose-CFEx7wb4.js.map +1 -0
  116. package/web/app/site/app/assets/compose-ZJN5xSb4.js +35 -0
  117. package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +1 -0
  118. package/web/app/site/app/assets/debug.module-YFQgwnIr.js +52 -0
  119. package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +1 -0
  120. package/web/app/site/app/assets/dom-p1YcJ5Rw.js +2 -0
  121. package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +1 -0
  122. package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  123. package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  124. package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  125. package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  126. package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +1 -0
  127. package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +1 -0
  128. package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +1 -0
  129. package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +1 -0
  130. package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +1 -0
  131. package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +1 -0
  132. package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +1 -0
  133. package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +1 -0
  134. package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +1 -0
  135. package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +1 -0
  136. package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +1 -0
  137. package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +1 -0
  138. package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +1 -0
  139. package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +1 -0
  140. package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +1 -0
  141. package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +1 -0
  142. package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +1 -0
  143. package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +1 -0
  144. package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +1 -0
  145. package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +1 -0
  146. package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +1 -0
  147. package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +1 -0
  148. package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +1 -0
  149. package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +1 -0
  150. package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +1 -0
  151. package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +1 -0
  152. package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +1 -0
  153. package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +1 -0
  154. package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +1 -0
  155. package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +1 -0
  156. package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +1 -0
  157. package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +1 -0
  158. package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +1 -0
  159. package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +1 -0
  160. package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +1 -0
  161. package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +1 -0
  162. package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +1 -0
  163. package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +1 -0
  164. package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +1 -0
  165. package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +1 -0
  166. package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +1 -0
  167. package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +1 -0
  168. package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +1 -0
  169. package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +1 -0
  170. package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +1 -0
  171. package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +1 -0
  172. package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +1 -0
  173. package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +1 -0
  174. package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +1 -0
  175. package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +1 -0
  176. package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +1 -0
  177. package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +1 -0
  178. package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +1 -0
  179. package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +1 -0
  180. package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +1 -0
  181. package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +1 -0
  182. package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +1 -0
  183. package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +1 -0
  184. package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +1 -0
  185. package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +1 -0
  186. package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +1 -0
  187. package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +1 -0
  188. package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +1 -0
  189. package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +1 -0
  190. package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +1 -0
  191. package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +1 -0
  192. package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +1 -0
  193. package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +1 -0
  194. package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +1 -0
  195. package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +1 -0
  196. package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +1 -0
  197. package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +1 -0
  198. package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +1 -0
  199. package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +1 -0
  200. package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +1 -0
  201. package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +1 -0
  202. package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +1 -0
  203. package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +1 -0
  204. package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +1 -0
  205. package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +1 -0
  206. package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +1 -0
  207. package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +1 -0
  208. package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +1 -0
  209. package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +1 -0
  210. package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +1 -0
  211. package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +1 -0
  212. package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +1 -0
  213. package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +1 -0
  214. package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +1 -0
  215. package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +1 -0
  216. package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +1 -0
  217. package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +1 -0
  218. package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +1 -0
  219. package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +1 -0
  220. package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +1 -0
  221. package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +1 -0
  222. package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +1 -0
  223. package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +1 -0
  224. package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +1 -0
  225. package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +1 -0
  226. package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +1 -0
  227. package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +1 -0
  228. package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +1 -0
  229. package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +1 -0
  230. package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +1 -0
  231. package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +1 -0
  232. package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +1 -0
  233. package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +1 -0
  234. package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +1 -0
  235. package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +1 -0
  236. package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +1 -0
  237. package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +1 -0
  238. package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +1 -0
  239. package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +1 -0
  240. package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +1 -0
  241. package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +1 -0
  242. package/web/app/site/app/assets/instances-BNPptnn-.json +333 -0
  243. package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +1 -0
  244. package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +1 -0
  245. package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +1 -0
  246. package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +1 -0
  247. package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +1 -0
  248. package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +1 -0
  249. package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +1 -0
  250. package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +1 -0
  251. package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +1 -0
  252. package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +1 -0
  253. package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +1 -0
  254. package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +1 -0
  255. package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +1 -0
  256. package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +1 -0
  257. package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +1 -0
  258. package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +1 -0
  259. package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +1 -0
  260. package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +1 -0
  261. package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +1 -0
  262. package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +1 -0
  263. package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +1 -0
  264. package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +1 -0
  265. package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +1 -0
  266. package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +1 -0
  267. package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +1 -0
  268. package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +1 -0
  269. package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +1 -0
  270. package/web/app/site/app/assets/locales/tok-CfheRbCu.js +1 -0
  271. package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +1 -0
  272. package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +1 -0
  273. package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +1 -0
  274. package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +1 -0
  275. package/web/app/site/app/assets/main-BdqNbG-a.js +26 -0
  276. package/web/app/site/app/assets/main-BdqNbG-a.js.map +1 -0
  277. package/web/app/site/app/assets/mock-home-DGhR__He.js +2 -0
  278. package/web/app/site/app/assets/mock-home-DGhR__He.js.map +1 -0
  279. package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
  280. package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  281. package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  282. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +2 -0
  283. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +1 -0
  284. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +20 -0
  285. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +1 -0
  286. package/web/app/site/app/assets/style-B1iLPTPR.css +2 -0
  287. package/web/app/site/app/assets/temml-DuyIQMei.js +97 -0
  288. package/web/app/site/app/assets/temml-DuyIQMei.js.map +1 -0
  289. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +2 -0
  290. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +1 -0
  291. package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
  292. package/web/app/site/app/assets/year-in-posts-DJv_star.js +3 -0
  293. package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +1 -0
  294. package/web/app/site/app/compose/index.html +20 -0
  295. package/web/app/site/app/favicon.ico +0 -0
  296. package/web/app/site/app/index.html +73 -0
  297. package/web/app/site/app/logo-192.png +0 -0
  298. package/web/app/site/app/logo-512.png +0 -0
  299. package/web/app/site/app/logo-badge-72.png +0 -0
  300. package/web/app/site/app/logo-maskable-512.png +0 -0
  301. package/web/app/site/app/logo-monochrome-512.png +0 -0
  302. package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
  303. package/web/app/site/app/manifest.webmanifest +1 -0
  304. package/web/app/site/app/og-image-2.jpg +0 -0
  305. package/web/app/site/app/og-image.png +0 -0
  306. package/web/app/site/app/robots.txt +2 -0
  307. package/web/app/site/app/version.json +1 -0
  308. package/web/app/site/boot.js +34067 -0
  309. package/web/app/site/index.html +203 -0
  310. package/web/app/site/sw.js +53745 -0
  311. package/web/app/sw-src.mjs +205 -0
  312. package/web/front/#new-account.html# +43 -0
  313. package/web/front/admin.html +1 -81
  314. package/web/front/admin.js +85 -0
  315. package/web/front/new-account.html +1 -9
  316. package/web/front/new-account.js +13 -0
  317. package/web/front/run.html +1 -92
  318. package/web/front/run.js +107 -0
  319. package/lib/podrdf.mjs +0 -132
@@ -0,0 +1,549 @@
1
+ // admin-facade.mjs — the owner's record/manage surface, answered in the browser.
2
+ //
3
+ // The Node agent serves web/admin over lib/admin.mjs (buildAdminSurface), which
4
+ // is Node through and through — filesystem static serving, process control,
5
+ // multi-actor, group moderation. The browser reuses the SAME web/admin pages
6
+ // (staged static by Netlify) but cannot run that handler: it needs fs, http,
7
+ // child_process. So this answers the DATA endpoints those pages call, for the
8
+ // one thing a browser identity is: a single, personal, embedded actor.
9
+ //
10
+ // Personal only. Everything group (joins, members, moderation, mute) or local
11
+ // (drain, profiles-of-siblings, start/stop, state-move, update) is either 404
12
+ // here or hidden by the page on kind:'person'. What remains is reading and
13
+ // editing your own identity: status, config, gateway, alias, key rotation.
14
+ //
15
+ // It mirrors lib/admin.mjs's handlers for those paths, calling the same agent
16
+ // objects (store, publisher, intake, deliverer, remote) the Node agent does.
17
+ import { publicHandle, webfingerHost } from '../../lib/wire.mjs';
18
+ import * as podInbox from '../../lib/pod/inbox.mjs';
19
+ import { normalizeImport, IMPORT_KINDS } from '../../lib/import.mjs';
20
+ import { hashPassword } from '../../lib/mastoapi.mjs';
21
+
22
+ // The identity itself — changing any means a different actor, i.e. a new setup.
23
+ const PERMANENT_CONFIG = ['handle', 'remotePod', 'issuer', 'root', 'kind'];
24
+ // Carried in the actor document, so a change is not live until republished.
25
+ const WIRE_CONFIG = ['name', 'summary', 'icon', 'image', 'fields', 'aliases'];
26
+
27
+ // The data endpoints this agent answers at the origin root (the page computes
28
+ // its base as the origin, so it asks for /status, not /admin/status). Anything
29
+ // not here falls through to the network as a static file or the gateway.
30
+ export const ADMIN_PATHS = new Set([
31
+ '/status', '/config', '/gateway', '/alias', '/rotate-key', '/import',
32
+ '/deadletter', '/blocks', '/profiles', '/modqueue', '/log', '/fediacct', '/describe',
33
+ '/atproto', '/atproto/connect', '/atproto/disconnect',
34
+ '/rebuild', '/move', '/retire', '/inbox/prune', '/park', '/revive',
35
+ '/fediacct/connect', '/fediacct/disconnect', '/fediacct/callback',
36
+ ]);
37
+
38
+ // The page the fediverse account's OAuth redirect lands on (a top-level
39
+ // navigation, so it is HTML, not JSON). On success it steps back to the record
40
+ // page on its own; either way it offers the link.
41
+ function callbackHtml(ok, msg) {
42
+ const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
43
+ return '<!doctype html><html lang="en"><head><meta charset="utf-8">'
44
+ + '<meta name="viewport" content="width=device-width, initial-scale=1">'
45
+ + (ok ? '<meta http-equiv="refresh" content="2;url=/admin/">' : '')
46
+ + `<title>FediPod — ${ok ? 'connected' : 'not connected'}</title>`
47
+ + '<link rel="stylesheet" href="/admin/tokens.css">'
48
+ + '<style>body{font:20px/1.5 system-ui,sans-serif;margin:0;background:var(--surface);color:var(--fg);'
49
+ + 'display:grid;min-height:100dvh;place-items:center}main{max-width:32rem;padding:2rem;text-align:center}'
50
+ + 'h1{color:var(--heading)}a{color:var(--accent)}.err{color:var(--err)}</style></head><body><main>'
51
+ + `<h1>${ok ? 'Account connected' : 'Could not connect'}</h1>`
52
+ + `<p class="${ok ? '' : 'err'}">${esc(msg)}</p>`
53
+ + '<p><a href="/admin/">Back to your account</a></p></main></body></html>';
54
+ }
55
+
56
+ export class AdminFacade {
57
+ constructor({ agent, log = console.log }) { this.agent = agent; this.log = log; }
58
+
59
+ // Resolve an old account (a URL, or @user@host) to its canonical actor id,
60
+ // verifying by dereference the way intake does — a URL may claim any id, so
61
+ // that id must answer for itself before it is stored as an alias.
62
+ async resolveActor(input) {
63
+ const a = this.agent;
64
+ const s = String(input || '').trim().replace(/^@/, '');
65
+ if (!s) return null;
66
+ if (/^https?:\/\//.test(s)) {
67
+ let doc = await a.intake.fetchAP(s).catch(() => null);
68
+ if (doc?.id && doc.id !== s) {
69
+ const own = await a.intake.fetchAP(doc.id).catch(() => null);
70
+ doc = own?.id === doc.id ? own : null;
71
+ }
72
+ return doc?.id || null;
73
+ }
74
+ const at = s.indexOf('@');
75
+ if (at < 1) return null;
76
+ const host = s.slice(at + 1);
77
+ const wf = `https://${host}/.well-known/webfinger?resource=${encodeURIComponent('acct:' + s)}`;
78
+ try {
79
+ const res = await a.deliverer.signedFetch(wf, { method: 'GET', headers: { accept: 'application/jrd+json' } });
80
+ const jrd = await res.json();
81
+ const self = (jrd?.links || []).find(l => l.rel === 'self' && /(activity|ld)\+json/.test(l.type || ''));
82
+ if (!self?.href) return null;
83
+ const doc = await a.intake.fetchAP(self.href).catch(() => null);
84
+ return doc?.id || null;
85
+ } catch { return null; }
86
+ }
87
+
88
+ /** Answer an admin data request. Returns true when it owns the path. */
89
+ async handle(req, res, p, url, bodyText) {
90
+ const a = this.agent;
91
+ const json = (status, obj) => {
92
+ res.writeHead(status, { 'content-type': 'application/json' });
93
+ res.end(JSON.stringify(obj));
94
+ return true;
95
+ };
96
+ if (!ADMIN_PATHS.has(p)) return false;
97
+ const method = req.method;
98
+ const isGroup = () => a.store.getConfig()?.kind === 'group';
99
+
100
+ // ---- reads ----
101
+ if (method === 'GET') {
102
+ switch (p) {
103
+ case '/status': return json(200, a.status());
104
+ case '/config': {
105
+ const cfg = a.store.getConfig();
106
+ if (!cfg) return json(409, { error: 'agent not configured' });
107
+ const urls = a.urls || a.publisher?.urls || null;
108
+ const wfHost = urls ? new URL(urls.base).host : null;
109
+ const address = cfg.gateway?.frontActor
110
+ ? `@${publicHandle(cfg)}@${new URL(cfg.gateway.frontActor).host}`
111
+ : (wfHost ? `@${cfg.handle}@${wfHost}` : null);
112
+ return json(200, {
113
+ handle: cfg.handle, remotePod: cfg.remotePod, issuer: cfg.issuer,
114
+ root: cfg.root || null, kind: cfg.kind || 'person',
115
+ actor: urls?.actor || null, webId: a.webId || null,
116
+ accountId: urls?.actor ? a.store.idFor(urls.actor) : null,
117
+ address,
118
+ name: cfg.name || null, summary: cfg.summary || null, icon: cfg.icon || null,
119
+ image: cfg.image || null, fields: cfg.fields || [],
120
+ aliases: cfg.aliases || [],
121
+ autoAcceptFollows: !!cfg.autoAcceptFollows,
122
+ hasUiPassword: !!cfg.uiPassword,
123
+ quiescedAt: cfg.quiescedAt || null, movedTo: cfg.movedTo || null,
124
+ mode: a.status().mode,
125
+ // Live from the credential's actual backend on THIS browser (a
126
+ // browser-stored connection is only present where its token is);
127
+ // crossPost stays a default-on config value, no longer a toggle.
128
+ atproto: (() => { const s = a.atproto?.status(); return s?.connected
129
+ ? { handle: s.handle, service: s.service, did: s.did, connected: true, storage: s.storage, feedPaused: s.feedPaused, crossPost: cfg.atproto?.crossPost ?? true }
130
+ : null; })(),
131
+ fediAccounts: a.fediaccts?.status() || [],
132
+ });
133
+ }
134
+ case '/gateway': {
135
+ const g = a.store.getConfig()?.gateway || null;
136
+ return json(200, {
137
+ configured: !!(g && g.url), url: g?.url || null, webId: g?.webId || null,
138
+ frontActor: g?.frontActor || null, mode: g?.mode || 'off', hasSecret: !!g?.hmacSecret,
139
+ stats: a.store.read('gateway-stats.json', { verified: 0, unverified: 0, lastAt: null }),
140
+ });
141
+ }
142
+ case '/deadletter': return json(200, { items: a.store.getDeadLetters() });
143
+ case '/blocks': return json(200, a.store.getBlocklist());
144
+ // One personal identity — no siblings on a machine to list. The bar's
145
+ // dropdown shows this one plus "add an account".
146
+ case '/profiles': {
147
+ const s = a.status();
148
+ const host = s.actor ? new URL(s.actor).host : null;
149
+ return json(200, {
150
+ identities: [{
151
+ name: s.handle, current: true, handle: s.handle,
152
+ address: (s.handle && host) ? `${s.handle}@${host}` : null,
153
+ admin: '/admin/', app: '/app/', kind: s.kind, mode: s.mode,
154
+ }],
155
+ });
156
+ }
157
+ case '/modqueue': return json(200, []); // person: no moderation queue
158
+ case '/log': return json(200, { lines: [] }); // no local log file in the browser
159
+ case '/fediacct': return json(200, { accounts: a.fediaccts?.status() || [] });
160
+ case '/import':
161
+ return json(200, a.importer ? a.importer.progress() : { running: false, total: 0, done: 0 });
162
+ // The OAuth redirect returns here as a top-level navigation, so this one
163
+ // answers with an HTML page, not JSON. It completes the connection, then
164
+ // shows the result with a way back to the record page.
165
+ case '/fediacct/callback': {
166
+ const q = url.searchParams;
167
+ const page = (ok, msg) => { res.writeHead(ok ? 200 : 400, { 'content-type': 'text/html; charset=utf-8' }); res.end(callbackHtml(ok, msg)); return true; };
168
+ if (q.get('error')) return page(false, q.get('error_description') || q.get('error'));
169
+ if (!q.get('code') || !q.get('state')) return page(false, 'that sign-in came back incomplete');
170
+ try {
171
+ const row = await a.fediaccts.complete({ state: q.get('state'), code: q.get('code') });
172
+ // The roster is computed live from the backends by /config; a
173
+ // browser-stored token must not put the account on the pod, so
174
+ // nothing is written to pod config here.
175
+ a.startAccts?.();
176
+ return page(true, `${row.handle} is connected.`);
177
+ } catch (e) { return page(false, e.message); }
178
+ }
179
+ default: return json(405, { error: 'GET not supported here' });
180
+ }
181
+ }
182
+
183
+ if (method !== 'POST') return json(405, { error: 'POST only' });
184
+ let body = {};
185
+ try { body = bodyText ? JSON.parse(bodyText) : {}; }
186
+ catch { return json(400, { error: 'expected JSON body' }); }
187
+
188
+ // ---- writes ----
189
+ switch (p) {
190
+ case '/config': {
191
+ const fixed = PERMANENT_CONFIG.filter(k => k in body);
192
+ if (fixed.length) return json(400, { error: `${fixed.join(', ')} cannot be changed — that is the identity itself` });
193
+ // Group-only knobs are not offered on a personal identity.
194
+ for (const k of ['approveJoins', 'review', 'moderators']) {
195
+ if (k in body) return json(404, { error: 'not a group' });
196
+ }
197
+ await a.requestTakeover?.();
198
+ const cfg = { ...a.store.getConfig() };
199
+ if ('name' in body) {
200
+ if (!body.name) return json(400, { error: 'a display name is required' });
201
+ cfg.name = String(body.name);
202
+ }
203
+ if ('summary' in body) cfg.summary = body.summary || undefined;
204
+ if ('icon' in body) cfg.icon = body.icon || undefined;
205
+ if ('image' in body) cfg.image = body.image || undefined;
206
+ if ('fields' in body) {
207
+ cfg.fields = (Array.isArray(body.fields) ? body.fields : [])
208
+ .filter(f => f?.name?.trim())
209
+ .map(f => ({ name: String(f.name).trim(), value: String(f.value ?? '').trim() }));
210
+ }
211
+ if ('autoAcceptFollows' in body) cfg.autoAcceptFollows = !!body.autoAcceptFollows;
212
+ if ('password' in body) {
213
+ if (body.password) cfg.uiPassword = hashPassword(body.password);
214
+ else delete cfg.uiPassword;
215
+ }
216
+ const republish = WIRE_CONFIG.some(k => k in body);
217
+ a.store.setConfig(cfg);
218
+ if ('autoAcceptFollows' in body && a.publisher) a.publisher.config.autoAcceptFollows = cfg.autoAcceptFollows;
219
+ if (republish && a.publisher) {
220
+ Object.assign(a.publisher.config, {
221
+ name: cfg.name, summary: cfg.summary, icon: cfg.icon,
222
+ image: cfg.image, fields: cfg.fields, aliases: cfg.aliases,
223
+ });
224
+ }
225
+ await a.store.flush();
226
+ const pub = republish ? await a.publisher.publishProfile() : null;
227
+ return json(200, {
228
+ ok: true, published: republish,
229
+ ...(pub?.unreachable?.length ? { unreachable: pub.unreachable } : {}),
230
+ });
231
+ }
232
+
233
+ case '/describe': {
234
+ const cfg = { ...a.store.getConfig() };
235
+ if ('summary' in body) cfg.summary = body.summary || undefined;
236
+ if ('icon' in body) cfg.icon = body.icon || undefined;
237
+ a.store.setConfig(cfg);
238
+ Object.assign(a.publisher.config, { summary: cfg.summary, icon: cfg.icon });
239
+ await a.store.flush();
240
+ await a.publisher.publishProfile();
241
+ return json(200, { ok: true, summary: cfg.summary || null, icon: cfg.icon || null });
242
+ }
243
+
244
+ // Going quiet, and coming back. The record page's active/parked select.
245
+ case '/park': {
246
+ if (!await a.requestTakeover?.()) return json(503, { error: 'another device is active for this pod — park from there' });
247
+ return json(200, { ok: true, ...await a.park() });
248
+ }
249
+ case '/revive': {
250
+ if (!await a.requestTakeover?.()) return json(503, { error: 'another device is active for this pod — revive from there' });
251
+ return json(200, { ok: true, ...await a.revive() });
252
+ }
253
+
254
+ case '/rotate-key': {
255
+ await a.requestTakeover?.();
256
+ // The pod's copy of the key is wrapped under the account password, so a
257
+ // rotation has to be given one. 428 rather than 400: nothing is wrong
258
+ // with the request, something is required before it can be made.
259
+ try {
260
+ const r = await a.rotateKey({ password: body.password });
261
+ return json(200, { ok: true, changed: !!r?.changed });
262
+ } catch (e) {
263
+ if (e.code !== 'key-password-needed') throw e;
264
+ return json(428, { error: e.message, needsPassword: true });
265
+ }
266
+ }
267
+
268
+ // Recover posts this browser lost, from what the pod still holds.
269
+ case '/rebuild': {
270
+ if (!await a.requestTakeover?.()) return json(503, { error: 'another device is active for this pod — recover from there' });
271
+ return json(200, await a.publisher.rebuildStatuses({ fromNotes: !!body.fromNotes }));
272
+ }
273
+
274
+ // Catch up on a backlog: drop old inbox posts (older than `before`) but
275
+ // keep applying the follows/unfollows/accepts/deletes in that window. The
276
+ // browser's own Intake does the work, exactly as the Node agent's does.
277
+ case '/inbox/prune': {
278
+ if (!body.before) return json(400, { error: 'before (a date) required' });
279
+ if (!await a.requestTakeover?.()) return json(503, { error: 'another device is active for this pod — discard from there' });
280
+ return json(200, await a.intake.prune({ before: body.before, ...(body.keepConcerning ? { keepConcerning: true } : {}) }));
281
+ }
282
+
283
+ // Hand the account away: a federated Move migrates followers to the
284
+ // target, this handle is left redirecting. The typed-handle interlock is
285
+ // the same as the Node agent's — a stray click cannot produce it.
286
+ case '/move': {
287
+ if (!body.target) return json(400, { error: 'target required' });
288
+ if (!body.confirm || body.confirm !== a.store.getConfig()?.handle) {
289
+ return json(400, { error: 'type the handle to confirm' });
290
+ }
291
+ await a.requestTakeover?.();
292
+ let target = String(body.target).trim();
293
+ if (!/^https?:\/\//.test(target)) {
294
+ const id = await this.resolveActor(target);
295
+ if (!id) return json(400, { error: `could not resolve ${body.target}` });
296
+ target = id;
297
+ }
298
+ return json(200, { ok: true, ...await a.moveTo(target) });
299
+ }
300
+
301
+ // End the identity for good: a Delete to every follower, the actor
302
+ // replaced by a Tombstone that stays fetchable. Typed-handle interlock.
303
+ case '/retire': {
304
+ if (!body.confirm || body.confirm !== a.store.getConfig()?.handle) {
305
+ return json(400, { error: 'type the handle to confirm' });
306
+ }
307
+ await a.requestTakeover?.();
308
+ return json(200, { ok: true, ...await a.publisher.retireActor() });
309
+ }
310
+
311
+ case '/alias': {
312
+ const urls = a.publisher?.urls;
313
+ if (!urls) return json(409, { error: 'agent not connected yet' });
314
+ if (!body.add && !body.remove) return json(400, { error: 'add or remove required' });
315
+ await a.requestTakeover?.();
316
+ const cfg = { ...a.store.getConfig() };
317
+ const aliases = [...(cfg.aliases || [])];
318
+ if (body.add) {
319
+ if (!webfingerHost(urls.base) && !cfg.gateway?.frontActor) {
320
+ return json(400, { error: 'this pod is a path on a shared host, so other servers could never resolve it as a Move target' });
321
+ }
322
+ const id = await this.resolveActor(body.add);
323
+ if (!id) return json(400, { error: `could not fetch the old account (${body.add}) — enter its URL or @user@host, and it must answer` });
324
+ if (id === urls.actor) return json(400, { error: 'that is this account' });
325
+ if (!aliases.includes(id)) aliases.push(id);
326
+ } else {
327
+ const target = String(body.remove).trim();
328
+ if (!aliases.includes(target)) return json(404, { error: 'no such alias' });
329
+ if (!body.confirm) {
330
+ return json(409, { error: 'servers still retrying the Move check this alias — send confirm: true to remove it anyway' });
331
+ }
332
+ aliases.splice(aliases.indexOf(target), 1);
333
+ }
334
+ cfg.aliases = aliases;
335
+ a.store.setConfig(cfg);
336
+ a.publisher.config.aliases = aliases;
337
+ await a.store.flush();
338
+ const pub = await a.publisher.publishProfile();
339
+ return json(200, { ok: true, aliases, ...(pub?.unreachable?.length ? { unreachable: pub.unreachable } : {}) });
340
+ }
341
+
342
+ case '/gateway': {
343
+ const cfg = { ...a.store.getConfig() };
344
+ const g = { ...(cfg.gateway || {}) };
345
+ const inboxUrl = a.publisher?.urls?.inbox;
346
+ if (body.action === 'check') {
347
+ const front = String(body.front || '').replace(/\/+$/, '');
348
+ try { new URL(front); } catch { return json(400, { error: 'front must be a gateway origin URL' }); }
349
+ const chk = await fetch(`${front}/api/handle?handle=${encodeURIComponent(String(body.handle || '').toLowerCase())}`,
350
+ { headers: { accept: 'application/json' } }).then(r => r.json()).catch(() => null);
351
+ if (!chk) return json(502, { error: `${front} did not answer its handle check` });
352
+ return json(200, { available: !!chk.available, reason: chk.reason || null });
353
+ }
354
+ await a.requestTakeover?.();
355
+ const persist = async () => {
356
+ cfg.gateway = g; a.store.setConfig(cfg);
357
+ if (a.publisher) a.publisher.config.gateway = g;
358
+ await a.store.flush();
359
+ };
360
+ if (body.action === 'configure') {
361
+ if (!/^https:\/\/\S+$/.test(String(body.url || ''))) return json(400, { error: 'gateway url must be https' });
362
+ if (!/^https?:\/\/\S+$/.test(String(body.webId || ''))) return json(400, { error: 'gateway webId must be a URL' });
363
+ g.url = String(body.url); g.webId = String(body.webId);
364
+ if (!g.hmacSecret) {
365
+ const rnd = crypto.getRandomValues(new Uint8Array(32));
366
+ g.hmacSecret = btoa(String.fromCharCode(...rnd));
367
+ }
368
+ g.mode = g.mode || 'off';
369
+ if ('frontActor' in body) {
370
+ const fa = String(body.frontActor || '');
371
+ if (fa && !/^https:\/\/\S+\/ap\/actor$/.test(fa)) return json(400, { error: 'frontActor must be an https …/ap/actor URL' });
372
+ g.frontActor = fa || undefined;
373
+ }
374
+ await persist();
375
+ return json(200, { ok: true, mode: g.mode, url: g.url, webId: g.webId, hmacSecret: g.hmacSecret });
376
+ }
377
+ if (body.action === 'mode') {
378
+ const target = body.mode;
379
+ if (!['off', 'shadow', 'trust', 'locked'].includes(target)) return json(400, { error: 'mode must be off, shadow, trust or locked' });
380
+ if (target !== 'off' && !g.url) return json(400, { error: 'point at a gateway first (action: configure)' });
381
+ if (target === 'locked' && !g.webId) return json(400, { error: "locked needs the gateway's WebID — set it with action: configure" });
382
+ const prev = g.mode || 'off';
383
+ if (target === 'locked') await a.publisher?.lockInboxToGateway(g.webId);
384
+ else if (prev === 'locked' && inboxUrl) await podInbox.setPosture(a.remote, a.urls, 'open');
385
+ g.mode = target;
386
+ await persist();
387
+ if ((prev === 'off') !== (target === 'off')) await a.publisher?.publishProfile();
388
+ else if (target !== 'off') await a.publisher?.publishGatewayPolicy().catch(() => {});
389
+ return json(200, { ok: true, mode: g.mode });
390
+ }
391
+ // Attach the pod's mail through a gateway (fedipod.net and kin). The
392
+ // agent proves the pod with its own DPoP session — no password — and the
393
+ // front answers with the door URL and the receipt secret. Browser model
394
+ // only: @you@yourpod (inbox door), never a fronted @you@front — the
395
+ // latter renames every id and needs a process restart the browser has
396
+ // no equivalent for.
397
+ if (body.action === 'attach') {
398
+ const front = String(body.front || '').replace(/\/+$/, '');
399
+ let fu;
400
+ try { fu = new URL(front); } catch { return json(400, { error: 'front must be the gateway origin, like https://fedipod.net' }); }
401
+ if (fu.protocol !== 'https:' && !/^(localhost|127\.0\.0\.1)$|\.localhost$/.test(fu.hostname)) return json(400, { error: 'front must be https' });
402
+ if (body.fronted === true) return json(400, { error: 'a fronted @you@front identity is not the browser model — the browser uses the mail-door form @you@yourpod' });
403
+ const named = !!String(body.handle || '').trim();
404
+ let handle = String(body.handle || cfg.handle || '').toLowerCase().trim();
405
+ if (!handle) return json(400, { error: 'a name at the gateway is required' });
406
+ const avail = async (h) => fetch(`${front}/api/handle?handle=${encodeURIComponent(h)}`,
407
+ { headers: { accept: 'application/json' } }).then(r => r.json()).catch(() => null);
408
+ let chk = await avail(handle);
409
+ if (!chk) return json(502, { error: `${front} did not answer its handle check` });
410
+ if (!chk.available && !named) {
411
+ for (let i = 2; i <= 9 && !chk.available; i++) {
412
+ const cand = `${handle}${i}`; const c = await avail(cand);
413
+ if (c?.available) { handle = cand; chk = c; }
414
+ }
415
+ }
416
+ if (!chk.available) return json(409, { error: chk.reason || `the name ${handle} is taken at ${front}` });
417
+ const attach = await a.remote.session.fetch(`${front}/api/attach`, {
418
+ method: 'POST', headers: { 'content-type': 'application/json' },
419
+ body: JSON.stringify({ handle, podHome: a.urls.home, kind: cfg.kind || 'person', fronted: false }),
420
+ }).catch(() => null);
421
+ if (!attach) return json(502, { error: `${front} did not answer the attach` });
422
+ const d = await attach.json().catch(() => ({}));
423
+ if (attach.status !== 201) {
424
+ return json(attach.status >= 400 && attach.status < 500 ? attach.status : 502, { error: d.error || `attach failed (HTTP ${attach.status})` });
425
+ }
426
+ g.url = String(d.doorInbox || `${front}/u/${handle}/ap/inbox/`);
427
+ if (d.hmacSecret) g.hmacSecret = String(d.hmacSecret);
428
+ if (!g.mode || g.mode === 'off') g.mode = 'shadow';
429
+ await persist();
430
+ await a.publisher?.publishProfile();
431
+ await a.publisher?.publishGatewayPolicy?.().catch(() => {});
432
+ return json(200, { ok: true, mode: g.mode, url: g.url });
433
+ }
434
+ // Detach: forget the gateway and re-advertise the pod's own inbox.
435
+ if (body.action === 'forget') {
436
+ if (g.frontActor) return json(400, { error: 'this identity publishes under a gateway front; detaching a front is not supported in the browser build' });
437
+ const wasLocked = g.mode === 'locked';
438
+ delete cfg.gateway; a.store.setConfig(cfg);
439
+ if (a.publisher) a.publisher.config.gateway = undefined;
440
+ await a.store.flush();
441
+ if (wasLocked && inboxUrl) await podInbox.setPosture(a.remote, a.urls, 'open').catch(() => {});
442
+ await a.publisher?.publishProfile();
443
+ return json(200, { ok: true, mode: 'off', forgotten: true });
444
+ }
445
+ return json(400, { error: 'unknown gateway action' });
446
+ }
447
+
448
+ // Connect a Bluesky account. This is the one place an app password is
449
+ // seen; it is stored owner-only on the pod (never returned), and the
450
+ // mirror poll starts. Same shape as the Node agent's /atproto/connect.
451
+ case '/atproto/connect': {
452
+ if (!body.identifier || !body.appPassword) return json(400, { error: 'identifier and appPassword required' });
453
+ await a.requestTakeover?.();
454
+ let conn;
455
+ try { conn = await a.atproto.connect({ service: body.service, identifier: body.identifier, appPassword: body.appPassword }); }
456
+ catch (e) { return json(400, { error: e.message }); }
457
+ const cfg = a.store.getConfig();
458
+ a.store.setConfig({ ...cfg, atproto: { ...conn, crossPost: cfg.atproto?.crossPost ?? true } });
459
+ await a.store.flush();
460
+ a.startBsky?.();
461
+ return json(200, { ok: true, ...a.atproto.status() });
462
+ }
463
+ case '/atproto/disconnect': {
464
+ a.stopBsky?.();
465
+ await a.atproto.disconnect();
466
+ const { atproto, ...rest } = a.store.getConfig();
467
+ a.store.setConfig(rest);
468
+ await a.store.flush();
469
+ return json(200, { ok: true });
470
+ }
471
+ // Bluesky settings: pause/resume the mirror, and move the credential
472
+ // between this browser and the pod. Cross-post is default-on, not toggled.
473
+ case '/atproto': {
474
+ if (!a.atproto?.connected()) return json(400, { error: 'no bluesky account connected' });
475
+ if ('feedPaused' in body) { a.atproto.setFeedPaused(!!body.feedPaused); a.restartBsky?.(); }
476
+ if ('storage' in body) {
477
+ if (!['pod', 'browser'].includes(body.storage)) return json(400, { error: 'storage must be pod or browser' });
478
+ a.atproto.setStorage(body.storage);
479
+ }
480
+ return json(200, { ok: true, ...a.atproto.status() });
481
+ }
482
+
483
+ // Connect a fediverse account on another server: start its OAuth at that
484
+ // server. The browser is then sent to `authorize`; the code comes back to
485
+ // GET /fediacct/callback (above). The token never touches the pod.
486
+ case '/fediacct/connect': {
487
+ if (!body.host) return json(400, { error: 'the address of the server is required' });
488
+ try {
489
+ const { url: authorize } = await a.fediaccts.begin({ host: body.host, redirectUri: `${url.origin}/fediacct/callback` });
490
+ return json(200, { ok: true, authorize });
491
+ } catch (e) { return json(400, { error: e.message }); }
492
+ }
493
+ case '/fediacct/disconnect': {
494
+ if (!body.id) return json(400, { error: 'id required' });
495
+ await a.fediaccts.revoke(body.id); // best-effort revoke at the server, then forget it here
496
+ if (!a.fediaccts.remove(body.id)) return json(404, { error: 'no such account' });
497
+ a.restartAccts?.();
498
+ return json(200, { ok: true });
499
+ }
500
+ // Pause/resume the mirror, or move the credential between browser and pod.
501
+ case '/fediacct': {
502
+ if (!body.id) return json(400, { error: 'id required' });
503
+ let row;
504
+ if ('storage' in body) {
505
+ if (!['pod', 'browser'].includes(body.storage)) return json(400, { error: 'storage must be pod or browser' });
506
+ row = a.fediaccts.setStorage(body.id, body.storage);
507
+ } else row = a.fediaccts.setEnabled(body.id, !!body.enabled);
508
+ if (!row) return json(404, { error: 'no such account' });
509
+ a.restartAccts?.();
510
+ return json(200, { ok: true, account: row });
511
+ }
512
+
513
+ // CSV import — follows, blocks, mutes, lists and domains, staged onto the
514
+ // pod and applied by a worker over the following minutes. The same
515
+ // ImportWorker the Node agent runs (lib/import.mjs); it needs only the
516
+ // store and social.mjs, both of which are in this bundle.
517
+ case '/import': {
518
+ if (!a.importer) return json(409, { error: 'agent not connected yet' });
519
+ // Staging arms a worker that keeps writing for minutes, so a device
520
+ // that could not take the lease must not run one beside the device
521
+ // that holds it.
522
+ if (!await a.requestTakeover?.()) {
523
+ return json(503, { error: 'another device is active for this pod — import from there' });
524
+ }
525
+ if (body.clear === true) {
526
+ a.importer.clear();
527
+ return json(200, { ok: true, cleared: true });
528
+ }
529
+ if (!IMPORT_KINDS.includes(body.kind)) {
530
+ return json(400, { error: `kind must be one of: ${IMPORT_KINDS.join(', ')}` });
531
+ }
532
+ if (typeof body.text !== 'string' || !body.text.trim()) {
533
+ return json(400, { error: 'text required — the CSV file contents' });
534
+ }
535
+ const { values, invalid } = normalizeImport(body.kind, body.text);
536
+ const r = a.importer.stage(body.kind, values);
537
+ await a.store.flush();
538
+ return json(200, {
539
+ ok: true, kind: body.kind, ...r,
540
+ invalid: invalid.length,
541
+ ...(invalid.length ? { invalidSample: invalid.slice(0, 5) } : {}),
542
+ });
543
+ }
544
+
545
+ default:
546
+ return json(404, { error: 'not available on a personal browser identity' });
547
+ }
548
+ }
549
+ }