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
@@ -21,19 +21,29 @@ import { readCapped, safeFetch, isLoopbackHost } from '../shared/safefetch.mjs';
21
21
  import * as podRoot from '../pod/root.mjs';
22
22
  import * as podPolicy from '../pod/policy.mjs';
23
23
  import { podBaseOfWebId } from '../pod/urls.mjs';
24
+ import { routeQuietApi, noteOpened, noteReceived, closedState, accountState, closedAnswer } from './quiet.mjs';
25
+ import { routeNoticesApi } from './notices.mjs';
24
26
 
25
27
  // The one WebFinger document, spelled out here rather than imported from
26
28
  // wire.mjs: wire drags the agent's whole HTML pipeline (sanitize-html and
27
29
  // friends), which a serverless front must never carry — it crashed the
28
30
  // deployed function before it answered its first request.
29
- const jrd = ({ handle, host, actor, aliases = [], page = null }) => ({
31
+ const jrd = ({ handle, host, actor, aliases = [], page = null, subscribe = null }) => ({
30
32
  subject: `acct:${handle}@${host}`,
31
33
  ...(aliases.length ? { aliases } : {}),
32
34
  links: [
33
35
  { rel: 'self', type: 'application/activity+json', href: actor },
34
36
  ...(page ? [{ rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: page }] : []),
37
+ // Remote follow. Somebody reading this account on another server presses
38
+ // Follow, says they are here, and that server looks for this link to find
39
+ // out where to send them. `{uri}` is a literal the other server fills in
40
+ // with whatever they are following — it must not be escaped.
41
+ ...(subscribe ? [{ rel: 'http://ostatus.org/schema/1.0/subscribe', template: subscribe }] : []),
35
42
  ],
36
43
  });
44
+ // The door that link names, which the site staged beside this function serves.
45
+ const subscribeTemplate = (origin) => (origin
46
+ ? `${String(origin).replace(/\/$/u, '')}/authorize_interaction?uri={uri}` : null);
37
47
  // The public profile page of any account the front answers for lives on
38
48
  // its pod, whether the address is the front's or the pod's own.
39
49
  const profilePageOf = (rec) => (rec?.podHome ? rec.podHome + 'ap/profile.html' : null);
@@ -136,10 +146,25 @@ const RELAY_MAX_BODY = 1024 * 1024;
136
146
  const RELAY_TIMEOUT_MS = 8_000;
137
147
  // The only headers a relayed request may carry to the remote server. Host
138
148
  // comes from the URL; the user agent from safeFetch.
149
+ const SERVER_PROBE_MS = 6000;
150
+ const SERVER_PROBE_BYTES = 64 * 1024;
139
151
  const RELAY_HEADERS = new Set(['date', 'digest', 'signature', 'content-type', 'accept']);
140
152
 
141
153
  const keyIdOf = (signature) => (/keyId="([^"]+)"/.exec(signature || '') || [])[1] || null;
142
154
 
155
+ // The owner's APIs a browser at ANOTHER origin may call: an address moving
156
+ // to a new gateway is driven from the new gateway's page, and it has to tell
157
+ // this one it moved and send the Move through this one's relay. Both take a
158
+ // pod token as proof, the way the outbox door does, so any origin may ask.
159
+ const API_CORS = {
160
+ 'access-control-allow-origin': '*',
161
+ 'access-control-allow-methods': 'POST, OPTIONS',
162
+ 'access-control-allow-headers': 'Authorization, DPoP, Content-Type',
163
+ 'access-control-max-age': '86400',
164
+ };
165
+ const withApiCors = (out) => ({ ...out, headers: { ...(out.headers || {}), ...API_CORS } });
166
+ const apiPreflight = () => ({ status: 204, headers: { ...API_CORS, allow: 'POST, OPTIONS' }, body: null });
167
+
143
168
  async function relayOne(item, rec, fetchImpl) {
144
169
  const url = String(item?.url || '');
145
170
  const method = String(item?.method || 'POST').toUpperCase();
@@ -203,9 +228,57 @@ function parseUserPath(pathname) {
203
228
  return m ? { handle: decodeURIComponent(m[1]), rest: m[2] } : null;
204
229
  }
205
230
 
231
+ // A public document is the same for everybody who asks, so the edge may hold
232
+ // it and answer the next reader itself. Without this every read of an actor,
233
+ // a topic, a post or a card was a function call: one visit to a forum is
234
+ // twenty of them, and every visitor and every refresh paid again.
235
+ //
236
+ // `netlify-cdn-cache-control` is the edge's own instruction; `cache-control`
237
+ // is what the reader's browser is told. The edge may serve a stale copy while
238
+ // it fetches a fresh one, so nobody waits on the pod for a document that is
239
+ // merely minutes old. The edge holds a document far longer than a browser
240
+ // is asked to: the edge is what turns a thousand readers into one function
241
+ // call, while a browser that held an actor for ten minutes would show one
242
+ // person a stale profile they had just edited.
243
+ const BROWSER_MAX_AGE = 60;
244
+ const publicFor = (seconds, stale = seconds * 4) => ({
245
+ 'cache-control': `public, max-age=${Math.min(seconds, BROWSER_MAX_AGE)}`,
246
+ 'netlify-cdn-cache-control': `public, s-maxage=${seconds}, stale-while-revalidate=${stale}`,
247
+ });
248
+ // How long the edge holds a public document, and a handle's WebFinger
249
+ // answer. Every server that has heard of an account asks for its actor and
250
+ // its handle again and again; a post or a collection changes when its owner
251
+ // acts, and ten minutes of edge is what the plan's budget of function calls
252
+ // can afford.
253
+ const PUBLIC_EDGE_SECONDS = 600;
254
+ const WEBFINGER_EDGE_SECONDS = 3600;
255
+
256
+ // Where a pod owner opts their identity in. An operator may name it, because
257
+ // the path it takes is one their pod server can no longer serve; the dot says
258
+ // it belongs to the server rather than to anybody's data, the way
259
+ // `.well-known` and `.account` do.
260
+ export const DEFAULT_RUN_PATH = '/.fediverse-account';
261
+ export const runPath = (ctx) => {
262
+ const said = String(ctx?.runPath || '').trim().replace(/\/+$/u, '');
263
+ if (!said) return DEFAULT_RUN_PATH;
264
+ return said.startsWith('/') ? said : `/${said}`;
265
+ };
266
+
267
+ // A request the browser itself says came from this very origin. `Sec-Fetch-Site`
268
+ // is the browser's word and a page cannot forge it; an `Origin` that matches is
269
+ // the older way of saying the same. This is what makes it safe to read a
270
+ // session cookie as proof: a page on another site gets neither.
271
+ const sameOriginRequest = (request, origin) => {
272
+ const site = request.headers.get('sec-fetch-site');
273
+ if (site) return site === 'same-origin';
274
+ const said = request.headers.get('origin');
275
+ if (!said || !origin) return false;
276
+ try { return new URL(said).origin === new URL(origin).origin; } catch { return false; }
277
+ };
278
+
206
279
  // Top-level paths a handle may not take, so a name never shadows a route.
207
280
  const RESERVED = new Set(['u', 'api', 'signup', 'new-account', 'run', 'admin', 'roster', 'gateway',
208
- 'gw', 'well-known', 'inbox', 'outbox', 'actor', 'install']);
281
+ 'gw', 'well-known', 'inbox', 'outbox', 'actor', 'install', 'notices']);
209
282
 
