fedipod-server 0.21.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/README.md +5 -4
  2. package/dist/claims.d.ts +3 -1
  3. package/dist/claims.js +20 -7
  4. package/dist/components/context.jsonld +6 -0
  5. package/dist/handler.d.ts +29 -1
  6. package/dist/handler.js +89 -4
  7. package/dist/handler.jsonld +36 -1
  8. package/lib/client/c2s.mjs +100 -3
  9. package/lib/client/masto/accounts.mjs +44 -4
  10. package/lib/client/masto/body.mjs +90 -3
  11. package/lib/client/masto/index.mjs +5 -1
  12. package/lib/client/masto/instance.mjs +11 -3
  13. package/lib/client/masto/media.mjs +3 -57
  14. package/lib/client/masto/oauth.mjs +66 -9
  15. package/lib/client/masto/render.mjs +62 -5
  16. package/lib/client/masto/statuses.mjs +25 -1
  17. package/lib/client/masto/timelines.mjs +128 -6
  18. package/lib/connections/tagfeed.mjs +42 -5
  19. package/lib/core/as2.mjs +36 -5
  20. package/lib/core/contexts/index.mjs +14 -0
  21. package/lib/core/contexts/litepub-0.1.json +60 -0
  22. package/lib/core/contexts/map.json +3 -1
  23. package/lib/core/contexts/quotes.json +18 -0
  24. package/lib/core/graphview.mjs +41 -3
  25. package/lib/core/intake/activities.mjs +136 -2
  26. package/lib/core/intake/activity.mjs +79 -1
  27. package/lib/core/intake/group.mjs +37 -3
  28. package/lib/core/intake/index.mjs +32 -2
  29. package/lib/core/intake/notes.mjs +60 -3
  30. package/lib/core/lease.mjs +12 -2
  31. package/lib/core/profile-page.mjs +104 -0
  32. package/lib/core/publisher/collections.mjs +6 -1
  33. package/lib/core/publisher/index.mjs +35 -17
  34. package/lib/core/publisher/notes.mjs +104 -3
  35. package/lib/core/shapes/activitystreams.ttl +28 -0
  36. package/lib/core/shapes/index.mjs +26 -1
  37. package/lib/core/wire-quotes.mjs +71 -0
  38. package/lib/core/wire.mjs +59 -103
  39. package/lib/device/admin/routes/lifecycle.mjs +9 -0
  40. package/lib/device/admin/static.mjs +4 -1
  41. package/lib/device/admin/surface.mjs +26 -2
  42. package/lib/device/certs.mjs +18 -2
  43. package/lib/device/cli/commands/setup.mjs +6 -2
  44. package/lib/device/cli/commands/state.mjs +3 -1
  45. package/lib/device/gateway-move.mjs +58 -0
  46. package/lib/device/migrate.mjs +3 -1
  47. package/lib/device/setup.mjs +67 -7
  48. package/lib/gateway/front-core.mjs +307 -36
  49. package/lib/gateway/gateway-core.mjs +15 -6
  50. package/lib/gateway/notices.mjs +84 -0
  51. package/lib/gateway/quiet.mjs +184 -0
  52. package/lib/pod/root.mjs +6 -3
  53. package/lib/pod/state.mjs +8 -7
  54. package/lib/pod/transport.mjs +5 -1
  55. package/lib/session/README.md +123 -0
  56. package/lib/session/demo.html +94 -0
  57. package/lib/session/fedi-account.mjs +346 -0
  58. package/lib/session/fedi-login.mjs +123 -0
  59. package/lib/session/oidc-session.mjs +233 -0
  60. package/lib/session/package.json +41 -0
  61. package/package.json +1 -1
  62. package/run-agent.mjs +28 -0
  63. package/vendor/gate.cjs +6 -3
  64. package/web/admin/bar.css +66 -0
  65. package/web/admin/bar.js +38 -0
  66. package/web/admin/client/index.html +31 -3
  67. package/web/admin/client-pick.js +79 -0
  68. package/web/admin/gateway.js +22 -0
  69. package/web/admin/index.html +54 -13
  70. package/web/admin/notices-bar.js +90 -0
  71. package/web/admin/oauth-signin.mjs +37 -0
  72. package/web/admin/record.js +13 -1
  73. package/web/admin/setup/index.html +31 -3
  74. package/web/admin/tokens.css +15 -3
  75. package/web/admin/upkeep.js +4 -5
  76. package/web/app/README.md +14 -10
  77. package/web/app/admin-facade.mjs +51 -12
  78. package/web/app/agent.mjs +157 -19
  79. package/web/app/authorize_interaction.html +56 -0
  80. package/web/app/authorize_interaction.js +69 -0
  81. package/web/app/boot.mjs +233 -95
  82. package/web/app/dist/boot.js +477 -569
  83. package/web/app/dist/boot.js.map +4 -4
  84. package/web/app/dist/sw.js +1861 -532
  85. package/web/app/dist/sw.js.map +4 -4
  86. package/web/app/gateway-move.mjs +60 -0
  87. package/web/app/index.html +40 -49
  88. package/web/app/keys-browser.mjs +12 -15
  89. package/web/app/keystore.mjs +19 -36
  90. package/web/app/oidc-session.mjs +5 -188
  91. package/web/app/signup.mjs +172 -132
  92. package/web/app/sw-src.mjs +1 -1
  93. package/web/app/update.js +66 -0
  94. package/web/front/admin.html +5 -3
  95. package/web/front/new-account.html +2 -2
  96. package/web/front/notices.html +69 -0
  97. package/web/front/notices.js +107 -0
  98. package/web/front/run.html +15 -8
  99. package/web/front/run.js +124 -36
  100. package/web/icons/forum.svg +11 -0
  101. package/web/icons/gateway.svg +14 -0
  102. package/web/icons/local.svg +11 -0
  103. package/web/app/index.html~ +0 -152
  104. package/web/app/site/_headers +0 -9
  105. package/web/app/site/_redirects +0 -22
  106. package/web/app/site/admin/actors.js +0 -145
  107. package/web/app/site/admin/bar.css +0 -60
  108. package/web/app/site/admin/bar.js +0 -61
  109. package/web/app/site/admin/client/client.js +0 -66
  110. package/web/app/site/admin/client/index.html +0 -43
  111. package/web/app/site/admin/common.js +0 -23
  112. package/web/app/site/admin/connections.js +0 -112
  113. package/web/app/site/admin/gateway.js +0 -111
  114. package/web/app/site/admin/group.js +0 -258
  115. package/web/app/site/admin/index.html +0 -564
  116. package/web/app/site/admin/record.js +0 -378
  117. package/web/app/site/admin/setup/index.html +0 -215
  118. package/web/app/site/admin/setup/setup.js +0 -326
  119. package/web/app/site/admin/tokens.css +0 -56
  120. package/web/app/site/admin/upkeep.js +0 -178
  121. package/web/app/site/admin/window.css +0 -55
  122. package/web/app/site/admin/window.js +0 -136
  123. package/web/app/site/app/404.html +0 -40
  124. package/web/app/site/app/_headers +0 -2
  125. package/web/app/site/app/apple-touch-icon.png +0 -0
  126. package/web/app/site/app/assets/ICONS-DIPJeU0C.js +0 -2
  127. package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +0 -1
  128. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +0 -183
  129. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
  130. package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
  131. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +0 -2
  132. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +0 -1
  133. package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  134. package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
  135. package/web/app/site/app/assets/chunk-aKtaBQYM.js +0 -1
  136. package/web/app/site/app/assets/compose-CFEx7wb4.js +0 -2
  137. package/web/app/site/app/assets/compose-CFEx7wb4.js.map +0 -1
  138. package/web/app/site/app/assets/compose-ZJN5xSb4.js +0 -35
  139. package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +0 -1
  140. package/web/app/site/app/assets/debug.module-YFQgwnIr.js +0 -52
  141. package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +0 -1
  142. package/web/app/site/app/assets/dom-p1YcJ5Rw.js +0 -2
  143. package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +0 -1
  144. package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  145. package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  146. package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  147. package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  148. package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +0 -1
  149. package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +0 -1
  150. package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
  151. package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +0 -1
  152. package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
  153. package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
  154. package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
  155. package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
  156. package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
  157. package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
  158. package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
  159. package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
  160. package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +0 -1
  161. package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +0 -1
  162. package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
  163. package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +0 -1
  164. package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
  165. package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +0 -1
  166. package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
  167. package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
  168. package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +0 -1
  169. package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
  170. package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +0 -1
  171. package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
  172. package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
  173. package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
  174. package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
  175. package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
  176. package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +0 -1
  177. package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +0 -1
  178. package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +0 -1
  179. package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +0 -1
  180. package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +0 -1
  181. package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
  182. package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +0 -1
  183. package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
  184. package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +0 -1
  185. package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +0 -1
  186. package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +0 -1
  187. package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
  188. package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
  189. package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +0 -1
  190. package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +0 -1
  191. package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
  192. package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
  193. package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +0 -1
  194. package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +0 -1
  195. package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +0 -1
  196. package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
  197. package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +0 -1
  198. package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +0 -1
  199. package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +0 -1
  200. package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
  201. package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
  202. package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
  203. package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +0 -1
  204. package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +0 -1
  205. package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +0 -1
  206. package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +0 -1
  207. package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +0 -1
  208. package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
  209. package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
  210. package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +0 -1
  211. package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +0 -1
  212. package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +0 -1
  213. package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +0 -1
  214. package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +0 -1
  215. package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +0 -1
  216. package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +0 -1
  217. package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +0 -1
  218. package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
  219. package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +0 -1
  220. package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +0 -1
  221. package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +0 -1
  222. package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
  223. package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +0 -1
  224. package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +0 -1
  225. package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
  226. package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
  227. package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
  228. package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +0 -1
  229. package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +0 -1
  230. package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
  231. package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +0 -1
  232. package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +0 -1
  233. package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +0 -1
  234. package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +0 -1
  235. package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +0 -1
  236. package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +0 -1
  237. package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +0 -1
  238. package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +0 -1
  239. package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +0 -1
  240. package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
  241. package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +0 -1
  242. package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
  243. package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
  244. package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
  245. package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +0 -1
  246. package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
  247. package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +0 -1
  248. package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +0 -1
  249. package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
  250. package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +0 -1
  251. package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
  252. package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +0 -1
  253. package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
  254. package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +0 -1
  255. package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +0 -1
  256. package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
  257. package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +0 -1
  258. package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +0 -1
  259. package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
  260. package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +0 -1
  261. package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +0 -1
  262. package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +0 -1
  263. package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
  264. package/web/app/site/app/assets/instances-BNPptnn-.json +0 -333
  265. package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +0 -1
  266. package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +0 -1
  267. package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
  268. package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +0 -1
  269. package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +0 -1
  270. package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +0 -1
  271. package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +0 -1
  272. package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +0 -1
  273. package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +0 -1
  274. package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +0 -1
  275. package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +0 -1
  276. package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +0 -1
  277. package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +0 -1
  278. package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +0 -1
  279. package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +0 -1
  280. package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +0 -1
  281. package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +0 -1
  282. package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
  283. package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +0 -1
  284. package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
  285. package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +0 -1
  286. package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +0 -1
  287. package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
  288. package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +0 -1
  289. package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +0 -1
  290. package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
  291. package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +0 -1
  292. package/web/app/site/app/assets/locales/tok-CfheRbCu.js +0 -1
  293. package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +0 -1
  294. package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +0 -1
  295. package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +0 -1
  296. package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +0 -1
  297. package/web/app/site/app/assets/main-BdqNbG-a.js +0 -26
  298. package/web/app/site/app/assets/main-BdqNbG-a.js.map +0 -1
  299. package/web/app/site/app/assets/mock-home-DGhR__He.js +0 -2
  300. package/web/app/site/app/assets/mock-home-DGhR__He.js.map +0 -1
  301. package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
  302. package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  303. package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  304. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +0 -2
  305. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +0 -1
  306. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +0 -20
  307. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +0 -1
  308. package/web/app/site/app/assets/style-B1iLPTPR.css +0 -2
  309. package/web/app/site/app/assets/temml-DuyIQMei.js +0 -97
  310. package/web/app/site/app/assets/temml-DuyIQMei.js.map +0 -1
  311. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +0 -2
  312. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
  313. package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
  314. package/web/app/site/app/assets/year-in-posts-DJv_star.js +0 -3
  315. package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +0 -1
  316. package/web/app/site/app/compose/index.html +0 -20
  317. package/web/app/site/app/favicon.ico +0 -0
  318. package/web/app/site/app/index.html +0 -73
  319. package/web/app/site/app/logo-192.png +0 -0
  320. package/web/app/site/app/logo-512.png +0 -0
  321. package/web/app/site/app/logo-badge-72.png +0 -0
  322. package/web/app/site/app/logo-maskable-512.png +0 -0
  323. package/web/app/site/app/logo-monochrome-512.png +0 -0
  324. package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
  325. package/web/app/site/app/manifest.webmanifest +0 -1
  326. package/web/app/site/app/og-image-2.jpg +0 -0
  327. package/web/app/site/app/og-image.png +0 -0
  328. package/web/app/site/app/robots.txt +0 -2
  329. package/web/app/site/app/version.json +0 -1
  330. package/web/app/site/boot.js +0 -34277
  331. package/web/app/site/index.html +0 -251
  332. package/web/app/site/sw.js +0 -71331
@@ -157,6 +157,116 @@ var init_urls = __esm({
157
157
  }
158
158
  });
159
159
 
160
+ // lib/core/profile-page.mjs
161
+ function profilePageHtml({
162
+ name,
163
+ address,
164
+ summary = null,
165
+ icon = null,
166
+ image = null,
167
+ fields = [],
168
+ joined = null,
169
+ pinned = [],
170
+ kind = "person"
171
+ }) {
172
+ const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ ...HTML_ESCAPES, '"': "&quot;" })[c]);
173
+ const what = kind === "group" ? "a group" : "an account";
174
+ const month = (iso) => {
175
+ const d = new Date(iso);
176
+ return Number.isNaN(d.getTime()) ? null : d.toLocaleDateString("en", { month: "long", year: "numeric", timeZone: "UTC" });
177
+ };
178
+ const day = (iso) => {
179
+ const d = new Date(iso);
180
+ return Number.isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10);
181
+ };
182
+ const fieldRows = fields.filter((f) => f?.name && f?.value !== void 0 && f?.value !== null && String(f.value) !== "");
183
+ const joinedText = joined ? month(joined) : null;
184
+ const pins = pinned.filter((x) => x && typeof x.content === "string");
185
+ return `<!doctype html>
186
+ <html lang="en">
187
+ <head>
188
+ <meta charset="utf-8">
189
+ <meta name="viewport" content="width=device-width, initial-scale=1">
190
+ <title>${esc(name)} (${esc(address)})</title>
191
+ <style>
192
+ :root { color-scheme: light dark; }
193
+ body { font: 112.5%/1.5 system-ui, sans-serif; max-width: 34rem; margin: 3rem auto; padding: 0 1rem; }
194
+ img.header { width: 100%; max-height: 12rem; object-fit: cover; border-radius: 1rem; }
195
+ img.avatar { width: 6rem; height: 6rem; border-radius: 1rem; object-fit: cover; margin-top: 1rem; }
196
+ h1 { margin: .5rem 0 0; }
197
+ .address { font-size: 1.1rem; user-select: all; }
198
+ dl.fields { display: grid; grid-template-columns: max-content 1fr; gap: .4rem 1rem; margin: 1.5rem 0; }
199
+ dl.fields dt { font-weight: 600; }
200
+ dl.fields dd { margin: 0; overflow-wrap: anywhere; }
201
+ .joined { margin: 1rem 0; }
202
+ section.pinned h2 { font-size: 1.1rem; margin: 2rem 0 .5rem; }
203
+ article.pin { border: 1px solid #8884; border-radius: 1rem; padding: 1rem 1.2rem; margin: 1rem 0; }
204
+ article.pin p:last-child { margin-bottom: 0; }
205
+ article.pin .when { font-size: 1rem; }
206
+ form { margin-top: 2rem; }
207
+ label { display: block; margin-bottom: .3rem; }
208
+ input { font: inherit; padding: .5rem; width: 14rem; max-width: 100%; }
209
+ button { font: inherit; padding: .5rem 1rem; }
210
+ .hint { color: #666; font-size: 1rem; }
211
+ .err { color: #b00020; }
212
+ .err:empty { display: none; }
213
+ @media (prefers-color-scheme: dark) {
214
+ .hint { color: #aaa; }
215
+ .err { color: #ff8a80; }
216
+ }
217
+ </style>
218
+ </head>
219
+ <body>
220
+ <main>
221
+ ${image ? `<img class="header" src="${esc(image)}" alt="">` : ""}
222
+ ${icon ? `<img class="avatar" src="${esc(icon)}" alt="">` : ""}
223
+ <h1>${esc(name)}</h1>
224
+ <p class="address">${esc(address)}</p>
225
+ ${summary ? `<div>${summary}</div>` : ""}
226
+ ${fieldRows.length ? `<dl class="fields">
227
+ ${fieldRows.map((f) => `<dt>${esc(f.name)}</dt><dd>${esc(f.value)}</dd>`).join("\n")}
228
+ </dl>` : ""}
229
+ ${joinedText ? `<p class="joined">Joined ${esc(joinedText)}</p>` : ""}
230
+ ${pins.length ? `<section class="pinned">
231
+ <h2>Pinned</h2>
232
+ ${pins.map((x) => `<article class="pin">
233
+ ${x.content}
234
+ <p class="when">${x.url ? `<a href="${esc(x.url)}">${esc(day(x.published))}</a>` : esc(day(x.published))}</p>
235
+ </article>`).join("\n")}
236
+ </section>` : ""}
237
+ <p>This is ${what} on the Fediverse. To follow it, paste the address above
238
+ into the search box of Mastodon or any Fediverse app \u2014 or use the form.</p>
239
+ <form id="follow">
240
+ <label for="server">your server</label>
241
+ <input id="server" type="text" placeholder="mastodon.social" autocomplete="off"
242
+ required aria-describedby="follow-err">
243
+ <button type="submit">Follow</button>
244
+ <p class="err" id="follow-err" role="alert"></p>
245
+ </form>
246
+ <p class="hint">The form sends you to your own server's follow screen.</p>
247
+ </main>
248
+ <script>
249
+ document.getElementById('follow').addEventListener('submit', (ev) => {
250
+ ev.preventDefault();
251
+ const s = document.getElementById('server').value.trim().replace(/^https?:\\/\\//, '').replace(/\\/.*$/, '');
252
+ const err = document.getElementById('follow-err');
253
+ const addr = document.querySelector('.address').textContent.trim();
254
+ if (!s) { err.textContent = 'Enter your server, e.g. mastodon.social'; return; }
255
+ err.textContent = '';
256
+ location.href = 'https://' + s + '/authorize_interaction?uri=' + encodeURIComponent(addr);
257
+ });
258
+ <\/script>
259
+ </body>
260
+ </html>
261
+ `;
262
+ }
263
+ var HTML_ESCAPES;
264
+ var init_profile_page = __esm({
265
+ "lib/core/profile-page.mjs"() {
266
+ HTML_ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;" };
267
+ }
268
+ });
269
+
160
270
  // node_modules/entities/dist/decode-codepoint.js
