fedipod-server 0.10.0 → 0.12.1

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 (416) hide show
  1. package/README.md +15 -6
  2. package/dist/claims.js +1 -1
  3. package/dist/handler.d.ts +7 -1
  4. package/dist/handler.js +58 -12
  5. package/dist/handler.jsonld +5 -1
  6. package/dist/store-pod.js +18 -4
  7. package/lib/{c2s.mjs → client/c2s.mjs} +8 -3
  8. package/lib/{localapi.mjs → client/localapi.mjs} +2 -2
  9. package/lib/client/masto/accounts.mjs +264 -0
  10. package/lib/client/masto/body.mjs +69 -0
  11. package/lib/client/masto/index.mjs +183 -0
  12. package/lib/client/masto/instance.mjs +104 -0
  13. package/lib/client/masto/media.mjs +133 -0
  14. package/lib/client/masto/oauth.mjs +599 -0
  15. package/lib/client/masto/render.mjs +459 -0
  16. package/lib/client/masto/statuses.mjs +331 -0
  17. package/lib/client/masto/timelines.mjs +316 -0
  18. package/lib/{streaming.mjs → client/streaming.mjs} +1 -1
  19. package/lib/{acctfeed.mjs → connections/acctfeed.mjs} +1 -1
  20. package/lib/{atproto.mjs → connections/atproto.mjs} +15 -16
  21. package/lib/{bskygroup.mjs → connections/bskygroup.mjs} +1 -1
  22. package/lib/{fediacct.mjs → connections/fediacct.mjs} +31 -35
  23. package/lib/{import.mjs → connections/import.mjs} +2 -2
  24. package/lib/{tagfeed.mjs → connections/tagfeed.mjs} +21 -4
  25. package/lib/connections/vault.mjs +114 -0
  26. package/lib/core/as2.mjs +124 -0
  27. package/lib/core/contexts/activitystreams.json +379 -0
  28. package/lib/core/contexts/did-v1.json +57 -0
  29. package/lib/core/contexts/fep-5711.json +36 -0
  30. package/lib/core/contexts/gotosocial.json +86 -0
  31. package/lib/core/contexts/identity-v1.json +152 -0
  32. package/lib/core/contexts/index.mjs +45 -0
  33. package/lib/core/contexts/join-lemmy.json +33 -0
  34. package/lib/core/contexts/joinmastodon.json +28 -0
  35. package/lib/core/contexts/map.json +16 -0
  36. package/lib/core/contexts/miscellany.json +19 -0
  37. package/lib/core/contexts/schemaorg.json +8845 -0
  38. package/lib/core/contexts/security-data-integrity-v1.json +78 -0
  39. package/lib/core/contexts/security-data-integrity-v2.json +81 -0
  40. package/lib/core/contexts/security-multikey-v1.json +35 -0
  41. package/lib/core/contexts/security-v1.json +74 -0
  42. package/lib/core/contexts/webfinger.json +10 -0
  43. package/lib/{deliver.mjs → core/deliver.mjs} +8 -2
  44. package/lib/core/intake/activities.mjs +437 -0
  45. package/lib/core/intake/activity.mjs +240 -0
  46. package/lib/core/intake/channel.mjs +144 -0
  47. package/lib/core/intake/group.mjs +222 -0
  48. package/lib/core/intake/index.mjs +629 -0
  49. package/lib/core/intake/notes.mjs +288 -0
  50. package/lib/core/intake/verify.mjs +141 -0
  51. package/lib/{keys.mjs → core/keys.mjs} +1 -1
  52. package/lib/core/publisher/collections.mjs +229 -0
  53. package/lib/core/publisher/index.mjs +421 -0
  54. package/lib/core/publisher/notes.mjs +188 -0
  55. package/lib/core/publisher/questions.mjs +233 -0
  56. package/lib/core/publisher/restore.mjs +196 -0
  57. package/lib/core/shapes/activitystreams.ttl +129 -0
  58. package/lib/core/shapes/index.mjs +107 -0
  59. package/lib/core/shapes/shapes-text.mjs +13 -0
  60. package/lib/{social.mjs → core/social.mjs} +19 -10
  61. package/lib/{storage.mjs → core/storage.mjs} +2 -2
  62. package/lib/{store.mjs → core/store.mjs} +12 -1
  63. package/lib/{wire.mjs → core/wire.mjs} +48 -67
  64. package/lib/device/admin/index.mjs +13 -0
  65. package/lib/device/admin/origins.mjs +35 -0
  66. package/lib/device/admin/routes/connections.mjs +144 -0
  67. package/lib/device/admin/routes/gateway.mjs +199 -0
  68. package/lib/device/admin/routes/lifecycle.mjs +191 -0
  69. package/lib/device/admin/routes/owner.mjs +322 -0
  70. package/lib/device/admin/routes/setup.mjs +393 -0
  71. package/lib/device/admin/routes/social.mjs +188 -0
  72. package/lib/device/admin/server.mjs +95 -0
  73. package/lib/device/admin/static.mjs +244 -0
  74. package/lib/device/admin/surface.mjs +274 -0
  75. package/lib/device/cli/commands/account.mjs +586 -0
  76. package/lib/device/cli/commands/run.mjs +278 -0
  77. package/lib/device/cli/commands/service.mjs +221 -0
  78. package/lib/device/cli/commands/setup.mjs +410 -0
  79. package/lib/device/cli/commands/state.mjs +559 -0
  80. package/lib/device/cli/context.mjs +288 -0
  81. package/lib/{export-collections.mjs → device/export-collections.mjs} +1 -1
  82. package/lib/{migrate.mjs → device/migrate.mjs} +5 -19
  83. package/lib/device/remote.mjs +54 -0
  84. package/lib/{setup.mjs → device/setup.mjs} +3 -3
  85. package/lib/{update.mjs → device/update.mjs} +1 -1
  86. package/lib/{directory.mjs → gateway/directory.mjs} +1 -1
  87. package/lib/{front-core.mjs → gateway/front-core.mjs} +272 -49
  88. package/lib/{gateway-core.mjs → gateway/gateway-core.mjs} +3 -7
  89. package/lib/{httpsig.mjs → gateway/httpsig.mjs} +34 -3
  90. package/lib/pod/README.md +61 -0
  91. package/lib/pod/actor.mjs +96 -0
  92. package/lib/pod/collection.mjs +77 -0
  93. package/lib/pod/containers.mjs +136 -0
  94. package/lib/pod/discovery.mjs +46 -0
  95. package/lib/pod/featured.mjs +18 -0
  96. package/lib/pod/followers.mjs +24 -0
  97. package/lib/pod/following.mjs +10 -0
  98. package/lib/pod/http.mjs +74 -0
  99. package/lib/pod/inbox.mjs +144 -0
  100. package/lib/{links.mjs → pod/links.mjs} +1 -1
  101. package/lib/pod/media.mjs +27 -0
  102. package/lib/pod/notes.mjs +80 -0
  103. package/lib/pod/notifications.mjs +89 -0
  104. package/lib/pod/outbox.mjs +18 -0
  105. package/lib/pod/package.json +19 -0
  106. package/lib/pod/policy.mjs +58 -0
  107. package/lib/pod/private.mjs +19 -0
  108. package/lib/pod/root.mjs +63 -0
  109. package/lib/pod/state.mjs +39 -0
  110. package/lib/{remote.mjs → pod/transport.mjs} +217 -159
  111. package/lib/pod/urls.mjs +71 -0
  112. package/lib/{embed.mjs → server/embed.mjs} +149 -22
  113. package/lib/shared/links.mjs +9 -0
  114. package/lib/{ua.mjs → shared/ua.mjs} +1 -1
  115. package/package.json +1 -1
  116. package/run-agent.mjs +47 -64
  117. package/web/admin/actors.js +145 -0
  118. package/web/admin/bar.css +9 -13
  119. package/web/admin/bar.js +2 -2
  120. package/web/admin/client/client.js +1 -1
  121. package/web/admin/client/index.html +5 -6
  122. package/web/admin/common.js +23 -0
  123. package/web/admin/connections.js +112 -0
  124. package/web/admin/gateway.js +111 -0
  125. package/web/admin/group.js +258 -0
  126. package/web/admin/index.html +60 -66
  127. package/web/admin/record.js +378 -0
  128. package/web/admin/setup/index.html +3 -1
  129. package/web/admin/setup/setup.js +2 -13
  130. package/web/admin/tokens.css +56 -0
  131. package/web/admin/upkeep.js +170 -0
  132. package/web/app/README.md +77 -0
  133. package/web/app/admin-facade.mjs +549 -0
  134. package/web/app/agent.mjs +490 -0
  135. package/web/app/atproto-browser.mjs +86 -0
  136. package/web/app/boot.mjs +340 -0
  137. package/web/app/deliver-relay.mjs +72 -0
  138. package/web/app/dist/boot.js +34067 -0
  139. package/web/app/dist/boot.js.map +7 -0
  140. package/web/app/dist/sw.js +70014 -0
  141. package/web/app/dist/sw.js.map +7 -0
  142. package/web/app/fediacct-browser.mjs +146 -0
  143. package/web/app/idb-kv.mjs +35 -0
  144. package/web/app/index.html +203 -0
  145. package/web/app/index.html~ +152 -0
  146. package/web/app/keys-browser.mjs +46 -0
  147. package/web/app/keystore.mjs +99 -0
  148. package/web/app/oidc-session.mjs +189 -0
  149. package/web/app/pod-auth.mjs +248 -0
  150. package/web/app/pod-remote.mjs +84 -0
  151. package/web/app/shims/_globals.mjs +2 -0
  152. package/web/app/shims/fedify-sig.mjs +77 -0
  153. package/web/app/shims/node-crypto.mjs +113 -0
  154. package/web/app/shims/node-fs.mjs +17 -0
  155. package/web/app/shims/node-path.mjs +19 -0
  156. package/web/app/shims/node-url.mjs +7 -0
  157. package/web/app/shims/prelude.js +58 -0
  158. package/web/app/shims/safefetch.mjs +58 -0
  159. package/web/app/shims/shapes-text.mjs +8 -0
  160. package/web/app/shims/web-push.mjs +14 -0
  161. package/web/app/signup.mjs +248 -0
  162. package/web/app/site/_headers +9 -0
  163. package/web/app/site/_redirects +21 -0
  164. package/web/app/site/admin/actors.js +145 -0
  165. package/web/app/site/admin/bar.css +60 -0
  166. package/web/app/site/admin/bar.js +61 -0
  167. package/web/app/site/admin/client/client.js +66 -0
  168. package/web/app/site/admin/client/index.html +43 -0
  169. package/web/app/site/admin/common.js +23 -0
  170. package/web/app/site/admin/connections.js +112 -0
  171. package/web/app/site/admin/gateway.js +111 -0
  172. package/web/app/site/admin/group.js +258 -0
  173. package/web/app/site/admin/index.html +562 -0
  174. package/web/app/site/admin/record.js +378 -0
  175. package/web/app/site/admin/setup/index.html +192 -0
  176. package/web/app/site/admin/setup/setup.js +297 -0
  177. package/web/app/site/admin/tokens.css +56 -0
  178. package/web/app/site/admin/upkeep.js +170 -0
  179. package/web/app/site/admin/window.css +55 -0
  180. package/web/app/site/admin/window.js +136 -0
  181. package/web/app/site/app/404.html +40 -0
  182. package/web/app/site/app/_headers +2 -0
  183. package/web/app/site/app/apple-touch-icon.png +0 -0
  184. package/web/app/site/app/assets/ICONS-DIPJeU0C.js +2 -0
  185. package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +1 -0
  186. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +183 -0
  187. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +1 -0
  188. package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
  189. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +2 -0
  190. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +1 -0
  191. package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  192. package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
  193. package/web/app/site/app/assets/chunk-aKtaBQYM.js +1 -0
  194. package/web/app/site/app/assets/compose-CFEx7wb4.js +2 -0
  195. package/web/app/site/app/assets/compose-CFEx7wb4.js.map +1 -0
  196. package/web/app/site/app/assets/compose-ZJN5xSb4.js +35 -0
  197. package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +1 -0
  198. package/web/app/site/app/assets/debug.module-YFQgwnIr.js +52 -0
  199. package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +1 -0
  200. package/web/app/site/app/assets/dom-p1YcJ5Rw.js +2 -0
  201. package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +1 -0
  202. package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  203. package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  204. package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  205. package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  206. package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +1 -0
  207. package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +1 -0
  208. package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +1 -0
  209. package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +1 -0
  210. package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +1 -0
  211. package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +1 -0
  212. package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +1 -0
  213. package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +1 -0
  214. package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +1 -0
  215. package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +1 -0
  216. package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +1 -0
  217. package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +1 -0
  218. package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +1 -0
  219. package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +1 -0
  220. package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +1 -0
  221. package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +1 -0
  222. package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +1 -0
  223. package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +1 -0
  224. package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +1 -0
  225. package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +1 -0
  226. package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +1 -0
  227. package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +1 -0
  228. package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +1 -0
  229. package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +1 -0
  230. package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +1 -0
  231. package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +1 -0
  232. package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +1 -0
  233. package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +1 -0
  234. package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +1 -0
  235. package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +1 -0
  236. package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +1 -0
  237. package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +1 -0
  238. package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +1 -0
  239. package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +1 -0
  240. package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +1 -0
  241. package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +1 -0
  242. package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +1 -0
  243. package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +1 -0
  244. package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +1 -0
  245. package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +1 -0
  246. package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +1 -0
  247. package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +1 -0
  248. package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +1 -0
  249. package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +1 -0
  250. package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +1 -0
  251. package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +1 -0
  252. package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +1 -0
  253. package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +1 -0
  254. package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +1 -0
  255. package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +1 -0
  256. package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +1 -0
  257. package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +1 -0
  258. package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +1 -0
  259. package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +1 -0
  260. package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +1 -0
  261. package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +1 -0
  262. package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +1 -0
  263. package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +1 -0
  264. package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +1 -0
  265. package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +1 -0
  266. package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +1 -0
  267. package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +1 -0
  268. package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +1 -0
  269. package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +1 -0
  270. package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +1 -0
  271. package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +1 -0
  272. package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +1 -0
  273. package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +1 -0
  274. package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +1 -0
  275. package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +1 -0
  276. package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +1 -0
  277. package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +1 -0
  278. package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +1 -0
  279. package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +1 -0
  280. package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +1 -0
  281. package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +1 -0
  282. package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +1 -0
  283. package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +1 -0
  284. package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +1 -0
  285. package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +1 -0
  286. package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +1 -0
  287. package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +1 -0
  288. package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +1 -0
  289. package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +1 -0
  290. package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +1 -0
  291. package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +1 -0
  292. package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +1 -0
  293. package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +1 -0
  294. package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +1 -0
  295. package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +1 -0
  296. package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +1 -0
  297. package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +1 -0
  298. package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +1 -0
  299. package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +1 -0
  300. package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +1 -0
  301. package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +1 -0
  302. package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +1 -0
  303. package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +1 -0
  304. package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +1 -0
  305. package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +1 -0
  306. package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +1 -0
  307. package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +1 -0
  308. package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +1 -0
  309. package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +1 -0
  310. package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +1 -0
  311. package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +1 -0
  312. package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +1 -0
  313. package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +1 -0
  314. package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +1 -0
  315. package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +1 -0
  316. package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +1 -0
  317. package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +1 -0
  318. package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +1 -0
  319. package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +1 -0
  320. package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +1 -0
  321. package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +1 -0
  322. package/web/app/site/app/assets/instances-BNPptnn-.json +333 -0
  323. package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +1 -0
  324. package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +1 -0
  325. package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +1 -0
  326. package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +1 -0
  327. package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +1 -0
  328. package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +1 -0
  329. package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +1 -0
  330. package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +1 -0
  331. package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +1 -0
  332. package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +1 -0
  333. package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +1 -0
  334. package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +1 -0
  335. package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +1 -0
  336. package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +1 -0
  337. package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +1 -0
  338. package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +1 -0
  339. package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +1 -0
  340. package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +1 -0
  341. package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +1 -0
  342. package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +1 -0
  343. package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +1 -0
  344. package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +1 -0
  345. package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +1 -0
  346. package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +1 -0
  347. package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +1 -0
  348. package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +1 -0
  349. package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +1 -0
  350. package/web/app/site/app/assets/locales/tok-CfheRbCu.js +1 -0
  351. package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +1 -0
  352. package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +1 -0
  353. package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +1 -0
  354. package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +1 -0
  355. package/web/app/site/app/assets/main-BdqNbG-a.js +26 -0
  356. package/web/app/site/app/assets/main-BdqNbG-a.js.map +1 -0
  357. package/web/app/site/app/assets/mock-home-DGhR__He.js +2 -0
  358. package/web/app/site/app/assets/mock-home-DGhR__He.js.map +1 -0
  359. package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
  360. package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  361. package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  362. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +2 -0
  363. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +1 -0
  364. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +20 -0
  365. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +1 -0
  366. package/web/app/site/app/assets/style-B1iLPTPR.css +2 -0
  367. package/web/app/site/app/assets/temml-DuyIQMei.js +97 -0
  368. package/web/app/site/app/assets/temml-DuyIQMei.js.map +1 -0
  369. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +2 -0
  370. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +1 -0
  371. package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
  372. package/web/app/site/app/assets/year-in-posts-DJv_star.js +3 -0
  373. package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +1 -0
  374. package/web/app/site/app/compose/index.html +20 -0
  375. package/web/app/site/app/favicon.ico +0 -0
  376. package/web/app/site/app/index.html +73 -0
  377. package/web/app/site/app/logo-192.png +0 -0
  378. package/web/app/site/app/logo-512.png +0 -0
  379. package/web/app/site/app/logo-badge-72.png +0 -0
  380. package/web/app/site/app/logo-maskable-512.png +0 -0
  381. package/web/app/site/app/logo-monochrome-512.png +0 -0
  382. package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
  383. package/web/app/site/app/manifest.webmanifest +1 -0
  384. package/web/app/site/app/og-image-2.jpg +0 -0
  385. package/web/app/site/app/og-image.png +0 -0
  386. package/web/app/site/app/robots.txt +2 -0
  387. package/web/app/site/app/version.json +1 -0
  388. package/web/app/site/boot.js +34067 -0
  389. package/web/app/site/index.html +203 -0
  390. package/web/app/site/sw.js +70014 -0
  391. package/web/app/sw-src.mjs +220 -0
  392. package/web/front/#new-account.html# +43 -0
  393. package/web/front/admin.html +1 -81
  394. package/web/front/admin.js +85 -0
  395. package/web/front/new-account.html +1 -9
  396. package/web/front/new-account.js +13 -0
  397. package/web/front/run.html +1 -92
  398. package/web/front/run.js +107 -0
  399. package/lib/admin.mjs +0 -1913
  400. package/lib/intake.mjs +0 -1764
  401. package/lib/mastoapi.mjs +0 -1947
  402. package/lib/podrdf.mjs +0 -132
  403. package/lib/publisher.mjs +0 -1263
  404. package/web/admin/admin.js +0 -1114
  405. /package/lib/{oidc-auth.mjs → client/oidc-auth.mjs} +0 -0
  406. /package/lib/{webpush.mjs → client/webpush.mjs} +0 -0
  407. /package/lib/{bskyfeed.mjs → connections/bskyfeed.mjs} +0 -0
  408. /package/lib/{lease.mjs → core/lease.mjs} +0 -0
  409. /package/lib/{polls.mjs → core/polls.mjs} +0 -0
  410. /package/lib/{proof.mjs → core/proof.mjs} +0 -0
  411. /package/lib/{account.mjs → device/account.mjs} +0 -0
  412. /package/lib/{certs.mjs → device/certs.mjs} +0 -0
  413. /package/lib/{home.mjs → device/home.mjs} +0 -0
  414. /package/lib/{ports.mjs → device/ports.mjs} +0 -0
  415. /package/lib/{guard.mjs → shared/guard.mjs} +0 -0
  416. /package/lib/{safefetch.mjs → shared/safefetch.mjs} +0 -0
