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,367 +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
- // Blind copies are for their sender's delivery alone and never travel on
95
- // (§6, §7.1), from the activity or its object.
96
- const strip = (o) => { if (!o || typeof o !== 'object') return o; const { bto, bcc, ...rest } = o; return rest; }; // eslint-disable-line no-unused-vars
97
- const onward = { ...strip(activity), ...(activity.object && typeof activity.object === 'object' ? { object: strip(activity.object) } : {}) };
98
- await intake.deliverer.deliverToAll(inboxes, onward);
99
- intake.store.write('forwarded.json', [...forwarded, id].slice(-MAX_FORWARDED));
100
- intake.log(`forwarded ${activity.type} ${id} to ${inboxes.length} follower inbox(es)`);
101
- } catch (e) {
102
- intake.log(`inbox forwarding: ${e.message}`); // never stalls the drain
103
- }
104
- }
105
-
106
- // The "objects owned by the server" half of §7.1.2: does the activity reply
107
- // to, like, boost or otherwise name one of our own objects?
108
- export function referencesOurObject(intake, activity) {
109
- const refs = [];
110
- const add = (v) => { const id = typeof v === 'string' ? v : v?.id; if (id) refs.push(String(id)); };
111
- const obj = activity.object;
112
- if (obj && typeof obj === 'object') add(obj.inReplyTo); // a reply's parent
113
- add(activity.inReplyTo);
114
- add(activity.object); // a Like/Announce of our note
115
- add(activity.target);
116
- if (refs.some(r => r.startsWith(intake.urls.notes))) return true;
117
- return intake.config.kind === 'group'
118
- && refs.some(r => intake.store.getStatuses().some(s => s.noteId === r));
119
- }
120
-
121
- // Shared tail of Create/Announce: deref the note at its origin (never trust
122
- // the delivered copy), mirror it into pod RDF + statuses, notify on replies
123
- // to our own notes. Returns a rejection reason string, or undefined.
124
- export async function ingestNote(intake, objectId, actor, { via, inline = null } = {}) {
125
- // `inline` is the note as delivered, offered only for a delivery whose
126
- // signature the door verified (onCreate); otherwise the origin's copy.
127
- const note = inline || await intake.fetchAP(objectId);
128
- if (!note) return `object fetch failed (${objectId})`;
129
- if (note.id !== objectId || !isContentType(note.type)) return `object not verifiable content (${objectId}, ${note.type})`;
130
- const { attachmentsOf, titledContent } = await import('../wire.mjs');
131
- const attachments = attachmentsOf(note);
132
- const content = titledContent(note); // hostile markup never reaches pod or client
133
- // The delivering actor was checked on arrival; the author is only known once
134
- // the note is dereferenced. authorOf refuses an author the note's own origin
135
- // does not vouch for — see its comment; this is where a forged attribution
136
- // would otherwise become a timeline entry, a pod document, and for a group a
137
- // signed Announce to every member.
138
- const author = authorOf(note, actor);
139
- if (!author) return `object names an author its origin does not vouch for (${objectId})`;
140
- // The verified document, kept for whoever acts on this note after the
141
- // carry: a forum caches it for its readers. Only when someone asked.
142
- intake.recentNotes?.set(note.id, note);
143
- // This is the check that catches a blocked actor reaching us through
144
- // somebody else's boost, or through a hashtag feed.
145
- if (intake.store.isBlocked(author)) return `blocked author (${author})`;
146
-
147
- // The ENVELOPE said this concerns us. The envelope is a document a stranger
148
- // wrote: onCreate reads addressing off the delivered copy to decide whether
149
- // fetching is worth it, and nothing re-asked the question of the copy that
150
- // came back from the author's own server. So anyone could take any public
151
- // post, address the delivery to us, and have it filed as "X mentioned you"
152
- // — or, to a group, have a member's post carried to every follower when the
153
- // member never sent it there.
154
- //
155
- // A boost is exempt (`via`): someone we follow deliberately putting a post
156
- // in front of us is the whole point, and the note will not address us.
157
- if (!via && !intake.concernsUs(note, author)) {
158
- return `the note its own server serves does not address us (${objectId})`;
159
- }
160
-
161
- // An answer to one of our polls is a number on a document, not a post. It
162
- // arrives as an ordinary reply naming an option and carrying nothing else,
163
- // so filing it as one would put a blank entry in the thread and ring the
164
- // owner once per voter. Counted or refused — a second answer, an option we
165
- // do not offer, a poll already shut — it stops here either way.
166
- const asked = note.inReplyTo && intake.store.getStatuses()
167
- .find(x => x.noteId === String(note.inReplyTo) && x.kind === 'post' && x.poll);
168
- if (asked && polls.isVoteShape(note)) {
169
- const counted = await intake.publisher.recordVote(asked.noteId, author, note.name)
170
- .catch(e => { intake.log(`vote on ${asked.noteId}: ${e.message}`); return false; });
171
- intake.log(counted
172
- ? `vote counted (${note.name}): ${asked.noteId}`
173
- : `vote not counted (${note.name}) from ${author}: ${asked.noteId}`);
174
- return;
175
- }
176
-
177
- // Anyone can Append to a public inbox, so arriving is not the same as
178
- // belonging in the home timeline. Follow Mastodon's split: people you
179
- // follow (and their boosts) are HOME; anyone else is a MENTION — kept,
180
- // notified, readable in the Mentions view, but out of the timeline, and
181
- // mirror-only so unsolicited content never accumulates in the pod.
182
- // A group's people are its FOLLOWERS — it follows nobody. Reading the
183
- // following list for one filed every member's post as a stranger's mention,
184
- // so nothing reached the pod RDF and each post raised a notification.
185
- const contacts = intake.store.getContacts();
186
- const known = intake.config.kind === 'group'
187
- ? contacts.followers.some(f => f.actor === author)
188
- : contacts.following.some(f => f.actor === author && f.accepted);
189
- // Someone in a group you are in is not a stranger: their reply belongs in
190
- // the room, not in the drawer of unsolicited mail. Whose word this is on
191
- // is the group's — its published membership — so the group's own door
192
- // decides who gets in.
193
- const followed = via || known || (!known && await intake.isCoMember(author));
194
- const kind = followed ? 'timeline' : 'mention';
195
-
196
- // Mastodon carries a thread's mentions into every reply, which is the only
197
- // reason a reply ever reaches a group. Keep them so our composer can too.
198
- const mentions = [].concat(note.tag || [])
199
- .filter(t => t?.type === 'Mention' && t.href && t.name)
200
- .slice(0, MAX_MENTIONS)
201
- .map(t => ({ href: httpOnly(String(t.href).slice(0, MAX_URL_CHARS)), name: String(t.name).slice(0, 256) }))
202
- .filter(m => m.href);
203
- const hashtags = [].concat(note.tag || [])
204
- .filter(t => t?.type === 'Hashtag' && t.name)
205
- .slice(0, MAX_MENTIONS)
206
- .map(t => ({ name: String(t.name).replace(/^#/, '').slice(0, 128), url: httpOnly(String(t.href || '').slice(0, MAX_URL_CHARS)) }))
207
- .filter(t => t.name);
208
- const emojis = emojisOf(note);
209
- const poll = pollOf(note);
210
- // What the note quotes, and what its author lets others quote. The quoted
211
- // post is fetched and kept so the quote can be shown whole; whether the
212
- // quote was allowed is checked, never assumed.
213
- const quote = quoteOf(note);
214
- const quotePolicy = quotePolicyOf(note, author);
215
- const quoteState = quote ? await intake.quoteStateOf(note, quote) : null;
216
- if (quote) await holdQuoted(intake, quote);
217
- // Explicitly addressed, but to nobody public and to no followers
218
- // collection: a direct message, which belongs to the conversations view
219
- // rather than a timeline. A note with no addressing at all is NOT direct —
220
- // some servers omit to/cc, and vanishing from home is the wrong reading.
221
- const audience = [].concat(note.to || [], note.cc || []).map(String);
222
- const direct = audience.length > 0
223
- && !audience.includes(PUBLIC) && !audience.some(a => a.endsWith('/followers'));
224
- // Addressed to less than the world: whatever else happens to it, a group
225
- // must never widen its audience by carrying it.
226
- const nonPublic = audience.length > 0 && !audience.includes(PUBLIC);
227
- intake.store.addStatus({
228
- noteId: note.id, actor: author, content,
229
- published: note.published, inReplyTo: note.inReplyTo, kind,
230
- ...(direct ? { direct: true } : {}),
231
- ...(nonPublic ? { nonPublic: true } : {}),
232
- // The author's content warning, shown as one: plain text only.
233
- ...(note.summary ? { spoiler: String(note.summary).replace(/<[^>]*>/g, '') } : {}),
234
- ...(note.sensitive === true ? { sensitive: true } : {}),
235
- ...(hashtags.length ? { tags: hashtags } : {}),
236
- ...(poll ? { poll } : {}),
237
- ...(emojis.length ? { emojis } : {}),
238
- ...(mentions.length ? { mentions } : {}),
239
- ...(attachments.length ? { attachments } : {}),
240
- ...(quote ? { quote, quoteState } : {}),
241
- ...(quotePolicy ? { quotePolicy } : {}),
242
- ...(via ? { via } : {}),
243
- });
244
- // A notification for anything addressed to this person: a stranger's
245
- // post, a reply to one of ours, a direct message, or a post that names
246
- // us — whoever sent it. A direct message from someone followed used to
247
- // raise nothing and sat unseen in the private mentions. A post quoting
248
- // one of ours is its own kind of notification.
249
- const namesUs = mentions.some((m) => m.href === intake.urls.actor);
250
- const replyToOurs = !!note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes);
251
- const quotesUs = !!quote && quote.startsWith(intake.urls.notes);
252
- if (quotesUs) {
253
- intake.store.addNotification({ type: 'quote', actor: author, noteId: note.id });
254
- } else if (!followed || replyToOurs || direct || namesUs) {
255
- intake.store.addNotification({ type: 'mention', actor: author, noteId: note.id });
256
- }
257
- if (note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes)) {
258
- await intake.addReply(String(note.inReplyTo), note.id)
259
- .catch(e => intake.log(`replies collection: ${e.message}`));
260
- }
261
- intake.log(`${kind}: ${note.id}${via ? ` (boosted by ${via})` : ''}`);
262
- }
263
-
264
- // The post a quote shows: fetched at its origin and kept out of every
265
- // timeline (`remote`), the way a thread's ancestors are — it is there to be
266
- // shown inside the quoting post, not on its own. Nothing is fetched twice,
267
- // and nothing is followed further: a quote of a quote shows one level.
268
- export async function holdQuoted(intake, quoteId) {
269
- if (intake.store.getStatuses().some(s => s.noteId === quoteId)) return;
270
- if (intake.store.isBlocked(quoteId)) return;
271
- const q = await intake.fetchAP(quoteId).catch(() => null);
272
- if (!q || q.id !== quoteId || !isContentType(q.type)) {
273
- intake.log(`quoted post ${quoteId} could not be read — the quote shows without it`);
274
- return;
275
- }
276
- const by = authorOf(q, null);
277
- if (!by || intake.store.isBlocked(by)) return;
278
- const { attachmentsOf, titledContent } = await import('../wire.mjs');
279
- const attachments = attachmentsOf(q);
280
- const emojis = emojisOf(q);
281
- const poll = pollOf(q);
282
- const quotePolicy = quotePolicyOf(q, by);
283
- const mentions = [].concat(q.tag || [])
284
- .filter(t => t?.type === 'Mention' && t.href && t.name)
285
- .slice(0, MAX_MENTIONS)
286
- .map(t => ({ href: httpOnly(String(t.href).slice(0, MAX_URL_CHARS)), name: String(t.name).slice(0, 256) }))
287
- .filter(m => m.href);
288
- intake.store.addStatus({
289
- noteId: q.id, actor: by, content: titledContent(q),
290
- published: q.published, inReplyTo: q.inReplyTo, kind: 'remote',
291
- ...(q.summary ? { spoiler: String(q.summary).replace(/<[^>]*>/g, '') } : {}),
292
- ...(q.sensitive === true ? { sensitive: true } : {}),
293
- ...(poll ? { poll } : {}),
294
- ...(emojis.length ? { emojis } : {}),
295
- ...(mentions.length ? { mentions } : {}),
296
- ...(attachments.length ? { attachments } : {}),
297
- ...(quotePolicy ? { quotePolicy } : {}),
298
- });
299
- intake.log(`quoted post kept: ${q.id}`);
300
- }
301
-
302
- // Drop a post we were holding. A group that carried it also unsays its own
303
- // Announce — forwarding the author's Delete would be signed by us and not by
304
- // them, which receivers are right to refuse.
305
- // `collect` batches the outbox side: retract pushes the Announce id onto it
306
- // instead of republishing, and the caller writes once for all of them.
307
- export async function forget(intake, s, { collect = null } = {}) {
308
- if (s.announceActivity) {
309
- await intake.retract(s.noteId, { collect }).catch(e => intake.log(`retract: ${e.message}`));
310
- }
311
- intake.store.removeStatus(s.noteId);
312
- }
313
-
314
- // Undo an Announce this group made. Shared with the operator's `retract`.
315
- export async function retract(intake, noteId, { collect = null } = {}) {
316
- const s = intake.store.getStatuses().find(x => x.noteId === noteId);
317
- if (!s) throw new Error('no such post');
318
- // A Bluesky carry is a repost, and unsaying it is deleting the repost.
319
- if (s.repostUri) {
320
- if (!intake.bskyGroup) throw new Error('no bluesky account connected');
321
- return intake.bskyGroup.retract(s);
322
- }
323
- if (!s.announceActivity) throw new Error('that post was never carried');
324
- const { undoActivity } = await import('../wire.mjs');
325
- const inboxes = intake.announceTargets(s.actor);
326
- const undo = undoActivity({ urls: intake.urls, activity: s.announceActivity, serial: intake.serial++ });
327
- await intake.deliverer.deliverToAll(inboxes, undo);
328
- // The Undo takes the carry's place in the outbox, so a reader catching up
329
- // from it learns the carry was withdrawn.
330
- if (collect) collect.push(undo);
331
- else await intake.publisher.unrecordOutbox(i => i?.id === s.announceActivity.id,
332
- { record: () => (intake.config.private ? null : undo) });
333
- intake.store.updateStatus(noteId, {
334
- announcedAt: undefined, announceActivity: undefined, retractedAt: new Date().toISOString(),
335
- });
336
- return { ok: true, noteId, inboxes: inboxes.length };
337
- }
338
-
339
- // Read-modify-write, and the drain is serialized, so two replies in one sweep
340
- // do not race. Nothing else writes this document.
341
- // MAX_REPLIES_RECORDED caps the collection. It is a discovery aid — a client
342
- // reading a thread — and the statuses index is what actually holds the
343
- // replies, so dropping the oldest costs a hop, not a record.
344
- export async function addReply(intake, parentId, replyId) {
345
- // The parent has to be a post we actually made. The only check used to be
346
- // that the id started with our notes prefix, and `inReplyTo` is read off a
347
- // document at the sender's own origin — so a stranger could name a note we
348
- // never wrote, and we would CREATE a document on the pod at a URL of their
349
- // choosing and then grow it, one whole re-PUT per reply, with no cap. Four
350
- // pod requests each and bytes quadratic in the number of replies.
351
- if (!intake.store.getStatuses().some(s => s.noteId === parentId && s.kind === 'post')) {
352
- intake.log(`reply names ${parentId}, which is not a post of ours — not recorded`);
353
- return;
354
- }
355
- const { repliesId, collection } = await import('../wire.mjs');
356
- const url = repliesId(parentId);
357
- // Deliberately NOT caught: a read we could not make is not an empty
358
- // collection, and rewriting on top of one erases every reply already
359
- // recorded. getJson returns null only for a genuine 404 — the document does
360
- // not exist yet — and throws otherwise, which the caller logs and retries.
361
- const cur = await podNotes.readReplies(intake.remote, url);
362
- const items = Array.isArray(cur?.items) ? cur.items : [];
363
- if (items.includes(replyId)) return;
364
- items.push(replyId);
365
- await podNotes.writeReplies(intake.remote, url, collection(url, items.slice(-MAX_REPLIES_RECORDED)));
366
- intake.log(`reply recorded on ${parentId}`);
367
- }
@@ -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);