fedipod-server 0.28.1 → 0.30.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 (438) hide show
  1. package/README.md +38 -34
  2. package/config/server.json +22 -0
  3. package/dist/claims.js +3 -2
  4. package/dist/directory.d.ts +2 -0
  5. package/dist/door.d.ts +4 -0
  6. package/dist/door.js +31 -0
  7. package/dist/fedipod.d.ts +16 -0
  8. package/dist/fedipod.js +13 -0
  9. package/dist/front.d.ts +3 -0
  10. package/dist/front.js +94 -0
  11. package/dist/handler.d.ts +39 -101
  12. package/dist/handler.js +86 -617
  13. package/dist/handler.jsonld +6 -30
  14. package/dist/identities.d.ts +62 -0
  15. package/dist/identities.js +236 -0
  16. package/dist/mounts.d.ts +43 -0
  17. package/dist/mounts.js +123 -0
  18. package/dist/opt-in.d.ts +46 -0
  19. package/dist/opt-in.js +176 -0
  20. package/dist/store-pod.js +37 -0
  21. package/package.json +10 -18
  22. package/lib/client/c2s.mjs +0 -582
  23. package/lib/client/localapi.mjs +0 -87
  24. package/lib/client/masto/accounts.mjs +0 -309
  25. package/lib/client/masto/body.mjs +0 -156
  26. package/lib/client/masto/index.mjs +0 -206
  27. package/lib/client/masto/instance.mjs +0 -112
  28. package/lib/client/masto/media.mjs +0 -79
  29. package/lib/client/masto/oauth.mjs +0 -656
  30. package/lib/client/masto/render.mjs +0 -518
  31. package/lib/client/masto/statuses.mjs +0 -372
  32. package/lib/client/masto/timelines.mjs +0 -438
  33. package/lib/client/oidc-auth.mjs +0 -62
  34. package/lib/client/streaming.mjs +0 -154
  35. package/lib/client/webpush.mjs +0 -98
  36. package/lib/connections/acctfeed.mjs +0 -257
  37. package/lib/connections/atproto.mjs +0 -309
  38. package/lib/connections/bskyfeed.mjs +0 -243
  39. package/lib/connections/bskygroup.mjs +0 -162
  40. package/lib/connections/fediacct.mjs +0 -253
  41. package/lib/connections/import.mjs +0 -357
  42. package/lib/connections/tagfeed.mjs +0 -212
  43. package/lib/connections/vault.mjs +0 -114
  44. package/lib/core/as2.mjs +0 -206
  45. package/lib/core/contexts/activitystreams.json +0 -379
  46. package/lib/core/contexts/anno.json +0 -126
  47. package/lib/core/contexts/did-v1.json +0 -57
  48. package/lib/core/contexts/fep-5711.json +0 -36
  49. package/lib/core/contexts/gotosocial.json +0 -86
  50. package/lib/core/contexts/identity-v1.json +0 -152
  51. package/lib/core/contexts/index.mjs +0 -63
  52. package/lib/core/contexts/join-lemmy.json +0 -33
  53. package/lib/core/contexts/joinmastodon.json +0 -28
  54. package/lib/core/contexts/litepub-0.1.json +0 -60
  55. package/lib/core/contexts/map.json +0 -19
  56. package/lib/core/contexts/miscellany.json +0 -19
  57. package/lib/core/contexts/quotes.json +0 -18
  58. package/lib/core/contexts/schemaorg.json +0 -8845
  59. package/lib/core/contexts/security-data-integrity-v1.json +0 -78
  60. package/lib/core/contexts/security-data-integrity-v2.json +0 -81
  61. package/lib/core/contexts/security-multikey-v1.json +0 -35
  62. package/lib/core/contexts/security-v1.json +0 -74
  63. package/lib/core/contexts/webfinger.json +0 -10
  64. package/lib/core/deliver.mjs +0 -371
  65. package/lib/core/graphview.mjs +0 -327
  66. package/lib/core/intake/activities.mjs +0 -633
  67. package/lib/core/intake/activity.mjs +0 -318
  68. package/lib/core/intake/channel.mjs +0 -144
  69. package/lib/core/intake/group.mjs +0 -256
  70. package/lib/core/intake/index.mjs +0 -701
  71. package/lib/core/intake/notes.mjs +0 -360
  72. package/lib/core/intake/verify.mjs +0 -142
  73. package/lib/core/keys.mjs +0 -153
  74. package/lib/core/lease.mjs +0 -206
  75. package/lib/core/polls.mjs +0 -105
  76. package/lib/core/profile-page.mjs +0 -104
  77. package/lib/core/proof.mjs +0 -79
  78. package/lib/core/publisher/collections.mjs +0 -237
  79. package/lib/core/publisher/index.mjs +0 -500
  80. package/lib/core/publisher/notes.mjs +0 -453
  81. package/lib/core/publisher/questions.mjs +0 -239
  82. package/lib/core/publisher/restore.mjs +0 -224
  83. package/lib/core/shapes/activitystreams.ttl +0 -157
  84. package/lib/core/shapes/index.mjs +0 -132
  85. package/lib/core/shapes/shapes-text.mjs +0 -13
  86. package/lib/core/social.mjs +0 -531
  87. package/lib/core/storage.mjs +0 -176
  88. package/lib/core/store.mjs +0 -568
  89. package/lib/core/wire-quotes.mjs +0 -71
  90. package/lib/core/wire.mjs +0 -889
  91. package/lib/device/account.mjs +0 -87
  92. package/lib/device/admin/index.mjs +0 -13
  93. package/lib/device/admin/origins.mjs +0 -35
  94. package/lib/device/admin/routes/connections.mjs +0 -144
  95. package/lib/device/admin/routes/gateway.mjs +0 -199
  96. package/lib/device/admin/routes/lifecycle.mjs +0 -200
  97. package/lib/device/admin/routes/owner.mjs +0 -322
  98. package/lib/device/admin/routes/setup.mjs +0 -412
  99. package/lib/device/admin/routes/social.mjs +0 -188
  100. package/lib/device/admin/server.mjs +0 -95
  101. package/lib/device/admin/static.mjs +0 -247
  102. package/lib/device/admin/surface.mjs +0 -315
  103. package/lib/device/certs.mjs +0 -281
  104. package/lib/device/cli/commands/account.mjs +0 -586
  105. package/lib/device/cli/commands/run.mjs +0 -278
  106. package/lib/device/cli/commands/service.mjs +0 -221
  107. package/lib/device/cli/commands/setup.mjs +0 -447
  108. package/lib/device/cli/commands/state.mjs +0 -561
  109. package/lib/device/cli/context.mjs +0 -288
  110. package/lib/device/export-collections.mjs +0 -161
  111. package/lib/device/gateway-move.mjs +0 -58
  112. package/lib/device/home.mjs +0 -161
  113. package/lib/device/migrate.mjs +0 -115
  114. package/lib/device/ports.mjs +0 -27
  115. package/lib/device/remote.mjs +0 -54
  116. package/lib/device/setup.mjs +0 -459
  117. package/lib/device/update.mjs +0 -96
  118. package/lib/gateway/caches.mjs +0 -36
  119. package/lib/gateway/directory.mjs +0 -90
  120. package/lib/gateway/front-core.mjs +0 -970
  121. package/lib/gateway/gateway-core.mjs +0 -165
  122. package/lib/gateway/headers.mjs +0 -49
  123. package/lib/gateway/httpsig.mjs +0 -159
  124. package/lib/gateway/notices.mjs +0 -84
  125. package/lib/gateway/quiet.mjs +0 -189
  126. package/lib/pod/README.md +0 -61
  127. package/lib/pod/actor.mjs +0 -96
  128. package/lib/pod/collection.mjs +0 -77
  129. package/lib/pod/containers.mjs +0 -140
  130. package/lib/pod/discovery.mjs +0 -46
  131. package/lib/pod/featured.mjs +0 -18
  132. package/lib/pod/followers.mjs +0 -24
  133. package/lib/pod/following.mjs +0 -10
  134. package/lib/pod/http.mjs +0 -74
  135. package/lib/pod/inbox.mjs +0 -166
  136. package/lib/pod/links.mjs +0 -35
  137. package/lib/pod/media.mjs +0 -27
  138. package/lib/pod/notes.mjs +0 -80
  139. package/lib/pod/notifications.mjs +0 -89
  140. package/lib/pod/outbox.mjs +0 -18
  141. package/lib/pod/package.json +0 -19
  142. package/lib/pod/policy.mjs +0 -58
  143. package/lib/pod/private.mjs +0 -19
  144. package/lib/pod/root.mjs +0 -104
  145. package/lib/pod/state.mjs +0 -40
  146. package/lib/pod/transport.mjs +0 -525
  147. package/lib/pod/urls.mjs +0 -85
  148. package/lib/server/embed.mjs +0 -423
  149. package/lib/session/README.md +0 -137
  150. package/lib/session/demo.html +0 -94
  151. package/lib/session/fedi-account.mjs +0 -408
  152. package/lib/session/fedi-login.mjs +0 -123
  153. package/lib/session/oidc-session.mjs +0 -233
  154. package/lib/session/package.json +0 -49
  155. package/lib/shared/guard.mjs +0 -213
  156. package/lib/shared/links.mjs +0 -9
  157. package/lib/shared/safefetch.mjs +0 -235
  158. package/lib/shared/ua.mjs +0 -29
  159. package/phanpy/dist/404.html +0 -40
  160. package/phanpy/dist/_headers +0 -2
  161. package/phanpy/dist/apple-touch-icon.png +0 -0
  162. package/phanpy/dist/assets/ICONS-DIPJeU0C.js +0 -2
  163. package/phanpy/dist/assets/ICONS-DIPJeU0C.js.map +0 -1
  164. package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js +0 -183
  165. package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
  166. package/phanpy/dist/assets/Temml-CCjoacWt.woff2 +0 -0
  167. package/phanpy/dist/assets/__vite-browser-external-C7iut881.js +0 -2
  168. package/phanpy/dist/assets/__vite-browser-external-C7iut881.js.map +0 -1
  169. package/phanpy/dist/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  170. package/phanpy/dist/assets/catch-up-O5q5C75Z.png +0 -0
  171. package/phanpy/dist/assets/chunk-aKtaBQYM.js +0 -1
  172. package/phanpy/dist/assets/compose-CFEx7wb4.js +0 -2
  173. package/phanpy/dist/assets/compose-CFEx7wb4.js.map +0 -1
  174. package/phanpy/dist/assets/compose-ZJN5xSb4.js +0 -35
  175. package/phanpy/dist/assets/compose-ZJN5xSb4.js.map +0 -1
  176. package/phanpy/dist/assets/debug.module-YFQgwnIr.js +0 -52
  177. package/phanpy/dist/assets/debug.module-YFQgwnIr.js.map +0 -1
  178. package/phanpy/dist/assets/dom-p1YcJ5Rw.js +0 -2
  179. package/phanpy/dist/assets/dom-p1YcJ5Rw.js.map +0 -1
  180. package/phanpy/dist/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  181. package/phanpy/dist/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  182. package/phanpy/dist/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  183. package/phanpy/dist/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  184. package/phanpy/dist/assets/icons/add-circle-line-WD0mwORD.js +0 -1
  185. package/phanpy/dist/assets/icons/alert-line-CpFUMf9s.js +0 -1
  186. package/phanpy/dist/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
  187. package/phanpy/dist/assets/icons/announcement-line-5nwaJFJm.js +0 -1
  188. package/phanpy/dist/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
  189. package/phanpy/dist/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
  190. package/phanpy/dist/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
  191. package/phanpy/dist/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
  192. package/phanpy/dist/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
  193. package/phanpy/dist/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
  194. package/phanpy/dist/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
  195. package/phanpy/dist/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
  196. package/phanpy/dist/assets/icons/at-line-CF66Bznk.js +0 -1
  197. package/phanpy/dist/assets/icons/attachment-line-BY8It-l9.js +0 -1
  198. package/phanpy/dist/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
  199. package/phanpy/dist/assets/icons/board-line-C0UWXyA6.js +0 -1
  200. package/phanpy/dist/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
  201. package/phanpy/dist/assets/icons/building-5-line-Dd59MzbO.js +0 -1
  202. package/phanpy/dist/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
  203. package/phanpy/dist/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
  204. package/phanpy/dist/assets/icons/calendar-month-line-vommts2C.js +0 -1
  205. package/phanpy/dist/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
  206. package/phanpy/dist/assets/icons/camera-line-Cv69MEei.js +0 -1
  207. package/phanpy/dist/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
  208. package/phanpy/dist/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
  209. package/phanpy/dist/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
  210. package/phanpy/dist/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
  211. package/phanpy/dist/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
  212. package/phanpy/dist/assets/icons/clipboard-line-BBp364gJ.js +0 -1
  213. package/phanpy/dist/assets/icons/close-circle-line-CICdiuGz.js +0 -1
  214. package/phanpy/dist/assets/icons/close-line-DYrFWcjv.js +0 -1
  215. package/phanpy/dist/assets/icons/cloud-line-tiQAbah8.js +0 -1
  216. package/phanpy/dist/assets/icons/code-line-fAl8iiV3.js +0 -1
  217. package/phanpy/dist/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
  218. package/phanpy/dist/assets/icons/copy-2-line-CLW69yF5.js +0 -1
  219. package/phanpy/dist/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
  220. package/phanpy/dist/assets/icons/document-3-line-DBGNhAg2.js +0 -1
  221. package/phanpy/dist/assets/icons/document-line-C7mb9sb7.js +0 -1
  222. package/phanpy/dist/assets/icons/down-line-BGqSSThd.js +0 -1
  223. package/phanpy/dist/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
  224. package/phanpy/dist/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
  225. package/phanpy/dist/assets/icons/exit-line-Dc07BS-x.js +0 -1
  226. package/phanpy/dist/assets/icons/external-link-line-BvpSkmjO.js +0 -1
  227. package/phanpy/dist/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
  228. package/phanpy/dist/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
  229. package/phanpy/dist/assets/icons/filter-2-line-B_mR2usy.js +0 -1
  230. package/phanpy/dist/assets/icons/filter-line-C_nm-3cU.js +0 -1
  231. package/phanpy/dist/assets/icons/flag-1-line-C2HB05I0.js +0 -1
  232. package/phanpy/dist/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
  233. package/phanpy/dist/assets/icons/formula-line-DLrt2d6j.js +0 -1
  234. package/phanpy/dist/assets/icons/grid-line-CgUmEikf.js +0 -1
  235. package/phanpy/dist/assets/icons/group-line-C5fnX0wU.js +0 -1
  236. package/phanpy/dist/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
  237. package/phanpy/dist/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
  238. package/phanpy/dist/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
  239. package/phanpy/dist/assets/icons/heart-line-CJ7K47J4.js +0 -1
  240. package/phanpy/dist/assets/icons/history-2-line-kWWvFzqv.js +0 -1
  241. package/phanpy/dist/assets/icons/history-line-bGQiwbTH.js +0 -1
  242. package/phanpy/dist/assets/icons/home-3-line-fRU1zeLG.js +0 -1
  243. package/phanpy/dist/assets/icons/information-line-iNk6-sOY.js +0 -1
  244. package/phanpy/dist/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
  245. package/phanpy/dist/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
  246. package/phanpy/dist/assets/icons/layout-5-line-B_znt5wO.js +0 -1
  247. package/phanpy/dist/assets/icons/left-line-eUw0tya4.js +0 -1
  248. package/phanpy/dist/assets/icons/lightning-line-DaKoDdix.js +0 -1
  249. package/phanpy/dist/assets/icons/link-2-line-DCqLmMA7.js +0 -1
  250. package/phanpy/dist/assets/icons/list-check-line-BAxosBh9.js +0 -1
  251. package/phanpy/dist/assets/icons/lock-line-CPoaXeUW.js +0 -1
  252. package/phanpy/dist/assets/icons/mail-line-B0P8aa1L.js +0 -1
  253. package/phanpy/dist/assets/icons/moon-line-CEqNAToP.js +0 -1
  254. package/phanpy/dist/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
  255. package/phanpy/dist/assets/icons/more-3-line-BIiVjYTz.js +0 -1
  256. package/phanpy/dist/assets/icons/notification-line-BZZq2Gtu.js +0 -1
  257. package/phanpy/dist/assets/icons/pencil-line-CKh8-A1v.js +0 -1
  258. package/phanpy/dist/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
  259. package/phanpy/dist/assets/icons/pin-line-DpTolyhs.js +0 -1
  260. package/phanpy/dist/assets/icons/play-fill-B0-G_8lX.js +0 -1
  261. package/phanpy/dist/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
  262. package/phanpy/dist/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
  263. package/phanpy/dist/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
  264. package/phanpy/dist/assets/icons/radar-line-CKyroTth.js +0 -1
  265. package/phanpy/dist/assets/icons/react-line-CoIKhhtK.js +0 -1
  266. package/phanpy/dist/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
  267. package/phanpy/dist/assets/icons/right-line-B3KWJaqc.js +0 -1
  268. package/phanpy/dist/assets/icons/rocket-line-4KZl537D.js +0 -1
  269. package/phanpy/dist/assets/icons/round-fill-C69NTrZ0.js +0 -1
  270. package/phanpy/dist/assets/icons/round-line-B9k_1uo-.js +0 -1
  271. package/phanpy/dist/assets/icons/route-line-B28ReHcE.js +0 -1
  272. package/phanpy/dist/assets/icons/rows-4-line-CrBniagM.js +0 -1
  273. package/phanpy/dist/assets/icons/scan-line-DSf74YdP.js +0 -1
  274. package/phanpy/dist/assets/icons/search-2-line-B6geIU1z.js +0 -1
  275. package/phanpy/dist/assets/icons/settings-3-line-azJwK47w.js +0 -1
  276. package/phanpy/dist/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
  277. package/phanpy/dist/assets/icons/share-2-line-D5MxWjrm.js +0 -1
  278. package/phanpy/dist/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
  279. package/phanpy/dist/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
  280. package/phanpy/dist/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
  281. package/phanpy/dist/assets/icons/time-line-BF3eet1v.js +0 -1
  282. package/phanpy/dist/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
  283. package/phanpy/dist/assets/icons/translate-line-BFWol2Ce.js +0 -1
  284. package/phanpy/dist/assets/icons/unlock-line-X_x5vwv3.js +0 -1
  285. package/phanpy/dist/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
  286. package/phanpy/dist/assets/icons/user-4-line-BVSynZCp.js +0 -1
  287. package/phanpy/dist/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
  288. package/phanpy/dist/assets/icons/user-add-line-BbXdHG4z.js +0 -1
  289. package/phanpy/dist/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
  290. package/phanpy/dist/assets/icons/user-follow-line-BXux0wSd.js +0 -1
  291. package/phanpy/dist/assets/icons/user-star-line-DdYlpHLU.js +0 -1
  292. package/phanpy/dist/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
  293. package/phanpy/dist/assets/icons/user-x-line-DIDgNlPH.js +0 -1
  294. package/phanpy/dist/assets/icons/volume-line-Bs390KQt.js +0 -1
  295. package/phanpy/dist/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
  296. package/phanpy/dist/assets/icons/walk-line-yNXfB-md.js +0 -1
  297. package/phanpy/dist/assets/icons/world-2-line-CenkGIwj.js +0 -1
  298. package/phanpy/dist/assets/icons/zoom-in-line-CaENUphf.js +0 -1
  299. package/phanpy/dist/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
  300. package/phanpy/dist/assets/instances-BNPptnn-.json +0 -333
  301. package/phanpy/dist/assets/locales/ar-SA-CfheRbCu.js +0 -1
  302. package/phanpy/dist/assets/locales/ca-ES-DHRW86Tz.js +0 -1
  303. package/phanpy/dist/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
  304. package/phanpy/dist/assets/locales/de-DE-ByRPMRFy.js +0 -1
  305. package/phanpy/dist/assets/locales/eo-UY-CPC7wLCS.js +0 -1
  306. package/phanpy/dist/assets/locales/es-ES-dJa0Fd9t.js +0 -1
  307. package/phanpy/dist/assets/locales/eu-ES-C3djlPR2.js +0 -1
  308. package/phanpy/dist/assets/locales/fa-IR-DTT490Ck.js +0 -1
  309. package/phanpy/dist/assets/locales/fi-FI-B23MCPuz.js +0 -1
  310. package/phanpy/dist/assets/locales/fr-FR-BcaQsO5C.js +0 -1
  311. package/phanpy/dist/assets/locales/gl-ES-BR-CVKBN.js +0 -1
  312. package/phanpy/dist/assets/locales/he-IL-CfheRbCu.js +0 -1
  313. package/phanpy/dist/assets/locales/hu-HU-CfheRbCu.js +0 -1
  314. package/phanpy/dist/assets/locales/it-IT-C1136UFV.js +0 -1
  315. package/phanpy/dist/assets/locales/ja-JP-CRZmRAhD.js +0 -1
  316. package/phanpy/dist/assets/locales/kab-Cz8xH_TT.js +0 -1
  317. package/phanpy/dist/assets/locales/ko-KR-CG9UiY4o.js +0 -1
  318. package/phanpy/dist/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
  319. package/phanpy/dist/assets/locales/nb-NO-CfheRbCu.js +0 -1
  320. package/phanpy/dist/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
  321. package/phanpy/dist/assets/locales/oc-FR-CfheRbCu.js +0 -1
  322. package/phanpy/dist/assets/locales/pl-PL-DkwGXf6P.js +0 -1
  323. package/phanpy/dist/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
  324. package/phanpy/dist/assets/locales/pt-BR-nGL0HdFS.js +0 -1
  325. package/phanpy/dist/assets/locales/pt-PT-Cp17aj2z.js +0 -1
  326. package/phanpy/dist/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
  327. package/phanpy/dist/assets/locales/th-TH-CfheRbCu.js +0 -1
  328. package/phanpy/dist/assets/locales/tok-CfheRbCu.js +0 -1
  329. package/phanpy/dist/assets/locales/tr-TR-CfheRbCu.js +0 -1
  330. package/phanpy/dist/assets/locales/uk-UA-C5E3dFBf.js +0 -1
  331. package/phanpy/dist/assets/locales/zh-CN-BHJi7_et.js +0 -1
  332. package/phanpy/dist/assets/locales/zh-TW-CfheRbCu.js +0 -1
  333. package/phanpy/dist/assets/main-BdqNbG-a.js +0 -26
  334. package/phanpy/dist/assets/main-BdqNbG-a.js.map +0 -1
  335. package/phanpy/dist/assets/mock-home-DGhR__He.js +0 -2
  336. package/phanpy/dist/assets/mock-home-DGhR__He.js.map +0 -1
  337. package/phanpy/dist/assets/multi-column-ROck0NVt.jpg +0 -0
  338. package/phanpy/dist/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  339. package/phanpy/dist/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  340. package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js +0 -2
  341. package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js.map +0 -1
  342. package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js +0 -20
  343. package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js.map +0 -1
  344. package/phanpy/dist/assets/style-B1iLPTPR.css +0 -2
  345. package/phanpy/dist/assets/temml-DuyIQMei.js +0 -97
  346. package/phanpy/dist/assets/temml-DuyIQMei.js.map +0 -1
  347. package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js +0 -2
  348. package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
  349. package/phanpy/dist/assets/year-in-posts-B2lywqdx.png +0 -0
  350. package/phanpy/dist/assets/year-in-posts-DJv_star.js +0 -3
  351. package/phanpy/dist/assets/year-in-posts-DJv_star.js.map +0 -1
  352. package/phanpy/dist/compose/index.html +0 -20
  353. package/phanpy/dist/favicon.ico +0 -0
  354. package/phanpy/dist/index.html +0 -73
  355. package/phanpy/dist/logo-192.png +0 -0
  356. package/phanpy/dist/logo-512.png +0 -0
  357. package/phanpy/dist/logo-badge-72.png +0 -0
  358. package/phanpy/dist/logo-maskable-512.png +0 -0
  359. package/phanpy/dist/logo-monochrome-512.png +0 -0
  360. package/phanpy/dist/logo-monochrome-maskable-512.png +0 -0
  361. package/phanpy/dist/manifest.webmanifest +0 -1
  362. package/phanpy/dist/og-image-2.jpg +0 -0
  363. package/phanpy/dist/og-image.png +0 -0
  364. package/phanpy/dist/robots.txt +0 -2
  365. package/phanpy/dist/sw.js +0 -2
  366. package/phanpy/dist/sw.js.map +0 -1
  367. package/phanpy/dist/version.json +0 -1
  368. package/run-agent.mjs +0 -897
  369. package/vendor/gate.cjs +0 -138
  370. package/vendor/idp-grant.cjs +0 -394
  371. package/vendor/jfetch.cjs +0 -16
  372. package/web/admin/actors.js +0 -145
  373. package/web/admin/bar.css +0 -126
  374. package/web/admin/bar.js +0 -99
  375. package/web/admin/client/client.js +0 -66
  376. package/web/admin/client/index.html +0 -69
  377. package/web/admin/client-pick.js +0 -79
  378. package/web/admin/common.js +0 -23
  379. package/web/admin/connections.js +0 -112
  380. package/web/admin/gateway.js +0 -133
  381. package/web/admin/group.js +0 -258
  382. package/web/admin/index.html +0 -603
  383. package/web/admin/notices-bar.js +0 -90
  384. package/web/admin/oauth-signin.mjs +0 -37
  385. package/web/admin/record.js +0 -390
  386. package/web/admin/setup/index.html +0 -241
  387. package/web/admin/setup/setup.js +0 -326
  388. package/web/admin/tokens.css +0 -68
  389. package/web/admin/upkeep.js +0 -177
  390. package/web/admin/window.css +0 -55
  391. package/web/admin/window.js +0 -136
  392. package/web/app/README.md +0 -81
  393. package/web/app/admin-facade.mjs +0 -588
  394. package/web/app/agent.mjs +0 -663
  395. package/web/app/atproto-browser.mjs +0 -86
  396. package/web/app/authorize_interaction.html +0 -56
  397. package/web/app/authorize_interaction.js +0 -69
  398. package/web/app/boot.mjs +0 -576
  399. package/web/app/deliver-relay.mjs +0 -129
  400. package/web/app/dist/boot.js +0 -34218
  401. package/web/app/dist/boot.js.map +0 -7
  402. package/web/app/dist/sw.js +0 -72810
  403. package/web/app/dist/sw.js.map +0 -7
  404. package/web/app/fediacct-browser.mjs +0 -146
  405. package/web/app/gateway-move.mjs +0 -60
  406. package/web/app/idb-kv.mjs +0 -35
  407. package/web/app/index.html +0 -242
  408. package/web/app/keys-browser.mjs +0 -71
  409. package/web/app/keystore.mjs +0 -82
  410. package/web/app/oidc-session.mjs +0 -6
  411. package/web/app/pod-auth.mjs +0 -248
  412. package/web/app/pod-remote.mjs +0 -84
  413. package/web/app/shims/_globals.mjs +0 -2
  414. package/web/app/shims/fedify-sig.mjs +0 -77
  415. package/web/app/shims/node-crypto.mjs +0 -113
  416. package/web/app/shims/node-fs.mjs +0 -17
  417. package/web/app/shims/node-path.mjs +0 -19
  418. package/web/app/shims/node-url.mjs +0 -7
  419. package/web/app/shims/prelude.js +0 -58
  420. package/web/app/shims/safefetch.mjs +0 -58
  421. package/web/app/shims/shapes-text.mjs +0 -8
  422. package/web/app/shims/web-push.mjs +0 -14
  423. package/web/app/signup.mjs +0 -318
  424. package/web/app/sw-src.mjs +0 -220
  425. package/web/app/update.js +0 -70
  426. package/web/front/admin.html +0 -55
  427. package/web/front/admin.js +0 -89
  428. package/web/front/install.sh +0 -69
  429. package/web/front/new-account.html +0 -35
  430. package/web/front/new-account.js +0 -13
  431. package/web/front/notices.html +0 -69
  432. package/web/front/notices.js +0 -107
  433. package/web/front/run.html +0 -58
  434. package/web/front/run.js +0 -195
  435. package/web/front/solid-oidc-client.js +0 -6
  436. package/web/icons/forum.svg +0 -11
  437. package/web/icons/gateway.svg +0 -14
  438. package/web/icons/local.svg +0 -11
