fedipod-server 0.21.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/README.md +5 -4
  2. package/dist/claims.d.ts +3 -1
  3. package/dist/claims.js +20 -7
  4. package/dist/components/context.jsonld +6 -0
  5. package/dist/handler.d.ts +29 -1
  6. package/dist/handler.js +89 -4
  7. package/dist/handler.jsonld +36 -1
  8. package/lib/client/c2s.mjs +100 -3
  9. package/lib/client/masto/accounts.mjs +44 -4
  10. package/lib/client/masto/body.mjs +90 -3
  11. package/lib/client/masto/index.mjs +5 -1
  12. package/lib/client/masto/instance.mjs +11 -3
  13. package/lib/client/masto/media.mjs +3 -57
  14. package/lib/client/masto/oauth.mjs +66 -9
  15. package/lib/client/masto/render.mjs +62 -5
  16. package/lib/client/masto/statuses.mjs +25 -1
  17. package/lib/client/masto/timelines.mjs +128 -6
  18. package/lib/connections/tagfeed.mjs +42 -5
  19. package/lib/core/as2.mjs +36 -5
  20. package/lib/core/contexts/index.mjs +14 -0
  21. package/lib/core/contexts/litepub-0.1.json +60 -0
  22. package/lib/core/contexts/map.json +3 -1
  23. package/lib/core/contexts/quotes.json +18 -0
  24. package/lib/core/graphview.mjs +41 -3
  25. package/lib/core/intake/activities.mjs +136 -2
  26. package/lib/core/intake/activity.mjs +79 -1
  27. package/lib/core/intake/group.mjs +37 -3
  28. package/lib/core/intake/index.mjs +32 -2
  29. package/lib/core/intake/notes.mjs +60 -3
  30. package/lib/core/lease.mjs +12 -2
  31. package/lib/core/profile-page.mjs +104 -0
  32. package/lib/core/publisher/collections.mjs +6 -1
  33. package/lib/core/publisher/index.mjs +35 -17
  34. package/lib/core/publisher/notes.mjs +104 -3
  35. package/lib/core/shapes/activitystreams.ttl +28 -0
  36. package/lib/core/shapes/index.mjs +26 -1
  37. package/lib/core/wire-quotes.mjs +71 -0
  38. package/lib/core/wire.mjs +59 -103
  39. package/lib/device/admin/routes/lifecycle.mjs +9 -0
  40. package/lib/device/admin/static.mjs +4 -1
  41. package/lib/device/admin/surface.mjs +26 -2
  42. package/lib/device/certs.mjs +18 -2
  43. package/lib/device/cli/commands/setup.mjs +6 -2
  44. package/lib/device/cli/commands/state.mjs +3 -1
  45. package/lib/device/gateway-move.mjs +58 -0
  46. package/lib/device/migrate.mjs +3 -1
  47. package/lib/device/setup.mjs +67 -7
  48. package/lib/gateway/front-core.mjs +307 -36
  49. package/lib/gateway/gateway-core.mjs +15 -6
  50. package/lib/gateway/notices.mjs +84 -0
  51. package/lib/gateway/quiet.mjs +184 -0
  52. package/lib/pod/root.mjs +6 -3
  53. package/lib/pod/state.mjs +8 -7
  54. package/lib/pod/transport.mjs +5 -1
  55. package/lib/session/README.md +123 -0
  56. package/lib/session/demo.html +94 -0
  57. package/lib/session/fedi-account.mjs +346 -0
  58. package/lib/session/fedi-login.mjs +123 -0
  59. package/lib/session/oidc-session.mjs +233 -0
  60. package/lib/session/package.json +41 -0
  61. package/package.json +1 -1
  62. package/run-agent.mjs +28 -0
  63. package/vendor/gate.cjs +6 -3
  64. package/web/admin/bar.css +66 -0
  65. package/web/admin/bar.js +38 -0
  66. package/web/admin/client/index.html +31 -3
  67. package/web/admin/client-pick.js +79 -0
  68. package/web/admin/gateway.js +22 -0
  69. package/web/admin/index.html +54 -13
  70. package/web/admin/notices-bar.js +90 -0
  71. package/web/admin/oauth-signin.mjs +37 -0
  72. package/web/admin/record.js +13 -1
  73. package/web/admin/setup/index.html +31 -3
  74. package/web/admin/tokens.css +15 -3
  75. package/web/admin/upkeep.js +4 -5
  76. package/web/app/README.md +14 -10
  77. package/web/app/admin-facade.mjs +51 -12
  78. package/web/app/agent.mjs +157 -19
  79. package/web/app/authorize_interaction.html +56 -0
  80. package/web/app/authorize_interaction.js +69 -0
  81. package/web/app/boot.mjs +233 -95
  82. package/web/app/dist/boot.js +477 -569
  83. package/web/app/dist/boot.js.map +4 -4
  84. package/web/app/dist/sw.js +1861 -532
  85. package/web/app/dist/sw.js.map +4 -4
  86. package/web/app/gateway-move.mjs +60 -0
  87. package/web/app/index.html +40 -49
  88. package/web/app/keys-browser.mjs +12 -15
  89. package/web/app/keystore.mjs +19 -36
  90. package/web/app/oidc-session.mjs +5 -188
  91. package/web/app/signup.mjs +172 -132
  92. package/web/app/sw-src.mjs +1 -1
  93. package/web/app/update.js +66 -0
  94. package/web/front/admin.html +5 -3
  95. package/web/front/new-account.html +2 -2
  96. package/web/front/notices.html +69 -0
  97. package/web/front/notices.js +107 -0
  98. package/web/front/run.html +15 -8
  99. package/web/front/run.js +124 -36
  100. package/web/icons/forum.svg +11 -0
  101. package/web/icons/gateway.svg +14 -0
  102. package/web/icons/local.svg +11 -0
  103. package/web/app/index.html~ +0 -152
  104. package/web/app/site/_headers +0 -9
  105. package/web/app/site/_redirects +0 -22
  106. package/web/app/site/admin/actors.js +0 -145
  107. package/web/app/site/admin/bar.css +0 -60
  108. package/web/app/site/admin/bar.js +0 -61
  109. package/web/app/site/admin/client/client.js +0 -66
  110. package/web/app/site/admin/client/index.html +0 -43
  111. package/web/app/site/admin/common.js +0 -23
  112. package/web/app/site/admin/connections.js +0 -112
  113. package/web/app/site/admin/gateway.js +0 -111
  114. package/web/app/site/admin/group.js +0 -258
  115. package/web/app/site/admin/index.html +0 -564
  116. package/web/app/site/admin/record.js +0 -378
  117. package/web/app/site/admin/setup/index.html +0 -215
  118. package/web/app/site/admin/setup/setup.js +0 -326
  119. package/web/app/site/admin/tokens.css +0 -56
  120. package/web/app/site/admin/upkeep.js +0 -178
  121. package/web/app/site/admin/window.css +0 -55
  122. package/web/app/site/admin/window.js +0 -136
  123. package/web/app/site/app/404.html +0 -40
  124. package/web/app/site/app/_headers +0 -2
  125. package/web/app/site/app/apple-touch-icon.png +0 -0
  126. package/web/app/site/app/assets/ICONS-DIPJeU0C.js +0 -2
  127. package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +0 -1
  128. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +0 -183
  129. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
  130. package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
  131. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +0 -2
  132. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +0 -1
  133. package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  134. package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
  135. package/web/app/site/app/assets/chunk-aKtaBQYM.js +0 -1
  136. package/web/app/site/app/assets/compose-CFEx7wb4.js +0 -2
  137. package/web/app/site/app/assets/compose-CFEx7wb4.js.map +0 -1
  138. package/web/app/site/app/assets/compose-ZJN5xSb4.js +0 -35
  139. package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +0 -1
  140. package/web/app/site/app/assets/debug.module-YFQgwnIr.js +0 -52
  141. package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +0 -1
  142. package/web/app/site/app/assets/dom-p1YcJ5Rw.js +0 -2
  143. package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +0 -1
  144. package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  145. package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  146. package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  147. package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  148. package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +0 -1
  149. package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +0 -1
  150. package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
  151. package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +0 -1
  152. package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
  153. package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
  154. package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
  155. package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
  156. package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
  157. package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
  158. package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
  159. package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
  160. package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +0 -1
  161. package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +0 -1
  162. package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
  163. package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +0 -1
  164. package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
  165. package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +0 -1
  166. package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
  167. package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
  168. package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +0 -1
  169. package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
  170. package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +0 -1
  171. package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
  172. package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
  173. package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
  174. package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
  175. package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
  176. package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +0 -1
  177. package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +0 -1
  178. package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +0 -1
  179. package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +0 -1
  180. package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +0 -1
  181. package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
  182. package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +0 -1
  183. package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
  184. package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +0 -1
  185. package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +0 -1
  186. package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +0 -1
  187. package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
  188. package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
  189. package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +0 -1
  190. package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +0 -1
  191. package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
  192. package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
  193. package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +0 -1
  194. package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +0 -1
  195. package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +0 -1
  196. package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
  197. package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +0 -1
  198. package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +0 -1
  199. package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +0 -1
  200. package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
  201. package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
  202. package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
  203. package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +0 -1
  204. package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +0 -1
  205. package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +0 -1
  206. package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +0 -1
  207. package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +0 -1
  208. package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
  209. package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
  210. package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +0 -1
  211. package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +0 -1
  212. package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +0 -1
  213. package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +0 -1
  214. package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +0 -1
  215. package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +0 -1
  216. package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +0 -1
  217. package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +0 -1
  218. package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
  219. package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +0 -1
  220. package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +0 -1
  221. package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +0 -1
  222. package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
  223. package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +0 -1
  224. package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +0 -1
  225. package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
  226. package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
  227. package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
  228. package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +0 -1
  229. package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +0 -1
  230. package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
  231. package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +0 -1
  232. package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +0 -1
  233. package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +0 -1
  234. package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +0 -1
  235. package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +0 -1
  236. package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +0 -1
  237. package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +0 -1
  238. package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +0 -1
  239. package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +0 -1
  240. package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
  241. package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +0 -1
  242. package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
  243. package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
  244. package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
  245. package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +0 -1
  246. package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
  247. package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +0 -1
  248. package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +0 -1
  249. package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
  250. package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +0 -1
  251. package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
  252. package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +0 -1
  253. package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
  254. package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +0 -1
  255. package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +0 -1
  256. package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
  257. package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +0 -1
  258. package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +0 -1
  259. package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
  260. package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +0 -1
  261. package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +0 -1
  262. package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +0 -1
  263. package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
  264. package/web/app/site/app/assets/instances-BNPptnn-.json +0 -333
  265. package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +0 -1
  266. package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +0 -1
  267. package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
  268. package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +0 -1
  269. package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +0 -1
  270. package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +0 -1
  271. package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +0 -1
  272. package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +0 -1
  273. package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +0 -1
  274. package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +0 -1
  275. package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +0 -1
  276. package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +0 -1
  277. package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +0 -1
  278. package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +0 -1
  279. package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +0 -1
  280. package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +0 -1
  281. package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +0 -1
  282. package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
  283. package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +0 -1
  284. package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
  285. package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +0 -1
  286. package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +0 -1
  287. package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
  288. package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +0 -1
  289. package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +0 -1
  290. package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
  291. package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +0 -1
  292. package/web/app/site/app/assets/locales/tok-CfheRbCu.js +0 -1
  293. package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +0 -1
  294. package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +0 -1
  295. package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +0 -1
  296. package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +0 -1
  297. package/web/app/site/app/assets/main-BdqNbG-a.js +0 -26
  298. package/web/app/site/app/assets/main-BdqNbG-a.js.map +0 -1
  299. package/web/app/site/app/assets/mock-home-DGhR__He.js +0 -2
  300. package/web/app/site/app/assets/mock-home-DGhR__He.js.map +0 -1
  301. package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
  302. package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  303. package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  304. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +0 -2
  305. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +0 -1
  306. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +0 -20
  307. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +0 -1
  308. package/web/app/site/app/assets/style-B1iLPTPR.css +0 -2
  309. package/web/app/site/app/assets/temml-DuyIQMei.js +0 -97
  310. package/web/app/site/app/assets/temml-DuyIQMei.js.map +0 -1
  311. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +0 -2
  312. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
  313. package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
  314. package/web/app/site/app/assets/year-in-posts-DJv_star.js +0 -3
  315. package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +0 -1
  316. package/web/app/site/app/compose/index.html +0 -20
  317. package/web/app/site/app/favicon.ico +0 -0
  318. package/web/app/site/app/index.html +0 -73
  319. package/web/app/site/app/logo-192.png +0 -0
  320. package/web/app/site/app/logo-512.png +0 -0
  321. package/web/app/site/app/logo-badge-72.png +0 -0
  322. package/web/app/site/app/logo-maskable-512.png +0 -0
  323. package/web/app/site/app/logo-monochrome-512.png +0 -0
  324. package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
  325. package/web/app/site/app/manifest.webmanifest +0 -1
  326. package/web/app/site/app/og-image-2.jpg +0 -0
  327. package/web/app/site/app/og-image.png +0 -0
  328. package/web/app/site/app/robots.txt +0 -2
  329. package/web/app/site/app/version.json +0 -1
  330. package/web/app/site/boot.js +0 -34277
  331. package/web/app/site/index.html +0 -251
  332. package/web/app/site/sw.js +0 -71331
