fedipod-server 0.10.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 (316) 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/deliver.mjs +6 -0
  7. package/lib/export-collections.mjs +1 -1
  8. package/lib/front-core.mjs +272 -49
  9. package/lib/gateway-core.mjs +3 -7
  10. package/lib/httpsig.mjs +33 -2
  11. package/lib/import.mjs +1 -1
  12. package/lib/intake.mjs +335 -118
  13. package/lib/links.mjs +6 -32
  14. package/lib/mastoapi.mjs +372 -35
  15. package/lib/migrate.mjs +4 -18
  16. package/lib/pod/README.md +61 -0
  17. package/lib/pod/actor.mjs +96 -0
  18. package/lib/pod/collection.mjs +77 -0
  19. package/lib/pod/containers.mjs +136 -0
  20. package/lib/pod/discovery.mjs +46 -0
  21. package/lib/pod/featured.mjs +18 -0
  22. package/lib/pod/followers.mjs +24 -0
  23. package/lib/pod/following.mjs +10 -0
  24. package/lib/pod/http.mjs +74 -0
  25. package/lib/pod/inbox.mjs +144 -0
  26. package/lib/pod/links.mjs +35 -0
  27. package/lib/pod/media.mjs +27 -0
  28. package/lib/pod/notes.mjs +80 -0
  29. package/lib/pod/notifications.mjs +89 -0
  30. package/lib/pod/outbox.mjs +18 -0
  31. package/lib/pod/package.json +19 -0
  32. package/lib/pod/policy.mjs +58 -0
  33. package/lib/pod/private.mjs +19 -0
  34. package/lib/pod/root.mjs +63 -0
  35. package/lib/pod/state.mjs +39 -0
  36. package/lib/pod/transport.mjs +477 -0
  37. package/lib/pod/urls.mjs +71 -0
  38. package/lib/publisher.mjs +112 -183
  39. package/lib/remote.mjs +30 -395
  40. package/lib/social.mjs +18 -9
  41. package/lib/storage.mjs +2 -2
  42. package/lib/store.mjs +8 -1
  43. package/lib/tagfeed.mjs +19 -2
  44. package/lib/wire.mjs +48 -67
  45. package/package.json +1 -1
  46. package/run-agent.mjs +14 -39
  47. package/web/admin/admin.js +141 -74
  48. package/web/admin/bar.css +9 -13
  49. package/web/admin/bar.js +2 -2
  50. package/web/admin/client/client.js +1 -1
  51. package/web/admin/client/index.html +5 -6
  52. package/web/admin/index.html +53 -65
  53. package/web/admin/setup/index.html +2 -1
  54. package/web/admin/setup/setup.js +1 -1
  55. package/web/admin/tokens.css +56 -0
  56. package/web/app/README.md +77 -0
  57. package/web/app/admin-facade.mjs +549 -0
  58. package/web/app/agent.mjs +490 -0
  59. package/web/app/atproto-browser.mjs +86 -0
  60. package/web/app/boot.mjs +340 -0
  61. package/web/app/deliver-relay.mjs +72 -0
  62. package/web/app/dist/boot.js +34067 -0
  63. package/web/app/dist/boot.js.map +7 -0
  64. package/web/app/dist/sw.js +53745 -0
  65. package/web/app/dist/sw.js.map +7 -0
  66. package/web/app/fediacct-browser.mjs +146 -0
  67. package/web/app/idb-kv.mjs +35 -0
  68. package/web/app/index.html +203 -0
  69. package/web/app/index.html~ +152 -0
  70. package/web/app/keys-browser.mjs +46 -0
  71. package/web/app/keystore.mjs +99 -0
  72. package/web/app/oidc-session.mjs +189 -0
  73. package/web/app/pod-auth.mjs +248 -0
  74. package/web/app/pod-remote.mjs +84 -0
  75. package/web/app/shims/_globals.mjs +2 -0
  76. package/web/app/shims/fedify-sig.mjs +77 -0
  77. package/web/app/shims/node-crypto.mjs +113 -0
  78. package/web/app/shims/node-fs.mjs +17 -0
  79. package/web/app/shims/node-path.mjs +19 -0
  80. package/web/app/shims/node-url.mjs +7 -0
  81. package/web/app/shims/prelude.js +58 -0
  82. package/web/app/shims/safefetch.mjs +58 -0
  83. package/web/app/shims/web-push.mjs +14 -0
  84. package/web/app/signup.mjs +248 -0
  85. package/web/app/site/_headers +9 -0
  86. package/web/app/site/_redirects +21 -0
  87. package/web/app/site/admin/admin.js +1181 -0
  88. package/web/app/site/admin/bar.css +60 -0
  89. package/web/app/site/admin/bar.js +61 -0
  90. package/web/app/site/admin/client/client.js +66 -0
  91. package/web/app/site/admin/client/index.html +43 -0
  92. package/web/app/site/admin/index.html +556 -0
  93. package/web/app/site/admin/setup/index.html +191 -0
  94. package/web/app/site/admin/setup/setup.js +308 -0
  95. package/web/app/site/admin/tokens.css +56 -0
  96. package/web/app/site/admin/window.css +55 -0
  97. package/web/app/site/admin/window.js +136 -0
  98. package/web/app/site/app/404.html +40 -0
  99. package/web/app/site/app/_headers +2 -0
  100. package/web/app/site/app/apple-touch-icon.png +0 -0
  101. package/web/app/site/app/assets/ICONS-DIPJeU0C.js +2 -0
  102. package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +1 -0
  103. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +183 -0
  104. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +1 -0
  105. package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
  106. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +2 -0
  107. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +1 -0
  108. package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  109. package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
  110. package/web/app/site/app/assets/chunk-aKtaBQYM.js +1 -0
  111. package/web/app/site/app/assets/compose-CFEx7wb4.js +2 -0
  112. package/web/app/site/app/assets/compose-CFEx7wb4.js.map +1 -0
  113. package/web/app/site/app/assets/compose-ZJN5xSb4.js +35 -0
  114. package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +1 -0
  115. package/web/app/site/app/assets/debug.module-YFQgwnIr.js +52 -0
  116. package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +1 -0
  117. package/web/app/site/app/assets/dom-p1YcJ5Rw.js +2 -0
  118. package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +1 -0
  119. package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  120. package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  121. package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  122. package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  123. package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +1 -0
  124. package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +1 -0
  125. package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +1 -0
  126. package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +1 -0
  127. package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +1 -0
  128. package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +1 -0
  129. package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +1 -0
  130. package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +1 -0
  131. package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +1 -0
  132. package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +1 -0
  133. package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +1 -0
  134. package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +1 -0
  135. package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +1 -0
  136. package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +1 -0
  137. package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +1 -0
  138. package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +1 -0
  139. package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +1 -0
  140. package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +1 -0
  141. package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +1 -0
  142. package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +1 -0
  143. package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +1 -0
  144. package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +1 -0
  145. package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +1 -0
  146. package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +1 -0
  147. package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +1 -0
  148. package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +1 -0
  149. package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +1 -0
  150. package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +1 -0
  151. package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +1 -0
  152. package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +1 -0
  153. package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +1 -0
  154. package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +1 -0
  155. package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +1 -0
  156. package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +1 -0
  157. package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +1 -0
  158. package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +1 -0
  159. package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +1 -0
  160. package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +1 -0
  161. package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +1 -0
  162. package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +1 -0
  163. package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +1 -0
  164. package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +1 -0
  165. package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +1 -0
  166. package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +1 -0
  167. package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +1 -0
  168. package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +1 -0
  169. package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +1 -0
  170. package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +1 -0
  171. package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +1 -0
  172. package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +1 -0
  173. package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +1 -0
  174. package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +1 -0
  175. package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +1 -0
  176. package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +1 -0
  177. package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +1 -0
  178. package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +1 -0
  179. package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +1 -0
  180. package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +1 -0
  181. package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +1 -0
  182. package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +1 -0
  183. package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +1 -0
  184. package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +1 -0
  185. package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +1 -0
  186. package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +1 -0
  187. package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +1 -0
  188. package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +1 -0
  189. package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +1 -0
  190. package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +1 -0
  191. package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +1 -0
  192. package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +1 -0
  193. package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +1 -0
  194. package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +1 -0
  195. package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +1 -0
  196. package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +1 -0
  197. package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +1 -0
  198. package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +1 -0
  199. package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +1 -0
  200. package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +1 -0
  201. package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +1 -0
  202. package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +1 -0
  203. package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +1 -0
  204. package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +1 -0
  205. package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +1 -0
  206. package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +1 -0
  207. package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +1 -0
  208. package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +1 -0
  209. package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +1 -0
  210. package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +1 -0
  211. package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +1 -0
  212. package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +1 -0
  213. package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +1 -0
  214. package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +1 -0
  215. package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +1 -0
  216. package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +1 -0
  217. package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +1 -0
  218. package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +1 -0
  219. package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +1 -0
  220. package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +1 -0
  221. package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +1 -0
  222. package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +1 -0
  223. package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +1 -0
  224. package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +1 -0
  225. package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +1 -0
  226. package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +1 -0
  227. package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +1 -0
  228. package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +1 -0
  229. package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +1 -0
  230. package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +1 -0
  231. package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +1 -0
  232. package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +1 -0
  233. package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +1 -0
  234. package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +1 -0
  235. package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +1 -0
  236. package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +1 -0
  237. package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +1 -0
  238. package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +1 -0
  239. package/web/app/site/app/assets/instances-BNPptnn-.json +333 -0
  240. package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +1 -0
  241. package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +1 -0
  242. package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +1 -0
  243. package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +1 -0
  244. package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +1 -0
  245. package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +1 -0
  246. package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +1 -0
  247. package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +1 -0
  248. package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +1 -0
  249. package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +1 -0
  250. package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +1 -0
  251. package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +1 -0
  252. package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +1 -0
  253. package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +1 -0
  254. package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +1 -0
  255. package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +1 -0
  256. package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +1 -0
  257. package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +1 -0
  258. package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +1 -0
  259. package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +1 -0
  260. package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +1 -0
  261. package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +1 -0
  262. package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +1 -0
  263. package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +1 -0
  264. package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +1 -0
  265. package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +1 -0
  266. package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +1 -0
  267. package/web/app/site/app/assets/locales/tok-CfheRbCu.js +1 -0
  268. package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +1 -0
  269. package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +1 -0
  270. package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +1 -0
  271. package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +1 -0
  272. package/web/app/site/app/assets/main-BdqNbG-a.js +26 -0
  273. package/web/app/site/app/assets/main-BdqNbG-a.js.map +1 -0
  274. package/web/app/site/app/assets/mock-home-DGhR__He.js +2 -0
  275. package/web/app/site/app/assets/mock-home-DGhR__He.js.map +1 -0
  276. package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
  277. package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  278. package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  279. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +2 -0
  280. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +1 -0
  281. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +20 -0
  282. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +1 -0
  283. package/web/app/site/app/assets/style-B1iLPTPR.css +2 -0
  284. package/web/app/site/app/assets/temml-DuyIQMei.js +97 -0
  285. package/web/app/site/app/assets/temml-DuyIQMei.js.map +1 -0
  286. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +2 -0
  287. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +1 -0
  288. package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
  289. package/web/app/site/app/assets/year-in-posts-DJv_star.js +3 -0
  290. package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +1 -0
  291. package/web/app/site/app/compose/index.html +20 -0
  292. package/web/app/site/app/favicon.ico +0 -0
  293. package/web/app/site/app/index.html +73 -0
  294. package/web/app/site/app/logo-192.png +0 -0
  295. package/web/app/site/app/logo-512.png +0 -0
  296. package/web/app/site/app/logo-badge-72.png +0 -0
  297. package/web/app/site/app/logo-maskable-512.png +0 -0
  298. package/web/app/site/app/logo-monochrome-512.png +0 -0
  299. package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
  300. package/web/app/site/app/manifest.webmanifest +1 -0
  301. package/web/app/site/app/og-image-2.jpg +0 -0
  302. package/web/app/site/app/og-image.png +0 -0
  303. package/web/app/site/app/robots.txt +2 -0
  304. package/web/app/site/app/version.json +1 -0
  305. package/web/app/site/boot.js +34067 -0
  306. package/web/app/site/index.html +203 -0
  307. package/web/app/site/sw.js +53745 -0
  308. package/web/app/sw-src.mjs +205 -0
  309. package/web/front/#new-account.html# +43 -0
  310. package/web/front/admin.html +1 -81
  311. package/web/front/admin.js +85 -0
  312. package/web/front/new-account.html +1 -9
  313. package/web/front/new-account.js +13 -0
  314. package/web/front/run.html +1 -92
  315. package/web/front/run.js +107 -0
  316. package/lib/podrdf.mjs +0 -132
