fedipod-server 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/dist/claims.js +1 -1
  2. package/dist/handler.d.ts +1 -1
  3. package/dist/handler.js +35 -2
  4. package/dist/handler.jsonld +1 -1
  5. package/lib/admin.mjs +3 -3
  6. package/lib/c2s.mjs +27 -2
  7. package/lib/deliver.mjs +6 -0
  8. package/lib/embed.mjs +1 -0
  9. package/lib/export-collections.mjs +1 -1
  10. package/lib/front-core.mjs +272 -49
  11. package/lib/gateway-core.mjs +3 -7
  12. package/lib/httpsig.mjs +33 -2
  13. package/lib/import.mjs +1 -1
  14. package/lib/intake.mjs +357 -118
  15. package/lib/links.mjs +6 -32
  16. package/lib/mastoapi.mjs +449 -38
  17. package/lib/migrate.mjs +4 -18
  18. package/lib/pod/README.md +61 -0
  19. package/lib/pod/actor.mjs +96 -0
  20. package/lib/pod/collection.mjs +77 -0
  21. package/lib/pod/containers.mjs +136 -0
  22. package/lib/pod/discovery.mjs +46 -0
  23. package/lib/pod/featured.mjs +18 -0
  24. package/lib/pod/followers.mjs +24 -0
  25. package/lib/pod/following.mjs +10 -0
  26. package/lib/pod/http.mjs +74 -0
  27. package/lib/pod/inbox.mjs +144 -0
  28. package/lib/pod/links.mjs +35 -0
  29. package/lib/pod/media.mjs +27 -0
  30. package/lib/pod/notes.mjs +80 -0
  31. package/lib/pod/notifications.mjs +89 -0
  32. package/lib/pod/outbox.mjs +18 -0
  33. package/lib/pod/package.json +19 -0
  34. package/lib/pod/policy.mjs +58 -0
  35. package/lib/pod/private.mjs +19 -0
  36. package/lib/pod/root.mjs +63 -0
  37. package/lib/pod/state.mjs +39 -0
  38. package/lib/pod/transport.mjs +477 -0
  39. package/lib/pod/urls.mjs +71 -0
  40. package/lib/polls.mjs +105 -0
  41. package/lib/publisher.mjs +357 -188
  42. package/lib/remote.mjs +30 -395
  43. package/lib/social.mjs +20 -9
  44. package/lib/storage.mjs +2 -2
  45. package/lib/store.mjs +8 -1
  46. package/lib/tagfeed.mjs +19 -2
  47. package/lib/wire.mjs +87 -67
  48. package/package.json +1 -1
  49. package/run-agent.mjs +20 -39
  50. package/web/admin/admin.js +141 -74
  51. package/web/admin/bar.css +9 -13
  52. package/web/admin/bar.js +2 -2
  53. package/web/admin/client/client.js +1 -1
  54. package/web/admin/client/index.html +5 -6
  55. package/web/admin/index.html +53 -65
  56. package/web/admin/setup/index.html +2 -1
  57. package/web/admin/setup/setup.js +1 -1
  58. package/web/admin/tokens.css +56 -0
  59. package/web/app/README.md +77 -0
  60. package/web/app/admin-facade.mjs +549 -0
  61. package/web/app/agent.mjs +490 -0
  62. package/web/app/atproto-browser.mjs +86 -0
  63. package/web/app/boot.mjs +340 -0
  64. package/web/app/deliver-relay.mjs +72 -0
  65. package/web/app/dist/boot.js +34067 -0
  66. package/web/app/dist/boot.js.map +7 -0
  67. package/web/app/dist/sw.js +53745 -0
  68. package/web/app/dist/sw.js.map +7 -0
  69. package/web/app/fediacct-browser.mjs +146 -0
  70. package/web/app/idb-kv.mjs +35 -0
  71. package/web/app/index.html +203 -0
  72. package/web/app/index.html~ +152 -0
  73. package/web/app/keys-browser.mjs +46 -0
  74. package/web/app/keystore.mjs +99 -0
  75. package/web/app/oidc-session.mjs +189 -0
  76. package/web/app/pod-auth.mjs +248 -0
  77. package/web/app/pod-remote.mjs +84 -0
  78. package/web/app/shims/_globals.mjs +2 -0
  79. package/web/app/shims/fedify-sig.mjs +77 -0
  80. package/web/app/shims/node-crypto.mjs +113 -0
  81. package/web/app/shims/node-fs.mjs +17 -0
  82. package/web/app/shims/node-path.mjs +19 -0
  83. package/web/app/shims/node-url.mjs +7 -0
  84. package/web/app/shims/prelude.js +58 -0
  85. package/web/app/shims/safefetch.mjs +58 -0
  86. package/web/app/shims/web-push.mjs +14 -0
  87. package/web/app/signup.mjs +248 -0
  88. package/web/app/site/_headers +9 -0
  89. package/web/app/site/_redirects +21 -0
  90. package/web/app/site/admin/admin.js +1181 -0
  91. package/web/app/site/admin/bar.css +60 -0
  92. package/web/app/site/admin/bar.js +61 -0
  93. package/web/app/site/admin/client/client.js +66 -0
  94. package/web/app/site/admin/client/index.html +43 -0
  95. package/web/app/site/admin/index.html +556 -0
  96. package/web/app/site/admin/setup/index.html +191 -0
  97. package/web/app/site/admin/setup/setup.js +308 -0
  98. package/web/app/site/admin/tokens.css +56 -0
  99. package/web/app/site/admin/window.css +55 -0
  100. package/web/app/site/admin/window.js +136 -0
  101. package/web/app/site/app/404.html +40 -0
  102. package/web/app/site/app/_headers +2 -0
  103. package/web/app/site/app/apple-touch-icon.png +0 -0
  104. package/web/app/site/app/assets/ICONS-DIPJeU0C.js +2 -0
  105. package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +1 -0
  106. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +183 -0
  107. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +1 -0
  108. package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
  109. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +2 -0
  110. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +1 -0
  111. package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  112. package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
  113. package/web/app/site/app/assets/chunk-aKtaBQYM.js +1 -0
  114. package/web/app/site/app/assets/compose-CFEx7wb4.js +2 -0
  115. package/web/app/site/app/assets/compose-CFEx7wb4.js.map +1 -0
  116. package/web/app/site/app/assets/compose-ZJN5xSb4.js +35 -0
  117. package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +1 -0
  118. package/web/app/site/app/assets/debug.module-YFQgwnIr.js +52 -0
  119. package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +1 -0
  120. package/web/app/site/app/assets/dom-p1YcJ5Rw.js +2 -0
  121. package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +1 -0
  122. package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  123. package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  124. package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  125. package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  126. package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +1 -0
  127. package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +1 -0
  128. package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +1 -0
  129. package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +1 -0
  130. package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +1 -0
  131. package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +1 -0
  132. package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +1 -0
  133. package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +1 -0
  134. package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +1 -0
  135. package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +1 -0
  136. package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +1 -0
  137. package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +1 -0
  138. package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +1 -0
  139. package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +1 -0
  140. package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +1 -0
  141. package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +1 -0
  142. package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +1 -0
  143. package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +1 -0
  144. package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +1 -0
  145. package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +1 -0
  146. package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +1 -0
  147. package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +1 -0
  148. package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +1 -0
  149. package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +1 -0
  150. package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +1 -0
  151. package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +1 -0
  152. package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +1 -0
  153. package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +1 -0
  154. package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +1 -0
  155. package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +1 -0
  156. package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +1 -0
  157. package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +1 -0
  158. package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +1 -0
  159. package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +1 -0
  160. package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +1 -0
  161. package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +1 -0
  162. package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +1 -0
  163. package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +1 -0
  164. package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +1 -0
  165. package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +1 -0
  166. package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +1 -0
  167. package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +1 -0
  168. package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +1 -0
  169. package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +1 -0
  170. package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +1 -0
  171. package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +1 -0
  172. package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +1 -0
  173. package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +1 -0
  174. package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +1 -0
  175. package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +1 -0
  176. package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +1 -0
  177. package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +1 -0
  178. package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +1 -0
  179. package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +1 -0
  180. package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +1 -0
  181. package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +1 -0
  182. package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +1 -0
  183. package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +1 -0
  184. package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +1 -0
  185. package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +1 -0
  186. package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +1 -0
  187. package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +1 -0
  188. package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +1 -0
  189. package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +1 -0
  190. package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +1 -0
  191. package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +1 -0
  192. package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +1 -0
  193. package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +1 -0
  194. package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +1 -0
  195. package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +1 -0
  196. package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +1 -0
  197. package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +1 -0
  198. package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +1 -0
  199. package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +1 -0
  200. package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +1 -0
  201. package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +1 -0
  202. package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +1 -0
  203. package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +1 -0
  204. package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +1 -0
  205. package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +1 -0
  206. package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +1 -0
  207. package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +1 -0
  208. package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +1 -0
  209. package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +1 -0
  210. package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +1 -0
  211. package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +1 -0
  212. package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +1 -0
  213. package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +1 -0
  214. package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +1 -0
  215. package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +1 -0
  216. package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +1 -0
  217. package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +1 -0
  218. package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +1 -0
  219. package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +1 -0
  220. package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +1 -0
  221. package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +1 -0
  222. package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +1 -0
  223. package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +1 -0
  224. package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +1 -0
  225. package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +1 -0
  226. package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +1 -0
  227. package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +1 -0
  228. package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +1 -0
  229. package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +1 -0
  230. package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +1 -0
  231. package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +1 -0
  232. package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +1 -0
  233. package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +1 -0
  234. package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +1 -0
  235. package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +1 -0
  236. package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +1 -0
  237. package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +1 -0
  238. package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +1 -0
  239. package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +1 -0
  240. package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +1 -0
  241. package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +1 -0
  242. package/web/app/site/app/assets/instances-BNPptnn-.json +333 -0
  243. package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +1 -0
  244. package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +1 -0
  245. package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +1 -0
  246. package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +1 -0
  247. package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +1 -0
  248. package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +1 -0
  249. package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +1 -0
  250. package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +1 -0
  251. package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +1 -0
  252. package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +1 -0
  253. package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +1 -0
  254. package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +1 -0
  255. package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +1 -0
  256. package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +1 -0
  257. package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +1 -0
  258. package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +1 -0
  259. package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +1 -0
  260. package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +1 -0
  261. package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +1 -0
  262. package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +1 -0
  263. package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +1 -0
  264. package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +1 -0
  265. package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +1 -0
  266. package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +1 -0
  267. package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +1 -0
  268. package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +1 -0
  269. package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +1 -0
  270. package/web/app/site/app/assets/locales/tok-CfheRbCu.js +1 -0
  271. package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +1 -0
  272. package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +1 -0
  273. package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +1 -0
  274. package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +1 -0
  275. package/web/app/site/app/assets/main-BdqNbG-a.js +26 -0
  276. package/web/app/site/app/assets/main-BdqNbG-a.js.map +1 -0
  277. package/web/app/site/app/assets/mock-home-DGhR__He.js +2 -0
  278. package/web/app/site/app/assets/mock-home-DGhR__He.js.map +1 -0
  279. package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
  280. package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  281. package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  282. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +2 -0
  283. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +1 -0
  284. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +20 -0
  285. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +1 -0
  286. package/web/app/site/app/assets/style-B1iLPTPR.css +2 -0
  287. package/web/app/site/app/assets/temml-DuyIQMei.js +97 -0
  288. package/web/app/site/app/assets/temml-DuyIQMei.js.map +1 -0
  289. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +2 -0
  290. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +1 -0
  291. package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
  292. package/web/app/site/app/assets/year-in-posts-DJv_star.js +3 -0
  293. package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +1 -0
  294. package/web/app/site/app/compose/index.html +20 -0
  295. package/web/app/site/app/favicon.ico +0 -0
  296. package/web/app/site/app/index.html +73 -0
  297. package/web/app/site/app/logo-192.png +0 -0
  298. package/web/app/site/app/logo-512.png +0 -0
  299. package/web/app/site/app/logo-badge-72.png +0 -0
  300. package/web/app/site/app/logo-maskable-512.png +0 -0
  301. package/web/app/site/app/logo-monochrome-512.png +0 -0
  302. package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
  303. package/web/app/site/app/manifest.webmanifest +1 -0
  304. package/web/app/site/app/og-image-2.jpg +0 -0
  305. package/web/app/site/app/og-image.png +0 -0
  306. package/web/app/site/app/robots.txt +2 -0
  307. package/web/app/site/app/version.json +1 -0
  308. package/web/app/site/boot.js +34067 -0
  309. package/web/app/site/index.html +203 -0
  310. package/web/app/site/sw.js +53745 -0
  311. package/web/app/sw-src.mjs +205 -0
  312. package/web/front/#new-account.html# +43 -0
  313. package/web/front/admin.html +1 -81
  314. package/web/front/admin.js +85 -0
  315. package/web/front/new-account.html +1 -9
  316. package/web/front/new-account.js +13 -0
  317. package/web/front/run.html +1 -92
  318. package/web/front/run.js +107 -0
  319. package/lib/podrdf.mjs +0 -132