210
283
  // Why a handle is unusable, or null when it is fine. Lowercase letters, digits
211
284
  // and hyphens; 2–30 chars; not edge-hyphenated; not a reserved route.
@@ -240,7 +313,9 @@ const policyFor = (rec, fetchImpl = fetch) =>
240
313
  // `policy` is the person's published filtering policy when there is one.
241
314
  function identFor(rec, policy = null) {
242
315
  return {
243
- inboxUrl: rec.podHome + 'ap/inbox/',
316
+ // A row may name the inbox its deliveries are written to: a forum's
317
+ // categories share one, on the forum's pod, above their own trees.
318
+ inboxUrl: rec.inboxUrl || rec.podHome + 'ap/inbox/',
244
319
  actorUrl: rec.actorUrl, // the FRONT actor id (what mail is addressed to)
245
320
  followersUrl: rec.actorUrl.replace(/actor$/, 'followers'),
246
321
  notesPrefix: rec.actorUrl.replace(/ap\/actor$/, 'ap/notes/'),
@@ -259,6 +334,7 @@ function identFor(rec, policy = null) {
259
334
  // listDirectory() -> { handle: record } every row, for the admin roster
260
335
  // removeDirectory(handle) -> boolean drop a row; false when a seeded row remains
261
336
  // podPut(url, body, ct) -> boolean append to a user's pod (per-user cred inside)
337
+ // readReceived / writeReceived / dropReceived, pauseItems, closeDays — see quiet.mjs
262
338
  // podGet(url, { podHome }) -> Response read a user's pod (public reads; plain fetch
263
339
  // is fine). `podHome` is the row's own pod, for
264
340
  // an adapter that reads a store directly and so
@@ -330,20 +406,19 @@ async function route(request, ctx) {
330
406
  // The run page: a pod owner opting their identity in or out of being run by
331
407
  // this server. Meaningful only on a host that embeds the agent; a host that
332
408
  // does not simply supplies no page.
333
- if (pathname === '/run') {
409
+ if (pathname === runPath(ctx)) {
334
410
  if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
335
411
  if (!ctx.runPage) return notFound();
336
- return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: ctx.runPage };
412
+ return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' }, body: ctx.runPage };
337
413
  }
338
414
 
339
- // The roster page: the host reading who has accounts here. The page signs in
340
- // and calls the roster API below; a deploy with no admin supplies no page.
341
- // Off /admin, which is the owner's own record page on every agent.
342
- if (pathname === '/roster') {
343
- if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
344
- if (!ctx.adminPage) return notFound();
345
- return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: ctx.adminPage };
346
- }
415
+ // The roster page (/roster, off /admin, which is the owner's own record
416
+ // page on every agent) is served with the notices page, in notices.mjs.
417
+
418
+ // Notices from the operator to every account here, and the page they are
419
+ // written on (notices.mjs).
420
+ const notices = await routeNoticesApi(request, pathname, ctx, { j, verifyPodToken, publicFor, notFound });
421
+ if (notices) return notices;
347
422
 
348
423
  // The roster: every directory row, secrets stripped, for the host's own
349
424
  // eyes. The reader proves themself the way attach proves a pod — a
@@ -359,6 +434,7 @@ async function route(request, ctx) {
359
434
  handle: r.handle, kind: r.kind || 'person', fronted: !r.inboxOnly,
360
435
  podHome: r.podHome, webId: r.webId || null, actorUrl: r.actorUrl,
361
436
  address: r.address || `@${r.handle}@${ctx.host}`,
437
+ openedAt: r.openedAt || null, pausedAt: r.pausedAt || null, closedAt: r.closedAt || null,
362
438
  }))
363
439
  .sort((a, b) => (a.address || a.handle).localeCompare(b.address || b.handle));
364
440
  return j(200, { host: ctx.host, accounts });
@@ -398,6 +474,48 @@ async function route(request, ctx) {
398
474
  offersPods: !!ctx.offersPods, version: ctx.version || null });