package/lib/publisher.mjs CHANGED
@@ -1,6 +1,6 @@
1
- // publisher.mjs — builds/maintains the actor's public face on the remote pod
2
- // (webfinger, actor doc, collections, notes) and mirrors truth into the local
3
- // pod. The remote /ap/ tree is disposable: publishProfile() rebuilds it.
1
+ // publisher.mjs — builds/maintains the actor's public face on the remote pod:
2
+ // webfinger, actor doc, collections, notes. The /ap/ tree is disposable:
3
+ // publishProfile() rebuilds it.
4
4
 
5
5
  import crypto from 'node:crypto';
6
6
  import fs from 'node:fs';
@@ -10,6 +10,18 @@ import * as wire from './wire.mjs';
10
10
  import * as polls from './polls.mjs';
11
11
  import { USER_AGENT } from './ua.mjs';
12
12
  import { HTTP_TIMEOUT_MS } from './safefetch.mjs';
13
+ import * as containers from './pod/containers.mjs';
14
+ import * as discovery from './pod/discovery.mjs';
15
+ import * as podActor from './pod/actor.mjs';
16
+ import * as podInbox from './pod/inbox.mjs';
17
+ import * as podOutbox from './pod/outbox.mjs';
18
+ import * as podFollowers from './pod/followers.mjs';
19
+ import * as podFollowing from './pod/following.mjs';
20
+ import * as podFeatured from './pod/featured.mjs';
21
+ import * as podPrivate from './pod/private.mjs';
22
+ import * as podPolicy from './pod/policy.mjs';
23
+ import * as podNotes from './pod/notes.mjs';
24
+ import * as podMedia from './pod/media.mjs';
13
25
 
