fedipod-server 0.29.0 → 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 (440) hide show
  1. package/README.md +11 -6
  2. package/dist/door.d.ts +4 -0
  3. package/dist/door.js +31 -0
  4. package/dist/fedipod.d.ts +16 -0
  5. package/dist/fedipod.js +13 -0
  6. package/dist/front.d.ts +3 -0
  7. package/dist/front.js +94 -0
  8. package/dist/handler.d.ts +36 -101
  9. package/dist/handler.js +85 -645
  10. package/dist/handler.jsonld +2 -30
  11. package/dist/identities.d.ts +62 -0
  12. package/dist/identities.js +236 -0
  13. package/dist/mounts.d.ts +43 -0
  14. package/dist/mounts.js +123 -0
  15. package/dist/opt-in.d.ts +46 -0
  16. package/dist/opt-in.js +176 -0
  17. package/package.json +10 -18
  18. package/lib/client/c2s.mjs +0 -636
  19. package/lib/client/localapi.mjs +0 -87
  20. package/lib/client/masto/accounts.mjs +0 -310
  21. package/lib/client/masto/body.mjs +0 -156
  22. package/lib/client/masto/index.mjs +0 -206
  23. package/lib/client/masto/instance.mjs +0 -112
  24. package/lib/client/masto/media.mjs +0 -79
  25. package/lib/client/masto/oauth.mjs +0 -656
  26. package/lib/client/masto/render.mjs +0 -518
  27. package/lib/client/masto/statuses.mjs +0 -372
  28. package/lib/client/masto/timelines.mjs +0 -438
  29. package/lib/client/oidc-auth.mjs +0 -62
  30. package/lib/client/streaming.mjs +0 -154
  31. package/lib/client/webpush.mjs +0 -98
  32. package/lib/connections/acctfeed.mjs +0 -257
  33. package/lib/connections/atproto.mjs +0 -309
  34. package/lib/connections/bskyfeed.mjs +0 -243
  35. package/lib/connections/bskygroup.mjs +0 -162
  36. package/lib/connections/fediacct.mjs +0 -253
  37. package/lib/connections/import.mjs +0 -357
  38. package/lib/connections/tagfeed.mjs +0 -212
  39. package/lib/connections/vault.mjs +0 -114
  40. package/lib/core/as2.mjs +0 -206
  41. package/lib/core/contexts/activitystreams.json +0 -379
  42. package/lib/core/contexts/anno.json +0 -126
  43. package/lib/core/contexts/did-v1.json +0 -57
  44. package/lib/core/contexts/fep-5711.json +0 -36
  45. package/lib/core/contexts/gotosocial.json +0 -86
  46. package/lib/core/contexts/identity-v1.json +0 -152
  47. package/lib/core/contexts/index.mjs +0 -63
  48. package/lib/core/contexts/join-lemmy.json +0 -33
  49. package/lib/core/contexts/joinmastodon.json +0 -28
  50. package/lib/core/contexts/litepub-0.1.json +0 -60
  51. package/lib/core/contexts/map.json +0 -19
  52. package/lib/core/contexts/miscellany.json +0 -19
  53. package/lib/core/contexts/quotes.json +0 -18
  54. package/lib/core/contexts/schemaorg.json +0 -8845
  55. package/lib/core/contexts/security-data-integrity-v1.json +0 -78
  56. package/lib/core/contexts/security-data-integrity-v2.json +0 -81
  57. package/lib/core/contexts/security-multikey-v1.json +0 -35
  58. package/lib/core/contexts/security-v1.json +0 -74
  59. package/lib/core/contexts/webfinger.json +0 -10
  60. package/lib/core/deliver.mjs +0 -387
  61. package/lib/core/graphview.mjs +0 -327
  62. package/lib/core/intake/activities.mjs +0 -649
  63. package/lib/core/intake/activity.mjs +0 -318
  64. package/lib/core/intake/channel.mjs +0 -144
  65. package/lib/core/intake/group.mjs +0 -258
  66. package/lib/core/intake/index.mjs +0 -732
  67. package/lib/core/intake/notes.mjs +0 -367
  68. package/lib/core/intake/verify.mjs +0 -142
  69. package/lib/core/keys.mjs +0 -153
  70. package/lib/core/lease.mjs +0 -206
  71. package/lib/core/place.mjs +0 -82
  72. package/lib/core/polls.mjs +0 -105
  73. package/lib/core/profile-page.mjs +0 -104
  74. package/lib/core/proof.mjs +0 -79
  75. package/lib/core/publisher/collections.mjs +0 -272
  76. package/lib/core/publisher/index.mjs +0 -522
  77. package/lib/core/publisher/notes.mjs +0 -519
  78. package/lib/core/publisher/own.mjs +0 -143
  79. package/lib/core/publisher/questions.mjs +0 -241
  80. package/lib/core/publisher/restore.mjs +0 -224
  81. package/lib/core/shapes/activitystreams.ttl +0 -157
  82. package/lib/core/shapes/index.mjs +0 -132
  83. package/lib/core/shapes/shapes-text.mjs +0 -13
  84. package/lib/core/social.mjs +0 -561
  85. package/lib/core/storage.mjs +0 -176
  86. package/lib/core/store.mjs +0 -578
  87. package/lib/core/wire-quotes.mjs +0 -71
  88. package/lib/core/wire.mjs +0 -925
  89. package/lib/device/account.mjs +0 -87
  90. package/lib/device/admin/index.mjs +0 -13
  91. package/lib/device/admin/origins.mjs +0 -35
  92. package/lib/device/admin/routes/connections.mjs +0 -144
  93. package/lib/device/admin/routes/gateway.mjs +0 -199
  94. package/lib/device/admin/routes/lifecycle.mjs +0 -200
  95. package/lib/device/admin/routes/owner.mjs +0 -322
  96. package/lib/device/admin/routes/setup.mjs +0 -428
  97. package/lib/device/admin/routes/social.mjs +0 -188
  98. package/lib/device/admin/server.mjs +0 -95
  99. package/lib/device/admin/static.mjs +0 -247
  100. package/lib/device/admin/surface.mjs +0 -315
  101. package/lib/device/certs.mjs +0 -281
  102. package/lib/device/cli/commands/account.mjs +0 -586
  103. package/lib/device/cli/commands/run.mjs +0 -278
  104. package/lib/device/cli/commands/service.mjs +0 -221
  105. package/lib/device/cli/commands/setup.mjs +0 -489
  106. package/lib/device/cli/commands/state.mjs +0 -561
  107. package/lib/device/cli/context.mjs +0 -288
  108. package/lib/device/export-collections.mjs +0 -161
  109. package/lib/device/gateway-move.mjs +0 -58
  110. package/lib/device/home.mjs +0 -161
  111. package/lib/device/migrate.mjs +0 -115
  112. package/lib/device/ports.mjs +0 -27
  113. package/lib/device/remote.mjs +0 -54
  114. package/lib/device/setup.mjs +0 -496
  115. package/lib/device/update.mjs +0 -96
  116. package/lib/gateway/caches.mjs +0 -36
  117. package/lib/gateway/directory.mjs +0 -90
  118. package/lib/gateway/front-core.mjs +0 -966
  119. package/lib/gateway/gateway-core.mjs +0 -244
  120. package/lib/gateway/headers.mjs +0 -49
  121. package/lib/gateway/httpsig.mjs +0 -159
  122. package/lib/gateway/notices.mjs +0 -84
  123. package/lib/gateway/quiet.mjs +0 -189
  124. package/lib/gateway/relay-extras.mjs +0 -89
  125. package/lib/gateway/token-claims.mjs +0 -16
  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/location.mjs +0 -52
  138. package/lib/pod/media.mjs +0 -27
  139. package/lib/pod/notes.mjs +0 -78
  140. package/lib/pod/notifications.mjs +0 -89
  141. package/lib/pod/outbox.mjs +0 -18
  142. package/lib/pod/package.json +0 -19
  143. package/lib/pod/policy.mjs +0 -58
  144. package/lib/pod/private.mjs +0 -19
  145. package/lib/pod/root.mjs +0 -104
  146. package/lib/pod/state.mjs +0 -40
  147. package/lib/pod/transport.mjs +0 -674
  148. package/lib/pod/type-index.mjs +0 -101
  149. package/lib/pod/urls.mjs +0 -91
  150. package/lib/server/embed.mjs +0 -424
  151. package/lib/session/README.md +0 -137
  152. package/lib/session/demo.html +0 -94
  153. package/lib/session/fedi-account.mjs +0 -417
  154. package/lib/session/fedi-login.mjs +0 -123
  155. package/lib/session/oidc-session.mjs +0 -233
  156. package/lib/session/package.json +0 -49
  157. package/lib/shared/guard.mjs +0 -213
  158. package/lib/shared/links.mjs +0 -9
  159. package/lib/shared/safefetch.mjs +0 -235
  160. package/lib/shared/ua.mjs +0 -29
  161. package/phanpy/dist/404.html +0 -40
  162. package/phanpy/dist/_headers +0 -2
  163. package/phanpy/dist/apple-touch-icon.png +0 -0
  164. package/phanpy/dist/assets/ICONS-DIPJeU0C.js +0 -2
  165. package/phanpy/dist/assets/ICONS-DIPJeU0C.js.map +0 -1
  166. package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js +0 -183
  167. package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
  168. package/phanpy/dist/assets/Temml-CCjoacWt.woff2 +0 -0
  169. package/phanpy/dist/assets/__vite-browser-external-C7iut881.js +0 -2
  170. package/phanpy/dist/assets/__vite-browser-external-C7iut881.js.map +0 -1
  171. package/phanpy/dist/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  172. package/phanpy/dist/assets/catch-up-O5q5C75Z.png +0 -0
  173. package/phanpy/dist/assets/chunk-aKtaBQYM.js +0 -1
  174. package/phanpy/dist/assets/compose-CFEx7wb4.js +0 -2
  175. package/phanpy/dist/assets/compose-CFEx7wb4.js.map +0 -1
  176. package/phanpy/dist/assets/compose-ZJN5xSb4.js +0 -35
  177. package/phanpy/dist/assets/compose-ZJN5xSb4.js.map +0 -1
  178. package/phanpy/dist/assets/debug.module-YFQgwnIr.js +0 -52
  179. package/phanpy/dist/assets/debug.module-YFQgwnIr.js.map +0 -1
  180. package/phanpy/dist/assets/dom-p1YcJ5Rw.js +0 -2
  181. package/phanpy/dist/assets/dom-p1YcJ5Rw.js.map +0 -1
  182. package/phanpy/dist/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  183. package/phanpy/dist/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  184. package/phanpy/dist/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  185. package/phanpy/dist/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  186. package/phanpy/dist/assets/icons/add-circle-line-WD0mwORD.js +0 -1
  187. package/phanpy/dist/assets/icons/alert-line-CpFUMf9s.js +0 -1
  188. package/phanpy/dist/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
  189. package/phanpy/dist/assets/icons/announcement-line-5nwaJFJm.js +0 -1
  190. package/phanpy/dist/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
  191. package/phanpy/dist/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
  192. package/phanpy/dist/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
  193. package/phanpy/dist/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
  194. package/phanpy/dist/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
  195. package/phanpy/dist/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
  196. package/phanpy/dist/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
  197. package/phanpy/dist/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
  198. package/phanpy/dist/assets/icons/at-line-CF66Bznk.js +0 -1
  199. package/phanpy/dist/assets/icons/attachment-line-BY8It-l9.js +0 -1
  200. package/phanpy/dist/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
  201. package/phanpy/dist/assets/icons/board-line-C0UWXyA6.js +0 -1
  202. package/phanpy/dist/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
  203. package/phanpy/dist/assets/icons/building-5-line-Dd59MzbO.js +0 -1
  204. package/phanpy/dist/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
  205. package/phanpy/dist/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
  206. package/phanpy/dist/assets/icons/calendar-month-line-vommts2C.js +0 -1
  207. package/phanpy/dist/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
  208. package/phanpy/dist/assets/icons/camera-line-Cv69MEei.js +0 -1
  209. package/phanpy/dist/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
  210. package/phanpy/dist/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
  211. package/phanpy/dist/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
  212. package/phanpy/dist/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
  213. package/phanpy/dist/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
  214. package/phanpy/dist/assets/icons/clipboard-line-BBp364gJ.js +0 -1
  215. package/phanpy/dist/assets/icons/close-circle-line-CICdiuGz.js +0 -1
  216. package/phanpy/dist/assets/icons/close-line-DYrFWcjv.js +0 -1
  217. package/phanpy/dist/assets/icons/cloud-line-tiQAbah8.js +0 -1
  218. package/phanpy/dist/assets/icons/code-line-fAl8iiV3.js +0 -1
  219. package/phanpy/dist/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
  220. package/phanpy/dist/assets/icons/copy-2-line-CLW69yF5.js +0 -1
  221. package/phanpy/dist/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
  222. package/phanpy/dist/assets/icons/document-3-line-DBGNhAg2.js +0 -1
  223. package/phanpy/dist/assets/icons/document-line-C7mb9sb7.js +0 -1
  224. package/phanpy/dist/assets/icons/down-line-BGqSSThd.js +0 -1
  225. package/phanpy/dist/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
  226. package/phanpy/dist/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
  227. package/phanpy/dist/assets/icons/exit-line-Dc07BS-x.js +0 -1
  228. package/phanpy/dist/assets/icons/external-link-line-BvpSkmjO.js +0 -1
  229. package/phanpy/dist/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
  230. package/phanpy/dist/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
  231. package/phanpy/dist/assets/icons/filter-2-line-B_mR2usy.js +0 -1
  232. package/phanpy/dist/assets/icons/filter-line-C_nm-3cU.js +0 -1
  233. package/phanpy/dist/assets/icons/flag-1-line-C2HB05I0.js +0 -1
  234. package/phanpy/dist/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
  235. package/phanpy/dist/assets/icons/formula-line-DLrt2d6j.js +0 -1
  236. package/phanpy/dist/assets/icons/grid-line-CgUmEikf.js +0 -1
  237. package/phanpy/dist/assets/icons/group-line-C5fnX0wU.js +0 -1
  238. package/phanpy/dist/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
  239. package/phanpy/dist/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
  240. package/phanpy/dist/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
  241. package/phanpy/dist/assets/icons/heart-line-CJ7K47J4.js +0 -1
  242. package/phanpy/dist/assets/icons/history-2-line-kWWvFzqv.js +0 -1
  243. package/phanpy/dist/assets/icons/history-line-bGQiwbTH.js +0 -1
  244. package/phanpy/dist/assets/icons/home-3-line-fRU1zeLG.js +0 -1
  245. package/phanpy/dist/assets/icons/information-line-iNk6-sOY.js +0 -1
  246. package/phanpy/dist/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
  247. package/phanpy/dist/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
  248. package/phanpy/dist/assets/icons/layout-5-line-B_znt5wO.js +0 -1
  249. package/phanpy/dist/assets/icons/left-line-eUw0tya4.js +0 -1
  250. package/phanpy/dist/assets/icons/lightning-line-DaKoDdix.js +0 -1
  251. package/phanpy/dist/assets/icons/link-2-line-DCqLmMA7.js +0 -1
  252. package/phanpy/dist/assets/icons/list-check-line-BAxosBh9.js +0 -1
  253. package/phanpy/dist/assets/icons/lock-line-CPoaXeUW.js +0 -1
  254. package/phanpy/dist/assets/icons/mail-line-B0P8aa1L.js +0 -1
  255. package/phanpy/dist/assets/icons/moon-line-CEqNAToP.js +0 -1
  256. package/phanpy/dist/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
  257. package/phanpy/dist/assets/icons/more-3-line-BIiVjYTz.js +0 -1
  258. package/phanpy/dist/assets/icons/notification-line-BZZq2Gtu.js +0 -1
  259. package/phanpy/dist/assets/icons/pencil-line-CKh8-A1v.js +0 -1
  260. package/phanpy/dist/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
  261. package/phanpy/dist/assets/icons/pin-line-DpTolyhs.js +0 -1
  262. package/phanpy/dist/assets/icons/play-fill-B0-G_8lX.js +0 -1
  263. package/phanpy/dist/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
  264. package/phanpy/dist/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
  265. package/phanpy/dist/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
  266. package/phanpy/dist/assets/icons/radar-line-CKyroTth.js +0 -1
  267. package/phanpy/dist/assets/icons/react-line-CoIKhhtK.js +0 -1
  268. package/phanpy/dist/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
  269. package/phanpy/dist/assets/icons/right-line-B3KWJaqc.js +0 -1
  270. package/phanpy/dist/assets/icons/rocket-line-4KZl537D.js +0 -1
  271. package/phanpy/dist/assets/icons/round-fill-C69NTrZ0.js +0 -1
  272. package/phanpy/dist/assets/icons/round-line-B9k_1uo-.js +0 -1
  273. package/phanpy/dist/assets/icons/route-line-B28ReHcE.js +0 -1
  274. package/phanpy/dist/assets/icons/rows-4-line-CrBniagM.js +0 -1
  275. package/phanpy/dist/assets/icons/scan-line-DSf74YdP.js +0 -1
  276. package/phanpy/dist/assets/icons/search-2-line-B6geIU1z.js +0 -1
  277. package/phanpy/dist/assets/icons/settings-3-line-azJwK47w.js +0 -1
  278. package/phanpy/dist/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
  279. package/phanpy/dist/assets/icons/share-2-line-D5MxWjrm.js +0 -1
  280. package/phanpy/dist/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
  281. package/phanpy/dist/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
  282. package/phanpy/dist/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
  283. package/phanpy/dist/assets/icons/time-line-BF3eet1v.js +0 -1
  284. package/phanpy/dist/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
  285. package/phanpy/dist/assets/icons/translate-line-BFWol2Ce.js +0 -1
  286. package/phanpy/dist/assets/icons/unlock-line-X_x5vwv3.js +0 -1
  287. package/phanpy/dist/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
  288. package/phanpy/dist/assets/icons/user-4-line-BVSynZCp.js +0 -1
  289. package/phanpy/dist/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
  290. package/phanpy/dist/assets/icons/user-add-line-BbXdHG4z.js +0 -1
  291. package/phanpy/dist/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
  292. package/phanpy/dist/assets/icons/user-follow-line-BXux0wSd.js +0 -1
  293. package/phanpy/dist/assets/icons/user-star-line-DdYlpHLU.js +0 -1
  294. package/phanpy/dist/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
  295. package/phanpy/dist/assets/icons/user-x-line-DIDgNlPH.js +0 -1
  296. package/phanpy/dist/assets/icons/volume-line-Bs390KQt.js +0 -1
  297. package/phanpy/dist/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
  298. package/phanpy/dist/assets/icons/walk-line-yNXfB-md.js +0 -1
  299. package/phanpy/dist/assets/icons/world-2-line-CenkGIwj.js +0 -1
  300. package/phanpy/dist/assets/icons/zoom-in-line-CaENUphf.js +0 -1
  301. package/phanpy/dist/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
  302. package/phanpy/dist/assets/instances-BNPptnn-.json +0 -333
  303. package/phanpy/dist/assets/locales/ar-SA-CfheRbCu.js +0 -1
  304. package/phanpy/dist/assets/locales/ca-ES-DHRW86Tz.js +0 -1
  305. package/phanpy/dist/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
  306. package/phanpy/dist/assets/locales/de-DE-ByRPMRFy.js +0 -1
  307. package/phanpy/dist/assets/locales/eo-UY-CPC7wLCS.js +0 -1
  308. package/phanpy/dist/assets/locales/es-ES-dJa0Fd9t.js +0 -1
  309. package/phanpy/dist/assets/locales/eu-ES-C3djlPR2.js +0 -1
  310. package/phanpy/dist/assets/locales/fa-IR-DTT490Ck.js +0 -1
  311. package/phanpy/dist/assets/locales/fi-FI-B23MCPuz.js +0 -1
  312. package/phanpy/dist/assets/locales/fr-FR-BcaQsO5C.js +0 -1
  313. package/phanpy/dist/assets/locales/gl-ES-BR-CVKBN.js +0 -1
  314. package/phanpy/dist/assets/locales/he-IL-CfheRbCu.js +0 -1
  315. package/phanpy/dist/assets/locales/hu-HU-CfheRbCu.js +0 -1
  316. package/phanpy/dist/assets/locales/it-IT-C1136UFV.js +0 -1
  317. package/phanpy/dist/assets/locales/ja-JP-CRZmRAhD.js +0 -1
  318. package/phanpy/dist/assets/locales/kab-Cz8xH_TT.js +0 -1
  319. package/phanpy/dist/assets/locales/ko-KR-CG9UiY4o.js +0 -1
  320. package/phanpy/dist/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
  321. package/phanpy/dist/assets/locales/nb-NO-CfheRbCu.js +0 -1
  322. package/phanpy/dist/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
  323. package/phanpy/dist/assets/locales/oc-FR-CfheRbCu.js +0 -1
  324. package/phanpy/dist/assets/locales/pl-PL-DkwGXf6P.js +0 -1
  325. package/phanpy/dist/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
  326. package/phanpy/dist/assets/locales/pt-BR-nGL0HdFS.js +0 -1
  327. package/phanpy/dist/assets/locales/pt-PT-Cp17aj2z.js +0 -1
  328. package/phanpy/dist/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
  329. package/phanpy/dist/assets/locales/th-TH-CfheRbCu.js +0 -1
  330. package/phanpy/dist/assets/locales/tok-CfheRbCu.js +0 -1
  331. package/phanpy/dist/assets/locales/tr-TR-CfheRbCu.js +0 -1
  332. package/phanpy/dist/assets/locales/uk-UA-C5E3dFBf.js +0 -1
  333. package/phanpy/dist/assets/locales/zh-CN-BHJi7_et.js +0 -1
  334. package/phanpy/dist/assets/locales/zh-TW-CfheRbCu.js +0 -1
  335. package/phanpy/dist/assets/main-BdqNbG-a.js +0 -26
  336. package/phanpy/dist/assets/main-BdqNbG-a.js.map +0 -1
  337. package/phanpy/dist/assets/mock-home-DGhR__He.js +0 -2
  338. package/phanpy/dist/assets/mock-home-DGhR__He.js.map +0 -1
  339. package/phanpy/dist/assets/multi-column-ROck0NVt.jpg +0 -0
  340. package/phanpy/dist/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  341. package/phanpy/dist/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  342. package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js +0 -2
  343. package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js.map +0 -1
  344. package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js +0 -20
  345. package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js.map +0 -1
  346. package/phanpy/dist/assets/style-B1iLPTPR.css +0 -2
  347. package/phanpy/dist/assets/temml-DuyIQMei.js +0 -97
  348. package/phanpy/dist/assets/temml-DuyIQMei.js.map +0 -1
  349. package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js +0 -2
  350. package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
  351. package/phanpy/dist/assets/year-in-posts-B2lywqdx.png +0 -0
  352. package/phanpy/dist/assets/year-in-posts-DJv_star.js +0 -3
  353. package/phanpy/dist/assets/year-in-posts-DJv_star.js.map +0 -1
  354. package/phanpy/dist/compose/index.html +0 -20
  355. package/phanpy/dist/favicon.ico +0 -0
  356. package/phanpy/dist/index.html +0 -73
  357. package/phanpy/dist/logo-192.png +0 -0
  358. package/phanpy/dist/logo-512.png +0 -0
  359. package/phanpy/dist/logo-badge-72.png +0 -0
  360. package/phanpy/dist/logo-maskable-512.png +0 -0
  361. package/phanpy/dist/logo-monochrome-512.png +0 -0
  362. package/phanpy/dist/logo-monochrome-maskable-512.png +0 -0
  363. package/phanpy/dist/manifest.webmanifest +0 -1
  364. package/phanpy/dist/og-image-2.jpg +0 -0
  365. package/phanpy/dist/og-image.png +0 -0
  366. package/phanpy/dist/robots.txt +0 -2
  367. package/phanpy/dist/sw.js +0 -2
  368. package/phanpy/dist/sw.js.map +0 -1
  369. package/phanpy/dist/version.json +0 -1
  370. package/run-agent.mjs +0 -899
  371. package/vendor/gate.cjs +0 -138
  372. package/vendor/idp-grant.cjs +0 -394
  373. package/vendor/jfetch.cjs +0 -16
  374. package/web/admin/actors.js +0 -147
  375. package/web/admin/bar.css +0 -126
  376. package/web/admin/bar.js +0 -99
  377. package/web/admin/client/client.js +0 -66
  378. package/web/admin/client/index.html +0 -69
  379. package/web/admin/client-pick.js +0 -79
  380. package/web/admin/common.js +0 -23
  381. package/web/admin/connections.js +0 -112
  382. package/web/admin/gateway.js +0 -133
  383. package/web/admin/group.js +0 -258
  384. package/web/admin/index.html +0 -612
  385. package/web/admin/notices-bar.js +0 -90
  386. package/web/admin/oauth-signin.mjs +0 -37
  387. package/web/admin/record.js +0 -393
  388. package/web/admin/setup/index.html +0 -257
  389. package/web/admin/setup/setup.js +0 -339
  390. package/web/admin/tokens.css +0 -68
  391. package/web/admin/upkeep.js +0 -177
  392. package/web/admin/window.css +0 -55
  393. package/web/admin/window.js +0 -136
  394. package/web/app/README.md +0 -81
  395. package/web/app/admin-facade.mjs +0 -588
  396. package/web/app/agent.mjs +0 -672
  397. package/web/app/atproto-browser.mjs +0 -86
  398. package/web/app/authorize_interaction.html +0 -56
  399. package/web/app/authorize_interaction.js +0 -69
  400. package/web/app/boot.mjs +0 -616
  401. package/web/app/deliver-relay.mjs +0 -129
  402. package/web/app/dist/boot.js +0 -34562
  403. package/web/app/dist/boot.js.map +0 -7
  404. package/web/app/dist/sw.js +0 -73456
  405. package/web/app/dist/sw.js.map +0 -7
  406. package/web/app/fediacct-browser.mjs +0 -146
  407. package/web/app/gateway-move.mjs +0 -60
  408. package/web/app/idb-kv.mjs +0 -35
  409. package/web/app/index.html +0 -258
  410. package/web/app/keys-browser.mjs +0 -71
  411. package/web/app/keystore.mjs +0 -82
  412. package/web/app/oidc-session.mjs +0 -6
  413. package/web/app/pod-auth.mjs +0 -248
  414. package/web/app/pod-remote.mjs +0 -84
  415. package/web/app/shims/_globals.mjs +0 -2
  416. package/web/app/shims/fedify-sig.mjs +0 -77
  417. package/web/app/shims/node-crypto.mjs +0 -113
  418. package/web/app/shims/node-fs.mjs +0 -17
  419. package/web/app/shims/node-path.mjs +0 -19
  420. package/web/app/shims/node-url.mjs +0 -7
  421. package/web/app/shims/prelude.js +0 -58
  422. package/web/app/shims/safefetch.mjs +0 -58
  423. package/web/app/shims/shapes-text.mjs +0 -8
  424. package/web/app/shims/web-push.mjs +0 -14
  425. package/web/app/signup.mjs +0 -357
  426. package/web/app/sw-src.mjs +0 -220
  427. package/web/app/update.js +0 -70
  428. package/web/front/admin.html +0 -55
  429. package/web/front/admin.js +0 -89
  430. package/web/front/install.sh +0 -69
  431. package/web/front/new-account.html +0 -35
  432. package/web/front/new-account.js +0 -13
  433. package/web/front/notices.html +0 -69
  434. package/web/front/notices.js +0 -107
  435. package/web/front/run.html +0 -64
  436. package/web/front/run.js +0 -221
  437. package/web/front/solid-oidc-client.js +0 -6
  438. package/web/icons/forum.svg +0 -11
  439. package/web/icons/gateway.svg +0 -14
  440. package/web/icons/local.svg +0 -11