package/dist/claims.js CHANGED
@@ -6,7 +6,7 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.claims = claims;
8
8
  exports.agentClaims = agentClaims;
9
- const FRONT_PATHS = new Set(['/', '/signup', '/new-account', '/run', '/admin',
9
+ const FRONT_PATHS = new Set(['/', '/signup', '/new-account', '/run', '/roster',
10
10
  '/.well-known/webfinger', '/api/handle', '/api/attach', '/api/agent',
11
11
  '/api/roster', '/api/revoke',
12
12
  // What the pages load: the sign-in library the /run and /admin pages use, and
package/dist/handler.d.ts CHANGED
@@ -18,7 +18,7 @@ export interface FediPodServerArgs {
18
18
  signupPage?: string;
19
19
  /** The run-your-identity page HTML served at /run. */
20
20
  runPage?: string;
21
- /** The accounts-roster page HTML served at /admin. */
21
+ /** The accounts-roster page HTML served at /roster. */
22
22
  adminPage?: string;
23
23
  /** Directory holding each agent identity's signing key and log. Required when runtime opt-in is on. */
24
24
  agentDataDir?: string;
package/dist/handler.js CHANGED
@@ -441,15 +441,48 @@ class FediPodServerHandler extends community_server_1.HttpHandler {
441
441
  signupPage: this.args.signupPage || webFile('new-account.html'),
442
442
  runPage: this.args.runPage || webFile('run.html'),
443
443
  adminPage: this.args.adminPage || webFile('admin.html'),
444
- // The /run and /admin pages load the sign-in library, and the signup page
444
+ // The /run and /roster pages load the sign-in library, and the signup page
445
445
  // hands out the installer command; without these the pages render but
446
446
  // cannot be used.
447
447
  authBundle: webFile('solid-oidc-client.js'),
448
+ // Each page's own script — inline until 2026-09-09, so that the pages can
449
+ // be served under `script-src 'self'` (see lib/front-core.mjs).
450
+ pageScripts: {
451
+ 'new-account.js': webFile('new-account.js'),
452
+ 'run.js': webFile('run.js'),
453
+ 'admin.js': webFile('admin.js'),
454
+ },
448
455
  installScript: webFile('install.sh'),
449
456
  lookup: (h) => this.dir.lookup(h),
450
457
  putDirectory: (h, rec) => this.dir.putDirectory(h, rec),
451
458
  podPut: (_handle, url, body, ct) => this.podPut(url, body, ct),
452
- podGet: async (url) => {
459
+ // Reads for the public proxy. `this.io.read` goes STRAIGHT into the
460
+ // resource store, which applies no access control of its own — so this is
461
+ // the one place that has to say what may be read, and it says: only
462
+ // inside the pod of the handle being served, and never a server-internal
463
+ // tree. Without it a row could name any location and this would fetch it
464
+ // (the directory, with every user's receipt secret, included).
465
+ //
466
+ // The front now refuses to attach such a row at all (podHomeProblem in
467
+ // lib/front-core.mjs); this is the same refusal at the other end, because
468
+ // a row can also arrive from a seed or an older deploy.
469
+ podGet: async (url, opts) => {
470
+ const denied = { status: 403, text: async () => '', headers: { get: () => null } };
471
+ let target;
472
+ try {
473
+ target = new URL(url);
474
+ }
475
+ catch {
476
+ return denied;
477
+ }
478
+ if (/(^|\/)\.internal(\/|$)/u.test(target.pathname))
479
+ return denied;
480
+ // Which pod asked: routeFront reads `rec.podHome + rest` and passes
481
+ // that podHome here, so the confinement is a value on the call rather
482
+ // than state on the handler. No podHome, no read.
483
+ const home = opts?.podHome;
484
+ if (!home || !url.startsWith(home))
485
+ return denied;
453
486
  const raw = await this.io.read(url);
454
487
  return raw == null
455
488
  ? { status: 404, text: async () => '', headers: { get: () => null } }
@@ -106,7 +106,7 @@
106
106
  }
107
107
  ]
108
108
  },
109
- "comment": "The accounts-roster page HTML served at /admin."
109
+ "comment": "The accounts-roster page HTML served at /roster."
110
110
  },
111
111
  {
112
112
  "@id": "fps:dist/handler.jsonld#FediPodServerHandler_args_agentDataDir",
package/lib/admin.mjs CHANGED
@@ -1379,15 +1379,15 @@ export function buildAdminSurface({ agent, gate, allowed, log = console.log,
1379
1379
  await agent.store.flush();
1380
1380
  const destCred = { ...cred, privateRoot: target };
1381
1381
  const copied = await copyPrivateHalf({
1382
- from: { state: agent.privateStorage(cred, 'state'), fediverse: agent.privateStorage(cred, 'fediverse') },
1383
- to: { state: agent.privateStorage(destCred, 'state'), fediverse: agent.privateStorage(destCred, 'fediverse') },
1382
+ from: { state: agent.privateStorage(cred, 'state') },
1383
+ to: { state: agent.privateStorage(destCred, 'state') },
1384
1384
  log,
1385
1385
  });
1386
1386
  if (target) cred.privateRoot = target; else delete cred.privateRoot;
1387
1387
  if (isCurrent(cred)) cred.layout = CURRENT_LAYOUT; else delete cred.layout;
1388
1388
  writeJsonAtomic(path.join(agent.home, 'credential.json'), cred);
1389
1389
  // connect() rebuilds everything that pointed at the old location —
1390
- // store, RDF tree, publisher, intake — and restarts them.
1390
+ // store, publisher, intake — and restarts them.
1391
1391
  agent.store.attach(agent.privateStorage(cred, 'state'));
1392
1392
  agent.stateLoaded = false;
1393
1393
  await agent.connect();
package/lib/c2s.mjs CHANGED
@@ -234,8 +234,9 @@ export class C2S {
234
234
 
235
235
  switch (activity.type) {
236
236
  case 'Create': {
237
- if (!object || (object.type && object.type !== 'Note')) {
238
- return this.send(res, 422, { error: 'only a Note (or a bare Note) can be created here' });
237
+ const makes = object?.type || 'Note';
238
+ if (!object || (makes !== 'Note' && makes !== 'Question')) {
239
+ return this.send(res, 422, { error: 'only a Note or a Question (or a bare Note) can be created here' });
239
240
  }
240
241
  const visibility = this.visibilityOf(activity, object);
241
242
  if (!visibility) {
@@ -246,6 +247,30 @@ export class C2S {
246
247
  // visible characters rather than as markup. Documented v1 limit.
247
248
  const text = String(object.source?.content ?? object.content ?? '');
248
249
  if (!text.trim()) return this.send(res, 422, { error: 'the note has no content' });
250
+
251
+ // A Question is a poll: the choices are in oneOf (pick one) or anyOf
252
+ // (pick several), each naming itself, and endTime is when it shuts.
253
+ if (makes === 'Question') {
254
+ const one = arr(object.oneOf);
255
+ const many = arr(object.anyOf);
256
+ const titles = (one.length ? one : many).map((c) => String(c?.name ?? '').trim()).filter(Boolean);
257
+ try {
258
+ const question = await agent.publisher.publishQuestion(text, {
259
+ options: titles,
260
+ multiple: !one.length && many.length > 0,
261
+ expiresAt: object.endTime || null,
262
+ inReplyTo: idOf(object.inReplyTo) || undefined,
263
+ visibility,
264
+ spoilerText: object.summary || null,
265
+ });
266
+ return this.send(res, 201,
267
+ { id: wire.createActivityId(question.id), object: question.id },
268
+ { location: wire.createActivityId(question.id) });
269
+ } catch (e) {
270
+ return this.send(res, 422, { error: e.message });
271
+ }
272
+ }
273
+
249
274
  const attachments = arr(object.attachment).map((a) => ({
250
275
  url: a?.url, mediaType: a?.mediaType,
251
276
  ...(a?.name ? { description: a.name } : {}),
package/lib/deliver.mjs CHANGED
@@ -107,6 +107,12 @@ export class Deliverer {
107
107
  current = new URL(res.headers.get('location'), current).href;
108
108
  continue;
109
109
  }
110
+ // Where this ended up, which is not always where it was asked to go.
111
+ // Callers compare the document's own id against the URL they REQUESTED,
112
+ // so without this an open redirector on a victim's origin let an
113
+ // attacker's server answer for a victim's URL and be believed.
114
+ try { Object.defineProperty(res, 'finalUrl', { value: current, configurable: true }); }
115
+ catch { /* a frozen Response: the caller falls back to the requested URL */ }
110
116
  return res;
111
117
  }
112
118
  throw new Error(`too many redirects from ${url}`);
package/lib/embed.mjs CHANGED
@@ -202,6 +202,7 @@ export async function startEmbeddedAgent({
202
202
  agent.importer?.stop();
203
203
  clearInterval(agent.schedTimer);
204
204
  clearInterval(agent.refreshTimer);
205
+ agent.publisher?.stopPolls();
205
206
  // Same order the standalone agent's shutdown uses: write what is pending,
206
207
  // then let go of the lease so the next agent need not wait out the TTL.
207
208
  await Promise.allSettled([
@@ -10,7 +10,7 @@
10
10
  // DMs, pins and standalone note documents have no place in that layout and
11
11
  // are not carried.
12
12
  //
13
- // Serialised by rdflib, never by hand — the podrdf.mjs rule.
13
+ // Serialised by rdflib, never by hand.
14
14
 
15
15
  import * as $rdf from 'rdflib';
16
16
 
@@ -17,8 +17,9 @@
17
17
 
18
18
  import crypto from 'node:crypto';
19
19
  import { handleDelivery } from './gateway-core.mjs';
20
- import { readCapped } from './safefetch.mjs';
21
- import { linkTargets, REL } from './links.mjs';
20
+ import { readCapped, safeFetch, isLoopbackHost } from './safefetch.mjs';
21
+ import * as podRoot from './pod/root.mjs';
22
+ import * as podPolicy from './pod/policy.mjs';
22
23
 
23
24
  // The one WebFinger document, spelled out here rather than imported from
24
25
  // wire.mjs: wire drags the agent's whole HTML pipeline (sanitize-html and
@@ -58,25 +59,122 @@ const OWNER_LOOKUP_MS = 5_000;
58
59
  * authority on that, so when it answers, its answer decides. A server that
59
60
  * says nothing leaves where the WebID lives as the only evidence there is.
60
61
  */
61
- async function podOwners(podBase, fetchImpl = fetch) {
62
- try {
63
- const res = await fetchImpl(podBase, {
64
- method: 'HEAD', signal: AbortSignal.timeout(OWNER_LOOKUP_MS),
65
- });
66
- return linkTargets(res?.headers?.get?.('link'), REL.owner, podBase);
67
- } catch { return []; }
68
- }
62
+ const podOwners = (podBase, fetchImpl = fetch) =>
63
+ podRoot.readOwnerLinks(fetchImpl, podBase, { timeoutMs: OWNER_LOOKUP_MS });
69
64
 
65
+ // Whether a token's WebID vouches for this pod, on the evidence of where it
66
+ // lives. Weak on purpose, and never used alone any more — see provesPod.
70
67
  function webidUnderPod(webid, podHome) {
71
68
  try { return new URL(webid).origin === new URL(podHome).origin; } catch { return false; }
72
69
  }
73
70
 
71
+ /**
72
+ * Whether this WebID may speak for this pod.
73
+ *
74
+ * Sharing an origin was the whole test, which is right where an origin is one
75
+ * person (a subdomain pod) and wrong where it is not. On a path-based server
76
+ * every account is on the same origin, so any account holder could attach
77
+ * somebody else's pod under a name of their choosing — and, worse, re-attach a
78
+ * name that already existed and be handed its receipt secret (S7).
79
+ *
80
+ * The pod server is the authority on who owns the pod, and it will say so in a
81
+ * `solid:owner` link if asked. When it answers, its answer decides. Only when
82
+ * it says nothing does this fall back to where the WebID lives, which is the
83
+ * best a silent server leaves available. This is the rule /api/agent's opt-in
84
+ * has used all along; attach simply never asked.
85
+ */
86
+ async function provesPod(webid, podHome, fetchImpl) {
87
+ const owners = await podOwners(podHome, fetchImpl || fetch);
88
+ if (owners.length) return owners.includes(webid);
89
+ return webidUnderPod(webid, podHome);
90
+ }
91
+
92
+ // A pod this front will read from at all.
93
+ //
94
+ // The front reads `rec.podHome + <the rest of the request path>` and, on the
95
+ // CSS server component, that read goes straight into the resource store with
96
+ // no access control of its own — so a row whose podHome names the FRONT is a
97
+ // row that can read the server's own internals, `/.internal/fedipod/directory/`
98
+ // included, where every user's receipt secret is kept (S2). The front is not a
99
+ // pod and never was one; saying so outright is the fix, and it closes the
100
+ // self-recursion of S8 with the same line.
101
+ function podHomeProblem(podHome, frontOrigin) {
102
+ let u;
103
+ try { u = new URL(podHome); } catch { return 'podHome is not a URL'; }
104
+ if (frontOrigin) {
105
+ try {
106
+ if (u.origin === new URL(frontOrigin).origin) {
107
+ return 'a pod cannot live on this gateway — name the pod that holds your data';
108
+ }
109
+ } catch { /* no usable frontOrigin: the other checks still apply */ }
110
+ }
111
+ // Nothing may reach a server's private trees, whatever origin they are on.
112
+ if (/(^|\/)\.internal(\/|$)/u.test(u.pathname) || /(^|\/)\.\.(\/|$)/u.test(u.pathname)) {
113
+ return 'that is not a pod address';
114
+ }
115
+ return null;
116
+ }
117
+
118
+ const RELAY_MAX_REQUESTS = 20;
119
+ const RELAY_MAX_BODY = 1024 * 1024;
120
+ const RELAY_TIMEOUT_MS = 8_000;
121
+ // The only headers a relayed request may carry to the remote server. Host
122
+ // comes from the URL; the user agent from safeFetch.
123
+ const RELAY_HEADERS = new Set(['date', 'digest', 'signature', 'content-type', 'accept']);
124
+
125
+ const keyIdOf = (signature) => (/keyId="([^"]+)"/.exec(signature || '') || [])[1] || null;
126
+
127
+ async function relayOne(item, rec, fetchImpl) {
128
+ const url = String(item?.url || '');
129
+ const method = String(item?.method || 'POST').toUpperCase();
130
+ if (method !== 'GET' && method !== 'POST') return { url, status: 0, error: 'method must be GET or POST' };
131
+ let u;
132
+ try { u = new URL(url); } catch { return { url, status: 0, error: 'not a URL' }; }
133
+ if (u.protocol !== 'https:' && !(u.protocol === 'http:' && process.env.AP_ALLOW_PRIVATE_TARGETS === '1')) {
134
+ return { url, status: 0, error: 'https only' };
135
+ }
136
+ const headers = {};
137
+ for (const [k, v] of Object.entries(item?.headers || {})) {
138
+ const name = k.toLowerCase();
139
+ if (RELAY_HEADERS.has(name) && typeof v === 'string') headers[name] = v;
140
+ }
141
+ const body = method === 'POST' ? String(item?.body ?? '') : undefined;
142
+ if (body !== undefined && Buffer.byteLength(body) > RELAY_MAX_BODY) return { url, status: 0, error: 'body too large' };
143
+ const keyId = keyIdOf(headers.signature);
144
+ if (method === 'POST' && !keyId) return { url, status: 0, error: 'a delivery must be signed' };
145
+ if (keyId && !keyId.startsWith(rec.actorUrl + '#')) return { url, status: 0, error: "signed with a key that is not this account's" };
146
+ if (headers.digest) {
147
+ const want = 'SHA-256=' + crypto.createHash('sha256').update(body || '').digest('base64');
148
+ if (headers.digest !== want) return { url, status: 0, error: 'digest does not match the body' };
149
+ }
150
+ try {
151
+ const res = await safeFetch(url, { method, headers, body, signal: AbortSignal.timeout(RELAY_TIMEOUT_MS) }, fetchImpl);
152
+ const out = { url, status: res.status };
153
+ // The far server asking to be left alone has to reach the agent that will
154
+ // do the asking again. Without this the browser build could not honour a
155
+ // Retry-After at all — every delivery it makes goes through here — and fell
156
+ // back to its own ladder against a server that had already said how long.
157
+ const retryAfter = res.headers.get('retry-after');
158
+ if (retryAfter) out.retryAfter = retryAfter;
159
+ if (method === 'GET') {
160
+ out.contentType = res.headers.get('content-type') || null;
161
+ out.body = await readCapped(res, RELAY_MAX_BODY);
162
+ }
163
+ return out;
164
+ } catch (e) { return { url, status: 0, error: e.message }; }
165
+ }
166
+
74
167
  const j = (status, obj, ct = 'application/json') =>
75
168
  ({ status, headers: { 'content-type': ct, 'cache-control': 'no-store' }, body: JSON.stringify(obj) });
76
169
  const notFound = () => ({ status: 404, headers: { 'content-type': 'text/plain' }, body: 'not found\n' });
77
170
 
78
171
  // A user's public base on the front, a 1:1 mirror of their pod home.
79
- export const userBase = (frontOrigin, handle) => `${frontOrigin}/u/${handle}/`;
172
+ // The directory key is the FULL fediverse address — handle@host never the
173
+ // bare handle, which is not unique: two different pods can each have a "me".
174
+ // A fronted identity answers at this front's host; a mail-door identity at its
175
+ // own pod's host. The `/u/` path segment carries that same key.
176
+ export const addressKey = (handle, host) => `${handle}@${host}`;
177
+ export const userBase = (frontOrigin, key) => `${frontOrigin}/u/${encodeURIComponent(key)}/`;
80
178
 
81
179
  // Rewrite every occurrence of the pod home to the front base (or back). The
82
180
  // bodies are JSON, so a whole-string swap rewrites ids wherever they appear —
@@ -90,7 +188,7 @@ function parseUserPath(pathname) {
90
188
  }
91
189
 
92
190
  // Top-level paths a handle may not take, so a name never shadows a route.
93
- const RESERVED = new Set(['u', 'api', 'signup', 'new-account', 'run', 'admin', 'gateway',
191
+ const RESERVED = new Set(['u', 'api', 'signup', 'new-account', 'run', 'admin', 'roster', 'gateway',
94
192
  'gw', 'well-known', 'inbox', 'outbox', 'actor', 'install']);
95
193
 
96
194
  // Why a handle is unusable, or null when it is fine. Lowercase letters, digits
@@ -114,19 +212,12 @@ function handleProblem(h) {
114
212
  const POLICY_TTL_MS = 5 * 60_000;
115
213
  const policyCache = new Map(); // podHome -> { at, policy }
116
214
 
117
- async function policyFor(rec, fetchImpl = fetch) {
118
- const key = rec.podHome;
119
- const hit = policyCache.get(key);
120
- if (hit && Date.now() - hit.at < POLICY_TTL_MS) return hit.policy;
121
- let policy = null;
122
- try {
123
- const res = await fetchImpl(rec.podHome + 'ap/gateway-policy.json',
124
- { headers: { accept: 'application/json' } });
125
- if (res && res.status < 400) policy = JSON.parse(await readCapped(res, 256 * 1024));
126
- } catch { /* unpublished or unreachable: the row's own fields stand */ }
127
- policyCache.set(key, { at: Date.now(), policy });
128
- return policy;
129
- }
215
+ // The cache is OURS, not the library's: on a warm serverless container one Map
216
+ // is shared across every user this front serves, so what it is keyed by is a
217
+ // correctness property. Keyed by podHome, it is one person's policy under one
218
+ // person's key.
219
+ const policyFor = (rec, fetchImpl = fetch) =>
220
+ podPolicy.read(fetchImpl, rec.podHome, { cache: policyCache, ttlMs: POLICY_TTL_MS });
130
221
 
131
222
  // The verified-delivery identity for a directory record: what gateway-core
132
223
  // needs to check what concerns the user and to append to their pod inbox.
@@ -152,8 +243,61 @@ function identFor(rec, policy = null) {
152
243
  // listDirectory() -> { handle: record } every row, for the admin roster
153
244
  // removeDirectory(handle) -> boolean drop a row; false when a seeded row remains
154
245
  // podPut(url, body, ct) -> boolean append to a user's pod (per-user cred inside)
155
- // podGet(url) -> Response read a user's pod (public reads; plain fetch is fine)
246
+ // podGet(url, { podHome }) -> Response read a user's pod (public reads; plain fetch
247
+ // is fine). `podHome` is the row's own pod, for
248
+ // an adapter that reads a store directly and so
249
+ // must confine the read itself.
156
250
  export async function routeFront(request, ctx) {
251
+ const out = await route(request, ctx);
252
+ return { ...out, headers: withSecurityHeaders(out.headers, out.body) };
253
+ }
254
+
255
+ // Every response this file makes, hardened in one place rather than in each of
256
+ // the dozen shapes below.
257
+ //
258
+ // `nosniff` matters most: the front serves user-supplied JSON straight from
259
+ // somebody's pod (the proxied actor and object documents), and without it a
260
+ // browser is free to decide for itself that a document is HTML and run what is
261
+ // inside it. The rest is the same posture the app already has — nothing may be
262
+ // framed, no base tag may be rewritten, no plugin content.
263
+ //
264
+ // A content-security-policy goes on the HTML only: it would mean nothing on a
265
+ // JSON document, and `frame-ancestors` has to be a header rather than a meta
266
+ // tag anyway.
267
+ //
268
+ // `script-src 'self'` is the one that matters, and it is only possible because
269
+ // none of these pages carries inline script any more — each has its own file and
270
+ // its own route above. A policy cannot tell an inline block the author wrote
271
+ // from one an attacker injected, so as long as any inline script has to run,
272
+ // every inline script may.
273
+ //
274
+ // `connect-src` allows https: because the pages sign in against the user's own
275
+ // pod, which is a different origin by definition and not one we can name here.
276
+ function withSecurityHeaders(headers = {}, body = null) {
277
+ const ct = String(headers['content-type'] || '');
278
+ const isHtml = ct.startsWith('text/html');
279
+ return {
280
+ ...headers,
281
+ 'x-content-type-options': 'nosniff',
282
+ 'referrer-policy': 'same-origin',
283
+ 'x-frame-options': 'SAMEORIGIN',
284
+ ...(isHtml && body ? {
285
+ 'content-security-policy': [
286
+ "default-src 'self'",
287
+ "script-src 'self'", // no inline script: see above
288
+ "style-src 'self' 'unsafe-inline'",
289
+ "img-src 'self' https: data:",
290
+ "connect-src 'self' https:", // sign-in goes to the user's own pod
291
+ "object-src 'none'", // no plugin content, ever
292
+ "base-uri 'none'", // no rewriting where relative URLs resolve
293
+ "frame-ancestors 'self'", // nobody else may frame these pages
294
+ "form-action 'self'", // a form here submits here
295
+ ].join('; '),
296
+ } : {}),
297
+ };
298
+ }
299
+
300
+ async function route(request, ctx) {
157
301
  const url = new URL(request.url);
158
302
  const { pathname } = url;
159
303
 
@@ -176,9 +320,10 @@ export async function routeFront(request, ctx) {
176
320
  return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: ctx.runPage };
177
321
  }
178
322
 
179
- // The admin page: the host reading who has accounts here. The page signs in
323
+ // The roster page: the host reading who has accounts here. The page signs in
180
324
  // and calls the roster API below; a deploy with no admin supplies no page.
181
- if (pathname === '/admin') {
325
+ // Off /admin, which is the owner's own record page on every agent.
326
+ if (pathname === '/roster') {
182
327
  if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
183
328
  if (!ctx.adminPage) return notFound();
184
329
  return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: ctx.adminPage };
@@ -197,9 +342,9 @@ export async function routeFront(request, ctx) {
197
342
  .map((r) => ({
198
343
  handle: r.handle, kind: r.kind || 'person', fronted: !r.inboxOnly,
199
344
  podHome: r.podHome, webId: r.webId || null, actorUrl: r.actorUrl,
200
- address: `@${r.handle}@${ctx.host}`,
345
+ address: r.address || `@${r.handle}@${ctx.host}`,
201
346
  }))
202
- .sort((a, b) => a.handle.localeCompare(b.handle));
347
+ .sort((a, b) => (a.address || a.handle).localeCompare(b.address || b.handle));
203
348
  return j(200, { host: ctx.host, accounts });
204
349
  }
205
350
 
@@ -213,11 +358,13 @@ export async function routeFront(request, ctx) {
213
358
  if (webid !== ctx.adminWebId) return j(403, { error: 'that WebID is not the admin of this front' });
214
359
  let body;
215
360
  try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
216
- const handle = String(body.handle || '').toLowerCase();
217
- if (!(await ctx.lookup(handle))) return j(404, { error: 'no such account' });
218
- const removed = (await ctx.removeDirectory(handle)) === true;
219
- return j(200, removed ? { handle, removed }
220
- : { handle, removed: false,
361
+ // Revoke by full address (what the roster lists); a bare handle still works
362
+ // for any legacy row keyed that way.
363
+ const key = String(body.address || body.handle || '').replace(/^@/, '').toLowerCase();
364
+ if (!key || !(await ctx.lookup(key))) return j(404, { error: 'no such account' });
365
+ const removed = (await ctx.removeDirectory(key)) === true;
366
+ return j(200, removed ? { account: key, removed }
367
+ : { account: key, removed: false,
221
368
  reason: 'this row is seeded in the deploy environment (FEDIPOD_DIRECTORY_JSON) — remove it there and redeploy' });
222
369
  }
223
370
 
@@ -247,38 +394,94 @@ export async function routeFront(request, ctx) {
247
394
  const podHome = String(body.podHome || '');
248
395
  const problem = handleProblem(handle);
249
396
  if (problem) return j(400, { error: problem });
250
- if (!/^https:\/\/\S+\/$/.test(podHome)) return j(400, { error: 'podHome must be an https URL ending in /' });
251
- if (await ctx.lookup(handle)) return j(409, { error: 'that name is taken' });
397
+ const podLoopback = (() => { try { return isLoopbackHost(new URL(podHome).hostname); } catch { return false; } })();
398
+ if (!(/^https:\/\/\S+\/$/.test(podHome) || (/^http:\/\/\S+\/$/.test(podHome) && podLoopback))) {
399
+ return j(400, { error: 'podHome must be an https URL ending in /' });
400
+ }
401
+ const badPod = podHomeProblem(podHome, ctx.frontOrigin);
402
+ if (badPod) return j(400, { error: badPod });
252
403
  const webid = await verifyPodToken(request, pathname, ctx.verifier);
253
404
  if (!webid) return j(401, { error: 'a Solid-OIDC token proving the pod is required' });
254
- if (!webidUnderPod(webid, podHome)) {
405
+ if (!await provesPod(webid, podHome, ctx.fetchImpl)) {
255
406
  return j(403, { error: 'the token proves a different pod than the one you listed' });
256
407
  }
257
408
  // The usual attachment keeps the user's identity on their pod and moves
258
409
  // only the mail door here (@me@mypod). Fronted identity — actor ids on
259
410
  // this domain — remains available to an explicit `fronted: true`.
260
411
  const fronted = body.fronted === true;
261
- const actorUrl = fronted ? `${ctx.frontOrigin}/u/${handle}/ap/actor` : podHome + 'ap/actor';
262
- const hmacSecret = crypto.randomBytes(32).toString('base64');
412
+ // The full address is the key: a fronted name answers at this front's host,
413
+ // a mail-door name at its pod's host. The same handle on two pods is two
414
+ // distinct addresses, so it never collides.
415
+ let addressHost; try { addressHost = fronted ? ctx.host : new URL(podHome).host; } catch { return j(400, { error: 'bad podHome' }); }
416
+ // A fronted name answers at this front's host, where the handle alone is the
417
+ // whole address and is unique — so it keeps keying by bare handle (and its
418
+ // actor id stays /u/<handle>/, unchanged for existing accounts). A mail-door
419
+ // name answers at its own pod, where "me" is not unique, so it keys by the
420
+ // full address handle@podhost, which the /u/ segment then carries.
421
+ const key = fronted ? handle : addressKey(handle, addressHost);
422
+ const address = `@${handle}@${addressHost}`;
423
+ // "Taken" only when this exact address already belongs to a DIFFERENT pod.
424
+ // Re-attaching your own pod — a retry, a re-provision, a second browser — is
425
+ // idempotent: it updates the row in place and keeps the same door secret.
426
+ const prior = await ctx.lookup(key);
427
+ if (prior && prior.podHome !== podHome) return j(409, { error: 'that name is taken' });
428
+ // Re-attaching is idempotent — a retry, a re-provision, a second browser —
429
+ // and it hands back the EXISTING receipt secret. So it has to be the same
430
+ // person: matching podHome was enough on a subdomain server and, on a
431
+ // path-based one, meant any co-tenant could re-attach your row, flip its
432
+ // kind, and be given your secret (S7). A row that predates `webId` has
433
+ // nothing to compare and falls back to the pod check above.
434
+ if (prior?.webId && prior.webId !== webid) {
435
+ return j(409, { error: 'that name belongs to a different account on this pod' });
436
+ }
437
+ // The pod's actor lives under its AP root container, which the pod owner
438
+ // knows and the front does not — so take the caller's actorUrl when it is on
439
+ // this pod, and fall back only when none is given.
440
+ const claimedActor = typeof body.actorUrl === 'string' && body.actorUrl.startsWith(podHome) ? body.actorUrl : null;
441
+ const actorUrl = fronted ? `${userBase(ctx.frontOrigin, key)}ap/actor` : (claimedActor || podHome + 'ap/actor');
442
+ const hmacSecret = prior?.hmacSecret || crypto.randomBytes(32).toString('base64');
263
443
  const record = {
264
- handle, podHome, webId: webid, actorUrl,
444
+ handle, address, podHome, webId: webid, actorUrl,
265
445
  kind: body.kind === 'group' ? 'group' : 'person',
266
446
  gatewayWebId: ctx.gatewayWebId || null, hmacSecret,
267
447
  ...(fronted ? {} : { inboxOnly: true }),
268
448
  };
269
- await ctx.putDirectory(handle, record);
449
+ await ctx.putDirectory(key, record);
270
450
  // The secret is returned ONCE, with the one command the user runs to
271
451
  // point their agent at this gateway.
272
- const doorInbox = `${ctx.frontOrigin}/u/${handle}/ap/inbox/`;
452
+ const doorInbox = `${userBase(ctx.frontOrigin, key)}ap/inbox/`;
273
453
  if (fronted) {
274
- return j(201, { ok: true, handle, actorUrl, frontActor: actorUrl, hmacSecret,
275
- address: `@${handle}@${ctx.host}`,
454
+ return j(201, { ok: true, handle, address, actorUrl, frontActor: actorUrl, hmacSecret,
276
455
  command: `fedipod gateway ${actorUrl} --secret ${hmacSecret}` });
277
456
  }
278
- return j(201, { ok: true, handle, doorInbox, hmacSecret,
457
+ return j(201, { ok: true, handle, address, doorInbox, hmacSecret,
279
458
  command: `fedipod gateway ${doorInbox} --secret ${hmacSecret} --inbox-only` });
280
459
  }
281
460
 
461
+ // The relay: the front sends requests a browser has already signed. A page
462
+ // may not set the Date or Host header, and both are inside an HTTP
463
+ // signature, so a browser-run agent signs and hands the request here; the
464
+ // front sends it with exactly the headers that were signed. It can forward
465
+ // a signature and cannot make one. Only the account's own key may sign,
466
+ // only the account's own pod may call, and nothing unsigned-for reaches
467
+ // the remote server.
468
+ if (pathname === '/api/relay' && request.method === 'POST') {
469
+ let body;
470
+ try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
471
+ const handle = String(body.handle || '').toLowerCase();
472
+ const rec = await ctx.lookup(handle);
473
+ if (!rec) return j(404, { error: 'no such account' });
474
+ const webid = await verifyPodToken(request, pathname, ctx.verifier);
475
+ if (!webid) return j(401, { error: 'a Solid-OIDC token proving the pod is required' });
476
+ const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
477
+ if (!owner) return j(403, { error: "the token proves a different pod than this account's" });
478
+ const items = Array.isArray(body.requests) ? body.requests : [];
479
+ if (!items.length) return j(400, { error: 'requests must be a non-empty list' });
480
+ if (items.length > RELAY_MAX_REQUESTS) return j(400, { error: `at most ${RELAY_MAX_REQUESTS} requests per call` });
481
+ const results = await Promise.all(items.map((it) => relayOne(it, rec, ctx.fetchImpl || fetch)));
482
+ return j(200, { results });
483
+ }
484
+
282
485
  // Runtime opt-in: a pod owner asks the server this front sits on to RUN
283
486
  // their identity. Same proof as attach — a Solid-OIDC token — but the
284
487
  // ownership check is stricter: the WebID must live UNDER the pod base, not
@@ -327,6 +530,20 @@ export async function routeFront(request, ctx) {
327
530
  return j(400, { error: 'action must be opt-in or opt-out' });
328
531
  }
329
532
 
533
+ // Each page's own script. They were inline until 2026-09-09; out here the
534
+ // pages can be served under `script-src 'self'`, which is the directive that
535
+ // tells running code from injected markup — and inline script defeats it by
536
+ // construction. Same mechanism the vendored library below has always used.
537
+ //
538
+ // The name is matched against a literal list, so nothing about the request
539
+ // chooses a file.
540
+ const mPageScript = /^\/(new-account|run|admin)\.js$/u.exec(pathname);
541
+ if (mPageScript) {
542
+ const body = ctx.pageScripts?.[`${mPageScript[1]}.js`];
543
+ if (!body) return notFound();
544
+ return { status: 200, headers: { 'content-type': 'text/javascript; charset=utf-8' }, body };
545
+ }
546
+
330
547
  // The vendored Solid-OIDC browser library the /run and /admin pages load —
331
548
  // served here because this function owns every path on the domain.
332
549
  if (pathname === '/solid-oidc-client.js') {
@@ -373,9 +590,15 @@ export async function routeFront(request, ctx) {
373
590
  // fixed to the front so a consumer cross-checks it consistently.
374
591
  if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
375
592
  const podTarget = rec.podHome + up.rest;
376
- const res = await (ctx.podGet ? ctx.podGet(podTarget) : fetch(podTarget, { headers: { accept: AP_CT } }));
377
- if (!res || res.status >= 400) return { status: res?.status || 502, headers: {}, body: '' };
378
- let text = await readCapped(res, 1024 * 1024);
593
+ // The pod this read belongs to travels with it: an adapter reading a store
594
+ // directly (the CSS server component) has no access control of its own and
595
+ // needs to be told what it may reach. See podHomeProblem above for the other
596
+ // half — a row that names something which is not a pod.
597
+ const got = await podRoot.readPublicDocument(
598
+ ctx.podGet || ((u) => fetch(u, { headers: { accept: AP_CT } })),
599
+ podTarget, { podHome: rec.podHome });
600
+ if (got.text === null) return { status: got.status, headers: {}, body: '' };
601
+ let text = got.text;
379
602
  text = swap(text, rec.podHome, base);
380
603
  if (up.rest === 'ap/actor') {
381
604
  try {
@@ -14,6 +14,7 @@
14
14
 
15
15
  import crypto from 'node:crypto';
16
16
  import { verifyHttpSignature, makeSafeLoader, makeReceipt, signReceipt } from './httpsig.mjs';
17
+ import * as inbox from './pod/inbox.mjs';
17
18
 
18
19
  const DEFAULT_MAX_BYTES = 512 * 1024; // mirror intake.mjs MAX_ITEM_BYTES
19
20
 
@@ -100,17 +101,12 @@ export async function handleDelivery(request, ident, { podPut, fetchImpl = fetch
100
101
  ? signReceipt(makeReceipt(v, { gateway: ident.gatewayWebId }), ident.hmacSecret)
101
102
  : null;
102
103
  const hash = sha256hex(raw);
103
- const okA = await podPut(ident.inboxUrl + hash, raw, 'application/activity+json');
104
+ const okA = await inbox.appendVerifiedDelivery(podPut, ident.inboxUrl, hash, raw);
104
105
  // A pod-write failure returns 5xx so the ORIGIN retries over its own ladder —
105
106
  // that is what preserves the pod's buffer property without the gateway
106
107
  // holding any state.
107
108
  if (!okA) return { status: 502, reason: 'pod inbox write failed' };
108
- // The receipt is best-effort, like _archive: a delivery that landed but whose
109
- // receipt did not is simply unverified, never lost.
110
- if (receipt) {
111
- await podPut(ident.inboxUrl + hash + '.receipt.json', JSON.stringify(receipt), 'application/json')
112
- .catch(() => {});
113
- }
109
+ if (receipt) await inbox.writeReceiptBeside(podPut, ident.inboxUrl, hash, receipt);
114
110
  return { status: 202, reason: v.verified ? 'verified' : 'buffered-unverified' };
115
111
  }
116
112