161
271
  function replaceCodePoint(codePoint) {
162
272
  if (codePoint >= 55296 && codePoint <= 57343 || codePoint > 1114111) {
@@ -9590,9 +9700,12 @@ __export(wire_exports, {
9590
9700
  DEFAULT_ROOT: () => DEFAULT_ROOT,
9591
9701
  FOLLOWERS_PAGE_SIZE: () => FOLLOWERS_PAGE_SIZE,
9592
9702
  HASHTAG_RE: () => HASHTAG_RE,
9703
+ LEMMY_CTX: () => LEMMY_CTX,
9593
9704
  MENTION_RE: () => MENTION_RE,
9594
9705
  OUTBOX_PAGE_SIZE: () => OUTBOX_PAGE_SIZE,
9706
+ POLICY_CTX: () => POLICY_CTX,
9595
9707
  PUBLIC: () => PUBLIC,
9708
+ QUOTE_CTX: () => QUOTE_CTX,
9596
9709
  SEC_CTX: () => SEC_CTX,
9597
9710
  acceptActivity: () => acceptActivity,
9598
9711
  actorDoc: () => actorDoc,
@@ -9640,6 +9753,7 @@ __export(wire_exports, {
9640
9753
  profilePageHtml: () => profilePageHtml,
9641
9754
  publicHandle: () => publicHandle,
9642
9755
  questionDoc: () => questionDoc,
9756
+ quotePolicy: () => quotePolicy,
9643
9757
  rejectActivity: () => rejectActivity,
9644
9758
  repliesId: () => repliesId,
9645
9759
  sanitizeHtml: () => sanitizeHtml,
@@ -9699,7 +9813,8 @@ function actorDoc({
9699
9813
  inbox = null,
9700
9814
  outbox = null,
9701
9815
  oauthAuthorize = null,
9702
- oauthToken = null
9816
+ oauthToken = null,
9817
+ postingRestrictedToMods = null
9703
9818
  }) {
9704
9819
  const context = [AS_CTX, SEC_CTX];
9705
9820
  context.push({ toot: "http://joinmastodon.org/ns#", featured: { "@id": "toot:featured", "@type": "@id" } });
@@ -9722,13 +9837,16 @@ function actorDoc({
9722
9837
  });
9723
9838
  }
9724
9839
  if (webId || aliases.length) context.push({ alsoKnownAs: { "@id": "as:alsoKnownAs", "@type": "@id" } });
9840
+ if (kind === "group" && postingRestrictedToMods !== null) context.push(LEMMY_CTX);
9725
9841
  if (fields.length) {
9726
9842
  context.push({ schema: "http://schema.org#", PropertyValue: "schema:PropertyValue", value: "schema:value" });
9727
9843
  }
9728
9844
  return {
9729
9845
  "@context": context,
9730
9846
  id: urls.actor,
9731
- type: kind === "group" ? "Group" : "Person",
9847
+ // A group is a community; an application is a service that speaks for a
9848
+ // site rather than a person — the actor a forum publishes for itself.
9849
+ type: kind === "group" ? "Group" : kind === "application" ? "Application" : "Person",
9732
9850
  ...approveJoins ? { manuallyApprovesFollowers: true } : {},
9733
9851
  ...assertionKey ? { assertionMethod: [{
9734
9852
  id: assertionKeyId(urls),
@@ -9738,6 +9856,7 @@ function actorDoc({
9738
9856
  }] } : {},
9739
9857
  ...movedTo ? { movedTo } : {},
9740
9858
  ...webId || aliases.length ? { alsoKnownAs: [...webId ? [webId] : [], ...aliases] } : {},
9859
+ ...kind === "group" && postingRestrictedToMods !== null ? { postingRestrictedToMods: !!postingRestrictedToMods } : {},
9741
9860
  preferredUsername: handle7,
9742
9861
  name: name || handle7,
9743
9862
  // The bio, and the avatar. For a group, `summary` is where what the group
@@ -9753,8 +9872,8 @@ function actorDoc({
9753
9872
  ...fields.length ? {
9754
9873
  attachment: fields.map((f) => ({
9755
9874
  type: "PropertyValue",
9756
- name: String(f.name).replace(/[&<>]/g, (c) => HTML_ESCAPES[c]),
9757
- value: String(f.value).replace(/[&<>]/g, (c) => HTML_ESCAPES[c])
9875
+ name: String(f.name).replace(/[&<>]/g, (c) => HTML_ESCAPES2[c]),
9876
+ value: String(f.value).replace(/[&<>]/g, (c) => HTML_ESCAPES2[c])
9758
9877
  }))
9759
9878
  } : {},
9760
9879
  // The human page. Remote servers send profile clicks here, so it must be
@@ -9797,108 +9916,6 @@ function actorDoc({
9797
9916
  publicKey: { id: urls.actor + "#main-key", owner: urls.actor, publicKeyPem }
9798
9917
  };
9799
9918
  }
9800
- function profilePageHtml({
9801
- name,
9802
- address,
9803
- summary = null,
9804
- icon = null,
9805
- image = null,
9806
- fields = [],
9807
- joined = null,
9808
- pinned = [],
9809
- kind = "person"
9810
- }) {
9811
- const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ ...HTML_ESCAPES, '"': "&quot;" })[c]);
9812
- const what = kind === "group" ? "a group" : "an account";
9813
- const month = (iso) => {
9814
- const d = new Date(iso);
9815
- return Number.isNaN(d.getTime()) ? null : d.toLocaleDateString("en", { month: "long", year: "numeric", timeZone: "UTC" });
9816
- };
9817
- const day = (iso) => {
9818
- const d = new Date(iso);
9819
- return Number.isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10);
9820
- };
9821
- const fieldRows = fields.filter((f) => f?.name && f?.value !== void 0 && f?.value !== null && String(f.value) !== "");
9822
- const joinedText = joined ? month(joined) : null;
9823
- const pins = pinned.filter((x) => x && typeof x.content === "string");
9824
- return `<!doctype html>
9825
- <html lang="en">
9826
- <head>
9827
- <meta charset="utf-8">
9828
- <meta name="viewport" content="width=device-width, initial-scale=1">
9829
- <title>${esc(name)} (${esc(address)})</title>
9830
- <style>
9831
- :root { color-scheme: light dark; }
9832
- body { font: 112.5%/1.5 system-ui, sans-serif; max-width: 34rem; margin: 3rem auto; padding: 0 1rem; }
9833
- img.header { width: 100%; max-height: 12rem; object-fit: cover; border-radius: 1rem; }
9834
- img.avatar { width: 6rem; height: 6rem; border-radius: 1rem; object-fit: cover; margin-top: 1rem; }
9835
- h1 { margin: .5rem 0 0; }
9836
- .address { font-size: 1.1rem; user-select: all; }
9837
- dl.fields { display: grid; grid-template-columns: max-content 1fr; gap: .4rem 1rem; margin: 1.5rem 0; }
9838
- dl.fields dt { font-weight: 600; }
9839
- dl.fields dd { margin: 0; overflow-wrap: anywhere; }
9840
- .joined { margin: 1rem 0; }
9841
- section.pinned h2 { font-size: 1.1rem; margin: 2rem 0 .5rem; }
9842
- article.pin { border: 1px solid #8884; border-radius: 1rem; padding: 1rem 1.2rem; margin: 1rem 0; }
9843
- article.pin p:last-child { margin-bottom: 0; }
9844
- article.pin .when { font-size: 1rem; }
9845
- form { margin-top: 2rem; }
9846
- label { display: block; margin-bottom: .3rem; }
9847
- input { font: inherit; padding: .5rem; width: 14rem; max-width: 100%; }
9848
- button { font: inherit; padding: .5rem 1rem; }
9849
- .hint { color: #666; font-size: 1rem; }
9850
- .err { color: #b00020; }
9851
- .err:empty { display: none; }
9852
- @media (prefers-color-scheme: dark) {
9853
- .hint { color: #aaa; }
9854
- .err { color: #ff8a80; }
9855
- }
9856
- </style>
9857
- </head>
9858
- <body>
9859
- <main>
9860
- ${image ? `<img class="header" src="${esc(image)}" alt="">` : ""}
9861
- ${icon ? `<img class="avatar" src="${esc(icon)}" alt="">` : ""}
9862
- <h1>${esc(name)}</h1>
9863
- <p class="address">${esc(address)}</p>
9864
- ${summary ? `<div>${summary}</div>` : ""}
9865
- ${fieldRows.length ? `<dl class="fields">
9866
- ${fieldRows.map((f) => `<dt>${esc(f.name)}</dt><dd>${esc(f.value)}</dd>`).join("\n")}
9867
- </dl>` : ""}
9868
- ${joinedText ? `<p class="joined">Joined ${esc(joinedText)}</p>` : ""}
9869
- ${pins.length ? `<section class="pinned">
9870
- <h2>Pinned</h2>
9871
- ${pins.map((x) => `<article class="pin">
9872
- ${x.content}
9873
- <p class="when">${x.url ? `<a href="${esc(x.url)}">${esc(day(x.published))}</a>` : esc(day(x.published))}</p>
9874
- </article>`).join("\n")}
9875
- </section>` : ""}
9876
- <p>This is ${what} on the Fediverse. To follow it, paste the address above
9877
- into the search box of Mastodon or any Fediverse app \u2014 or use the form.</p>
9878
- <form id="follow">
9879
- <label for="server">your server</label>
9880
- <input id="server" type="text" placeholder="mastodon.social" autocomplete="off"
9881
- required aria-describedby="follow-err">
9882
- <button type="submit">Follow</button>
9883
- <p class="err" id="follow-err" role="alert"></p>
9884
- </form>
9885
- <p class="hint">The form sends you to your own server's follow screen.</p>
9886
- </main>
9887
- <script>
9888
- document.getElementById('follow').addEventListener('submit', (ev) => {
9889
- ev.preventDefault();
9890
- const s = document.getElementById('server').value.trim().replace(/^https?:\\/\\//, '').replace(/\\/.*$/, '');
9891
- const err = document.getElementById('follow-err');
9892
- const addr = document.querySelector('.address').textContent.trim();
9893
- if (!s) { err.textContent = 'Enter your server, e.g. mastodon.social'; return; }
9894
- err.textContent = '';
9895
- location.href = 'https://' + s + '/authorize_interaction?uri=' + encodeURIComponent(addr);
9896
- });
9897
- <\/script>
9898
- </body>
9899
- </html>
9900
- `;
9901
- }
9902
9919
  function deleteActorActivity(urls, stamp) {
9903
9920
  return {
9904
9921
  "@context": AS_CTX,
@@ -10082,7 +10099,7 @@ function titledContent(note) {
10082
10099
  if (!title || types.includes("Note")) return html;
10083
10100
  const opening = html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().toLowerCase();
10084
10101
  if (opening.startsWith(title.toLowerCase())) return html;
10085
- const esc = String(title).replace(/[&<>]/g, (c) => HTML_ESCAPES[c]);
10102
+ const esc = String(title).replace(/[&<>]/g, (c) => HTML_ESCAPES2[c]);
10086
10103
  return `<p><strong>${esc}</strong></p>${html}`;
10087
10104
  }
10088
10105
  function attachmentsOf(note) {
@@ -10108,18 +10125,18 @@ function tagHref(urls, name) {
10108
10125
  return urls.profileHtml + "?tag=" + encodeURIComponent(String(name).toLowerCase());
10109
10126
  }
10110
10127
  function contentHtml(text, mentions = [], hashtags = []) {
10111
- let html = String(text).replace(/[&<>]/g, (c) => HTML_ESCAPES[c]);
10128
+ let html = String(text).replace(/[&<>]/g, (c) => HTML_ESCAPES2[c]);
10112
10129
  if (hashtags.length) {
10113
10130
  const byName = new Map(hashtags.filter((h) => h?.name && h?.href).map((h) => [h.name.toLowerCase(), h.href]));
10114
10131
  html = html.replace(HASHTAG_RE, (all, lead, name) => {
10115
10132
  const href = byName.get(name.toLowerCase());
10116
10133
  if (!href) return all;
10117
- return `${lead}<a href="${String(href).replace(/[&<>"]/g, (c) => HTML_ESCAPES[c] || "&quot;")}" class="mention hashtag" rel="tag">#<span>${name}</span></a>`;
10134
+ return `${lead}<a href="${String(href).replace(/[&<>"]/g, (c) => HTML_ESCAPES2[c] || "&quot;")}" class="mention hashtag" rel="tag">#<span>${name}</span></a>`;
10118
10135
  });
10119
10136
  }
10120
10137
  for (const m of mentions) {
10121
10138
  if (!m?.handle || !m?.actor) continue;
10122
- const href = String(m.page || m.actor).replace(/[&<>"]/g, (c) => HTML_ESCAPES[c] || "&quot;");
10139
+ const href = String(m.page || m.actor).replace(/[&<>"]/g, (c) => HTML_ESCAPES2[c] || "&quot;");
10123
10140
  html = html.split("@" + m.handle).join(
10124
10141
  `<a href="${href}" class="u-url mention">@${m.handle.split("@")[0]}</a>`
10125
10142
  );
@@ -10134,6 +10151,10 @@ function addressing(urls, visibility, who = []) {
10134
10151
  direct: { to: who, cc: [] }
10135
10152
  }[visibility] || { to: [PUBLIC], cc: [urls.followers, ...who] };
10136
10153
  }
10154
+ function quotePolicy(visibility) {
10155
+ const open2 = visibility === "public" || visibility === "unlisted";
10156
+ return { canQuote: { automaticApproval: open2 ? [PUBLIC] : [], manualApproval: [] } };
10157
+ }
10137
10158
  function noteDoc({
10138
10159
  urls,
10139
10160
  slug,
@@ -10147,7 +10168,8 @@ function noteDoc({
10147
10168
  sensitive = false,
10148
10169
  updated = null,
10149
10170
  container = null,
10150
- also = []
10171
+ also = [],
10172
+ quote = null
10151
10173
  }) {
10152
10174
  const id = (container || urls.notes) + slug;
10153
10175
  const who = mentions.map((m) => m.actor);
@@ -10155,7 +10177,7 @@ function noteDoc({
10155
10177
  const addressed = addressing(urls, visibility, [...who, ...extra]);
10156
10178
  const hashtags = hashtagsIn(content).map((name) => ({ name, href: tagHref(urls, name) }));
10157
10179
  const note = {
10158
- "@context": AS_CTX,
10180
+ "@context": [AS_CTX, { ...POLICY_CTX, ...quote?.id ? QUOTE_CTX : {} }],
10159
10181
  id,
10160
10182
  type: "Note",
10161
10183
  attributedTo: urls.actor,
@@ -10163,8 +10185,15 @@ function noteDoc({
10163
10185
  published,
10164
10186
  to: addressed.to,
10165
10187
  cc: addressed.cc,
10166
- replies: repliesId(id)
10188
+ replies: repliesId(id),
10189
+ interactionPolicy: quotePolicy(visibility)
10167
10190
  };
10191
+ if (quote?.id) {
10192
+ note.quote = quote.id;
10193
+ note.quoteUri = quote.id;
10194
+ note._misskey_quote = quote.id;
10195
+ if (quote.authorization) note.quoteAuthorization = quote.authorization;
10196
+ }
10168
10197
  if (summary) note.summary = summary;
10169
10198
  if (summary || sensitive) note.sensitive = true;
10170
10199
  if (updated) note.updated = updated;
@@ -10206,7 +10235,7 @@ function questionDoc({
10206
10235
  if (endTime) question.endTime = endTime;
10207
10236
  if (closed) question.closed = closed;
10208
10237
  if (votersCount !== null && votersCount !== void 0) {
10209
- question["@context"] = [AS_CTX, {
10238
+ question["@context"] = [...[].concat(note["@context"]), {
10210
10239
  toot: "http://joinmastodon.org/ns#",
10211
10240
  votersCount: {
10212
10241
  "@id": "toot:votersCount",
@@ -10301,10 +10330,10 @@ function updateActorActivity({ urls, actor, serial, published = (/* @__PURE__ */
10301
10330
  object
10302
10331
  };
10303
10332
  }
10304
- function updateActivity(note, urls) {
10333
+ function updateActivity(note, urls, { serial = null } = {}) {
10305
10334
  return {
10306
10335
  "@context": AS_CTX,
10307
- id: note.id + "#update-" + String(note.updated || "").replace(/[^0-9TZ]/g, ""),
10336
+ id: note.id + "#update-" + (note.updated ? String(note.updated).replace(/[^0-9TZ]/g, "") : "q" + (serial ?? Date.now())),
10308
10337
  type: "Update",
10309
10338
  actor: urls.actor,
10310
10339
  to: note.to,
@@ -10350,13 +10379,15 @@ function addRemoveActivity({ urls, type, object, target, serial }) {
10350
10379
  target
10351
10380
  };
10352
10381
  }
10353
- var import_sanitize_html, AS_CTX, SEC_CTX, PUBLIC, DEFAULT_ROOT, assertionKeyId, OUTBOX_PAGE_SIZE, outboxPageId, outboxPageCount, outboxItemId, outboxWireItem, outboxLocalItem, FOLLOWERS_PAGE_SIZE, followersPageId, followersPageCount, ALLOWED_TAGS, ALLOWED_ATTRS, MAX_ATTACHMENTS, MAX_ATTACHMENT_URL, attachmentUrl, HTML_ESCAPES, MENTION_RE, HASHTAG_RE;
10382
+ var import_sanitize_html, AS_CTX, LEMMY_CTX, SEC_CTX, PUBLIC, DEFAULT_ROOT, assertionKeyId, OUTBOX_PAGE_SIZE, outboxPageId, outboxPageCount, outboxItemId, outboxWireItem, outboxLocalItem, FOLLOWERS_PAGE_SIZE, followersPageId, followersPageCount, ALLOWED_TAGS, ALLOWED_ATTRS, MAX_ATTACHMENTS, MAX_ATTACHMENT_URL, attachmentUrl, HTML_ESCAPES2, MENTION_RE, HASHTAG_RE, QUOTE_CTX, POLICY_CTX;
10354
10383
  var init_wire = __esm({
10355
10384
  "lib/core/wire.mjs"() {
10356
10385
  init_urls();
10357
10386
  init_urls();
10387
+ init_profile_page();
10358
10388
  import_sanitize_html = __toESM(require_sanitize_html(), 1);
10359
10389
  AS_CTX = "https://www.w3.org/ns/activitystreams";
10390
+ LEMMY_CTX = "https://join-lemmy.org/context.json";
10360
10391
  SEC_CTX = "https://w3id.org/security/v1";
10361
10392
  PUBLIC = "https://www.w3.org/ns/activitystreams#Public";
10362
10393
  DEFAULT_ROOT = "fedipod/";
@@ -10405,9 +10436,22 @@ var init_wire = __esm({
10405
10436
  return null;
10406
10437
  }
10407
10438
  };
10408
- HTML_ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;" };
10439
+ HTML_ESCAPES2 = { "&": "&amp;", "<": "&lt;", ">": "&gt;" };
10409
10440
  MENTION_RE = /@([A-Za-z0-9_.-]+)@([A-Za-z0-9.-]+\.[A-Za-z]{2,}(?::\d+)?)/g;
10410
10441
  HASHTAG_RE = /(^|[^\/\w)])#([\p{L}\p{N}_]*[\p{L}_][\p{L}\p{N}_]*)/gu;
10442
+ QUOTE_CTX = {
10443
+ quote: { "@id": "https://w3id.org/fep/044f#quote", "@type": "@id" },
10444
+ quoteUri: { "@id": "http://fedibird.com/ns#quoteUri", "@type": "@id" },
10445
+ _misskey_quote: { "@id": "https://misskey-hub.net/ns#_misskey_quote", "@type": "@id" },
10446
+ quoteAuthorization: { "@id": "https://w3id.org/fep/044f#quoteAuthorization", "@type": "@id" }
10447
+ };
10448
+ POLICY_CTX = {
10449
+ gts: "https://gotosocial.org/ns#",
10450
+ interactionPolicy: { "@id": "gts:interactionPolicy", "@type": "@id" },
10451
+ canQuote: { "@id": "gts:canQuote", "@type": "@id" },
10452
+ automaticApproval: { "@id": "gts:automaticApproval", "@type": "@id" },
10453
+ manualApproval: { "@id": "gts:manualApproval", "@type": "@id" }
10454
+ };
10411
10455
  }
10412
10456
  });
10413
10457
 
@@ -23600,7 +23644,7 @@ var require_constants = __commonJS({
23600
23644
  "node_modules/jsonld/lib/constants.js"(exports, module2) {
23601
23645
  "use strict";
23602
23646
  var RDF5 = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
23603
- var XSD2 = "http://www.w3.org/2001/XMLSchema#";
23647
+ var XSD3 = "http://www.w3.org/2001/XMLSchema#";
23604
23648
  module2.exports = {
23605
23649
  // TODO: Deprecated and will be removed later. Use LINK_HEADER_CONTEXT.
23606
23650
  LINK_HEADER_REL: "http://www.w3.org/ns/json-ld#context",
@@ -23616,11 +23660,11 @@ var require_constants = __commonJS({
23616
23660
  RDF_JSON_LITERAL: RDF5 + "JSON",
23617
23661
  RDF_OBJECT: RDF5 + "object",
23618
23662
  RDF_LANGSTRING: RDF5 + "langString",
23619
- XSD: XSD2,
23620
- XSD_BOOLEAN: XSD2 + "boolean",
23621
- XSD_DOUBLE: XSD2 + "double",
23622
- XSD_INTEGER: XSD2 + "integer",
23623
- XSD_STRING: XSD2 + "string"
23663
+ XSD: XSD3,
23664
+ XSD_BOOLEAN: XSD3 + "boolean",
23665
+ XSD_DOUBLE: XSD3 + "double",
23666
+ XSD_INTEGER: XSD3 + "integer",
23667
+ XSD_STRING: XSD3 + "string"
23624
23668
  };
23625
23669
  }
23626
23670
  });
@@ -32650,6 +32694,73 @@ var init_safefetch = __esm({
32650
32694
  }
32651
32695
  });
32652
32696
 
32697
+ // lib/core/wire-quotes.mjs
32698
+ var wire_quotes_exports = {};
32699
+ __export(wire_quotes_exports, {
32700
+ QUOTE_TYPES_CTX: () => QUOTE_TYPES_CTX,
32701
+ quoteAnswerActivity: () => quoteAnswerActivity,
32702
+ quoteAuthorizationDoc: () => quoteAuthorizationDoc,
32703
+ quoteAuthorizationId: () => quoteAuthorizationId,
32704
+ quoteRequestActivity: () => quoteRequestActivity
32705
+ });
32706
+ function quoteRequestActivity({ urls, note, quoted, quotedActor, serial }) {
32707
+ const { "@context": ctx, ...instrument } = note;
32708
+ return {
32709
+ "@context": [AS_CTX, { ...QUOTE_CTX, ...POLICY_CTX, QuoteRequest: QUOTE_TYPES_CTX.QuoteRequest }],
32710
+ id: urls.actor + "#quote-request-" + serial,
32711
+ type: "QuoteRequest",
32712
+ actor: urls.actor,
32713
+ to: [quotedActor],
32714
+ object: quoted,
32715
+ instrument
32716
+ };
32717
+ }
32718
+ function quoteAuthorizationId(noteId, instrumentId) {
32719
+ return noteId + "-quote-" + node_crypto_default.createHash("sha256").update(String(instrumentId)).digest("hex").slice(0, 16);
32720
+ }
32721
+ function quoteAuthorizationDoc({ urls, id, instrument, target }) {
32722
+ return {
32723
+ "@context": [AS_CTX, QUOTE_TYPES_CTX],
32724
+ id,
32725
+ type: "QuoteAuthorization",
32726
+ attributedTo: urls.actor,
32727
+ interactingObject: instrument,
32728
+ interactionTarget: target
32729
+ };
32730
+ }
32731
+ function quoteAnswerActivity({ urls, type, request, result = null, to, serial }) {
32732
+ const idOf3 = (v) => typeof v === "string" ? v : v?.id;
32733
+ return {
32734
+ "@context": [AS_CTX, { QuoteRequest: QUOTE_TYPES_CTX.QuoteRequest }],
32735
+ id: urls.actor + "#" + type.toLowerCase() + "-" + serial,
32736
+ type,
32737
+ actor: urls.actor,
32738
+ to: [to],
32739
+ object: {
32740
+ id: idOf3(request),
32741
+ type: "QuoteRequest",
32742
+ actor: idOf3(request?.actor),
32743
+ object: idOf3(request?.object),
32744
+ instrument: idOf3(request?.instrument)
32745
+ },
32746
+ ...result ? { result } : {}
32747
+ };
32748
+ }
32749
+ var QUOTE_TYPES_CTX;
32750
+ var init_wire_quotes = __esm({
32751
+ "lib/core/wire-quotes.mjs"() {
32752
+ init_node_crypto();
32753
+ init_wire();
32754
+ QUOTE_TYPES_CTX = {
32755
+ QuoteRequest: "https://w3id.org/fep/044f#QuoteRequest",
32756
+ QuoteAuthorization: "https://w3id.org/fep/044f#QuoteAuthorization",
32757
+ gts: "https://gotosocial.org/ns#",
32758
+ interactingObject: { "@id": "gts:interactingObject", "@type": "@id" },
32759
+ interactionTarget: { "@id": "gts:interactionTarget", "@type": "@id" }
32760
+ };
32761
+ }
32762
+ });
32763
+
32653
32764
  // web/app/shims/fedify-sig.mjs
32654
32765
  async function sign({ url, method = "POST", headers = {}, body }, privateKey, keyId) {
32655
32766
  const u = new URL(url);
@@ -34096,6 +34207,61 @@ var require_src = __commonJS({
34096
34207
  }
34097
34208
  });
34098
34209
 
34210
+ // web/app/idb-kv.mjs
34211
+ var DB = "fedipod-accounts";
34212
+ function open() {
34213
+ return new Promise((res, rej) => {
34214
+ const r = indexedDB.open(DB, 1);
34215
+ r.onupgradeneeded = () => {
34216
+ const db = r.result;
34217
+ if (!db.objectStoreNames.contains("kv")) db.createObjectStore("kv");
34218
+ };
34219
+ r.onsuccess = () => res(r.result);
34220
+ r.onerror = () => rej(r.error);
34221
+ });
34222
+ }
34223
+ async function kvGet(key) {
34224
+ const db = await open();
34225
+ return new Promise((res, rej) => {
34226
+ const rq = db.transaction("kv", "readonly").objectStore("kv").get(key);
34227
+ rq.onsuccess = () => res(rq.result ?? null);
34228
+ rq.onerror = () => rej(rq.error);
34229
+ });
34230
+ }
34231
+ async function kvAll() {
34232
+ const db = await open();
34233
+ return new Promise((res, rej) => {
34234
+ const out = {};
34235
+ const cur = db.transaction("kv", "readonly").objectStore("kv").openCursor();
34236
+ cur.onsuccess = () => {
34237
+ const c = cur.result;
34238
+ if (c) {
34239
+ out[c.key] = c.value;
34240
+ c.continue();
34241
+ } else res(out);
34242
+ };
34243
+ cur.onerror = () => rej(cur.error);
34244
+ });
34245
+ }
34246
+ async function kvPut(key, val) {
34247
+ const db = await open();
34248
+ return new Promise((res, rej) => {
34249
+ const tx = db.transaction("kv", "readwrite");
34250
+ tx.objectStore("kv").put(val, key);
34251
+ tx.oncomplete = () => res();
34252
+ tx.onerror = () => rej(tx.error);
34253
+ });
34254
+ }
34255
+ async function kvDel(key) {
34256
+ const db = await open();
34257
+ return new Promise((res, rej) => {
34258
+ const tx = db.transaction("kv", "readwrite");
34259
+ tx.objectStore("kv").delete(key);
34260
+ tx.oncomplete = () => res();
34261
+ tx.onerror = () => rej(tx.error);
34262
+ });
34263
+ }
34264
+
34099
34265
  // web/app/agent.mjs
34100
34266
  init_wire();
34101
34267
 
@@ -34159,8 +34325,8 @@ async function probePrivateEnforcement(probe, podKeepUrl) {
34159
34325
  }
34160
34326
 
34161
34327
  // lib/pod/state.mjs
34162
- var readWrappedKeys = (pod, urls) => pod.getJson(urls.state + "keys.json");
34163
- var writeWrappedKeys = (pod, urls, envelope) => pod.putJson(urls.state + "keys.json", envelope, "application/json");
34328
+ var readKeys = (pod, urls) => pod.getJson(urls.state + "keys.json");
34329
+ var writeKeys = (pod, urls, keys) => pod.putJson(urls.state + "keys.json", keys, "application/json");
34164
34330
 
34165
34331
  // lib/core/store.mjs
34166
34332
  init_node_crypto();
@@ -45482,7 +45648,8 @@ var ALL_COLLECTIONS = {
45482
45648
  outbox: true,
45483
45649
  acls: true,
45484
45650
  pending: true,
45485
- blocked: true
45651
+ blocked: true,
45652
+ featured: true
45486
45653
  };
45487
45654
  async function publishOutbox(publisher, outbox, { acls = false, force = false } = {}) {
45488
45655
  const { urls } = publisher;
@@ -45581,6 +45748,7 @@ async function publishCollections(publisher, which = ALL_COLLECTIONS) {
45581
45748
  }
45582
45749
  if (which.pending) await publisher.publishPending();
45583
45750
  if (which.blocked) await publisher.publishBlocked();
45751
+ if (which.featured) await publisher.publishFeatured();
45584
45752
  if (which.outbox) {
45585
45753
  const outbox = publisher.store.read("outbox.json", []);
45586
45754
  const known2 = publisher.store.read("published.json", {}).outboxIndex;
@@ -55704,6 +55872,88 @@ var anno_default = {
55704
55872
  }
55705
55873
  };
55706
55874
 
55875
+ // lib/core/contexts/litepub-0.1.json
55876
+ var litepub_0_1_default = {
55877
+ "@context": [
55878
+ "https://www.w3.org/ns/activitystreams",
55879
+ "https://w3id.org/security/v1",
55880
+ {
55881
+ Emoji: "toot:Emoji",
55882
+ Hashtag: "as:Hashtag",
55883
+ PropertyValue: "schema:PropertyValue",
55884
+ atomUri: "ostatus:atomUri",
55885
+ conversation: {
55886
+ "@id": "ostatus:conversation",
55887
+ "@type": "@id"
55888
+ },
55889
+ discoverable: "toot:discoverable",
55890
+ manuallyApprovesFollowers: "as:manuallyApprovesFollowers",
55891
+ capabilities: "litepub:capabilities",
55892
+ ostatus: "http://ostatus.org#",
55893
+ schema: "http://schema.org#",
55894
+ toot: "http://joinmastodon.org/ns#",
55895
+ misskey: "https://misskey-hub.net/ns#",
55896
+ fedibird: "http://fedibird.com/ns#",
55897
+ sharkey: "https://joinsharkey.org/ns#",
55898
+ value: "schema:value",
55899
+ sensitive: "as:sensitive",
55900
+ litepub: "http://litepub.social/ns#",
55901
+ invisible: "litepub:invisible",
55902
+ directMessage: "litepub:directMessage",
55903
+ listMessage: {
55904
+ "@id": "litepub:listMessage",
55905
+ "@type": "@id"
55906
+ },
55907
+ quoteUrl: "as:quoteUrl",
55908
+ quoteUri: "fedibird:quoteUri",
55909
+ oauthRegistrationEndpoint: {
55910
+ "@id": "litepub:oauthRegistrationEndpoint",
55911
+ "@type": "@id"
55912
+ },
55913
+ EmojiReact: "litepub:EmojiReact",
55914
+ ChatMessage: "litepub:ChatMessage",
55915
+ alsoKnownAs: {
55916
+ "@id": "as:alsoKnownAs",
55917
+ "@type": "@id"
55918
+ },
55919
+ vcard: "http://www.w3.org/2006/vcard/ns#",
55920
+ formerRepresentations: "litepub:formerRepresentations",
55921
+ contentMap: {
55922
+ "@id": "as:content",
55923
+ "@container": "@language"
55924
+ },
55925
+ featured: {
55926
+ "@id": "toot:featured",
55927
+ "@type": "@id"
55928
+ },
55929
+ backgroundUrl: {
55930
+ "@id": "sharkey:backgroundUrl",
55931
+ "@type": "@id"
55932
+ }
55933
+ }
55934
+ ]
55935
+ };
55936
+
55937
+ // lib/core/contexts/quotes.json
55938
+ var quotes_default = {
55939
+ "@context": {
55940
+ quote: {
55941
+ "@id": "https://w3id.org/fep/044f#quote",
55942
+ "@type": "@id"
55943
+ },
55944
+ quoteAuthorization: {
55945
+ "@id": "https://w3id.org/fep/044f#quoteAuthorization",
55946
+ "@type": "@id"
55947
+ },
55948
+ QuoteRequest: "https://w3id.org/fep/044f#QuoteRequest",
55949
+ QuoteAuthorization: "https://w3id.org/fep/044f#QuoteAuthorization",
55950
+ _misskey_quote: {
55951
+ "@id": "https://misskey-hub.net/ns#_misskey_quote",
55952
+ "@type": "@id"
55953
+ }
55954
+ }
55955
+ };
55956
+
55707
55957
  // lib/core/contexts/index.mjs
55708
55958
  var CONTEXTS = {
55709
55959
  "https://www.w3.org/ns/activitystreams": activitystreams_default,
@@ -55720,7 +55970,9 @@ var CONTEXTS = {
55720
55970
  "https://join-lemmy.org/context.json": join_lemmy_default,
55721
55971
  "http://joinmastodon.org/ns": joinmastodon_default,
55722
55972
  "https://purl.archive.org/miscellany": miscellany_default,
55723
- "http://www.w3.org/ns/anno.jsonld": anno_default
55973
+ "http://www.w3.org/ns/anno.jsonld": anno_default,
55974
+ "http://litepub.social/ns": litepub_0_1_default,
55975
+ "https://w3id.org/fep/044f": quotes_default
55724
55976
  };
55725
55977
 
55726
55978
  // lib/core/graphview.mjs
@@ -55757,8 +56009,10 @@ function buildTerms() {
55757
56009
  const terms = /* @__PURE__ */ new Map();
55758
56010
  const order = [AS_CTX, ...Object.keys(CONTEXTS).filter((u) => u !== AS_CTX)];
55759
56011
  for (const url of order) {
55760
- const ctx = CONTEXTS[url]?.["@context"];
55761
- if (!ctx || typeof ctx !== "object") continue;
56012
+ const body = CONTEXTS[url]?.["@context"];
56013
+ const maps = (Array.isArray(body) ? body : [body]).filter((c) => c && typeof c === "object");
56014
+ if (!maps.length) continue;
56015
+ const ctx = Object.assign({}, ...maps);
55762
56016
  const prefixes = {};
55763
56017
  for (const [k, v] of Object.entries(ctx)) {
55764
56018
  if (typeof v === "string" && /^https?:\/\//.test(v)) prefixes[k] = v;
@@ -55843,8 +56097,40 @@ function isListHead(term3, ctx) {
55843
56097
  if (term3.termType !== "BlankNode" && term3.termType !== "NamedNode") return false;
55844
56098
  return !!ctx.bySubject.get(term3.value)?.has(RDF_FIRST);
55845
56099
  }
56100
+ var XSD2 = "http://www.w3.org/2001/XMLSchema#";
56101
+ var NUMERIC = new Set([
56102
+ "integer",
56103
+ "decimal",
56104
+ "double",
56105
+ "float",
56106
+ "long",
56107
+ "int",
56108
+ "short",
56109
+ "byte",
56110
+ "nonNegativeInteger",
56111
+ "positiveInteger",
56112
+ "nonPositiveInteger",
56113
+ "negativeInteger",
56114
+ "unsignedLong",
56115
+ "unsignedInt",
56116
+ "unsignedShort",
56117
+ "unsignedByte"
56118
+ ].map((t) => XSD2 + t));
56119
+ function fromLiteral(term3) {
56120
+ const type = term3.datatype?.value || "";
56121
+ if (type === `${XSD2}boolean`) {
56122
+ if (term3.value === "true" || term3.value === "1") return true;
56123
+ if (term3.value === "false" || term3.value === "0") return false;
56124
+ return term3.value;
56125
+ }
56126
+ if (NUMERIC.has(type)) {
56127
+ const n = Number(term3.value);
56128
+ return Number.isFinite(n) ? n : term3.value;
56129
+ }
56130
+ return term3.value;
56131
+ }
55846
56132
  function toValue(term3, ctx, depth, path) {
55847
- if (term3.termType === "Literal") return term3.value;
56133
+ if (term3.termType === "Literal") return fromLiteral(term3);
55848
56134
  if (isListHead(term3, ctx)) return readList(term3.value, ctx, depth, path);
55849
56135
  const preds = ctx.bySubject.get(term3.value);
55850
56136
  if (preds && preds.size) return makeView(term3.value, ctx, depth + 1, path);
@@ -55912,8 +56198,10 @@ var AS2Error = class extends Error {
55912
56198
  this.context = context;
55913
56199
  }
55914
56200
  };
56201
+ var LITEPUB = "http://litepub.social/ns";
56202
+ var heldKey = (url) => typeof url === "string" && /\/schemas\/litepub-0\.1\.jsonld$/u.test(url) ? LITEPUB : url;
55915
56203
  function contextLoader(url) {
55916
- const doc = CONTEXTS[url];
56204
+ const doc = CONTEXTS[heldKey(url)];
55917
56205
  if (!doc) {
55918
56206
  return Promise.reject(new AS2Error(`context not held here: ${url}`, { context: url }));
55919
56207
  }
@@ -55948,7 +56236,7 @@ async function parseAS2(raw) {
55948
56236
  }
55949
56237
  function groundContext(input) {
55950
56238
  const ctx = input["@context"];
55951
- const held = (c) => typeof c === "string" ? Object.hasOwn(CONTEXTS, c) : c && typeof c === "object";
56239
+ const held = (c) => typeof c === "string" ? Object.hasOwn(CONTEXTS, heldKey(c)) : c && typeof c === "object";
55952
56240
  let kept;
55953
56241
  if (ctx === void 0 || ctx === null) kept = [];
55954
56242
  else if (Array.isArray(ctx)) kept = ctx.filter(held);
@@ -55956,6 +56244,17 @@ function groundContext(input) {
55956
56244
  if (!kept.some((c) => c === AS_CTX)) kept.unshift(AS_CTX);
55957
56245
  return { ...input, "@context": kept.length === 1 ? kept[0] : kept };
55958
56246
  }
56247
+ function carryRemovals(input, view) {
56248
+ if (!input || typeof input !== "object" || !view || typeof view !== "object") return view;
56249
+ for (const [key, value] of Object.entries(input)) {
56250
+ if (value === null) {
56251
+ if (!(key in view)) view[key] = null;
56252
+ } else if (key === "object" && typeof value === "object" && !Array.isArray(value) && view.object && typeof view.object === "object" && !Array.isArray(view.object)) {
56253
+ carryRemovals(value, view.object);
56254
+ }
56255
+ }
56256
+ return view;
56257
+ }
55959
56258
  async function readLenient(raw) {
55960
56259
  let input = null;
55961
56260
  try {
@@ -55966,12 +56265,12 @@ async function readLenient(raw) {
55966
56265
  const root = typeof own === "string" && /^https?:\/\//u.test(own) ? own : null;
55967
56266
  try {
55968
56267
  const { doc, graph: graph2 } = await parseAS2(raw);
55969
- return { doc, graph: graph2, view: graphView(graph2, { root }), degraded: null };
56268
+ return { doc, graph: graph2, view: carryRemovals(input, graphView(graph2, { root })), degraded: null };
55970
56269
  } catch (e) {
55971
56270
  if (!input || typeof input !== "object") return { doc: null, graph: null, view: null, degraded: e.message };
55972
56271
  try {
55973
56272
  const { graph: graph2 } = await parseAS2(groundContext(input));
55974
- return { doc: input, graph: graph2, view: graphView(graph2, { root }), degraded: e.message };
56273
+ return { doc: input, graph: graph2, view: carryRemovals(input, graphView(graph2, { root })), degraded: e.message };
55975
56274
  } catch (inner) {
55976
56275
  return { doc: input, graph: null, view: null, degraded: `${e.message}; grounded read also failed: ${inner.message}` };
55977
56276
  }
@@ -55981,6 +56280,7 @@ async function readLenient(raw) {
55981
56280
  // lib/core/publisher/notes.mjs
55982
56281
  init_node_crypto();
55983
56282
  init_wire();
56283
+ init_wire_quotes();
55984
56284
  async function ensureMediaContainer(publisher) {
55985
56285
  if (publisher._mediaReady) return;
55986
56286
  if (await exists(publisher.remote, publisher.urls.media)) {
@@ -56084,7 +56384,7 @@ function rowContent(obj) {
56084
56384
  const esc = (v) => String(v).replace(/[&<>"]/gu, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
56085
56385
  return `<p><a href="${esc(obj?.id || "")}">${esc(obj?.type || "object")}</a></p>`;
56086
56386
  }
56087
- async function publishNote(publisher, content, { inReplyTo, attachments, visibility = "public", spoilerText = null, sensitive = false, slug: wanted = null, also = [], deliverTo = [] } = {}) {
56387
+ async function publishNote(publisher, content, { inReplyTo, attachments, visibility = "public", spoilerText = null, sensitive = false, slug: wanted = null, also = [], deliverTo = [], quote = null } = {}) {
56088
56388
  const { urls } = publisher;
56089
56389
  const priv = visibility === "private" || visibility === "direct";
56090
56390
  if (priv) {
@@ -56096,6 +56396,12 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56096
56396
  const mentions = await publisher._mentionsFor(content, inReplyTo);
56097
56397
  if (visibility === "direct") assertDirectAddressed(content, mentions);
56098
56398
  const reply = await replyTarget(publisher, inReplyTo, mentions, visibility);
56399
+ const selfQuote = quote?.id && quote.actor === urls.actor;
56400
+ const quoted = quote?.id ? {
56401
+ id: quote.id,
56402
+ // Our own post: the authorization is minted here, before the note goes out.
56403
+ authorization: selfQuote ? await grantQuote(publisher, quote.id, (priv ? urls.privateNotes : urls.notes) + slug) : null
56404
+ } : null;
56099
56405
  const note = noteDoc({
56100
56406
  urls,
56101
56407
  slug,
@@ -56108,9 +56414,10 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56108
56414
  summary: spoilerText,
56109
56415
  sensitive,
56110
56416
  container: priv ? urls.privateNotes : urls.notes,
56111
- also: [...also, ...reply ? [reply.actor] : []]
56417
+ also: [...also, ...reply ? [reply.actor] : [], ...quote?.actor && !selfQuote ? [quote.actor] : []],
56418
+ quote: quoted
56112
56419
  });
56113
- const addressedInboxes = await inboxesFor(publisher, [...also, ...deliverTo]);
56420
+ const addressedInboxes = await inboxesFor(publisher, [...also, ...deliverTo, ...quote?.actor && !selfQuote ? [quote.actor] : []]);
56114
56421
  await write4(publisher.remote, note.id, note);
56115
56422
  await writeEmptyReplies(
56116
56423
  publisher.remote,
@@ -56118,6 +56425,7 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56118
56425
  collection(repliesId(note.id), [])
56119
56426
  );
56120
56427
  if (!priv) await publisher.recordOutbox(note.id);
56428
+ const request = quote?.id && !selfQuote ? quoteRequestActivity({ urls, note, quoted: quote.id, quotedActor: quote.actor, serial: Date.now() }) : null;
56121
56429
  publisher.store.addStatus({
56122
56430
  noteId: note.id,
56123
56431
  actor: urls.actor,
@@ -56131,7 +56439,13 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56131
56439
  ...spoilerText ? { spoiler: spoilerText } : {},
56132
56440
  ...note.sensitive ? { sensitive: true } : {},
56133
56441
  ...attachments?.length ? { attachments } : {},
56134
- ...rowTags(note)
56442
+ ...rowTags(note),
56443
+ ...quote?.id ? {
56444
+ quote: quote.id,
56445
+ quoteState: selfQuote ? "accepted" : "pending",
56446
+ ...quoted.authorization ? { quoteAuthorization: quoted.authorization } : {},
56447
+ ...request ? { quoteRequest: { id: request.id, actor: quote.actor } } : {}
56448
+ } : {}
56135
56449
  });
56136
56450
  if (await publisher.store.commit?.() === false) publisher.log(`post published but its timeline row was refused: ${note.id}`);
56137
56451
  const create = createActivity(note, urls);
@@ -56145,6 +56459,11 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56145
56459
  ].filter(Boolean))];
56146
56460
  await publisher.deliverer.deliverToAll(inboxes, create);
56147
56461
  publisher.log(`note published: ${note.id} \u2192 ${inboxes.length} inbox(es)`);
56462
+ if (request) {
56463
+ const [inbox] = await inboxesFor(publisher, [quote.actor]);
56464
+ if (inbox) await publisher.deliverer.deliver(inbox, request);
56465
+ else publisher.log(`quote of ${quote.id}: no inbox found for ${quote.actor} \u2014 the quote stays pending`);
56466
+ }
56148
56467
  if (visibility === "public" && publisher.atproto?.connected() && publisher.store.getConfig()?.atproto?.crossPost) {
56149
56468
  try {
56150
56469
  const mirror = await publisher.atproto.crossPost(
@@ -56253,7 +56572,8 @@ async function updateNote(publisher, s, { content, spoilerText = null, sensitive
56253
56572
  sensitive: sensitive ?? !!s.sensitive,
56254
56573
  updated,
56255
56574
  container,
56256
- also: reply ? [reply.actor] : []
56575
+ also: reply ? [reply.actor] : [],
56576
+ quote: rowQuote(s)
56257
56577
  });
56258
56578
  await write4(publisher.remote, note.id, note);
56259
56579
  await writeCreate(publisher.remote, createActivityId(note.id), createActivity(note, urls));
@@ -56278,6 +56598,71 @@ async function updateNote(publisher, s, { content, spoilerText = null, sensitive
56278
56598
  publisher.log(`note edited: ${note.id} \u2192 ${inboxes.length} inbox(es)`);
56279
56599
  return patched;
56280
56600
  }
56601
+ function rowQuote(s) {
56602
+ if (!s?.quote || s.quoteState === "rejected") return null;
56603
+ return { id: s.quote, ...s.quoteAuthorization ? { authorization: s.quoteAuthorization } : {} };
56604
+ }
56605
+ async function restateNote(publisher, s, { quote }) {
56606
+ const { urls } = publisher;
56607
+ const content = s.text ?? "";
56608
+ const mentions = await publisher._mentionsFor(content, s.inReplyTo);
56609
+ const container = String(s.noteId).startsWith(urls.privateNotes) ? urls.privateNotes : urls.notes;
56610
+ const slug = s.slug || String(s.noteId).slice(container.length);
56611
+ const reply = await replyTarget(publisher, s.inReplyTo, mentions, s.visibility || "public");
56612
+ const note = noteDoc({
56613
+ urls,
56614
+ slug,
56615
+ content,
56616
+ published: s.published,
56617
+ inReplyTo: s.inReplyTo,
56618
+ attachments: s.attachments || [],
56619
+ mentions,
56620
+ visibility: s.visibility || "public",
56621
+ summary: s.spoiler || null,
56622
+ sensitive: !!s.sensitive,
56623
+ updated: s.editedAt || null,
56624
+ container,
56625
+ also: [...reply ? [reply.actor] : [], ...s.quoteRequest?.actor ? [s.quoteRequest.actor] : []],
56626
+ quote
56627
+ });
56628
+ await write4(publisher.remote, note.id, note);
56629
+ await writeCreate(publisher.remote, createActivityId(note.id), createActivity(note, urls));
56630
+ const update = updateActivity(note, urls, { serial: Date.now() });
56631
+ const contacts = publisher.store.getContacts();
56632
+ const inboxes = [...new Set([
56633
+ ...s.visibility === "direct" ? [] : contacts.followers.map((f) => f.sharedInbox || f.inbox),
56634
+ ...mentions.map((m) => m.inbox),
56635
+ reply?.inbox,
56636
+ ...await inboxesFor(publisher, s.quoteRequest?.actor ? [s.quoteRequest.actor] : [])
56637
+ ].filter(Boolean))];
56638
+ await publisher.deliverer.deliverToAll(inboxes, update);
56639
+ return inboxes.length;
56640
+ }
56641
+ async function authorizeQuote(publisher, s, authorizationId) {
56642
+ const patched = publisher.store.updateStatus(s.noteId, { quoteState: "accepted", quoteAuthorization: authorizationId });
56643
+ const n = await restateNote(publisher, patched || s, { quote: { id: s.quote, authorization: authorizationId } });
56644
+ publisher.log(`quote allowed by ${s.quoteRequest?.actor || "the author"}: ${s.noteId} \u2192 ${n} inbox(es)`);
56645
+ return patched;
56646
+ }
56647
+ async function withdrawQuote(publisher, s) {
56648
+ const patched = publisher.store.updateStatus(s.noteId, { quoteState: "rejected", quoteAuthorization: void 0 });
56649
+ const n = await restateNote(publisher, patched || s, { quote: null });
56650
+ publisher.log(`quote refused by ${s.quoteRequest?.actor || "the author"}: ${s.noteId} \u2192 ${n} inbox(es)`);
56651
+ return patched;
56652
+ }
56653
+ var MAX_QUOTED_BY = 200;
56654
+ async function grantQuote(publisher, noteId, instrumentId) {
56655
+ const { urls } = publisher;
56656
+ const id = quoteAuthorizationId(noteId, instrumentId);
56657
+ await write4(publisher.remote, id, quoteAuthorizationDoc({ urls, id, instrument: instrumentId, target: noteId }));
56658
+ const s = publisher.store.getStatuses().find((x) => x.noteId === noteId);
56659
+ if (s) {
56660
+ const quotedBy = (s.quotedBy || []).filter((q) => q.note !== instrumentId);
56661
+ quotedBy.unshift({ note: instrumentId, authorization: id });
56662
+ publisher.store.updateStatus(noteId, { quotedBy: quotedBy.slice(0, MAX_QUOTED_BY) });
56663
+ }
56664
+ return id;
56665
+ }
56281
56666
 
56282
56667
  // lib/core/publisher/restore.mjs
56283
56668
  var ACCEPT_AP = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
@@ -56331,11 +56716,11 @@ async function reconcileOutbox(publisher, outbox) {
56331
56716
  return 0;
56332
56717
  }
56333
56718
  if (!Array.isArray(published) || !published.length) return 0;
56334
- const idOf2 = (i) => typeof i === "string" ? i : i?.id || null;
56335
- const known2 = new Set(outbox.map(idOf2).filter(Boolean));
56719
+ const idOf3 = (i) => typeof i === "string" ? i : i?.id || null;
56720
+ const known2 = new Set(outbox.map(idOf3).filter(Boolean));
56336
56721
  const removed = new Set(publisher.store.read("outbox-removed.json", []).map((r) => r.id));
56337
56722
  const missing = published.filter((i) => {
56338
- const id = idOf2(i);
56723
+ const id = idOf3(i);
56339
56724
  return id && !known2.has(id) && !removed.has(id);
56340
56725
  });
56341
56726
  if (!missing.length) return 0;
@@ -56774,7 +57159,16 @@ var Publisher = class {
56774
57159
  // Every actor document this identity publishes, from one place: the
56775
57160
  // profile publish and a Move both write it, and a Move that rebuilt it
56776
57161
  // by hand dropped the gateway inbox, the outbox door and the collections.
56777
- actorDocFor({ priv = false, moderators = null, inbox = null, movedTo = this.config.movedTo || null } = {}) {
57162
+ // `clientOrigin` is overridden only by the agent serving a client its own
57163
+ // local actor: standalone, the published document cannot name a loopback
57164
+ // address, but a client already talking to that address can be told it.
57165
+ actorDocFor({
57166
+ priv = false,
57167
+ moderators = null,
57168
+ inbox = null,
57169
+ movedTo = this.config.movedTo || null,
57170
+ clientOrigin = this.clientOrigin
57171
+ } = {}) {
56778
57172
  const { urls } = this;
56779
57173
  return actorDoc({
56780
57174
  urls,
@@ -56791,6 +57185,9 @@ var Publisher = class {
56791
57185
  fields: this.config.fields || [],
56792
57186
  webId: this.remote.webId || null,
56793
57187
  aliases: this.config.aliases || [],
57188
+ // A group says whether only its moderators may open posts (Lemmy's
57189
+ // term); a person's actor carries nothing of the kind.
57190
+ postingRestrictedToMods: this.config.kind === "group" ? !!this.config.postingRestrictedToMods : null,
56794
57191
  moderators,
56795
57192
  pendingFollowers: priv ? urls.pendingFollowers : null,
56796
57193
  pendingFollowing: priv ? urls.pendingFollowing : null,
@@ -56799,11 +57196,11 @@ var Publisher = class {
56799
57196
  // The agent's own outbox endpoint, where it is reachable: a client
56800
57197
  // following the actor must arrive somewhere that will take a write.
56801
57198
  // Otherwise the Gateway's outbox door, when one is attached.
56802
- outbox: this.clientOrigin ? `${this.clientOrigin}ap/outbox` : this.gatewayOutbox(),
57199
+ outbox: clientOrigin ? `${clientOrigin}ap/outbox` : this.gatewayOutbox(),
56803
57200
  // How a client-to-server client finds the way in with nothing configured
56804
57201
  // by hand. Advertised only where the surface is publicly reachable.
56805
- oauthAuthorize: this.clientOrigin ? `${this.clientOrigin}oauth/authorize` : null,
56806
- oauthToken: this.clientOrigin ? `${this.clientOrigin}oauth/token` : null
57202
+ oauthAuthorize: clientOrigin ? `${clientOrigin}oauth/authorize` : null,
57203
+ oauthToken: clientOrigin ? `${clientOrigin}oauth/token` : null
56807
57204
  });
56808
57205
  }
56809
57206
  // The human half: a page a browser can open and follow from. The actor
@@ -56847,7 +57244,7 @@ var Publisher = class {
56847
57244
  const moderators = (this.config.moderators || []).length ? urls.moderators : null;
56848
57245
  const gw = this.config.gateway;
56849
57246
  const gwActive = gw && gw.url && gw.mode && gw.mode !== "off";
56850
- const actorDoc2 = this.actorDocFor({ priv, moderators, inbox: gwActive ? gw.url : null });
57247
+ const actorDoc2 = this.actorDocFor({ priv, moderators, inbox: gwActive ? gw.url : this.config.inboxUrl || null });
56851
57248
  const surface = node_crypto_default.createHash("sha256").update(JSON.stringify({
56852
57249
  actor: actorDoc2,
56853
57250
  handle: this.config.handle,
@@ -56861,18 +57258,20 @@ var Publisher = class {
56861
57258
  this.log("profile unchanged \u2014 nothing republished");
56862
57259
  return { unreachable: [], updated: 0, skipped: true };
56863
57260
  }
56864
- await writeWebfinger(
56865
- this.remote,
56866
- urls,
56867
- jrd({ handle: this.config.handle, host, actor: urls.actor, page: urls.profileHtml })
56868
- );
56869
- await writeHostMeta(this.remote, urls, hostMeta(urls.base));
56870
- const nodeinfoDocUrl = urls.home + "ap/nodeinfo-2.0";
56871
- const localPosts = this.store.getStatuses().filter((s) => s.kind === "post").length;
56872
- await writeNodeinfo(this.remote, urls, {
56873
- pointer: nodeinfoPointer(nodeinfoDocUrl),
56874
- doc: nodeinfoDoc({ version: AGENT_VERSION, localPosts })
56875
- });
57261
+ if (!this.config.forum) {
57262
+ await writeWebfinger(
57263
+ this.remote,
57264
+ urls,
57265
+ jrd({ handle: this.config.handle, host, actor: urls.actor, page: urls.profileHtml })
57266
+ );
57267
+ await writeHostMeta(this.remote, urls, hostMeta(urls.base));
57268
+ const nodeinfoDocUrl = urls.home + "ap/nodeinfo-2.0";
57269
+ const localPosts = this.store.getStatuses().filter((s) => s.kind === "post").length;
57270
+ await writeNodeinfo(this.remote, urls, {
57271
+ pointer: nodeinfoPointer(nodeinfoDocUrl),
57272
+ doc: nodeinfoDoc({ version: AGENT_VERSION, localPosts })
57273
+ });
57274
+ }
56876
57275
  const actor = actorDoc2;
56877
57276
  await write(this.remote, urls, actor);
56878
57277
  if (moderators) {
@@ -56883,7 +57282,8 @@ var Publisher = class {
56883
57282
  );
56884
57283
  }
56885
57284
  if (gwActive) await this.publishGatewayPolicy();
56886
- try {
57285
+ if (this.config.forum) {
57286
+ } else try {
56887
57287
  const wrote = await linkInWebIdProfile(this.remote, {
56888
57288
  actorUrl: urls.actor,
56889
57289
  accountName: `@${this.config.handle}@${host}`,
@@ -56949,12 +57349,13 @@ var Publisher = class {
56949
57349
  async verifyPublicSurface() {
56950
57350
  const { urls } = this;
56951
57351
  const targets = [
56952
- ["webfinger", urls.webfinger],
56953
- ["host-meta", urls.base + ".well-known/host-meta"],
57352
+ // Discovery documents are the pod's, not a forum category's (see publishProfile).
57353
+ ...this.config.forum ? [] : [["webfinger", urls.webfinger], ["host-meta", urls.base + ".well-known/host-meta"]],
56954
57354
  ["actor", urls.actor],
56955
57355
  ["notes", urls.notes],
56956
57356
  ["followers", urls.followers],
56957
57357
  ["following", urls.following],
57358
+ ["featured", urls.featured],
56958
57359
  ["outbox", urls.outbox]
56959
57360
  ];
56960
57361
  const unreachable = [];
@@ -57152,6 +57553,15 @@ var Publisher = class {
57152
57553
  publishNote(...a) {
57153
57554
  return publishNote(this, ...a);
57154
57555
  }
57556
+ authorizeQuote(...a) {
57557
+ return authorizeQuote(this, ...a);
57558
+ }
57559
+ withdrawQuote(...a) {
57560
+ return withdrawQuote(this, ...a);
57561
+ }
57562
+ grantQuote(...a) {
57563
+ return grantQuote(this, ...a);
57564
+ }
57155
57565
  publishObject(...a) {
57156
57566
  return publishObject(this, ...a);
57157
57567
  }
@@ -57237,15 +57647,25 @@ var MAX_FORWARDS_PER_DRAIN = 20;
57237
57647
  var ACCEPT_AP2 = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
57238
57648
  function trimActivity(a) {
57239
57649
  if (!a || typeof a !== "object") return a ?? null;
57240
- const idOf2 = (v) => typeof v === "string" ? v : v?.id ?? null;
57650
+ const idOf3 = (v) => typeof v === "string" ? v : v?.id ?? null;
57241
57651
  const out = {};
57242
57652
  for (const k of ["id", "type", "actor", "target"]) {
57243
- const v = idOf2(a[k]);
57653
+ const v = idOf3(a[k]);
57244
57654
  if (v) out[k] = String(v).slice(0, 2048);
57245
57655
  }
57246
- const obj = idOf2(a.object);
57247
- if (obj) out.object = String(obj).slice(0, 2048);
57248
- else if (a.object && typeof a.object === "object" && a.object.type) {
57656
+ const obj = idOf3(a.object);
57657
+ if (obj) {
57658
+ const asked = a.object && typeof a.object === "object" ? a.object : null;
57659
+ const said = {};
57660
+ if (asked) {
57661
+ if (typeof asked.name === "string") said.name = asked.name.slice(0, 300);
57662
+ if ("closed" in asked) {
57663
+ const v = asked.closed;
57664
+ said.closed = v === "false" ? false : v === "true" ? true : typeof v === "string" ? v.slice(0, 64) : !!v;
57665
+ }
57666
+ }
57667
+ out.object = Object.keys(said).length ? { id: String(obj).slice(0, 2048), ...said } : String(obj).slice(0, 2048);
57668
+ } else if (a.object && typeof a.object === "object" && a.object.type) {
57249
57669
  out.object = { type: String(a.object.type).slice(0, 64) };
57250
57670
  }
57251
57671
  return out;
@@ -57297,6 +57717,40 @@ function authorOf(note, delivered = null) {
57297
57717
  if (!author) return null;
57298
57718
  return sameIdentity(note?.id, author) ? author : null;
57299
57719
  }
57720
+ var MAX_REACTION_CHARS = 64;
57721
+ function reactionOf(activity) {
57722
+ if (!activity || activity.type !== "EmojiReact" && activity.type !== "Like") return null;
57723
+ const content = typeof activity.content === "string" ? activity.content.trim() : "";
57724
+ if (!content) return null;
57725
+ const emoji = content.slice(0, MAX_REACTION_CHARS);
57726
+ const bare = emoji.replace(/^:|:$/g, "");
57727
+ const custom = emojisOf(activity).find((e) => e.shortcode === bare);
57728
+ return { emoji, ...custom ? { url: custom.url } : {} };
57729
+ }
57730
+ var QUOTE_REL = "https://misskey-hub.net/ns#_misskey_quote";
57731
+ function quoteOf(note) {
57732
+ const idOf3 = (v) => typeof v === "string" ? v : v?.id;
57733
+ const named = [note?.quote, note?.quoteUri, note?._misskey_quote, note?.quoteUrl].map((v) => idOf3(Array.isArray(v) ? v[0] : v)).find(Boolean);
57734
+ const linked = [].concat(note?.tag || []).find((t) => t?.type === "Link" && t.href && [].concat(t.rel || []).includes(QUOTE_REL))?.href;
57735
+ const q = named || linked;
57736
+ return q && q !== note?.id ? httpOnly(String(q).slice(0, MAX_URL_CHARS)) : null;
57737
+ }
57738
+ function quotePolicyOf(note, author) {
57739
+ const can = note?.interactionPolicy?.canQuote;
57740
+ if (!can || typeof can !== "object") return null;
57741
+ const word = (v) => {
57742
+ const id = typeof v === "string" ? v : v?.id;
57743
+ if (!id) return null;
57744
+ if (id === "https://www.w3.org/ns/activitystreams#Public" || id === "as:Public" || id === "Public") return "public";
57745
+ if (author && sameIdentity(id, author)) {
57746
+ if (/\/followers\/?$/u.test(id)) return "followers";
57747
+ if (/\/following\/?$/u.test(id)) return "following";
57748
+ }
57749
+ return null;
57750
+ };
57751
+ const list3 = (v) => [...new Set([].concat(v || []).map(word).filter(Boolean))];
57752
+ return { automatic: list3(can.automaticApproval), manual: list3(can.manualApproval) };
57753
+ }
57300
57754
 
57301
57755
  // lib/pod/links.mjs
57302
57756
  function linkTargets(headerValue, rel, baseUrl) {
@@ -57554,6 +58008,7 @@ async function isGone(intake, url) {
57554
58008
 
57555
58009
  // lib/core/intake/group.mjs
57556
58010
  function isModerationAsk(intake, activity) {
58011
+ if (intake.isModerationAskExtra?.(activity)) return true;
57557
58012
  if (activity.type === "Block") return true;
57558
58013
  if (activity.type === "Undo") {
57559
58014
  return typeof activity.object === "object" && activity.object?.type === "Block";
@@ -57603,14 +58058,23 @@ async function amplify(intake, noteId, { approved = false, activity = null } = {
57603
58058
  const s = intake.store.getStatuses().find((x) => x.noteId === noteId);
57604
58059
  if (!s) return;
57605
58060
  if (s.announcedAt) return;
57606
- if (s.direct || s.nonPublic) {
58061
+ if (s.direct || s.nonPublic && !intake.config.private) {
57607
58062
  intake.log(`not amplified \u2014 ${noteId} was not addressed publicly, and a group never widens a post's audience`);
57608
58063
  return;
57609
58064
  }
57610
58065
  const contacts = intake.store.getContacts();
57611
58066
  if (!contacts.followers.some((f) => f.actor === s.actor)) {
57612
- intake.log(`not amplified \u2014 ${s.actor} is not a member`);
57613
- return;
58067
+ if (intake.onStranger) {
58068
+ const held2 = await intake.onStranger({ noteId, actor: s.actor, activity }).catch(() => false);
58069
+ if (held2) {
58070
+ intake.log(`held for a moderator \u2014 ${s.actor} is not a member`);
58071
+ return;
58072
+ }
58073
+ }
58074
+ if (!intake.store.getContacts().followers.some((f) => f.actor === s.actor)) {
58075
+ intake.log(`not amplified \u2014 ${s.actor} is not a member`);
58076
+ return;
58077
+ }
57614
58078
  }
57615
58079
  if (intake.store.getMuted().actors.includes(s.actor)) {
57616
58080
  intake.log(`not amplified \u2014 ${s.actor} is muted`);
@@ -57655,6 +58119,9 @@ async function amplify(intake, noteId, { approved = false, activity = null } = {
57655
58119
  await intake.publisher.recordOutbox(act);
57656
58120
  intake.store.setPending(intake.store.getPending().filter((p) => p.noteId !== noteId));
57657
58121
  intake.log(`amplified ${noteId} \u2192 ${inboxes.length} inbox(es)`);
58122
+ if (intake.onCarried) {
58123
+ await intake.onCarried({ noteId, actor: s.actor, activity: act, sent: activity }).catch((e) => intake.log(`after the carry of ${noteId}: ${e.message}`));
58124
+ }
57658
58125
  await intake.bskyGroup?.mirrorCarry(s).catch((e) => intake.log(`bluesky mirror of the carry failed: ${e.message}`));
57659
58126
  }
57660
58127
  async function isCoMember(intake, actor) {
@@ -57702,6 +58169,7 @@ function announceTargets(intake, author) {
57702
58169
  }
57703
58170
 
57704
58171
  // lib/core/intake/activities.mjs
58172
+ var idOf = (v) => typeof v === "string" ? v : v?.id;
57705
58173
  function onAddRemove(intake, activity, actor) {
57706
58174
  const target = typeof activity.target === "string" ? activity.target : activity.target?.id;
57707
58175
  const ours = target && [intake.urls.followers, intake.urls.following, intake.urls.featured].filter(Boolean).includes(target);
@@ -57765,7 +58233,7 @@ async function onFollow(intake, activity, actor, { trusted = false } = {}) {
57765
58233
  }
57766
58234
  async function onUndo(intake, activity, actor, { trusted = false } = {}) {
57767
58235
  const inner = activity.object && typeof activity.object === "object" ? activity.object : null;
57768
- if (inner?.type === "Like" || inner?.type === "Announce") return onUndoReaction(intake, inner, actor, { trusted });
58236
+ if (inner?.type === "Like" || inner?.type === "Announce" || inner?.type === "EmojiReact") return onUndoReaction(intake, inner, actor, { trusted });
57769
58237
  if (typeof activity.object === "object" && activity.object?.type && activity.object.type !== "Follow") return;
57770
58238
  const named = typeof activity.object === "string" ? activity.object : activity.object?.id;
57771
58239
  if (!named && !trusted) return;
@@ -57861,6 +58329,13 @@ async function onUndoReaction(intake, inner, actor, { trusted = false } = {}) {
57861
58329
  const objectId = typeof target === "string" ? target : target?.id;
57862
58330
  if (!objectId) return;
57863
58331
  if (objectId.startsWith(intake.urls.notes)) {
58332
+ const reaction = reactionOf(inner);
58333
+ if (reaction) {
58334
+ const gone2 = intake.store.removeNotifications((n) => n.type === "reaction" && n.actor === actor && n.noteId === objectId && n.emoji === reaction.emoji);
58335
+ if (gone2) intake.log(`reaction ${reaction.emoji} withdrawn by ${actor} on ${objectId}`);
58336
+ return;
58337
+ }
58338
+ if (inner.type === "EmojiReact") return;
57864
58339
  const type = inner.type === "Like" ? "favourite" : "reblog";
57865
58340
  const gone = intake.store.removeNotifications((n) => n.type === type && n.actor === actor && n.noteId === objectId);
57866
58341
  if (gone) intake.log(`${inner.type} withdrawn by ${actor} on ${objectId}`);
@@ -57921,6 +58396,9 @@ async function onDelete(intake, activity, actor) {
57921
58396
  }
57922
58397
  const s = intake.store.getStatuses().find((x) => x.noteId === objectId);
57923
58398
  if (s) await intake.forget(s);
58399
+ if (s && intake.onCarriedGone) {
58400
+ await intake.onCarriedGone({ noteId: objectId, actor }).catch((e) => intake.log(`carried gone ${objectId}: ${e.message}`));
58401
+ }
57924
58402
  intake.log(`deleted upstream: ${objectId}`);
57925
58403
  }
57926
58404
  async function onUpdate(intake, activity, actor) {
@@ -57945,10 +58423,16 @@ async function onUpdate(intake, activity, actor) {
57945
58423
  const attachments = attachmentsOf3(note);
57946
58424
  const freshPoll = pollOf(note);
57947
58425
  const freshEmojis = emojisOf(note);
58426
+ const quote = quoteOf(note);
58427
+ const quoteState = quote ? await intake.quoteStateOf(note, quote) : null;
58428
+ const quotePolicy2 = quotePolicyOf(note, s.actor);
57948
58429
  intake.store.updateStatus(objectId, {
57949
58430
  content,
57950
58431
  ...attachments.length ? { attachments } : {},
57951
58432
  emojis: freshEmojis.length ? freshEmojis : void 0,
58433
+ quote: quote || void 0,
58434
+ quoteState: quoteState || void 0,
58435
+ quotePolicy: quotePolicy2 || void 0,
57952
58436
  // The edit's own stamp when the note carries one; tallies and the
57953
58437
  // content warning follow the edit too. A poll refresh keeps our vote.
57954
58438
  editedAt: note.updated || (/* @__PURE__ */ new Date()).toISOString(),
@@ -57957,9 +58441,14 @@ async function onUpdate(intake, activity, actor) {
57957
58441
  poll: { ...freshPoll, voted: !!s.poll?.voted, ownVotes: s.poll?.ownVotes || [] }
57958
58442
  } : {}
57959
58443
  });
58444
+ if (intake.onCarriedEdit) {
58445
+ await intake.onCarriedEdit({ noteId: objectId, actor, note }).catch((e) => intake.log(`carried edit ${objectId}: ${e.message}`));
58446
+ }
57960
58447
  intake.log(`edited upstream: ${objectId}`);
57961
58448
  }
57962
58449
  async function onReject(intake, activity, actor, { trusted = false } = {}) {
58450
+ const asked = quoteAsked(intake, activity);
58451
+ if (asked) return onQuoteAnswered(intake, asked, activity, actor, "Reject");
57963
58452
  if (activity.object?.type && activity.object.type !== "Follow") return;
57964
58453
  const contacts = intake.store.getContacts();
57965
58454
  const rec = contacts.following.find((f) => f.actor === actor);
@@ -57997,6 +58486,8 @@ async function onMove(intake, activity, actor) {
57997
58486
  intake.log(`${actor} moved to ${target} \u2014 follow the new account to keep seeing them`);
57998
58487
  }
57999
58488
  async function onAccept(intake, activity, actor, { trusted = false } = {}) {
58489
+ const asked = quoteAsked(intake, activity);
58490
+ if (asked) return onQuoteAnswered(intake, asked, activity, actor, "Accept");
58000
58491
  const contacts = intake.store.getContacts();
58001
58492
  const rec = contacts.following.find((f) => f.actor === actor);
58002
58493
  const named = typeof activity.object === "string" ? activity.object : activity.object?.id;
@@ -58012,6 +58503,86 @@ async function onAccept(intake, activity, actor, { trusted = false } = {}) {
58012
58503
  intake.log(`follow accepted by ${actor}`);
58013
58504
  }
58014
58505
  }
58506
+ function quoteAsked(intake, activity) {
58507
+ const named = idOf(activity.object);
58508
+ if (!named) return null;
58509
+ return intake.store.getStatuses().find((x) => x.kind === "post" && x.quoteRequest?.id === named) || null;
58510
+ }
58511
+ async function onQuoteAnswered(intake, s, activity, actor, type) {
58512
+ if (actor !== s.quoteRequest?.actor) {
58513
+ intake.log(`${type} of our quote request from ${actor}, who is not ${s.quoteRequest?.actor} \u2014 ignored`);
58514
+ return;
58515
+ }
58516
+ if (type === "Reject") {
58517
+ if (s.quoteState === "rejected") return;
58518
+ await intake.publisher.withdrawQuote(s);
58519
+ return;
58520
+ }
58521
+ if (s.quoteState === "accepted") return;
58522
+ const resultId = idOf(activity.result);
58523
+ if (!resultId || !intake.sameIdentity(resultId, actor)) return `Accept of a quote request carries no authorization at ${actor}`;
58524
+ const auth = await intake.fetchAP(resultId);
58525
+ if (!auth) throw new Error(`cannot fetch quote authorization ${resultId} \u2014 will retry`);
58526
+ const ok = auth.id === resultId && auth.type === "QuoteAuthorization" && [].concat(auth.attributedTo || []).map(idOf).includes(actor) && idOf(auth.interactingObject) === s.noteId && idOf(auth.interactionTarget) === s.quote;
58527
+ if (!ok) return `quote authorization ${resultId} does not say ${s.noteId} may quote ${s.quote}`;
58528
+ await intake.publisher.authorizeQuote(s, resultId);
58529
+ }
58530
+ async function onQuoteRequest(intake, activity, actor) {
58531
+ const objectId = idOf(activity.object);
58532
+ const instrumentId = idOf(activity.instrument);
58533
+ if (!objectId || !instrumentId) return "QuoteRequest without object or instrument";
58534
+ const ours = objectId.startsWith(intake.urls.notes) || intake.urls.privateNotes && objectId.startsWith(intake.urls.privateNotes);
58535
+ if (!ours) {
58536
+ intake.log(`QuoteRequest from ${actor} names ${objectId}, not a post of ours \u2014 ignored`);
58537
+ return;
58538
+ }
58539
+ if (!intake.sameIdentity(instrumentId, actor)) return `QuoteRequest instrument/actor identity mismatch (${instrumentId})`;
58540
+ const s = intake.store.getStatuses().find((x) => x.noteId === objectId && x.kind === "post");
58541
+ if (!s) return `QuoteRequest for a post we do not hold (${objectId})`;
58542
+ const note = await intake.fetchAP(instrumentId);
58543
+ if (!note) return `quote request: instrument fetch failed (${instrumentId})`;
58544
+ if (note.id !== instrumentId || !isContentType(note.type)) return `quote request: instrument is not verifiable content (${instrumentId})`;
58545
+ if (authorOf(note, actor) !== actor) return `quote request: ${instrumentId} is not by ${actor}`;
58546
+ if (quoteOf(note) !== objectId) return `quote request: ${instrumentId} does not quote ${objectId}`;
58547
+ const doc = await intake.fetchAP(actor);
58548
+ const inbox = doc?.endpoints?.sharedInbox || doc?.inbox;
58549
+ if (!inbox) return `quote request: no inbox for ${actor}`;
58550
+ const { quoteAnswerActivity: quoteAnswerActivity2 } = await Promise.resolve().then(() => (init_wire_quotes(), wire_quotes_exports));
58551
+ const open2 = !s.visibility || s.visibility === "public" || s.visibility === "unlisted";
58552
+ if (!open2) {
58553
+ await intake.deliverer.deliver(
58554
+ inbox,
58555
+ quoteAnswerActivity2({ urls: intake.urls, type: "Reject", request: activity, to: actor, serial: intake.serial++ })
58556
+ );
58557
+ intake.log(`quote refused: ${instrumentId} asked to quote ${objectId}, which is not public`);
58558
+ return;
58559
+ }
58560
+ const authorization = await intake.publisher.grantQuote(objectId, instrumentId);
58561
+ await intake.deliverer.deliver(
58562
+ inbox,
58563
+ quoteAnswerActivity2({ urls: intake.urls, type: "Accept", request: activity, result: authorization, to: actor, serial: intake.serial++ })
58564
+ );
58565
+ const held = intake.store.getStatuses().find((x) => x.noteId === instrumentId);
58566
+ if (!held) {
58567
+ const rejected = await intake.ingestNote(instrumentId, actor, { inline: note });
58568
+ if (rejected) intake.log(`quote of ${objectId} by ${actor}: ${rejected}`);
58569
+ } else if (held.quoteState !== "accepted") {
58570
+ intake.store.updateStatus(instrumentId, { quote: objectId, quoteState: "accepted" });
58571
+ }
58572
+ intake.store.addNotification({ type: "quote", actor, noteId: instrumentId });
58573
+ intake.log(`quote allowed: ${instrumentId} quotes ${objectId}`);
58574
+ }
58575
+ async function quoteStateOf(intake, note, quote) {
58576
+ if (quote.startsWith(intake.urls.notes)) {
58577
+ const target = intake.store.getStatuses().find((x) => x.noteId === quote);
58578
+ return (target?.quotedBy || []).some((q) => q.note === note.id) ? "accepted" : "pending";
58579
+ }
58580
+ const authId = idOf(note.quoteAuthorization);
58581
+ if (!authId || !intake.sameIdentity(authId, quote)) return "pending";
58582
+ const auth = await intake.fetchAP(authId).catch(() => null);
58583
+ const ok = auth && auth.id === authId && auth.type === "QuoteAuthorization" && idOf(auth.interactingObject) === note.id && idOf(auth.interactionTarget) === quote;
58584
+ return ok ? "accepted" : "pending";
58585
+ }
58015
58586
 
58016
58587
  // lib/core/intake/notes.mjs
58017
58588
  init_wire();
@@ -58021,6 +58592,8 @@ function concernsUs(intake, doc, actor) {
58021
58592
  if (audience.includes(intake.urls.actor) || audience.includes(intake.urls.followers)) return true;
58022
58593
  const tagged = [].concat(doc?.tag || []).some((t) => t?.type === "Mention" && (t.href === intake.urls.actor || t.name?.includes(intake.urls.actor)));
58023
58594
  if (tagged) return true;
58595
+ const quoted = quoteOf(doc);
58596
+ if (quoted && quoted.startsWith(intake.urls.notes)) return true;
58024
58597
  const inReplyTo = typeof doc?.inReplyTo === "string" ? doc.inReplyTo : doc?.inReplyTo?.id;
58025
58598
  if (!inReplyTo) return false;
58026
58599
  if (String(inReplyTo).startsWith(intake.urls.notes)) return true;
@@ -58081,6 +58654,7 @@ async function ingestNote(intake, objectId, actor, { via, inline = null } = {})
58081
58654
  const content = titledContent2(note);
58082
58655
  const author = authorOf(note, actor);
58083
58656
  if (!author) return `object names an author its origin does not vouch for (${objectId})`;
58657
+ intake.recentNotes?.set(note.id, note);
58084
58658
  if (intake.store.isBlocked(author)) return `blocked author (${author})`;
58085
58659
  if (!via && !intake.concernsUs(note, author)) {
58086
58660
  return `the note its own server serves does not address us (${objectId})`;
@@ -58102,6 +58676,10 @@ async function ingestNote(intake, objectId, actor, { via, inline = null } = {})
58102
58676
  const hashtags = [].concat(note.tag || []).filter((t) => t?.type === "Hashtag" && t.name).slice(0, MAX_MENTIONS).map((t) => ({ name: String(t.name).replace(/^#/, "").slice(0, 128), url: httpOnly(String(t.href || "").slice(0, MAX_URL_CHARS)) })).filter((t) => t.name);
58103
58677
  const emojis = emojisOf(note);
58104
58678
  const poll = pollOf(note);
58679
+ const quote = quoteOf(note);
58680
+ const quotePolicy2 = quotePolicyOf(note, author);
58681
+ const quoteState = quote ? await intake.quoteStateOf(note, quote) : null;
58682
+ if (quote) await holdQuoted(intake, quote);
58105
58683
  const audience = [].concat(note.to || [], note.cc || []).map(String);
58106
58684
  const direct = audience.length > 0 && !audience.includes(PUBLIC) && !audience.some((a) => a.endsWith("/followers"));
58107
58685
  const nonPublic = audience.length > 0 && !audience.includes(PUBLIC);
@@ -58122,11 +58700,16 @@ async function ingestNote(intake, objectId, actor, { via, inline = null } = {})
58122
58700
  ...emojis.length ? { emojis } : {},
58123
58701
  ...mentions.length ? { mentions } : {},
58124
58702
  ...attachments.length ? { attachments } : {},
58703
+ ...quote ? { quote, quoteState } : {},
58704
+ ...quotePolicy2 ? { quotePolicy: quotePolicy2 } : {},
58125
58705
  ...via ? { via } : {}
58126
58706
  });
58127
58707
  const namesUs = mentions.some((m) => m.href === intake.urls.actor);
58128
58708
  const replyToOurs = !!note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes);
58129
- if (!followed || replyToOurs || direct || namesUs) {
58709
+ const quotesUs = !!quote && quote.startsWith(intake.urls.notes);
58710
+ if (quotesUs) {
58711
+ intake.store.addNotification({ type: "quote", actor: author, noteId: note.id });
58712
+ } else if (!followed || replyToOurs || direct || namesUs) {
58130
58713
  intake.store.addNotification({ type: "mention", actor: author, noteId: note.id });
58131
58714
  }
58132
58715
  if (note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes)) {
@@ -58134,6 +58717,39 @@ async function ingestNote(intake, objectId, actor, { via, inline = null } = {})
58134
58717
  }
58135
58718
  intake.log(`${kind}: ${note.id}${via ? ` (boosted by ${via})` : ""}`);
58136
58719
  }
58720
+ async function holdQuoted(intake, quoteId) {
58721
+ if (intake.store.getStatuses().some((s) => s.noteId === quoteId)) return;
58722
+ if (intake.store.isBlocked(quoteId)) return;
58723
+ const q = await intake.fetchAP(quoteId).catch(() => null);
58724
+ if (!q || q.id !== quoteId || !isContentType(q.type)) {
58725
+ intake.log(`quoted post ${quoteId} could not be read \u2014 the quote shows without it`);
58726
+ return;
58727
+ }
58728
+ const by = authorOf(q, null);
58729
+ if (!by || intake.store.isBlocked(by)) return;
58730
+ const { attachmentsOf: attachmentsOf3, titledContent: titledContent2 } = await Promise.resolve().then(() => (init_wire(), wire_exports));
58731
+ const attachments = attachmentsOf3(q);
58732
+ const emojis = emojisOf(q);
58733
+ const poll = pollOf(q);
58734
+ const quotePolicy2 = quotePolicyOf(q, by);
58735
+ const mentions = [].concat(q.tag || []).filter((t) => t?.type === "Mention" && t.href && t.name).slice(0, MAX_MENTIONS).map((t) => ({ href: httpOnly(String(t.href).slice(0, MAX_URL_CHARS)), name: String(t.name).slice(0, 256) })).filter((m) => m.href);
58736
+ intake.store.addStatus({
58737
+ noteId: q.id,
58738
+ actor: by,
58739
+ content: titledContent2(q),
58740
+ published: q.published,
58741
+ inReplyTo: q.inReplyTo,
58742
+ kind: "remote",
58743
+ ...q.summary ? { spoiler: String(q.summary).replace(/<[^>]*>/g, "") } : {},
58744
+ ...q.sensitive === true ? { sensitive: true } : {},
58745
+ ...poll ? { poll } : {},
58746
+ ...emojis.length ? { emojis } : {},
58747
+ ...mentions.length ? { mentions } : {},
58748
+ ...attachments.length ? { attachments } : {},
58749
+ ...quotePolicy2 ? { quotePolicy: quotePolicy2 } : {}
58750
+ });
58751
+ intake.log(`quoted post kept: ${q.id}`);
58752
+ }
58137
58753
  async function forget(intake, s, { collect = null } = {}) {
58138
58754
  if (s.announceActivity) {
58139
58755
  await intake.retract(s.noteId, { collect }).catch((e) => intake.log(`retract: ${e.message}`));
@@ -63145,6 +63761,34 @@ var activitystreams_default2 = `# activitystreams.ttl \u2014 shapes for the Acti
63145
63761
  sh:minCount 1 ;
63146
63762
  sh:maxCount 1 ;
63147
63763
  ] .
63764
+
63765
+ <#Update>
63766
+ a sh:NodeShape ;
63767
+ sh:targetClass as:Update ;
63768
+ sh:name "Update" ;
63769
+ sh:description "A change to the object it names, carrying the parts that are now different." ;
63770
+ sh:property [
63771
+ sh:path as:actor ;
63772
+ sh:name "actor" ;
63773
+ sh:description "The one that performed the activity." ;
63774
+ sh:nodeKind sh:BlankNodeOrIRI ;
63775
+ sh:minCount 1 ;
63776
+ sh:maxCount 1 ;
63777
+ ] ;
63778
+ sh:property [
63779
+ sh:path as:object ;
63780
+ sh:name "object" ;
63781
+ sh:description "The thing the activity is about." ;
63782
+ sh:minCount 1 ;
63783
+ sh:maxCount 1 ;
63784
+ ] ;
63785
+ sh:property [
63786
+ sh:path ( as:object as:closed ) ;
63787
+ sh:name "closed" ;
63788
+ sh:description "The time the object stopped taking anything more." ;
63789
+ sh:datatype xsd:dateTime ;
63790
+ sh:maxCount 1 ;
63791
+ ] .
63148
63792
  `;
63149
63793
 
63150
63794
  // web/app/shims/shapes-text.mjs
@@ -63173,6 +63817,18 @@ function asTerm(q) {
63173
63817
  };
63174
63818
  return rdf3.quad(term3(q.subject), term3(q.predicate), term3(q.object));
63175
63819
  }
63820
+ var SH_NAME = "http://www.w3.org/ns/shacl#name";
63821
+ function pathName(r) {
63822
+ const direct = r.path?.value ?? null;
63823
+ if (direct && /[#/]/u.test(direct)) return direct;
63824
+ const shape = r.sourceShape;
63825
+ if (shape) {
63826
+ for (const q of shacl().$shapes?.dataset ?? []) {
63827
+ if (q.subject.equals(shape) && q.predicate.value === SH_NAME) return q.object.value;
63828
+ }
63829
+ }
63830
+ return direct;
63831
+ }
63176
63832
  async function checkShapes(quads) {
63177
63833
  if (!Array.isArray(quads) || quads.length === 0) return null;
63178
63834
  let report;
@@ -63190,7 +63846,12 @@ async function checkShapes(quads) {
63190
63846
  conforms: false,
63191
63847
  results: report.results.slice(0, 10).map((r) => ({
63192
63848
  focus: r.focusNode?.value ?? null,
63193
- path: r.path?.value ?? null,
63849
+ // A SEQUENCE path — `( as:object as:closed )` — is an RDF list, so its
63850
+ // node is blank and `.value` is a generated label like "n3-13". Reported
63851
+ // as that, the record named nothing a reader could act on. The shape's
63852
+ // own `sh:name` is what it is called, so that is used when the path
63853
+ // cannot name itself.
63854
+ path: pathName(r),
63194
63855
  message: r.message?.[0]?.value ?? String(r.sourceConstraintComponent?.value ?? "does not fit the shape"),
63195
63856
  shape: r.sourceShape?.value ?? null
63196
63857
  }))
@@ -63457,6 +64118,7 @@ var Intake = class {
63457
64118
  }
63458
64119
  this.lastDrain = (/* @__PURE__ */ new Date()).toISOString();
63459
64120
  this.lastDrainAtMs = Date.now();
64121
+ if (this.afterDrain) setTimeout(() => this.afterDrain(), 0);
63460
64122
  this._pruneAttempts();
63461
64123
  let all;
63462
64124
  try {
@@ -63608,8 +64270,11 @@ var Intake = class {
63608
64270
  return this.onCreate(activity, actor, { trusted });
63609
64271
  case "Accept":
63610
64272
  return this.onAccept(activity, actor, { trusted });
64273
+ case "QuoteRequest":
64274
+ return this.onQuoteRequest(activity, actor, { trusted });
63611
64275
  case "Like":
63612
- case "Announce": {
64276
+ case "Announce":
64277
+ case "EmojiReact": {
63613
64278
  const wrapped = activity.object;
63614
64279
  if (activity.type === "Announce" && wrapped && typeof wrapped === "object" && wrapped.type === "Delete") {
63615
64280
  return this.onAnnouncedDelete(actor, wrapped);
@@ -63617,6 +64282,27 @@ var Intake = class {
63617
64282
  const inner = wrapped && typeof wrapped === "object" && (wrapped.type === "Create" || wrapped.type === "Update") ? wrapped.object : wrapped;
63618
64283
  const objectId = typeof inner === "string" ? inner : inner?.id;
63619
64284
  this.log(`${activity.type} from ${actor} on ${objectId}`);
64285
+ const reaction = activity.type !== "Announce" ? reactionOf(activity) : null;
64286
+ if (reaction) {
64287
+ if (!objectId || !objectId.startsWith(this.urls.notes)) return;
64288
+ this.store.addNotification({
64289
+ type: "reaction",
64290
+ actor,
64291
+ noteId: objectId,
64292
+ emoji: reaction.emoji,
64293
+ ...reaction.url ? { url: reaction.url } : {},
64294
+ ...this.known(actor) ? {} : { unverified: true }
64295
+ });
64296
+ if (reaction.url) {
64297
+ const s = this.store.getStatuses().find((x) => x.noteId === objectId);
64298
+ const shortcode = reaction.emoji.replace(/^:|:$/g, "");
64299
+ if (s && !(s.emojis || []).some((e) => e.shortcode === shortcode) && (s.emojis || []).length < MAX_EMOJIS) {
64300
+ this.store.updateStatus(objectId, { emojis: [...s.emojis || [], { shortcode, url: reaction.url }] });
64301
+ }
64302
+ }
64303
+ return;
64304
+ }
64305
+ if (activity.type === "EmojiReact") return;
63620
64306
  if (objectId && objectId.startsWith(this.urls.notes)) {
63621
64307
  this.store.addNotification({
63622
64308
  type: activity.type === "Like" ? "favourite" : "reblog",
@@ -63642,6 +64328,9 @@ var Intake = class {
63642
64328
  return this.onAddRemove(activity, actor);
63643
64329
  case "Block":
63644
64330
  return this.onBlock(activity, actor, { trusted });
64331
+ case "Flag":
64332
+ if (this.onReport) return this.onReport(activity, actor, { trusted });
64333
+ return;
63645
64334
  default:
63646
64335
  this.log(`ignored ${activity.type} from ${actor}`);
63647
64336
  }
@@ -63747,6 +64436,12 @@ var Intake = class {
63747
64436
  onAnnounce(...a) {
63748
64437
  return onAnnounce(this, ...a);
63749
64438
  }
64439
+ onQuoteRequest(...a) {
64440
+ return onQuoteRequest(this, ...a);
64441
+ }
64442
+ quoteStateOf(...a) {
64443
+ return quoteStateOf(this, ...a);
64444
+ }
63750
64445
  onDelete(...a) {
63751
64446
  return onDelete(this, ...a);
63752
64447
  }
@@ -64230,6 +64925,7 @@ async function pinStatus(agent2, s, pinned) {
64230
64925
  init_wire();
64231
64926
  var MAX_BODY = 512 * 1024;
64232
64927
  var MAX_INBOX_PAGE = 500;
64928
+ var MAX_OUTBOX_PAGE = 50;
64233
64929
  var ACTIVITY_TYPES = /* @__PURE__ */ new Set([
64234
64930
  "Create",
64235
64931
  "Update",
@@ -64246,7 +64942,7 @@ var ACTIVITY_TYPES = /* @__PURE__ */ new Set([
64246
64942
  "Move"
64247
64943
  ]);
64248
64944
  var arr = (v) => v === void 0 || v === null ? [] : Array.isArray(v) ? v : [v];
64249
- var idOf = (v) => typeof v === "string" ? v : v?.id || null;
64945
+ var idOf2 = (v) => typeof v === "string" ? v : v?.id || null;
64250
64946
  function readBody(req) {
64251
64947
  return new Promise((resolve2, reject) => {
64252
64948
  let size = 0;
@@ -64265,10 +64961,12 @@ function readBody(req) {
64265
64961
  });
64266
64962
  }
64267
64963
  var C2S = class {
64268
- constructor({ agent: agent2, log: log2 = console.log, auth }) {
64964
+ constructor({ agent: agent2, log: log2 = console.log, auth, scheme = null, mount = "" }) {
64269
64965
  this.agent = agent2;
64270
64966
  this.log = log2;
64271
64967
  this.auth = auth;
64968
+ this.scheme = scheme;
64969
+ this.mount = mount;
64272
64970
  }
64273
64971
  get store() {
64274
64972
  return this.agent.store;
@@ -64355,6 +65053,79 @@ var C2S = class {
64355
65053
  orderedItems: kept.map((k) => k.activity)
64356
65054
  });
64357
65055
  }
65056
+ // The actor a client-to-server client on this machine reads. The same
65057
+ // document the pod holds, but for the fields naming where a write goes —
65058
+ // the outbox and the two OAuth endpoints — and for `id`, which names this
65059
+ // address so the client files its credential here and comes back. The pod's
65060
+ // id stays on as an alias; the two are one identity.
65061
+ // What this agent calls itself, to the client that just asked. The Host
65062
+ // passed the Authorities firewall before any route ran, so whichever alias
65063
+ // the client used is one this agent answers on. The scheme is read off the
65064
+ // connection unless the caller named one: only the Server passes a scheme,
65065
+ // and standalone this listener is its own TLS.
65066
+ localOrigin(req) {
65067
+ const host = req.headers.host;
65068
+ if (!host) return null;
65069
+ const scheme = this.scheme || (req.socket?.encrypted || req.headers["x-forwarded-proto"] === "https" ? "https" : "http");
65070
+ return `${scheme}://${host}${this.mount}/`;
65071
+ }
65072
+ /**
65073
+ * The owner's own outbox, from what this agent recorded. The pod holds the
65074
+ * published copy and it stays the canonical one; this is served only where
65075
+ * that copy cannot be read by the client asking.
65076
+ */
65077
+ sendLocalOutbox(res, url, origin) {
65078
+ const id = `${origin}ap/outbox`;
65079
+ const outbox = this.store.read("outbox.json", []);
65080
+ const page2 = url?.searchParams?.get("page") || null;
65081
+ const ct = { "content-type": "application/activity+json; charset=utf-8" };
65082
+ if (!page2) {
65083
+ return this.send(res, 200, {
65084
+ "@context": AS_CTX,
65085
+ id,
65086
+ type: "OrderedCollection",
65087
+ totalItems: outbox.length,
65088
+ ...outbox.length ? { first: `${id}?page=1` } : { orderedItems: [] }
65089
+ }, ct);
65090
+ }
65091
+ const n = Number(page2);
65092
+ if (!Number.isInteger(n) || n < 1) {
65093
+ return this.send(res, 400, { error: "page is a whole number from 1" });
65094
+ }
65095
+ const start = (n - 1) * MAX_OUTBOX_PAGE;
65096
+ const items = outbox.slice(start, start + MAX_OUTBOX_PAGE);
65097
+ return this.send(res, 200, {
65098
+ "@context": AS_CTX,
65099
+ id: `${id}?page=${n}`,
65100
+ type: "OrderedCollectionPage",
65101
+ partOf: id,
65102
+ ...start + items.length < outbox.length ? { next: `${id}?page=${n + 1}` } : {},
65103
+ // A post is recorded by its object id and published as the Create beside
65104
+ // it — the same mapping the pod's copy uses, from the same helper.
65105
+ orderedItems: items.map(outboxWireItem)
65106
+ }, ct);
65107
+ }
65108
+ sendLocalActor(req, res, origin) {
65109
+ const pub = this.agent.publisher;
65110
+ const cfg = pub.config || {};
65111
+ const gw = cfg.gateway;
65112
+ const gwActive = gw && gw.url && gw.mode && gw.mode !== "off";
65113
+ const doc = pub.actorDocFor({
65114
+ inbox: gwActive ? gw.url : cfg.inboxUrl || null,
65115
+ clientOrigin: origin
65116
+ });
65117
+ const canonical2 = doc.id;
65118
+ doc.id = `${origin}ap/actor`;
65119
+ doc.alsoKnownAs = [.../* @__PURE__ */ new Set([...doc.alsoKnownAs || [], canonical2])];
65120
+ const body = JSON.stringify(doc);
65121
+ res.writeHead(200, {
65122
+ "content-type": "application/activity+json; charset=utf-8",
65123
+ "cache-control": "no-store",
65124
+ "content-length": Buffer.byteLength(body)
65125
+ });
65126
+ res.end(req.method === "HEAD" ? void 0 : body);
65127
+ return true;
65128
+ }
64358
65129
  async handle(req, res, pathname, url) {
64359
65130
  if (pathname !== "/ap/outbox" && pathname !== "/ap/actor" && pathname !== "/ap/inbox") return false;
64360
65131
  if (req.method === "OPTIONS") {
@@ -64374,6 +65145,9 @@ var C2S = class {
64374
65145
  return this.sendInbox(res, url);
64375
65146
  }
64376
65147
  if (req.method === "GET" || req.method === "HEAD") {
65148
+ const local = this.agent.publisher?.clientOrigin ? null : this.localOrigin(req);
65149
+ if (local && pathname === "/ap/actor") return this.sendLocalActor(req, res, local);
65150
+ if (local && pathname === "/ap/outbox") return this.sendLocalOutbox(res, url, local);
64377
65151
  const target = pathname === "/ap/actor" ? this.urls.actor : this.urls.outbox;
64378
65152
  res.writeHead(303, { location: target, "cache-control": "no-store" });
64379
65153
  res.end();
@@ -64414,8 +65188,8 @@ var C2S = class {
64414
65188
  // every client means by a post with no audience chosen, and what a client
64415
65189
  // that never addresses (dokieli's annotations) needs.
64416
65190
  visibilityOf(activity, object) {
64417
- const to = arr(activity.to ?? object?.to).map(idOf);
64418
- const cc = arr(activity.cc ?? object?.cc).map(idOf);
65191
+ const to = arr(activity.to ?? object?.to).map(idOf2);
65192
+ const cc = arr(activity.cc ?? object?.cc).map(idOf2);
64419
65193
  if (!to.length && !cc.length) return "public";
64420
65194
  if (to.includes(PUBLIC)) return "public";
64421
65195
  if (cc.includes(PUBLIC)) return "unlisted";
@@ -64441,13 +65215,13 @@ var C2S = class {
64441
65215
  // in to/cc are listed and delivered to; the ones in bto/bcc are delivered
64442
65216
  // to and never listed (§6).
64443
65217
  addressedActors(activity, object) {
64444
- const pick = (...fields) => [...new Set(fields.flatMap((f) => arr(activity[f] ?? object?.[f]).map(idOf)))].filter((a) => typeof a === "string" && /^https?:\/\//u.test(a) && a !== PUBLIC && a !== this.urls.followers && a !== this.urls.actor);
65218
+ const pick = (...fields) => [...new Set(fields.flatMap((f) => arr(activity[f] ?? object?.[f]).map(idOf2)))].filter((a) => typeof a === "string" && /^https?:\/\//u.test(a) && a !== PUBLIC && a !== this.urls.followers && a !== this.urls.actor);
64445
65219
  return { also: pick("to", "cc"), deliverTo: pick("bto", "bcc") };
64446
65220
  }
64447
65221
  async _dispatch(activity, { slug, raw, reply }) {
64448
65222
  const agent2 = this.agent;
64449
65223
  const object = typeof activity.object === "object" && activity.object !== null ? activity.object : null;
64450
- const objectId = idOf(activity.object);
65224
+ const objectId = idOf2(activity.object);
64451
65225
  switch (activity.type) {
64452
65226
  case "Create": {
64453
65227
  const makes = object?.type || "Note";
@@ -64470,7 +65244,7 @@ var C2S = class {
64470
65244
  options: titles,
64471
65245
  multiple: !one.length && many.length > 0,
64472
65246
  expiresAt: object.endTime || null,
64473
- inReplyTo: idOf(object.inReplyTo) || void 0,
65247
+ inReplyTo: idOf2(object.inReplyTo) || void 0,
64474
65248
  visibility,
64475
65249
  spoilerText: object.summary || null,
64476
65250
  sensitive: object.sensitive === true
@@ -64490,7 +65264,7 @@ var C2S = class {
64490
65264
  ...a?.name ? { description: a.name } : {}
64491
65265
  })).filter((a) => a.url);
64492
65266
  const note = await agent2.publisher.publishNote(text, {
64493
- inReplyTo: idOf(object.inReplyTo) || void 0,
65267
+ inReplyTo: idOf2(object.inReplyTo) || void 0,
64494
65268
  attachments,
64495
65269
  visibility,
64496
65270
  spoilerText: object.summary || null,
@@ -64583,28 +65357,28 @@ var C2S = class {
64583
65357
  }
64584
65358
  case "Undo": {
64585
65359
  const inner = object;
64586
- const innerId = idOf(activity.object);
65360
+ const innerId = idOf2(activity.object);
64587
65361
  if (inner?.type === "Block") {
64588
- const target = idOf(inner.object);
65362
+ const target = idOf2(inner.object);
64589
65363
  if (!target) return reply(400, { error: "unblock whom?" });
64590
65364
  await unblockActor(agent2, target);
64591
65365
  return reply(200, { ok: true, object: target });
64592
65366
  }
64593
65367
  const statuses = this.store.getStatuses();
64594
65368
  let s = innerId ? statuses.find((x) => x.likeActivity?.id === innerId) : null;
64595
- if (!s && inner?.type === "Like") s = this.byIri(idOf(inner.object));
65369
+ if (!s && inner?.type === "Like") s = this.byIri(idOf2(inner.object));
64596
65370
  if (s?.favourited) {
64597
65371
  const updated = await unfavourite(agent2, s);
64598
65372
  return reply(200, { ok: true, object: updated.noteId });
64599
65373
  }
64600
65374
  s = innerId ? statuses.find((x) => x.announceActivity?.id === innerId) : null;
64601
- if (!s && inner?.type === "Announce") s = this.byIri(idOf(inner.object));
65375
+ if (!s && inner?.type === "Announce") s = this.byIri(idOf2(inner.object));
64602
65376
  if (s?.reblogged) {
64603
65377
  const updated = await unreblog(agent2, s);
64604
65378
  return reply(200, { ok: true, object: updated.noteId });
64605
65379
  }
64606
65380
  const following = this.store.getContacts().following;
64607
- const rec = following.find((f) => f.followActivity?.id === innerId) || (inner?.type === "Follow" ? following.find((f) => f.actor === idOf(inner.object)) : null);
65381
+ const rec = following.find((f) => f.followActivity?.id === innerId) || (inner?.type === "Follow" ? following.find((f) => f.actor === idOf2(inner.object)) : null);
64608
65382
  if (rec) {
64609
65383
  await unfollowActor(agent2, rec.actor);
64610
65384
  return reply(200, { ok: true, object: rec.actor });
@@ -64618,7 +65392,7 @@ var C2S = class {
64618
65392
  }
64619
65393
  case "Add":
64620
65394
  case "Remove": {
64621
- if (idOf(activity.target) !== this.urls.featured) {
65395
+ if (idOf2(activity.target) !== this.urls.featured) {
64622
65396
  return reply(422, { error: "the featured collection is the one Add/Remove edits here" });
64623
65397
  }
64624
65398
  const s = this.byIri(objectId);
@@ -64628,7 +65402,7 @@ var C2S = class {
64628
65402
  }
64629
65403
  case "Accept":
64630
65404
  case "Reject": {
64631
- const requester = object?.actor ? idOf(object.actor) : objectId;
65405
+ const requester = object?.actor ? idOf2(object.actor) : objectId;
64632
65406
  if (!requester) return reply(400, { error: "whose request? object must name the Follow or its actor" });
64633
65407
  const r = activity.type === "Accept" ? await admitRequest(agent2, requester).catch((e) => ({ error: e.message })) : await refuseRequest(agent2, requester).catch((e) => ({ error: e.message }));
64634
65408
  if (r.error) return reply(404, { error: r.error });
@@ -64649,11 +65423,21 @@ var RENEW_MS = 9e4;
64649
65423
  var JITTER = () => 0.85 + Math.random() * 0.3;
64650
65424
  var UNREADABLE = /* @__PURE__ */ Symbol("lease-unreadable");
64651
65425
  var Lease = class {
64652
- constructor({ url, fetchImpl, log: log2 = console.log }) {
65426
+ // `id` names the HOLDER, and acquire() lets a holder reclaim its own lease
65427
+ // without waiting for the TTL — so whether it is stable decides whether a
65428
+ // restart is the same agent coming back or a second one arriving. A process
65429
+ // that is gone for good wants a new one each time; a browser does not, since
65430
+ // its agent lives in a service worker the browser is free to kill and restart
65431
+ // whenever it likes. Left to the default, every one of those restarts looked
65432
+ // like another device and asked the owner to take over their own account —
65433
+ // which is what switching between the two clients did, because that is a
65434
+ // navigation and a navigation can take the worker with it. The browser build
65435
+ // passes one it keeps.
65436
+ constructor({ url, fetchImpl, log: log2 = console.log, id = null }) {
64653
65437
  this.url = url;
64654
65438
  this.fetchImpl = fetchImpl;
64655
65439
  this.log = log2;
64656
- this.id = node_crypto_default.randomUUID();
65440
+ this.id = id || node_crypto_default.randomUUID();
64657
65441
  this.stopped = false;
64658
65442
  this.timer = null;
64659
65443
  this.heldUntil = 0;
@@ -64968,6 +65752,7 @@ function readBody2(req) {
64968
65752
  const ct = String(req.headers["content-type"] || "");
64969
65753
  try {
64970
65754
  if (ct.includes("application/json")) return resolve2(data ? JSON.parse(data) : {});
65755
+ if (ct.includes("multipart/form-data")) return resolve2(textFields(data, ct));
64971
65756
  const form = new URLSearchParams(data);
64972
65757
  const out = {};
64973
65758
  for (const key of new Set(form.keys())) {
@@ -64981,6 +65766,76 @@ function readBody2(req) {
64981
65766
  req.on("error", reject);
64982
65767
  });
64983
65768
  }
65769
+ function textFields(data, contentType) {
65770
+ const m = /boundary=(?:"([^"]+)"|([^;]+))/.exec(contentType);
65771
+ if (!m) return {};
65772
+ const out = {};
65773
+ for (const part of data.split("--" + (m[1] || m[2]).trim())) {
65774
+ const sep = part.indexOf("\r\n\r\n");
65775
+ if (sep < 0) continue;
65776
+ const head = part.slice(0, sep);
65777
+ const name = /name="([^"]*)"/.exec(head)?.[1];
65778
+ if (!name || /filename="/.test(head)) continue;
65779
+ const value = part.slice(sep + 4).replace(/\r\n$/, "");
65780
+ if (name.endsWith("[]")) (out[name] ||= []).push(value);
65781
+ else out[name] = value;
65782
+ }
65783
+ return out;
65784
+ }
65785
+ function readMultipart(req, limit = 12e6) {
65786
+ return new Promise((resolve2, reject) => {
65787
+ const chunks = [];
65788
+ let n = 0;
65789
+ req.on("data", (c) => {
65790
+ n += c.length;
65791
+ if (n > limit) {
65792
+ reject(new Error("upload too large"));
65793
+ req.destroy();
65794
+ return;
65795
+ }
65796
+ chunks.push(c);
65797
+ });
65798
+ req.on("error", reject);
65799
+ req.on("end", () => {
65800
+ try {
65801
+ const m = /boundary=(?:"([^"]+)"|([^;]+))/.exec(String(req.headers["content-type"] || ""));
65802
+ if (!m) return resolve2({ fields: {}, file: null });
65803
+ const buf = Buffer.concat(chunks);
65804
+ const boundary = Buffer.from("--" + (m[1] || m[2]).trim());
65805
+ const fields = {};
65806
+ const files = {};
65807
+ let file = null;
65808
+ let i = buf.indexOf(boundary);
65809
+ while (i >= 0) {
65810
+ const start = i + boundary.length;
65811
+ if (buf.slice(start, start + 2).toString() === "--") break;
65812
+ const next = buf.indexOf(boundary, start);
65813
+ if (next < 0) break;
65814
+ const part = buf.slice(start + 2, next - 2);
65815
+ const sep = part.indexOf("\r\n\r\n");
65816
+ if (sep >= 0) {
65817
+ const head = part.slice(0, sep).toString();
65818
+ const body = part.slice(sep + 4);
65819
+ const name = /name="([^"]*)"/.exec(head)?.[1];
65820
+ const filename = /filename="([^"]*)"/.exec(head)?.[1];
65821
+ if (filename !== void 0) {
65822
+ file = {
65823
+ filename,
65824
+ contentType: /content-type:\s*([^\r\n]+)/i.exec(head)?.[1]?.trim() || "application/octet-stream",
65825
+ data: body
65826
+ };
65827
+ if (name) files[name] = file;
65828
+ } else if (name) fields[name] = body.toString();
65829
+ }
65830
+ i = next;
65831
+ }
65832
+ resolve2({ fields, file, files });
65833
+ } catch (e) {
65834
+ reject(e);
65835
+ }
65836
+ });
65837
+ });
65838
+ }
64984
65839
 
64985
65840
  // lib/client/masto/oauth.mjs
64986
65841
  var TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
@@ -65008,6 +65863,39 @@ function checkPassword(rec, password) {
65008
65863
  }
65009
65864
  var escapeHtml = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
65010
65865
  var parseRedirects = (v) => (Array.isArray(v) ? v : String(v || "").split(/\s+/)).map((s) => s.trim()).filter(Boolean);
65866
+ function sendPodSigninPage(res, client, webId, mount = "") {
65867
+ let asking = "";
65868
+ if (client && (client.name || client.redirect)) {
65869
+ let where = "";
65870
+ try {
65871
+ where = client.redirect ? new URL(client.redirect).host : "";
65872
+ } catch {
65873
+ }
65874
+ const who = client.name ? escapeHtml(client.name) : where ? escapeHtml(where) : "A client";
65875
+ asking = `<p><strong>${who}</strong> is asking to use your account${where ? `, and will be sent back to <code>${escapeHtml(where)}</code>` : ""}.</p>`;
65876
+ }
65877
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-cache" });
65878
+ res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
65879
+ <title>Sign in with your pod</title>
65880
+ <style>:root{color-scheme:light dark;--heading:#1a4f8a;--btn:#3d6b35;--line:#9a9a9a}
65881
+ body{font:20px Arial,Helvetica,sans-serif;max-width:28rem;margin:12vh auto;padding:0 1rem}
65882
+ h1{color:var(--heading);font-size:1.5rem}
65883
+ code{word-break:break-all;background:rgba(120,120,120,.12);padding:.05em .35em;border-radius:4px}
65884
+ button{font:inherit;padding:.6rem 1.1rem;background:var(--btn);color:#fff;border:none;border-radius:9px;cursor:pointer}
65885
+ button[hidden]{display:none}
65886
+ #signin-status{min-height:1.5em;padding:.6rem .8rem;border:1px solid var(--line);border-radius:8px;margin-top:1rem}
65887
+ @media (prefers-color-scheme:dark){:root{--heading:#7fb3e8;--btn:#5a8a4f}}</style></head><body>
65888
+ <main>
65889
+ <h1>Sign in with your pod</h1>
65890
+ ${asking}
65891
+ <p>This account belongs to <a id="webid" href="${escapeHtml(webId)}">${escapeHtml(webId)}</a>. Sign in at that pod to allow it.</p>
65892
+ <p><button type="button" id="signin" hidden>Sign in with your pod</button></p>
65893
+ <p id="signin-status" role="status" aria-live="polite"></p>
65894
+ </main>
65895
+ <script type="module" src="${escapeHtml(mount)}/oauth/session/signin.mjs"><\/script>
65896
+ </body></html>`);
65897
+ return true;
65898
+ }
65011
65899
  function sendLoginForm(res, params, error2 = "", client = null, status2 = null, headers = {}) {
65012
65900
  const hidden = [...params.entries()].filter(([k]) => k !== "password").map(([k, v]) => `<input type="hidden" name="${escapeHtml(k)}" value="${escapeHtml(v)}">`).join("\n");
65013
65901
  let asking = "<p>Enter the agent password to authorize this client.</p>";
@@ -65276,6 +66164,16 @@ async function handle(api, ctx) {
65276
66164
  params = new URLSearchParams(body);
65277
66165
  }
65278
66166
  const redirect = params.get("redirect_uri") || "";
66167
+ let proved = false;
66168
+ if (req.method === "POST" && req.headers.authorization && req.headers.dpop && api.podAuth) {
66169
+ const proof = await api.podAuth(req, pathname);
66170
+ if (!proof.ok) return send(proof.status || 401, { error: proof.error });
66171
+ proved = true;
66172
+ }
66173
+ const ownerWebId = api.ownerWebId?.() || null;
66174
+ if (req.method === "GET" && !params.get("client_id") && params.get("code") && params.get("state") && ownerWebId) {
66175
+ return sendPodSigninPage(res, null, ownerWebId, api.mount || "");
66176
+ }
65279
66177
  const app = api.findApp(params.get("client_id") || "");
65280
66178
  const doc = app ? null : await api.resolveClientDocument(params.get("client_id") || "");
65281
66179
  const external = !!app || !!doc;
@@ -65283,13 +66181,14 @@ async function handle(api, ctx) {
65283
66181
  api.log(`authorize refused: cross-site navigation to the mint from ${req.headers.referer || "nowhere"}`);
65284
66182
  return send(403, { error: "a cross-site navigation may not authorize a client" });
65285
66183
  }
65286
- if (external && !api.store.getConfig()?.uiPassword && !api.redirectAllowed(redirect)) {
66184
+ const client = { name: app?.name || doc?.name || null, redirect, scope: params.get("scope") || "read" };
66185
+ if (external && !proved && !api.store.getConfig()?.uiPassword && !api.redirectAllowed(redirect)) {
66186
+ if (req.method === "GET" && ownerWebId) return sendPodSigninPage(res, client, ownerWebId, api.mount || "");
65287
66187
  api.log(`authorize refused: no UI password, and "${redirect}" is not an address of this agent`);
65288
66188
  return send(403, {
65289
66189
  error: "this client asks to be sent somewhere other than this agent, and no password is set to approve that with. Run `fedipod passwd` and try again."
65290
66190
  });
65291
66191
  }
65292
- const client = { name: app?.name || doc?.name || null, redirect, scope: params.get("scope") || "read" };
65293
66192
  if (app) {
65294
66193
  if (!app.redirectUris.includes(redirect)) {
65295
66194
  api.log(`authorize refused: redirect_uri "${redirect}" not registered for ${app.clientId}`);
@@ -65308,7 +66207,7 @@ async function handle(api, ctx) {
65308
66207
  api.log(`authorize refused: redirect_uri "${redirect}" is not this agent`);
65309
66208
  return send(400, { error: "redirect_uri must be an address of this agent" });
65310
66209
  }
65311
- if (req.method === "POST") {
66210
+ if (req.method === "POST" && !proved) {
65312
66211
  if (api.rateLimited()) {
65313
66212
  api.log("authorize rate limited");
65314
66213
  return sendLoginForm(
@@ -65323,12 +66222,13 @@ async function handle(api, ctx) {
65323
66222
  if (!pw || !checkPassword(pw, body.password || "")) {
65324
66223
  return sendLoginForm(res, params, "wrong password \u2014 try again", client);
65325
66224
  }
65326
- } else if (pw) {
66225
+ } else if (pw && !proved) {
65327
66226
  return sendLoginForm(res, params, "", client);
65328
- } else if (api.allowed && !api.allowed.isLocalRequest(req)) {
66227
+ } else if (!proved && api.allowed && !api.allowed.isLocalRequest(req) && !api.throughDoor?.(req)) {
66228
+ if (req.method === "GET" && ownerWebId) return sendPodSigninPage(res, client, ownerWebId, api.mount || "");
65329
66229
  api.log(`authorize refused: no UI password, and "${req.headers.host}" is not this machine`);
65330
66230
  return send(403, {
65331
- error: "this agent answers on an address outside this machine and has no password set \u2014 " + (api.embedded ? `POST {"password":"\u2026"} to the owner door's /config with its door secret before logging in` : "run `fedipod passwd` before logging in over that address")
66231
+ error: api.embedded ? "Open your account's management page first, then sign in from there." : "Signing in from another machine needs a password. On the machine that runs your account, run: fedipod passwd"
65332
66232
  });
65333
66233
  }
65334
66234
  const code = external ? api.mintCode({
@@ -65342,6 +66242,7 @@ async function handle(api, ctx) {
65342
66242
  const target = new URL(redirect);
65343
66243
  target.searchParams.set("code", code);
65344
66244
  if (params.get("state")) target.searchParams.set("state", params.get("state"));
66245
+ if (proved) return send(200, { redirect: target.href });
65345
66246
  res.writeHead(302, { location: target.href });
65346
66247
  res.end();
65347
66248
  return true;
@@ -65656,6 +66557,7 @@ var BskyFeed = class {
65656
66557
 
65657
66558
  // lib/client/masto/render.mjs
65658
66559
  var TRANSPARENT_PNG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
66560
+ var DEFAULT_AVATAR = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAABd0lEQVR42u3ZoW4CQRDG8Xk6NBLbVCJ5gBpMTQ2iBlFR29RXNsFhz1JHXV9hydfkEkoobW5n2F34i0kIHMfN73ZvdwYbTabpmsMAAAAAAAAAAAAAAAAAAACiY3wzS/ePz+n1bZW6zTZ9fH59h17rPX2mYy4OQEktnl5+JP1b6Bgdey6IcIDb2Ty9r7s/Ez8MfUffbRpACfznrp8aDdEIFjnsh9z5YyMhcjqEAWge5ybfh87VFIDuWM7QPzYVokZBCICWM6/k+9A5mwHQmu4NoHM2A+A5/PenQTMA3sn3AQBTgIcgyyAbIbbCFEOUwzREaInRFKUtDgAAFQLcPSxDCqH9Z4V+o0oAXVhU4ofhiWCtJe+NYKV3e6V3idkAHvv9nDqhKIBnxVeqUrRaav5SvQKrqetTols0GGDdbaoB0LWcFUBP31qS72PoimCtD//caWC1tL1Ltc2ttbXfe08wCKC25HP+OgMAAAAAAAAAAAAAAAAArhNgB1PE/x/JOiuNAAAAAElFTkSuQmCC";
65659
66561
  var selfIcon = (api, self2, cached) => (self2 ? api.store.getConfig()?.icon : null) || cached.icon;
65660
66562
  function selfAccount(api) {
65661
66563
  return api.account(api.urls.actor, { selfAcct: publicHandle(api.store.getConfig()) });
@@ -65693,8 +66595,8 @@ function account(api, actorUrl, { selfAcct } = {}) {
65693
66595
  note: (self2 ? api.store.getConfig()?.summary : cached.summary) || "",
65694
66596
  url: actorUrl,
65695
66597
  uri: actorUrl,
65696
- avatar: selfIcon(api, self2, cached) || TRANSPARENT_PNG,
65697
- avatar_static: selfIcon(api, self2, cached) || TRANSPARENT_PNG,
66598
+ avatar: selfIcon(api, self2, cached) || DEFAULT_AVATAR,
66599
+ avatar_static: selfIcon(api, self2, cached) || DEFAULT_AVATAR,
65698
66600
  header: (self2 ? api.store.getConfig()?.image : null) || TRANSPARENT_PNG,
65699
66601
  header_static: (self2 ? api.store.getConfig()?.image : null) || TRANSPARENT_PNG,
65700
66602
  // A remote actor's counts are whatever its own collections said when we
@@ -65709,11 +66611,11 @@ function account(api, actorUrl, { selfAcct } = {}) {
65709
66611
  fields: (self2 ? api.store.getConfig()?.fields : cached.fields) || []
65710
66612
  };
65711
66613
  }
65712
- function page(api, items, url, { limit = 20, max = 40, idOf: idOf2 = (s) => api.store.idFor(s.noteId) } = {}) {
66614
+ function page(api, items, url, { limit = 20, max = 40, idOf: idOf3 = (s) => api.store.idFor(s.noteId) } = {}) {
65713
66615
  const n = Math.min(Number(url.searchParams.get("limit")) || limit, max);
65714
66616
  const ids = /* @__PURE__ */ new Map();
65715
66617
  const idAt = (s) => {
65716
- if (!ids.has(s)) ids.set(s, idOf2(s));
66618
+ if (!ids.has(s)) ids.set(s, idOf3(s));
65717
66619
  return ids.get(s);
65718
66620
  };
65719
66621
  const cut = (param) => {
@@ -65737,8 +66639,8 @@ function page(api, items, url, { limit = 20, max = 40, idOf: idOf2 = (s) => api.
65737
66639
  for (const [k, v] of Object.entries(extra)) u.searchParams.set(k, v);
65738
66640
  return u.href;
65739
66641
  };
65740
- const links = [`<${q({ max_id: idOf2(pageItems2[pageItems2.length - 1]) })}>; rel="next"`];
65741
- if (pageItems2.length) links.push(`<${q({ min_id: idOf2(pageItems2[0]) })}>; rel="prev"`);
66642
+ const links = [`<${q({ max_id: idOf3(pageItems2[pageItems2.length - 1]) })}>; rel="next"`];
66643
+ if (pageItems2.length) links.push(`<${q({ min_id: idOf3(pageItems2[0]) })}>; rel="prev"`);
65742
66644
  return { items: pageItems2, headers: { link: links.join(", ") } };
65743
66645
  }
65744
66646
  async function bskyReply(api, send, body, parent, visibility) {
@@ -65849,7 +66751,26 @@ async function acctReply(api, send, body, parent, visibility) {
65849
66751
  return send(422, { error: e.message });
65850
66752
  }
65851
66753
  }
65852
- function status(api, s, { all } = {}) {
66754
+ function quoteJson(api, s, { all, depth = 0 } = {}) {
66755
+ if (!s.quote) return null;
66756
+ const state = s.quoteState || "pending";
66757
+ const quoted = state !== "rejected" && (all || api.store.getStatuses()).find((x) => x.noteId === s.quote);
66758
+ if (depth > 0) return { state, quoted_status_id: quoted ? api.store.idFor(quoted.noteId) : null };
66759
+ return { state, quoted_status: quoted ? api.status(quoted, { all, depth: depth + 1 }) : null };
66760
+ }
66761
+ function quoteApproval(api, s) {
66762
+ if (s.actor === api.urls?.actor) {
66763
+ const open2 = !s.visibility || s.visibility === "public" || s.visibility === "unlisted";
66764
+ return { automatic: open2 ? ["public"] : [], manual: [], current_user: open2 ? "automatic" : "denied" };
66765
+ }
66766
+ const p = s.quotePolicy;
66767
+ if (!p) return { automatic: [], manual: [], current_user: "unknown" };
66768
+ const follows = () => api.store.getContacts().following.some((f) => f.actor === s.actor && f.accepted);
66769
+ const may = (list3) => list3.includes("public") || list3.includes("followers") && follows();
66770
+ const current = may(p.automatic) ? "automatic" : may(p.manual) ? "manual" : "denied";
66771
+ return { automatic: p.automatic, manual: p.manual, current_user: current };
66772
+ }
66773
+ function status(api, s, { all, depth = 0 } = {}) {
65853
66774
  const replies = (all || api.store.getStatuses()).filter((x) => x.inReplyTo === s.noteId).length;
65854
66775
  return {
65855
66776
  id: api.store.idFor(s.noteId),
@@ -65905,6 +66826,8 @@ function status(api, s, { all } = {}) {
65905
66826
  })),
65906
66827
  card: null,
65907
66828
  poll: s.poll ? api.pollJson(s) : null,
66829
+ quote: quoteJson(api, s, { all, depth }),
66830
+ quote_approval: quoteApproval(api, s),
65908
66831
  // What a filter matched, if any. Mastodon's clients read this and do the
65909
66832
  // hiding or warning; they do NOT match keywords themselves — Phanpy does
65910
66833
  // not — so filters that were stored and served but never applied were a
@@ -65991,7 +66914,9 @@ function pushNotify(api, n) {
65991
66914
  reblog: "boosted your post",
65992
66915
  follow: "followed you",
65993
66916
  "follow-request": "asked to follow you",
65994
- move: "moved account"
66917
+ move: "moved account",
66918
+ reaction: `reacted ${n.emoji || ""} to your post`,
66919
+ quote: "quoted your post"
65995
66920
  };
65996
66921
  const s = n.noteId && api.store.getStatuses().find((x) => x.noteId === n.noteId);
65997
66922
  return api.push.notify(n, {
@@ -66072,10 +66997,14 @@ function relationship(api, actorUrl) {
66072
66997
  };
66073
66998
  }
66074
66999
  function notificationType(api, t) {
66075
- return t === "follow-request" ? "follow_request" : t;
67000
+ return t === "follow-request" ? "follow_request" : t === "reaction" ? "emoji_reaction" : t;
66076
67001
  }
66077
67002
  function notification(api, n) {
66078
67003
  const out = { id: n.id, type: api.notificationType(n.type), created_at: n.at, account: api.account(n.actor) };
67004
+ if (n.type === "reaction") {
67005
+ out.emoji = n.emoji;
67006
+ if (n.url) out.emoji_url = n.url;
67007
+ }
66079
67008
  if (n.noteId) {
66080
67009
  const s = api.store.getStatuses().find((x) => x.noteId === n.noteId);
66081
67010
  if (s) out.status = api.status(s);
@@ -66120,7 +67049,6 @@ var STUBS = new Map(Object.entries({
66120
67049
  "/api/v1/custom_emojis": [],
66121
67050
  "/api/v1/announcements": [],
66122
67051
  "/api/v1/instance/peers": [],
66123
- "/api/v1/trends/tags": [],
66124
67052
  "/api/v1/trends/links": [],
66125
67053
  "/api/v2/suggestions": [],
66126
67054
  "/api/v1/preferences": {}
@@ -66157,14 +67085,15 @@ function instanceTitle(api) {
66157
67085
  const cfg = api.store.getConfig();
66158
67086
  return cfg?.handle ? `@${cfg.handle}@${api.host}` : "FediPod";
66159
67087
  }
66160
- function tagObject(api, name, following, req) {
67088
+ function tagObject(api, name, following, req, history = []) {
66161
67089
  const host = req?.headers?.host || api.host;
66162
- return { name, url: `https://${host}/tags/${name}`, history: [], following: !!following };
67090
+ return { name, url: `https://${host}/tags/${name}`, history, following: !!following };
66163
67091
  }
66164
67092
  function instanceBlurb(api) {
66165
67093
  const kind = api.store.getConfig()?.kind === "group" ? "group" : "actor";
66166
67094
  return `Solid pod ActivityPub ${kind}`;
66167
67095
  }
67096
+ var API_VERSIONS = { mastodon: 7 };
66168
67097
  async function handle2(api, ctx) {
66169
67098
  const { req, res, pathname, url, send } = ctx;
66170
67099
  if (pathname === "/api/v1/instance") {
@@ -66176,6 +67105,7 @@ async function handle2(api, ctx) {
66176
67105
  description: api.instanceBlurb(),
66177
67106
  email: "",
66178
67107
  version: "4.2.0 (compatible; fedipod)",
67108
+ api_versions: API_VERSIONS,
66179
67109
  urls: su ? { streaming_api: su } : {},
66180
67110
  stats: { user_count: 1, status_count: api.store.countStatuses(), domain_count: 1 },
66181
67111
  languages: ["en"],
@@ -66193,6 +67123,7 @@ async function handle2(api, ctx) {
66193
67123
  domain: api.host,
66194
67124
  title: api.instanceTitle(),
66195
67125
  version: "4.2.0 (compatible; fedipod)",
67126
+ api_versions: API_VERSIONS,
66196
67127
  source_url: "https://github.com/jeff-zucker/FediPod",
66197
67128
  description: api.instanceBlurb(),
66198
67129
  usage: { users: { active_month: 1 } },
@@ -66220,124 +67151,15 @@ async function handle2(api, ctx) {
66220
67151
  // lib/client/masto/accounts.mjs
66221
67152
  var accounts_exports = {};
66222
67153
  __export(accounts_exports, {
66223
- handle: () => handle4
67154
+ handle: () => handle3
66224
67155
  });
66225
67156
  init_node_crypto();
66226
67157
 
66227
67158
  // lib/pod/media.mjs
66228
67159
  var write6 = (pod, url, bytes, contentType) => pod.put(url, bytes, contentType);
66229
67160
 
66230
- // lib/client/masto/media.mjs
66231
- var media_exports = {};
66232
- __export(media_exports, {
66233
- attachmentType: () => attachmentType,
66234
- extensionFor: () => extensionFor,
66235
- handle: () => handle3,
66236
- readMultipart: () => readMultipart
66237
- });
66238
- init_node_crypto();
66239
- var ATTACHMENT_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio"]);
66240
- var NEVER = /* @__PURE__ */ new Set(["image/svg+xml", "image/svg"]);
66241
- var OPAQUE = "application/octet-stream";
66242
- function attachmentType(claimed) {
66243
- const t = String(claimed || "").split(";")[0].trim().toLowerCase();
66244
- if (!/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(t) || NEVER.has(t)) return OPAQUE;
66245
- return ATTACHMENT_KINDS.has(t.split("/")[0]) ? t : OPAQUE;
66246
- }
66247
- function extensionFor(mediaType, filename = "") {
66248
- if (mediaType === OPAQUE) return "bin";
66249
- const sub = mediaType.split("/")[1].replace(/[^a-z0-9]/g, "");
66250
- const given = String(filename || "").includes(".") ? filename.split(".").pop().toLowerCase().replace(/[^a-z0-9]/g, "") : "";
66251
- return given && (given === sub || sub.startsWith(given) || given.startsWith(sub)) ? given : sub || "bin";
66252
- }
66253
- function readMultipart(req, limit = 12e6) {
66254
- return new Promise((resolve2, reject) => {
66255
- const chunks = [];
66256
- let n = 0;
66257
- req.on("data", (c) => {
66258
- n += c.length;
66259
- if (n > limit) {
66260
- reject(new Error("upload too large"));
66261
- req.destroy();
66262
- return;
66263
- }
66264
- chunks.push(c);
66265
- });
66266
- req.on("error", reject);
66267
- req.on("end", () => {
66268
- try {
66269
- const m = /boundary=(?:"([^"]+)"|([^;]+))/.exec(String(req.headers["content-type"] || ""));
66270
- if (!m) return resolve2({ fields: {}, file: null });
66271
- const buf = Buffer.concat(chunks);
66272
- const boundary = Buffer.from("--" + (m[1] || m[2]).trim());
66273
- const fields = {};
66274
- const files = {};
66275
- let file = null;
66276
- let i = buf.indexOf(boundary);
66277
- while (i >= 0) {
66278
- const start = i + boundary.length;
66279
- if (buf.slice(start, start + 2).toString() === "--") break;
66280
- const next = buf.indexOf(boundary, start);
66281
- if (next < 0) break;
66282
- const part = buf.slice(start + 2, next - 2);
66283
- const sep = part.indexOf("\r\n\r\n");
66284
- if (sep >= 0) {
66285
- const head = part.slice(0, sep).toString();
66286
- const body = part.slice(sep + 4);
66287
- const name = /name="([^"]*)"/.exec(head)?.[1];
66288
- const filename = /filename="([^"]*)"/.exec(head)?.[1];
66289
- if (filename !== void 0) {
66290
- file = {
66291
- filename,
66292
- contentType: /content-type:\s*([^\r\n]+)/i.exec(head)?.[1]?.trim() || "application/octet-stream",
66293
- data: body
66294
- };
66295
- if (name) files[name] = file;
66296
- } else if (name) fields[name] = body.toString();
66297
- }
66298
- i = next;
66299
- }
66300
- resolve2({ fields, file, files });
66301
- } catch (e) {
66302
- reject(e);
66303
- }
66304
- });
66305
- });
66306
- }
66307
- async function handle3(api, ctx) {
66308
- const { req, res, pathname, url, send } = ctx;
66309
- if ((pathname === "/api/v2/media" || pathname === "/api/v1/media") && req.method === "POST") {
66310
- const { fields, file } = await readMultipart(req);
66311
- if (!file?.data?.length) return send(422, { error: "file required" });
66312
- const mediaType = attachmentType(file.contentType);
66313
- const ext = extensionFor(mediaType, file.filename);
66314
- const slug = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex") + "." + ext;
66315
- const mediaUrl = api.urls.media + slug;
66316
- await api.agent.publisher.ensureMediaContainer();
66317
- await write6(api.agent.remote, mediaUrl, file.data, mediaType);
66318
- const entry = { url: mediaUrl, mediaType, description: fields.description || "" };
66319
- const id = api.store.idFor(mediaUrl);
66320
- api.store.setMedia(id, entry);
66321
- return send(200, api.mediaJson({ id, ...entry }));
66322
- }
66323
- const mMedia = /^\/api\/v1\/media\/([a-f0-9]+)$/.exec(pathname);
66324
- if (mMedia) {
66325
- const entry = api.store.getMedia()[mMedia[1]];
66326
- if (!entry) return send(404, { error: "Record not found" });
66327
- if (req.method === "PUT") {
66328
- const body = await readBody2(req);
66329
- if (typeof body.description === "string") {
66330
- entry.description = body.description;
66331
- api.store.setMedia(mMedia[1], entry);
66332
- }
66333
- }
66334
- return send(200, api.mediaJson({ id: mMedia[1], ...entry }));
66335
- }
66336
- return false;
66337
- }
66338
-
66339
67161
  // lib/client/masto/accounts.mjs
66340
- async function handle4(api, ctx) {
67162
+ async function handle3(api, ctx) {
66341
67163
  const { req, res, pathname, url, send } = ctx;
66342
67164
  if (pathname === "/api/v1/accounts/verify_credentials") {
66343
67165
  const cfg0 = api.store.getConfig() || {};
@@ -66516,6 +67338,31 @@ async function handle4(api, ctx) {
66516
67338
  }
66517
67339
  }
66518
67340
  if (/^\/api\/v1\/accounts\/[a-f0-9]+\/featured_tags$/.test(pathname)) return send(200, []);
67341
+ if (pathname === "/api/v1/directory") return send(200, api.urls?.actor ? [api.account(api.urls.actor)] : []);
67342
+ if (pathname === "/api/v1/suggestions") return send(200, []);
67343
+ const mAccLists = /^\/api\/v1\/accounts\/([a-f0-9]+)\/lists$/.exec(pathname);
67344
+ if (mAccLists && req.method === "GET") {
67345
+ const actorUrl = api.store.urlFor(mAccLists[1]);
67346
+ if (!actorUrl) return send(404, { error: "Record not found" });
67347
+ return send(200, api.store.getLists().filter((l) => (l.members || []).includes(actorUrl)).map((l) => ({ id: l.id, title: l.title, replies_policy: l.repliesPolicy || "list", exclusive: false })));
67348
+ }
67349
+ if (pathname === "/api/v1/mutes" && req.method === "GET") {
67350
+ return send(200, (api.store.getMuted().actors || []).map((a) => api.account(a)));
67351
+ }
67352
+ if (pathname === "/api/v1/blocks" && req.method === "GET") {
67353
+ return send(200, (api.store.getBlocklist().actors || []).map((a) => api.account(a)));
67354
+ }
67355
+ if (pathname === "/api/v1/domain_blocks") {
67356
+ const b = api.store.getBlocklist();
67357
+ if (req.method === "GET") return send(200, [...b.domains]);
67358
+ const body = await readBody2(req);
67359
+ const domain = String(body.domain || url.searchParams.get("domain") || "").trim().toLowerCase();
67360
+ if (!/^[a-z0-9.-]+\.[a-z]{2,}$/u.test(domain)) return send(422, { error: "a domain is required" });
67361
+ if (req.method === "POST" && !b.domains.includes(domain)) b.domains.push(domain);
67362
+ if (req.method === "DELETE") b.domains = b.domains.filter((d) => d !== domain);
67363
+ api.store.setBlocklist(b);
67364
+ return send(200, {});
67365
+ }
66519
67366
  const mAccList = /^\/api\/v1\/accounts\/([a-f0-9]+)\/(following|followers)$/.exec(pathname);
66520
67367
  if (mAccList && req.method === "GET") {
66521
67368
  const actorUrl = api.store.urlFor(mAccList[1]);
@@ -66538,9 +67385,13 @@ async function handle4(api, ctx) {
66538
67385
  if (mAccStatuses) {
66539
67386
  const actorUrl = api.store.urlFor(mAccStatuses[1]);
66540
67387
  const all = api.store.getStatuses();
66541
- const pinnedOnly = url.searchParams.get("pinned") === "true";
67388
+ const q = url.searchParams;
67389
+ const is = (name) => q.get(name) === "true" || q.get(name) === "1";
67390
+ const pinnedOnly = is("pinned");
67391
+ const tagged = String(q.get("tagged") || "").replace(/^#/, "").toLowerCase();
67392
+ const isBoost = (s) => !!s.via && s.via !== s.actor;
66542
67393
  const { items, headers } = api.page(
66543
- all.filter((s) => s.actor === actorUrl && (!pinnedOnly || s.pinned)),
67394
+ all.filter((s) => s.actor === actorUrl && (!pinnedOnly || s.pinned)).filter((s) => !is("exclude_replies") || !s.inReplyTo).filter((s) => !is("exclude_reblogs") || !isBoost(s)).filter((s) => !is("only_media") || (s.attachments || []).length > 0).filter((s) => !tagged || (s.tags || []).some((t) => String(t.name).toLowerCase() === tagged)),
66544
67395
  url
66545
67396
  );
66546
67397
  return send(200, items.map((s) => api.statusOrBoost(s, { all })), headers);
@@ -66551,11 +67402,11 @@ async function handle4(api, ctx) {
66551
67402
  // lib/client/masto/timelines.mjs
66552
67403
  var timelines_exports = {};
66553
67404
  __export(timelines_exports, {
66554
- handle: () => handle5
67405
+ handle: () => handle4
66555
67406
  });
66556
67407
  init_node_crypto();
66557
67408
  init_wire();
66558
- async function handle5(api, ctx) {
67409
+ async function handle4(api, ctx) {
66559
67410
  const { req, res, pathname, url, send } = ctx;
66560
67411
  if (pathname === "/api/v1/timelines/home" || pathname === "/api/v1/timelines/public" || pathname === "/api/v1/trends/statuses") {
66561
67412
  const localOnly = pathname === "/api/v1/timelines/public" && url.searchParams.get("local") === "true";
@@ -66586,6 +67437,41 @@ async function handle5(api, ctx) {
66586
67437
  const following = (api.agent.tagfeed?.config().tags || []).includes(name);
66587
67438
  return send(200, api.tagObject(name, following, req));
66588
67439
  }
67440
+ if (pathname === "/api/v1/trends/tags" || pathname === "/api/v1/trends") {
67441
+ const DAY = 864e5;
67442
+ const today = Math.floor(Date.now() / DAY) * DAY;
67443
+ const seen = /* @__PURE__ */ new Map();
67444
+ for (const st2 of api.store.getStatuses()) {
67445
+ if (st2.kind !== "tag" || !st2.tag) continue;
67446
+ const at = Date.parse(st2.published || "");
67447
+ if (!Number.isFinite(at) || at < today - 6 * DAY) continue;
67448
+ const day = Math.floor(at / DAY) * DAY;
67449
+ if (!seen.has(st2.tag)) seen.set(st2.tag, /* @__PURE__ */ new Map());
67450
+ const days = seen.get(st2.tag);
67451
+ if (!days.has(day)) days.set(day, { uses: 0, accounts: /* @__PURE__ */ new Set() });
67452
+ const rec = days.get(day);
67453
+ rec.uses++;
67454
+ if (st2.actor) rec.accounts.add(st2.actor);
67455
+ }
67456
+ const followed = new Set(api.agent.tagfeed?.config().tags || []);
67457
+ const ranked = [...seen.entries()].map(([name, days]) => {
67458
+ const history = [];
67459
+ for (let i = 0; i < 7; i++) {
67460
+ const rec = days.get(today - i * DAY);
67461
+ history.push({
67462
+ day: String(Math.floor((today - i * DAY) / 1e3)),
67463
+ uses: String(rec ? rec.uses : 0),
67464
+ accounts: String(rec ? rec.accounts.size : 0)
67465
+ });
67466
+ }
67467
+ return {
67468
+ total: history.reduce((n, h) => n + Number(h.uses), 0),
67469
+ tag: api.tagObject(name, followed.has(name), req, history)
67470
+ };
67471
+ }).sort((a, b) => b.total - a.total);
67472
+ const limit = Math.min(Math.max(Number(url.searchParams.get("limit")) || 10, 1), 40);
67473
+ return send(200, ranked.slice(0, limit).map((x) => x.tag));
67474
+ }
66589
67475
  const mTagTl = pathname.match(/^\/api\/v1\/timelines\/tag\/([^/]+)$/);
66590
67476
  if (mTagTl && req.method === "GET") {
66591
67477
  const name = decodeURIComponent(mTagTl[1]).replace(/^#/, "").toLowerCase();
@@ -66722,7 +67608,81 @@ async function handle5(api, ctx) {
66722
67608
  last_status: api.status(c.last, { all })
66723
67609
  })));
66724
67610
  }
67611
+ if (pathname === "/api/v1/timelines/direct" && req.method === "GET") {
67612
+ const all = api.store.getStatuses();
67613
+ const items = all.filter((s) => s.direct || s.visibility === "direct");
67614
+ const { items: page2, headers } = api.page(items, url);
67615
+ return send(200, page2.map((s) => api.status(s, { all })), headers);
67616
+ }
67617
+ if (pathname === "/api/v1/notifications/clear" && req.method === "POST") {
67618
+ api.store.removeNotifications(() => true);
67619
+ return send(200, {});
67620
+ }
67621
+ const mNotif = /^\/api\/v1\/notifications\/([A-Za-z0-9_-]+)(\/dismiss)?$/.exec(pathname);
67622
+ if (mNotif) {
67623
+ const n = api.store.getNotifications().find((x) => x.id === mNotif[1]);
67624
+ if (!n) return send(404, { error: "Record not found" });
67625
+ if (mNotif[2] && req.method === "POST") {
67626
+ api.store.removeNotifications((x) => x.id === n.id);
67627
+ return send(200, {});
67628
+ }
67629
+ if (!mNotif[2] && req.method === "GET") return send(200, api.notification(n));
67630
+ }
67631
+ const mGroup = /^\/api\/v2\/notifications\/([A-Za-z0-9_-]+)(\/accounts|\/dismiss)?$/.exec(pathname);
67632
+ if (mGroup) {
67633
+ const n = api.store.getNotifications().find((x) => x.id === mGroup[1]);
67634
+ if (!n) return send(404, { error: "Record not found" });
67635
+ if (mGroup[2] === "/dismiss" && req.method === "POST") {
67636
+ api.store.removeNotifications((x) => x.id === n.id);
67637
+ return send(200, {});
67638
+ }
67639
+ if (mGroup[2] === "/accounts" && req.method === "GET") return send(200, [api.account(n.actor)]);
67640
+ if (!mGroup[2] && req.method === "GET") return send(200, groupedNotifications(api, [n]));
67641
+ }
67642
+ if (pathname === "/api/v2/notifications") {
67643
+ const { page: page2, headers } = notificationPage(api, ctx);
67644
+ return send(200, groupedNotifications(api, page2), headers);
67645
+ }
66725
67646
  if (pathname === "/api/v1/notifications") {
67647
+ const { page: page2, headers } = notificationPage(api, ctx);
67648
+ return send(200, page2.map((n) => api.notification(n)), headers);
67649
+ }
67650
+ if (pathname === "/api/v2/search" || pathname === "/api/v1/search") {
67651
+ return searchHandler(api, ctx);
67652
+ }
67653
+ return false;
67654
+ }
67655
+ function groupedNotifications(api, page2) {
67656
+ const accounts = /* @__PURE__ */ new Map();
67657
+ const statuses = /* @__PURE__ */ new Map();
67658
+ const groups = page2.map((n) => {
67659
+ const acct = api.account(n.actor);
67660
+ accounts.set(acct.id, acct);
67661
+ const s = n.noteId && api.store.getStatuses().find((x) => x.noteId === n.noteId);
67662
+ let statusId = null;
67663
+ if (s) {
67664
+ const j = api.status(s);
67665
+ statuses.set(j.id, j);
67666
+ statusId = j.id;
67667
+ }
67668
+ return {
67669
+ group_key: n.id,
67670
+ notifications_count: 1,
67671
+ type: api.notificationType(n.type),
67672
+ most_recent_notification_id: n.id,
67673
+ page_min_id: n.id,
67674
+ page_max_id: n.id,
67675
+ latest_page_notification_at: n.at,
67676
+ sample_account_ids: [acct.id],
67677
+ ...statusId ? { status_id: statusId } : {},
67678
+ ...n.type === "reaction" ? { emoji: n.emoji, ...n.url ? { emoji_url: n.url } : {} } : {}
67679
+ };
67680
+ });
67681
+ return { accounts: [...accounts.values()], statuses: [...statuses.values()], notification_groups: groups };
67682
+ }
67683
+ function notificationPage(api, ctx) {
67684
+ const { req, pathname, url } = ctx;
67685
+ {
66726
67686
  const limit = Math.min(Number(url.searchParams.get("limit")) || 30, 60);
66727
67687
  const q = url.searchParams;
66728
67688
  let items = api.store.getNotifications();
@@ -66753,13 +67713,16 @@ async function handle5(api, ctx) {
66753
67713
  for (const [k, v] of Object.entries(params)) u.searchParams.set(k, v);
66754
67714
  return u.href;
66755
67715
  };
66756
- return send(200, page2.map((n) => api.notification(n)), {
67716
+ return { page: page2, headers: {
66757
67717
  link: `<${link({ max_id: page2[page2.length - 1].id })}>; rel="next", <${link({ min_id: page2[0].id })}>; rel="prev"`
66758
- });
67718
+ } };
66759
67719
  }
66760
- return send(200, []);
67720
+ return { page: [], headers: void 0 };
66761
67721
  }
66762
- if (pathname === "/api/v2/search" || pathname === "/api/v1/search") {
67722
+ }
67723
+ async function searchHandler(api, ctx) {
67724
+ const { req, res, pathname, url, send } = ctx;
67725
+ {
66763
67726
  const q = String(url.searchParams.get("q") || "").trim();
66764
67727
  const type = url.searchParams.get("type");
66765
67728
  const out = { accounts: [], statuses: [], hashtags: [] };
@@ -66806,10 +67769,10 @@ async function handle5(api, ctx) {
66806
67769
  // lib/client/masto/statuses.mjs
66807
67770
  var statuses_exports = {};
66808
67771
  __export(statuses_exports, {
66809
- handle: () => handle6
67772
+ handle: () => handle5
66810
67773
  });
66811
67774
  init_node_crypto();
66812
- async function handle6(api, ctx) {
67775
+ async function handle5(api, ctx) {
66813
67776
  const { req, res, pathname, url, send } = ctx;
66814
67777
  if (pathname === "/api/v1/statuses" && req.method === "POST") {
66815
67778
  const body = await readBody2(req);
@@ -66832,8 +67795,22 @@ async function handle6(api, ctx) {
66832
67795
  const mediaIds = [].concat(body.media_ids || body["media_ids[]"] || []).filter(Boolean);
66833
67796
  const media = api.store.getMedia();
66834
67797
  const attachments = mediaIds.map((id) => media[id] && { id, ...media[id] }).filter(Boolean);
67798
+ let quote = null;
67799
+ if (body.quoted_status_id) {
67800
+ const quotedUrl = api.store.urlFor(String(body.quoted_status_id));
67801
+ const quoted = quotedUrl && api.store.getStatuses().find((x) => x.noteId === quotedUrl);
67802
+ if (!quoted) return send(404, { error: "Record not found" });
67803
+ if (["bsky", "acct"].includes(quoted.kind)) return send(422, { error: "that post cannot be quoted from this account" });
67804
+ if (visibility === "private" || visibility === "direct") return send(422, { error: "a quote post is public or unlisted" });
67805
+ const may = api.quoteApproval(quoted).current_user;
67806
+ if (may === "denied") return send(422, { error: "the author of that post does not allow quotes" });
67807
+ if (may === "unknown") return send(422, { error: "the author of that post has not said whether it may be quoted" });
67808
+ if (body.scheduled_at) return send(422, { error: "a quote cannot be scheduled" });
67809
+ quote = { id: quoted.noteId, actor: quoted.actor };
67810
+ }
66835
67811
  const asking = pollParams(body);
66836
67812
  if (asking) {
67813
+ if (quote) return send(422, { error: "a poll cannot quote a post" });
66837
67814
  if (attachments.length) return send(422, { error: "a poll cannot carry media" });
66838
67815
  if (body.scheduled_at) return send(422, { error: "a poll cannot be scheduled" });
66839
67816
  if (asking.options.length < 2) return send(422, { error: "a poll needs at least two options" });
@@ -66889,7 +67866,7 @@ async function handle6(api, ctx) {
66889
67866
  try {
66890
67867
  note = await api.agent.publisher.publishNote(
66891
67868
  body.status,
66892
- { inReplyTo, attachments, visibility, spoilerText, sensitive }
67869
+ { inReplyTo, attachments, visibility, spoilerText, sensitive, quote }
66893
67870
  );
66894
67871
  } catch (e) {
66895
67872
  if (e.code === "unaddressed") return send(422, { error: e.message });
@@ -66898,6 +67875,9 @@ async function handle6(api, ctx) {
66898
67875
  const s = api.store.getStatuses().find((x) => x.noteId === note.id);
66899
67876
  return send(200, api.status(s));
66900
67877
  }
67878
+ if (/^\/api\/v1\/statuses\/[a-f0-9]+\/interaction_policy$/.test(pathname) && req.method === "PUT") {
67879
+ return send(422, { error: "the quote policy is the same for every post here: anyone may quote a public or unlisted post, nobody a followers-only or direct one" });
67880
+ }
66901
67881
  const mSource = /^\/api\/v1\/statuses\/([a-f0-9]+)\/source$/.exec(pathname);
66902
67882
  if (mSource && req.method === "GET") {
66903
67883
  const noteUrl = api.store.urlFor(mSource[1]);
@@ -67106,6 +68086,60 @@ async function handle6(api, ctx) {
67106
68086
  return false;
67107
68087
  }
67108
68088
 
68089
+ // lib/client/masto/media.mjs
68090
+ var media_exports2 = {};
68091
+ __export(media_exports2, {
68092
+ attachmentType: () => attachmentType,
68093
+ extensionFor: () => extensionFor,
68094
+ handle: () => handle6
68095
+ });
68096
+ init_node_crypto();
68097
+ var ATTACHMENT_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio"]);
68098
+ var NEVER = /* @__PURE__ */ new Set(["image/svg+xml", "image/svg"]);
68099
+ var OPAQUE = "application/octet-stream";
68100
+ function attachmentType(claimed) {
68101
+ const t = String(claimed || "").split(";")[0].trim().toLowerCase();
68102
+ if (!/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(t) || NEVER.has(t)) return OPAQUE;
68103
+ return ATTACHMENT_KINDS.has(t.split("/")[0]) ? t : OPAQUE;
68104
+ }
68105
+ function extensionFor(mediaType, filename = "") {
68106
+ if (mediaType === OPAQUE) return "bin";
68107
+ const sub = mediaType.split("/")[1].replace(/[^a-z0-9]/g, "");
68108
+ const given = String(filename || "").includes(".") ? filename.split(".").pop().toLowerCase().replace(/[^a-z0-9]/g, "") : "";
68109
+ return given && (given === sub || sub.startsWith(given) || given.startsWith(sub)) ? given : sub || "bin";
68110
+ }
68111
+ async function handle6(api, ctx) {
68112
+ const { req, res, pathname, url, send } = ctx;
68113
+ if ((pathname === "/api/v2/media" || pathname === "/api/v1/media") && req.method === "POST") {
68114
+ const { fields, file } = await readMultipart(req);
68115
+ if (!file?.data?.length) return send(422, { error: "file required" });
68116
+ const mediaType = attachmentType(file.contentType);
68117
+ const ext = extensionFor(mediaType, file.filename);
68118
+ const slug = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex") + "." + ext;
68119
+ const mediaUrl = api.urls.media + slug;
68120
+ await api.agent.publisher.ensureMediaContainer();
68121
+ await write6(api.agent.remote, mediaUrl, file.data, mediaType);
68122
+ const entry = { url: mediaUrl, mediaType, description: fields.description || "" };
68123
+ const id = api.store.idFor(mediaUrl);
68124
+ api.store.setMedia(id, entry);
68125
+ return send(200, api.mediaJson({ id, ...entry }));
68126
+ }
68127
+ const mMedia = /^\/api\/v1\/media\/([a-f0-9]+)$/.exec(pathname);
68128
+ if (mMedia) {
68129
+ const entry = api.store.getMedia()[mMedia[1]];
68130
+ if (!entry) return send(404, { error: "Record not found" });
68131
+ if (req.method === "PUT") {
68132
+ const body = await readBody2(req);
68133
+ if (typeof body.description === "string") {
68134
+ entry.description = body.description;
68135
+ api.store.setMedia(mMedia[1], entry);
68136
+ }
68137
+ }
68138
+ return send(200, api.mediaJson({ id: mMedia[1], ...entry }));
68139
+ }
68140
+ return false;
68141
+ }
68142
+
67109
68143
  // lib/client/masto/index.mjs
67110
68144
  var MastoApi = class _MastoApi {
67111
68145
  constructor({
@@ -67114,6 +68148,7 @@ var MastoApi = class _MastoApi {
67114
68148
  allowed = null,
67115
68149
  scheme = null,
67116
68150
  embedded = false,
68151
+ throughDoor = null,
67117
68152
  mount = "",
67118
68153
  streaming = true,
67119
68154
  webPush = true,
@@ -67124,6 +68159,7 @@ var MastoApi = class _MastoApi {
67124
68159
  this.embedded = embedded;
67125
68160
  this.log = log2;
67126
68161
  this.allowed = allowed;
68162
+ this.throughDoor = throughDoor;
67127
68163
  this.scheme = scheme;
67128
68164
  this.streaming = streaming;
67129
68165
  this.webPush = webPush;
@@ -67202,7 +68238,7 @@ var MastoApi = class _MastoApi {
67202
68238
  const took = await this.agent.requestTakeover?.();
67203
68239
  if (!took) return send(503, { error: "another agent is active for this pod \u2014 takeover failed, try again" });
67204
68240
  }
67205
- for (const area of [accounts_exports, timelines_exports, statuses_exports, media_exports]) {
68241
+ for (const area of [accounts_exports, timelines_exports, statuses_exports, media_exports2]) {
67206
68242
  if (await area.handle(this, ctx)) return true;
67207
68243
  }
67208
68244
  this.log(`mastoapi: unhandled ${req.method} ${pathname} \u2014 punch list`);
@@ -67273,6 +68309,9 @@ var MastoApi = class _MastoApi {
67273
68309
  status(...a) {
67274
68310
  return status(this, ...a);
67275
68311
  }
68312
+ quoteApproval(...a) {
68313
+ return quoteApproval(this, ...a);
68314
+ }
67276
68315
  filtersFor(...a) {
67277
68316
  return filtersFor(this, ...a);
67278
68317
  }
@@ -67322,11 +68361,32 @@ var MastoApi = class _MastoApi {
67322
68361
  var MAX_TIMELINE_BYTES = 2 * 1024 * 1024;
67323
68362
  var DEFAULTS2 = {
67324
68363
  instance: "https://mastodon.social",
67325
- tags: ["solidproject", "linkeddata", "rdf"],
68364
+ tags: [
68365
+ "fediverse",
68366
+ "activitypub",
68367
+ // the network this account is on
68368
+ "opensource",
68369
+ "foss",
68370
+ // how the software gets made
68371
+ "solidproject",
68372
+ "linkeddata",
68373
+ // where the data lives
68374
+ "privacy",
68375
+ "digitalrights",
68376
+ // what is at stake
68377
+ "indieweb",
68378
+ "smallweb",
68379
+ // publishing for yourself
68380
+ "commons",
68381
+ "platformcooperativism"
68382
+ // owning it together
68383
+ ],
67326
68384
  intervalMin: 15
67327
68385
  };
67328
68386
  var PER_TAG = 20;
67329
- var MAX_NEW_PER_SWEEP = 12;
68387
+ var MAX_NEW_PER_SWEEP = 8;
68388
+ var MAX_NEW_PER_TAG = 2;
68389
+ var TAGS_PER_SWEEP = 4;
67330
68390
  var MAX_TAG_ENTRIES = 200;
67331
68391
  var BACKOFF_MIN_MS2 = 15 * 6e4;
67332
68392
  var BACKOFF_MAX_MS2 = 6 * 60 * 6e4;
@@ -67336,6 +68396,7 @@ var TagFeed = class {
67336
68396
  Object.assign(this, { store, intake, log: log2, fetcher: fetcher2 });
67337
68397
  this.lastSweep = null;
67338
68398
  this.lastAdded = 0;
68399
+ this.cursor = 0;
67339
68400
  }
67340
68401
  config() {
67341
68402
  return { ...DEFAULTS2, ...this.store.read("tagfeed.json", {}) };
@@ -67393,9 +68454,13 @@ var TagFeed = class {
67393
68454
  const known2 = new Set(this.store.getStatuses().map((s) => s.noteId));
67394
68455
  let budget = MAX_NEW_PER_SWEEP;
67395
68456
  let added = 0;
68457
+ const start = this.cursor % tags.length;
68458
+ this.cursor = (start + TAGS_PER_SWEEP) % tags.length;
68459
+ const order = [...tags, ...tags].slice(start, start + Math.min(TAGS_PER_SWEEP, tags.length));
67396
68460
  this.store.hold?.();
67397
68461
  try {
67398
- for (const tag of tags) {
68462
+ for (const tag of order) {
68463
+ let perTag = MAX_NEW_PER_TAG;
67399
68464
  let list3;
67400
68465
  try {
67401
68466
  const { safeFetch: safeFetch2, retryAfterMs: retryAfterMs3, readCapped: readCapped3 } = await Promise.resolve().then(() => (init_safefetch(), safefetch_exports));
@@ -67415,7 +68480,9 @@ var TagFeed = class {
67415
68480
  for (const st2 of Array.isArray(list3) ? list3 : []) {
67416
68481
  const noteId = st2?.uri;
67417
68482
  if (!noteId || known2.has(noteId) || this.store.isBlocked(noteId)) continue;
67418
- if (budget-- <= 0) break;
68483
+ if (perTag <= 0 || budget <= 0) break;
68484
+ perTag--;
68485
+ budget--;
67419
68486
  const note = await this.intake.fetchAP(noteId).catch(() => null);
67420
68487
  if (!note || note.id !== noteId || !isContentType(note.type)) continue;
67421
68488
  const author = authorOf(note);
@@ -67439,6 +68506,7 @@ var TagFeed = class {
67439
68506
  known2.add(noteId);
67440
68507
  added++;
67441
68508
  }
68509
+ if (budget <= 0) break;
67442
68510
  }
67443
68511
  const all = this.store.getStatuses();
67444
68512
  const tagged = all.filter((s) => s.kind === "tag");
@@ -67450,7 +68518,7 @@ var TagFeed = class {
67450
68518
  this.store.release?.();
67451
68519
  }
67452
68520
  this.lastAdded = added;
67453
- if (added) this.log(`tagfeed: +${added} from #${tags.join(" #")}`);
68521
+ if (added) this.log(`tagfeed: +${added} from #${order.join(" #")}`);
67454
68522
  }
67455
68523
  };
67456
68524
 
@@ -67743,7 +68811,7 @@ var PodTransport = class {
67743
68811
  // leaves the private trees world-readable. $rdf.sym() also throws on an
67744
68812
  // illegal IRI, so a pod URL with something odd in it fails here rather than
67745
68813
  // silently producing a document that means something else.
67746
- aclDoc(targetUrl, publicModes, { appendAgents = [], aclUrl = null } = {}) {
68814
+ aclDoc(targetUrl, publicModes, { appendAgents = [], readAgents = [], aclUrl = null } = {}) {
67747
68815
  const url = aclUrl || targetUrl + ".acl";
67748
68816
  const doc = namedNode2(url);
67749
68817
  const target = namedNode2(targetUrl);
@@ -67759,6 +68827,7 @@ var PodTransport = class {
67759
68827
  authorize(namedNode2(url + "#public"), ACL("agentClass"), FOAF("Agent"), publicModes);
67760
68828
  }
67761
68829
  appendAgents.forEach((webId, i) => authorize(namedNode2(url + `#gw${i}`), ACL("agent"), namedNode2(webId), ["Append"]));
68830
+ readAgents.forEach((webId, i) => authorize(namedNode2(url + `#r${i}`), ACL("agent"), namedNode2(webId), ["Read"]));
67762
68831
  authorize(
67763
68832
  namedNode2(url + "#owner"),
67764
68833
  ACL("agent"),
@@ -67948,61 +69017,6 @@ var BrowserRemotePod = class extends PodTransport {
67948
69017
  }
67949
69018
  };
67950
69019
 
67951
- // web/app/idb-kv.mjs
67952
- var DB = "fedipod-accounts";
67953
- function open() {
67954
- return new Promise((res, rej) => {
67955
- const r = indexedDB.open(DB, 1);
67956
- r.onupgradeneeded = () => {
67957
- const db = r.result;
67958
- if (!db.objectStoreNames.contains("kv")) db.createObjectStore("kv");
67959
- };
67960
- r.onsuccess = () => res(r.result);
67961
- r.onerror = () => rej(r.error);
67962
- });
67963
- }
67964
- async function kvGet(key) {
67965
- const db = await open();
67966
- return new Promise((res, rej) => {
67967
- const rq = db.transaction("kv", "readonly").objectStore("kv").get(key);
67968
- rq.onsuccess = () => res(rq.result ?? null);
67969
- rq.onerror = () => rej(rq.error);
67970
- });
67971
- }
67972
- async function kvAll() {
67973
- const db = await open();
67974
- return new Promise((res, rej) => {
67975
- const out = {};
67976
- const cur = db.transaction("kv", "readonly").objectStore("kv").openCursor();
67977
- cur.onsuccess = () => {
67978
- const c = cur.result;
67979
- if (c) {
67980
- out[c.key] = c.value;
67981
- c.continue();
67982
- } else res(out);
67983
- };
67984
- cur.onerror = () => rej(cur.error);
67985
- });
67986
- }
67987
- async function kvPut(key, val) {
67988
- const db = await open();
67989
- return new Promise((res, rej) => {
67990
- const tx = db.transaction("kv", "readwrite");
67991
- tx.objectStore("kv").put(val, key);
67992
- tx.oncomplete = () => res();
67993
- tx.onerror = () => rej(tx.error);
67994
- });
67995
- }
67996
- async function kvDel(key) {
67997
- const db = await open();
67998
- return new Promise((res, rej) => {
67999
- const tx = db.transaction("kv", "readwrite");
68000
- tx.objectStore("kv").delete(key);
68001
- tx.oncomplete = () => res();
68002
- tx.onerror = () => rej(tx.error);
68003
- });
68004
- }
68005
-
68006
69020
  // web/app/keystore.mjs
68007
69021
  var PEM = (der, label) => {
68008
69022
  const b642 = btoa(String.fromCharCode(...new Uint8Array(der)));
@@ -68034,18 +69048,6 @@ async function generateKeys() {
68034
69048
  }
68035
69049
  return rec;
68036
69050
  }
68037
- var PBKDF2_ITERATIONS = 31e4;
68038
- async function deriveAesKey(password, salt, iterations) {
68039
- const base = await crypto.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveKey"]);
68040
- return crypto.subtle.deriveKey(
68041
- { name: "PBKDF2", salt, iterations, hash: "SHA-256" },
68042
- base,
68043
- { name: "AES-GCM", length: 256 },
68044
- false,
68045
- ["encrypt", "decrypt"]
68046
- );
68047
- }
68048
- var toB64 = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)));
68049
69051
  function isKeyEnvelope(doc) {
68050
69052
  return !!doc && doc.v === 1 && typeof doc.ct === "string" && typeof doc.salt === "string";
68051
69053
  }
@@ -68055,21 +69057,6 @@ var KeyPasswordNeeded = class extends Error {
68055
69057
  }
68056
69058
  code = "key-password-needed";
68057
69059
  };
68058
- async function wrapKeys(keysRecord, password) {
68059
- const salt = crypto.getRandomValues(new Uint8Array(16));
68060
- const iv = crypto.getRandomValues(new Uint8Array(12));
68061
- const aes = await deriveAesKey(password, salt, PBKDF2_ITERATIONS);
68062
- const plaintext = new TextEncoder().encode(JSON.stringify(keysRecord));
68063
- const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, aes, plaintext);
68064
- return {
68065
- v: 1,
68066
- kdf: "PBKDF2-SHA256",
68067
- iterations: PBKDF2_ITERATIONS,
68068
- salt: toB64(salt),
68069
- iv: toB64(iv),
68070
- ct: toB64(ct)
68071
- };
68072
- }
68073
69060
 
68074
69061
  // web/app/keys-browser.mjs
68075
69062
  var pemToDer = (pem) => Uint8Array.from(
@@ -68101,7 +69088,7 @@ async function loadKeysFromPod(remote, urls) {
68101
69088
  const cached = await kvGet(keyCacheKey(podActor)).catch(() => null);
68102
69089
  if (isOpenedKey(cached)) return fromCache(cached);
68103
69090
  if (cached?.rsa?.privatePem) return cacheOpenedKeys(podActor, cached);
68104
- const doc = await readWrappedKeys(remote, urls);
69091
+ const doc = await readKeys(remote, urls);
68105
69092
  if (isKeyEnvelope(doc)) throw new KeyPasswordNeeded();
68106
69093
  if (!doc || !doc.rsa) throw new Error("no signing key on the pod \u2014 sign up did not finish");
68107
69094
  return cacheOpenedKeys(podActor, doc);
@@ -68921,6 +69908,7 @@ var ADMIN_PATHS = /* @__PURE__ */ new Set([
68921
69908
  "/log",
68922
69909
  "/fediacct",
68923
69910
  "/describe",
69911
+ "/follow",
68924
69912
  "/atproto",
68925
69913
  "/atproto/connect",
68926
69914
  "/atproto/disconnect",
@@ -68930,6 +69918,9 @@ var ADMIN_PATHS = /* @__PURE__ */ new Set([
68930
69918
  "/inbox/prune",
68931
69919
  "/park",
68932
69920
  "/revive",
69921
+ "/takeover",
69922
+ "/gateway/pause",
69923
+ "/gateway/close",
68933
69924
  "/fediacct/connect",
68934
69925
  "/fediacct/disconnect",
68935
69926
  "/fediacct/callback"
@@ -69034,7 +70025,11 @@ var AdminFacade = class {
69034
70025
  frontActor: g?.frontActor || null,
69035
70026
  mode: g?.mode || "off",
69036
70027
  hasSecret: !!g?.hmacSecret,
69037
- stats: a.store.read("gateway-stats.json", { verified: 0, unverified: 0, lastAt: null })
70028
+ stats: a.store.read("gateway-stats.json", { verified: 0, unverified: 0, lastAt: null }),
70029
+ // The account's standing at the gateway — paused, closed — as the
70030
+ // gateway last said it (agent.mjs tellGateway). The record page
70031
+ // shows its pause and close controls only when this is here.
70032
+ standing: a.gatewayStanding || null
69038
70033
  });
69039
70034
  }
69040
70035
  case "/deadletter":
@@ -69155,6 +70150,47 @@ var AdminFacade = class {
69155
70150
  await a.publisher.publishProfile();
69156
70151
  return json2(200, { ok: true, summary: cfg.summary || null, icon: cfg.icon || null });
69157
70152
  }
70153
+ // Following somebody, the same call the Node agent's admin surface takes
70154
+ // (lib/device/admin/routes/social.mjs): by handle normally, by actor url
70155
+ // when WebFinger cannot answer for them. The remote-follow page
70156
+ // (/authorize_interaction) is what asks for it here — another server
70157
+ // sends its reader to that page when they press Follow and say they are
70158
+ // at this host.
70159
+ case "/follow": {
70160
+ await a.requestTakeover?.();
70161
+ if (body.actor) {
70162
+ await followActor(a, String(body.actor));
70163
+ return json2(200, { ok: true, actor: String(body.actor) });
70164
+ }
70165
+ if (!body.handle) return json2(400, { error: "handle or actor required" });
70166
+ return json2(200, await followHandle(a, String(body.handle)));
70167
+ }
70168
+ // Taking the account back. A viewer publishes nothing and drains
70169
+ // nothing, so an account whose lease is held by something that is not
70170
+ // running any more stays silent with no way to say so. The owner acting
70171
+ // HERE outranks whatever holds it.
70172
+ case "/takeover": {
70173
+ if (!await a.requestTakeover?.()) return json2(503, { error: "the lease could not be taken \u2014 the pod refused the write" });
70174
+ return json2(200, { ok: true, mode: a.status().mode });
70175
+ }
70176
+ // The account's standing at its gateway: paused by its owner, or closed
70177
+ // for good (front-core: accounts that go quiet). Both go to the gateway
70178
+ // with the pod session as proof, and its answer is the new standing.
70179
+ case "/gateway/pause": {
70180
+ if (typeof body.paused !== "boolean") return json2(400, { error: "paused must be true or false" });
70181
+ if (!a.pauseAtGateway || !a.gatewayApi) return json2(501, { error: "this account is not at a gateway" });
70182
+ const r = await a.pauseAtGateway(body.paused);
70183
+ if (!r) return json2(502, { error: "the gateway could not be reached" });
70184
+ return json2(r.status === 200 ? 200 : r.status, r);
70185
+ }
70186
+ case "/gateway/close": {
70187
+ const handle7 = String(a.store.getConfig()?.handle || "").toLowerCase();
70188
+ if (!handle7 || String(body.confirm || "").toLowerCase() !== handle7) return json2(400, { error: "type the handle to confirm" });
70189
+ if (!a.closeAtGateway || !a.gatewayApi) return json2(501, { error: "this account is not at a gateway" });
70190
+ const r = await a.closeAtGateway();
70191
+ if (!r) return json2(502, { error: "the gateway could not be reached" });
70192
+ return json2(r.status === 200 ? 200 : r.status, r);
70193
+ }
69158
70194
  // Going quiet, and coming back. The record page's active/parked select.
69159
70195
  case "/park": {
69160
70196
  if (!await a.requestTakeover?.()) return json2(503, { error: "another device is active for this pod \u2014 park from there" });
@@ -69166,13 +70202,8 @@ var AdminFacade = class {
69166
70202
  }
69167
70203
  case "/rotate-key": {
69168
70204
  await a.requestTakeover?.();
69169
- try {
69170
- const r = await a.rotateKey({ password: body.password });
69171
- return json2(200, { ok: true, changed: !!r?.changed });
69172
- } catch (e) {
69173
- if (e.code !== "key-password-needed") throw e;
69174
- return json2(428, { error: e.message, needsPassword: true });
69175
- }
70205
+ const r = await a.rotateKey();
70206
+ return json2(200, { ok: true, changed: !!r?.changed });
69176
70207
  }
69177
70208
  // Recover posts this browser lost, from what the pod still holds.
69178
70209
  case "/rebuild": {
@@ -69471,6 +70502,60 @@ var AdminFacade = class {
69471
70502
  }
69472
70503
  };
69473
70504
 
70505
+ // web/app/gateway-move.mjs
70506
+ init_wire();
70507
+ async function completeGatewayMove(agent2) {
70508
+ const cfg = agent2.store.getConfig();
70509
+ const mv = cfg?.movedFrom;
70510
+ if (!mv || mv.completedAt) return null;
70511
+ const log2 = agent2.log;
70512
+ const newActor = agent2.urls.actor;
70513
+ let told;
70514
+ try {
70515
+ told = await agent2.sessionFetch(`${mv.gateway}/api/move`, {
70516
+ method: "POST",
70517
+ headers: { "content-type": "application/json" },
70518
+ body: JSON.stringify({ handle: mv.handle, movedTo: newActor })
70519
+ });
70520
+ } catch (e) {
70521
+ told = { status: 0, statusText: e.message };
70522
+ }
70523
+ if (told.status !== 200) {
70524
+ log2(`gateway move: ${mv.gateway} did not mark @${mv.handle} as moved (${told.status || told.statusText}) \u2014 will retry`);
70525
+ return { told: false };
70526
+ }
70527
+ const contacts = agent2.store.getContacts();
70528
+ const inboxes = [...new Set(contacts.followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean))];
70529
+ const activity = moveActivity({ actor: mv.actor }, newActor, Date.now());
70530
+ const sender = new RelayDeliverer({
70531
+ passive: true,
70532
+ store: agent2.store,
70533
+ rsaPrivate: agent2.deliverer.rsaPrivate,
70534
+ keyId: `${mv.actor}#main-key`,
70535
+ actorId: mv.actor,
70536
+ log: log2,
70537
+ relayUrl: `${mv.gateway}/api/relay`,
70538
+ handle: mv.handle,
70539
+ sessionFetch: agent2.sessionFetch
70540
+ });
70541
+ let sent = 0;
70542
+ let failed = 0;
70543
+ for (const inbox of inboxes) {
70544
+ try {
70545
+ await sender.deliverNow(inbox, activity);
70546
+ sent++;
70547
+ } catch (e) {
70548
+ failed++;
70549
+ log2(`gateway move: Move to ${inbox} failed: ${e.message}`);
70550
+ }
70551
+ }
70552
+ const done = { ...mv, completedAt: (/* @__PURE__ */ new Date()).toISOString(), moveSent: sent, moveFailed: failed };
70553
+ agent2.store.setConfig({ ...agent2.store.getConfig(), movedFrom: done });
70554
+ await agent2.store.flush?.();
70555
+ log2(`moved from ${mv.actor} to ${newActor}: Move sent to ${sent} inbox(es)${failed ? `, ${failed} failed` : ""}`);
70556
+ return done;
70557
+ }
70558
+
69474
70559
  // stub:node:os
69475
70560
  var fail2 = () => {
69476
70561
  throw new Error("node:os is not available in the browser agent");
@@ -70582,6 +71667,48 @@ var originAuthorities = (host) => ({
70582
71667
  }
70583
71668
  // fetch-only: this build serves no socket
70584
71669
  });
71670
+ async function accountNotRead({ unread, podBase, state, webId, actorUrl = null, fetchImpl = globalThis.fetch }) {
71671
+ const host = (() => {
71672
+ try {
71673
+ return new URL(podBase).host;
71674
+ } catch {
71675
+ return podBase;
71676
+ }
71677
+ })();
71678
+ const of = (code, message) => {
71679
+ const e = new Error(message);
71680
+ e.code = code;
71681
+ return e;
71682
+ };
71683
+ if (!unread) {
71684
+ const actor = actorUrl ? await fetchImpl(actorUrl, { headers: { accept: "application/activity+json" } }).then((r) => r.ok ? r.json() : null).catch(() => null) : null;
71685
+ if (actor?.preferredUsername) {
71686
+ const at = (() => {
71687
+ try {
71688
+ return `@${actor.preferredUsername}@${new URL(actor.id || actorUrl).host}`;
71689
+ } catch {
71690
+ return actor.preferredUsername;
71691
+ }
71692
+ })();
71693
+ return of("device-account", `${host} holds the account ${at}, but not its record: this account is run by a DeviceAgent, and its record lives on that device. Open it from that device's admin page, not here. The sign-in used here was ${webId}.`);
71694
+ }
71695
+ return of("no-account", `${host} answered, and there is no FediPod account in it. Sign in with the pod that holds your account, or make an account in this one \u2014 both start from the sign-in page. The sign-in used here was ${webId}.`);
71696
+ }
71697
+ const status2 = Number((/HTTP (\d{3})/u.exec(unread.message) || [])[1]) || 0;
71698
+ if (status2 === 401 || status2 === 403) {
71699
+ return of("sign-in-refused", `${host} would not let this sign-in read your account \u2014 it answered "not allowed" (HTTP ${status2}) \u2014 and renewing the sign-in did not change its mind. Your account and everything in it are untouched. Signing in at ${host} again is the one thing that helps.`);
71700
+ }
71701
+ if (status2 === 429) {
71702
+ return of("pod-busy", `${host} is telling this browser it has asked for too much too quickly (HTTP 429), so your account could not be read. Nothing is wrong with your account, and nothing here needs doing: this page is waiting the throttle out and will ask again by itself. While a pod is rate-limiting you, only time helps.`);
71703
+ }
71704
+ if (status2 >= 500) {
71705
+ return of("pod-error", `${host} broke while handing your account over (HTTP ${status2}). That is the pod's own failure, not your account's. This page asks again by itself in a moment; if it keeps saying this, the pod is down and there is nothing to do here.`);
71706
+ }
71707
+ if (status2 === 404) {
71708
+ return of("no-account-here", `${host} says there is nothing at ${state} (HTTP 404). Either no FediPod account has ever been set up in this pod, or you signed in with a different pod than the one holding yours. Sign in with that one, or make an account in this one \u2014 both start from the sign-in page.`);
71709
+ }
71710
+ return of("pod-unreachable", `This browser could not reach ${host} to read your account (${unread.message}). Your account is still there. This page tries again by itself; if it keeps saying this, check that you are online and that the pod is up.`);
71711
+ }
70585
71712
  var BrowserAgent = class _BrowserAgent {
70586
71713
  constructor({ log: log2 = console.log } = {}) {
70587
71714
  this.log = log2;
@@ -70605,10 +71732,46 @@ var BrowserAgent = class _BrowserAgent {
70605
71732
  this.log("took over from the other device (action here)");
70606
71733
  return true;
70607
71734
  }
71735
+ // One call to the gateway's owner API, proved with the pod session. What
71736
+ // comes back on 200 or 410 is the account's standing there, kept for the
71737
+ // manage page; anything else is logged and forgotten.
71738
+ async tellGateway(what, body) {
71739
+ if (!this.gatewayApi) return null;
71740
+ let res;
71741
+ try {
71742
+ res = await this.sessionFetch(`${this.gatewayApi}/${what}`, {
71743
+ method: "POST",
71744
+ headers: { "content-type": "application/json" },
71745
+ body: JSON.stringify(body)
71746
+ });
71747
+ } catch (e) {
71748
+ this.log(`gateway ${what}: ${e.message}`);
71749
+ return null;
71750
+ }
71751
+ const json2 = await res.json().catch(() => ({}));
71752
+ const out = { status: res.status, ...json2 };
71753
+ if (res.status === 200 || res.status === 410) this.gatewayStanding = out;
71754
+ else this.log(`gateway ${what}: ${res.status} ${json2.error || ""}`);
71755
+ return out;
71756
+ }
71757
+ openAtGateway() {
71758
+ return this.tellGateway("open", { handle: this.doorKey });
71759
+ }
71760
+ pauseAtGateway(paused) {
71761
+ return this.tellGateway("pause", { handle: this.doorKey, paused: !!paused });
71762
+ }
71763
+ closeAtGateway() {
71764
+ return this.tellGateway("close", { handle: this.doorKey, confirm: true });
71765
+ }
71766
+ static OPEN_EVERY_MS = 60 * 6e4;
70608
71767
  // Become the active agent: renew the lease, then start what a viewer skips —
70609
71768
  // publish the face, drain the inbox, run the mirrors.
70610
71769
  async goActive() {
70611
71770
  this.viewer = false;
71771
+ clearInterval(this._openTimer);
71772
+ this._openTimer = setInterval(() => {
71773
+ this.openAtGateway();
71774
+ }, _BrowserAgent.OPEN_EVERY_MS);
70612
71775
  this.lease.onLost = () => this.demote();
70613
71776
  this.lease.startRenewal();
70614
71777
  try {
@@ -70618,6 +71781,7 @@ var BrowserAgent = class _BrowserAgent {
70618
71781
  this.deliverer?.startQueue?.();
70619
71782
  await this.publisher.publishProfile();
70620
71783
  await this.store.flush?.();
71784
+ await completeGatewayMove(this).catch((e) => this.log(`gateway move: ${e.message}`));
70621
71785
  await this.intake.start();
70622
71786
  this.startBsky();
70623
71787
  this.startAccts();
@@ -70634,6 +71798,8 @@ var BrowserAgent = class _BrowserAgent {
70634
71798
  this.viewer = true;
70635
71799
  this.log("another device took over \u2014 read-only here");
70636
71800
  this.lease.stopRenewal();
71801
+ clearInterval(this._openTimer);
71802
+ this._openTimer = null;
70637
71803
  this.intake?.stop?.();
70638
71804
  this.deliverer?.stop?.();
70639
71805
  this.importer?.stop?.();
@@ -70685,6 +71851,21 @@ var BrowserAgent = class _BrowserAgent {
70685
71851
  * - { credential, keysRecord, config }: a client-credential session with the
70686
71852
  * material in hand (the offline test path).
70687
71853
  */
71854
+ // This browser's name for itself, minted once and kept. Not an identity and
71855
+ // not a secret — it says only "the same browser as last time" to the lease.
71856
+ // If storage cannot be reached, a fresh one is honest: this browser cannot
71857
+ // prove it is the one that held the lease, so it should not claim to be.
71858
+ async deviceId() {
71859
+ try {
71860
+ const had = await kvGet("device-id");
71861
+ if (had) return had;
71862
+ const made = crypto.randomUUID();
71863
+ await kvPut("device-id", made);
71864
+ return made;
71865
+ } catch {
71866
+ return null;
71867
+ }
71868
+ }
70688
71869
  async boot({ oidc, credential, keysRecord, config, frontOrigin }) {
70689
71870
  let session;
70690
71871
  let webId;
@@ -70700,15 +71881,18 @@ var BrowserAgent = class _BrowserAgent {
70700
71881
  remotePod = credential.remotePod;
70701
71882
  }
70702
71883
  this.webId = webId;
71884
+ this.sessionFetch = session.fetch;
70703
71885
  const root = config && config.root || "fedipod/";
70704
71886
  this.remote = new BrowserRemotePod(session, { webId, log: this.log });
70705
71887
  this.urls = apUrls2(remotePod, root);
70706
71888
  const podFetch = (u, i) => this.remote.fetch(u, i);
70707
71889
  this.store = new PodStore({ storage: new HttpStorage(this.urls.state, podFetch), log: this.log });
70708
- await this.store.load().catch(() => {
71890
+ let unread = null;
71891
+ await this.store.load().catch((e) => {
71892
+ unread = e;
70709
71893
  });
70710
71894
  const cfg = config || this.store.getConfig();
70711
- if (!cfg) throw new Error("no account config on this pod \u2014 sign up first");
71895
+ if (!cfg) throw await accountNotRead({ unread, podBase: remotePod, state: this.urls.state, webId, actorUrl: this.urls.actor });
70712
71896
  this.store.setConfig({ ...this.store.getConfig() || {}, ...cfg, root });
70713
71897
  config = this.store.getConfig();
70714
71898
  const publicBase = config.gateway?.frontActor ? config.gateway.frontActor.replace(/ap\/actor\/?$/, "") : null;
@@ -70729,6 +71913,29 @@ var BrowserAgent = class _BrowserAgent {
70729
71913
  sessionFetch: session.fetch,
70730
71914
  onGone: () => this.publisher.publishCollections({ followers: true })
70731
71915
  });
71916
+ this.gatewayApi = null;
71917
+ try {
71918
+ if (config.gateway?.url) this.gatewayApi = `${new URL(config.gateway.url).origin}/api`;
71919
+ } catch {
71920
+ this.gatewayApi = null;
71921
+ }
71922
+ this.doorKey = doorKeyOf(config.gateway?.url) || config.handle;
71923
+ this.gatewayStanding = null;
71924
+ const standing = await this.openAtGateway();
71925
+ if (standing?.status === 410) {
71926
+ const host = (() => {
71927
+ try {
71928
+ return new URL(this.gatewayApi).host;
71929
+ } catch {
71930
+ return "the gateway";
71931
+ }
71932
+ })();
71933
+ const address = this.doorKey.includes("@") ? `@${this.doorKey}` : `@${this.doorKey}@${host}`;
71934
+ const why = standing.closedBy === "owner" ? "you closed it" : "nothing opened it for six months";
71935
+ const e = new Error(`${address} is closed at ${host}: ${why}, and a closed address does not come back. Everything on your pod is untouched. The sign-in used here was ${webId}.`);
71936
+ e.code = "address-closed";
71937
+ throw e;
71938
+ }
70732
71939
  this.publisher = new Publisher({
70733
71940
  config: this.store.getConfig(),
70734
71941
  remote: this.remote,
@@ -70746,7 +71953,17 @@ var BrowserAgent = class _BrowserAgent {
70746
71953
  this.atproto = new BrowserAtproto({ store: this.store, actorId: this.urls.actor, log: this.log });
70747
71954
  this.publisher.atproto = this.atproto;
70748
71955
  this.fediaccts = new BrowserFediAccounts({ store: this.store, actorId: this.urls.actor, log: this.log });
70749
- this.lease = new Lease({ url: this.urls.state + "lease.json", fetchImpl: podFetch, log: this.log });
71956
+ this.lease = new Lease({
71957
+ url: this.urls.state + "lease.json",
71958
+ fetchImpl: podFetch,
71959
+ log: this.log,
71960
+ // Kept on this origin, so THIS browser is one holder however many times
71961
+ // its worker is killed and restarted. Without it every restart was a new
71962
+ // holder: the old lease still had minutes to run, so the browser found
71963
+ // its own account "active on another device" and asked to take it over.
71964
+ // Switching between the clients did it every time, being a navigation.
71965
+ id: await this.deviceId()
71966
+ });
70750
71967
  this.c2s = new C2S({ agent: this, log: this.log });
70751
71968
  this.intake = new Intake({
70752
71969
  config: this.store.getConfig(),
@@ -70827,23 +72044,11 @@ var BrowserAgent = class _BrowserAgent {
70827
72044
  // actor so the new public key is on the wire. The old key stops signing the
70828
72045
  // moment this returns — same one-way change as the Node agent's rotateKey
70829
72046
  // (run-agent.mjs).
70830
- //
70831
- // The pod's copy is wrapped, so this needs the account password. There is
70832
- // nowhere to get it from without asking: the worker boots from a stored
70833
- // session and holds no password, and caching one to save a prompt on a
70834
- // once-in-a-while action would put the account password in storage to avoid
70835
- // typing it. So the caller supplies it, and rotating without one is refused
70836
- // rather than quietly writing a bare key back where a wrapped one was.
70837
- async rotateKey({ password } = {}) {
70838
- if (!password) {
70839
- const e = new Error("rotating the signing key needs your account password \u2014 it is what the new key is locked under on the pod");
70840
- e.code = "key-password-needed";
70841
- throw e;
70842
- }
72047
+ async rotateKey() {
70843
72048
  const before = this.publisher.publicKeyPem;
70844
72049
  const rec = await generateKeys();
70845
72050
  rec.mintedFor = this.urls.actor;
70846
- await writeWrappedKeys(this.remote, this.urls, await wrapKeys(rec, password));
72051
+ await writeKeys(this.remote, this.urls, rec);
70847
72052
  const keys = await cacheOpenedKeys(podActorOf(this.urls), rec);
70848
72053
  this.publisher.publicKeyPem = keys.rsaPublicPem;
70849
72054
  this.deliverer.rsaPrivate = keys.rsaPrivate;
@@ -70978,43 +72183,14 @@ var BrowserAgent = class _BrowserAgent {
70978
72183
  }
70979
72184
  };
70980
72185
 
70981
- // web/app/oidc-session.mjs
70982
- var DB2 = "fedipod-oidc";
72186
+ // lib/session/oidc-session.mjs
70983
72187
  var STORE = "session";
70984
72188
  var b64u3 = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
70985
72189
  var enc3 = (o) => b64u3(new TextEncoder().encode(JSON.stringify(o)));
70986
72190
  var sha2564 = (s) => crypto.subtle.digest("SHA-256", new TextEncoder().encode(s));
70987
- function idb() {
70988
- return new Promise((res, rej) => {
70989
- const r = indexedDB.open(DB2, 1);
70990
- r.onupgradeneeded = () => r.result.createObjectStore(STORE);
70991
- r.onsuccess = () => res(r.result);
70992
- r.onerror = () => rej(r.error);
70993
- });
70994
- }
70995
- async function idbGet(key) {
70996
- const db = await idb();
70997
- return new Promise((res, rej) => {
70998
- const t = db.transaction(STORE).objectStore(STORE).get(key);
70999
- t.onsuccess = () => res(t.result);
71000
- t.onerror = () => rej(t.error);
71001
- });
71002
- }
71003
- async function idbPut(key, val) {
71004
- const db = await idb();
71005
- return new Promise((res, rej) => {
71006
- const t = db.transaction(STORE, "readwrite").objectStore(STORE).put(val, key);
71007
- t.onsuccess = () => res();
71008
- t.onerror = () => rej(t.error);
71009
- });
71010
- }
71011
- async function idbDel(key) {
71012
- const db = await idb();
71013
- return new Promise((res, rej) => {
71014
- const t = db.transaction(STORE, "readwrite").objectStore(STORE).delete(key);
71015
- t.onsuccess = () => res();
71016
- t.onerror = () => rej(t.error);
71017
- });
72191
+ var rand = (n = 32) => b64u3(crypto.getRandomValues(new Uint8Array(n)));
72192
+ async function dpopKey() {
72193
+ return crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
71018
72194
  }
71019
72195
  async function publicJwk(pair) {
71020
72196
  const j = await crypto.subtle.exportKey("jwk", pair.publicKey);
@@ -71027,64 +72203,217 @@ async function dpopProof(pair, jwk, htm, htu, ath) {
71027
72203
  const sig = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, pair.privateKey, new TextEncoder().encode(data));
71028
72204
  return `${data}.${b64u3(sig)}`;
71029
72205
  }
71030
- async function getSession() {
71031
- const s = await idbGet("session");
71032
- return s ? sessionHandle(s) : null;
71033
- }
71034
- async function signOut() {
71035
- const s = await idbGet("session").catch(() => null);
71036
- if (s?.revocationEndpoint && s.refreshToken) {
71037
- try {
71038
- await fetch(s.revocationEndpoint, {
71039
- method: "POST",
71040
- headers: { "content-type": "application/x-www-form-urlencoded" },
71041
- body: new URLSearchParams({
71042
- token: s.refreshToken,
71043
- token_type_hint: "refresh_token",
71044
- client_id: s.client_id
71045
- })
71046
- });
71047
- } catch {
71048
- }
72206
+ var jwtPayload = (jwt) => {
72207
+ try {
72208
+ return JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(jwt.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0))));
72209
+ } catch {
72210
+ return {};
71049
72211
  }
71050
- await idbDel("session");
71051
- await idbDel("pending");
72212
+ };
72213
+ async function discover(issuer) {
72214
+ const res = await fetch(`${issuer.replace(/\/+$/, "")}/.well-known/openid-configuration`, { headers: { accept: "application/json" } });
72215
+ if (!res.ok) throw new Error(`OIDC discovery failed at ${issuer} (HTTP ${res.status})`);
72216
+ return res.json();
71052
72217
  }
71053
- function sessionHandle(s) {
71054
- let { accessToken, expiresAt, refreshToken } = s;
71055
- const jwkP = publicJwk(s.pair);
71056
- const refresh = async () => {
71057
- if (!refreshToken) throw new Error("session expired and there is no refresh token \u2014 sign in again");
71058
- const jwk = await jwkP;
71059
- const res = await fetch(s.tokenEndpoint, {
72218
+ async function registerClient(cfg, redirectUri, clientName) {
72219
+ const res = await fetch(cfg.registration_endpoint, {
72220
+ method: "POST",
72221
+ headers: { "content-type": "application/json" },
72222
+ body: JSON.stringify({
72223
+ client_name: clientName,
72224
+ redirect_uris: [redirectUri],
72225
+ token_endpoint_auth_method: "none",
72226
+ grant_types: ["authorization_code", "refresh_token"],
72227
+ response_types: ["code"],
72228
+ application_type: "web",
72229
+ scope: "openid webid offline_access"
72230
+ })
72231
+ });
72232
+ if (!res.ok) throw new Error(`client registration failed (HTTP ${res.status})`);
72233
+ return (await res.json()).client_id;
72234
+ }
72235
+ function solidOidcSession({ dbName = "solid-oidc-session", clientName = "Solid app" } = {}) {
72236
+ function idb() {
72237
+ return new Promise((res, rej) => {
72238
+ const r = indexedDB.open(dbName, 1);
72239
+ r.onupgradeneeded = () => r.result.createObjectStore(STORE);
72240
+ r.onsuccess = () => res(r.result);
72241
+ r.onerror = () => rej(r.error);
72242
+ });
72243
+ }
72244
+ async function idbGet(key) {
72245
+ const db = await idb();
72246
+ return new Promise((res, rej) => {
72247
+ const t = db.transaction(STORE).objectStore(STORE).get(key);
72248
+ t.onsuccess = () => res(t.result);
72249
+ t.onerror = () => rej(t.error);
72250
+ });
72251
+ }
72252
+ async function idbPut(key, val) {
72253
+ const db = await idb();
72254
+ return new Promise((res, rej) => {
72255
+ const t = db.transaction(STORE, "readwrite").objectStore(STORE).put(val, key);
72256
+ t.onsuccess = () => res();
72257
+ t.onerror = () => rej(t.error);
72258
+ });
72259
+ }
72260
+ async function idbDel(key) {
72261
+ const db = await idb();
72262
+ return new Promise((res, rej) => {
72263
+ const t = db.transaction(STORE, "readwrite").objectStore(STORE).delete(key);
72264
+ t.onsuccess = () => res();
72265
+ t.onerror = () => rej(t.error);
72266
+ });
72267
+ }
72268
+ async function beginLogin2({ issuer, redirectUri, clientName: name = clientName, returnTo = null }) {
72269
+ const cfg = await discover(issuer);
72270
+ const client_id = await registerClient(cfg, redirectUri, name);
72271
+ const pair = await dpopKey();
72272
+ const verifier = rand(48);
72273
+ const challenge = b64u3(await sha2564(verifier));
72274
+ const state = rand(16);
72275
+ await idbPut("pending", {
72276
+ issuer,
72277
+ client_id,
72278
+ redirectUri,
72279
+ verifier,
72280
+ state,
72281
+ pair,
72282
+ returnTo,
72283
+ tokenEndpoint: cfg.token_endpoint,
72284
+ authorizationEndpoint: cfg.authorization_endpoint,
72285
+ revocationEndpoint: cfg.revocation_endpoint || null
72286
+ });
72287
+ const url = new URL(cfg.authorization_endpoint);
72288
+ for (const [k, v] of Object.entries({
72289
+ client_id,
72290
+ redirect_uri: redirectUri,
72291
+ response_type: "code",
72292
+ scope: "openid webid offline_access",
72293
+ code_challenge: challenge,
72294
+ code_challenge_method: "S256",
72295
+ state,
72296
+ prompt: "consent"
72297
+ })) url.searchParams.set(k, v);
72298
+ return { authorizationUrl: url.href };
72299
+ }
72300
+ async function completeLogin2({ currentUrl }) {
72301
+ const u = new URL(currentUrl);
72302
+ const code = u.searchParams.get("code");
72303
+ const state = u.searchParams.get("state");
72304
+ if (!code) return null;
72305
+ const p = await idbGet("pending");
72306
+ if (!p || p.state !== state) throw new Error("login state mismatch");
72307
+ const jwk = await publicJwk(p.pair);
72308
+ const res = await fetch(p.tokenEndpoint, {
71060
72309
  method: "POST",
71061
- headers: { "content-type": "application/x-www-form-urlencoded", dpop: await dpopProof(s.pair, jwk, "POST", s.tokenEndpoint) },
71062
- body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: s.client_id, scope: "openid webid offline_access" })
72310
+ headers: { "content-type": "application/x-www-form-urlencoded", dpop: await dpopProof(p.pair, jwk, "POST", p.tokenEndpoint) },
72311
+ body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: p.redirectUri, client_id: p.client_id, code_verifier: p.verifier })
71063
72312
  });
71064
72313
  const tok = await res.json().catch(() => ({}));
71065
- if (!res.ok || !tok.access_token) {
71066
- await idbDel("session");
71067
- throw new Error("refresh failed \u2014 sign in again");
71068
- }
71069
- accessToken = tok.access_token;
71070
- expiresAt = Date.now() + Math.max(30, tok.expires_in || 300) * 1e3;
71071
- if (tok.refresh_token) refreshToken = tok.refresh_token;
71072
- const now = await idbGet("session");
71073
- if (now && now.webId !== s.webId) {
71074
- throw new Error("signed in as somebody else while this session was refreshing");
71075
- }
71076
- await idbPut("session", { ...s, accessToken, expiresAt, refreshToken });
71077
- };
71078
- const authFetch = async (url, init = {}) => {
71079
- if (Date.now() > expiresAt - 3e4) await refresh();
71080
- const jwk = await jwkP;
71081
- const ath = b64u3(await sha2564(accessToken));
71082
- const headers = { ...init.headers || {}, authorization: `DPoP ${accessToken}`, dpop: await dpopProof(s.pair, jwk, init.method || "GET", url, ath) };
71083
- return fetch(url, { ...init, headers });
71084
- };
71085
- return { webId: s.webId, fetch: authFetch, refresh, signOut };
72314
+ if (!res.ok || !tok.access_token) throw new Error(`token exchange failed (HTTP ${res.status}): ${tok.error || ""}`);
72315
+ const webId = jwtPayload(tok.access_token).webid || jwtPayload(tok.id_token || "").webid || null;
72316
+ const session = {
72317
+ issuer: p.issuer,
72318
+ client_id: p.client_id,
72319
+ tokenEndpoint: p.tokenEndpoint,
72320
+ pair: p.pair,
72321
+ // Where to hand the refresh token back at sign-out. Kept on the session
72322
+ // because discovery is a network round trip and sign-out should not need
72323
+ // one — see signOut().
72324
+ revocationEndpoint: p.revocationEndpoint || null,
72325
+ refreshToken: tok.refresh_token || null,
72326
+ accessToken: tok.access_token,
72327
+ expiresAt: Date.now() + Math.max(30, tok.expires_in || 300) * 1e3,
72328
+ webId
72329
+ };
72330
+ await idbPut("session", session);
72331
+ await idbDel("pending");
72332
+ return Object.assign(sessionHandle(session), p.returnTo ? { returnTo: p.returnTo } : {});
72333
+ }
72334
+ async function getSession2() {
72335
+ const s = await idbGet("session");
72336
+ return s ? sessionHandle(s) : null;
72337
+ }
72338
+ async function signOut2() {
72339
+ const s = await idbGet("session").catch(() => null);
72340
+ if (s?.revocationEndpoint && s.refreshToken) {
72341
+ try {
72342
+ await fetch(s.revocationEndpoint, {
72343
+ method: "POST",
72344
+ headers: { "content-type": "application/x-www-form-urlencoded" },
72345
+ body: new URLSearchParams({
72346
+ token: s.refreshToken,
72347
+ token_type_hint: "refresh_token",
72348
+ client_id: s.client_id
72349
+ })
72350
+ });
72351
+ } catch {
72352
+ }
72353
+ }
72354
+ await idbDel("session");
72355
+ await idbDel("pending");
72356
+ }
72357
+ function sessionHandle(s) {
72358
+ let { accessToken, expiresAt, refreshToken } = s;
72359
+ const jwkP = publicJwk(s.pair);
72360
+ const refresh = async () => {
72361
+ if (!refreshToken) throw new Error("session expired and there is no refresh token \u2014 sign in again");
72362
+ const jwk = await jwkP;
72363
+ let res;
72364
+ try {
72365
+ res = await fetch(s.tokenEndpoint, {
72366
+ method: "POST",
72367
+ headers: { "content-type": "application/x-www-form-urlencoded", dpop: await dpopProof(s.pair, jwk, "POST", s.tokenEndpoint) },
72368
+ body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: s.client_id, scope: "openid webid offline_access" })
72369
+ });
72370
+ } catch (e) {
72371
+ throw new Error(`${e.message} \u2014 renewing your sign-in at ${new URL(s.tokenEndpoint).host}`);
72372
+ }
72373
+ const tok = await res.json().catch(() => ({}));
72374
+ if (res.status === 429) {
72375
+ throw new Error(`${new URL(s.tokenEndpoint).host} is asking us to slow down \u2014 your sign-in could not be renewed just now`);
72376
+ }
72377
+ if (!res.ok || !tok.access_token) {
72378
+ await idbDel("session");
72379
+ throw new Error("refresh failed \u2014 sign in again");
72380
+ }
72381
+ accessToken = tok.access_token;
72382
+ expiresAt = Date.now() + Math.max(30, tok.expires_in || 300) * 1e3;
72383
+ if (tok.refresh_token) refreshToken = tok.refresh_token;
72384
+ const now = await idbGet("session");
72385
+ if (now && now.webId !== s.webId) {
72386
+ throw new Error("signed in as somebody else while this session was refreshing");
72387
+ }
72388
+ await idbPut("session", { ...s, accessToken, expiresAt, refreshToken });
72389
+ };
72390
+ const send = async (url, init) => {
72391
+ const jwk = await jwkP;
72392
+ const ath = b64u3(await sha2564(accessToken));
72393
+ const headers = { ...init.headers || {}, authorization: `DPoP ${accessToken}`, dpop: await dpopProof(s.pair, jwk, init.method || "GET", url, ath) };
72394
+ return fetch(url, { ...init, headers });
72395
+ };
72396
+ const authFetch = async (url, init = {}) => {
72397
+ if (Date.now() > expiresAt - 3e4) await refresh();
72398
+ const res = await send(url, init);
72399
+ if (res.status === 401 && refreshToken) {
72400
+ try {
72401
+ await refresh();
72402
+ } catch {
72403
+ return res;
72404
+ }
72405
+ return send(url, init);
72406
+ }
72407
+ return res;
72408
+ };
72409
+ return { webId: s.webId, issuer: s.issuer, fetch: authFetch, refresh, signOut: signOut2 };
72410
+ }
72411
+ return { beginLogin: beginLogin2, completeLogin: completeLogin2, getSession: getSession2, signOut: signOut2 };
71086
72412
  }
71087
72413
 
72414
+ // web/app/oidc-session.mjs
72415
+ var { beginLogin, completeLogin, getSession, signOut } = solidOidcSession({ dbName: "fedipod-oidc", clientName: "FediPod" });
72416
+
71088
72417
  // web/app/sw-src.mjs
71089
72418
  var agent = null;
71090
72419
  var booting = null;
@@ -71172,7 +72501,7 @@ async function serve(request, url) {
71172
72501
  } catch {
71173
72502
  }
71174
72503
  }
71175
- if (!agent) return json(503, { error: "the agent is not booted yet \u2014 open the app from the sign-in page" });
72504
+ if (!agent) return json(503, { error: "not signed in on this browser \u2014 sign in at the front page" });
71176
72505
  const bodyBytes = request.method === "GET" || request.method === "HEAD" ? null : Buffer.from(new Uint8Array(await request.arrayBuffer()));
71177
72506
  const bodyText = bodyBytes ? new TextDecoder().decode(bodyBytes) : "";
71178
72507
  const reqHeaders = {};