399
475
  }
400
476
 
477
+ // What a reader's own server speaks, asked here rather than in their
478
+ // browser: a page cannot probe a stranger's server (no CORS), and the
479
+ // answer decides how that reader takes part in a forum.
480
+ if (pathname === '/api/server') {
481
+ const host = (url.searchParams.get('host') || '').trim().toLowerCase();
482
+ const open = { 'access-control-allow-origin': '*' };
483
+ const said = (kind, extra = {}) => ({ ...j(200, { host, kind, ...extra }), headers: { ...j(200, {}).headers, ...open } });
484
+ // An address at this front is a FediPod account: its client is the
485
+ // browser here, and no other server holds it. Asked before the shape of
486
+ // the host, since a front may answer at a name with no dot in it.
487
+ if (host && host === String(ctx.host || '').toLowerCase()) {
488
+ const handle = (url.searchParams.get('handle') || '').toLowerCase();
489
+ const rec = handle ? await ctx.lookup(handle) : null;
490
+ if (!rec) return said('fedipod');
491
+ // Where the owner of this pod signs in: the pod says so itself, at the
492
+ // well-known document every Solid server answers (or redirects to).
493
+ let issuer = null;
494
+ try {
495
+ const res = await safeFetch(new URL('/.well-known/openid-configuration', rec.podHome).href,
496
+ { headers: { accept: 'application/json' }, redirect: 'follow', signal: AbortSignal.timeout(SERVER_PROBE_MS) },
497
+ ctx.fetchImpl || fetch);
498
+ if (res.ok) issuer = JSON.parse(await readCapped(res, SERVER_PROBE_BYTES)).issuer || null;
499
+ } catch { /* a pod that will not say leaves the reader to type it */ }
500
+ return said('fedipod', { podHome: rec.podHome, actor: rec.actorUrl, issuer, inbox: identFor(rec).inboxUrl });
501
+ }
502
+ if (!/^[a-z0-9.-]+\.[a-z0-9-]+(:\d+)?$/u.test(host)) return said('invalid');
503
+ const f = ctx.fetchImpl || fetch;
504
+ const ask = async (path) => {
505
+ try {
506
+ const res = await safeFetch(`https://${host}${path}`,
507
+ { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(SERVER_PROBE_MS) }, f);
508
+ if (!res.ok) return null;
509
+ return JSON.parse(await readCapped(res, SERVER_PROBE_BYTES));
510
+ } catch { return null; }
511
+ };
512
+ const masto = await ask('/api/v1/instance');
513
+ if (masto) return said('mastodon-api', { name: masto.title || masto.domain || host });
514
+ const lemmy = await ask('/api/v3/site');
515
+ if (lemmy) return said('lemmy', { name: lemmy.site_view?.site?.name || host });
516
+ return said('unknown');
517
+ }
518
+
401
519
  // Attach: create the account. The user proves control of the pod they claim
402
520
  // by presenting a Solid-OIDC token whose WebID lives on that pod — no
403
521
  // password reaches the front. On success the directory gets a row and the
@@ -463,10 +581,20 @@ async function route(request, ctx) {
463
581
  const claimedActor = typeof body.actorUrl === 'string' && body.actorUrl.startsWith(podHome) ? body.actorUrl : null;
464
582
  const actorUrl = fronted ? `${userBase(ctx.frontOrigin, key)}ap/actor` : (claimedActor || podHome + 'ap/actor');
465
583
  const hmacSecret = prior?.hmacSecret || crypto.randomBytes(32).toString('base64');
584
+ // Where the door writes: the row's own tree unless the caller names an
585
+ // inbox on the same pod — a forum's categories name the forum's.
586
+ let inboxUrl = null;
587
+ if (typeof body.inboxUrl === 'string' && body.inboxUrl) {
588
+ let ok = false;
589
+ try { ok = new URL(body.inboxUrl).origin === new URL(podHome).origin && body.inboxUrl.endsWith('/'); } catch { ok = false; }
590
+ if (!ok) return j(400, { error: 'inboxUrl must be a container on the same pod' });
591
+ inboxUrl = body.inboxUrl;
592
+ }
466
593
  const record = {
467
594
  handle, address, podHome, webId: webid, actorUrl,
468
- kind: body.kind === 'group' ? 'group' : 'person',
595
+ kind: body.kind === 'group' ? 'group' : body.kind === 'application' ? 'application' : 'person',
469
596
  gatewayWebId: ctx.gatewayWebId || null, hmacSecret,
597
+ ...(inboxUrl ? { inboxUrl } : {}),
470
598
  ...(fronted ? {} : { inboxOnly: true }),
471
599
  };
472
600
  await ctx.putDirectory(key, record);
@@ -481,6 +609,43 @@ async function route(request, ctx) {
481
609
  command: `fedipod gateway ${doorInbox} --secret ${hmacSecret} --inbox-only` });