package/web/app/agent.mjs CHANGED
@@ -5,6 +5,7 @@
5
5
  // the signing key comes from WebCrypto (keys-browser), and delivery goes through
6
6
  // the relay (deliver-relay). Everything between — wire, the store, the
7
7
  // publisher, intake, the Mastodon facade — is lib/, unchanged.
8
+ import { kvGet, kvPut } from './idb-kv.mjs';
8
9
  import { apUrls } from '../../lib/core/wire.mjs';
9
10
  import * as containers from '../../lib/pod/containers.mjs';
10
11
  import * as podState from '../../lib/pod/state.mjs';
@@ -19,9 +20,10 @@ import { TagFeed } from '../../lib/connections/tagfeed.mjs';
19
20
  import { makeDpopSession } from './pod-auth.mjs';
20
21
  import { BrowserRemotePod } from './pod-remote.mjs';
21
22
  import { importSigningKey, loadKeysFromPod, cacheOpenedKeys, podActorOf } from './keys-browser.mjs';
22
- import { generateKeys, wrapKeys } from './keystore.mjs';
23
+ import { generateKeys } from './keystore.mjs';
23
24
  import { RelayDeliverer, doorKeyOf } from './deliver-relay.mjs';
24
25
  import { AdminFacade } from './admin-facade.mjs';
