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/lib/remote.mjs CHANGED
@@ -1,19 +1,18 @@
1
- // remote.mjs — authenticated I/O against the remote pod, via CSS
2
- // client-credentials + DPoP (vendor/idp-grant.cjs, extracted from
3
- // data-kitchen's "remember this IdP" machinery — plain Node, no Electron).
1
+ // remote.mjs — the Node agent's pod transport: CSS client-credentials + DPoP
2
+ // (vendor/idp-grant.cjs, extracted from data-kitchen's "remember this IdP"
3
+ // machinery — plain Node, no Electron).
4
+ //
5
+ // Everything about talking to a pod — the verbs, the ACL documents, the
6
+ // deletion deny-list, the Retry-After cooldown, the container listing — lives
7
+ // in lib/pod/transport.mjs, which knows nothing about Node and is shared with
8
+ // the browser. What is left here is the one thing that genuinely differs: how
9
+ // the session is obtained. That top-level `require` of the vendor grant is also
10
+ // exactly why the split exists — it cannot be bundled for a service worker.
4
11
 
5
12
  import { createRequire } from 'node:module';
6
13
  import path from 'node:path';
7
14
  import { fileURLToPath } from 'node:url';
8
- import * as $rdf from 'rdflib';
9
-
10
- const LDP = $rdf.Namespace('http://www.w3.org/ns/ldp#');
11
- const DC = $rdf.Namespace('http://purl.org/dc/terms/');
12
- const POSIX = $rdf.Namespace('http://www.w3.org/ns/posix/stat#');
13
- const RDF = $rdf.Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#');
14
- const ACL = $rdf.Namespace('http://www.w3.org/ns/auth/acl#');
15
- const FOAF = $rdf.Namespace('http://xmlns.com/foaf/0.1/');
16
- const AS = $rdf.Namespace('https://www.w3.org/ns/activitystreams#');
15
+ import { PodTransport } from './pod/transport.mjs';
17
16
 
18
17
  const require = createRequire(import.meta.url);
19
18
  const vendorDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../vendor');