482
610
  }
483
611
 
612
+ // An address at this gateway moving to another one. Its owner, proved the
613
+ // way attach proves a pod, says where it went. From then on this front
614
+ // answers the old actor as a moved stub — the same key under the old id,
615
+ // `movedTo` the new one, so the Move the owner sends from the old address
616
+ // verifies — every other old id redirects to its new address, and the
617
+ // mail door is shut: deliveries belong at the new one. Repeating it with
618
+ // the same target changes nothing.
619
+ if (pathname === '/api/move' && request.method === 'OPTIONS') return apiPreflight();
620
+ if (pathname === '/api/move' && request.method === 'POST') {
621
+ if (!ctx.putDirectory) return withApiCors(j(501, { error: 'this front keeps no directory' }));
622
+ let body;
623
+ try { body = JSON.parse(await request.clone().text()); } catch { return withApiCors(j(400, { error: 'bad JSON' })); }
624
+ const handle = String(body.handle || '').toLowerCase();
625
+ const rec = await ctx.lookup(handle);
626
+ if (!rec) return withApiCors(j(404, { error: 'no such account' }));
627
+ if (rec.inboxOnly) return withApiCors(j(400, { error: 'this address lives on its pod; only an address at this gateway can move' }));
628
+ const movedTo = String(body.movedTo || '');
629
+ let mu;
630
+ try { mu = new URL(movedTo); } catch { return withApiCors(j(400, { error: 'movedTo must be the new actor URL' })); }
631
+ if (!((mu.protocol === 'https:' || (mu.protocol === 'http:' && isLoopbackHost(mu.hostname))) && /\/ap\/actor$/u.test(mu.pathname))) {
632
+ return withApiCors(j(400, { error: 'movedTo must be an https …/ap/actor URL' }));
633
+ }
634
+ if (ctx.frontOrigin && mu.origin === new URL(ctx.frontOrigin).origin) return withApiCors(j(400, { error: 'that address is at this gateway' }));
635
+ const webid = await verifyPodToken(request, pathname, ctx.verifier);
636
+ if (!webid) return withApiCors(j(401, { error: 'a Solid-OIDC token proving the pod is required' }));
637
+ const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
638
+ if (!owner) return withApiCors(j(403, { error: "the token proves a different pod than this account's" }));
639
+ const movedAt = rec.movedTo === movedTo && rec.movedAt ? rec.movedAt : new Date().toISOString();
640
+ await ctx.putDirectory(handle, { ...rec, movedTo, movedAt });
641
+ console.log(`front @${handle}: moved to ${movedTo}`);
642
+ return withApiCors(j(200, { ok: true, handle, movedTo, movedAt }));
643
+ }
644
+
645
+ // The owner's say over an account that goes quiet (quiet.mjs).
646
+ const quiet = await routeQuietApi(request, pathname, ctx, { j, verifyPodToken, webidUnderPod });
647
+ if (quiet) return quiet;
648
+
484
649
  // The relay: the front sends requests a browser has already signed. A page
485
650
  // may not set the Date or Host header, and both are inside an HTTP
486
651
  // signature, so a browser-run agent signs and hands the request here; the