26
+ import { completeGatewayMove } from './gateway-move.mjs';
25
27
  import { BrowserAtproto } from './atproto-browser.mjs';
26
28
  import { BskyFeed } from '../../lib/connections/bskyfeed.mjs';
27
29
  import { BrowserFediAccounts } from './fediacct-browser.mjs';
@@ -49,6 +51,69 @@ const originAuthorities = (host) => ({
49
51
  wsAuthorities() { return []; }, // fetch-only: this build serves no socket
50
52
  });
51
53
 
54
+ // Why an account did not open, in words for the person it happened to, and a
55
+ // `code` the sign-in page turns into a title and the one button that leads
56
+ // somewhere (boot.mjs). Every one of these used to be the same sentence —
57
+ // "no account config on this pod — sign up first" — which named the wrong
58
+ // cause, offered the wrong cure, and left the reader with nothing to press.
59
+ //
60
+ // Each message says three things and stops: what the pod did, what that means
61
+ // for the account, what to do about it. The pod's own wording is carried only
62
+ // where it adds something the status code does not.
63
+ async function accountNotRead({ unread, podBase, state, webId, actorUrl = null, fetchImpl = globalThis.fetch }) {
64
+ const host = (() => { try { return new URL(podBase).host; } catch { return podBase; } })();
65
+ const of = (code, message) => { const e = new Error(message); e.code = code; return e; };
66
+ if (!unread) {
67
+ // A pod that holds the account's actor and no record is an account a
68
+ // DeviceAgent runs: the record lives on the device, and only the device
69
+ // can open it. Said so, rather than "there is no account".
70
+ const actor = actorUrl
71
+ ? await fetchImpl(actorUrl, { headers: { accept: 'application/activity+json' } }).then((r) => (r.ok ? r.json() : null)).catch(() => null)
72
+ : null;
73
+ if (actor?.preferredUsername) {
74
+ const at = (() => { try { return `@${actor.preferredUsername}@${new URL(actor.id || actorUrl).host}`; } catch { return actor.preferredUsername; } })();
75
+ return of('device-account', `${host} holds the account ${at}, but not its record: this account is run by a`
76
+ + ` DeviceAgent, and its record lives on that device. Open it from that device's admin page, not here.`
77
+ + ` The sign-in used here was ${webId}.`);
78
+ }
79
+ return of('no-account', `${host} answered, and there is no FediPod account in it.`
80
+ + ` Sign in with the pod that holds your account, or make an account in this one —`
81
+ + ` both start from the sign-in page. The sign-in used here was ${webId}.`);
82
+ }
83
+ const status = Number((/HTTP (\d{3})/u.exec(unread.message) || [])[1]) || 0;
84
+ if (status === 401 || status === 403) {
85
+ // The page renews a refused sign-in by itself, and takes the person to
86
+ // their pod's login when the renewal is refused too (boot.mjs). By the
87
+ // time anybody reads this, both have already been tried.
88
+ return of('sign-in-refused', `${host} would not let this sign-in read your account — it answered`
89
+ + ` "not allowed" (HTTP ${status}) — and renewing the sign-in did not change its mind.`
90
+ + ` Your account and everything in it are untouched. Signing in at ${host} again is the`
91
+ + ` one thing that helps.`);
92
+ }
93
+ if (status === 429) {
94
+ return of('pod-busy', `${host} is telling this browser it has asked for too much too quickly`
95
+ + ` (HTTP 429), so your account could not be read. Nothing is wrong with your account, and`
96
+ + ` nothing here needs doing: this page is waiting the throttle out and will ask again by`
97
+ + ` itself. While a pod is rate-limiting you, only time helps.`);
98
+ }
99
+ if (status >= 500) {
100
+ return of('pod-error', `${host} broke while handing your account over (HTTP ${status}).`
101
+ + ` That is the pod's own failure, not your account's. This page asks again by itself in`
102
+ + ` a moment; if it keeps saying this, the pod is down and there is nothing to do here.`);
103
+ }
104
+ if (status === 404) {
105
+ return of('no-account-here', `${host} says there is nothing at ${state} (HTTP 404).`
106
+ + ` Either no FediPod account has ever been set up in this pod, or you signed in with a`
107
+ + ` different pod than the one holding yours. Sign in with that one, or make an account`
108
+ + ` in this one — both start from the sign-in page.`);
109
+ }
110
+ // No status at all: the request never got an answer. The pod's own words are
111
+ // the only thing that says anything here, so they come along.
112
+ return of('pod-unreachable', `This browser could not reach ${host} to read your account`
113
+ + ` (${unread.message}). Your account is still there. This page tries again by itself;`
114
+ + ` if it keeps saying this, check that you are online and that the pod is up.`);
115
+ }
116
+
52
117
  export class BrowserAgent {
53
118
  constructor({ log = console.log } = {}) {
54
119
  this.log = log;
@@ -72,10 +137,35 @@ export class BrowserAgent {
72
137
  return true;
73
138
  }
74
139
 
140
+ // One call to the gateway's owner API, proved with the pod session. What
141
+ // comes back on 200 or 410 is the account's standing there, kept for the
142
+ // manage page; anything else is logged and forgotten.
143
+ async tellGateway(what, body) {
144
+ if (!this.gatewayApi) return null;
145
+ let res;
146
+ try {
147
+ res = await this.sessionFetch(`${this.gatewayApi}/${what}`, {
148
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body),
149
+ });
150
+ } catch (e) { this.log(`gateway ${what}: ${e.message}`); return null; }
151
+ const json = await res.json().catch(() => ({}));
152
+ const out = { status: res.status, ...json };
153
+ if (res.status === 200 || res.status === 410) this.gatewayStanding = out;
154
+ else this.log(`gateway ${what}: ${res.status} ${json.error || ''}`);
155
+ return out;
156
+ }
157
+ openAtGateway() { return this.tellGateway('open', { handle: this.doorKey }); }
158
+ pauseAtGateway(paused) { return this.tellGateway('pause', { handle: this.doorKey, paused: !!paused }); }
159
+ closeAtGateway() { return this.tellGateway('close', { handle: this.doorKey, confirm: true }); }
160
+ static OPEN_EVERY_MS = 60 * 60_000;
161
+
75
162
  // Become the active agent: renew the lease, then start what a viewer skips —
76
163
  // publish the face, drain the inbox, run the mirrors.
77
164
  async goActive() {
78
165
  this.viewer = false;
166
+ // Reading here is being here: the gateway hears so once an hour.
167
+ clearInterval(this._openTimer);
168
+ this._openTimer = setInterval(() => { this.openAtGateway(); }, BrowserAgent.OPEN_EVERY_MS);
79
169
  this.lease.onLost = () => this.demote();
80
170
  this.lease.startRenewal();
81
171
  try {
@@ -94,6 +184,9 @@ export class BrowserAgent {
94
184
  this.deliverer?.startQueue?.();
95
185
  await this.publisher.publishProfile();
96
186
  await this.store.flush?.();
187
+ // An address that just moved here from another gateway: the new
188
+ // actor is published, so the old gateway and the followers can be told.
189
+ await completeGatewayMove(this).catch((e) => this.log(`gateway move: ${e.message}`));
97
190
  await this.intake.start();
98
191
  this.startBsky();
99
192
  this.startAccts();
@@ -111,6 +204,7 @@ export class BrowserAgent {
111
204
  this.viewer = true;
112
205
  this.log('another device took over — read-only here');
113
206
  this.lease.stopRenewal();
207
+ clearInterval(this._openTimer); this._openTimer = null;
114
208
  this.intake?.stop?.();
115
209
  // The delivery queue as well. Its timer starts in the Deliverer's
116
210
  // constructor and nothing here ever switched it off, so a demoted device
@@ -160,6 +254,22 @@ export class BrowserAgent {
160
254
  * - { credential, keysRecord, config }: a client-credential session with the
161
255
  * material in hand (the offline test path).
162
256
  */
257
+ // This browser's name for itself, minted once and kept. Not an identity and
258
+ // not a secret — it says only "the same browser as last time" to the lease.
259
+ // If storage cannot be reached, a fresh one is honest: this browser cannot
260
+ // prove it is the one that held the lease, so it should not claim to be.
261
+ async deviceId() {
262
+ try {
263
+ const had = await kvGet('device-id');
264
+ if (had) return had;
265
+ const made = crypto.randomUUID();
266
+ await kvPut('device-id', made);
267
+ return made;
268
+ } catch {
269
+ return null;
270
+ }
271
+ }
272
+
163
273
  async boot({ oidc, credential, keysRecord, config, frontOrigin }) {
164
274
  let session; let webId; let remotePod;
165
275
  if (oidc) {
@@ -173,6 +283,9 @@ export class BrowserAgent {
173
283
  remotePod = credential.remotePod;
174
284
  }
175
285
  this.webId = webId;
286
+ // The pod session's own fetch, for the gateway APIs that take it as
287
+ // proof of the pod (attach, move) — the transport below is for the pod.
288
+ this.sessionFetch = session.fetch;
176
289
  const root = (config && config.root) || 'fedipod/';
177
290
  this.remote = new BrowserRemotePod(session, { webId, log: this.log });
178
291
  // Pod-native for now: the state store below is read with these, and only
@@ -186,10 +299,18 @@ export class BrowserAgent {
186
299
  // The Node agent has always passed remote.fetch here.
187
300
  const podFetch = (u, i) => this.remote.fetch(u, i);
188
301
  this.store = new PodStore({ storage: new HttpStorage(this.urls.state, podFetch), log: this.log });
189
- await this.store.load().catch(() => { /* first boot: nothing there yet */ });
302
+ // A read that FAILED is not an account that is not there. load() already
303
+ // tells those apart — a missing container, a refused token, a pod saying
304
+ // "too many requests" each come back with their own reason — and throwing
305
+ // the reason away turned every one of them into "sign up first", said to
306
+ // somebody who had just signed in and whose account was sitting on the pod
307
+ // unread. Keep it: below is the only place that can tell whether it
308
+ // mattered, because a sign-up hands its config in and needs no read at all.
309
+ let unread = null;
310
+ await this.store.load().catch((e) => { unread = e; });
190
311
  // Config: handed in on sign-up, or read from the pod on a returning sign-in.
191
312
  const cfg = config || this.store.getConfig();
192
- if (!cfg) throw new Error('no account config on this pod — sign up first');
313
+ if (!cfg) throw await accountNotRead({ unread, podBase: remotePod, state: this.urls.state, webId, actorUrl: this.urls.actor });
193
314
  // `root` is written INTO the config, not just used above. The Publisher
194
315
  // builds its own urls from `config.root` (publisher.mjs), and a config
195
316
  // without one falls to the Node default — so an account set up elsewhere
@@ -227,6 +348,28 @@ export class BrowserAgent {
227
348
  onGone: () => this.publisher.publishCollections({ followers: true }),
228
349
  });
229
350
 
351
+ // The gateway this account's mail comes through, and the account's own
352
+ // standing there — paused, closed, how much arrived unread (front-core:
353
+ // accounts that go quiet). The gateway is told on every sign-in and
354
+ // hourly while this device is active: that is what keeps an account
355
+ // somebody reads from counting as a quiet one. Only a closed address
356
+ // stops the boot; a gateway that cannot be reached is no reason to
357
+ // refuse a sign-in.
358
+ this.gatewayApi = null;
359
+ try { if (config.gateway?.url) this.gatewayApi = `${new URL(config.gateway.url).origin}/api`; } catch { this.gatewayApi = null; }
360
+ this.doorKey = doorKeyOf(config.gateway?.url) || config.handle;
361
+ this.gatewayStanding = null;
362
+ const standing = await this.openAtGateway();
363
+ if (standing?.status === 410) {
364
+ const host = (() => { try { return new URL(this.gatewayApi).host; } catch { return 'the gateway'; } })();
365
+ const address = this.doorKey.includes('@') ? `@${this.doorKey}` : `@${this.doorKey}@${host}`;
366
+ const why = standing.closedBy === 'owner' ? 'you closed it' : 'nothing opened it for six months';
367
+ const e = new Error(`${address} is closed at ${host}: ${why}, and a closed address does not come back.`
368
+ + ` Everything on your pod is untouched. The sign-in used here was ${webId}.`);
369
+ e.code = 'address-closed';
370
+ throw e;
371
+ }
372
+
230
373
  this.publisher = new Publisher({
231
374
  config: this.store.getConfig(), remote: this.remote, store: this.store,
232
375
  deliverer: this.deliverer, publicKeyPem: keys.rsaPublicPem, assertionKey: null, log: this.log,
@@ -263,7 +406,13 @@ export class BrowserAgent {
263
406
  // one browser/device may ACT on a pod at a time — a later arrival runs
264
407
  // read-only until the owner acts on it and it takes over. Written with fresh
265
408
  // fetches, never the cached store. Passed into Intake so the drain checks it.
266
- this.lease = new Lease({ url: this.urls.state + 'lease.json', fetchImpl: podFetch, log: this.log });
409
+ this.lease = new Lease({ url: this.urls.state + 'lease.json', fetchImpl: podFetch, log: this.log,
410
+ // Kept on this origin, so THIS browser is one holder however many times
411
+ // its worker is killed and restarted. Without it every restart was a new
412
+ // holder: the old lease still had minutes to run, so the browser found
413
+ // its own account "active on another device" and asked to take it over.
414
+ // Switching between the clients did it every time, being a navigation.
415
+ id: await this.deviceId() });
267
416
  // The client-to-server dispatcher, here only for what the Gateway's
268
417
  // outbox door takes on the owner's behalf: the browser answers no
269
418
  // /ap/outbox of its own.
@@ -377,24 +526,11 @@ export class BrowserAgent {
377
526
  // actor so the new public key is on the wire. The old key stops signing the
378
527
  // moment this returns — same one-way change as the Node agent's rotateKey
379
528
  // (run-agent.mjs).
380
- //
381
- // The pod's copy is wrapped, so this needs the account password. There is
382
- // nowhere to get it from without asking: the worker boots from a stored
383
- // session and holds no password, and caching one to save a prompt on a
384
- // once-in-a-while action would put the account password in storage to avoid
385
- // typing it. So the caller supplies it, and rotating without one is refused
386
- // rather than quietly writing a bare key back where a wrapped one was.
387
- async rotateKey({ password } = {}) {
388
- if (!password) {
389
- const e = new Error('rotating the signing key needs your account password — '
390
- + 'it is what the new key is locked under on the pod');
391
- e.code = 'key-password-needed';
392
- throw e;
393
- }
529
+ async rotateKey() {
394
530
  const before = this.publisher.publicKeyPem;
395
531
  const rec = await generateKeys();
396
532
  rec.mintedFor = this.urls.actor; // one key, one actor (lib/keys.mjs)
397
- await podState.writeWrappedKeys(this.remote, this.urls, await wrapKeys(rec, password));
533
+ await podState.writeKeys(this.remote, this.urls, rec);
398
534
  const keys = await cacheOpenedKeys(podActorOf(this.urls), rec);
399
535
  this.publisher.publicKeyPem = keys.rsaPublicPem;
400
536
  this.deliverer.rsaPrivate = keys.rsaPrivate;
@@ -518,3 +654,5 @@ export class BrowserAgent {
518
654
  return { ...moved, ...r, snapshot: following.length };
519
655
  }
520
656
  }
657
+
658
+ export { accountNotRead };
@@ -0,0 +1,56 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <!-- The remote-follow door, at the path every Fediverse server sends its
7
+ readers to: /authorize_interaction?uri=<who>. Another server's Follow
8
+ button asks its reader which server they are on; when they say this one,
9
+ that server looks up the subscribe link in our WebFinger (front-core.mjs)
10
+ and lands them here. A static page, not a function: the account that does
11
+ the following is the agent in THIS browser, and the page asks it over the
12
+ same origin routes the record page uses. The underscore is Mastodon's
13
+ spelling of the path, kept so the address is the one other servers
14
+ already know. -->
15
+ <title>Follow — FediPod</title>
16
+ <link rel="icon" href="/icons/local.svg" type="image/svg+xml">
17
+ <link rel="stylesheet" href="/admin/tokens.css">
18
+ <style>
19
+ body { margin: 0; background: var(--surface); color: var(--fg); font-family: var(--font); }
20
+ main { max-width: 34rem; margin: 0 auto; padding: 2.5rem 1.25rem 3rem; }
21
+ h1 { color: var(--heading); margin: 0 0 1.5rem; font-size: 1.6rem; }
22
+ .address { background: var(--raised); border: 1px solid var(--line); border-radius: 8px;
23
+ padding: 1rem 1.1rem; margin: 0 0 1.25rem; word-break: break-word; }
24
+ .as { color: var(--ok); margin: 0 0 1.5rem; }
25
+ .row { margin: 0 0 1.25rem; }
26
+ button { font: inherit; padding: 0.6rem 1.4rem; border-radius: 8px; border: 1px solid var(--btn);
27
+ background: var(--btn); color: var(--btn-text); cursor: pointer; }
28
+ button:hover { background: var(--btn-hover); }
29
+ button:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
30
+ .msg { margin: 0 0 1.25rem; }
31
+ .msg.err { color: var(--err); }
32
+ .msg.ok { color: var(--ok); }
33
+ p { line-height: 1.55; }
34
+ a { color: var(--accent); }
35
+ .hint { color: var(--sub); }
36
+ </style>
37
+ <script src="/authorize_interaction.js" defer></script>
38
+ </head>
39
+ <body>
40
+ <main>
41
+ <h1>Follow</h1>
42
+ <p class="address" id="target">…</p>
43
+ <p class="as" id="as" hidden></p>
44
+ <p class="row"><button id="go" hidden>Follow</button></p>
45
+ <p class="msg" id="msg" role="status" aria-live="polite"></p>
46
+ <div id="by-hand" hidden>
47
+ <p>No FediPod account is signed in to this browser, so the follow cannot be
48
+ sent from here. If your agent runs on your own machine, or you are signed
49
+ in somewhere else, paste the address above into your client's search box
50
+ and follow from the profile it finds.</p>
51
+ <p><a href="/">Sign in to FediPod</a></p>
52
+ </div>
53
+ <p class="hint"><a href="/admin/client/">Your account</a></p>
54
+ </main>
55
+ </body>
56
+ </html>
@@ -0,0 +1,69 @@
1
+ // authorize_interaction.js — the script for the remote-follow page.
2
+ //
3
+ // It reads who is to be followed out of the `uri` parameter, asks the agent in
4
+ // this browser who it is, and sends the follow through the same same-origin
5
+ // route the record page uses (/follow, admin-facade.mjs). The header is what
6
+ // the service worker's gate reads: a cross-site form cannot set one, and a
7
+ // cross-origin fetch that does is preflighted and never answered (sw-src.mjs).
8
+ const PAGE = { 'x-fedipod-page': '1' };
9
+ const $ = (id) => document.getElementById(id);
10
+
11
+ const say = (text, kind = '') => { const m = $('msg'); m.textContent = text; m.className = `msg ${kind}`.trim(); };
12
+
13
+ // What the other server put in `uri`: an acct URI, a bare or @-prefixed
14
+ // handle, or the actor's address. Anything with a scheme is taken as the
15
+ // actor itself — pasting an actor url is a normal way to follow.
16
+ function readTarget(raw) {
17
+ const s = String(raw || '').trim();
18
+ if (!s) return null;
19
+ if (/^https?:\/\//iu.test(s)) return { actor: s, label: s };
20
+ const handle = s.replace(/^acct:/iu, '').replace(/^@/u, '');
21
+ if (!/^[^@\s/]+@[^@\s/]+$/u.test(handle)) return null;
22
+ return { handle, label: `@${handle}` };
23
+ }
24
+
25
+ const target = readTarget(new URLSearchParams(location.search).get('uri'));
26
+
27
+ async function whoIsHere() {
28
+ try {
29
+ const res = await fetch('/config', { headers: PAGE });
30
+ if (!res.ok) return null;
31
+ const cfg = await res.json();
32
+ return cfg.address || (cfg.handle ? `@${cfg.handle}` : null);
33
+ } catch { return null; }
34
+ }
35
+
36
+ async function follow() {
37
+ $('go').disabled = true;
38
+ say('sending…');
39
+ try {
40
+ const res = await fetch('/follow', {
41
+ method: 'POST', headers: { ...PAGE, 'content-type': 'application/json' },
42
+ body: JSON.stringify(target.actor ? { actor: target.actor } : { handle: target.handle }),
43
+ });
44
+ const out = await res.json().catch(() => ({}));
45
+ if (!res.ok) { say(out.error || `the follow was refused (${res.status})`, 'err'); $('go').disabled = false; return; }
46
+ // A Follow is delivered, not granted: it counts once the other server
47
+ // answers it, and a locked account answers when its owner gets to it.
48
+ say(`sent to ${target.label} — it counts once their server accepts it`, 'ok');
49
+ $('go').hidden = true;
50
+ } catch (err) {
51
+ say(err.message || 'the follow could not be sent', 'err');
52
+ $('go').disabled = false;
53
+ }
54
+ }
55
+
56
+ (async () => {
57
+ if (!target) {
58
+ $('target').textContent = 'nobody';
59
+ say('This address opens from another server’s Follow button, and it did not name anybody to follow.', 'err');
60
+ return;
61
+ }
62
+ $('target').textContent = target.label;
63
+ const me = await whoIsHere();
64
+ if (!me) { $('by-hand').hidden = false; return; }
65
+ $('as').textContent = `signed in as ${me}`;
66
+ $('as').hidden = false;
67
+ $('go').hidden = false;
68
+ $('go').addEventListener('click', follow);
69
+ })();