@@ -22,398 +21,34 @@ const { mintCredential, discoverTokenEndpoint, createGrantSession, revokeCredent
22
21
 
23
22
  export { mintCredential, discoverTokenEndpoint, revokeCredentialViaAccount };
24
23
 
25
- // A pod that answers 429 or 503 is asking to be left alone; Retry-After says
26
- // for how long. Every request to this pod goes through one cooldown, so a
27
- // server under strain gets silence rather than each timer discovering the
28
- // refusal separately. Requests during the window fail fast — no socket is
29
- // opened — and the callers' existing retry paths take it from there.
30
- // The parser is shared with the outbound path; see lib/safefetch.mjs.
31
- import { retryAfterMs, readCapped } from './safefetch.mjs';
32
- import { linkTargets, REL } from './links.mjs';
33
-
34
- // A pod whose access rules are ACP policies, not WAC authorizations. This
35
- // agent writes WAC; over an ACP resource that would be noise where the pod's
36
- // real rules used to be, so it stops instead.
37
- const ACP_NS = 'http://www.w3.org/ns/solid/acp#';
38
-
39
- // The inbox is public-Append, so the listing's size is in other people's
40
- // hands; reading it whole must still have a ceiling.
41
- const LISTING_MAX_BYTES = 10 * 1024 * 1024;
42
-
43
- // A pod's ability to BE a pod. Not data, and never ours to remove: delete one
44
- // of these and the pod does not degrade, it stops — and it cannot be repaired
45
- // by the tool that broke it, because that tool can no longer authenticate. A
46
- // pod was crippled exactly this way.
47
- //
48
- // Here rather than in any one script, because every DELETE this project sends
49
- // goes through RemotePod.delete — so this is the one place that cannot be
50
- // routed around. A DENY-list, not an allow-prefix: the next script will have a
51
- // different prefix and the same list of things it must never touch.
52
- const PROTECTED = [
53
- [/\/profile(\/|$)/, 'the WebID document — nothing could authenticate as this pod again'],
54
- [/\/settings(\/|$)/, "the pod's own settings"],
55
- [/\/\.well-known(\/|$)/, 'discovery — the handle would stop resolving'],
56
- [/\.acl$/, 'an access-control document — what it governs becomes unreachable'],
57
- [/\.meta$/, 'a resource description the server itself reads'],
58
- // The single-active-agent lock. It is on the pod precisely because the
59
- // private half need not be — a lease only one machine can reach coordinates
60
- // nothing — so it survives moving the state off, and nothing ever deletes it:
61
- // release() writes an expiry, it does not remove the document. Two agents
62
- // both believing they hold it is silent, destructive, duplicated inbox
63
- // draining.
64
- [/\/lease\.json$/, 'the lease — two agents would drain the same inbox'],
65
- ];
24
+ // Re-exported so the callers that guard their own deletions run-agent, the
25
+ // server package's live test, the smoke suite keep importing it from here.
26
+ export { protectedFromDeletion } from './pod/transport.mjs';
66
27
 
67
- export function protectedFromDeletion(url) {
68
- let p;
69
- try { p = new URL(url).pathname; } catch { throw new Error(`refusing to DELETE an unparsable URL: ${url}`); }
70
- if (p === '/' || p === '') throw new Error(`refusing to DELETE the pod root: ${url}`);
71
- for (const [re, why] of PROTECTED) {
72
- if (re.test(p)) {
73
- throw new Error(`refusing to DELETE ${url} — ${why}. Deny-list: lib/remote.mjs.`);
74
- }
75
- }
76
- }
77
-
78
- export class RemotePod {
79
- constructor(credential, { log = () => {}, home = null, session = null } = {}) {
28
+ export class RemotePod extends PodTransport {
29
+ constructor(credential, { log = () => {}, home = null, session = null, role = 'agent' } = {}) {
80
30
  // An injected session replaces the credential-backed one, and everything
81
- // below it — the cooldown, the deny-list, the url map — is unchanged. That
31
+ // above it — the cooldown, the deny-list, the url map — is unchanged. That
82
32
  // is how the agent runs inside a pod server: same class, no token to mint.
83
- this.session = session || createGrantSession(credential, home ? {
33
+ super(session || createGrantSession(credential, home ? {
84
34
  backoffFile: path.join(home, 'backoff.json'),
85
35
  tokenFile: path.join(home, 'token.json'),
86
- } : {});
87
- this.webId = credential.webId;
88
- this.pausedUntil = 0;
89
- this.probeCount = 0;
90
- this.log = log;
91
- // Where each resource's access control lives, as the pod itself said. WAC
92
- // forbids working it out from the resource's own URL, so it is asked for
93
- // and remembered rather than assembled.
94
- this.aclUrls = new Map();
95
- this.aclFlavour = null; // null until the first write asks what this pod speaks
96
- // A fronted identity advertises ids on a shared domain but writes to the
97
- // pod. run-agent installs the fronted→pod mapping here, so every request
98
- // built from an advertised id lands on the pod — one choke point, and
99
- // publisher/social/intake stay unaware. Unset → identity, byte-for-byte
100
- // the unfronted behaviour.
101
- this.toPod = null;
36
+ } : {}), {
37
+ webId: credential.webId,
38
+ log,
39
+ role,
40
+ runtime: 'node',
41
+ // Fail fast for the window rather than sleeping it out: a daemon has
42
+ // timers behind it, and a request that waits is a timer that does not
43
+ // fire. The callers' existing retry paths take it from there.
44
+ cooldownMode: 'refuse',
45
+ });
102
46
  }
103
47
 
104
- // Install the advertised→pod url map for a fronted identity (urls.toPod).
105
- setUrlMap(fn) { this.toPod = typeof fn === 'function' ? fn : null; }
106
-
107
48
  async warmup() { return this.session.warmup(); }
108
49
 
109
- // What we have asked of this pod, for /status and for answering an operator.
50
+ /** The grant's own counters, under the transport's. */
110
51
  stats() {
111
- return {
112
- ...this.session.stats(),
113
- probes: this.probeCount,
114
- pausedFor: Math.max(0, Math.round((this.pausedUntil - Date.now()) / 1000)),
115
- };
116
- }
117
-
118
- // A deliberately CREDENTIAL-FREE request: it asks what a stranger would see,
119
- // so it cannot go through session.fetch without answering a different
120
- // question. It still belongs to this pod, though — it opens a socket to it
121
- // and takes one of its workers — so it observes the same Retry-After window
122
- // and is counted. Ten of these ride on every profile save, and none of them
123
- // used to appear in /status, which made podRequests under-report by design.
124
- async probe(url, init = {}) {
125
- const left = this.pausedUntil - Date.now();
126
- if (left > 0) {
127
- throw new Error(`pod asked us to back off — ${Math.ceil(left / 1000)}s left of its Retry-After`);
128
- }
129
- this.probeCount++;
130
- const res = await fetch(url, init);
131
- if (res.status === 429 || res.status === 503) {
132
- const ms = retryAfterMs(res) ?? 60_000;
133
- this.pausedUntil = Date.now() + ms;
134
- this.log(`pod returned ${res.status} to an unauthenticated probe — pausing for ${Math.round(ms / 1000)}s`);
135
- }
136
- return res;
137
- }
138
-
139
- async fetch(url, init) {
140
- const left = this.pausedUntil - Date.now();
141
- if (left > 0) {
142
- throw new Error(`pod asked us to back off — ${Math.ceil(left / 1000)}s left of its Retry-After`);
143
- }
144
- // Fronted identity: an advertised id maps to its pod location here, so the
145
- // request lands on the pod. A no-op for a pod-native id or an unfronted
146
- // identity (toPod unset).
147
- if (this.toPod) url = this.toPod(url);
148
- const res = await this.session.fetch(url, init);
149
- if (res.status === 429 || res.status === 503) {
150
- const ms = retryAfterMs(res) ?? 60_000; // no header: our own cooldown
151
- this.pausedUntil = Date.now() + ms;
152
- this.log(`pod returned ${res.status} — pausing all requests for ${Math.round(ms / 1000)}s`);
153
- }
154
- return res;
155
- }
156
-
157
- // Everything below goes through this.fetch, never session.fetch directly:
158
- // that is the only place the Retry-After cooldown is both OBSERVED and ARMED.
159
- // Writing straight to the session meant a pod could answer 429 to every PUT
160
- // and DELETE we sent without any of them ever being recorded as a refusal,
161
- // and meant a cooldown armed by a read did not stop a publish burst.
162
- async put(url, body, contentType) {
163
- const res = await this.fetch(url, {
164
- method: 'PUT', headers: { 'content-type': contentType }, body,
165
- });
166
- if (res.status >= 400) throw new Error(`PUT ${url} → ${res.status}`);
167
- // Writing a document is usually the step before setting its access, and
168
- // the answer to the write already says where that lives. Taking it here
169
- // spares the extra request the ACL write would otherwise make.
170
- this.noteAclLink(url, res);
171
- return res;
172
- }
173
-
174
- /** Remember an access-control location the pod volunteered on a response. */
175
- noteAclLink(url, res) {
176
- if (this.aclUrls.has(url)) return;
177
- const [acl] = linkTargets(res?.headers?.get?.('link'), REL.acl, url);
178
- if (acl) this.aclUrls.set(url, acl);
179
- }
180
-
181
- /**
182
- * Where this resource's access control lives. The pod says so on any
183
- * response about the resource; a pod that says nothing is taken to keep it
184
- * at the usual suffix, which is what every server this runs against does.
185
- */
186
- async aclUrlFor(targetUrl) {
187
- const known = this.aclUrls.get(targetUrl);
188
- if (known) return known;
189
- try {
190
- const res = await this.fetch(targetUrl, { method: 'HEAD' });
191
- this.noteAclLink(targetUrl, res);
192
- } catch { /* unreachable or no such resource yet: the suffix below */ }
193
- const resolved = this.aclUrls.get(targetUrl) || targetUrl + '.acl';
194
- this.aclUrls.set(targetUrl, resolved);
195
- return resolved;
196
- }
197
-
198
- /**
199
- * Whether writing a WAC document here is meaningful. Asked once per pod, on
200
- * the first access-control write. A pod that answers with ACP policies is
201
- * left alone: replacing them with authorizations it does not read would take
202
- * away the rules actually protecting it.
203
- */
204
- async aclWritable(aclUrl) {
205
- if (this.aclFlavour !== null) return this.aclFlavour;
206
- this.aclFlavour = true;
207
- try {
208
- const res = await this.fetch(aclUrl, { headers: { accept: 'text/turtle' } });
209
- if (res.status < 300) {
210
- const g = $rdf.graph();
211
- $rdf.parse(await res.text(), g, aclUrl, 'text/turtle');
212
- const acp = g.statements.some(st => st.predicate.value.startsWith(ACP_NS)
213
- || st.object.value.startsWith(ACP_NS));
214
- if (acp) {
215
- this.aclFlavour = false;
216
- this.log('this pod states access as ACP policies, which this agent does not write — '
217
- + 'its access rules are left exactly as they are, and nothing here is published private');
218
- }
219
- }
220
- } catch { /* absent, unreadable or unparsable: WAC is what we write */ }
221
- return this.aclFlavour;
222
- }
223
-
224
- async putJson(url, obj, contentType = 'application/activity+json') {
225
- return this.put(url, JSON.stringify(obj), contentType);
226
- }
227
-
228
- // A read that FAILED is not a document that is ABSENT. Returning null for
229
- // both let a 429 read as "no replies yet", and the caller then rewrote the
230
- // collection from empty — erasing every reply already recorded. Only a real
231
- // 404/410 is absence; anything else throws and the caller retries later.
232
- async getJson(url) {
233
- const res = await this.fetch(url, { headers: { accept: '*/*' } });
234
- if (res.status === 404 || res.status === 410) return null;
235
- if (res.status >= 400) throw new Error(`GET ${url} → ${res.status}`);
236
- return res.json().catch(() => null);
237
- }
238
-
239
- async delete(url) {
240
- protectedFromDeletion(url);
241
- // The pattern list above knows the usual name for an access-control
242
- // document. One the pod named itself is just as fatal to remove.
243
- for (const acl of this.aclUrls.values()) {
244
- if (acl === url) throw new Error(`refusing to DELETE an access-control document: ${url}`);
245
- }
246
- const res = await this.fetch(url, { method: 'DELETE' });
247
- return res.status < 400 || res.status === 404;
248
- }
249
-
250
- // Child documents of an LDP container (URLs under it, excluding aux docs).
251
- // Revalidated: the inbox is polled every two minutes and is usually
252
- // unchanged, so ask conditionally and let the server answer 304.
253
- async listContainer(url) {
254
- this._listCache ||= new Map();
255
- const known = this._listCache.get(url);
256
- const res = await this.fetch(url, {
257
- headers: { accept: 'text/turtle', ...(known?.etag ? { 'if-none-match': known.etag } : {}) },
258
- });
259
- if (res.status === 304 && known) return known.children;
260
- if (res.status >= 400) return [];
261
- // rdflib, not a regex: a container listing is RDF, and the gap between
262
- // "any angle-bracketed thing in the document" and "what this container
263
- // actually contains" is where quiet bugs live. claude/plans/no-regex-rdf.md
264
- //
265
- // CSS puts dc:modified and posix:size on every child of the same listing,
266
- // so age and weight cost nothing extra — which is what lets the drain work
267
- // oldest-first and lets a backlog be measured without reading any of it.
268
- let body;
269
- try {
270
- body = await readCapped(res, LISTING_MAX_BYTES);
271
- } catch (e) {
272
- this.log(`listing at ${url}: ${e.message} — using the last known listing`);
273
- return known?.children ?? [];
274
- }
275
- const g = $rdf.graph();
276
- $rdf.parse(body, g, url, 'text/turtle');
277
- const here = $rdf.sym(url);
278
- const seen = new Set();
279
- const list = [];
280
- for (const child of g.each(here, LDP('contains'), null, here)) {
281
- const u = child.value;
282
- // `.receipt.json` is a verification receipt a gateway wrote beside an
283
- // inbox item — read with the item, never enumerated as an item itself.
284
- if (!u.startsWith(url) || u === url || /\.(acl|meta|receipt\.json)$/.test(u) || seen.has(u)) continue;
285
- seen.add(u);
286
- list.push({
287
- url: u,
288
- size: Number(g.any(child, POSIX('size'), null, here)?.value || 0),
289
- modified: g.any(child, DC('modified'), null, here)?.value || null,
290
- });
291
- }
292
- // Oldest first. An LDP listing is a set, so without this the drain works
293
- // in whatever order the graph happened to parse — a mention from last week
294
- // after one from today, and no way to make progress predictable.
295
- list.sort((a, b) => String(a.modified || '').localeCompare(String(b.modified || '')));
296
- this._listCache.set(url, { etag: res.headers.get('etag'), children: list });
297
- return list;
298
- }
299
-
300
- // WAC doc granting the public `publicModes` on target, owner full control.
301
- // An empty publicModes list yields an owner-only document.
302
- //
303
- // Built and serialised by rdflib, like every other document this project
304
- // writes — see claude/plans/no-regex-rdf.md. This was the last site still
305
- // assembling Turtle from template literals, and it is the highest-consequence
306
- // RDF here: an ACL that comes out malformed, or naming the wrong subject,
307
- // either locks the owner out or leaves the private trees world-readable.
308
- // $rdf.sym() also throws on an illegal IRI, so a pod URL with something odd
309
- // in it fails here rather than silently producing a document that means
310
- // something else.
311
- aclDoc(targetUrl, publicModes, { appendAgents = [], aclUrl = null } = {}) {
312
- const url = aclUrl || targetUrl + '.acl';
313
- const doc = $rdf.sym(url);
314
- const target = $rdf.sym(targetUrl);
315
- const g = $rdf.graph();
316
- const authorize = (subject, agentPred, agent, modes) => {
317
- g.add(subject, RDF('type'), ACL('Authorization'), doc);
318
- g.add(subject, agentPred, agent, doc);
319
- g.add(subject, ACL('accessTo'), target, doc);
320
- g.add(subject, ACL('default'), target, doc);
321
- for (const m of modes) g.add(subject, ACL('mode'), ACL(m), doc);
322
- };
323
- if (publicModes.length) {
324
- authorize($rdf.sym(url + '#public'), ACL('agentClass'), FOAF('Agent'), publicModes);
325
- }
326
- // Named agents granted Append but not public: this is how the inbox is
327
- // locked to a gateway — public loses Append, the gateway's WebID keeps it.
328
- appendAgents.forEach((webId, i) =>
329
- authorize($rdf.sym(url + `#gw${i}`), ACL('agent'), $rdf.sym(webId), ['Append']));
330
- authorize($rdf.sym(url + '#owner'), ACL('agent'), $rdf.sym(this.webId),
331
- ['Read', 'Write', 'Control']);
332
- return $rdf.serialize(doc, g, url, 'text/turtle');
333
- }
334
-
335
- async setAcl(targetUrl, publicModes, opts = {}) {
336
- const url = await this.aclUrlFor(targetUrl);
337
- if (!await this.aclWritable(url)) return null;
338
- return this.put(url, this.aclDoc(targetUrl, publicModes, { ...opts, aclUrl: url }), 'text/turtle');
339
- }
340
-
341
- // The WebID profile advertises the actor as an account:
342
- // <webId> foaf:account <actor> .
343
- // <actor> a foaf:OnlineAccount, as:Person|as:Group ; foaf:accountName "@handle@host" .
344
- // Read–check–write through rdflib. Returns false when the profile already
345
- // says all of it. The parsed graph must mention the WebID before anything is
346
- // written back — an empty or foreign body must never become the new profile.
347
- async linkAccountInProfile({ actorUrl, accountName, kind = 'person' }) {
348
- const docUrl = this.webId.split('#')[0];
349
- const res = await this.fetch(docUrl, { headers: { accept: 'text/turtle' } });
350
- if (res.status >= 400) throw new Error(`GET ${docUrl} → ${res.status}`);
351
- const g = $rdf.graph();
352
- $rdf.parse(await res.text(), g, docUrl, 'text/turtle');
353
- const doc = $rdf.sym(docUrl);
354
- const me = $rdf.sym(this.webId);
355
- if (!g.statementsMatching(me, null, null, doc).length) {
356
- throw new Error(`profile at ${docUrl} does not mention ${this.webId} — not rewriting it`);
357
- }
358
- const actor = $rdf.sym(actorUrl);
359
- const wanted = [
360
- [me, FOAF('account'), actor],
361
- [actor, RDF('type'), FOAF('OnlineAccount')],
362
- [actor, RDF('type'), kind === 'group' ? AS('Group') : AS('Person')],
363
- [actor, FOAF('accountName'), $rdf.literal(accountName)],
364
- ];
365
- const missing = wanted.filter(([s, p, o]) => !g.holds(s, p, o, doc));
366
- // A handle change leaves the old accountName behind; ours is replaced.
367
- const stale = g.statementsMatching(actor, FOAF('accountName'), null, doc)
368
- .filter(st => st.object.value !== accountName);
369
- if (!missing.length && !stale.length) return false;
370
- // A patch touches these statements and nothing else. Rewriting the whole
371
- // profile re-serialises statements that are not ours — the OIDC issuer
372
- // among them — and a server is entitled to refuse a write that would.
373
- const deletes = stale.map(st => [ st.subject, st.predicate, st.object ]);
374
- if (await this.patchDocument(docUrl, missing, deletes)) return true;
375
- for (const st of stale) g.remove(st);
376
- for (const [s, p, o] of missing) g.add(s, p, o, doc);
377
- await this.put(docUrl, $rdf.serialize(doc, g, docUrl, 'text/turtle'), 'text/turtle');
378
- return true;
379
- }
380
-
381
- /**
382
- * An N3 Patch of exactly these statements, or false when the pod will not
383
- * take one and the caller should write the document instead.
384
- *
385
- * The statements are serialised by rdflib; only the wrapper naming what is
386
- * being patched is assembled here, because N3's braces have no rdflib form.
387
- */
388
- n3Patch(docUrl, inserts, deletes) {
389
- const block = (triples) => {
390
- const g = $rdf.graph();
391
- for (const [s, p, o] of triples) g.add(s, p, o);
392
- return $rdf.serialize(null, g, docUrl, 'application/n-triples').trim();
393
- };
394
- const clauses = [];
395
- if (deletes.length) clauses.push(` solid:deletes { ${block(deletes)} }`);
396
- if (inserts.length) clauses.push(` solid:inserts { ${block(inserts)} }`);
397
- return `@prefix solid: <http://www.w3.org/ns/solid/terms#>.\n`
398
- + `<> a solid:InsertDeletePatch;\n${clauses.join(';\n')}.\n`;
399
- }
400
-
401
- async patchDocument(docUrl, inserts, deletes) {
402
- let res;
403
- try {
404
- res = await this.fetch(docUrl, {
405
- method: 'PATCH',
406
- headers: { 'content-type': 'text/n3' },
407
- body: this.n3Patch(docUrl, inserts, deletes),
408
- });
409
- } catch {
410
- return false; // no PATCH on this transport at all
411
- }
412
- if (res.status < 300) return true;
413
- // The pod cannot patch. Anything else — a 409 saying what we meant to
414
- // remove is not there any more — is a real answer, and rewriting the whole
415
- // document over the top of it would destroy whatever changed it.
416
- if (res.status === 405 || res.status === 415 || res.status === 501) return false;
417
- throw new Error(`PATCH ${docUrl} → ${res.status}`);
52
+ return { ...(this.session.stats?.() || {}), ...super.stats() };
418
53
  }
419
54
  }
package/lib/social.mjs CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  import crypto from 'node:crypto';
7
7
  import * as wire from './wire.mjs';
8
+ import * as podNotes from './pod/notes.mjs';
8
9
  import { dropFollower } from './store.mjs';
9
10
 
10
11
  // WebFinger, ours rather than fedify's — v2 dropped lookupWebFinger.
@@ -26,7 +27,17 @@ export async function lookupWebFinger(acct) {
26
27
  headers: { accept: 'application/jrd+json, application/json' },
27
28
  }).catch(() => null);
28
29
  if (!res || res.status >= 400) return null;
29
- try { return JSON.parse(await readCapped(res, 256 * 1024)); } catch { return null; }
30
+ let jrd;
31
+ try { jrd = JSON.parse(await readCapped(res, 256 * 1024)); } catch { return null; }
32
+ // The JRD says who it is about. Nothing checked it, so a host answering every
33
+ // query with one account's document made `@anyone@that.host` resolve to that
34
+ // account — and it was then stored under the handle that was asked for, not
35
+ // the one it turned out to be.
36
+ const subject = String(jrd?.subject || '').replace(/^acct:/, '').toLowerCase();
37
+ if (subject && subject !== clean.toLowerCase()) {
38
+ return null;
39
+ }
40
+ return jrd;
30
41
  }
31
42
 
32
43
  export const selfLink = (jrd) => jrd?.links?.find(l => l.rel === 'self' &&
@@ -317,6 +328,8 @@ export async function votePoll(agent, s, choices) {
317
328
  return { ok: false, error: 'this poll has closed' };
318
329
  }
319
330
  if (poll.voted) return { ok: false, error: 'already voted' };
331
+ // Your own poll is the one you are counting, not one you are answering.
332
+ if (s.actor === urls.actor) return { ok: false, error: 'this is your own poll' };
320
333
  const picks = [...new Set(choices)].filter(i => i >= 0 && i < opts.length);
321
334
  if (!picks.length) return { ok: false, error: 'invalid choice' };
322
335
  if (!poll.multiple && picks.length > 1) return { ok: false, error: 'this poll takes one choice' };
@@ -335,8 +348,8 @@ export async function votePoll(agent, s, choices) {
335
348
  '@context': wire.AS_CTX, id: wire.createActivityId(id), type: 'Create',
336
349
  actor: urls.actor, published, to: [s.actor], object: note,
337
350
  };
338
- await agent.remote.putJson(id, note);
339
- await agent.remote.putJson(create.id, create);
351
+ await podNotes.write(agent.remote, id, note);
352
+ await podNotes.writeCreate(agent.remote, create.id, create);
340
353
  await agent.deliverer.deliverToAll([inbox], create);
341
354
  }
342
355
  agent.store.updateStatus(s.noteId, {
@@ -362,10 +375,9 @@ export async function deleteNote(agent, s) {
362
375
  const stuck = [];
363
376
  const deletedAt = new Date().toISOString();
364
377
  try {
365
- await agent.remote.putJson(s.noteId, wire.noteTombstone(s.noteId, deletedAt));
366
- await agent.remote.setAcl(s.noteId, ['Read']);
378
+ await podNotes.writeTombstone(agent.remote, s.noteId, wire.noteTombstone(s.noteId, deletedAt));
367
379
  } catch { stuck.push(s.noteId); }
368
- if (!await agent.remote.delete(wire.createActivityId(s.noteId)).catch(() => false)) {
380
+ if (!await podNotes.dropCreate(agent.remote, wire.createActivityId(s.noteId))) {
369
381
  stuck.push(wire.createActivityId(s.noteId));
370
382
  }
371
383
  if (stuck.length) {
@@ -376,7 +388,7 @@ export async function deleteNote(agent, s) {
376
388
  };
377
389
  }
378
390
  // Empty, and referenced by nothing once the note is gone.
379
- await agent.remote.delete(wire.repliesId(s.noteId)).catch(() => {});
391
+ await podNotes.dropReplies(agent.remote, wire.repliesId(s.noteId));
380
392
 
381
393
  // The Bluesky mirror goes with the post. Best-effort: a mirror the far side
382
394
  // already lost is not a reason to keep the post here.
@@ -386,9 +398,8 @@ export async function deleteNote(agent, s) {
386
398
  .catch(e => agent.log?.(`bluesky mirror not deleted (${e.message}): ${s.atproto.uri}`));
387
399
  }
388
400
 
389
- // The note, and our own boost of it if there was one.
401
+ // The note's outbox entry, and our own boost of it if there was one.
390
402
  await agent.publisher.unrecordOutbox(i => i === s.noteId || (i?.id && i.id === s.announceActivity?.id));
391
- if (s.slug) await agent.local.delete(agent.local.fedi + 'posts/' + s.slug).catch(() => {});
392
403
  agent.store.removeStatus(s.noteId);
393
404
  return { ok: true };
394
405
  }
package/lib/storage.mjs CHANGED
@@ -30,8 +30,8 @@ export class HttpStorage {
30
30
  // The same jail FileStorage has, and for the same reason: a path here can
31
31
  // carry `..`, encodeURI leaves both `.` and `/` alone, and fetch normalises
32
32
  // the segments away — so a name derived from remote input could name a
33
- // resource outside the container entirely. PodRdf._rel only checks the
34
- // PREFIX, which a concatenated URL always satisfies, so it catches nothing.
33
+ // resource outside the container entirely. A prefix check is not enough on
34
+ // its own: a concatenated URL always satisfies one.
35
35
  _url(p) {
36
36
  const u = new URL(encodeURI(p), this.base);
37
37
  if (!u.href.startsWith(this.base)) throw new Error(`path escapes the container: ${p}`);
package/lib/store.mjs CHANGED
@@ -487,8 +487,15 @@ export class PodStore {
487
487
  if (ids[id] === url) return id; // already known, nothing to write
488
488
  const legacy = Object.entries(ids).find(([, u]) => u === url);
489
489
  if (legacy) return legacy[0]; // pre-hash entry a client may hold
490
+ // In memory only. This runs on the RENDER path — once per status, per
491
+ // account, per attachment, every time a timeline is drawn — and `write()`
492
+ // is write-through, so on the browser build scrolling a timeline was a pod
493
+ // PUT of this whole document. Nothing durable is lost: the id IS the hash
494
+ // of the url, so idFor recomputes it identically on any later run, and the
495
+ // map exists only so urlFor can still answer for ids minted under an older
496
+ // scheme (which are loaded from the pod and never written here).
490
497
  ids[id] = url;
491
- this.write('ids.json', ids);
498
+ this.cache.set('ids.json', ids);
492
499
  return id;
493
500
  }
494
501
  urlFor(id) { return this.getIds()[id] || null; }
package/lib/tagfeed.mjs CHANGED
@@ -8,6 +8,10 @@
8
8
 
9
9
  import { isContentType, authorOf } from './intake.mjs';
10
10
 
11
+ // A public timeline page from somebody else's instance. Generous for 20 posts,
12
+ // and a bound where there was none.
13
+ const MAX_TIMELINE_BYTES = 2 * 1024 * 1024;
14
+
11
15
  const DEFAULTS = {
12
16
  instance: 'https://mastodon.social',
13
17
  tags: ['solidproject', 'linkeddata', 'rdf'],
@@ -87,10 +91,18 @@ export class TagFeed {
87
91
  const known = new Set(this.store.getStatuses().map(s => s.noteId));
88
92
  let budget = MAX_NEW_PER_SWEEP;
89
93
  let added = 0;
94
+ // The store's 300ms debounce cannot coalesce a sweep that awaits between
95
+ // items — every note costs a fetch to somebody else's server, which outlives
96
+ // the timer — so statuses.json and actors.json, the two largest documents,
97
+ // were serialized and PUT whole once per new note. On by default, every 15
98
+ // minutes, and on the browser build every one of those is a pod write.
99
+ // acctfeed has done it this way since 2026-08-03; this is the same fix.
100
+ this.store.hold?.();
101
+ try {
90
102
  for (const tag of tags) {
91
103
  let list;
92
104
  try {
93
- const { safeFetch, retryAfterMs } = await import('./safefetch.mjs');
105
+ const { safeFetch, retryAfterMs, readCapped } = await import('./safefetch.mjs');
94
106
  const url = `${instance}/api/v1/timelines/tag/${encodeURIComponent(tag)}?limit=${PER_TAG}`;
95
107
  const res = this.fetcher === globalThis.fetch
96
108
  ? await safeFetch(url, { headers: { accept: 'application/json' } })
@@ -104,7 +116,9 @@ export class TagFeed {
104
116
  this._backOff(res.status, retryAfterMs(res));
105
117
  return;
106
118
  }
107
- list = await res.json();
119
+ // Somebody else's server, polled on our schedule: bounded, because
120
+ // res.json() would buffer a timeline of whatever size it sent.
121
+ list = JSON.parse(await readCapped(res, MAX_TIMELINE_BYTES));
108
122
  } catch (e) {
109
123
  // A network failure is the same signal, minus the courtesy.
110
124
  this.log(`tagfeed #${tag}: ${e.message}`);
@@ -152,6 +166,9 @@ export class TagFeed {
152
166
  const drop = new Set(tagged.slice(MAX_TAG_ENTRIES).map(s => s.noteId)); // arrival order: tail = oldest
153
167
  this.store.write('statuses.json', all.filter(s => !drop.has(s.noteId)));
154
168
  }
169
+ } finally {
170
+ this.store.release?.();
171
+ }
155
172
  this.lastAdded = added;
156
173
  if (added) this.log(`tagfeed: +${added} from #${tags.join(' #')}`);
157
174
  }