@@ -1,132 +0,0 @@
1
- // shapes/index.mjs — validating an ActivityStreams document against the shapes
2
- // beside this file.
3
- //
4
- // A shape says what a kind of document IS. Checking one against a document we
5
- // were sent is a different question from whether we trust it: a forged actor,
6
- // a receipt that does not bind, a reply naming a note we never wrote are each
7
- // guarded elsewhere, and none of those guards is replaced here.
8
- //
9
- // NOTHING HERE REJECTS ANYTHING. A failure is recorded and the activity is
10
- // handled exactly as it would have been. Every fediverse implementation writes
11
- // slightly different documents, and a shape strict enough to catch something
12
- // real is strict enough to reject somebody's quirk; which one cannot be known
13
- // in advance, so it is learned from the record rather than guessed at. A shape
14
- // earns enforcement by not firing.
15
-
16
- import { Parser } from 'n3';
17
- import Environment from '@rdfjs/environment';
18
- import DataFactory from '@rdfjs/data-model/Factory.js';
19
- import DatasetFactory from '@rdfjs/dataset/Factory.js';
20
- import NamespaceFactory from '@rdfjs/namespace/Factory.js';
21
- import TermSetFactory from '@rdfjs/term-set/Factory.js';
22
- import TermMapFactory from '@rdfjs/term-map/Factory.js';
23
- import ClownfaceFactory from 'clownface/Factory.js';
24
- import SHACLValidator from 'rdf-validate-shacl';
25
- import { SHAPES_TTL } from './shapes-text.mjs';
26
-
27
- // The validator needs a full RDF/JS environment, not a bare factory. These
28
- // pieces are what rdf-validate-shacl itself depends on, composed here rather
29
- // than pulling in a packaged environment that reads files — this has to run in
30
- // a service worker too.
31
- const rdf = new Environment([DataFactory, DatasetFactory, NamespaceFactory, TermSetFactory, TermMapFactory, ClownfaceFactory]);
32
-
33
- // The shapes document's own URL is the base, so `<#Create>` resolves to a
34
- // fragment of it and no namespace is claimed.
35
- const SHAPES_BASE = 'https://fedipod.invalid/shapes';
36
-
37
- let validator;
38
- let brokenReported = false;
39
-
40
- /** The validator, built once. Reading and parsing the shapes is not per-document work. */
41
- function shacl() {
42
- if (!validator) {
43
- const quads = new Parser({ baseIRI: SHAPES_BASE, format: 'text/turtle' }).parse(SHAPES_TTL);
44
- validator = new SHACLValidator(rdf.dataset(quads.map(asTerm)), { factory: rdf });
45
- }
46
- return validator;
47
- }
48
-
49
- /** jsonld and n3 each mint their own terms; the validator's environment must mint the ones it sees. */
50
- function asTerm(q) {
51
- const term = (t) => {
52
- if (t.termType === 'NamedNode') return rdf.namedNode(t.value);
53
- if (t.termType === 'BlankNode') return rdf.blankNode(String(t.value).replace(/^_:/u, ''));
54
- if (t.termType === 'Literal') {
55
- return rdf.literal(t.value, t.language || (t.datatype ? rdf.namedNode(t.datatype.value) : undefined));
56
- }
57
- return rdf.defaultGraph();
58
- };
59
- return rdf.quad(term(q.subject), term(q.predicate), term(q.object));
60
- }
61
-
62
- /**
63
- * What to call the property that did not fit.
64
- *
65
- * A single-predicate path names itself: the IRI, shortened. A sequence path is
66
- * a list, and its head is a blank node that names nothing — so the failing
67
- * property shape is asked for its own `sh:name` instead.
68
- */
69
- const SH_NAME = 'http://www.w3.org/ns/shacl#name';
70
- function pathName(r) {
71
- const direct = r.path?.value ?? null;
72
- if (direct && /[#/]/u.test(direct)) return direct;
73
- const shape = r.sourceShape;
74
- if (shape) {
75
- for (const q of shacl().$shapes?.dataset ?? []) {
76
- if (q.subject.equals(shape) && q.predicate.value === SH_NAME) return q.object.value;
77
- }
78
- }
79
- return direct;
80
- }
81
-
82
- /**
83
- * Check a document's graph against the shapes.
84
- *
85
- * `quads` is what `parseAS2` produced. Returns null when the document
86
- * conforms or when there was no graph to check, and otherwise a short record
87
- * of what did not fit — the failing node, the property, and the shape's own
88
- * words for it.
89
- *
90
- * Never throws. A validator that cannot run is not a reason to drop mail — but
91
- * it is said once, because a checker that silently passes everything is the
92
- * same silence this change exists to remove.
93
- */
94
- export async function checkShapes(quads) {
95
- if (!Array.isArray(quads) || quads.length === 0) return null;
96
- let report;
97
- try {
98
- report = await shacl().validate(rdf.dataset(quads.map(asTerm)));
99
- } catch (e) {
100
- // A validator that cannot run must not read as "everything conforms" —
101
- // that is the same silence this whole change exists to remove. Said once,
102
- // then quiet, because a broken validator would otherwise say it per item.
103
- if (!brokenReported) {
104
- brokenReported = true;
105
- return { conforms: false, broken: true, results: [{ focus: null, path: null, message: `shape validation is not running: ${e.message}`, shape: null }] };
106
- }
107
- return null;
108
- }
109
- if (report.conforms) return null;
110
- return {
111
- conforms: false,
112
- results: report.results.slice(0, 10).map((r) => ({
113
- focus: r.focusNode?.value ?? null,
114
- // A SEQUENCE path — `( as:object as:closed )` — is an RDF list, so its
115
- // node is blank and `.value` is a generated label like "n3-13". Reported
116
- // as that, the record named nothing a reader could act on. The shape's
117
- // own `sh:name` is what it is called, so that is used when the path
118
- // cannot name itself.
119
- path: pathName(r),
120
- message: r.message?.[0]?.value ?? String(r.sourceConstraintComponent?.value ?? 'does not fit the shape'),
121
- shape: r.sourceShape?.value ?? null,
122
- })),
123
- };
124
- }
125
-
126
- /** The failure as one line, for a log or a dead-letter record. */
127
- export function describeShapeFailure(failure) {
128
- if (!failure) return null;
129
- return failure.results
130
- .map((r) => `${r.path ? r.path.replace(/^.*[#/]/u, '') : 'node'}: ${r.message}`)
131
- .join('; ');
132
- }
@@ -1,13 +0,0 @@
1
- // shapes-text.mjs — the shapes document, as text.
2
- //
3
- // The shapes are authored as Turtle in activitystreams.ttl, which is the one
4
- // source of truth and opens in any RDF tool. Reading it differs by where this
5
- // runs: Node reads the file, and the browser bundle swaps this module for a
6
- // shim that imports the same .ttl through esbuild's text loader.
7
- import fs from 'node:fs';
8
- import path from 'node:path';
9
- import { fileURLToPath } from 'node:url';
10
-
11
- export const SHAPES_TTL = fs.readFileSync(
12
- path.join(path.dirname(fileURLToPath(import.meta.url)), 'activitystreams.ttl'), 'utf8',
13
- );
@@ -1,531 +0,0 @@
1
- // social.mjs — outbound social actions (follow, like, announce, delete)
2
- // shared by the admin API and the Mastodon-API facade. Each action delivers
3
- // the activity and mirrors state into the store; the store keeps the
4
- // activity a later Undo must name.
5
-
6
- import crypto from 'node:crypto';
7
- import * as wire from './wire.mjs';
8
- import * as podNotes from '../pod/notes.mjs';
9
- import { dropFollower } from './store.mjs';
10
-
11
- // WebFinger, ours rather than fedify's — v2 dropped lookupWebFinger.
12
- //
13
- // Better here anyway: the host comes out of a handle, and a handle can arrive
14
- // in a MENTION inside a post somebody else wrote, so it is remote input and
15
- // belongs behind the same SSRF guard as every other outbound request. fedify's
16
- // lookup did not go through ours. https only, because a handle resolved over
17
- // plaintext is a handle anyone on the path can answer for.
18
- //
19
- // `fetchImpl`, when given, makes the request instead of safeFetch: the agent's
20
- // own remote read, which in the browser build is the relay — a server-side
21
- // fetch, so the lookup does not depend on the far pod host answering a
22
- // browser's cross-origin request. Every other remote read there goes the
23
- // same way; WebFinger went direct and was the one lookup that could fail
24
- // while the rest worked.
25
- export async function lookupWebFinger(acct, { fetchImpl = null } = {}) {
26
- const clean = String(acct || '').replace(/^acct:/, '');
27
- const at = clean.lastIndexOf('@');
28
- if (at < 1) return null;
29
- const [user, host] = [clean.slice(0, at), clean.slice(at + 1)];
30
- if (!host || /[/\\?#]/.test(host)) return null;
31
- const { safeFetch, readCapped } = await import('../shared/safefetch.mjs');
32
- const url = `https://${host}/.well-known/webfinger?resource=${encodeURIComponent(`acct:${clean}`)}`;
33
- const headers = { accept: 'application/jrd+json, application/json' };
34
- const res = await (fetchImpl ? fetchImpl(url, { headers }) : safeFetch(url, { headers })).catch(() => null);
35
- if (!res || res.status >= 400) return null;
36
- let jrd;
37
- try { jrd = JSON.parse(await readCapped(res, 256 * 1024)); } catch { return null; }
38
- // The JRD says who it is about. Nothing checked it, so a host answering every
39
- // query with one account's document made `@anyone@that.host` resolve to that
40
- // account — and it was then stored under the handle that was asked for, not
41
- // the one it turned out to be.
42
- const subject = String(jrd?.subject || '').replace(/^acct:/, '').toLowerCase();
43
- if (subject && subject !== clean.toLowerCase()) {
44
- return null;
45
- }
46
- return jrd;
47
- }
48
-
49
- export const selfLink = (jrd) => jrd?.links?.find(l => l.rel === 'self' &&
50
- /application\/(activity\+json|ld\+json)/.test(l.type || ''));
51
-
52
- // A WebFinger server can name an actor on ANY host, and until this ran we
53
- // believed it. The handle is remote input — it arrives in the Mention tags of a
54
- // note somebody else wrote — so evil.example could answer for `@x@evil.example`
55
- // with a real, busy actor elsewhere, and a reply that carried that mention was
56
- // addressed, tagged and DELIVERED to a third party who had nothing to do with
57
- // the thread, over our signature.
58
- //
59
- // The host cannot simply be required to match: `@user@example.com` served by
60
- // mastodon.example is the ordinary delegated setup and the whole point of
61
- // WebFinger. So ask the ACTOR's own host who it says that actor is, and require
62
- // it to come back to the same document. A server may name anyone; it cannot
63
- // make that person's server agree. This is the round trip Mastodon does.
64
- //
65
- // Only when the two hosts differ — same-host is self-consistent by
66
- // construction, so the common case costs no extra request.
67
- export async function confirmDelegation(asked, doc, lookup = lookupWebFinger) {
68
- let home;
69
- try { home = new URL(doc.id).host.toLowerCase(); }
70
- catch { throw new Error(`actor id is not a URL (${doc.id})`); }
71
- if (home === asked.toLowerCase()) return;
72
- const user = doc.preferredUsername;
73
- if (!user) {
74
- throw new Error(`${asked} points at ${doc.id} on another host, which names no `
75
- + 'username to confirm it by');
76
- }
77
- const back = await lookup(`acct:${user}@${home}`);
78
- if (selfLink(back)?.href !== doc.id) {
79
- throw new Error(`${asked} claims ${doc.id}, but ${home} does not agree that is `
80
- + `@${user}@${home} — refusing to take one host's word for another's actor`);
81
- }
82
- }
83
-
84
- // acct:user@host → verified actor doc (cached by fetchAP), or throws.
85
- export async function resolveHandle(agent, handle) {
86
- const clean = String(handle || '').replace(/^@/, '');
87
- if (!/^[^@]+@[^@]+$/.test(clean)) throw new Error('handle must look like user@host');
88
- if (agent.store.isBlocked('https://' + clean.split('@')[1] + '/')) throw new Error('domain is blocked');
89
- // Through the agent's own remote read where it has one — the relay, in the
90
- // browser build — and directly otherwise.
91
- const remote = agent.intake?.deliverer?.signedFetch
92
- ? (u, i) => agent.intake.deliverer.signedFetch(u, i) : null;
93
- const lookup = (a) => lookupWebFinger(a, { fetchImpl: remote });
94
- const jrd = await lookup('acct:' + clean);
95
- const self = selfLink(jrd);
96
- if (!self?.href) throw new Error(`webfinger found no actor for ${clean}`);
97
- const doc = await agent.intake.fetchAP(self.href);
98
- if (!doc?.id) throw new Error(`actor document unusable for ${clean}`);
99
- if (agent.store.isBlocked(doc.id)) throw new Error('actor is blocked');
100
- await confirmDelegation(clean.slice(clean.lastIndexOf('@') + 1), doc, lookup);
101
- // Counts live in the collections, not the actor document, so a client asking
102
- // "who is this" would otherwise be told everyone has none. Two GETs, and only
103
- // when someone asked by name — never on the drain path.
104
- await cacheCounts(agent, doc);
105
- return doc;
106
- }
107
-
108
- // totalItems off the followers/following collections, stored beside the cached
109
- // actor. Best effort: a collection that will not answer leaves the count unset
110
- // rather than reported as zero, which is a different claim.
111
- async function cacheCounts(agent, doc) {
112
- const total = async (url) => {
113
- if (!url) return null;
114
- try {
115
- const res = await agent.intake.fetchAP(url);
116
- return Number.isInteger(res?.totalItems) ? res.totalItems : null;
117
- } catch { return null; }
118
- };
119
- const [followers, following] = await Promise.all([total(doc.followers), total(doc.following)]);
120
- if (followers === null && following === null) return;
121
- const actors = agent.store.getActors();
122
- if (!actors[doc.id]) return;
123
- actors[doc.id] = { ...actors[doc.id], counts: { followers, following } };
124
- agent.store.write('actors.json', actors);
125
- }
126
-
127
- // `doc` skips the fetch when the caller already resolved the actor; a bulk
128
- // import passes publish:false and republishes the collections once at the end.
129
- export async function followActor(agent, actorUrl, { publish = true, doc: fetched = null } = {}) {
130
- const doc = fetched || await agent.intake.fetchAP(actorUrl);
131
- if (!doc?.inbox) throw new Error(`actor document unusable (${actorUrl})`);
132
- if (agent.store.isBlocked(doc.id)) throw new Error('actor is blocked');
133
- const contacts = agent.store.getContacts();
134
- let rec = contacts.following.find(f => f.actor === doc.id);
135
- if (!rec) {
136
- rec = { actor: doc.id, inbox: doc.inbox, accepted: false };
137
- contacts.following.push(rec);
138
- }
139
- const follow = wire.followActivity({ urls: agent.publisher.urls, targetActor: doc.id, serial: Date.now() });
140
- rec.followActivity = follow; // kept for a later Undo
141
- agent.store.setContacts(contacts);
142
- await agent.deliverer.deliver(doc.inbox, follow);
143
- if (publish) await agent.publisher.publishCollections({ following: true, pending: true });
144
- return doc;
145
- }
146
-
147
- export async function followHandle(agent, handle) {
148
- const doc = await resolveHandle(agent, handle);
149
- await followActor(agent, doc.id);
150
- const clean = String(handle || '').replace(/^@/, '');
151
- const contacts = agent.store.getContacts();
152
- const rec = contacts.following.find(f => f.actor === doc.id);
153
- if (rec && !rec.handle) { rec.handle = clean; agent.store.setContacts(contacts); }
154
- return { ok: true, actor: doc.id };
155
- }
156
-
157
- export async function unfollowActor(agent, actor) {
158
- const contacts = agent.store.getContacts();
159
- const rec = contacts.following.find(f => f.actor === actor);
160
- if (!rec) throw new Error('not following that actor');
161
- if (rec.followActivity) {
162
- await agent.deliverer.deliver(rec.inbox,
163
- wire.undoActivity({ urls: agent.publisher.urls, activity: rec.followActivity, serial: Date.now() }));
164
- }
165
- contacts.following = contacts.following.filter(f => f.actor !== actor);
166
- agent.store.setContacts(contacts);
167
- await agent.publisher.publishCollections({ following: true, pending: true });
168
- return { ok: true };
169
- }
170
-
171
- export async function favourite(agent, s) {
172
- if (s.favourited) return s;
173
- const doc = await agent.intake.fetchAP(s.actor);
174
- if (!doc?.inbox) throw new Error('author inbox unavailable');
175
- const act = wire.likeActivity({ urls: agent.publisher.urls, noteId: s.noteId, serial: Date.now() });
176
- await agent.deliverer.deliver(doc.endpoints?.sharedInbox || doc.inbox, act);
177
- return agent.store.updateStatus(s.noteId, { favourited: true, likeActivity: act });
178
- }
179
-
180
- export async function unfavourite(agent, s) {
181
- if (s.likeActivity) {
182
- const doc = await agent.intake.fetchAP(s.actor).catch(() => null);
183
- if (doc?.inbox) {
184
- await agent.deliverer.deliver(doc.endpoints?.sharedInbox || doc.inbox,
185
- wire.undoActivity({ urls: agent.publisher.urls, activity: s.likeActivity, serial: Date.now() }));
186
- }
187
- }
188
- return agent.store.updateStatus(s.noteId, { favourited: false, likeActivity: undefined });
189
- }
190
-
191
- // Announce goes to our followers (that's what boosting is) and to the author.
192
- export async function reblog(agent, s) {
193
- if (s.reblogged) return s;
194
- const { urls } = agent.publisher;
195
- const act = wire.announceActivity({ urls, object: s.noteId, serial: Date.now() });
196
- const inboxes = new Set(agent.store.getContacts().followers
197
- .map(f => f.sharedInbox || f.inbox).filter(Boolean));
198
- if (s.actor !== urls.actor) {
199
- const doc = await agent.intake.fetchAP(s.actor).catch(() => null);
200
- if (doc?.inbox) inboxes.add(doc.endpoints?.sharedInbox || doc.inbox);
201
- }
202
- await agent.deliverer.deliverToAll([...inboxes], act);
203
- // Mark it boosted before recording it: a failed outbox write costs one
204
- // missing entry, a failed status write would let a retry announce twice.
205
- const updated = agent.store.updateStatus(s.noteId, { reblogged: true, announceActivity: act });
206
- await agent.publisher.recordOutbox(act);
207
- if (await agent.store.commit?.() === false) agent.log(`boost sent but its timeline row was refused: ${s.noteId}`);
208
- return updated;
209
- }
210
-
211
- export async function unreblog(agent, s) {
212
- if (s.announceActivity) {
213
- const undo = wire.undoActivity({ urls: agent.publisher.urls, activity: s.announceActivity, serial: Date.now() });
214
- const inboxes = new Set(agent.store.getContacts().followers
215
- .map(f => f.sharedInbox || f.inbox).filter(Boolean));
216
- const doc = await agent.intake.fetchAP(s.actor).catch(() => null);
217
- if (doc?.inbox && s.actor !== agent.publisher.urls.actor) inboxes.add(doc.endpoints?.sharedInbox || doc.inbox);
218
- await agent.deliverer.deliverToAll([...inboxes], undo);
219
- await agent.publisher.unrecordOutbox(i => i?.id === s.announceActivity.id);
220
- }
221
- return agent.store.updateStatus(s.noteId, { reblogged: false, announceActivity: undefined });
222
- }
223
-
224
- // Group: end a following we will no longer carry. Forgetting them locally is
225
- // not enough — without the Reject their server keeps the relationship and they
226
- // keep receiving everything the group announces. Muting them too, because an
227
- // ejection anyone can undo by re-following is not an ejection; a re-follow is
228
- // still auto-accepted, but nothing of theirs gets carried.
229
- export async function ejectFollower(agent, actor) {
230
- const { urls } = agent.publisher;
231
- const contacts = agent.store.getContacts();
232
- const rec = contacts.followers.find(f => f.actor === actor);
233
- if (!rec) throw new Error('not a member');
234
- dropFollower(contacts, actor, 'ejected');
235
- agent.store.setContacts(contacts);
236
- const muted = agent.store.getMuted();
237
- muted.actors = [...new Set([...muted.actors, actor])];
238
- agent.store.setMuted(muted);
239
- await agent.publisher.publishCollections({ followers: true });
240
- // The ban is broadcast to the membership (FEP-1b12), so their servers can
241
- // mirror it. Never sent to the ejected member — they get the Reject below.
242
- await announceModeration(agent,
243
- wire.blockActivity({ urls, targetActor: actor, serial: Date.now() }))
244
- .catch(e => agent.log?.(`moderation announce failed: ${e.message}`));
245
- // A Bluesky-side member's departure sticks by blocking the DID.
246
- if (rec.bsky) {
247
- await agent.bskygroup?.blockDid(rec.bsky.did)
248
- .catch(e => agent.log?.(`bluesky block failed for ${rec.bsky.handle}: ${e.message}`));
249
- return { ok: true, actor, told: true };
250
- }
251
- if (rec.inbox) {
252
- await agent.deliverer.deliver(rec.inbox, wire.rejectActivity({
253
- urls,
254
- followActivity: { id: rec.followId, type: 'Follow', actor, object: urls.actor },
255
- serial: Date.now(),
256
- }));
257
- }
258
- return { ok: true, actor, told: !!rec.inbox };
259
- }
260
-
261
- // Group: answer a held join request. Admitting is the Accept onFollow would
262
- // have sent immediately had the group not been gated.
263
- export async function admitRequest(agent, actor, { publish = true } = {}) {
264
- const reqs = agent.store.getRequests();
265
- const req = reqs.find(r => r.actor === actor);
266
- if (!req) throw new Error('no such request');
267
- const contacts = agent.store.getContacts();
268
- if (!contacts.followers.some(f => f.actor === actor)) {
269
- // Admitting a bridged Bluesky member retires their Bluesky-only record,
270
- // the same way an ungated group's follow does: one person, one membership.
271
- const bridgedDid = /^https:\/\/bsky\.brid\.gy\/ap\/(did:[^/]+)$/.exec(actor)?.[1];
272
- if (bridgedDid) contacts.followers = contacts.followers.filter(f => f.bsky?.did !== bridgedDid);
273
- contacts.followers.push(req.bsky
274
- ? { actor, bsky: req.bsky }
275
- : { actor, inbox: req.inbox, sharedInbox: req.sharedInbox, followId: req.activity?.id });
276
- agent.store.setContacts(contacts);
277
- agent.store.addNotification({ type: 'follow', actor, ...(req.bsky ? { bsky: true } : {}) });
278
- }
279
- agent.store.setRequests(reqs.filter(r => r.actor !== actor));
280
- // A Bluesky-only member has no inbox and no Accept to receive; the follow
281
- // already stands on their side. The one-time bridge nudge goes out instead.
282
- if (req.bsky) {
283
- await agent.bskygroup?.nudgeOnce(contacts.followers.find(f => f.actor === actor));
284
- return { ok: true, actor };
285
- }
286
- // A bulk admit passes publish:false and republishes once for the lot.
287
- if (publish) await agent.publisher.publishCollections({ followers: true, pending: true });
288
- await agent.deliverer.deliver(req.inbox, wire.acceptActivity({
289
- urls: agent.publisher.urls, followActivity: req.activity, serial: Date.now(),
290
- }));
291
- return { ok: true, actor };
292
- }
293
-
294
- // Refusing is not a ban: they may ask again, and the queue is where you would
295
- // see that. Eject is the sticky one, because it mutes.
296
- export async function refuseRequest(agent, actor) {
297
- const reqs = agent.store.getRequests();
298
- const req = reqs.find(r => r.actor === actor);
299
- if (!req) throw new Error('no such request');
300
- agent.store.setRequests(reqs.filter(r => r.actor !== actor));
301
- await agent.publisher?.publishCollections({ pending: true }).catch(() => {});
302
- // No Reject can reach a Bluesky-only requester, so the refusal is enforced
303
- // where it can be: their DID is blocked.
304
- if (req.bsky) {
305
- await agent.bskygroup?.blockDid(req.bsky.did)
306
- .catch(e => agent.log?.(`bluesky block failed for ${req.bsky.handle}: ${e.message}`));
307
- return { ok: true, actor };
308
- }
309
- await agent.deliverer.deliver(req.inbox, wire.rejectActivity({
310
- urls: agent.publisher.urls, followActivity: req.activity, serial: Date.now(),
311
- }));
312
- return { ok: true, actor };
313
- }
314
-
315
- // Group: unsay an Announce. The work is in Intake because an upstream Delete
316
- // has to do the same thing, and both must use the recipient set amplify used.
317
- export async function retractAnnouncement(agent, noteId) {
318
- return agent.intake.retract(noteId);
319
- }
320
-
321
- // Remove an own note everywhere: Delete→followers, remote note + outbox,
322
- // local pod RDF, statuses mirror.
323
- //
324
- // Two of the three pod deletes have to land, and both are checked. While the
325
- // note document answers, anyone holding its id can still read a post its author
326
- // deleted — and the `-create` beside it EMBEDS the whole note, so leaving that
327
- // one behind publishes the same content under another name. A failure keeps the
328
- // post here and says so, rather than reporting a deletion that did not happen:
329
- // removing it locally would leave nothing to try again with, and nothing to see
330
- // that it is still up.
331
- // A vote is the fediverse convention: one bare Note per chosen option, whose
332
- // name IS the option, inReplyTo the question, addressed to the question's
333
- // author alone. The documents are stored for dereference but never listed —
334
- // not in the outbox, not in the local index.
335
- export async function votePoll(agent, s, choices) {
336
- const { urls } = agent.publisher;
337
- const poll = s.poll || {};
338
- const opts = poll.options || [];
339
- if (poll.closed || (poll.expiresAt && Date.parse(poll.expiresAt) < Date.now())) {
340
- return { ok: false, error: 'this poll has closed' };
341
- }
342
- if (poll.voted) return { ok: false, error: 'already voted' };
343
- // Your own poll is the one you are counting, not one you are answering.
344
- if (s.actor === urls.actor) return { ok: false, error: 'this is your own poll' };
345
- const picks = [...new Set(choices)].filter(i => i >= 0 && i < opts.length);
346
- if (!picks.length) return { ok: false, error: 'invalid choice' };
347
- if (!poll.multiple && picks.length > 1) return { ok: false, error: 'this poll takes one choice' };
348
- const author = await agent.intake.fetchAP(s.actor).catch(() => null);
349
- const inbox = author?.endpoints?.sharedInbox || author?.inbox;
350
- if (!inbox) return { ok: false, error: 'could not reach the poll author' };
351
- const published = new Date().toISOString();
352
- for (const i of picks) {
353
- const id = urls.notes + published.slice(0, 10) + '-vote-' + crypto.randomBytes(4).toString('hex');
354
- const note = {
355
- '@context': wire.AS_CTX, id, type: 'Note',
356
- attributedTo: urls.actor, name: opts[i].title,
357
- inReplyTo: s.noteId, published, to: [s.actor],
358
- };
359
- const create = {
360
- '@context': wire.AS_CTX, id: wire.createActivityId(id), type: 'Create',
361
- actor: urls.actor, published, to: [s.actor], object: note,
362
- };
363
- await podNotes.write(agent.remote, id, note);
364
- await podNotes.writeCreate(agent.remote, create.id, create);
365
- await agent.deliverer.deliverToAll([inbox], create);
366
- }
367
- agent.store.updateStatus(s.noteId, {
368
- poll: {
369
- ...poll, voted: true, ownVotes: picks,
370
- options: opts.map((o, j) => (picks.includes(j) ? { ...o, votes: (o.votes || 0) + 1 } : o)),
371
- },
372
- });
373
- return { ok: true };
374
- }
375
-
376
- export async function deleteNote(agent, s) {
377
- const { urls } = agent.publisher;
378
- const inboxes = agent.store.getContacts().followers
379
- .map(f => f.sharedInbox || f.inbox).filter(Boolean);
380
- await agent.deliverer.deliverToAll(inboxes, wire.deleteActivity({ urls, noteId: s.noteId }));
381
-
382
- // The note is REPLACED with a Tombstone rather than removed, so a peer that
383
- // dereferences it learns the post was deleted instead of a bare 404 (§7.4).
384
- // The Tombstone must stay readable; the container above it is owner-only, so
385
- // the ACL cannot be inherited. The Create activity is the withdrawn action,
386
- // not the object, so it is deleted outright.
387
- const stuck = [];
388
- const deletedAt = new Date().toISOString();
389
- try {
390
- await podNotes.writeTombstone(agent.remote, s.noteId, wire.noteTombstone(s.noteId, deletedAt));
391
- } catch { stuck.push(s.noteId); }
392
- if (!await podNotes.dropCreate(agent.remote, wire.createActivityId(s.noteId))) {
393
- stuck.push(wire.createActivityId(s.noteId));
394
- }
395
- if (stuck.length) {
396
- agent.log?.(`delete ${s.noteId}: the pod kept ${stuck.length} document(s) — the post is STILL PUBLISHED`);
397
- return {
398
- ok: false, stillPublished: stuck,
399
- error: 'the pod would not remove the post — it is still published, and has been kept here so you can try again',
400
- };
401
- }
402
- // Empty, and referenced by nothing once the note is gone.
403
- await podNotes.dropReplies(agent.remote, wire.repliesId(s.noteId));
404
-
405
- // The Bluesky mirror goes with the post. Best-effort: a mirror the far side
406
- // already lost is not a reason to keep the post here.
407
- if (s.atproto?.uri && agent.atproto?.connected()) {
408
- await agent.atproto.deleteCrossPost(s.atproto.uri)
409
- .then(() => agent.log?.(`bluesky mirror deleted: ${s.atproto.uri}`))
410
- .catch(e => agent.log?.(`bluesky mirror not deleted (${e.message}): ${s.atproto.uri}`));
411
- }
412
-
413
- // The note's outbox entry, and our own boost of it if there was one.
414
- await agent.publisher.unrecordOutbox(i => i === s.noteId || (i?.id && i.id === s.announceActivity?.id));
415
- agent.store.removeStatus(s.noteId);
416
- return { ok: true };
417
- }
418
-
419
- // FEP-1b12: a GROUP's moderation is announced to its followers, so their
420
- // servers can mirror the action. Group moderation only — a person's blocks
421
- // never go on the wire (that is also the spec's own SHOULD NOT), which is why
422
- // the guard lives here rather than in every caller. Never delivered to the
423
- // moderated party; the announce goes to the membership.
424
- export async function announceModeration(agent, activity) {
425
- if (agent.store.getConfig()?.kind !== 'group') return null;
426
- const { urls } = agent.publisher;
427
- const act = wire.announceActivity({ urls, object: activity, serial: Date.now(), audience: urls.actor });
428
- const inboxes = [...new Set(agent.store.getContacts().followers
429
- .map(f => f.sharedInbox || f.inbox).filter(Boolean))];
430
- await agent.deliverer.deliverToAll(inboxes, act);
431
- await agent.publisher.recordOutbox(act);
432
- return act;
433
- }
434
-
435
- // A block is state, not speech: the list changes and the follow ends, and
436
- // nothing is delivered TO THE BLOCKED PARTY (§6.10). Intake already refuses a
437
- // blocked author on arrival. A group additionally announces the ban to its
438
- // members (FEP-1b12) — announceModeration keeps a person's blocks silent.
439
- export async function blockActor(agent, actorUrl) {
440
- if (!/^https?:\/\/\S+$/.test(String(actorUrl || ''))) throw new Error('actor must be a URL');
441
- const b = agent.store.getBlocklist();
442
- if (!b.actors.includes(actorUrl)) b.actors.push(actorUrl);
443
- agent.store.setBlocklist(b);
444
- await unfollowActor(agent, actorUrl).catch(() => {}); // not following is fine
445
- await agent.publisher?.publishCollections({ blocked: true }).catch(() => {});
446
- await announceModeration(agent,
447
- wire.blockActivity({ urls: agent.publisher.urls, targetActor: actorUrl, serial: Date.now() }))
448
- .catch(e => agent.log?.(`moderation announce failed: ${e.message}`));
449
- return { ok: true, actor: actorUrl };
450
- }
451
-
452
- export async function unblockActor(agent, actorUrl) {
453
- const b = agent.store.getBlocklist();
454
- b.actors = b.actors.filter(a => a !== actorUrl);
455
- agent.store.setBlocklist(b);
456
- await agent.publisher?.publishCollections({ blocked: true }).catch(() => {});
457
- // The unban is announced as the Undo of the ban, the ban re-stated inside it
458
- // — its id was a fragment nobody stored, so receivers match on the shape.
459
- await announceModeration(agent, wire.undoActivity({
460
- urls: agent.publisher.urls,
461
- activity: { type: 'Block', actor: agent.publisher.urls.actor, object: actorUrl },
462
- serial: Date.now(),
463
- })).catch(e => agent.log?.(`moderation announce failed: ${e.message}`));
464
- return { ok: true, actor: actorUrl };
465
- }
466
-
467
- // Apply (or refuse) one queued moderation request — an action a LISTED
468
- // moderator asked for over federation. Deliveries carry nothing that proves
469
- // the sender, which is exactly why these wait in a queue for the operator
470
- // instead of running on arrival; applying one is the operator vouching for it.
471
- export async function applyModeration(agent, entry) {
472
- const { urls } = agent.publisher;
473
- const target = typeof entry.activity?.object === 'string'
474
- ? entry.activity.object : entry.activity?.object?.id;
475
- switch (entry.type) {
476
- case 'Block': {
477
- if (!target) throw new Error('the queued Block names nobody');
478
- // A member is ejected (which mutes and announces the ban); a stranger is
479
- // simply blocked. Either way the ban is broadcast once.
480
- const member = agent.store.getContacts().followers.some(f => f.actor === target);
481
- return member ? ejectFollower(agent, target) : blockActor(agent, target);
482
- }
483
- case 'Undo': {
484
- const inner = entry.activity?.object;
485
- const unbanned = typeof inner === 'object' && inner?.type === 'Block'
486
- ? (typeof inner.object === 'string' ? inner.object : inner.object?.id) : null;
487
- if (!unbanned) throw new Error('the queued Undo does not name a Block');
488
- const muted = agent.store.getMuted();
489
- muted.actors = muted.actors.filter(a => a !== unbanned);
490
- agent.store.setMuted(muted);
491
- return unblockActor(agent, unbanned);
492
- }
493
- case 'Delete': {
494
- if (!target) throw new Error('the queued Delete names nothing');
495
- // Removing a post from the group is unsaying its carry.
496
- return agent.intake.retract(target);
497
- }
498
- case 'Add':
499
- case 'Remove': {
500
- if (!target) throw new Error('the queued roster change names nobody');
501
- const cfg = { ...agent.store.getConfig() };
502
- const prev = cfg.moderators || [];
503
- cfg.moderators = entry.type === 'Add'
504
- ? [...new Set([...prev, target])]
505
- : prev.filter(m => m !== target);
506
- agent.store.setConfig(cfg);
507
- if (agent.publisher) agent.publisher.config.moderators = cfg.moderators;
508
- await agent.publisher.publishProfile();
509
- await announceModeration(agent, wire.addRemoveActivity({
510
- urls, type: entry.type, object: target, target: urls.moderators, serial: Date.now(),
511
- })).catch(e => agent.log?.(`moderation announce failed: ${e.message}`));
512
- return { ok: true, moderators: cfg.moderators };
513
- }
514
- default: throw new Error(`no way to apply a queued ${entry.type}`);
515
- }
516
- }
517
-
518
- // Pin or unpin an own post. Republishing the featured collection is what makes
519
- // the pin real to anyone else — the actor document names that collection, and
520
- // a visitor's server reads it, so the profile is republished too (it skips
521
- // itself when nothing changed).
522
- export async function pinStatus(agent, s, pinned) {
523
- if (s.actor !== agent.publisher.urls.actor) throw new Error('not your status');
524
- const updated = agent.store.updateStatus(s.noteId, { pinned: !!pinned });
525
- await agent.publisher.publishFeatured().catch(e => agent.log?.(`featured: ${e.message}`));
526
- if (agent.publisher.publishProfilePage) {
527
- await agent.publisher.publishProfilePage().catch(e => agent.log?.(`profile page: ${e.message}`));
528
- }
529
- agent.publisher.publishProfile?.().catch(() => {});
530
- return updated || s;
531
- }