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,360 +0,0 @@
1
- // notes.mjs — a note on its way in: whether it concerns us, fetching it at
2
- // its origin, filing it, the replies collection on our own posts, and the
3
- // two ways a held post leaves again (forget, retract). Also the §7.1.2
4
- // forward of a reply into one of our threads to our followers.
5
-
6
- import * as podNotes from '../../pod/notes.mjs';
7
- import { PUBLIC } from '../wire.mjs';
8
- import * as polls from '../polls.mjs';
9
- import { httpOnly, emojisOf, pollOf, isContentType, authorOf, quoteOf, quotePolicyOf, MAX_MENTIONS, MAX_URL_CHARS, MAX_REPLIES_RECORDED, MAX_FORWARDED, FORWARDABLE, FORWARD_TYPES } from './activity.mjs';
10
-
11
- // Does this activity/note concern us at all? Either it comes from someone
12
- // we follow, or it names us (to/cc, mention tag) or replies to one of our
13
- // notes. Anything else is a stranger blasting inboxes — refuse it before
14
- // spending a dereference on it.
15
- export function concernsUs(intake, doc, actor) {
16
- if (intake.store.getContacts().following.some(f => f.actor === actor && f.accepted)) return true;
17
- const audience = []
18
- .concat(doc?.to || [], doc?.cc || [], doc?.bto || [], doc?.bcc || [], doc?.audience || [])
19
- .map(v => (typeof v === 'string' ? v : v?.id)).filter(Boolean);
20
- if (audience.includes(intake.urls.actor) || audience.includes(intake.urls.followers)) return true;
21
- const tagged = [].concat(doc?.tag || [])
22
- .some(t => t?.type === 'Mention' && (t.href === intake.urls.actor || t.name?.includes(intake.urls.actor)));
23
- if (tagged) return true;
24
- // A post that quotes one of ours is about us as much as a reply is.
25
- const quoted = quoteOf(doc);
26
- if (quoted && quoted.startsWith(intake.urls.notes)) return true;
27
- const inReplyTo = typeof doc?.inReplyTo === 'string' ? doc.inReplyTo : doc?.inReplyTo?.id;
28
- if (!inReplyTo) return false;
29
- if (String(inReplyTo).startsWith(intake.urls.notes)) return true;
30
- // A group also owns the conversation under anything it carried. Without
31
- // this, a reply that lost the group's mention on its way round the
32
- // fediverse is refused, and the thread breaks for everyone who was only
33
- // ever following the group.
34
- return intake.config.kind === 'group'
35
- && intake.store.getStatuses().some(s => s.noteId === String(inReplyTo));
36
- }
37
-
38
- // §7.1.2 Forwarding from the inbox. A reply into one of our threads reaches
39
- // only the servers the replier's server chose to deliver to — never our
40
- // followers on servers it has never heard of. As the actor those followers
41
- // follow, WE close that gap: an activity addressed to our followers collection
42
- // that names one of our objects is re-delivered to our followers' inboxes.
43
- //
44
- // Only what was addressed to the followers COLLECTION is carried — bto/bcc are
45
- // never read here, so a direct message (addressed to a person) never qualifies
46
- // and is never rebroadcast. And this runs only after handle() accepted the
47
- // activity, so anything blocked or muted was already refused upstream and is
48
- // never forwarded.
49
- export async function maybeForward(intake, activity) {
50
- if (!activity || typeof activity !== 'object') return;
51
- if (!FORWARDABLE.has(activity.type)) return; // see FORWARDABLE
52
- // Forwarding signs somebody else's activity with OUR key and pushes it to
53
- // every follower we have. That is a lot to do on the word of an unsigned
54
- // POST, so it is narrowed three ways:
55
- //
56
- // 1. Only the types whose object this drain actually DEREFERENCED at the
57
- // author's own origin. A Create/Update/Delete went through onCreate /
58
- // onUpdate / onDelete, which fetch and check the author; a Like, an
59
- // Announce or an Undo is believed on the envelope alone, so relaying
60
- // one made us a signed relay for anything a stranger cared to write.
61
- // 2. The actor has to be someone we already know of. A complete stranger
62
- // addressing our followers collection is not a conversation we are
63
- // party to.
64
- // 3. A budget per drain, so a flood cannot turn one sweep into thousands
65
- // of outbound deliveries under our signature.
66
- if (!FORWARD_TYPES.has(activity.type)) return;
67
- try {
68
- const audience = []
69
- .concat(activity.to || [], activity.cc || [], activity.audience || [])
70
- .map(v => (typeof v === 'string' ? v : v?.id)).filter(Boolean);
71
- if (!audience.includes(intake.urls.followers)) return; // not for our followers
72
- if (!intake._referencesOurObject(activity)) return; // not into a thread of ours
73
- const actor = typeof activity.actor === 'string' ? activity.actor : activity.actor?.id;
74
- if (actor === intake.urls.actor) return; // our own; nothing to forward
75
- if (!intake.known(actor)) {
76
- intake.log(`not forwarding ${activity.type} from ${actor}: nobody we know of`);
77
- return;
78
- }
79
- if (intake._forwardBudget <= 0) {
80
- intake.log(`not forwarding ${activity.type}: this drain's forwarding budget is spent`);
81
- return;
82
- }
83
- const id = typeof activity.id === 'string' ? activity.id : null;
84
- if (!id) return;
85
- const forwarded = intake.store.read('forwarded.json', []);
86
- if (forwarded.includes(id)) return; // already done
87
-
88
- const inboxes = [...new Set(intake.store.getContacts().followers
89
- .filter(f => !f.bsky) // Bluesky members are not AP inboxes
90
- .map(f => f.sharedInbox || f.inbox)
91
- .filter(Boolean))];
92
- if (!inboxes.length) return;
93
- intake._forwardBudget -= 1;
94
- await intake.deliverer.deliverToAll(inboxes, activity);
95
- intake.store.write('forwarded.json', [...forwarded, id].slice(-MAX_FORWARDED));
96
- intake.log(`forwarded ${activity.type} ${id} to ${inboxes.length} follower inbox(es)`);
97
- } catch (e) {
98
- intake.log(`inbox forwarding: ${e.message}`); // never stalls the drain
99
- }
100
- }
101
-
102
- // The "objects owned by the server" half of §7.1.2: does the activity reply
103
- // to, like, boost or otherwise name one of our own objects?
104
- export function referencesOurObject(intake, activity) {
105
- const refs = [];
106
- const add = (v) => { const id = typeof v === 'string' ? v : v?.id; if (id) refs.push(String(id)); };
107
- const obj = activity.object;
108
- if (obj && typeof obj === 'object') add(obj.inReplyTo); // a reply's parent
109
- add(activity.inReplyTo);
110
- add(activity.object); // a Like/Announce of our note
111
- add(activity.target);
112
- if (refs.some(r => r.startsWith(intake.urls.notes))) return true;
113
- return intake.config.kind === 'group'
114
- && refs.some(r => intake.store.getStatuses().some(s => s.noteId === r));
115
- }
116
-
117
- // Shared tail of Create/Announce: deref the note at its origin (never trust
118
- // the delivered copy), mirror it into pod RDF + statuses, notify on replies
119
- // to our own notes. Returns a rejection reason string, or undefined.
120
- export async function ingestNote(intake, objectId, actor, { via, inline = null } = {}) {
121
- // `inline` is the note as delivered, offered only for a delivery whose
122
- // signature the door verified (onCreate); otherwise the origin's copy.
123
- const note = inline || await intake.fetchAP(objectId);
124
- if (!note) return `object fetch failed (${objectId})`;
125
- if (note.id !== objectId || !isContentType(note.type)) return `object not verifiable content (${objectId}, ${note.type})`;
126
- const { attachmentsOf, titledContent } = await import('../wire.mjs');
127
- const attachments = attachmentsOf(note);
128
- const content = titledContent(note); // hostile markup never reaches pod or client
129
- // The delivering actor was checked on arrival; the author is only known once
130
- // the note is dereferenced. authorOf refuses an author the note's own origin
131
- // does not vouch for — see its comment; this is where a forged attribution
132
- // would otherwise become a timeline entry, a pod document, and for a group a
133
- // signed Announce to every member.
134
- const author = authorOf(note, actor);
135
- if (!author) return `object names an author its origin does not vouch for (${objectId})`;
136
- // The verified document, kept for whoever acts on this note after the
137
- // carry: a forum caches it for its readers. Only when someone asked.
138
- intake.recentNotes?.set(note.id, note);
139
- // This is the check that catches a blocked actor reaching us through
140
- // somebody else's boost, or through a hashtag feed.
141
- if (intake.store.isBlocked(author)) return `blocked author (${author})`;
142
-
143
- // The ENVELOPE said this concerns us. The envelope is a document a stranger
144
- // wrote: onCreate reads addressing off the delivered copy to decide whether
145
- // fetching is worth it, and nothing re-asked the question of the copy that
146
- // came back from the author's own server. So anyone could take any public
147
- // post, address the delivery to us, and have it filed as "X mentioned you"
148
- // — or, to a group, have a member's post carried to every follower when the
149
- // member never sent it there.
150
- //
151
- // A boost is exempt (`via`): someone we follow deliberately putting a post
152
- // in front of us is the whole point, and the note will not address us.
153
- if (!via && !intake.concernsUs(note, author)) {
154
- return `the note its own server serves does not address us (${objectId})`;
155
- }
156
-
157
- // An answer to one of our polls is a number on a document, not a post. It
158
- // arrives as an ordinary reply naming an option and carrying nothing else,
159
- // so filing it as one would put a blank entry in the thread and ring the
160
- // owner once per voter. Counted or refused — a second answer, an option we
161
- // do not offer, a poll already shut — it stops here either way.
162
- const asked = note.inReplyTo && intake.store.getStatuses()
163
- .find(x => x.noteId === String(note.inReplyTo) && x.kind === 'post' && x.poll);
164
- if (asked && polls.isVoteShape(note)) {
165
- const counted = await intake.publisher.recordVote(asked.noteId, author, note.name)
166
- .catch(e => { intake.log(`vote on ${asked.noteId}: ${e.message}`); return false; });
167
- intake.log(counted
168
- ? `vote counted (${note.name}): ${asked.noteId}`
169
- : `vote not counted (${note.name}) from ${author}: ${asked.noteId}`);
170
- return;
171
- }
172
-
173
- // Anyone can Append to a public inbox, so arriving is not the same as
174
- // belonging in the home timeline. Follow Mastodon's split: people you
175
- // follow (and their boosts) are HOME; anyone else is a MENTION — kept,
176
- // notified, readable in the Mentions view, but out of the timeline, and
177
- // mirror-only so unsolicited content never accumulates in the pod.
178
- // A group's people are its FOLLOWERS — it follows nobody. Reading the
179
- // following list for one filed every member's post as a stranger's mention,
180
- // so nothing reached the pod RDF and each post raised a notification.
181
- const contacts = intake.store.getContacts();
182
- const known = intake.config.kind === 'group'
183
- ? contacts.followers.some(f => f.actor === author)
184
- : contacts.following.some(f => f.actor === author && f.accepted);
185
- // Someone in a group you are in is not a stranger: their reply belongs in
186
- // the room, not in the drawer of unsolicited mail. Whose word this is on
187
- // is the group's — its published membership — so the group's own door
188
- // decides who gets in.
189
- const followed = via || known || (!known && await intake.isCoMember(author));
190
- const kind = followed ? 'timeline' : 'mention';
191
-
192
- // Mastodon carries a thread's mentions into every reply, which is the only
193
- // reason a reply ever reaches a group. Keep them so our composer can too.
194
- const mentions = [].concat(note.tag || [])
195
- .filter(t => t?.type === 'Mention' && t.href && t.name)
196
- .slice(0, MAX_MENTIONS)
197
- .map(t => ({ href: httpOnly(String(t.href).slice(0, MAX_URL_CHARS)), name: String(t.name).slice(0, 256) }))
198
- .filter(m => m.href);
199
- const hashtags = [].concat(note.tag || [])
200
- .filter(t => t?.type === 'Hashtag' && t.name)
201
- .slice(0, MAX_MENTIONS)
202
- .map(t => ({ name: String(t.name).replace(/^#/, '').slice(0, 128), url: httpOnly(String(t.href || '').slice(0, MAX_URL_CHARS)) }))
203
- .filter(t => t.name);
204
- const emojis = emojisOf(note);
205
- const poll = pollOf(note);
206
- // What the note quotes, and what its author lets others quote. The quoted
207
- // post is fetched and kept so the quote can be shown whole; whether the
208
- // quote was allowed is checked, never assumed.
209
- const quote = quoteOf(note);
210
- const quotePolicy = quotePolicyOf(note, author);
211
- const quoteState = quote ? await intake.quoteStateOf(note, quote) : null;
212
- if (quote) await holdQuoted(intake, quote);
213
- // Explicitly addressed, but to nobody public and to no followers
214
- // collection: a direct message, which belongs to the conversations view
215
- // rather than a timeline. A note with no addressing at all is NOT direct —
216
- // some servers omit to/cc, and vanishing from home is the wrong reading.
217
- const audience = [].concat(note.to || [], note.cc || []).map(String);
218
- const direct = audience.length > 0
219
- && !audience.includes(PUBLIC) && !audience.some(a => a.endsWith('/followers'));
220
- // Addressed to less than the world: whatever else happens to it, a group
221
- // must never widen its audience by carrying it.
222
- const nonPublic = audience.length > 0 && !audience.includes(PUBLIC);
223
- intake.store.addStatus({
224
- noteId: note.id, actor: author, content,
225
- published: note.published, inReplyTo: note.inReplyTo, kind,
226
- ...(direct ? { direct: true } : {}),
227
- ...(nonPublic ? { nonPublic: true } : {}),
228
- // The author's content warning, shown as one: plain text only.
229
- ...(note.summary ? { spoiler: String(note.summary).replace(/<[^>]*>/g, '') } : {}),
230
- ...(note.sensitive === true ? { sensitive: true } : {}),
231
- ...(hashtags.length ? { tags: hashtags } : {}),
232
- ...(poll ? { poll } : {}),
233
- ...(emojis.length ? { emojis } : {}),
234
- ...(mentions.length ? { mentions } : {}),
235
- ...(attachments.length ? { attachments } : {}),
236
- ...(quote ? { quote, quoteState } : {}),
237
- ...(quotePolicy ? { quotePolicy } : {}),
238
- ...(via ? { via } : {}),
239
- });
240
- // A notification for anything addressed to this person: a stranger's
241
- // post, a reply to one of ours, a direct message, or a post that names
242
- // us — whoever sent it. A direct message from someone followed used to
243
- // raise nothing and sat unseen in the private mentions. A post quoting
244
- // one of ours is its own kind of notification.
245
- const namesUs = mentions.some((m) => m.href === intake.urls.actor);
246
- const replyToOurs = !!note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes);
247
- const quotesUs = !!quote && quote.startsWith(intake.urls.notes);
248
- if (quotesUs) {
249
- intake.store.addNotification({ type: 'quote', actor: author, noteId: note.id });
250
- } else if (!followed || replyToOurs || direct || namesUs) {
251
- intake.store.addNotification({ type: 'mention', actor: author, noteId: note.id });
252
- }
253
- if (note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes)) {
254
- await intake.addReply(String(note.inReplyTo), note.id)
255
- .catch(e => intake.log(`replies collection: ${e.message}`));
256
- }
257
- intake.log(`${kind}: ${note.id}${via ? ` (boosted by ${via})` : ''}`);
258
- }
259
-
260
- // The post a quote shows: fetched at its origin and kept out of every
261
- // timeline (`remote`), the way a thread's ancestors are — it is there to be
262
- // shown inside the quoting post, not on its own. Nothing is fetched twice,
263
- // and nothing is followed further: a quote of a quote shows one level.
264
- export async function holdQuoted(intake, quoteId) {
265
- if (intake.store.getStatuses().some(s => s.noteId === quoteId)) return;
266
- if (intake.store.isBlocked(quoteId)) return;
267
- const q = await intake.fetchAP(quoteId).catch(() => null);
268
- if (!q || q.id !== quoteId || !isContentType(q.type)) {
269
- intake.log(`quoted post ${quoteId} could not be read — the quote shows without it`);
270
- return;
271
- }
272
- const by = authorOf(q, null);
273
- if (!by || intake.store.isBlocked(by)) return;
274
- const { attachmentsOf, titledContent } = await import('../wire.mjs');
275
- const attachments = attachmentsOf(q);
276
- const emojis = emojisOf(q);
277
- const poll = pollOf(q);
278
- const quotePolicy = quotePolicyOf(q, by);
279
- const mentions = [].concat(q.tag || [])
280
- .filter(t => t?.type === 'Mention' && t.href && t.name)
281
- .slice(0, MAX_MENTIONS)
282
- .map(t => ({ href: httpOnly(String(t.href).slice(0, MAX_URL_CHARS)), name: String(t.name).slice(0, 256) }))
283
- .filter(m => m.href);
284
- intake.store.addStatus({
285
- noteId: q.id, actor: by, content: titledContent(q),
286
- published: q.published, inReplyTo: q.inReplyTo, kind: 'remote',
287
- ...(q.summary ? { spoiler: String(q.summary).replace(/<[^>]*>/g, '') } : {}),
288
- ...(q.sensitive === true ? { sensitive: true } : {}),
289
- ...(poll ? { poll } : {}),
290
- ...(emojis.length ? { emojis } : {}),
291
- ...(mentions.length ? { mentions } : {}),
292
- ...(attachments.length ? { attachments } : {}),
293
- ...(quotePolicy ? { quotePolicy } : {}),
294
- });
295
- intake.log(`quoted post kept: ${q.id}`);
296
- }
297
-
298
- // Drop a post we were holding. A group that carried it also unsays its own
299
- // Announce — forwarding the author's Delete would be signed by us and not by
300
- // them, which receivers are right to refuse.
301
- // `collect` batches the outbox side: retract pushes the Announce id onto it
302
- // instead of republishing, and the caller writes once for all of them.
303
- export async function forget(intake, s, { collect = null } = {}) {
304
- if (s.announceActivity) {
305
- await intake.retract(s.noteId, { collect }).catch(e => intake.log(`retract: ${e.message}`));
306
- }
307
- intake.store.removeStatus(s.noteId);
308
- }
309
-
310
- // Undo an Announce this group made. Shared with the operator's `retract`.
311
- export async function retract(intake, noteId, { collect = null } = {}) {
312
- const s = intake.store.getStatuses().find(x => x.noteId === noteId);
313
- if (!s) throw new Error('no such post');
314
- // A Bluesky carry is a repost, and unsaying it is deleting the repost.
315
- if (s.repostUri) {
316
- if (!intake.bskyGroup) throw new Error('no bluesky account connected');
317
- return intake.bskyGroup.retract(s);
318
- }
319
- if (!s.announceActivity) throw new Error('that post was never carried');
320
- const { undoActivity } = await import('../wire.mjs');
321
- const inboxes = intake.announceTargets(s.actor);
322
- await intake.deliverer.deliverToAll(inboxes,
323
- undoActivity({ urls: intake.urls, activity: s.announceActivity, serial: intake.serial++ }));
324
- if (collect) collect.push(s.announceActivity.id);
325
- else await intake.publisher.unrecordOutbox(i => i?.id === s.announceActivity.id);
326
- intake.store.updateStatus(noteId, {
327
- announcedAt: undefined, announceActivity: undefined, retractedAt: new Date().toISOString(),
328
- });
329
- return { ok: true, noteId, inboxes: inboxes.length };
330
- }
331
-
332
- // Read-modify-write, and the drain is serialized, so two replies in one sweep
333
- // do not race. Nothing else writes this document.
334
- // MAX_REPLIES_RECORDED caps the collection. It is a discovery aid — a client
335
- // reading a thread — and the statuses index is what actually holds the
336
- // replies, so dropping the oldest costs a hop, not a record.
337
- export async function addReply(intake, parentId, replyId) {
338
- // The parent has to be a post we actually made. The only check used to be
339
- // that the id started with our notes prefix, and `inReplyTo` is read off a
340
- // document at the sender's own origin — so a stranger could name a note we
341
- // never wrote, and we would CREATE a document on the pod at a URL of their
342
- // choosing and then grow it, one whole re-PUT per reply, with no cap. Four
343
- // pod requests each and bytes quadratic in the number of replies.
344
- if (!intake.store.getStatuses().some(s => s.noteId === parentId && s.kind === 'post')) {
345
- intake.log(`reply names ${parentId}, which is not a post of ours — not recorded`);
346
- return;
347
- }
348
- const { repliesId, collection } = await import('../wire.mjs');
349
- const url = repliesId(parentId);
350
- // Deliberately NOT caught: a read we could not make is not an empty
351
- // collection, and rewriting on top of one erases every reply already
352
- // recorded. getJson returns null only for a genuine 404 — the document does
353
- // not exist yet — and throws otherwise, which the caller logs and retries.
354
- const cur = await podNotes.readReplies(intake.remote, url);
355
- const items = Array.isArray(cur?.items) ? cur.items : [];
356
- if (items.includes(replyId)) return;
357
- items.push(replyId);
358
- await podNotes.writeReplies(intake.remote, url, collection(url, items.slice(-MAX_REPLIES_RECORDED)));
359
- intake.log(`reply recorded on ${parentId}`);
360
- }
@@ -1,142 +0,0 @@
1
- // verify.mjs — how the drain decides what to believe: a signed fetch of the
2
- // claimed document at its own origin, what local state already knows of, and
3
- // the receipt a gateway left beside a delivery.
4
-
5
- import * as podInbox from '../../pod/inbox.mjs';
6
- import { readCapped } from '../../shared/safefetch.mjs';
7
- import { isActorType, sameOrigin, ACCEPT_AP } from './activity.mjs';
8
- import { readLenient } from '../as2.mjs';
9
-
10
- export async function fetchAP(intake, url) {
11
- const res = await intake.deliverer.signedFetch(url, { headers: { accept: ACCEPT_AP } });
12
- if (res.status >= 400) return null;
13
- // Remote servers are untrusted: read with a byte budget rather than
14
- // letting res.json() buffer whatever they choose to send.
15
- const { readCapped } = await import('../../shared/safefetch.mjs');
16
- // Plenty of servers answer 200 text/html however politely we ask for AS2 —
17
- // people reply to ordinary web pages, and their id is that page. Say so,
18
- // rather than handing the HTML to JSON.parse and logging the parser's
19
- // complaint about an unexpected `<`.
20
- // Not logged: people reply to ordinary web pages, so the reply's object id
21
- // is that page and this is the expected answer, not a fault. Only a server
22
- // that CLAIMS to be sending JSON and then does not is worth a line.
23
- const ct = (res.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
24
- if (ct && !ct.endsWith('json')) return null;
25
- let doc = null;
26
- try {
27
- const read = await readLenient(await readCapped(res));
28
- if (read.degraded) intake.log(`fetch ${url} grounded to read: ${read.degraded}`);
29
- doc = read.view ?? read.doc;
30
- } catch (e) { intake.log(`fetch ${url}: unreadable — ${e.message}`); return null; }
31
- if (!doc) { intake.log(`fetch ${url}: unreadable as JSON`); return null; }
32
- // A document is only evidence about its OWN origin.
33
- //
34
- // signedFetch follows redirects and hands back only the final response, and
35
- // every caller checks `doc.id` against the URL it ASKED for — so an open
36
- // redirector on a victim's origin let an attacker's server answer for a
37
- // victim's URL and be believed by that check.
38
- //
39
- // Only a redirect that CROSSED AN ORIGIN is refused here. A same-origin
40
- // redirect is ordinary (canonicalisation, trailing slashes), and a document
41
- // fetched with no redirect at all is still returned whatever it claims —
42
- // the callers reject it on id, and they say something more useful about it
43
- // than this could ("actor id mismatch", "object not verifiable content").
44
- const landed = res.finalUrl || url;
45
- if (!sameOrigin(landed, url) && doc && doc.id && !sameOrigin(doc.id, landed)) {
46
- intake.log(`fetch ${url}: redirected to ${landed}, which is not where ${doc.id} lives — refused`);
47
- return null;
48
- }
49
- // Every actor type, not just Person. A Group was fetched, used and thrown
50
- // away, so nothing knew its preferredUsername — and a client rendering it
51
- // fell back to the last path segment of the actor URL, which is the literal
52
- // word `actor`. That is where @actor@host came from.
53
- // Under the id the document CLAIMS, but only when its own origin vouches
54
- // for that id. A stranger's actor document naming someone else's id used to
55
- // overwrite that actor's cached name, bio, avatar and Person/Group flag —
56
- // one appended Follow was enough, and the id-mismatch checks in onFollow
57
- // and ingestNote both run after this line and never undid it.
58
- //
59
- // Same origin rather than exact equality: signedFetch follows redirects
60
- // without reporting where it landed, so a server that redirects its own
61
- // canonical actor URL would otherwise stop being cached at all.
62
- if (isActorType(doc?.type) && doc.id && sameOrigin(doc.id, url)) {
63
- intake.store.cacheActor(doc.id, doc);
64
- }
65
- return doc;
66
- }
67
-
68
- // Have we ever heard of this actor or object? Answered entirely from local
69
- // state, so asking costs nothing. It is what stops a stranger's Delete or
70
- // Update — of which Mastodon broadcasts a great many, and of which anyone at
71
- // all can Append one — turning into a signed request to a host they chose.
72
- export function known(intake, id) {
73
- const c = intake.store.getContacts();
74
- return c.followers.some(f => f.actor === id)
75
- || c.following.some(f => f.actor === id)
76
- || intake.store.getStatuses().some(s => s.noteId === id || s.actor === id)
77
- || !!intake.store.getActors()[id];
78
- }
79
-
80
- // Returns a rejection reason string, or undefined when handled.
81
- // The gateway's shared HMAC secret, or null when no gateway is configured.
82
- // Its absence is what makes the whole receipt path dormant by default.
83
- export function gatewaySecret(intake) {
84
- return intake.store.getConfig()?.gateway?.hmacSecret || null;
85
- }
86
-
87
- // Shadow-mode measurement: how much real traffic actually verified. The one
88
- // number an operator needs before trusting receipts. Only touched while a
89
- // gateway is configured, so it costs a non-gateway install nothing.
90
- export function bumpGatewayStat(intake, verified) {
91
- const s = intake.store.read('gateway-stats.json', { verified: 0, unverified: 0 });
92
- if (verified) s.verified++; else s.unverified++;
93
- s.lastAt = new Date().toISOString();
94
- intake.store.write('gateway-stats.json', s);
95
- }
96
-
97
- // Read and authenticate the receipt a gateway wrote beside an inbox item.
98
- // Returns the receipt object only when its HMAC verifies against our secret;
99
- // null otherwise (no gateway, no receipt, a stranger's forged one, or a read
100
- // failure) — and null means "unverified", the pre-gateway behavior.
101
- export async function readReceipt(intake, itemUrl) {
102
- const secret = intake.gatewaySecret();
103
- if (!secret) return null;
104
- try {
105
- const { readCapped: cap } = await import('../../shared/safefetch.mjs');
106
- const receipt = await podInbox.readDeliveryReceipt(intake.remote, itemUrl, { maxBytes: 64 * 1024, readCapped: cap });
107
- if (!receipt) return null;
108
- const { verifyReceipt } = await import('../../gateway/httpsig.mjs');
109
- return verifyReceipt(receipt, secret) ? receipt : null;
110
- } catch { return null; }
111
- }
112
-
113
- // Whether a receipt says anything about THIS actor. Verified-and-about-someone
114
- // -else is worth exactly as much as unverified, and is treated the same way:
115
- // the drain's verify-by-dereference still stands behind it.
116
- export function receiptVouchesFor(intake, receipt, actor) {
117
- if (!receipt?.verified) return false;
118
- if (!receipt.actor || receipt.actor !== actor) return false;
119
- // A missing keyId with verified:true cannot come from our own door
120
- // (makeReceipt fills both from the same key), so refuse rather than waive.
121
- if (!receipt.keyId || !sameOrigin(receipt.keyId, actor)) return false;
122
- return true;
123
- }
124
-
125
- // Is this really gone at its origin? true / false / null when the origin
126
- // could not be asked. Delivered bodies carry no signature, so this is how a
127
- // Delete is verified — the same verify-by-dereference the rest of intake uses.
128
- export async function isGone(intake, url) {
129
- let res;
130
- try { res = await intake.deliverer.signedFetch(url, { headers: { accept: ACCEPT_AP } }); }
131
- catch { return null; }
132
- if (res.status === 404 || res.status === 410) return true;
133
- if (res.status < 400) {
134
- // A Tombstone answers 200 and still means deleted.
135
- try {
136
- const { readCapped } = await import('../../shared/safefetch.mjs');
137
- const read = await readLenient(await readCapped(res));
138
- return (read.view ?? read.doc)?.type === 'Tombstone';
139
- } catch { return false; }
140
- }
141
- return null; // 401/403/5xx — no answer, not a denial
142
- }
package/lib/core/keys.mjs DELETED
@@ -1,153 +0,0 @@
1
- // keys.mjs — actor signing keys. RSA (Mastodon's draft-cavage HTTP Signatures
2
- // require RSA-SHA256) signs deliveries; Ed25519 signs the FEP-8b32 proof that
3
- // travels with an activity. PEM at rest (0600), CryptoKey in memory for
4
- // Fedify's signRequest and for the proof.
5
- //
6
- // The key lives on THIS MACHINE by default (AP_HOME/keys.json): the pod host
7
- // then never holds it. `setup --keys pod` puts it in pod state instead,
8
- // which is what lets several devices sign as the same actor without copying
9
- // anything — see resolveKeys() for the guard that keeps the two modes from
10
- // silently breaking each other.
11
-
12
- import crypto from 'node:crypto';
13
- import fs from 'node:fs';
14
- import path from 'node:path';
15
- import { writeJsonAtomic } from '../device/home.mjs';
16
-
17
- const RSA_ALG = { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' };
18
-
19
- const localPath = (dir) => path.join(dir, 'keys.json');
20
- const readLocal = (dir) => {
21
- try { return JSON.parse(fs.readFileSync(localPath(dir), 'utf8')); } catch { return null; }
22
- };
23
- // Atomic: this is the private key remote servers have cached, and a truncated
24
- // one cannot be recovered from anywhere — the pod does not hold it.
25
- const writeLocal = (dir, rec) => {
26
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
27
- writeJsonAtomic(localPath(dir), rec);
28
- };
29
-
30
- function generate() {
31
- const rsa = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
32
- const ed = crypto.generateKeyPairSync('ed25519');
33
- return {
34
- rsa: {
35
- publicPem: rsa.publicKey.export({ type: 'spki', format: 'pem' }),
36
- privatePem: rsa.privateKey.export({ type: 'pkcs8', format: 'pem' }),
37
- },
38
- ed25519: {
39
- publicPem: ed.publicKey.export({ type: 'spki', format: 'pem' }),
40
- privatePem: ed.privateKey.export({ type: 'pkcs8', format: 'pem' }),
41
- },
42
- };
43
- }
44
-
45
- /**
46
- * Find or create the key record.
47
- *
48
- * The dangerous case is minting a NEW key when the actor already advertises
49
- * one: remote servers cache the public key, so a fresh keypair silently
50
- * invalidates every signature until the actor document is republished — and
51
- * then breaks whichever other device holds the old key. So when key material
52
- * is missing but the actor doc already has a key, we refuse and explain,
53
- * unless the caller passes rotate:true deliberately.
54
- *
55
- * @param {object} store pod-backed state store
56
- * @param {object} o
57
- * @param {string|null} o.localDir AP_HOME when keys are local (the default)
58
- * @param {boolean} o.rotate mint a replacement on purpose
59
- * @param {function} [o.actorHasKey] async () => boolean, consulted only when
60
- * no key material exists anywhere
61
- * @param {function} [o.log]
62
- */
63
- export async function resolveKeys(store, { localDir = null, rotate = false, actorHasKey = null, actorId = null, log = () => {} } = {}) {
64
- let rec = localDir ? readLocal(localDir) : null;
65
-
66
- // A key belongs to ONE actor. Adopting whatever keys.json happens to be in
67
- // the directory is right within an identity and wrong across two: it is how
68
- // a second setup in the same AP_HOME ended up signing as a different actor
69
- // with the first one's key, which links the two publicly for anyone who
70
- // compares the actor documents. A stamped key that names another actor is
71
- // treated as absent, so the mint path (and its actorHasKey guard) applies.
72
- if (rec && actorId && rec.mintedFor && rec.mintedFor !== actorId) {
73
- log(`local keys.json belongs to ${rec.mintedFor} — not reusing it for ${actorId}`);
74
- rec = null;
75
- } else if (rec && actorId && !rec.mintedFor) {
76
- // Written before keys were stamped: keep working, but say so, because it
77
- // may be another actor's key.
78
- log('keys.json predates per-identity stamping — if this actor was set up in a '
79
- + 'home that already held a key, `setup --rotate-key` mints one of its own');
80
- }
81
-
82
- if (!rec) {
83
- const podRec = store.read('keys.json', null);
84
- if (podRec && localDir) {
85
- // Migrating an existing actor to local keys: adopt the pod's key so
86
- // the identity is preserved, then remove the pod's copy — leaving it
87
- // would defeat the point of going local.
88
- writeLocal(localDir, podRec);
89
- rec = podRec;
90
- const gone = await store.remove('keys.json');
91
- log(gone
92
- ? `signing key moved to ${localPath(localDir)} and deleted from pod state`
93
- : `signing key copied to ${localPath(localDir)} — could NOT delete the pod copy, remove it by hand`);
94
- } else {
95
- rec = podRec;
96
- }
97
- }
98
-
99
- if (!rec && !rotate && actorHasKey && await actorHasKey()) {
100
- throw new Error(
101
- 'this actor already publishes a signing key, but no key material is present here.\n'
102
- + ` Copy keys.json from your other device into ${localDir || 'pod state'}, or\n`
103
- + ' re-run setup with --keys pod to share the key through the pod, or\n'
104
- // --force, because rotate-key connects before it rotates and would meet
105
- // this same refusal: advice has to name a command that can actually run.
106
- + ' run `fedipod rotate-key --force` to replace the published key (breaks other devices).');
107
- }
108
-
109
- if (!rec || rotate) {
110
- rec = { ...generate(), ...(actorId ? { mintedFor: actorId } : {}) };
111
- if (localDir) writeLocal(localDir, rec); else store.write('keys.json', rec);
112
- log(rotate ? 'minted a REPLACEMENT signing key — republish the profile' : 'minted a signing key');
113
- }
114
-
115
- // A record written before proofs existed has no Ed25519 half. Minting one is
116
- // safe in a way minting an RSA key is not: nothing has ever published or
117
- // cached it, so no remote signature is invalidated.
118
- if (!rec.ed25519) {
119
- rec = { ...rec, ed25519: generate().ed25519 };
120
- if (localDir) writeLocal(localDir, rec); else store.write('keys.json', rec);
121
- log('minted the Ed25519 half of the signing key');
122
- }
123
-
124
- const der = crypto.createPrivateKey(rec.rsa.privatePem).export({ type: 'pkcs8', format: 'der' });
125
- const rsaPrivate = await crypto.subtle.importKey('pkcs8', der, RSA_ALG, true, ['sign']);
126
- const { edPrivateKey, multibaseEd25519 } = await import('./proof.mjs');
127
- return {
128
- rsaPrivate, rsaPublicPem: rec.rsa.publicPem,
129
- edPrivate: await edPrivateKey(rec.ed25519.privatePem),
130
- edPublicMultibase: multibaseEd25519(rec.ed25519.publicPem),
131
- };
132
- }
133
-
134
- // Move a local key into pod state — the arrangement that lets several
135
- // devices sign as one actor once the state store is a pod they all reach.
136
- // The pod write is forced and checked BEFORE the local file goes: a key
137
- // remote servers have cached cannot be recovered from anywhere else.
138
- export async function moveKeysToPod(store, { localDir, actorId = null, log = () => {} }) {
139
- const rec = readLocal(localDir);
140
- if (!rec) throw new Error(`no local key at ${localPath(localDir)} — nothing to move`);
141
- if (actorId && rec.mintedFor && rec.mintedFor !== actorId) {
142
- throw new Error(`local keys.json belongs to ${rec.mintedFor}, not ${actorId} — refusing to move it`);
143
- }
144
- store.write('keys.json', rec);
145
- const landed = store.commit ? await store.commit() : await store.flush?.();
146
- if (landed === false) throw new Error('the pod write did not land — the local key is untouched');
147
- fs.rmSync(localPath(localDir), { force: true });
148
- log('signing key moved to pod state and removed from this machine');
149
- return true;
150
- }
151
-
152
- // Back-compat alias for callers that just want "the keys, wherever they are".
153
- export const ensureKeys = (store, opts) => resolveKeys(store, opts);