14
26
  const ACCEPT_AP = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
15
27
  // The default for publishCollections: the whole public surface, ACLs included.
@@ -28,12 +40,11 @@ const AGENT_VERSION = JSON.parse(fs.readFileSync(
28
40
  path.join(path.dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf8')).version;
29
41
 
30
42
  export class Publisher {
31
- constructor({ config, remote, local, store, deliverer, publicKeyPem, assertionKey = null, log = console.log,
32
- probeFetch = null, resolveMention = null, privateOnPod = true, clientOrigin = null,
43
+ constructor({ config, remote, store, deliverer, publicKeyPem, assertionKey = null, log = console.log,
44
+ probeFetch = null, resolveMention = null, clientOrigin = null,
33
45
  }) {
34
46
  this.config = config;
35
47
  this.remote = remote;
36
- this.local = local;
37
48
  this.store = store;
38
49
  this.deliverer = deliverer;
39
50
  this.publicKeyPem = publicKeyPem;
@@ -53,7 +64,6 @@ export class Publisher {
53
64
  // socket opened to that pod. Tests inject their own.
54
65
  this.probeFetch = probeFetch || ((u, i) => this.remote.probe(u, i));
55
66
  this.resolveMention = resolveMention;
56
- this.privateOnPod = privateOnPod;
57
67
  // Per-poll rewrite windows, keyed by question id. See POLL_REWRITE_MS.
58
68
  this.pollTimers = new Map();
59
69
  this.log = log;
@@ -118,34 +128,25 @@ export class Publisher {
118
128
  return { unreachable: [], updated: 0, skipped: true };
119
129
  }
120
130
 
121
- await this.remote.putJson(urls.webfinger,
122
- wire.jrd({ handle: this.config.handle, host, actor: urls.actor }), 'application/jrd+json');
123
- await this.remote.setAcl(urls.webfinger, ['Read']);
131
+ await discovery.writeWebfinger(this.remote, urls,
132
+ wire.jrd({ handle: this.config.handle, host, actor: urls.actor }));
133
+ await discovery.writeHostMeta(this.remote, urls, wire.hostMeta(urls.base));
124
134
 
125
- const hostMetaUrl = urls.base + '.well-known/host-meta';
126
- await this.remote.put(hostMetaUrl, wire.hostMeta(urls.base), 'application/xrd+xml');
127
- await this.remote.setAcl(hostMetaUrl, ['Read']);
128
-
129
- // NodeInfo: pointer at the protocol-required root, document in our tree.
130
135
  const nodeinfoDocUrl = urls.home + 'ap/nodeinfo-2.0';
131
136
  const localPosts = this.store.getStatuses().filter(s => s.kind === 'post').length;
132
- await this.remote.putJson(urls.base + '.well-known/nodeinfo',
133
- wire.nodeinfoPointer(nodeinfoDocUrl), 'application/json');
134
- await this.remote.setAcl(urls.base + '.well-known/nodeinfo', ['Read']);
135
- await this.remote.putJson(nodeinfoDocUrl,
136
- wire.nodeinfoDoc({ version: AGENT_VERSION, localPosts }), 'application/json');
137
- await this.remote.setAcl(nodeinfoDocUrl, ['Read']);
137
+ await discovery.writeNodeinfo(this.remote, urls, {
138
+ pointer: wire.nodeinfoPointer(nodeinfoDocUrl),
139
+ doc: wire.nodeinfoDoc({ version: AGENT_VERSION, localPosts }),
140
+ });
138
141
 
139
142
  const actor = actorDoc; // built above, for the digest
140
- await this.remote.putJson(urls.actor, actor);
141
- await this.remote.setAcl(urls.actor, ['Read']);
143
+ await podActor.write(this.remote, urls, actor);
142
144
 
143
145
  // FEP-1b12: the moderator roster, public — it is what a recipient
144
146
  // validates a group's announced moderation against.
145
147
  if (moderators) {
146
- await this.remote.putJson(urls.moderators,
148
+ await podFeatured.writeModerators(this.remote, urls,
147
149
  wire.orderedCollection(urls.moderators, this.config.moderators));
148
- await this.remote.setAcl(urls.moderators, ['Read']);
149
150
  }
150
151
  // The gateway policy doc: the PUBLIC data a keyless gateway reads to decide
151
152
  // what concerns this identity. Written only while a gateway is advertised.
@@ -153,20 +154,19 @@ export class Publisher {
153
154
 
154
155
  // The human half: a page a browser can open and follow from. The actor
155
156
  // document is for servers; this is the address you hand to a person.
156
- await this.remote.put(urls.profileHtml, wire.profilePageHtml({
157
+ await podActor.writeProfilePage(this.remote, urls, wire.profilePageHtml({
157
158
  name: this.config.name || this.config.handle,
158
159
  address: wire.webfingerHost(urls.base) ? `@${this.config.handle}@${host}` : urls.actor,
159
160
  summary: this.config.summary ? wire.contentHtml(this.config.summary) : null,
160
161
  icon: this.config.icon || null,
161
162
  kind: this.config.kind,
162
- }), 'text/html');
163
- await this.remote.setAcl(urls.profileHtml, ['Read']);
163
+ }));
164
164
 
165
165
  // WebID → actor: the profile card lists the actor as a foaf:account.
166
166
  // Best-effort — a profile that cannot be read or edited does not stop the
167
167
  // publish, it is logged and the rest of the surface still goes up.
168
168
  try {
169
- const wrote = await this.remote.linkAccountInProfile({
169
+ const wrote = await podActor.linkInWebIdProfile(this.remote, {
170
170
  actorUrl: urls.actor,
171
171
  accountName: `@${this.config.handle}@${host}`,
172
172
  kind: this.config.kind,
@@ -179,21 +179,15 @@ export class Publisher {
179
179
  // inbox: public may only Append; owner (the agent) reads + drains. A
180
180
  // quiesced actor keeps its name resolving but takes no more mail, so a
181
181
  // republish must not re-open the door.
182
- await this.remote.putJson(urls.inbox + '.keep', { keep: true }, 'application/json');
183
- if (this.config.quiescedAt) {
184
- await this.remote.setAcl(urls.inbox, []);
185
- this.log('inbox left closed — this actor is quiesced');
186
- } else {
187
- await this.remote.setAcl(urls.inbox, ['Append']);
188
- }
182
+ await podInbox.writeKeep(this.remote, urls);
183
+ await podInbox.setPosture(this.remote, urls, this.config.quiescedAt ? 'closed' : 'open');
184
+ if (this.config.quiescedAt) this.log('inbox left closed — this actor is quiesced');
189
185
 
190
186
  // notes live under a public-Read container (acl:default covers new notes).
191
- await this.remote.putJson(urls.notes + '.keep', { keep: true }, 'application/json');
192
- await this.remote.setAcl(urls.notes, ['Read']);
187
+ await podNotes.provisionContainer(this.remote, urls);
193
188
 
194
189
  await this.publishCollections({ ...ALL_COLLECTIONS, force });
195
190
  const updated = await this.announceProfileChange(actor, { force });
196
- await this.local.writeSettings({ handle: this.config.handle, actorUrl: urls.actor });
197
191
  await this.ensurePrivateAcls();
198
192
  const unreachable = await this.verifyPublicSurface();
199
193
  // Last, and merged: announceProfileChange writes this document too.
@@ -280,23 +274,14 @@ export class Publisher {
280
274
  // this runs on every connect: probe UNauthenticated, rewrite whatever
281
275
  // answers, and say so loudly if the rewrite does not take.
282
276
  async ensurePrivateAcls() {
283
- // Only trees that are actually ON the pod. With the default local
284
- // privateRoot the fediverse tree lives on disk, so probing it was a request
285
- // that could only ever 404, on every start.
286
- const onPod = [this.urls.home, this.urls.state,
287
- ...(this.privateOnPod === false ? [] : [this.urls.fediverse])];
288
- for (const url of onPod) {
289
- if (!await this.publiclyReadable(url)) continue;
290
- this.log(`${url} was readable without credentials — rewriting its ACL`);
291
- try {
292
- await this.remote.setAcl(url, []);
293
- } catch (e) {
294
- this.log(`SECURITY: ${url} is public and its ACL could not be rewritten: ${e.message}`);
295
- continue;
296
- }
297
- if (await this.publiclyReadable(url)) {
298
- this.log(`SECURITY: ${url} is STILL readable without credentials — check the pod's ACLs`);
299
- }
277
+ const findings = await containers.repairPrivateAcls(this.remote,
278
+ [this.urls.home, this.urls.state],
279
+ { isPublic: (u) => this.publiclyReadable(u) });
280
+ // What a finding MEANS is ours to say; the library just reports.
281
+ for (const f of findings) {
282
+ this.log(`${f.url} was readable without credentials — rewriting its ACL`);
283
+ if (f.error) { this.log(`SECURITY: ${f.url} is public and its ACL could not be rewritten: ${f.error}`); continue; }
284
+ if (f.stillPublic) this.log(`SECURITY: ${f.url} is STILL readable without credentials — check the pod's ACLs`);
300
285
  }
301
286
  }
302
287
 
@@ -304,14 +289,9 @@ export class Publisher {
304
289
  // accept: */* matters — asking for turtle makes the server answer 501 on the
305
290
  // JSON documents (webfinger, actor), which reads as "unreachable" when the
306
291
  // world can in fact see them perfectly well.
307
- async publiclyReadable(url) {
308
- try {
309
- const res = await this.probeFetch(url, {
310
- headers: { accept: '*/*', 'user-agent': USER_AGENT },
311
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
312
- });
313
- return res.status < 400;
314
- } catch { return false; } // unreachable is not a finding here
292
+ publiclyReadable(url) {
293
+ return containers.probePublicReadability(this.probeFetch, url,
294
+ { headers: { 'user-agent': USER_AGENT }, timeoutMs: HTTP_TIMEOUT_MS });
315
295
  }
316
296
 
317
297
  // Retire this identity for good: tell everyone who follows us to drop the
@@ -326,8 +306,7 @@ export class Publisher {
326
306
  const inboxes = [...new Set(contacts.followers.map(f => f.sharedInbox || f.inbox).filter(Boolean))];
327
307
  const deletedAt = new Date().toISOString();
328
308
  await this.deliverer.deliverToAll(inboxes, wire.deleteActorActivity(urls, Date.parse(deletedAt)));
329
- await this.remote.putJson(urls.actor, wire.tombstoneDoc(urls, deletedAt, this.config.kind));
330
- await this.remote.setAcl(urls.actor, ['Read']); // a Tombstone must stay fetchable
309
+ await podActor.writeTombstone(this.remote, urls, wire.tombstoneDoc(urls, deletedAt, this.config.kind));
331
310
  this.store.setConfig({ ...this.store.getConfig(), retiredAt: deletedAt });
332
311
  await this.store.flush();
333
312
  this.log(`retired: Delete sent to ${inboxes.length} inbox(es), actor replaced with a Tombstone`);
@@ -338,7 +317,7 @@ export class Publisher {
338
317
  // 401 rather than a 201 into storage nobody will ever drain. WebFinger,
339
318
  // host-meta and the actor stay published, so the handle still resolves.
340
319
  async closeInbox() {
341
- await this.remote.setAcl(this.urls.inbox, []);
320
+ await podInbox.setPosture(this.remote, this.urls, 'closed');
342
321
  const at = new Date().toISOString();
343
322
  this.store.setConfig({ ...this.store.getConfig(), quiescedAt: at });
344
323
  await this.store.flush();
@@ -348,7 +327,7 @@ export class Publisher {
348
327
 
349
328
  // Undo closeInbox: mail flows again and the actor is no longer quiesced.
350
329
  async openInbox() {
351
- await this.remote.setAcl(this.urls.inbox, ['Append']);
330
+ await podInbox.setPosture(this.remote, this.urls, 'open');
352
331
  const { quiescedAt, ...rest } = this.store.getConfig() || {};
353
332
  this.store.setConfig(rest);
354
333
  this.config.quiescedAt = undefined;
@@ -360,7 +339,7 @@ export class Publisher {
360
339
  // the pod except through the gateway's verify-at-the-door. Reversible with
361
340
  // openInbox (public-Append) — the one-call rollback.
362
341
  async lockInboxToGateway(gatewayWebId) {
363
- await this.remote.setAcl(this.urls.inbox, [], { appendAgents: [gatewayWebId] });
342
+ await podInbox.setPosture(this.remote, this.urls, { gatewayWebId });
364
343
  this.log(`inbox locked to gateway ${gatewayWebId} — public delivery is refused`);
365
344
  }
366
345
 
@@ -374,7 +353,7 @@ export class Publisher {
374
353
  await this.deliverer.deliverToAll(inboxes, wire.moveActivity(urls, target, Date.parse(at)));
375
354
  this.config.movedTo = target; // so the republish below carries it
376
355
  this.store.setConfig({ ...this.store.getConfig(), movedTo: target, movedAt: at });
377
- await this.remote.putJson(urls.actor, wire.actorDoc({
356
+ await podActor.writeMoved(this.remote, urls, wire.actorDoc({
378
357
  urls, handle: wire.publicHandle(this.config), name: this.config.name,
379
358
  publicKeyPem: this.publicKeyPem, movedTo: target, kind: this.config.kind,
380
359
  approveJoins: wire.followsNeedApproval(this.config),
@@ -384,7 +363,6 @@ export class Publisher {
384
363
  webId: this.remote.webId || null,
385
364
  aliases: this.config.aliases || [],
386
365
  }));
387
- await this.remote.setAcl(urls.actor, ['Read']);
388
366
  await this.store.flush();
389
367
  this.log(`moved to ${target}: Move sent to ${inboxes.length} inbox(es), actor now advertises movedTo`);
390
368
  return { inboxes: inboxes.length, target, movedAt: at };
@@ -508,8 +486,7 @@ export class Publisher {
508
486
  // `-replies` — plus a `.keep`. Which is which is settled by reading the
509
487
  // document below, not by its name: a slug is a date and eight hex
510
488
  // characters and says nothing about what it holds.
511
- for (const child of await this.remote.listContainer(urls.notes).catch(() => [])) {
512
- if (/(-create|-replies|\/\.keep)$/.test(child.url)) continue;
489
+ for (const child of await podNotes.list(this.remote, urls).catch(() => [])) {
513
490
  ids.add(child.url);
514
491
  }
515
492
  indexed = true;
@@ -528,7 +505,7 @@ export class Publisher {
528
505
  if (budget <= 0) { this.log(`rebuild: stopping at ${REBUILD_MAX_PER_RUN} this run — run it again for the rest`); break; }
529
506
  if (have.has(id) || removed.has(id)) continue;
530
507
  budget--;
531
- const note = await this.remote.getJson(id).catch(() => null);
508
+ const note = await podNotes.read(this.remote, id).catch(() => null);
532
509
  if (note?.type !== 'Note' || note.id !== id || note.attributedTo !== urls.actor) continue;
533
510
  const attachments = wire.attachmentsOf(note);
534
511
  const mentions = (Array.isArray(note.tag) ? note.tag : [])
@@ -560,31 +537,18 @@ export class Publisher {
560
537
  }
561
538
  if (!recovered.length && !reblogs) return { indexed: ids.size, recovered: 0, reblogs: 0, landed: true };
562
539
 
563
- // Written whole and sorted, the way backfillStatuses does it: addStatus
564
- // unshifts and fires the streaming event, so a loop of it would arrive
565
- // backwards and push every recovered post at connected clients as new.
540
+ // Written whole and sorted: addStatus unshifts and fires the streaming
541
+ // event, so a loop of it would arrive backwards and push every recovered
542
+ // post at connected clients as new.
566
543
  merged.sort((a, b) => String(b.published || '').localeCompare(String(a.published || '')));
567
544
  const kept = merged.slice(0, 1000);
568
545
  this.store.write('statuses.json', kept);
569
546
  const landed = await this.store.commit();
570
547
 
571
- // The RDF mirror is the other thing a lost machine took with it, and it is
572
- // what backfillStatuses reads on a fresh state. Best effort: a post in
573
- // statuses.json is already the recovery that matters.
574
- let rdf = 0;
575
- for (const s of recovered) {
576
- try {
577
- await this.local.writeNote('posts', s.slug, {
578
- noteId: s.noteId, actor: s.actor, published: s.published,
579
- content: s.content, inReplyTo: s.inReplyTo, attachments: s.attachments,
580
- });
581
- rdf++;
582
- } catch (e) { this.log(`rebuild: RDF for ${s.slug} not written (${e.message})`); }
583
- }
584
548
  this.log(`rebuilt ${recovered.length} post(s) and ${reblogs} boost(s) from the pod`
585
549
  + `${landed ? '' : ' — THE STATE WRITE DID NOT LAND'}`);
586
550
  return {
587
- indexed: ids.size, recovered: recovered.length, reblogs, rdf, landed,
551
+ indexed: ids.size, recovered: recovered.length, reblogs, landed,
588
552
  dropped: Math.max(0, merged.length - kept.length),
589
553
  };
590
554
  }
@@ -632,8 +596,8 @@ export class Publisher {
632
596
  const digest = crypto.createHash('sha256').update(JSON.stringify(doc)).digest('hex').slice(0, 16);
633
597
  after[n] = digest;
634
598
  if (before[n] === digest) continue; // sealed and unchanged
635
- await this.remote.putJson(wire.outboxPageId(urls.outbox, n), doc);
636
- if (!before[n] || acls) await this.remote.setAcl(wire.outboxPageId(urls.outbox, n), ['Read']);
599
+ await podOutbox.writePage(this.remote, wire.outboxPageId(urls.outbox, n), doc,
600
+ { publicRead: !before[n] || acls });
637
601
  wrote++;
638
602
  }
639
603
  // Pages above the new count are orphans: the outbox shrank past them, and
@@ -643,13 +607,12 @@ export class Publisher {
643
607
  const stale = Object.keys(seen.outboxPages || {}).map(Number)
644
608
  .filter(n => Number.isFinite(n) && n > pages.length);
645
609
  for (const n of stale) {
646
- await this.remote.delete(wire.outboxPageId(urls.outbox, n)).catch(() => {});
610
+ await podOutbox.dropPage(this.remote, wire.outboxPageId(urls.outbox, n));
647
611
  }
648
612
  // The head carries totalItems, so it moves whenever the outbox does. Four
649
613
  // lines, and constant however much you have posted.
650
- await this.remote.putJson(urls.outbox,
651
- wire.outboxHead(urls.outbox, outbox.length, pages.length));
652
- if (acls) await this.remote.setAcl(urls.outbox, ['Read']);
614
+ await podOutbox.writeHead(this.remote, urls,
615
+ wire.outboxHead(urls.outbox, outbox.length, pages.length), { publicRead: acls });
653
616
  this.store.write('published.json',
654
617
  { ...this.store.read('published.json', {}), outboxPages: after, outboxIndex: index });
655
618
  return wrote;
@@ -659,22 +622,7 @@ export class Publisher {
659
622
  // the flat collection this used to write, so an actor published before paging
660
623
  // is still readable — which matters because rebuild reads this to recover
661
624
  // posts a lost machine no longer has.
662
- async readPublishedOutbox() {
663
- const head = await this.remote.getJson(this.urls.outbox).catch(() => null);
664
- if (!head) return null;
665
- if (Array.isArray(head.orderedItems) && head.orderedItems.length) return head.orderedItems;
666
- const items = [];
667
- let next = head.first;
668
- const seen = new Set();
669
- while (next && !seen.has(next) && items.length < 10_000) {
670
- seen.add(next);
671
- const page = await this.remote.getJson(next).catch(() => null);
672
- if (!page) break;
673
- items.push(...(page.orderedItems || []));
674
- next = page.next;
675
- }
676
- return items;
677
- }
625
+ readPublishedOutbox() { return podOutbox.readPublished(this.remote, this.urls); }
678
626
 
679
627
  // The followers collection, paged like the outbox: a head that carries only
680
628
  // the count and the page bounds, and page documents holding the actor IRIs —
@@ -695,19 +643,18 @@ export class Publisher {
695
643
  const digest = crypto.createHash('sha256').update(JSON.stringify(doc)).digest('hex').slice(0, 16);
696
644
  after[n] = digest;
697
645
  if (before[n] === digest) continue; // unchanged page
698
- await this.remote.putJson(wire.followersPageId(urls.followers, n), doc);
699
- if (!before[n] || acls) await this.remote.setAcl(wire.followersPageId(urls.followers, n), ['Read']);
646
+ await podFollowers.writePage(this.remote, wire.followersPageId(urls.followers, n), doc,
647
+ { publicRead: !before[n] || acls });
700
648
  }
701
649
  // Pages the collection shrank past would keep serving names that no longer
702
650
  // follow; the head stops pointing at them but the URL is guessable.
703
651
  const stale = Object.keys(seen.followersPages || {}).map(Number)
704
652
  .filter(n => Number.isFinite(n) && n > pages.length);
705
653
  for (const n of stale) {
706
- await this.remote.delete(wire.followersPageId(urls.followers, n)).catch(() => {});
654
+ await podFollowers.dropPage(this.remote, wire.followersPageId(urls.followers, n));
707
655
  }
708
- await this.remote.putJson(urls.followers,
709
- wire.followersHead(urls.followers, actors.length, pages.length));
710
- if (acls) await this.remote.setAcl(urls.followers, ['Read']);
656
+ await podFollowers.writeHead(this.remote, urls,
657
+ wire.followersHead(urls.followers, actors.length, pages.length), { publicRead: acls });
711
658
  this.store.write('published.json',
712
659
  { ...this.store.read('published.json', {}), followersPages: after, followersIndex: index });
713
660
  }
@@ -715,23 +662,7 @@ export class Publisher {
715
662
  // Every actor in the published followers collection, walking pages. Also reads
716
663
  // the flat collection this used to write, so an actor published before paging
717
664
  // still reconciles.
718
- async readPublishedFollowers() {
719
- const head = await this.remote.getJson(this.urls.followers).catch(() => null);
720
- if (!head) return null;
721
- if (Array.isArray(head.orderedItems) && head.orderedItems.length) return head.orderedItems;
722
- if (Array.isArray(head.items) && head.items.length) return head.items;
723
- const items = [];
724
- let next = head.first;
725
- const seen = new Set();
726
- while (next && !seen.has(next) && items.length < 100_000) {
727
- seen.add(next);
728
- const page = await this.remote.getJson(next).catch(() => null);
729
- if (!page) break;
730
- items.push(...(page.orderedItems || []));
731
- next = page.next;
732
- }
733
- return items;
734
- }
665
+ readPublishedFollowers() { return podFollowers.readPublished(this.remote, this.urls); }
735
666
 
736
667
  async publishCollections(which = ALL_COLLECTIONS) {
737
668
  const { urls } = this;
@@ -748,9 +679,9 @@ export class Publisher {
748
679
  await this.publishFollowers(actors, { acls: which.acls, force: which.force });
749
680
  }
750
681
  if (which.following) {
751
- await this.remote.putJson(urls.following,
752
- wire.orderedCollection(urls.following, contacts.following.filter(f => f.accepted).map(f => f.actor)));
753
- if (which.acls) await this.remote.setAcl(urls.following, ['Read']);
682
+ await podFollowing.write(this.remote, urls,
683
+ wire.orderedCollection(urls.following, contacts.following.filter(f => f.accepted).map(f => f.actor)),
684
+ { publicRead: which.acls });
754
685
  }
755
686
  if (which.pending) await this.publishPending();
756
687
  if (which.blocked) await this.publishBlocked();
@@ -766,7 +697,6 @@ export class Publisher {
766
697
  if (which.force || !Array.isArray(known)) await this.reconcileOutbox(outbox);
767
698
  await this.publishOutbox(outbox, { acls: which.acls, force: which.force });
768
699
  }
769
- if (which.followers || which.following) await this.local.writeContacts(contacts);
770
700
  }
771
701
 
772
702
  // FEP-4ccd: the follows in limbo, as owner-only collections of the Follow
@@ -778,13 +708,13 @@ export class Publisher {
778
708
  if (await this.privateReady() !== true) return;
779
709
  const { urls } = this;
780
710
  const contacts = this.store.getContacts();
781
- await this.remote.putJson(urls.pendingFollowers, wire.orderedCollection(
782
- urls.pendingFollowers,
783
- this.store.getRequests().filter(r => r.activity && !r.bsky).map(r => r.activity)));
784
- await this.remote.putJson(urls.pendingFollowing, wire.orderedCollection(
785
- urls.pendingFollowing,
786
- contacts.following.filter(f => !f.accepted && f.followActivity)
787
- .map(f => f.followActivity).reverse()));
711
+ await podPrivate.writePending(this.remote, urls, {
712
+ followers: wire.orderedCollection(urls.pendingFollowers,
713
+ this.store.getRequests().filter(r => r.activity && !r.bsky).map(r => r.activity)),
714
+ following: wire.orderedCollection(urls.pendingFollowing,
715
+ contacts.following.filter(f => !f.accepted && f.followActivity)
716
+ .map(f => f.followActivity).reverse()),
717
+ });
788
718
  }
789
719
 
790
720
  // The gateway policy: a small PUBLIC document a keyless inbox gateway reads
@@ -797,7 +727,7 @@ export class Publisher {
797
727
  const { urls } = this;
798
728
  const contacts = this.store.getContacts();
799
729
  const bl = this.store.getBlocklist();
800
- await this.remote.putJson(urls.home + 'ap/gateway-policy.json', {
730
+ await podPolicy.write(this.remote, urls, {
801
731
  v: 1,
802
732
  actorUrl: urls.actor,
803
733
  followersUrl: urls.followers,
@@ -807,8 +737,7 @@ export class Publisher {
807
737
  kind: this.config.kind || 'person',
808
738
  following: contacts.following.filter(f => f.accepted && !f.bsky).map(f => f.actor),
809
739
  blocklist: { domains: bl.domains || [], actors: bl.actors || [] },
810
- }, 'application/json');
811
- await this.remote.setAcl(urls.home + 'ap/gateway-policy.json', ['Read']);
740
+ });
812
741
  }
813
742
 
814
743
  // FEP-c648: the blocked actors, as an owner-only collection. Actors only —
@@ -817,7 +746,7 @@ export class Publisher {
817
746
  if (await this.privateReady() !== true) return;
818
747
  const { urls } = this;
819
748
  const actors = [...(this.store.getBlocklist().actors || [])].reverse();
820
- await this.remote.putJson(urls.blocked, wire.orderedCollection(urls.blocked, actors));
749
+ await podPrivate.writeBlocked(this.remote, urls, wire.orderedCollection(urls.blocked, actors));
821
750
  }
822
751
 
823
752
  // The outbox is the public record of everything this actor has said, boosts
@@ -852,19 +781,32 @@ export class Publisher {
852
781
 
853
782
  // Media container on the remote pod — public-Read like notes, created lazily
854
783
  // at first upload (idempotent; the flag only saves round-trips).
784
+ // Ask before writing. `_mediaReady` is per PROCESS, and a service worker is
785
+ // restarted whenever the browser feels like it — so on the browser build this
786
+ // re-created a container that has existed since sign-up, and rewrote its ACL,
787
+ // on every restart. A HEAD is one request and usually the only one.
855
788
  async ensureMediaContainer() {
856
789
  if (this._mediaReady) return;
857
- await this.remote.putJson(this.urls.media + '.keep', { keep: true }, 'application/json');
858
- await this.remote.setAcl(this.urls.media, ['Read']);
790
+ if (await containers.exists(this.remote, this.urls.media)) { this._mediaReady = true; return; }
791
+ await containers.provisionPublic(this.remote, this.urls.media);
859
792
  this._mediaReady = true;
860
793
  }
861
794
 
795
+ // Whether the canary this class writes is already there. Only a definite
796
+ // "yes" counts: anything else falls through to the write, which is
797
+ // idempotent anyway, so a failed probe costs a request and never correctness.
798
+ _containerExists(base) { return containers.exists(this.remote, base); }
799
+
862
800
  // The owner-only container that followers-only and direct posts live in.
863
801
  // Its ACL is set once; every note under it inherits.
864
802
  async ensurePrivateContainer() {
865
803
  if (this._privateContainer) return;
866
- await this.remote.putJson(this.urls.privateNotes + '.keep', { keep: true }, 'application/json');
867
- await this.remote.setAcl(this.urls.privateNotes, []);
804
+ // Same reasoning as ensureMediaContainer: probe before provisioning, so a
805
+ // worker restart is not a re-provision. The ACL is NOT skipped on the
806
+ // strength of the container existing alone — ensurePrivateAcls re-checks
807
+ // that separately on every connect, which is the control that matters here.
808
+ if (await containers.exists(this.remote, this.urls.privateNotes)) { this._privateContainer = true; return; }
809
+ await containers.provisionOwnerOnly(this.remote, this.urls.privateNotes);
868
810
  this._privateContainer = true;
869
811
  }
870
812
 
@@ -881,9 +823,8 @@ export class Publisher {
881
823
  // fronted identity must map the advertised private container back to the
882
824
  // pod itself — the ACL we are testing is the pod's.
883
825
  const pn = this.urls.toPod ? this.urls.toPod(this.urls.privateNotes) : this.urls.privateNotes;
884
- const r = await (this.probeFetch || fetch)(pn + '.keep', { redirect: 'manual' });
885
- this._privateVerdict = (r.status === 401 || r.status === 403) ? true
886
- : `this pod serves private documents to strangers (HTTP ${r.status}) — private posts stay off it`;
826
+ const verdict = await containers.probePrivateEnforcement(this.probeFetch || fetch, pn + '.keep');
827
+ this._privateVerdict = verdict === true ? true : `${verdict} private posts stay off it`;
887
828
  return this._privateVerdict;
888
829
  } catch (e) {
889
830
  return `could not verify that the pod protects private posts (${e.message})`;
@@ -927,15 +868,13 @@ export class Publisher {
927
868
  const note = wire.noteDoc({ urls, slug, content, published, inReplyTo, attachments, mentions,
928
869
  visibility, summary: spoilerText, container: priv ? urls.privateNotes : urls.notes });
929
870
 
930
- await this.remote.putJson(note.id, note);
871
+ await podNotes.write(this.remote, note.id, note);
931
872
  // Empty, but present: a dangling `replies` that 404s is worse than none.
932
- await this.remote.putJson(wire.repliesId(note.id), wire.collection(wire.repliesId(note.id), []));
873
+ await podNotes.writeEmptyReplies(this.remote, wire.repliesId(note.id),
874
+ wire.collection(wire.repliesId(note.id), []));
933
875
  // The outbox is the PUBLIC index; a private or direct post is not in it.
934
876
  if (!priv) await this.recordOutbox(note.id);
935
877
 
936
- await this.local.writeNote('posts', slug, {
937
- noteId: note.id, actor: urls.actor, published, content: note.content, inReplyTo, attachments,
938
- });
939
878
  this.store.addStatus({
940
879
  noteId: note.id, actor: urls.actor, content: note.content, published, inReplyTo,
941
880
  kind: 'post', slug, text: content, visibility,
@@ -948,7 +887,7 @@ export class Publisher {
948
887
  // Published as its own document: a group that carries this post wraps the
949
888
  // whole activity, and the receiving server resolves it by fetching this id.
950
889
  // It inherits the notes container's public-Read acl:default.
951
- await this.remote.putJson(create.id, create);
890
+ await podNotes.writeCreate(this.remote, create.id, create);
952
891
  const contacts = this.store.getContacts();
953
892
  // A direct post goes to the people it names and to nobody else.
954
893
  const inboxes = [...new Set([
@@ -1036,14 +975,11 @@ export class Publisher {
1036
975
  options: poll.options, multiple: poll.multiple, endTime: poll.expiresAt, votersCount: 0,
1037
976
  });
1038
977
 
1039
- await this.remote.putJson(question.id, question);
978
+ await podNotes.write(this.remote, question.id, question);
1040
979
  // Empty, but present: a dangling `replies` that 404s is worse than none.
1041
- await this.remote.putJson(wire.repliesId(question.id), wire.collection(wire.repliesId(question.id), []));
980
+ await podNotes.writeEmptyReplies(this.remote, wire.repliesId(question.id),
981
+ wire.collection(wire.repliesId(question.id), []));
1042
982
  if (!priv) await this.recordOutbox(question.id);
1043
- await this.local.writeNote('posts', slug, {
1044
- noteId: question.id, actor: urls.actor, published, content: question.content,
1045
- inReplyTo, attachments: [],
1046
- });
1047
983
  this.store.addStatus({
1048
984
  noteId: question.id, actor: urls.actor, content: question.content, published,
1049
985
  kind: 'post', slug, text: content, visibility, poll, inReplyTo,
@@ -1052,7 +988,7 @@ export class Publisher {
1052
988
  });
1053
989
 
1054
990
  const create = this._pollActivity('Create', question, wire.createActivityId(question.id));
1055
- await this.remote.putJson(create.id, create);
991
+ await podNotes.writeCreate(this.remote, create.id, create);
1056
992
  const contacts = this.store.getContacts();
1057
993
  const inboxes = [...new Set([
1058
994
  ...(visibility === 'direct' ? [] : contacts.followers.map(f => f.sharedInbox || f.inbox)),
@@ -1136,10 +1072,10 @@ export class Publisher {
1136
1072
  options: poll.options, multiple: !!poll.multiple, endTime: poll.expiresAt,
1137
1073
  closed: poll.closed, votersCount: poll.votersCount || 0,
1138
1074
  });
1139
- await this.remote.putJson(question.id, question);
1075
+ await podNotes.write(this.remote, question.id, question);
1140
1076
  // The Create is overwritten too, so a group's Announce resolves to the
1141
1077
  // current count rather than to the one the poll opened with.
1142
- await this.remote.putJson(wire.createActivityId(question.id),
1078
+ await podNotes.writeCreate(this.remote, wire.createActivityId(question.id),
1143
1079
  this._pollActivity('Create', question, wire.createActivityId(question.id)));
1144
1080
  this.store.updateStatus(questionId, { poll });
1145
1081
 
@@ -1206,8 +1142,7 @@ export class Publisher {
1206
1142
  // remote server reads when it shows this profile's pins.
1207
1143
  async publishFeatured() {
1208
1144
  const ids = this.store.getStatuses().filter(s => s.kind === 'post' && s.pinned).map(s => s.noteId);
1209
- await this.remote.putJson(this.urls.featured, wire.orderedCollection(this.urls.featured, ids));
1210
- await this.remote.setAcl(this.urls.featured, ['Read']);
1145
+ await podFeatured.write(this.remote, this.urls, wire.orderedCollection(this.urls.featured, ids));
1211
1146
  return ids.length;
1212
1147
  }
1213
1148
 
@@ -1236,14 +1171,8 @@ export class Publisher {
1236
1171
  attachments: atts, mentions, visibility: s.visibility || 'public',
1237
1172
  summary: spoilerText, updated, container,
1238
1173
  });
1239
- await this.remote.putJson(note.id, note);
1240
- await this.remote.putJson(wire.createActivityId(note.id), wire.createActivity(note, urls));
1241
- if (s.slug) {
1242
- await this.local.writeNote('posts', s.slug, {
1243
- noteId: note.id, actor: urls.actor, published: s.published, content: note.content,
1244
- inReplyTo: s.inReplyTo, attachments: atts,
1245
- });
1246
- }
1174
+ await podNotes.write(this.remote, note.id, note);
1175
+ await podNotes.writeCreate(this.remote, wire.createActivityId(note.id), wire.createActivity(note, urls));
1247
1176
  const patched = this.store.updateStatus(s.noteId, {
1248
1177
  content: note.content, text: content, editedAt: updated,
1249
1178
  spoiler: spoilerText || undefined,