@@ -488,24 +653,49 @@ async function route(request, ctx) {
488
653
  // a signature and cannot make one. Only the account's own key may sign,
489
654
  // only the account's own pod may call, and nothing unsigned-for reaches
490
655
  // the remote server.
656
+ if (pathname === '/api/relay' && request.method === 'OPTIONS') return apiPreflight();
491
657
  if (pathname === '/api/relay' && request.method === 'POST') {
492
658
  let body;
493
- try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
659
+ try { body = JSON.parse(await request.clone().text()); } catch { return withApiCors(j(400, { error: 'bad JSON' })); }
494
660
  const handle = String(body.handle || '').toLowerCase();
495
661
  const rec = await ctx.lookup(handle);
496
- if (!rec) { console.log(`relay: no account named ${handle}`); return j(404, { error: 'no such account' }); }
662
+ if (!rec) { console.log(`relay: no account named ${handle}`); return withApiCors(j(404, { error: 'no such account' })); }
497
663
  const webid = await verifyPodToken(request, pathname, ctx.verifier);
498
- if (!webid) { console.log(`relay @${handle}: no usable pod token`); return j(401, { error: 'a Solid-OIDC token proving the pod is required' }); }
664
+ if (!webid) { console.log(`relay @${handle}: no usable pod token`); return withApiCors(j(401, { error: 'a Solid-OIDC token proving the pod is required' })); }
499
665
  const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
500
- if (!owner) { console.log(`relay @${handle}: token is for ${webid}, not this account's pod`); return j(403, { error: "the token proves a different pod than this account's" }); }
666
+ if (!owner) { console.log(`relay @${handle}: token is for ${webid}, not this account's pod`); return withApiCors(j(403, { error: "the token proves a different pod than this account's" })); }
501
667
  const items = Array.isArray(body.requests) ? body.requests : [];
502
- if (!items.length) return j(400, { error: 'requests must be a non-empty list' });
503
- if (items.length > RELAY_MAX_REQUESTS) return j(400, { error: `at most ${RELAY_MAX_REQUESTS} requests per call` });
668
+ if (!items.length) return withApiCors(j(400, { error: 'requests must be a non-empty list' }));
669
+ // Acting through the relay is being here. Hourly at most; see noteOpened.
670
+ await noteOpened(ctx, handle, rec).catch((e) => console.log(`relay @${handle}: stamp not written: ${e?.message || e}`));
671
+ if (items.length > RELAY_MAX_REQUESTS) return withApiCors(j(400, { error: `at most ${RELAY_MAX_REQUESTS} requests per call` }));
504
672
  const results = await Promise.all(items.map((it) => relayOne(it, rec, ctx.fetchImpl || fetch)));
505
673
  // One line per relayed request, so a lookup that fails on the far side is
506
674
  // visible here and not only as an empty result in someone's browser.
507
675
  for (const r of results) console.log(`relay @${handle}: ${r.method || ''} ${r.url} → ${r.status}${r.error ? ` (${r.error})` : ''}`);
508
- return j(200, { results });
676
+ return withApiCors(j(200, { results }));
677
+ }
678
+
679
+ // Who is here, and which pod of theirs could opt in. The opt-in page asks
680
+ // this first: on a pod server that issued the reader's session, the answer
681
+ // names their pod and its address, and the page shows one button instead of
682
+ // a form. A Gateway holds no such session and says so; the page then asks
683
+ // for the pod by hand and proves it with a sign-in, as before.
684
+ if (pathname === '/api/agent' && request.method === 'GET') {
685
+ if (!ctx.agentControl) return j(501, { error: 'this server does not run identities' });
686
+ if (!ctx.webIdsFromSession) return j(200, { offered: true, session: 'unknown' });
687
+ if (!sameOriginRequest(request, ctx.frontOrigin)) return j(200, { offered: true, session: 'none' });
688
+ const webIds = await ctx.webIdsFromSession(request).catch(() => []);
689
+ if (!webIds.length) return j(200, { offered: true, session: 'none', loginUrl: '/.account/login/password/' });
690
+ const pods = [];
691
+ for (const webId of webIds) {
692
+ let podBase;
693
+ try { podBase = podBaseOfWebId(webId); } catch { continue; }
694
+ let about = null;
695
+ try { about = ctx.agentControl.describe ? await ctx.agentControl.describe({ podBase }) : null; } catch { about = null; }
696
+ pods.push({ webId, podBase, ...(about || {}) });
697
+ }
698
+ return j(200, { offered: true, session: 'ok', pods });
509
699
  }
510
700
 
511
701
  // Runtime opt-in: a pod owner asks the server this front sits on to RUN
@@ -540,15 +730,27 @@ async function route(request, ctx) {
540
730
  if (/(^|\/)\.internal(\/|$)/u.test(podUrl.pathname) || /(^|\/)\.\.(\/|$)/u.test(podUrl.pathname)) {
541
731
  return j(403, { error: 'that is not a pod address' });
542
732
  }
543
- const webid = await verifyPodToken(request, pathname, ctx.verifier);
544
- if (!webid) return j(401, { error: 'a Solid-OIDC token proving the pod is required' });
545
733
  // The pod's own server names its owner when it can, and where it does that
546
734
  // is the proof. Where it does not, the WebID must live in EXACTLY this pod
547
735
  // — its own pod base equal to podBase, not merely starting with it, which on
548
736
  // a path server an ancestor of another's pod would.
549
737
  const owners = await podOwners(podBase, ctx.fetchImpl || fetch);
550
- const proven = owners.length ? owners.includes(webid) : podBaseOfWebId(webid) === podBase;
551
- if (!proven) {
738
+ const ownsIt = (who) => (owners.length ? owners.includes(who) : podBaseOfWebId(who) === podBase);
739
+ const token = await verifyPodToken(request, pathname, ctx.verifier);
740
+ // A server that issued the reader's own session can simply read it. On a
741
+ // pod server the page, the session and the pod are one origin, and a token
742
+ // proves nothing the session does not — so opting in cost a round trip to
743
+ // an identity provider that had just said who they were. A Gateway fronting
744
+ // somebody else's pod holds no such session and this does nothing there.
745
+ //
746
+ // One account can hold several WebIDs, so it is the one that owns this pod
747
+ // that is taken, not whichever came back first.
748
+ const session = !token && ctx.webIdsFromSession && sameOriginRequest(request, ctx.frontOrigin)
749
+ ? (await ctx.webIdsFromSession(request).catch(() => [])).find(ownsIt) || null
750
+ : null;
751
+ const webid = token || session;
752
+ if (!webid) return j(401, { error: 'a Solid-OIDC token proving the pod is required' });
753
+ if (!ownsIt(webid)) {
552
754
  return j(403, { error: 'the token proves a different pod than the one you listed' });
553
755
  }
554
756
  if (action === 'opt-in') {
@@ -574,14 +776,16 @@ async function route(request, ctx) {
574
776
  //
575
777
  // The name is matched against a literal list, so nothing about the request
576
778
  // chooses a file.
577
- const mPageScript = /^\/(new-account|run|admin)\.js$/u.exec(pathname);
779
+ const mPageScript = /^\/(new-account|run|admin|notices)\.js$/u.exec(pathname);
578
780
  if (mPageScript) {
579
781
  const body = ctx.pageScripts?.[`${mPageScript[1]}.js`];
580
782
  if (!body) return notFound();
581
- return { status: 200, headers: { 'content-type': 'text/javascript; charset=utf-8' }, body };
783
+ // Revalidated on every load: a page served fresh with a script the
784
+ // browser kept from last time is the mismatch nobody can see.
785
+ return { status: 200, headers: { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-cache' }, body };
582
786
  }
583
787
 
584
- // The vendored Solid-OIDC browser library the /run and /admin pages load —
788
+ // The vendored Solid-OIDC browser library the opt-in and admin pages load —
585
789
  // served here because this function owns every path on the domain.
586
790
  if (pathname === '/solid-oidc-client.js') {
587
791
  if (!ctx.authBundle) return notFound();
@@ -602,14 +806,22 @@ async function route(request, ctx) {
602
806
  if (!m || m[2] !== ctx.host) return notFound();
603
807
  const rec = await ctx.lookup(m[1]);
604
808
  if (!rec) return notFound();
809
+ // A closed address is gone, and says so rather than pretending never to
810
+ // have existed: the name stays taken.
811
+ if ((await closedState(ctx, m[1], rec)).closed) return closedAnswer({ 'access-control-allow-origin': '*' });
605
812
  // A fronted identity's documents live on its pod; the pod's own actor id
606
813
  // is the alias, so a client signing in by the fronted address can find
607
814
  // the pod (and its login) without a lookup only the host could answer.
608
815
  const podActor = rec.inboxOnly ? [] : [rec.podHome + 'ap/actor'];
609
- const out = j(200, jrd({ handle: m[1], host: ctx.host, actor: rec.actorUrl, aliases: podActor, page: profilePageOf(rec) }),
610
- 'application/jrd+json');
816
+ const out = j(200, jrd({ handle: m[1], host: ctx.host, actor: rec.actorUrl, aliases: podActor,
817
+ page: profilePageOf(rec), subscribe: subscribeTemplate(ctx.frontOrigin) }),
818
+ 'application/jrd+json');
611
819
  // RFC 7033: a JRD is read from any origin.
612
820
  out.headers['access-control-allow-origin'] = '*';
821
+ // A handle answers the same for everybody, and changes only when a row
822
+ // does. Every server that has heard of this account asks for it; the edge
823
+ // can answer most of them.
824
+ Object.assign(out.headers, publicFor(WEBFINGER_EDGE_SECONDS));
613
825
  return out;
614
826
  }
615
827
 
@@ -634,16 +846,28 @@ async function route(request, ctx) {
634
846
  const rec = await ctx.lookup(up.handle);
635
847
  if (!rec) return notFound();
636
848
  const base = userBase(ctx.frontOrigin, up.handle);
849
+ // An account that moved to another gateway (see /api/move): where its ids
850
+ // live now. The actor below answers as a stub; everything else redirects.
851
+ const movedBase = rec.movedTo ? rec.movedTo.replace(/ap\/actor$/u, '') : null;
852
+ // A closed address (see "accounts that go quiet" above): every id under it
853
+ // is gone. Asked after moved, which a close does not undo.
854
+ const closed = movedBase ? false : (await closedState(ctx, up.handle, rec)).closed;
855
+ const gone = (headers = {}, why = `this account has moved to ${rec.movedTo}`) => closedAnswer(headers, why);
856
+ const CLOSED = 'this address is closed';
637
857
 
638
858
  // Inbox: verify at the door, forward to the user's pod inbox. This is the
639
859
  // gateway, per user.
640
860
  if (up.rest === 'ap/inbox/' || up.rest === 'ap/inbox') {
641
861
  if (request.method !== 'POST') return { status: 405, headers: {}, body: '' };
862
+ if (movedBase) { console.log(`door @${up.handle}: delivery → 410 (moved)`); return gone(); }
863
+ if (closed) { console.log(`door @${up.handle}: delivery → 410 (closed)`); return gone({}, CLOSED); }
642
864
  const policy = await policyFor(rec, ctx.fetchImpl || fetch);
643
- const { status, reason } = await handleDelivery(request, identFor(rec, policy),
644
- { podPut: (u, b, ct) => ctx.podPut(up.handle, u, b, ct), fetchImpl: ctx.fetchImpl });
865
+ const standing = await accountState(ctx, up.handle, rec);
866
+ const { status, reason, content, bytes } = await handleDelivery(request, identFor(rec, policy),
867
+ { podPut: (u, b, ct) => ctx.podPut(up.handle, u, b, ct), fetchImpl: ctx.fetchImpl, paused: standing.paused });
645
868
  // One line per delivery, so "did it arrive at the door" has an answer.
646
869
  console.log(`door @${up.handle}: delivery → ${status} (${reason})`);
870
+ if (status === 202 && content) await noteReceived(ctx, up.handle, rec, bytes);
647
871
  return { status, headers: {}, body: '' };
648
872
  }
649
873
 
@@ -669,6 +893,11 @@ async function route(request, ctx) {
669
893
  return { status: 204, headers: { ...cors, allow: 'GET, POST, OPTIONS',
670
894
  'accept-post': 'application/ld+json, application/activity+json' }, body: null };
671
895
  }
896
+ if (movedBase && request.method === 'POST') return gone(cors);
897
+ if (closed && request.method === 'POST') return gone(cors, CLOSED);
898
+ if (movedBase && (request.method === 'GET' || request.method === 'HEAD')) {
899
+ return { status: 301, headers: { ...cors, location: movedBase + up.rest, 'cache-control': 'no-store' }, body: '' };
900
+ }
672
901
  if (request.method === 'POST') {
673
902
  const json = (status, obj, extra = {}) => ({ status,
674
903
  headers: { ...cors, 'content-type': 'application/json', 'cache-control': 'no-store', ...extra },
@@ -692,7 +921,21 @@ async function route(request, ctx) {
692
921
  // Everything else is a public GET, served by reading the user's pod and
693
922
  // rewriting pod ids to the front. The actor also gets its handle and inbox
694
923
  // fixed to the front so a consumer cross-checks it consistently.
924
+ // Public documents are read from any origin — a page on another site, the
925
+ // forum's own host among them — the way a pod serves them, so the read
926
+ // carries the open origin and a preflight is answered.
927
+ const open = { 'access-control-allow-origin': '*' };
928
+ if (request.method === 'OPTIONS') {
929
+ return { status: 204, headers: { ...open, 'access-control-allow-methods': 'GET, HEAD, OPTIONS',
930
+ 'access-control-allow-headers': 'Accept', 'access-control-max-age': '86400', allow: 'GET, HEAD, OPTIONS' }, body: null };
931
+ }
695
932
  if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
933
+ // Moved: every old id but the actor's answers with where it is now, so a
934
+ // reply or a boost another server still holds under the old id resolves.
935
+ if (movedBase && up.rest !== 'ap/actor') {
936
+ return { status: 301, headers: { ...open, location: movedBase + up.rest, 'cache-control': 'no-store' }, body: '' };
937
+ }
938
+ if (closed) return gone(open, CLOSED);
696
939
  const podTarget = rec.podHome + up.rest;
697
940
  // Media stays on the pod (lib/pod/urls.mjs keeps `media` off the front), but
698
941
  // the id rewrite below turns media links onto the front like every other
@@ -705,22 +948,50 @@ async function route(request, ctx) {
705
948
  // directly (the CSS server component) has no access control of its own and
706
949
  // needs to be told what it may reach. See podHomeProblem above for the other
707
950
  // half — a row that names something which is not a pod.
951
+ // A container is Turtle, a profile page is HTML, everything else here is
952
+ // ActivityPub. Ask for what the thing can be, not for what most of them are.
953
+ const container = podTarget.endsWith('/');
954
+ const page = /\.html?$/u.test(podTarget);
955
+ const accept = container ? 'text/turtle, */*;q=0.8'
956
+ : page ? 'text/html, */*;q=0.8'
957
+ : AP_CT;
708
958
  const got = await podRoot.readPublicDocument(
709
- ctx.podGet || ((u) => fetch(u, { headers: { accept: AP_CT } })),
710
- podTarget, { podHome: rec.podHome });
711
- if (got.text === null) return { status: got.status, headers: {}, body: '' };
959
+ ctx.podGet || ((u) => fetch(u, { headers: { accept } })),
960
+ podTarget, { podHome: rec.podHome, accept });
961
+ if (got.text === null) return { status: got.status, headers: open, body: '' };
712
962
  let text = got.text;
713
963
  text = swap(text, rec.podHome, base);
964
+ // The moved stub: the pod's actor now carries the NEW gateway's ids, and a
965
+ // server verifying the Move fetches the OLD id and needs a document whose
966
+ // id is that old id — with the same key under it, and `movedTo` saying
967
+ // where the account went. Swapping the new base back to this one gives
968
+ // exactly that; alsoKnownAs and movedTo are set below.
969
+ if (movedBase) text = swap(text, movedBase, base);
714
970
  if (up.rest === 'ap/actor') {
715
971
  try {
716
972
  const doc = JSON.parse(text);
717
973
  doc.preferredUsername = up.handle; // so @handle@front cross-checks
718
974
  doc.inbox = base + 'ap/inbox/'; // deliveries come to the front to be verified
719
975
  doc.endpoints = { ...(doc.endpoints || {}), sharedInbox: base + 'ap/inbox/' };
976
+ if (movedBase) {
977
+ doc.movedTo = rec.movedTo;
978
+ const aka = new Set([].concat(doc.alsoKnownAs || []).filter((a) => typeof a === 'string' && a !== doc.id));
979
+ aka.add(rec.movedTo);
980
+ doc.alsoKnownAs = [...aka];
981
+ const ctxs = [].concat(doc['@context'] || []);
982
+ const declared = (term) => ctxs.some((c) => c && typeof c === 'object' && term in c);
983
+ if (!declared('alsoKnownAs')) ctxs.push({ alsoKnownAs: { '@id': 'as:alsoKnownAs', '@type': '@id' } });
984
+ if (!declared('movedTo')) ctxs.push({ movedTo: { '@id': 'as:movedTo', '@type': '@id' } });
985
+ doc['@context'] = ctxs;
986
+ }
720
987
  text = JSON.stringify(doc);
721
988
  } catch { /* leave the rewritten text as-is if it will not parse */ }
722
989
  }
723
- return { status: 200, headers: { 'content-type': AP_CT, 'cache-control': 'no-store' }, body: text };
990
+ // `readPublicDocument` asked for it with no credential, so a document that
991
+ // came back is one anybody can read — which is what makes it cacheable.
992
+ return { status: 200,
993
+ headers: { ...open, 'content-type': got.type || (container ? 'text/turtle' : page ? 'text/html' : AP_CT), ...publicFor(PUBLIC_EDGE_SECONDS) },
994
+ body: text };
724
995
  }
725
996
 
726
997
  export const _internal = { parseUserPath, identFor, swap, handleProblem, policyFor, policyCache };
@@ -19,9 +19,11 @@ import * as inbox from '../pod/inbox.mjs';
19
19
  const DEFAULT_MAX_BYTES = 512 * 1024; // mirror intake.mjs MAX_ITEM_BYTES
20
20
 
21
21
  // Control activities are the message itself and must always pass — you want
22
- // Follows from strangers. Only CONTENT is subject to the concerns-us drop.
23
- const CONTROL = new Set(['Follow', 'Undo', 'Accept', 'Reject', 'Delete', 'Move',
22
+ // Follows from strangers. Only CONTENT is subject to the concerns-us drop,
23
+ // and to a pause: an account nobody is reading still takes its follows.
24
+ export const CONTROL = new Set(['Follow', 'Undo', 'Accept', 'Reject', 'Delete', 'Move',
24
25
  'Add', 'Remove', 'Block']);
26
+ export const isControl = (type) => CONTROL.has(type);
25
27
 
26
28
  const idOf = (v) => (typeof v === 'string' ? v : v?.id) || null;
27
29
  const sha256hex = (s) => crypto.createHash('sha256').update(s).digest('hex');
@@ -64,8 +66,13 @@ function concernsUsAtEdge(activity, ident) {
64
66
  // resolved identity policy: { inboxUrl, actorUrl, followersUrl, notesPrefix,
65
67
  // following, blocklist, kind, gatewayWebId, hmacSecret }. `podPut(url, body,
66
68
  // contentType) → boolean` appends to the pod with the gateway's credential.
67
- // Returns { status, reason } — the adapter turns it into an HTTP response.
68
- export async function handleDelivery(request, ident, { podPut, fetchImpl = fetch, maxBytes = DEFAULT_MAX_BYTES } = {}) {
69
+ // `paused` says nobody is reading this inbox: content is accepted and
70
+ // discarded (the same quiet 202 a blocked sender gets, so nothing retries
71
+ // and nothing counts a failure against this host), control still lands.
72
+ // Returns { status, reason } — the adapter turns it into an HTTP response —
73
+ // and, for a delivery that reached the pod, `content` (whether it was
74
+ // content rather than control) and `bytes`, so the caller can keep count.
75
+ export async function handleDelivery(request, ident, { podPut, fetchImpl = fetch, maxBytes = DEFAULT_MAX_BYTES, paused = false } = {}) {
69
76
  // Read the body once from a clone; the original, unconsumed, goes to the
70
77
  // verifier (which needs the body for the Digest check).
71
78
  let raw;
@@ -80,7 +87,9 @@ export async function handleDelivery(request, ident, { podPut, fetchImpl = fetch
80
87
  // Edge drops — silent 202 so a rejected sender does not retry a delivery we
81
88
  // will never accept. None of these becomes a pod write.
82
89
  if (isBlocked(actor, ident.blocklist)) return { status: 202, reason: 'blocked' };
83
- if (!CONTROL.has(activity.type) && !concernsUsAtEdge(activity, ident)) {
90
+ const content = !CONTROL.has(activity.type);
91
+ if (content && paused) return { status: 202, reason: 'paused' };
92
+ if (content && !concernsUsAtEdge(activity, ident)) {
84
93
  return { status: 202, reason: 'does not concern us' };
85
94
  }
86
95
 
@@ -107,7 +116,7 @@ export async function handleDelivery(request, ident, { podPut, fetchImpl = fetch
107
116
  // holding any state.
108
117
  if (!okA) return { status: 502, reason: 'pod inbox write failed' };
109
118
  if (receipt) await inbox.writeReceiptBeside(podPut, ident.inboxUrl, hash, receipt);
110
- return { status: 202, reason: v.verified ? 'verified' : 'buffered-unverified' };
119
+ return { status: 202, reason: v.verified ? 'verified' : 'buffered-unverified', content, bytes: Buffer.byteLength(raw) };
111
120
  }
112
121
 
113
122
  // The outbox door: the owner's own post, taken on their behalf.