@@ -1,1114 +0,0 @@
1
- // admin.js — the record, as a page. Everything here is an existing admin
2
- // route; the CLI's own group commands are thin clients over the same ones.
3
- //
4
- // Two rules the routes enforce and this page respects: a write is a MERGE, so a
5
- // form that never mentions a field cannot delete it; and anything on the wire is
6
- // not real until the actor is republished, which POST /config does for itself.
7
- //
8
- // The UI password is deliberately not here — `fedipod passwd` sets it. It
9
- // only gates /oauth/authorize, so it does nothing for a loopback-only agent.
10
-
11
- const $ = (id) => document.getElementById(id);
12
- // The door this page is served behind: nothing when the agent runs on this
13
- // machine, `/app` when the identity is hosted by its own pod server. Read from
14
- // the page's own address, so one build serves both.
15
- const BASE = location.pathname.replace(/\/admin\/.*$/u, '');
16
- const api = async (path, init) => {
17
- const res = await fetch(BASE + path, init);
18
- return { status: res.status, json: await res.json().catch(() => null) };
19
- };
20
- const postJson = (path, body) => api(path, {
21
- method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body || {}),
22
- });
23
-
24
- let config = null;
25
- // Held, because render() moves it into a generated row and empties that row's
26
- // list on the next pass — after which getElementById would not find it again.
27
- const MODERATION = $('moderation');
28
- const STATUS = $('status-ctl');
29
- const STATUS_PICK = $('status-pick'); // held: getElementById can't see it mid-render
30
- const BSKY_CTL = $('bsky-ctl');
31
- const IDENT_CTL = $('ident-ctl');
32
- const FOLLOWS_CTL = $('follows-ctl');
33
- const FOLLOWS_PICK = $('follows-pick');
34
- const UPDATE_CTL = $('update-ctl');
35
- const UPDATE_WORD = $('update-word');
36
- const UPDATE_GO = $('update-go');
37
- const BSKY_CROSSPOST = $('bsky-crosspost'); // held: it rides BSKY_CTL into a generated row
38
-
39
- // #say and #fatal live in the accessibility tree from load and hide by being
40
- // empty (see the stylesheet). Unhiding a live region and filling it in the same
41
- // task is the classic way to get no announcement at all.
42
- function say(text, cls = 'ok') {
43
- const el = $('say');
44
- el.className = cls;
45
- // Clear first, then set on the next task: a live region that is handed the
46
- // same text it already holds announces nothing, so two identical messages in
47
- // a row ("nothing changed", "nothing changed") would be silent the second time.
48
- el.textContent = '';
49
- setTimeout(() => { el.textContent = text; }, 30);
50
- }
51
-
52
- // Every write goes through here, so nothing silently half-succeeds.
53
- async function write(path, body, done) {
54
- const { status, json } = await postJson(path, body);
55
- if (status >= 400) { say(json?.error || `refused (HTTP ${status})`, 'err'); return null; }
56
- say(done);
57
- return json;
58
- }
59
-
60
- UPDATE_GO.onclick = async () => {
61
- UPDATE_GO.disabled = true;
62
- const r = await write('/update', {}, 'updating — the agents restart when it finishes; reload in a moment');
63
- if (!r) UPDATE_GO.disabled = false;
64
- };
65
-
66
- async function load() {
67
- const { status, json } = await api('/config');
68
- if (status === 409) {
69
- $('fatal').textContent = 'This agent has no identity yet.';
70
- const a = document.createElement('a');
71
- a.href = '/admin/setup/';
72
- a.textContent = ' Set it up.';
73
- $('fatal').appendChild(a);
74
- return;
75
- }
76
- if (status !== 200 || !json) {
77
- $('fatal').textContent = json?.error || `could not read the record (HTTP ${status})`;
78
- return;
79
- }
80
- config = json;
81
- render();
82
- if (new URLSearchParams(location.search).has('new')) openNewActor();
83
- }
84
-
85
- // Every disclosure on the page — a form, the log, a lifecycle confirmation —
86
- // is a panel declared inside the floating window and shown by window.js. It
87
- // used to be an accordion at the foot of the page, which pushed whatever you
88
- // were reading out from under you and could only ever show one thing.
89
- //
90
- // The reset lives here rather than in window.js: the window knows how to show a
91
- // panel, not what any of them mean.
92
- function resetConfirm() {
93
- pending = null;
94
- for (const k of Object.keys(LIFECYCLE)) $(`warn-${k}`)?.hidden !== undefined && ($(`warn-${k}`).hidden = true);
95
- $('confirm-handle').value = '';
96
- $('confirm-handle-move').value = '';
97
- $('move-target').value = '';
98
- $('state-path').value = '';
99
- $('state-url').value = '';
100
- document.querySelector('input[name=stateWhere][value=path]').checked = true;
101
- stateRows();
102
- }
103
- function closePanels(keep = null) {
104
- if (!keep) solWindow.close();
105
- if (keep !== 'output') outputSource = null;
106
- if (keep !== 'confirm-form') resetConfirm();
107
- }
108
- // Closing it by the ✕ or by Escape has to clear the same state a Cancel does.
109
- document.getElementById('win').addEventListener('win:closed', () => {
110
- outputSource = null;
111
- resetConfirm();
112
- $('bsky-password').value = ''; // never leave a secret in a closed panel
113
- });
114
-
115
-
116
- // One row per identity connected elsewhere, indented under the row that adds
117
- // them. Each carries its own way out: disconnecting one has nothing to do with
118
- // the others.
119
- function identityRows() {
120
- const out = [];
121
- const row = (label, ...nodes) => {
122
- const dt = document.createElement('dt');
123
- dt.className = 'under';
124
- dt.textContent = label;
125
- const dd = document.createElement('dd');
126
- dd.append(...nodes);
127
- out.push([dt, dd]);
128
- };
129
- if (config.atproto?.connected) {
130
- const a = document.createElement('a');
131
- a.href = `https://bsky.app/profile/${config.atproto.handle}`;
132
- a.textContent = `@${config.atproto.handle}`;
133
- a.target = '_blank';
134
- a.rel = 'noopener';
135
- a.title = ` Open this account's Bluesky page in a new tab`;
136
- BSKY_CTL.hidden = false;
137
- BSKY_CROSSPOST.value = config.atproto.crossPost ? 'on' : 'off';
138
- row('Bluesky', a, ' ', BSKY_CTL);
139
- } else {
140
- BSKY_CTL.hidden = true;
141
- }
142
- for (const acct of config.fediAccounts || []) {
143
- const name = document.createElement('span');
144
- name.textContent = acct.needsReconnect ? `${acct.handle} — sign in again` : acct.handle;
145
- if (acct.needsReconnect) name.className = 'warn';
146
- const off = document.createElement('button');
147
- off.type = 'button';
148
- off.className = 'inline danger';
149
- off.textContent = 'Disconnect';
150
- off.title = ` Stop reading ${acct.handle} and remove its stored token`;
151
- off.addEventListener('click', async () => {
152
- if (await write('/fediacct/disconnect', { id: acct.id }, `${acct.handle} disconnected`)) await load();
153
- });
154
- row(acct.host, name, ' ', off);
155
- }
156
- return out;
157
- }
158
-
159
- function render() {
160
- // The bar names the actor, the same way on every page. Only the tab title is
161
- // this page's own business.
162
- document.title = `FediPod — ${config.handle}`;
163
- const facts = $('facts');
164
- facts.textContent = '';
165
- // What this actor IS comes first. The handle is already in the address beside
166
- // the heading; the pod, the issuer and the actor URL all read off the WebID;
167
- // and where the private half sits is not something you act on from here.
168
- const origins = config.origins || {};
169
- // Two of these are addresses of things you can open, so they are links. The
170
- // fediverse one goes to this actor's own page in the client, same origin, so
171
- // it stays in the tab. The Solid one leaves for the pod, so it does not.
172
- const rows = [
173
- ['kind', config.kind ? config.kind[0].toUpperCase() + config.kind.slice(1) : config.kind],
174
- // The value is the select itself — what it shows IS the state, and
175
- // changing the word is the whole action, like the moderation controls.
176
- ['status', 'ctl'],
177
- ['Fediverse identity', config.address || `@${config.handle} — no resolvable address`,
178
- config.accountId && config.address ? { href: `/admin/client/#/a/${config.accountId}` } : null],
179
- ['Solid identity', config.webId, config.remotePod ? { href: config.remotePod, blank: true } : null],
180
- ['Other identities', 'ctl'],
181
- ['local store', config.home],
182
- // The address you actually open, not the bare number — the named origin when
183
- // there is one, since that is what the client and the OAuth redirect use.
184
- ['local host', (origins.named || origins.loopback || `http://localhost:${config.port}`)
185
- .replace(/\/$/, '')],
186
- ];
187
- if (config.version || config.update || config.pendingUpgrade?.length) rows.push(['software', 'ctl']);
188
- rows.push(['gateway', 'ctl']);
189
- if (config.quiescedAt) rows.push(['parked since', config.quiescedAt]);
190
- if (config.movedTo) rows.push(['moved to', config.movedTo]);
191
- // A person gates followers here; a group's gate is the joins control on its
192
- // kind row, so the row would be a second switch for the same thing.
193
- if (config.kind !== 'group') rows.splice(2, 0, ['new followers', 'ctl']);
194
- // Rows that belong under the one being written, appended after it.
195
- let kids = null;
196
- for (const [k, v, link] of rows) {
197
- if (!v) continue;
198
- const dt = document.createElement('dt');
199
- dt.textContent = k;
200
- const dd = document.createElement('dd');
201
- if (link) {
202
- const a = document.createElement('a');
203
- a.href = link.href;
204
- a.textContent = v;
205
- if (link.blank) {
206
- a.target = '_blank';
207
- // noopener because the pod is another origin: without it the page we
208
- // open gets a handle on this one through window.opener.
209
- a.rel = 'noopener';
210
- a.title = ` Open ${link.href} in a new tab`;
211
- } else {
212
- a.title = ` This actor's page in our client`;
213
- }
214
- dd.appendChild(a);
215
- } else {
216
- dd.textContent = v;
217
- }
218
- // What a group can be moderated into is a property of BEING a group, so the
219
- // controls sit on the row that says so. Moved rather than built here: the
220
- // page still declares them, and `facts` is emptied on every render.
221
- if (k === 'kind' && config.kind === 'group') dd.append(MODERATION);
222
- if (k === 'status') {
223
- dd.textContent = '';
224
- dd.append(STATUS);
225
- STATUS.hidden = false;
226
- renderStatus();
227
- }
228
- if (k === 'new followers') {
229
- dd.textContent = '';
230
- dd.append(FOLLOWS_CTL);
231
- FOLLOWS_CTL.hidden = false;
232
- FOLLOWS_PICK.value = config.autoAcceptFollows ? 'auto' : 'approve';
233
- }
234
- if (k === 'gateway') {
235
- dd.textContent = '';
236
- dd.append(GATEWAY_CTL);
237
- refreshGateway();
238
- }
239
- if (k === 'software') {
240
- dd.textContent = '';
241
- dd.append(UPDATE_CTL);
242
- UPDATE_CTL.hidden = false;
243
- const u = config.update;
244
- // The version this agent is running, never the one sitting in the
245
- // checkout — saying otherwise would name a version nobody is serving.
246
- const running = config.version || u?.current || null;
247
- const words = [];
248
- if (running) words.push(u?.available ? `FediPod ${running} — ${u.latest} available` : `FediPod ${running}`);
249
- if (config.versionOnDisk && running && config.versionOnDisk !== running)
250
- words.push(`${config.versionOnDisk} is on disk — restart to run it`);
251
- if (config.pendingUpgrade?.length) words.push('older data layout — run `fedipod upgrade` in a terminal');
252
- UPDATE_WORD.textContent = words.join('; ');
253
- UPDATE_GO.hidden = !u?.available;
254
- }
255
- // The way to add one. Each account already connected is a row of its own
256
- // underneath, so this row stays the action and never becomes a list.
257
- if (k === 'Other identities') {
258
- dd.textContent = '';
259
- dd.append(IDENT_CTL);
260
- IDENT_CTL.hidden = false;
261
- kids = identityRows();
262
- }
263
- facts.append(dt, dd);
264
- if (kids) {
265
- for (const [ckt, ckd] of kids) facts.append(ckt, ckd);
266
- kids = null;
267
- }
268
- }
269
- if (!config.address) {
270
- const p = document.createElement('p');
271
- p.className = 'warn';
272
- p.textContent = `${config.remotePod} is not the root of its own host, so this actor cannot be `
273
- + 'discovered as a handle by other servers. Posting and reading still work.';
274
- $('pane-identity').appendChild(p);
275
- }
276
-
277
- // pane-others carries the create control too, so it appears even when this is
278
- // the only actor and even if /profiles cannot be read.
279
- for (const id of ['pane-others', 'pane-identity', 'rail']) $(id).hidden = false;
280
- renderAliases();
281
- renderOthers();
282
- renderInbox();
283
- if (config.kind === 'group') {
284
- // Its lists have no bound, so this page scrolls — see body.group in the CSS.
285
- document.body.classList.add('group');
286
- $('pane-group').hidden = false;
287
- MODERATION.hidden = false; // only a group has any
288
- renderGroupToggles();
289
- refreshGroup();
290
- } else {
291
- // A person has a follow-request queue too now: nothing binds an inbound
292
- // Follow to the actor it names, so one that cannot be verified waits here
293
- // rather than being accepted on the strength of who it claims to be.
294
- refreshRequests();
295
- }
296
- }
297
-
298
- // ---- the actors running on this machine ----
299
- // Somewhere to go, and nothing else. A stopped actor is left out — the link
300
- // would land on nothing. The one you are on keeps its place in the row but
301
- // offers only `app`, because its admin page is the page you are reading.
302
-
303
- let actors = [];
304
-
305
- async function renderOthers() {
306
- const { json } = await api('/profiles');
307
- actors = json?.identities || [];
308
- const sel = $('actor-pick');
309
- sel.textContent = '';
310
- // The fediverse address, which is what the actor IS to everyone else, and the
311
- // only form that stays distinct: two identities can share a local handle, but
312
- // never a handle AND a pod. A stopped one has no address to report — nothing
313
- // answered — so it falls back to the name it is filed under.
314
- const label = (r) => (r.address || r.handle || r.name)
315
- + (r.mode && r.mode !== 'active' ? ` (${r.mode})` : '') + (r.mode ? '' : ' (stopped)');
316
- const seen = {};
317
- for (const r of actors) seen[label(r)] = (seen[label(r)] || 0) + 1;
318
-
319
- for (const [i, r] of actors.entries()) {
320
- const o = document.createElement('option');
321
- o.value = String(i);
322
- o.selected = !!r.current; // it opens showing where you already are
323
- o.textContent = label(r) + (seen[label(r)] > 1 && r.port ? ` :${r.port}` : '');
324
- sel.appendChild(o);
325
- }
326
- // The way to a NEW actor rides the same dropdown as the existing ones.
327
- const add = document.createElement('option');
328
- add.value = '__add';
329
- add.textContent = '+ add a new account…';
330
- sel.appendChild(add);
331
- }
332
-
333
- // Going to an actor commits a navigation and, for a stopped one, a POST that
334
- // boots its pod — so it must run once per choice. Pressing Enter on the select
335
- // fires BOTH a keydown (handled below) and a native `change`, so without a
336
- // guard a keyboard commit would start the actor twice.
337
- let goingToActor = false;
338
- const goToActor = async () => {
339
- if (goingToActor) return;
340
- if ($('actor-pick').value === '__add') { renderOthers(); openNewActor(); return; }
341
- const r = actors[Number($('actor-pick').value)];
342
- if (!r || r.current) return;
343
- goingToActor = true;
344
- try {
345
- if (r.mode) { location.href = r.admin; return; }
346
- $('actor-pick').disabled = true;
347
- say(`starting ${r.name}`);
348
- const started = await write('/start-actor', { name: r.name }, `${r.name} is up`);
349
- $('actor-pick').disabled = false;
350
- if (started?.url) location.href = `${started.url}admin/`;
351
- else renderOthers(); // put the picker back on the current actor
352
- } finally {
353
- goingToActor = false;
354
- }
355
- };
356
- // Choosing one goes to its record, as it always has.
357
- //
358
- // Arrowing is the one case that must not: on a closed select every Arrow
359
- // keypress fires `change`, so a keyboard user browsing the list would be
360
- // carried off to the first actor they passed — and for a stopped one that
361
- // boots its pod. An arrow key arms a flag and the change it causes is ignored;
362
- // Enter commits. A mouse never sets the flag, so clicking behaves as before.
363
- let arrowing = false;
364
- $('actor-pick').addEventListener('keydown', (ev) => {
365
- if (['ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown'].includes(ev.key)) arrowing = true;
366
- if (ev.key === 'Enter') { arrowing = false; goToActor(); }
367
- });
368
- $('actor-pick').addEventListener('pointerdown', () => { arrowing = false; });
369
- $('actor-pick').addEventListener('change', () => {
370
- if (!arrowing) goToActor();
371
- arrowing = false;
372
- });
373
-
374
- // Setting up a new actor asks for exactly what this page cannot already tell
375
- // it: the pod and the account behind it, and its permanent name. Display name,
376
- // bio and pictures are the client's job now — Phanpy's profile editor writes
377
- // them through /api/v1/accounts/update_credentials. Where the private data goes
378
- // is not a question either: it lands beside the credential, and `state --to`
379
- // moves it afterwards.
380
-
381
- const picked = (name) => document.querySelector(`input[name=${name}]:checked`)?.value;
382
-
383
- function newActorRows() {
384
- const mode = picked('newMode');
385
- $('new-row-podname').hidden = mode !== 'new';
386
- $('new-row-pod').hidden = mode !== 'existing';
387
- }
388
- for (const el of document.querySelectorAll('input[name=newMode]')) {
389
- el.addEventListener('change', newActorRows);
390
- }
391
-
392
- // While the form is up the page IS the new actor's setup — so the record of
393
- // the actor you came from goes away, list included. Which panes were showing
394
- // is remembered rather than recomputed: whether Group and Inbox belong is a
395
- // decision render() already made.
396
- const RECORD_PANES = ['pane-identity', 'pane-group', 'pane-inbox', 'rail'];
397
- let putBack = [];
398
-
399
- function showNewActor(on) {
400
- if (on) closePanels(); // nothing of the old actor left open behind it
401
- document.body.classList.toggle('adding', on); // see body.adding in the CSS
402
- $('new-actor-form').hidden = !on;
403
- if (on) {
404
- putBack = RECORD_PANES.filter(id => !$(id).hidden);
405
- for (const id of putBack) $(id).hidden = true;
406
- } else {
407
- for (const id of putBack) $(id).hidden = false;
408
- putBack = [];
409
- }
410
- }
411
-
412
- // `add a new account` is the last item of the Local Actors dropdown; choosing
413
- // it opens the form in place. Arriving with ?new does the same.
414
- function openNewActor() {
415
- showNewActor(true);
416
- newActorRows();
417
- $('new-handle').focus();
418
- }
419
- $('new-actor-cancel').addEventListener('click', () => { showNewActor(false); say('nothing changed'); });
420
-
421
- $('new-actor-form').addEventListener('submit', async (ev) => {
422
- ev.preventDefault();
423
- const handle = $('new-handle').value.trim();
424
- const answers = {
425
- handle,
426
- kind: picked('newKind'),
427
- mode: picked('newMode'),
428
- issuer: $('new-issuer').value.trim(),
429
- email: $('new-email').value.trim(),
430
- password: $('new-password').value,
431
- };
432
- if (answers.mode === 'new') answers.podName = $('new-podname').value.trim() || handle;
433
- else answers.pod = $('new-pod').value.trim();
434
- $('new-actor-go').disabled = true;
435
- say(`setting up ${handle || 'the new actor'} — this takes a while`);
436
- const r = await write('/new-actor', answers, `setting up ${handle}`);
437
- $('new-actor-go').disabled = false;
438
- if (r?.url) location.href = r.url; // its own page, where the progress is
439
- });
440
-
441
- // ---- bluesky ----
442
-
443
- // The connect form lives in the floating window, like every other form here.
444
- // The inbound transfer panel: the aliases live here now, not in the facts.
445
- $('do-transfer-in').addEventListener('click', () => {
446
- solWindow.show('transfer-in-form', 'Transfer an account here');
447
- });
448
-
449
- // The rail: one click hides the actions; on a phone it starts hidden.
450
- function setRail(closed) {
451
- $('rail').classList.toggle('closed', closed);
452
- const t = $('rail-toggle');
453
- t.setAttribute('aria-expanded', String(!closed));
454
- t.querySelector('.rail-arrow').textContent = closed ? '»' : '«';
455
- t.title = closed ? ' Show the action panel' : ' Hide the action panel';
456
- }
457
- $('rail-toggle').addEventListener('click', () => setRail(!$('rail').classList.contains('closed')));
458
- if (matchMedia('(max-width: 47rem)').matches) setRail(true);
459
-
460
- // One way in for both networks, because from the record they are the same
461
- // thing: an account elsewhere whose timeline joins the one you read here.
462
- $('ident-open').addEventListener('click', () => {
463
- closePanels();
464
- solWindow.show('ident-form', 'Connect an identity');
465
- });
466
- $('ident-pick-fedi').addEventListener('click', () => {
467
- solWindow.show('fediacct-form', 'Connect a Fediverse account');
468
- });
469
- $('ident-pick-bsky').addEventListener('click', () => {
470
- solWindow.show('bsky-form', 'Connect a Bluesky account');
471
- });
472
-
473
- let bskyBusy = false;
474
- $('bsky-form').addEventListener('submit', async (ev) => {
475
- ev.preventDefault();
476
- if (bskyBusy) return;
477
- bskyBusy = true;
478
- try {
479
- const r = await write('/atproto/connect', {
480
- service: $('bsky-service').value.trim(),
481
- identifier: $('bsky-identifier').value.trim(),
482
- appPassword: $('bsky-password').value,
483
- }, 'bluesky account connected');
484
- if (r) {
485
- $('bsky-password').value = '';
486
- closePanels();
487
- await load();
488
- }
489
- } finally { bskyBusy = false; }
490
- });
491
- // The sign-in happens at the other server, so this hands the browser over
492
- // rather than taking a password. What comes back lands on /fediacct/callback,
493
- // which this machine answers and nowhere else does.
494
- let fediBusy = false;
495
- $('fediacct-form').addEventListener('submit', async (ev) => {
496
- ev.preventDefault();
497
- if (fediBusy) return;
498
- fediBusy = true;
499
- try {
500
- const r = await write('/fediacct/connect', { host: $('fediacct-host').value.trim() }, '');
501
- if (r?.authorize) location.href = r.authorize;
502
- } finally { fediBusy = false; }
503
- });
504
-
505
- $('bsky-disconnect').addEventListener('click', async () => {
506
- if (bskyBusy) return;
507
- bskyBusy = true;
508
- try {
509
- if (await write('/atproto/disconnect', {}, 'bluesky account disconnected')) await load();
510
- } finally { bskyBusy = false; }
511
- });
512
- // Adding resolves the old account on the agent side, so what lands in
513
- // alsoKnownAs is its canonical id, not the string typed here.
514
- // The accounts elsewhere this one may receive a Move from — the chips in the
515
- // Transfer-an-account-here panel. Each entry is removable, behind a second
516
- // click: servers still retrying a Move check the list, so removal is not a
517
- // tidy-up.
518
- function renderAliases() {
519
- const box = $('alias-chips');
520
- box.textContent = '';
521
- for (const alias of config.aliases || []) {
522
- let armed = false;
523
- const rm = document.createElement('button');
524
- rm.type = 'button';
525
- rm.className = 'inline danger';
526
- rm.textContent = '✕';
527
- rm.title = ' Remove this alias';
528
- rm.addEventListener('click', async () => {
529
- if (!armed) {
530
- armed = true;
531
- rm.textContent = 'confirm ✕';
532
- rm.title = ' Servers still retrying the Move check this alias — click again to remove it anyway';
533
- return;
534
- }
535
- const r = await write('/alias', { remove: alias, confirm: true }, 'alias removed and the actor republished');
536
- if (r) { config.aliases = r.aliases; render(); }
537
- });
538
- const chip = document.createElement('span');
539
- chip.append(alias, ' ', rm);
540
- box.append(chip, ' ');
541
- }
542
- }
543
-
544
- let aliasBusy = false;
545
- $('alias-add').addEventListener('click', async () => {
546
- if (aliasBusy) return;
547
- const v = $('alias-input').value.trim();
548
- if (!v) { say('enter the old account first — @you@old.server', 'err'); return; }
549
- aliasBusy = true;
550
- try {
551
- const r = await write('/alias', { add: v }, 'alias added and the actor republished');
552
- if (r) { $('alias-input').value = ''; config.aliases = r.aliases; render(); }
553
- } finally { aliasBusy = false; }
554
- });
555
-
556
- // Silent on success — the select already shows the new state.
557
- const setCrossPost = async (on) => {
558
- if (bskyBusy || on === !!config.atproto?.crossPost) return;
559
- bskyBusy = true;
560
- try {
561
- if (await write('/atproto', { crossPost: on }, '')) config.atproto.crossPost = on;
562
- BSKY_CROSSPOST.value = config.atproto?.crossPost ? 'on' : 'off';
563
- } finally { bskyBusy = false; }
564
- };
565
-
566
- // ---- group ----
567
-
568
- // Each control shows the setting it would change, so what it displays IS the
569
- // current state — no separate sentence reporting it.
570
- const MOD = { on: 'moderated', off: 'unmoderated' };
571
- function renderGroupToggles() {
572
- $('joins-mod').value = config.approveJoins ? MOD.on : MOD.off;
573
- $('review-mod').value = config.review ? MOD.on : MOD.off;
574
- }
575
- function renderStatus() {
576
- STATUS_PICK.value = config.quiescedAt ? 'parked' : 'active';
577
- }
578
-
579
- // Parking is reversible — the follow graph is saved first and going back to
580
- // active re-sends a Follow to everyone in it — so the word applies directly,
581
- // like the moderation controls. The result line says what actually happened.
582
- let statusBusy = false;
583
- const setStatus = async (parked) => {
584
- if (statusBusy || parked === !!config.quiescedAt) return;
585
- statusBusy = true;
586
- try {
587
- const r = await write(parked ? '/park' : '/revive', {},
588
- parked ? 'parking — unfollowing everyone and closing the inbox' : 'reviving — re-following the saved graph');
589
- if (r) {
590
- say(parked
591
- ? `parked ${r.quiescedAt}: unfollowed ${r.unfollowed}/${r.following}, inbox closed`
592
- : `revived: inbox open, ${r.refollowed}/${r.of} follow(s) re-sent`);
593
- await load();
594
- }
595
- renderStatus();
596
- } finally { statusBusy = false; }
597
- };
598
-
599
- // One write at a time, because committing an open picker with Enter fires our
600
- // keydown AND the browser's change — the second call must find the first still
601
- // holding the flag, or config, not yet updated by the awaited write, lets a
602
- // duplicate POST through.
603
- let modBusy = false;
604
- const setJoins = async (approve) => {
605
- // Picking the value already in force is not a change: no write, and no
606
- // republish of the actor for a setting that did not move.
607
- if (modBusy || approve === !!config.approveJoins) return;
608
- modBusy = true;
609
- try {
610
- if (await write('/joins', { approve }, approve ? 'join requests will wait' : 'anyone may join')) {
611
- config.approveJoins = approve;
612
- refreshGroup();
613
- }
614
- // Both ways: refused, the control must not keep the value it did not get;
615
- // granted, a blur meanwhile may have repainted it to the old state.
616
- renderGroupToggles();
617
- } finally { modBusy = false; }
618
- };
619
- const setReview = async (on) => {
620
- if (modBusy || on === !!config.review) return;
621
- modBusy = true;
622
- try {
623
- if (await write('/review', { on }, on ? 'posts will be held' : 'posts will be carried at once')) {
624
- config.review = on;
625
- refreshGroup();
626
- }
627
- renderGroupToggles();
628
- } finally { modBusy = false; }
629
- };
630
-
631
- // Arrowing a closed select fires `change` on every keypress, so a keyboard user
632
- // running down the list would apply each value they passed — here that silently
633
- // opens or closes the group. An arrow arms a flag and the change it causes is
634
- // ignored; Enter commits. A mouse never sets the flag, so clicking is unchanged.
635
- // Same guard as the actor picker above, for the same reason.
636
- function onPick(el, apply, repaint = renderGroupToggles) {
637
- let arrowing = false;
638
- el.addEventListener('keydown', (ev) => {
639
- if (['ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown'].includes(ev.key)) arrowing = true;
640
- if (ev.key === 'Enter') { arrowing = false; apply(el.value); }
641
- // Escape abandons the armed value; what shows must return to what is.
642
- if (ev.key === 'Escape') { arrowing = false; repaint(); }
643
- });
644
- // Leaving abandons too. Unconditional, because the swallow above already
645
- // consumed the flag — the select may show a state that was never applied,
646
- // and repainting from config costs nothing when it does match.
647
- el.addEventListener('blur', () => { arrowing = false; repaint(); });
648
- el.addEventListener('pointerdown', () => { arrowing = false; });
649
- el.addEventListener('change', () => {
650
- if (!arrowing) apply(el.value);
651
- arrowing = false;
652
- });
653
- }
654
-
655
- onPick($('joins-mod'), (v) => setJoins(v === MOD.on));
656
- onPick($('review-mod'), (v) => setReview(v === MOD.on));
657
- onPick(STATUS_PICK, (v) => setStatus(v === 'parked'), renderStatus);
658
- onPick(BSKY_CROSSPOST, (v) => setCrossPost(v === 'on'),
659
- () => { BSKY_CROSSPOST.value = config?.atproto?.crossPost ? 'on' : 'off'; });
660
-
661
- // A person's follower gate. Automatic is what a migration wave needs: every
662
- // follower's server re-follows at once, and the waiting queue caps at 500.
663
- let followsBusy = false;
664
- const setAutoAccept = async (auto) => {
665
- if (followsBusy || auto === !!config.autoAcceptFollows) return;
666
- followsBusy = true;
667
- try {
668
- if (await write('/config', { autoAcceptFollows: auto },
669
- auto ? 'new followers are accepted automatically' : 'new followers will wait for you')) {
670
- config.autoAcceptFollows = auto;
671
- }
672
- FOLLOWS_PICK.value = config.autoAcceptFollows ? 'auto' : 'approve';
673
- } finally { followsBusy = false; }
674
- };
675
- onPick(FOLLOWS_PICK, (v) => setAutoAccept(v === 'auto'),
676
- () => { FOLLOWS_PICK.value = config?.autoAcceptFollows ? 'auto' : 'approve'; });
677
-
678
- // One row: what it is, then what can be done to it.
679
- function row(text, sub, actions) {
680
- const li = document.createElement('li');
681
- const span = document.createElement('span');
682
- span.textContent = text;
683
- if (sub) {
684
- const small = document.createElement('div');
685
- small.className = 'muted';
686
- small.textContent = sub;
687
- span.appendChild(small);
688
- }
689
- li.appendChild(span);
690
- for (const [label, run, hint] of actions) {
691
- const b = document.createElement('button');
692
- b.className = 'inline'; // sized like the page's other buttons
693
- b.textContent = label;
694
- // A list of fifteen buttons all reading "Mute" tells a screen-reader user
695
- // browsing by button nothing; name each one for the row it acts on.
696
- b.setAttribute('aria-label', `${label} ${text}`);
697
- // Three leading spaces: the tooltip appears under the pointer, and without
698
- // them the first word sits behind the cursor.
699
- if (hint) b.title = ` ${hint}`;
700
- // Where focus should land afterwards. `b.disabled = true` blurs it to
701
- // <body> at once, and refreshGroup() then wipes and rebuilds the list, so
702
- // without this every mute, eject, admit or refuse dropped a keyboard user
703
- // back to the top of the page.
704
- b.addEventListener('click', async () => {
705
- const list = li.parentNode;
706
- focusAfterRefresh = { listId: list?.id, index: [...(list?.children || [])].indexOf(li), label };
707
- b.disabled = true;
708
- await run();
709
- refreshGroup();
710
- });
711
- li.appendChild(b);
712
- }
713
- return li;
714
- }
715
-
716
- // Set by a row action, consumed by the fill() that replaces that row.
717
- let focusAfterRefresh = null;
718
-
719
- function fill(listId, countId, items, make) {
720
- const ul = $(listId);
721
- ul.textContent = '';
722
- $(countId).textContent = items.length ? `(${items.length})` : '(none)';
723
- for (const it of items) ul.appendChild(make(it));
724
- if (!focusAfterRefresh || focusAfterRefresh.listId !== listId) return;
725
- const { index, label } = focusAfterRefresh;
726
- focusAfterRefresh = null;
727
- // The same action on the row that took this one's place, or the row before it
728
- // when the list just got shorter. Nothing left to land on is the one case
729
- // where the heading is the honest answer.
730
- const rows = [...ul.children];
731
- const li = rows[Math.min(index, rows.length - 1)];
732
- const same = li && [...li.querySelectorAll('button')].find(x => x.textContent === label);
733
- const heading = $(countId).closest('h3');
734
- // A bare <h3> is not focusable, so the "nothing left" fallback would silently
735
- // drop focus to <body> — give it a programmatic-only tab stop first.
736
- if (heading && !heading.hasAttribute('tabindex')) heading.setAttribute('tabindex', '-1');
737
- (same || li?.querySelector('button') || heading)?.focus?.();
738
- }
739
-
740
- // The request rows, shared: a group calls them joins, a person calls them
741
- // follows, and the two do exactly the same thing to exactly the same queue.
742
- function fillRequests(list) {
743
- fill('requests', 'requests-count', list, (r) => row(r.actor, r.at, [
744
- ['Accept', () => write('/admit', { actor: r.actor }, 'accepted'), 'Let them follow you'],
745
- ['Refuse', () => write('/refuse', { actor: r.actor }, 'refused'), 'Turn this down; they may ask again'],
746
- ]));
747
- }
748
-
749
- // The whole queue in one action — the shape a migration wave arrives in.
750
- $('admit-all').addEventListener('click', async () => {
751
- if (await write('/admit', { all: true }, 'accepted everyone waiting')) {
752
- if (config.kind === 'group') refreshGroup(); else refreshRequests();
753
- }
754
- });
755
-
756
- // A person's whole group pane is this one block, and only when it has something
757
- // in it — an empty heading promising a list is what the group console avoids too.
758
- async function refreshRequests() {
759
- const { json } = await api('/requests');
760
- const list = json?.requests || [];
761
- $('pane-group').hidden = !list.length;
762
- $('block-requests').hidden = !list.length;
763
- if (list.length) fillRequests(list);
764
- }
765
-
766
- // What a queued moderation would do, in the words the buttons beside it use.
767
- function describeModeration(e) {
768
- const o = e.activity?.object;
769
- const id = typeof o === 'string' ? o : o?.id || '';
770
- if (e.type === 'Block') return `block ${id}`;
771
- if (e.type === 'Remove') return `eject ${id}`;
772
- if (e.type === 'Delete') return `take down ${id}`;
773
- if (e.type === 'Add') return `add ${id}`;
774
- return id || 'no target named';
775
- }
776
-
777
- async function refreshGroup() {
778
- const [members, requests, pending, announced, modqueue] = await Promise.all(
779
- ['/members', '/requests', '/pending', '/announced', '/modqueue']
780
- .map(p => api(p).then(r => r.json || {})));
781
-
782
- fill('requests', 'requests-count', requests.requests || [], (r) => row(r.actor, r.at, [
783
- ['Admit', () => write('/admit', { actor: r.actor }, 'admitted'), 'Let them in — they become a member'],
784
- ['Refuse', () => write('/refuse', { actor: r.actor }, 'refused'), 'Turn this request down; they may ask again'],
785
- ]));
786
-
787
- fill('pending', 'pending-count', pending.pending || [], (p) => row(p.noteId, `${p.actor} · ${p.at}`, [
788
- ['Carry it', () => write('/approve', { noteId: p.noteId }, 'carried'), 'Announce this post to every member'],
789
- ['Decline', () => write('/decline', { noteId: p.noteId }, 'declined'), 'Do not carry it — the post stays up on its author\u2019s pod'],
790
- ]));
791
-
792
- // Moderation another server asked for: apply it, or turn it down. The route
793
- // answers with the list itself, not an object wrapping one.
794
- const modq = Array.isArray(modqueue) ? modqueue : (modqueue.queue || []);
795
- fill('modqueue', 'modqueue-count', modq, (e) => row(
796
- `${e.type} — ${describeModeration(e)}`, `asked by ${e.moderator} · ${e.at}`, [
797
- ['Carry it out', () => write('/modqueue', { id: e.id, action: 'apply' }, 'applied'),
798
- 'Do what the moderator asked, as if you had asked it'],
799
- ['Turn it down', () => write('/modqueue', { id: e.id, action: 'dismiss' }, 'dismissed'),
800
- 'Leave things as they are and drop the request'],
801
- ]));
802
-
803
- // A queue nothing can arrive in is not an empty list, it is a list that does
804
- // not apply — so the setting has to be on before the heading appears at all.
805
- // This one has no setting: it fills only when a moderator has asked.
806
- $('block-requests').hidden = !(config.approveJoins && (requests.requests || []).length);
807
- $('block-pending').hidden = !(config.review && (pending.pending || []).length);
808
- $('block-modqueue').hidden = !modq.length;
809
-
810
- fill('members', 'members-count', members.members || [], (m) => row(
811
- m.handle || m.actor, m.muted ? 'muted — their posts are not carried' : null,
812
- [
813
- m.muted
814
- ? ['Unmute', () => write('/unmute', { actor: m.actor }, 'unmuted'), 'Carry their posts again']
815
- : ['Mute', () => write('/mute', { actor: m.actor }, 'muted'), 'Stop carrying their posts; they stay a member'],
816
- ['Eject', () => write('/eject', { actor: m.actor }, 'ejected'), 'Remove them and tell their server; also mutes'],
817
- ]));
818
-
819
- fill('announced', 'announced-count', announced.announced || [], (a) => row(
820
- a.noteId, `${a.actor} · ${a.announcedAt}`,
821
- [['Retract', () => write('/retract', { noteId: a.noteId }, 'retracted'), 'Un-say this announcement to everyone it reached']]));
822
- }
823
-
824
- // ---- upkeep ----
825
-
826
- // Which button filled the output pane, so clicking that one again closes it
827
- // rather than re-fetching the same thing under an already-open panel.
828
- let outputSource = null;
829
-
830
- const OUTPUT_TITLES = { log: 'Log', deadletter: 'Dead letters', drain: 'Inbox drain', rebuild: 'Recovered posts' };
831
- const output = (obj, source = null) => {
832
- closePanels('output');
833
- outputSource = source;
834
- solWindow.show('output', OUTPUT_TITLES[source] || 'Output');
835
- $('output').textContent = typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2);
836
- };
837
-
838
- const showingFrom = (source) => outputSource === source && solWindow.openId() === 'output';
839
-
840
- // Drain is the one that does work rather than reveals it, so a second click
841
- // re-runs instead of closing. It can take a while — the agent joins a sweep
842
- // already in flight and queues another after it — and a button that looks dead
843
- // for a minute reads as broken.
844
- $('do-drain').addEventListener('click', async (ev) => {
845
- const b = ev.currentTarget;
846
- b.disabled = true;
847
- b.textContent = 'draining…';
848
- say('draining the inbox — if a sweep is already running this waits for it to finish');
849
- const r = await write('/drain', {}, 'inbox drained');
850
- b.disabled = false;
851
- b.textContent = 'Drain the inbox';
852
- if (r) {
853
- const box = r.inbox || {};
854
- say(`inbox drained — ${box.count ?? 0} still waiting`);
855
- output(r, 'drain');
856
- }
857
- });
858
-
859
- // Like the drain, this does work rather than reveals it, so a second click
860
- // re-runs. Nothing here can lose anything: it only adds posts back.
861
- $('do-rebuild').addEventListener('click', async (ev) => {
862
- const b = ev.currentTarget;
863
- b.disabled = true;
864
- b.textContent = 'recovering…';
865
- say('reading what the pod still holds — one request per post, so this takes a moment');
866
- const r = await write('/rebuild', {}, 'checked');
867
- b.disabled = false;
868
- b.textContent = 'Recover posts';
869
- if (!r) return;
870
- if (r.why) { say(r.why, 'err'); return; }
871
- if (!r.landed) { say('recovered posts could NOT be saved — see the log', 'err'); return; }
872
- say(r.recovered
873
- ? `recovered ${r.recovered} post(s) the pod had and this machine did not`
874
- : `nothing was missing — the pod indexed ${r.indexed} post(s), all of them already here`);
875
- output(r, 'rebuild');
876
- });
877
-
878
- $('do-log').addEventListener('click', async () => {
879
- if (showingFrom('log')) { closePanels(); return; }
880
- const { json } = await api('/log');
881
- output((json?.lines || []).slice(-60).join('\n') || 'nothing logged yet', 'log');
882
- });
883
-
884
- $('do-deadletter').addEventListener('click', async () => {
885
- if (showingFrom('deadletter')) { closePanels(); return; }
886
- const { json } = await api('/deadletter');
887
- output(json?.items?.length ? json.items : 'no dead letters', 'deadletter');
888
- });
889
-
890
- // ---- lifecycle ----
891
- // None of these can be taken back by clicking again, so the button only opens
892
- // the matching warning in the markup; the second click is the one that acts.
893
- // Retire also wants the handle typed, because a misclick cannot produce it.
894
-
895
- const LIFECYCLE = {
896
- 'rotate-key': { path: '/rotate-key', title: 'Rotate the signing key', done: (r) => (r.changed ? 'rotated and republished' : 'no change — the key was already fresh') },
897
- retire: { path: '/retire', title: 'Retire this identity', go: 'Retire it', danger: true, done: (r) => `retired ${r.deletedAt}: Delete delivered to ${r.inboxes} inbox(es)` },
898
- move: { path: '/move', title: 'Transfer this account away', go: 'Transfer it', focus: 'move-target',
899
- done: (r) => `transferred to ${r.target}: Move delivered to ${r.inboxes} inbox(es), unfollowed ${r.unfollowed}/${r.following}` },
900
- 'move-state': { path: '/state-move', title: 'Move private data', go: 'Move it', focus: 'state-path',
901
- done: (r) => (r.unchanged ? 'already there — nothing moved'
902
- : `moved ${r.docs} document(s) and ${r.notes} post(s) — private data is now ${r.now}`) },
903
- };
904
-
905
- // The destination choice reveals the field it needs: a directory for this
906
- // device, an address for another one, nothing for the pod.
907
- function stateRows() {
908
- const w = picked('stateWhere');
909
- $('state-row-path').hidden = w !== 'path';
910
- $('state-row-url').hidden = w !== 'url';
911
- }
912
- for (const el of document.querySelectorAll('input[name=stateWhere]')) {
913
- el.addEventListener('change', stateRows);
914
- }
915
- let pending = null;
916
-
917
- const closeConfirm = () => closePanels();
918
-
919
- for (const btn of document.querySelectorAll('[data-confirm]')) {
920
- btn.addEventListener('click', () => {
921
- const what = btn.dataset.confirm;
922
- // The same button again closes its question rather than re-asking it.
923
- if (pending === what && solWindow.openId() === 'confirm-form') { closePanels(); return; }
924
- closePanels(); // including whatever else was open
925
- pending = what;
926
- const spec = LIFECYCLE[what];
927
- $(`warn-${what}`).hidden = false;
928
- solWindow.show('confirm-form', spec.title);
929
- $('confirm-go').className = spec.danger ? 'danger' : 'primary';
930
- $('confirm-go').textContent = spec.go || 'Confirm';
931
- if (spec.focus) $(spec.focus).focus();
932
- else if (what === 'retire') $('confirm-handle').focus();
933
- });
934
- }
935
-
936
- // Offered from inside the retire warning: someone reading it has already said
937
- // what they want ("not this account, here, any more") and these are the two
938
- // answers that are not destruction. Switching panels rather than closing means
939
- // they do not have to go and find the button themselves.
940
- const openConfirm = (what) => {
941
- closePanels(); // or `show` toggles: same panel, different warning
942
- document.querySelector(`[data-confirm="${what}"]`).click();
943
- };
944
- // Park lives on the status control now; from inside the retire warning it is
945
- // still one click — close the question and park.
946
- $('go-park').addEventListener('click', () => { closePanels(); setStatus(true); });
947
- $('go-move').addEventListener('click', () => openConfirm('move'));
948
-
949
- $('confirm-cancel').addEventListener('click', () => { closeConfirm(); say('nothing changed'); });
950
-
951
- $('confirm-form').addEventListener('submit', async (ev) => {
952
- ev.preventDefault();
953
- if (!pending) return;
954
- const what = pending;
955
- const body = what === 'retire' ? { confirm: $('confirm-handle').value.trim() }
956
- : what === 'move' ? { target: $('move-target').value.trim(), confirm: $('confirm-handle-move').value.trim() }
957
- : what === 'move-state' ? {
958
- to: picked('stateWhere') === 'pod' ? 'pod'
959
- : picked('stateWhere') === 'url' ? $('state-url').value.trim() : $('state-path').value.trim(),
960
- }
961
- : {};
962
- if (what === 'move' && !body.target) { say('name the account to transfer to', 'err'); return; }
963
- if (what === 'move-state' && !body.to) { say('name the destination', 'err'); return; }
964
- $('confirm-go').disabled = true;
965
- say(what === 'move-state' ? 'moving your private data — copying, checking, then switching over'
966
- : `${what} — this talks to the pod and to other servers, so it takes a moment`);
967
- const r = await write(LIFECYCLE[what].path, body, what);
968
- $('confirm-go').disabled = false;
969
- if (!r) return; // write() already said why
970
- closeConfirm();
971
- say(LIFECYCLE[what].done(r));
972
- load(); // mode, and whether it is retired, both changed
973
- });
974
-
975
- // ---- inbox ----
976
- // Only appears when there is enough waiting to be worth a decision. The agent
977
- // drains oldest-first regardless; this exists to let the owner say "do not
978
- // bother with that fortnight", which is not a call an agent should make on
979
- // someone's mail by itself.
980
- const INBOX_PROMPT_AT = 500;
981
- let dismissed = false;
982
-
983
- async function renderInbox() {
984
- if (dismissed) return;
985
- const { json: st } = await api('/status');
986
- const box = st?.inbox;
987
- const panel = $('pane-inbox');
988
- if (!box || box.count < INBOX_PROMPT_AT) { panel.hidden = true; return; }
989
- const mb = box.bytes >= 1048576
990
- ? `${(box.bytes / 1048576).toFixed(1)} MB` : `${Math.round(box.bytes / 1024)} kB`;
991
- const since = box.oldest ? new Date(box.oldest).toLocaleDateString() : 'unknown';
992
- $('inbox-summary').textContent =
993
- `${box.count.toLocaleString()} deliveries waiting (${mb}), the oldest from ${since}.`;
994
- // One request each to read and delete, and the agent holds itself to 60 a
995
- // minute, so the honest number is minutes not seconds.
996
- $('inbox-warn').hidden = box.count < 2000;
997
- $('inbox-warn').textContent = box.count >= 2000
998
- ? `At 60 requests a minute this is roughly ${Math.ceil(box.count * 2 / 60)} minutes of `
999
- + 'draining if you keep everything. Discarding the old content is much quicker.'
1000
- : '';
1001
- panel.hidden = false;
1002
- }
1003
-
1004
- // The gateway, as a facts row under software: attach through a multi-user
1005
- // front with this agent's own credential, detach back to the pod inbox. The
1006
- // forms live in the floating window, like every other disclosure.
1007
- const GATEWAY_CTL = $('gateway-ctl'); // held: it rides into a generated row
1008
- const GATEWAY_WORD = $('gateway-word');
1009
- const GW_OPEN_ATTACH = $('gateway-open-attach');
1010
- const GW_OPEN_DETACH = $('gateway-open-detach');
1011
- let gwState = null;
1012
- async function refreshGateway() {
1013
- const { status, json: g } = await api('/gateway');
1014
- if (status !== 200 || !g) return;
1015
- gwState = g;
1016
- GATEWAY_CTL.hidden = false;
1017
- if (g.configured) {
1018
- let host = g.url;
1019
- try { host = new URL(g.url).host; } catch { /* show it as-is */ }
1020
- const frontName = g.frontActor?.match(/\/u\/([^/]+)\/ap\/actor\/?$/)?.[1];
1021
- GATEWAY_WORD.textContent = frontName ? `${host} — publishing as @${frontName}@${host}` : host;
1022
- GW_OPEN_ATTACH.hidden = true;
1023
- GW_OPEN_DETACH.hidden = false;
1024
- } else {
1025
- GATEWAY_WORD.textContent = '';
1026
- GW_OPEN_ATTACH.hidden = false;
1027
- GW_OPEN_DETACH.hidden = true;
1028
- }
1029
- }
1030
- const gwShape = () => document.querySelector('input[name=gwShape]:checked')?.value || 'pod';
1031
- function gwPreviews() {
1032
- $('gw-pod-preview').textContent = config?.address || `@${config?.handle || 'you'}@your.pod`;
1033
- try { $('gw-front-host').textContent = new URL($('gw-front').value.trim()).host; }
1034
- catch { /* not a URL yet — the placeholder host stands */ }
1035
- }
1036
- function gwShapeChanged() { gwPreviews(); gwCheck(); }
1037
- for (const r of document.querySelectorAll('input[name=gwShape]')) r.addEventListener('change', gwShapeChanged);
1038
- // Typing in the blank IS choosing that shape.
1039
- $('gw-name').addEventListener('focus', () => {
1040
- const r = document.querySelector('input[name=gwShape][value=front]');
1041
- if (!r.checked) { r.checked = true; gwShapeChanged(); }
1042
- });
1043
- GW_OPEN_ATTACH.addEventListener('click', () => {
1044
- gwShapeChanged();
1045
- solWindow.show('gateway-attach-form', 'Attach to a gateway');
1046
- });
1047
- GW_OPEN_DETACH.addEventListener('click', () => {
1048
- $('gw-detach-fronted').hidden = !gwState?.frontActor;
1049
- $('gw-detach-plain').hidden = !!gwState?.frontActor;
1050
- solWindow.show('gateway-detach-form', 'Detach from gateway');
1051
- });
1052
- // Live availability, asked through the agent (the front answers it without CORS).
1053
- let gwTimer = null;
1054
- function gwCheck() {
1055
- clearTimeout(gwTimer);
1056
- const front = $('gw-front').value.trim().replace(/\/+$/, '');
1057
- const name = $('gw-name').value.trim().toLowerCase();
1058
- $('gw-name-msg').textContent = ''; $('gw-name-msg').className = 'hint';
1059
- if (gwShape() !== 'front' || !front || !name) return;
1060
- gwTimer = setTimeout(async () => {
1061
- const { status, json } = await postJson('/gateway', { action: 'check', front, handle: name });
1062
- if (status !== 200 || !json) return;
1063
- $('gw-name-msg').textContent = json.available
1064
- ? `${name} is free at ${front.replace(/^https?:\/\//, '')}`
1065
- : (json.reason || 'that name is taken');
1066
- $('gw-name-msg').className = json.available ? 'hint' : 'warn';
1067
- }, 300);
1068
- }
1069
- $('gw-front').addEventListener('input', () => { gwPreviews(); gwCheck(); });
1070
- $('gw-name').addEventListener('input', () => { gwPreviews(); gwCheck(); });
1071
- $('gw-attach').addEventListener('click', async () => {
1072
- const front = $('gw-front').value.trim().replace(/\/+$/, '');
1073
- const fronted = gwShape() === 'front';
1074
- const name = $('gw-name').value.trim().toLowerCase();
1075
- if (fronted && !name) { say('give the handle you want at the gateway', 'err'); return; }
1076
- $('gw-attach').disabled = true;
1077
- // Pod-based sends no name: the door's label is the agent's business, not
1078
- // the user's, and the agent picks a free variant by itself.
1079
- const r = await write('/gateway',
1080
- { action: 'attach', front, ...(fronted ? { handle: name, fronted: true } : {}) },
1081
- fronted ? 'attached — the agent is restarting to publish under the gateway handle; reload in a moment'
1082
- : 'attached — your mail now arrives through the gateway, filtered');
1083
- $('gw-attach').disabled = false;
1084
- if (r) { closePanels(); refreshGateway(); }
1085
- });
1086
- $('gw-detach').addEventListener('click', async () => {
1087
- const fronted = !!gwState?.frontActor;
1088
- const r = await write('/gateway', { action: 'forget' },
1089
- fronted ? 'detached — the agent is restarting under your pod\'s own name; reload in a moment'
1090
- : 'detached — the actor was republished advertising your pod\'s own inbox');
1091
- if (r) { closePanels(); refreshGateway(); }
1092
- });
1093
-
1094
- $('inbox-keep').addEventListener('click', () => {
1095
- dismissed = true;
1096
- $('pane-inbox').hidden = true;
1097
- say('leaving it to the agent — it drains oldest first');
1098
- });
1099
-
1100
- $('inbox-prune').addEventListener('click', async () => {
1101
- const days = Number($('inbox-before').value);
1102
- const before = new Date(Date.now() - days * 86400_000).toISOString();
1103
- $('inbox-prune').disabled = true;
1104
- say(`discarding content older than ${days} days — this takes a while`);
1105
- const r = await write('/inbox/prune', { before }, 'done');
1106
- $('inbox-prune').disabled = false;
1107
- if (r) {
1108
- say(`applied ${r.applied} follow/unfollow/delete, discarded ${r.dropped + r.discarded} posts`
1109
- + (r.failed ? `, ${r.failed} failed` : ''));
1110
- renderInbox();
1111
- }
1112
- });
1113
-
1114
- load();