@@ -1,649 +0,0 @@
1
- // activities.mjs — one handler per inbound activity type. Each is reached
2
- // from Intake.handle() after the sender passed the door checks, and returns a
3
- // rejection reason or nothing.
4
-
5
- import { dropFollower } from '../store.mjs';
6
- import { emojisOf, pollOf, isContentType, trimActivity, reactionOf, quoteOf, quotePolicyOf, authorOf } from './activity.mjs';
7
-
8
- // How long a follower's refollow waits before it is answered again.
9
- const REACCEPT_MS = 60 * 60 * 1000;
10
-
11
- const idOf = (v) => (typeof v === 'string' ? v : v?.id);
12
-
13
- // §7.6 Add / §7.9 Remove. The side effect would be to add or remove the object
14
- // to/from the collection named in `target` — but only a collection we own AND
15
- // that the sender is authorised to modify. No remote is granted write to our
16
- // collections (membership is Follow/Undo, pins are ours to set), so there is
17
- // nothing an inbound Add or Remove may change here. It is a valid activity,
18
- // not garbage: acknowledge it, make no change, and never dead-letter it.
19
- export function onAddRemove(intake, activity, actor) {
20
- const target = typeof activity.target === 'string' ? activity.target : activity.target?.id;
21
- const ours = target && [intake.urls.followers, intake.urls.following, intake.urls.featured]
22
- .filter(Boolean).includes(target);
23
- intake.log(ours
24
- ? `${activity.type} from ${actor} targets our ${target} — no remote may modify it; acknowledged`
25
- : `${activity.type} from ${actor} targets ${target || 'no collection of ours'}; nothing here to change`);
26
- return; // accepted, no side effect
27
- }
28
-
29
- export async function onFollow(intake, activity, actor, { trusted = false } = {}) {
30
- const doc = await intake.fetchAP(actor); // origin must vouch for the actor
31
- if (!doc) return `actor fetch failed (${actor})`;
32
- if (doc.id !== actor) return `actor id mismatch (${actor} vs ${doc.id})`;
33
- if (!doc.inbox) return `actor has no inbox (${actor})`;
34
- const contacts = intake.store.getContacts();
35
- const existing = contacts.followers.find(f => f.actor === actor);
36
- // NOTHING binds a delivered Follow to the actor it names. LDN bodies carry
37
- // no signature, and unlike Create, Delete and Update there is no object at
38
- // the origin to re-fetch and compare — dereferencing the actor proves only
39
- // that the actor EXISTS. So anyone at all could Append a Follow naming
40
- // anyone at all, and we would sign an Accept, deliver it to that person,
41
- // and send them everything published from then on.
42
- //
43
- // Until deliveries terminate somewhere their signature survives, a follow
44
- // we cannot verify is a REQUEST, waiting in the same queue a gated group
45
- // uses. The requester's client shows "Requested", which is the ordinary
46
- // locked-account state that manuallyApprovesFollowers tells it to expect.
47
- // `autoAcceptFollows: true` in config restores the old behaviour.
48
- // A GROUP is left alone: `approveJoins: false` is its operator saying, in
49
- // as many words, that anyone may join, and mute/eject are the remedy there.
50
- // A person has no such setting, so this is their default.
51
- //
52
- // A gateway-verified Follow (trust mode) is no longer unverifiable — the
53
- // door proved the sender — so it does not need the OK that unverifiability
54
- // alone demanded. An explicit `approveJoins` still holds: verified or not,
55
- // the operator asked to see joins.
56
- const unverifiedNeedsOk = intake.config.kind !== 'group' && !intake.config.autoAcceptFollows && !trusted;
57
- const mustApprove = intake.config.approveJoins || unverifiedNeedsOk;
58
- if (mustApprove && !existing) {
59
- const reqs = intake.store.getRequests();
60
- if (!reqs.some(r => r.actor === actor)) {
61
- reqs.unshift({
62
- actor, inbox: doc.inbox, sharedInbox: doc.endpoints?.sharedInbox,
63
- activity: trimActivity(activity), at: new Date().toISOString(),
64
- });
65
- intake.store.setRequests(reqs.slice(0, 500));
66
- intake.store.addNotification({ type: 'follow-request', actor });
67
- await intake.republish({ pending: true });
68
- }
69
- intake.log(`join requested: ${actor}`);
70
- return;
71
- }
72
- if (existing) {
73
- // Deliberately NOT updating followId. An inbound Follow is unverifiable —
74
- // that is what the queue above exists for — so letting one rewrite the id
75
- // of a follower we already hold hands an attacker the exact value onUndo
76
- // matches on: POST a Follow naming any follower in the published
77
- // collection, then POST an Undo naming the id you just chose, and they are
78
- // gone permanently. A genuine refollow needs nothing from us but the
79
- // Accept below, which is idempotent.
80
- } else {
81
- // A Bluesky member who bridges later arrives here as a second, different
82
- // actor: the bridge follows on their behalf from bsky.brid.gy/ap/<did>,
83
- // while the native join is recorded under bsky.app/profile/<did>. Left
84
- // alone that is one person listed twice, carried twice, and ejectable
85
- // only half at a time. The bridged record supersedes the native one —
86
- // it reaches the fediverse side, which the native one never could.
87
- const bridgedDid = /^https:\/\/bsky\.brid\.gy\/ap\/(did:[^/]+)$/.exec(actor)?.[1];
88
- if (bridgedDid) {
89
- const before = contacts.followers.length;
90
- contacts.followers = contacts.followers.filter(f => f.bsky?.did !== bridgedDid);
91
- if (contacts.followers.length < before) {
92
- intake.log(`bluesky member ${bridgedDid} is bridged now — the native record gives way to it`);
93
- }
94
- }
95
- contacts.followers.push({
96
- actor, inbox: doc.inbox, sharedInbox: doc.endpoints?.sharedInbox, followId: activity.id,
97
- ...(bridgedDid ? { bsky: { did: bridgedDid, bridged: true } } : {}),
98
- });
99
- intake.store.setContacts(contacts);
100
- intake.store.addNotification({ type: 'follow', actor });
101
- await intake.republish({ followers: true });
102
- intake.log(`new follower: ${actor}`);
103
- }
104
- // A refollow from someone already following is answered again — their server
105
- // may have lost our Accept — but not every time: the same Follow sent over and
106
- // over would otherwise make us send, and record, an Accept for each one.
107
- if (existing) {
108
- const c = intake.store.getContacts();
109
- const f = c.followers.find(x => x.actor === actor);
110
- if (f?.acceptedAt && Date.now() - Date.parse(f.acceptedAt) < REACCEPT_MS) {
111
- intake.log(`refollow from ${actor} — already accepted within the hour, not answered again`);
112
- return;
113
- }
114
- if (f) { f.acceptedAt = new Date().toISOString(); intake.store.setContacts(c); }
115
- }
116
- const { acceptActivity } = await import('../wire.mjs');
117
- await intake.deliverer.deliver(doc.inbox,
118
- acceptActivity({ urls: intake.urls, followActivity: activity, serial: intake.serial++ }));
119
- intake.log(`Accept sent → ${doc.inbox}`);
120
- }
121
-
122
- export async function onUndo(intake, activity, actor, { trusted = false } = {}) {
123
- const inner = activity.object && typeof activity.object === 'object' ? activity.object : null;
124
- if (inner?.type === 'Like' || inner?.type === 'Announce' || inner?.type === 'EmojiReact') return onUndoReaction(intake, inner, actor, { trusted });
125
- // AS2 allows `object` to be a bare IRI, and that IRI is exactly the Follow
126
- // id we stored. Reading `.type` off a string gives undefined, so the whole
127
- // Undo was dropped — silently, since handle() reads that as handled, so no
128
- // dead letter was kept and the item was DELETEd. The follower stayed, we
129
- // kept delivering to them, and their server had recorded the unfollow as
130
- // done and would never resend. Only a TYPED non-Follow is not ours.
131
- if (typeof activity.object === 'object' && activity.object?.type
132
- && activity.object.type !== 'Follow') return;
133
- // And it must NAME something. Widening the type test to admit a bare IRI
134
- // also admitted `object: undefined`, `null` and `{}` — which land on the
135
- // no-followId carve-out below and evict, which is the very hole the
136
- // followId check was added to close. An Undo that identifies nothing is
137
- // not an Undo of ours.
138
- const named = typeof activity.object === 'string' ? activity.object : activity.object?.id;
139
- // A gateway-verified Undo need not name a stored id — the door proved the
140
- // sender, so an Undo{Follow} that names us as its object is enough. An
141
- // unverified one must still identify something (the eviction-hole guard).
142
- if (!named && !trusted) return;
143
- // Deliveries arrive unordered: an Undo may land AFTER the refollow it
144
- // predates. It names the Follow id it revokes — only honor it when it
145
- // matches the follow we currently hold for that actor.
146
- const undoneId = typeof activity.object === 'string' ? activity.object : activity.object?.id;
147
- const contacts = intake.store.getContacts();
148
- const rec = contacts.followers.find(f => f.actor === actor);
149
- // Withdrawing a request that was never answered: drop it, or it sits in the
150
- // operator's queue forever asking about someone who left.
151
- if (!rec) {
152
- const reqs = intake.store.getRequests();
153
- const pending = reqs.find(r => r.actor === actor);
154
- if (pending) {
155
- // Bound the same way the follower eviction below is, and for the same
156
- // reason: `actor` is a field in an unsigned body, so without a match
157
- // anyone could withdraw anyone else's waiting request. The victim's
158
- // server believes the Follow is still pending and never resends, so
159
- // they simply never get followed and nobody sees why.
160
- const theirs = pending.activity?.id;
161
- if (!trusted && (!theirs || named !== theirs)) {
162
- intake.log(`Undo from ${actor} does not name the request we hold — ignored`);
163
- return;
164
- }
165
- intake.store.setRequests(reqs.filter(r => r.actor !== actor));
166
- await intake.republish({ pending: true });
167
- intake.log(`join request withdrawn: ${actor}`);
168
- }
169
- return;
170
- }
171
- // An Undo must NAME the Follow it revokes, and name the one we hold.
172
- //
173
- // The follow id is the ONLY thing binding an Undo to the follower. LDN
174
- // bodies carry no signature, and unlike every other inbound type this path
175
- // dereferences nothing, so there is no origin to disagree. Matching works
176
- // because the id was chosen by their server and delivered in a Follow we
177
- // accepted: we publish the followers collection, but never the ids.
178
- //
179
- // Which means a record with NO id cannot be matched at all — and the
180
- // carve-out that used to let those through turned "we cannot tell" into
181
- // "anyone may evict". reconcileFollowers writes exactly such records when a
182
- // restored machine recovers its followers from the pod, so after a restore
183
- // every follower could be removed by one unauthenticated POST, permanently:
184
- // dropFollower leaves a mark and the next reconcile will not bring them
185
- // back, their server recorded no unfollow so it never resends, and neither
186
- // side has anything to notice.
187
- //
188
- // Unmatchable is refused now. The cost is a follower who really did leave
189
- // staying on the list until the operator ejects them, which is the right way
190
- // round: `eject` is one command, and the alternative was silent, permanent,
191
- // and available to anyone.
192
- // A gateway-verified Undo carries the sender's proof, so it is honored on
193
- // its own — the followId match below exists only because an UNVERIFIED Undo
194
- // is otherwise unbindable. A verified one needs no such crutch.
195
- if (trusted) {
196
- dropFollower(contacts, actor, 'undo-follow');
197
- intake.store.setContacts(contacts);
198
- await intake.republish({ followers: true });
199
- intake.log(`unfollowed by ${actor} (gateway-verified)`);
200
- return;
201
- }
202
- if (!rec.followId) {
203
- intake.log(`Undo from ${actor} cannot be matched — this follower was `
204
- + `${rec.recovered ? 'recovered from the pod' : 'recorded before follow ids were kept'}, `
205
- + `so its follow id is unknown. Ignored; \`fedipod eject ${actor}\` if they did leave.`);
206
- return;
207
- }
208
- if (undoneId !== rec.followId) {
209
- intake.log(`Undo from ${actor} does not name the follow we hold `
210
- + `(revokes ${undoneId || 'nothing'}, current is ${rec.followId}) — ignored`);
211
- return;
212
- }
213
- dropFollower(contacts, actor, 'undo-follow');
214
- intake.store.setContacts(contacts);
215
- await intake.republish({ followers: true });
216
- intake.log(`unfollowed by ${actor}`);
217
- }
218
-
219
- export async function onCreate(intake, activity, actor, { trusted = false } = {}) {
220
- const objectId = typeof activity.object === 'string' ? activity.object : activity.object?.id;
221
- if (!objectId) return 'Create without object id';
222
- if (intake.store.isBlocked(objectId)) return `blocked domain (${objectId})`;
223
- if (!intake.sameIdentity(objectId, actor)) return `object/actor identity mismatch (${objectId})`;
224
- // The delivered copy is untrusted for CONTENT, but its addressing is
225
- // enough to decide whether to bother fetching the origin's copy.
226
- const envelope = typeof activity.object === 'object' ? { ...activity, ...activity.object } : activity;
227
- if (!intake.concernsUs(envelope, actor)) return `not addressed to us (${objectId})`;
228
- // The check onAnnounce has had all along. A re-delivered Create — a remote
229
- // retry, a group fan-out, or our own sweep seeing an item whose DELETE was
230
- // refused — cost a fresh signed GET to the origin and rewrote the private
231
- // RDF note every time, because addStatus only dedupes AFTER the deref.
232
- // Gated around the INGEST alone: a group must still reach amplify below,
233
- // which is separately idempotent on announcedAt.
234
- // Only a status that came from an INGEST counts as already done. TagFeed
235
- // writes a bare `kind:'tag'` row straight into the index — no pod RDF note,
236
- // no mention notification, no replies-collection entry — so treating that
237
- // as ingested loses all three when the same note is then delivered to us.
238
- const ingested = intake.store.getStatuses()
239
- .some(x => x.noteId === objectId && (x.kind === 'timeline' || x.kind === 'mention'));
240
- if (!ingested) {
241
- // A verified delivery that carries the note inline is read from that
242
- // copy: the door checked the sender's signature over these very bytes,
243
- // and the receipt vouches for this actor. That is the only way a direct
244
- // or followers-only post can ever be read — it lives in the sender's
245
- // owner-only container, and fetching it from there answers 401 to
246
- // everyone. An unverified delivery still fetches, as it always did.
247
- const inline = trusted && typeof activity.object === 'object' && activity.object?.id === objectId
248
- ? activity.object : null;
249
- const rejected = await intake.ingestNote(objectId, actor, { inline });
250
- if (rejected) return rejected;
251
- }
252
- // A group carries its members' posts onward. Only reached from Create, so an
253
- // inbound Announce is never re-announced. The activity is passed through
254
- // untouched — FEP-1b12 wants the original wrapped, not a summary of it.
255
- if (intake.config.kind === 'group') await intake.amplify(objectId, { activity });
256
- }
257
-
258
- // A boost: ingest the boosted note when the booster is someone we follow —
259
- // that's what following means, their boosts widen the timeline. Anything
260
- // else is unsolicited and only logged.
261
- // A group we follow announces a Delete: the carrier moderating away a post
262
- // it carried. Honored only within what the carry itself established — the
263
- // announcer is a group we follow AND the post reached us via that same
264
- // group — so no new party is trusted and nothing is dereferenced. Our own
265
- // posts are never removed by anyone's moderation.
266
- export async function onAnnouncedDelete(intake, actor, del) {
267
- const followed = intake.store.getContacts().following.some(f => f.actor === actor && f.accepted);
268
- if (!followed) { intake.log(`announced Delete from unfollowed ${actor} — ignored`); return; }
269
- const targetId = typeof del.object === 'string' ? del.object : del.object?.id;
270
- if (!targetId) return 'announced Delete without an object';
271
- const s = intake.store.getStatuses().find(x => x.noteId === targetId);
272
- if (!s || s.kind === 'post' || s.via !== actor) return;
273
- await intake.forget(s);
274
- intake.log(`moderated away by ${actor}: ${targetId}`);
275
- }
276
-
277
- export async function onAnnounce(intake, activity, actor, objectId) {
278
- if (!objectId) return 'Announce without object id';
279
- const followed = intake.store.getContacts().following.some(f => f.actor === actor && f.accepted);
280
- if (!followed) { intake.log(`Announce from unfollowed ${actor} — ignored`); return; }
281
- if (intake.store.isBlocked(objectId)) return `blocked domain (${objectId})`;
282
- const existing = intake.store.getStatuses().find(s => s.noteId === objectId);
283
- if (existing) {
284
- // Known, but possibly as a lesser kind — a stranger's mention, a tag or
285
- // search mirror — none of which the home timeline shows. A carry from
286
- // someone we follow is exactly what promotes it there.
287
- if (!['timeline', 'post'].includes(existing.kind)) {
288
- intake.store.updateStatus(objectId, { kind: 'timeline', via: actor });
289
- intake.log(`promoted to timeline (carried by ${actor}): ${objectId}`);
290
- }
291
- return;
292
- }
293
- return intake.ingestNote(objectId, actor, { via: actor });
294
- }
295
-
296
- // Undo{Like} and Undo{Announce}: the sender unsaying a favourite or a boost.
297
- // The Like it unsays was recorded on the sender's word alone, so its undoing
298
- // is taken the same way: the notification it left goes. A boost unsaid by
299
- // someone we follow takes the post they carried off the timeline — the carry
300
- // was accepted on that follow, and the retraction is honoured on it.
301
- export async function onUndoReaction(intake, inner, actor, { trusted = false } = {}) {
302
- const innerActor = typeof inner.actor === 'string' ? inner.actor : inner.actor?.id;
303
- if (innerActor && innerActor !== actor) return; // one cannot unsay another's
304
- const wrapped = inner.object;
305
- const target = (wrapped && typeof wrapped === 'object'
306
- && (wrapped.type === 'Create' || wrapped.type === 'Update')) ? wrapped.object : wrapped;
307
- const objectId = typeof target === 'string' ? target : target?.id;
308
- if (!objectId) return;
309
- if (objectId.startsWith(intake.urls.notes)) {
310
- // An emoji taken back: the one reaction it names, by that sender.
311
- const reaction = reactionOf(inner);
312
- if (reaction) {
313
- const gone = intake.store.removeNotifications(n => n.type === 'reaction' && n.actor === actor
314
- && n.noteId === objectId && n.emoji === reaction.emoji);
315
- if (gone) intake.log(`reaction ${reaction.emoji} withdrawn by ${actor} on ${objectId}`);
316
- return;
317
- }
318
- if (inner.type === 'EmojiReact') return;
319
- const type = inner.type === 'Like' ? 'favourite' : 'reblog';
320
- const gone = intake.store.removeNotifications(n => n.type === type && n.actor === actor && n.noteId === objectId);
321
- if (gone) intake.log(`${inner.type} withdrawn by ${actor} on ${objectId}`);
322
- return;
323
- }
324
- if (inner.type !== 'Announce') return;
325
- const followed = trusted || intake.store.getContacts().following.some(f => f.actor === actor && f.accepted);
326
- if (!followed) return;
327
- const s = intake.store.getStatuses().find(x => x.noteId === objectId);
328
- if (!s || s.kind === 'post' || s.via !== actor) return;
329
- await intake.forget(s);
330
- intake.log(`boost withdrawn by ${actor}: ${objectId} left the timeline`);
331
- }
332
-
333
- // Block: the sender wants nothing more to do with this account. Mastodon
334
- // drops the follow both ways on receipt, and so does this — on a delivery
335
- // the door verified. A Block carries nothing to check and names nothing we
336
- // hold, so one taken from the public-append inbox unverified would let
337
- // anybody evict any follower, the hole the Undo handler closes with the
338
- // follow id. Unverified: logged, nothing else.
339
- export async function onBlock(intake, activity, actor, { trusted = false } = {}) {
340
- const objectId = typeof activity.object === 'string' ? activity.object : activity.object?.id;
341
- if (objectId !== intake.urls.actor) return;
342
- if (!trusted) { intake.log(`Block from ${actor} arrived unverified — ignored`); return; }
343
- const contacts = intake.store.getContacts();
344
- const wasFollower = contacts.followers.some(f => f.actor === actor);
345
- const wasFollowing = contacts.following.some(f => f.actor === actor);
346
- if (wasFollower) dropFollower(contacts, actor, 'blocked-us');
347
- contacts.following = contacts.following.filter(f => f.actor !== actor);
348
- intake.store.setContacts(contacts);
349
- const reqs = intake.store.getRequests();
350
- const left = reqs.filter(r => r.actor !== actor);
351
- if (left.length !== reqs.length) intake.store.setRequests(left);
352
- await intake.republish({ followers: wasFollower, following: wasFollowing, pending: true });
353
- const what = [wasFollower && 'their follow of us', wasFollowing && 'our follow of them'].filter(Boolean).join(' and ');
354
- intake.log(`blocked by ${actor}: ${what || 'no relationship'} dropped`);
355
- }
356
-
357
- // Mastodon sends these constantly; ignoring them left deleted posts standing
358
- // for good. Two guards, because a forged Delete would otherwise erase anyone's
359
- // content: it must come from the object's own origin, and the object must
360
- // really be gone there. An origin we cannot reach is a retry, never a delete.
361
- export async function onDelete(intake, activity, actor) {
362
- const objectId = typeof activity.object === 'string' ? activity.object : activity.object?.id;
363
- if (!objectId) return 'Delete without object id';
364
- if (!intake.sameIdentity(objectId, actor)) return `Delete crosses identities (${objectId})`;
365
- // `objectId === actor` is NOT evidence we care: it is true of EVERY account
366
- // deletion, and Mastodon broadcasts those constantly. Taking it as known
367
- // meant a signed dereference to a stranger's server for each one.
368
- if (!intake.known(objectId)) return; // nothing of ours to remove
369
- const gone = await intake.isGone(objectId);
370
- if (gone === null) throw new Error(`cannot confirm ${objectId} is gone — will retry`);
371
- if (!gone) return `Delete for something still published (${objectId})`;
372
-
373
- if (objectId === actor) { // the account itself
374
- const contacts = intake.store.getContacts();
375
- dropFollower(contacts, actor, 'account-deleted');
376
- contacts.following = contacts.following.filter(f => f.actor !== actor);
377
- intake.store.setContacts(contacts);
378
- // One publish for the lot. Each forget() used to run its own
379
- // unrecordOutbox, and each of those republished the outbox — so a group
380
- // that had carried M of this actor's posts paid M full page sweeps for a
381
- // single inbox item. The Undo deliveries stay per-Announce, because each
382
- // Announce needs its own; only the pod write is collected.
383
- const retracted = [];
384
- for (const s of intake.store.getStatuses().filter(s => s.actor === actor)) {
385
- await intake.forget(s, { collect: retracted });
386
- }
387
- if (retracted.length) {
388
- const gone = new Set(retracted.map(u => u.object.id));
389
- await intake.publisher.unrecordOutbox(i => gone.has(i?.id),
390
- { record: (was) => (intake.config.private ? null : retracted.filter(u => was.includes(u.object.id))) });
391
- }
392
- // Both: an account deletion drops them from followers AND following.
393
- await intake.republish({ followers: true, following: true });
394
- intake.log(`account deleted upstream: ${actor}`);
395
- return;
396
- }
397
- const s = intake.store.getStatuses().find(x => x.noteId === objectId);
398
- if (s) await intake.forget(s);
399
- if (s && intake.onCarriedGone) {
400
- await intake.onCarriedGone({ noteId: objectId, actor }).catch(e => intake.log(`carried gone ${objectId}: ${e.message}`));
401
- }
402
- intake.log(`deleted upstream: ${objectId}`);
403
- }
404
-
405
- // An edited post, or a changed profile. Verified the only way we can: by
406
- // refetching at the origin and believing that, not the delivered copy.
407
- export async function onUpdate(intake, activity, actor) {
408
- const objectId = typeof activity.object === 'string' ? activity.object : activity.object?.id;
409
- if (!objectId) return 'Update without object id';
410
- if (!intake.sameIdentity(objectId, actor)) return `Update crosses identities (${objectId})`;
411
- if (objectId === actor) { // display name, avatar, bio
412
- // The same guard onDelete has, for the same reason: the inbox is
413
- // public-Append, so without it anyone can name any host and make us spend
414
- // a signed GET on it — and because the failure below THROWS rather than
415
- // returning a rejection, one planted item buys five of them, plus five
416
- // pod reads and the head of the inbox held for five sweeps.
417
- if (!intake.known(actor)) return; // nothing of ours to update
418
- const doc = await intake.fetchAP(actor);
419
- if (!doc) throw new Error(`cannot refetch ${actor} — will retry`);
420
- intake.store.cacheActor(actor, doc); // fetchAP caches Persons; Groups too
421
- intake.log(`profile updated: ${actor}`);
422
- return;
423
- }
424
- const s = intake.store.getStatuses().find(x => x.noteId === objectId);
425
- if (!s) return; // not one we hold
426
- const note = await intake.fetchAP(objectId);
427
- if (!note) throw new Error(`cannot refetch ${objectId} — will retry`);
428
- if (note.id !== objectId || !isContentType(note.type)) return `object not verifiable content (${objectId}, ${note.type})`;
429
- const { attachmentsOf, titledContent } = await import('../wire.mjs');
430
- const content = titledContent(note);
431
- const attachments = attachmentsOf(note);
432
- const freshPoll = pollOf(note);
433
- const freshEmojis = emojisOf(note);
434
- // A quote's authorization arrives on an Update, once the quoted author
435
- // has answered: the quote is checked again, and the policy follows the edit.
436
- const quote = quoteOf(note);
437
- const quoteState = quote ? await intake.quoteStateOf(note, quote) : null;
438
- const quotePolicy = quotePolicyOf(note, s.actor);
439
- intake.store.updateStatus(objectId, {
440
- content, ...(attachments.length ? { attachments } : {}),
441
- emojis: freshEmojis.length ? freshEmojis : undefined,
442
- quote: quote || undefined, quoteState: quoteState || undefined,
443
- quotePolicy: quotePolicy || undefined,
444
- // The edit's own stamp when the note carries one; tallies and the
445
- // content warning follow the edit too. A poll refresh keeps our vote.
446
- editedAt: note.updated || new Date().toISOString(),
447
- spoiler: note.summary ? String(note.summary).replace(/<[^>]*>/g, '') : undefined,
448
- ...(freshPoll ? {
449
- poll: { ...freshPoll, voted: !!s.poll?.voted, ownVotes: s.poll?.ownVotes || [] },
450
- } : {}),
451
- });
452
- if (intake.onCarriedEdit) {
453
- await intake.onCarriedEdit({ noteId: objectId, actor, note }).catch(e => intake.log(`carried edit ${objectId}: ${e.message}`));
454
- }
455
- intake.log(`edited upstream: ${objectId}`);
456
- }
457
-
458
- // The other answer to a Follow, and it was dropped on the floor. Their server
459
- // has recorded that we do not follow them; ours went on saying we did, and
460
- // published it — so the two disagreed permanently, and a retry would never
461
- // come because as far as they are concerned the question was answered.
462
- //
463
- // It has to answer the Follow we actually SENT. Only the type was checked, so
464
- // one Append per account you follow — from anyone, naming no particular
465
- // follow — severed every one of them at once, and silently: their server
466
- // never hears about it, so nothing ever retries and nothing looks wrong until
467
- // the timeline goes quiet. `followActivity` is stored by followActor
468
- // (lib/social.mjs) for exactly this kind of comparison.
469
- export async function onReject(intake, activity, actor, { trusted = false } = {}) {
470
- const asked = quoteAsked(intake, activity);
471
- if (asked) return onQuoteAnswered(intake, asked, activity, actor, 'Reject');
472
- if (activity.object?.type && activity.object.type !== 'Follow') return;
473
- const contacts = intake.store.getContacts();
474
- const rec = contacts.following.find(f => f.actor === actor);
475
- if (!rec) return; // nothing of ours to undo
476
- const named = typeof activity.object === 'string' ? activity.object : activity.object?.id;
477
- const ours = rec.followActivity?.id;
478
- // A gateway receipt bound to this actor is the other way to believe it —
479
- // the door checked a signature and said whose (see receiptVouchesFor).
480
- if (!trusted) {
481
- if (!ours) {
482
- intake.log(`Reject from ${actor}: no follow id on record to match it against — ignored`);
483
- return;
484
- }
485
- if (named !== ours) {
486
- intake.log(`Reject from ${actor} answers ${named || 'nothing'}, not the follow we sent — ignored`);
487
- return;
488
- }
489
- }
490
- contacts.following = contacts.following.filter(f => f.actor !== actor);
491
- intake.store.setContacts(contacts);
492
- await intake.republish({ following: true, pending: true });
493
- intake.log(`follow rejected by ${actor}`);
494
- }
495
-
496
- // Someone we follow has moved. Their server will stop delivering from the old
497
- // actor, so without this we keep an entry that can never produce another post
498
- // and never learn where they went. The new account is not followed
499
- // automatically — that is a Follow only the owner should send — but it is
500
- // recorded and raised, so it can be acted on.
501
- export async function onMove(intake, activity, actor) {
502
- const target = typeof activity.target === 'string' ? activity.target : activity.target?.id;
503
- if (!target) return 'Move without a target';
504
- const contacts = intake.store.getContacts();
505
- const rec = contacts.following.find(f => f.actor === actor);
506
- if (!rec) return; // not someone we follow
507
- // Believed only if the actor we follow says so at its OWN origin: a Move is
508
- // otherwise a redirect anyone could Append.
509
- const doc = await intake.fetchAP(actor);
510
- if (!doc) throw new Error(`cannot confirm ${actor} moved — will retry`);
511
- const movedTo = typeof doc.movedTo === 'string' ? doc.movedTo : doc.movedTo?.id;
512
- if (movedTo !== target) return `Move not corroborated by ${actor} (says ${movedTo || 'nothing'})`;
513
- rec.movedTo = target;
514
- intake.store.setContacts(contacts);
515
- intake.store.addNotification({ type: 'move', actor, target });
516
- intake.log(`${actor} moved to ${target} — follow the new account to keep seeing them`);
517
- }
518
-
519
- export async function onAccept(intake, activity, actor, { trusted = false } = {}) {
520
- const asked = quoteAsked(intake, activity);
521
- if (asked) return onQuoteAnswered(intake, asked, activity, actor, 'Accept');
522
- const contacts = intake.store.getContacts();
523
- const rec = contacts.following.find(f => f.actor === actor);
524
- // It has to answer the Follow we actually sent. followActor stores that
525
- // activity for the later Undo, so the id is here to compare against; without
526
- // the check any Accept from an actor we happen to follow flips the flag,
527
- // including one answering a Follow we never made.
528
- const named = typeof activity.object === 'string' ? activity.object : activity.object?.id;
529
- const ours = rec?.followActivity?.id;
530
- // `named &&` used to be part of this, so an Accept naming NOTHING sailed
531
- // past — which is the easy one to send, and it marks a request to a locked
532
- // account as accepted when it is still sitting in their queue.
533
- if (ours && named !== ours && !trusted) {
534
- intake.log(`Accept from ${actor} answers ${named || 'nothing'}, not the follow we sent — ignored`);
535
- return;
536
- }
537
- if (rec && !rec.accepted) {
538
- rec.accepted = true;
539
- intake.store.setContacts(contacts);
540
- await intake.republish({ following: true, pending: true });
541
- intake.log(`follow accepted by ${actor}`);
542
- }
543
- }
544
-
545
- // --- quotes (FEP-044f) ---
546
-
547
- // The post of ours whose quote request this Accept or Reject answers, if any.
548
- // Matched on the request id we sent, which only the quoted author was given.
549
- function quoteAsked(intake, activity) {
550
- const named = idOf(activity.object);
551
- if (!named) return null;
552
- return intake.store.getStatuses().find(x => x.kind === 'post' && x.quoteRequest?.id === named) || null;
553
- }
554
-
555
- // The quoted author's answer. A yes carries their authorization, a document
556
- // at their origin naming our post and theirs — fetched and compared, not
557
- // believed from the envelope; then the note says so to everyone who has it.
558
- // A no takes the quote off the note.
559
- async function onQuoteAnswered(intake, s, activity, actor, type) {
560
- if (actor !== s.quoteRequest?.actor) {
561
- intake.log(`${type} of our quote request from ${actor}, who is not ${s.quoteRequest?.actor} — ignored`);
562
- return;
563
- }
564
- if (type === 'Reject') {
565
- if (s.quoteState === 'rejected') return;
566
- await intake.publisher.withdrawQuote(s);
567
- return;
568
- }
569
- if (s.quoteState === 'accepted') return;
570
- const resultId = idOf(activity.result);
571
- if (!resultId || !intake.sameIdentity(resultId, actor)) return `Accept of a quote request carries no authorization at ${actor}`;
572
- const auth = await intake.fetchAP(resultId);
573
- if (!auth) throw new Error(`cannot fetch quote authorization ${resultId} — will retry`);
574
- const ok = auth.id === resultId && auth.type === 'QuoteAuthorization'
575
- && [].concat(auth.attributedTo || []).map(idOf).includes(actor)
576
- && idOf(auth.interactingObject) === s.noteId && idOf(auth.interactionTarget) === s.quote;
577
- if (!ok) return `quote authorization ${resultId} does not say ${s.noteId} may quote ${s.quote}`;
578
- await intake.publisher.authorizeQuote(s, resultId);
579
- }
580
-
581
- // Someone asks to quote one of our posts. Their post is read at its origin,
582
- // like every note that arrives — it has to be theirs, and has to quote ours.
583
- // A post the world can read may be quoted by anyone; a followers-only or
584
- // direct one by nobody. The answer is an Accept carrying our authorization,
585
- // written beside the post, or a Reject. The quoting post is then kept the
586
- // way a mention of ours is, under the one notification a quote raises.
587
- export async function onQuoteRequest(intake, activity, actor) {
588
- const objectId = idOf(activity.object);
589
- const instrumentId = idOf(activity.instrument);
590
- if (!objectId || !instrumentId) return 'QuoteRequest without object or instrument';
591
- // A followers-only post lives in the private container; a follower may
592
- // know its id and ask. It is ours, and the answer below is no.
593
- const ours = objectId.startsWith(intake.urls.notes)
594
- || (intake.urls.privateNotes && objectId.startsWith(intake.urls.privateNotes));
595
- if (!ours) {
596
- intake.log(`QuoteRequest from ${actor} names ${objectId}, not a post of ours — ignored`);
597
- return;
598
- }
599
- if (!intake.sameIdentity(instrumentId, actor)) return `QuoteRequest instrument/actor identity mismatch (${instrumentId})`;
600
- const s = intake.store.getStatuses().find(x => x.noteId === objectId && x.kind === 'post');
601
- if (!s) return `QuoteRequest for a post we do not hold (${objectId})`;
602
- // Not retried on failure: the instrument is a URL a stranger chose.
603
- const note = await intake.fetchAP(instrumentId);
604
- if (!note) return `quote request: instrument fetch failed (${instrumentId})`;
605
- if (note.id !== instrumentId || !isContentType(note.type)) return `quote request: instrument is not verifiable content (${instrumentId})`;
606
- if (authorOf(note, actor) !== actor) return `quote request: ${instrumentId} is not by ${actor}`;
607
- if (quoteOf(note) !== objectId) return `quote request: ${instrumentId} does not quote ${objectId}`;
608
- const doc = await intake.fetchAP(actor);
609
- const inbox = doc?.endpoints?.sharedInbox || doc?.inbox;
610
- if (!inbox) return `quote request: no inbox for ${actor}`;
611
- const { quoteAnswerActivity } = await import('../wire-quotes.mjs');
612
- const open = !s.visibility || s.visibility === 'public' || s.visibility === 'unlisted';
613
- if (!open) {
614
- await intake.deliverer.deliver(inbox,
615
- quoteAnswerActivity({ urls: intake.urls, type: 'Reject', request: activity, to: actor, serial: intake.serial++ }));
616
- intake.log(`quote refused: ${instrumentId} asked to quote ${objectId}, which is not public`);
617
- return;
618
- }
619
- const authorization = await intake.publisher.grantQuote(objectId, instrumentId);
620
- await intake.deliverer.deliver(inbox,
621
- quoteAnswerActivity({ urls: intake.urls, type: 'Accept', request: activity, result: authorization, to: actor, serial: intake.serial++ }));
622
- const held = intake.store.getStatuses().find(x => x.noteId === instrumentId);
623
- if (!held) {
624
- // The origin's copy, already fetched above.
625
- const rejected = await intake.ingestNote(instrumentId, actor, { inline: note });
626
- if (rejected) intake.log(`quote of ${objectId} by ${actor}: ${rejected}`);
627
- } else if (held.quoteState !== 'accepted') {
628
- intake.store.updateStatus(instrumentId, { quote: objectId, quoteState: 'accepted' });
629
- }
630
- intake.store.addNotification({ type: 'quote', actor, noteId: instrumentId });
631
- intake.log(`quote allowed: ${instrumentId} quotes ${objectId}`);
632
- }
633
-
634
- // What can be said about a note's quote. The quoted post is ours: allowed if
635
- // we granted it. Elsewhere: allowed when the note carries an authorization
636
- // that, fetched from the quoted post's own origin, names this note and that
637
- // one. Otherwise pending — the honest word for a quote nobody has vouched for.
638
- export async function quoteStateOf(intake, note, quote) {
639
- if (quote.startsWith(intake.urls.notes)) {
640
- const target = intake.store.getStatuses().find(x => x.noteId === quote);
641
- return (target?.quotedBy || []).some(q => q.note === note.id) ? 'accepted' : 'pending';
642
- }
643
- const authId = idOf(note.quoteAuthorization);
644
- if (!authId || !intake.sameIdentity(authId, quote)) return 'pending';
645
- const auth = await intake.fetchAP(authId).catch(() => null);
646
- const ok = auth && auth.id === authId && auth.type === 'QuoteAuthorization'
647
- && idOf(auth.interactingObject) === note.id && idOf(auth.interactionTarget) === quote;
648
- return ok ? 'accepted' : 'pending';
649
- }