fedipod-server 0.29.0 → 0.31.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 (444) hide show
  1. package/README.md +16 -6
  2. package/dist/claims.d.ts +2 -0
  3. package/dist/claims.js +13 -4
  4. package/dist/door.d.ts +4 -0
  5. package/dist/door.js +31 -0
  6. package/dist/fedipod.d.ts +15 -0
  7. package/dist/fedipod.js +13 -0
  8. package/dist/front.d.ts +3 -0
  9. package/dist/front.js +92 -0
  10. package/dist/handler.d.ts +36 -101
  11. package/dist/handler.js +93 -644
  12. package/dist/handler.jsonld +2 -30
  13. package/dist/identities.d.ts +71 -0
  14. package/dist/identities.js +236 -0
  15. package/dist/mounts.d.ts +43 -0
  16. package/dist/mounts.js +123 -0
  17. package/dist/opt-in.d.ts +46 -0
  18. package/dist/opt-in.js +176 -0
  19. package/dist/private-read.d.ts +4 -0
  20. package/dist/private-read.js +34 -0
  21. package/package.json +12 -20
  22. package/lib/client/c2s.mjs +0 -636
  23. package/lib/client/localapi.mjs +0 -87
  24. package/lib/client/masto/accounts.mjs +0 -310
  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 -387
  65. package/lib/core/graphview.mjs +0 -327
  66. package/lib/core/intake/activities.mjs +0 -649
  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 -258
  70. package/lib/core/intake/index.mjs +0 -732
  71. package/lib/core/intake/notes.mjs +0 -367
  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/place.mjs +0 -82
  76. package/lib/core/polls.mjs +0 -105
  77. package/lib/core/profile-page.mjs +0 -104
  78. package/lib/core/proof.mjs +0 -79
  79. package/lib/core/publisher/collections.mjs +0 -272
  80. package/lib/core/publisher/index.mjs +0 -522
  81. package/lib/core/publisher/notes.mjs +0 -519
  82. package/lib/core/publisher/own.mjs +0 -143
  83. package/lib/core/publisher/questions.mjs +0 -241
  84. package/lib/core/publisher/restore.mjs +0 -224
  85. package/lib/core/shapes/activitystreams.ttl +0 -157
  86. package/lib/core/shapes/index.mjs +0 -132
  87. package/lib/core/shapes/shapes-text.mjs +0 -13
  88. package/lib/core/social.mjs +0 -561
  89. package/lib/core/storage.mjs +0 -176
  90. package/lib/core/store.mjs +0 -578
  91. package/lib/core/wire-quotes.mjs +0 -71
  92. package/lib/core/wire.mjs +0 -925
  93. package/lib/device/account.mjs +0 -87
  94. package/lib/device/admin/index.mjs +0 -13
  95. package/lib/device/admin/origins.mjs +0 -35
  96. package/lib/device/admin/routes/connections.mjs +0 -144
  97. package/lib/device/admin/routes/gateway.mjs +0 -199
  98. package/lib/device/admin/routes/lifecycle.mjs +0 -200
  99. package/lib/device/admin/routes/owner.mjs +0 -322
  100. package/lib/device/admin/routes/setup.mjs +0 -428
  101. package/lib/device/admin/routes/social.mjs +0 -188
  102. package/lib/device/admin/server.mjs +0 -95
  103. package/lib/device/admin/static.mjs +0 -247
  104. package/lib/device/admin/surface.mjs +0 -315
  105. package/lib/device/certs.mjs +0 -281
  106. package/lib/device/cli/commands/account.mjs +0 -586
  107. package/lib/device/cli/commands/run.mjs +0 -278
  108. package/lib/device/cli/commands/service.mjs +0 -221
  109. package/lib/device/cli/commands/setup.mjs +0 -489
  110. package/lib/device/cli/commands/state.mjs +0 -561
  111. package/lib/device/cli/context.mjs +0 -288
  112. package/lib/device/export-collections.mjs +0 -161
  113. package/lib/device/gateway-move.mjs +0 -58
  114. package/lib/device/home.mjs +0 -161
  115. package/lib/device/migrate.mjs +0 -115
  116. package/lib/device/ports.mjs +0 -27
  117. package/lib/device/remote.mjs +0 -54
  118. package/lib/device/setup.mjs +0 -496
  119. package/lib/device/update.mjs +0 -96
  120. package/lib/gateway/caches.mjs +0 -36
  121. package/lib/gateway/directory.mjs +0 -90
  122. package/lib/gateway/front-core.mjs +0 -966
  123. package/lib/gateway/gateway-core.mjs +0 -244
  124. package/lib/gateway/headers.mjs +0 -49
  125. package/lib/gateway/httpsig.mjs +0 -159
  126. package/lib/gateway/notices.mjs +0 -84
  127. package/lib/gateway/quiet.mjs +0 -189
  128. package/lib/gateway/relay-extras.mjs +0 -89
  129. package/lib/gateway/token-claims.mjs +0 -16
  130. package/lib/pod/README.md +0 -61
  131. package/lib/pod/actor.mjs +0 -96
  132. package/lib/pod/collection.mjs +0 -77
  133. package/lib/pod/containers.mjs +0 -140
  134. package/lib/pod/discovery.mjs +0 -46
  135. package/lib/pod/featured.mjs +0 -18
  136. package/lib/pod/followers.mjs +0 -24
  137. package/lib/pod/following.mjs +0 -10
  138. package/lib/pod/http.mjs +0 -74
  139. package/lib/pod/inbox.mjs +0 -166
  140. package/lib/pod/links.mjs +0 -35
  141. package/lib/pod/location.mjs +0 -52
  142. package/lib/pod/media.mjs +0 -27
  143. package/lib/pod/notes.mjs +0 -78
  144. package/lib/pod/notifications.mjs +0 -89
  145. package/lib/pod/outbox.mjs +0 -18
  146. package/lib/pod/package.json +0 -19
  147. package/lib/pod/policy.mjs +0 -58
  148. package/lib/pod/private.mjs +0 -19
  149. package/lib/pod/root.mjs +0 -104
  150. package/lib/pod/state.mjs +0 -40
  151. package/lib/pod/transport.mjs +0 -674
  152. package/lib/pod/type-index.mjs +0 -101
  153. package/lib/pod/urls.mjs +0 -91
  154. package/lib/server/embed.mjs +0 -424
  155. package/lib/session/README.md +0 -137
  156. package/lib/session/demo.html +0 -94
  157. package/lib/session/fedi-account.mjs +0 -417
  158. package/lib/session/fedi-login.mjs +0 -123
  159. package/lib/session/oidc-session.mjs +0 -233
  160. package/lib/session/package.json +0 -49
  161. package/lib/shared/guard.mjs +0 -213
  162. package/lib/shared/links.mjs +0 -9
  163. package/lib/shared/safefetch.mjs +0 -235
  164. package/lib/shared/ua.mjs +0 -29
  165. package/phanpy/dist/404.html +0 -40
  166. package/phanpy/dist/_headers +0 -2
  167. package/phanpy/dist/apple-touch-icon.png +0 -0
  168. package/phanpy/dist/assets/ICONS-DIPJeU0C.js +0 -2
  169. package/phanpy/dist/assets/ICONS-DIPJeU0C.js.map +0 -1
  170. package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js +0 -183
  171. package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
  172. package/phanpy/dist/assets/Temml-CCjoacWt.woff2 +0 -0
  173. package/phanpy/dist/assets/__vite-browser-external-C7iut881.js +0 -2
  174. package/phanpy/dist/assets/__vite-browser-external-C7iut881.js.map +0 -1
  175. package/phanpy/dist/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  176. package/phanpy/dist/assets/catch-up-O5q5C75Z.png +0 -0
  177. package/phanpy/dist/assets/chunk-aKtaBQYM.js +0 -1
  178. package/phanpy/dist/assets/compose-CFEx7wb4.js +0 -2
  179. package/phanpy/dist/assets/compose-CFEx7wb4.js.map +0 -1
  180. package/phanpy/dist/assets/compose-ZJN5xSb4.js +0 -35
  181. package/phanpy/dist/assets/compose-ZJN5xSb4.js.map +0 -1
  182. package/phanpy/dist/assets/debug.module-YFQgwnIr.js +0 -52
  183. package/phanpy/dist/assets/debug.module-YFQgwnIr.js.map +0 -1
  184. package/phanpy/dist/assets/dom-p1YcJ5Rw.js +0 -2
  185. package/phanpy/dist/assets/dom-p1YcJ5Rw.js.map +0 -1
  186. package/phanpy/dist/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  187. package/phanpy/dist/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  188. package/phanpy/dist/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  189. package/phanpy/dist/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  190. package/phanpy/dist/assets/icons/add-circle-line-WD0mwORD.js +0 -1
  191. package/phanpy/dist/assets/icons/alert-line-CpFUMf9s.js +0 -1
  192. package/phanpy/dist/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
  193. package/phanpy/dist/assets/icons/announcement-line-5nwaJFJm.js +0 -1
  194. package/phanpy/dist/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
  195. package/phanpy/dist/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
  196. package/phanpy/dist/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
  197. package/phanpy/dist/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
  198. package/phanpy/dist/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
  199. package/phanpy/dist/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
  200. package/phanpy/dist/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
  201. package/phanpy/dist/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
  202. package/phanpy/dist/assets/icons/at-line-CF66Bznk.js +0 -1
  203. package/phanpy/dist/assets/icons/attachment-line-BY8It-l9.js +0 -1
  204. package/phanpy/dist/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
  205. package/phanpy/dist/assets/icons/board-line-C0UWXyA6.js +0 -1
  206. package/phanpy/dist/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
  207. package/phanpy/dist/assets/icons/building-5-line-Dd59MzbO.js +0 -1
  208. package/phanpy/dist/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
  209. package/phanpy/dist/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
  210. package/phanpy/dist/assets/icons/calendar-month-line-vommts2C.js +0 -1
  211. package/phanpy/dist/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
  212. package/phanpy/dist/assets/icons/camera-line-Cv69MEei.js +0 -1
  213. package/phanpy/dist/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
  214. package/phanpy/dist/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
  215. package/phanpy/dist/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
  216. package/phanpy/dist/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
  217. package/phanpy/dist/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
  218. package/phanpy/dist/assets/icons/clipboard-line-BBp364gJ.js +0 -1
  219. package/phanpy/dist/assets/icons/close-circle-line-CICdiuGz.js +0 -1
  220. package/phanpy/dist/assets/icons/close-line-DYrFWcjv.js +0 -1
  221. package/phanpy/dist/assets/icons/cloud-line-tiQAbah8.js +0 -1
  222. package/phanpy/dist/assets/icons/code-line-fAl8iiV3.js +0 -1
  223. package/phanpy/dist/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
  224. package/phanpy/dist/assets/icons/copy-2-line-CLW69yF5.js +0 -1
  225. package/phanpy/dist/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
  226. package/phanpy/dist/assets/icons/document-3-line-DBGNhAg2.js +0 -1
  227. package/phanpy/dist/assets/icons/document-line-C7mb9sb7.js +0 -1
  228. package/phanpy/dist/assets/icons/down-line-BGqSSThd.js +0 -1
  229. package/phanpy/dist/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
  230. package/phanpy/dist/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
  231. package/phanpy/dist/assets/icons/exit-line-Dc07BS-x.js +0 -1
  232. package/phanpy/dist/assets/icons/external-link-line-BvpSkmjO.js +0 -1
  233. package/phanpy/dist/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
  234. package/phanpy/dist/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
  235. package/phanpy/dist/assets/icons/filter-2-line-B_mR2usy.js +0 -1
  236. package/phanpy/dist/assets/icons/filter-line-C_nm-3cU.js +0 -1
  237. package/phanpy/dist/assets/icons/flag-1-line-C2HB05I0.js +0 -1
  238. package/phanpy/dist/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
  239. package/phanpy/dist/assets/icons/formula-line-DLrt2d6j.js +0 -1
  240. package/phanpy/dist/assets/icons/grid-line-CgUmEikf.js +0 -1
  241. package/phanpy/dist/assets/icons/group-line-C5fnX0wU.js +0 -1
  242. package/phanpy/dist/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
  243. package/phanpy/dist/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
  244. package/phanpy/dist/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
  245. package/phanpy/dist/assets/icons/heart-line-CJ7K47J4.js +0 -1
  246. package/phanpy/dist/assets/icons/history-2-line-kWWvFzqv.js +0 -1
  247. package/phanpy/dist/assets/icons/history-line-bGQiwbTH.js +0 -1
  248. package/phanpy/dist/assets/icons/home-3-line-fRU1zeLG.js +0 -1
  249. package/phanpy/dist/assets/icons/information-line-iNk6-sOY.js +0 -1
  250. package/phanpy/dist/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
  251. package/phanpy/dist/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
  252. package/phanpy/dist/assets/icons/layout-5-line-B_znt5wO.js +0 -1
  253. package/phanpy/dist/assets/icons/left-line-eUw0tya4.js +0 -1
  254. package/phanpy/dist/assets/icons/lightning-line-DaKoDdix.js +0 -1
  255. package/phanpy/dist/assets/icons/link-2-line-DCqLmMA7.js +0 -1
  256. package/phanpy/dist/assets/icons/list-check-line-BAxosBh9.js +0 -1
  257. package/phanpy/dist/assets/icons/lock-line-CPoaXeUW.js +0 -1
  258. package/phanpy/dist/assets/icons/mail-line-B0P8aa1L.js +0 -1
  259. package/phanpy/dist/assets/icons/moon-line-CEqNAToP.js +0 -1
  260. package/phanpy/dist/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
  261. package/phanpy/dist/assets/icons/more-3-line-BIiVjYTz.js +0 -1
  262. package/phanpy/dist/assets/icons/notification-line-BZZq2Gtu.js +0 -1
  263. package/phanpy/dist/assets/icons/pencil-line-CKh8-A1v.js +0 -1
  264. package/phanpy/dist/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
  265. package/phanpy/dist/assets/icons/pin-line-DpTolyhs.js +0 -1
  266. package/phanpy/dist/assets/icons/play-fill-B0-G_8lX.js +0 -1
  267. package/phanpy/dist/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
  268. package/phanpy/dist/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
  269. package/phanpy/dist/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
  270. package/phanpy/dist/assets/icons/radar-line-CKyroTth.js +0 -1
  271. package/phanpy/dist/assets/icons/react-line-CoIKhhtK.js +0 -1
  272. package/phanpy/dist/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
  273. package/phanpy/dist/assets/icons/right-line-B3KWJaqc.js +0 -1
  274. package/phanpy/dist/assets/icons/rocket-line-4KZl537D.js +0 -1
  275. package/phanpy/dist/assets/icons/round-fill-C69NTrZ0.js +0 -1
  276. package/phanpy/dist/assets/icons/round-line-B9k_1uo-.js +0 -1
  277. package/phanpy/dist/assets/icons/route-line-B28ReHcE.js +0 -1
  278. package/phanpy/dist/assets/icons/rows-4-line-CrBniagM.js +0 -1
  279. package/phanpy/dist/assets/icons/scan-line-DSf74YdP.js +0 -1
  280. package/phanpy/dist/assets/icons/search-2-line-B6geIU1z.js +0 -1
  281. package/phanpy/dist/assets/icons/settings-3-line-azJwK47w.js +0 -1
  282. package/phanpy/dist/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
  283. package/phanpy/dist/assets/icons/share-2-line-D5MxWjrm.js +0 -1
  284. package/phanpy/dist/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
  285. package/phanpy/dist/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
  286. package/phanpy/dist/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
  287. package/phanpy/dist/assets/icons/time-line-BF3eet1v.js +0 -1
  288. package/phanpy/dist/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
  289. package/phanpy/dist/assets/icons/translate-line-BFWol2Ce.js +0 -1
  290. package/phanpy/dist/assets/icons/unlock-line-X_x5vwv3.js +0 -1
  291. package/phanpy/dist/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
  292. package/phanpy/dist/assets/icons/user-4-line-BVSynZCp.js +0 -1
  293. package/phanpy/dist/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
  294. package/phanpy/dist/assets/icons/user-add-line-BbXdHG4z.js +0 -1
  295. package/phanpy/dist/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
  296. package/phanpy/dist/assets/icons/user-follow-line-BXux0wSd.js +0 -1
  297. package/phanpy/dist/assets/icons/user-star-line-DdYlpHLU.js +0 -1
  298. package/phanpy/dist/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
  299. package/phanpy/dist/assets/icons/user-x-line-DIDgNlPH.js +0 -1
  300. package/phanpy/dist/assets/icons/volume-line-Bs390KQt.js +0 -1
  301. package/phanpy/dist/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
  302. package/phanpy/dist/assets/icons/walk-line-yNXfB-md.js +0 -1
  303. package/phanpy/dist/assets/icons/world-2-line-CenkGIwj.js +0 -1
  304. package/phanpy/dist/assets/icons/zoom-in-line-CaENUphf.js +0 -1
  305. package/phanpy/dist/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
  306. package/phanpy/dist/assets/instances-BNPptnn-.json +0 -333
  307. package/phanpy/dist/assets/locales/ar-SA-CfheRbCu.js +0 -1
  308. package/phanpy/dist/assets/locales/ca-ES-DHRW86Tz.js +0 -1
  309. package/phanpy/dist/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
  310. package/phanpy/dist/assets/locales/de-DE-ByRPMRFy.js +0 -1
  311. package/phanpy/dist/assets/locales/eo-UY-CPC7wLCS.js +0 -1
  312. package/phanpy/dist/assets/locales/es-ES-dJa0Fd9t.js +0 -1
  313. package/phanpy/dist/assets/locales/eu-ES-C3djlPR2.js +0 -1
  314. package/phanpy/dist/assets/locales/fa-IR-DTT490Ck.js +0 -1
  315. package/phanpy/dist/assets/locales/fi-FI-B23MCPuz.js +0 -1
  316. package/phanpy/dist/assets/locales/fr-FR-BcaQsO5C.js +0 -1
  317. package/phanpy/dist/assets/locales/gl-ES-BR-CVKBN.js +0 -1
  318. package/phanpy/dist/assets/locales/he-IL-CfheRbCu.js +0 -1
  319. package/phanpy/dist/assets/locales/hu-HU-CfheRbCu.js +0 -1
  320. package/phanpy/dist/assets/locales/it-IT-C1136UFV.js +0 -1
  321. package/phanpy/dist/assets/locales/ja-JP-CRZmRAhD.js +0 -1
  322. package/phanpy/dist/assets/locales/kab-Cz8xH_TT.js +0 -1
  323. package/phanpy/dist/assets/locales/ko-KR-CG9UiY4o.js +0 -1
  324. package/phanpy/dist/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
  325. package/phanpy/dist/assets/locales/nb-NO-CfheRbCu.js +0 -1
  326. package/phanpy/dist/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
  327. package/phanpy/dist/assets/locales/oc-FR-CfheRbCu.js +0 -1
  328. package/phanpy/dist/assets/locales/pl-PL-DkwGXf6P.js +0 -1
  329. package/phanpy/dist/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
  330. package/phanpy/dist/assets/locales/pt-BR-nGL0HdFS.js +0 -1
  331. package/phanpy/dist/assets/locales/pt-PT-Cp17aj2z.js +0 -1
  332. package/phanpy/dist/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
  333. package/phanpy/dist/assets/locales/th-TH-CfheRbCu.js +0 -1
  334. package/phanpy/dist/assets/locales/tok-CfheRbCu.js +0 -1
  335. package/phanpy/dist/assets/locales/tr-TR-CfheRbCu.js +0 -1
  336. package/phanpy/dist/assets/locales/uk-UA-C5E3dFBf.js +0 -1
  337. package/phanpy/dist/assets/locales/zh-CN-BHJi7_et.js +0 -1
  338. package/phanpy/dist/assets/locales/zh-TW-CfheRbCu.js +0 -1
  339. package/phanpy/dist/assets/main-BdqNbG-a.js +0 -26
  340. package/phanpy/dist/assets/main-BdqNbG-a.js.map +0 -1
  341. package/phanpy/dist/assets/mock-home-DGhR__He.js +0 -2
  342. package/phanpy/dist/assets/mock-home-DGhR__He.js.map +0 -1
  343. package/phanpy/dist/assets/multi-column-ROck0NVt.jpg +0 -0
  344. package/phanpy/dist/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  345. package/phanpy/dist/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  346. package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js +0 -2
  347. package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js.map +0 -1
  348. package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js +0 -20
  349. package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js.map +0 -1
  350. package/phanpy/dist/assets/style-B1iLPTPR.css +0 -2
  351. package/phanpy/dist/assets/temml-DuyIQMei.js +0 -97
  352. package/phanpy/dist/assets/temml-DuyIQMei.js.map +0 -1
  353. package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js +0 -2
  354. package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
  355. package/phanpy/dist/assets/year-in-posts-B2lywqdx.png +0 -0
  356. package/phanpy/dist/assets/year-in-posts-DJv_star.js +0 -3
  357. package/phanpy/dist/assets/year-in-posts-DJv_star.js.map +0 -1
  358. package/phanpy/dist/compose/index.html +0 -20
  359. package/phanpy/dist/favicon.ico +0 -0
  360. package/phanpy/dist/index.html +0 -73
  361. package/phanpy/dist/logo-192.png +0 -0
  362. package/phanpy/dist/logo-512.png +0 -0
  363. package/phanpy/dist/logo-badge-72.png +0 -0
  364. package/phanpy/dist/logo-maskable-512.png +0 -0
  365. package/phanpy/dist/logo-monochrome-512.png +0 -0
  366. package/phanpy/dist/logo-monochrome-maskable-512.png +0 -0
  367. package/phanpy/dist/manifest.webmanifest +0 -1
  368. package/phanpy/dist/og-image-2.jpg +0 -0
  369. package/phanpy/dist/og-image.png +0 -0
  370. package/phanpy/dist/robots.txt +0 -2
  371. package/phanpy/dist/sw.js +0 -2
  372. package/phanpy/dist/sw.js.map +0 -1
  373. package/phanpy/dist/version.json +0 -1
  374. package/run-agent.mjs +0 -899
  375. package/vendor/gate.cjs +0 -138
  376. package/vendor/idp-grant.cjs +0 -394
  377. package/vendor/jfetch.cjs +0 -16
  378. package/web/admin/actors.js +0 -147
  379. package/web/admin/bar.css +0 -126
  380. package/web/admin/bar.js +0 -99
  381. package/web/admin/client/client.js +0 -66
  382. package/web/admin/client/index.html +0 -69
  383. package/web/admin/client-pick.js +0 -79
  384. package/web/admin/common.js +0 -23
  385. package/web/admin/connections.js +0 -112
  386. package/web/admin/gateway.js +0 -133
  387. package/web/admin/group.js +0 -258
  388. package/web/admin/index.html +0 -612
  389. package/web/admin/notices-bar.js +0 -90
  390. package/web/admin/oauth-signin.mjs +0 -37
  391. package/web/admin/record.js +0 -393
  392. package/web/admin/setup/index.html +0 -257
  393. package/web/admin/setup/setup.js +0 -339
  394. package/web/admin/tokens.css +0 -68
  395. package/web/admin/upkeep.js +0 -177
  396. package/web/admin/window.css +0 -55
  397. package/web/admin/window.js +0 -136
  398. package/web/app/README.md +0 -81
  399. package/web/app/admin-facade.mjs +0 -588
  400. package/web/app/agent.mjs +0 -672
  401. package/web/app/atproto-browser.mjs +0 -86
  402. package/web/app/authorize_interaction.html +0 -56
  403. package/web/app/authorize_interaction.js +0 -69
  404. package/web/app/boot.mjs +0 -616
  405. package/web/app/deliver-relay.mjs +0 -129
  406. package/web/app/dist/boot.js +0 -34562
  407. package/web/app/dist/boot.js.map +0 -7
  408. package/web/app/dist/sw.js +0 -73456
  409. package/web/app/dist/sw.js.map +0 -7
  410. package/web/app/fediacct-browser.mjs +0 -146
  411. package/web/app/gateway-move.mjs +0 -60
  412. package/web/app/idb-kv.mjs +0 -35
  413. package/web/app/index.html +0 -258
  414. package/web/app/keys-browser.mjs +0 -71
  415. package/web/app/keystore.mjs +0 -82
  416. package/web/app/oidc-session.mjs +0 -6
  417. package/web/app/pod-auth.mjs +0 -248
  418. package/web/app/pod-remote.mjs +0 -84
  419. package/web/app/shims/_globals.mjs +0 -2
  420. package/web/app/shims/fedify-sig.mjs +0 -77
  421. package/web/app/shims/node-crypto.mjs +0 -113
  422. package/web/app/shims/node-fs.mjs +0 -17
  423. package/web/app/shims/node-path.mjs +0 -19
  424. package/web/app/shims/node-url.mjs +0 -7
  425. package/web/app/shims/prelude.js +0 -58
  426. package/web/app/shims/safefetch.mjs +0 -58
  427. package/web/app/shims/shapes-text.mjs +0 -8
  428. package/web/app/shims/web-push.mjs +0 -14
  429. package/web/app/signup.mjs +0 -357
  430. package/web/app/sw-src.mjs +0 -220
  431. package/web/app/update.js +0 -70
  432. package/web/front/admin.html +0 -55
  433. package/web/front/admin.js +0 -89
  434. package/web/front/install.sh +0 -69
  435. package/web/front/new-account.html +0 -35
  436. package/web/front/new-account.js +0 -13
  437. package/web/front/notices.html +0 -69
  438. package/web/front/notices.js +0 -107
  439. package/web/front/run.html +0 -64
  440. package/web/front/run.js +0 -221
  441. package/web/front/solid-oidc-client.js +0 -6
  442. package/web/icons/forum.svg +0 -11
  443. package/web/icons/gateway.svg +0 -14
  444. package/web/icons/local.svg +0 -11
@@ -1,438 +0,0 @@
1
- // timelines.mjs — what a client reads by the page: the home and public
2
- // timelines, hashtags and the tag feed, lists and their timelines, the v2
3
- // filters, conversations, notifications, and search.
4
-
5
- import crypto from 'node:crypto';
6
- import * as social from '../../core/social.mjs';
7
- import { sanitizeHtml } from '../../core/wire.mjs';
8
- import { authorOf } from '../../core/intake/index.mjs';
9
- import { readBody } from './body.mjs';
10
-
11
- export async function handle(api, ctx) {
12
- const { req, res, pathname, url, send } = ctx; // eslint-disable-line no-unused-vars
13
-
14
- // home + public = everything known (follows, boosts, tag feed, own);
15
- // public?local=true = own posts; trends/statuses = the same activity (a
16
- // single-actor instance has no firehose — what it knows IS its public
17
- // face). Sorted by publish time so tag-feed backfill interleaves.
18
- if (pathname === '/api/v1/timelines/home' || pathname === '/api/v1/timelines/public'
19
- || pathname === '/api/v1/trends/statuses') {
20
- const localOnly = pathname === '/api/v1/timelines/public' && url.searchParams.get('local') === 'true';
21
- // The UNFILTERED list, both as the source and as the reply-count corpus:
22
- // a reply can be a mention, and mentions are filtered out of timelines
23
- // below — counting against the filtered set would undercount them.
24
- const all = api.store.getStatuses();
25
- const muted = new Set(api.store.getMuted().actors);
26
- let items = all
27
- // search ingests and strangers' mentions stay out of the timelines
28
- // (mentions remain in notifications and /api/v1/statuses); direct
29
- // posts belong to the conversations view, not a timeline; and the
30
- // local/public view shows only what is actually public-facing.
31
- .filter(s => s.kind !== 'remote' && s.kind !== 'mention')
32
- .filter(s => !s.direct && s.visibility !== 'direct')
33
- .filter(s => !muted.has(s.actor) && !muted.has(s.via))
34
- .filter(s => !localOnly || (s.kind === 'post' && s.visibility !== 'private'))
35
- .sort((a, b) => String(b.published || '').localeCompare(String(a.published || '')));
36
- const { items: page, headers } = api.page(items, url);
37
- return send(200, page.map(s => api.statusOrBoost(s, { all })), headers);
38
- }
39
-
40
- // --- hashtags: the client's Followed Hashtags surface drives the tag feed ---
41
- // followed_tags is what the feed currently pulls; follow/unfollow add and
42
- // remove a hashtag (unfollow the last and the topical feed goes quiet); the
43
- // tag timeline is the mirrored notes carrying that tag.
44
- if (pathname === '/api/v1/followed_tags') {
45
- const tags = api.agent.tagfeed?.config().tags || [];
46
- return send(200, tags.map(t => api.tagObject(t, true, req)));
47
- }
48
- const mTagFollow = pathname.match(/^\/api\/v1\/tags\/([^/]+)\/(follow|unfollow)$/);
49
- if (mTagFollow && req.method === 'POST') {
50
- const name = decodeURIComponent(mTagFollow[1]).replace(/^#/, '').toLowerCase();
51
- const follow = mTagFollow[2] === 'follow';
52
- const tf = api.agent.tagfeed;
53
- if (tf) {
54
- const cur = tf.config().tags;
55
- tf.setConfig({ tags: follow ? [...new Set([...cur, name])] : cur.filter(t => t !== name) });
56
- }
57
- return send(200, api.tagObject(name, follow, req));
58
- }
59
- const mTagGet = pathname.match(/^\/api\/v1\/tags\/([^/]+)$/);
60
- if (mTagGet && req.method === 'GET') {
61
- const name = decodeURIComponent(mTagGet[1]).replace(/^#/, '').toLowerCase();
62
- const following = (api.agent.tagfeed?.config().tags || []).includes(name);
63
- return send(200, api.tagObject(name, following, req));
64
- }
65
- // --- trending tags: counted off the notes already mirrored ---
66
- //
67
- // A single-actor instance has no firehose to measure, so "trending" here can
68
- // only mean what the tag feed has actually brought in — which is the honest
69
- // answer, and the one that makes a client's Explore surface show something
70
- // instead of an empty list. `/api/v1/trends` is Mastodon's older name for the
71
- // same thing.
72
- if (pathname === '/api/v1/trends/tags' || pathname === '/api/v1/trends') {
73
- const DAY = 86400000;
74
- const today = Math.floor(Date.now() / DAY) * DAY;
75
- // tag -> day -> { uses, accounts }. Accounts are counted distinctly, which
76
- // is what the number means in Mastodon: one person posting nine times is
77
- // nine uses by one account, and a client shows the second number.
78
- const seen = new Map();
79
- for (const st of api.store.getStatuses()) {
80
- if (st.kind !== 'tag' || !st.tag) continue;
81
- const at = Date.parse(st.published || '');
82
- if (!Number.isFinite(at) || at < today - 6 * DAY) continue;
83
- const day = Math.floor(at / DAY) * DAY;
84
- if (!seen.has(st.tag)) seen.set(st.tag, new Map());
85
- const days = seen.get(st.tag);
86
- if (!days.has(day)) days.set(day, { uses: 0, accounts: new Set() });
87
- const rec = days.get(day);
88
- rec.uses++;
89
- if (st.actor) rec.accounts.add(st.actor);
90
- }
91
- const followed = new Set(api.agent.tagfeed?.config().tags || []);
92
- const ranked = [...seen.entries()].map(([name, days]) => {
93
- // Seven entries, newest first, gaps filled with zeroes — a client draws
94
- // the sparkline straight off this and a short array draws a short line.
95
- const history = [];
96
- for (let i = 0; i < 7; i++) {
97
- const rec = days.get(today - i * DAY);
98
- history.push({ day: String(Math.floor((today - i * DAY) / 1000)),
99
- uses: String(rec ? rec.uses : 0), accounts: String(rec ? rec.accounts.size : 0) });
100
- }
101
- return { total: history.reduce((n, h) => n + Number(h.uses), 0),
102
- tag: api.tagObject(name, followed.has(name), req, history) };
103
- }).sort((a, b) => b.total - a.total);
104
- const limit = Math.min(Math.max(Number(url.searchParams.get('limit')) || 10, 1), 40);
105
- return send(200, ranked.slice(0, limit).map(x => x.tag));
106
- }
107
-
108
- const mTagTl = pathname.match(/^\/api\/v1\/timelines\/tag\/([^/]+)$/);
109
- if (mTagTl && req.method === 'GET') {
110
- const name = decodeURIComponent(mTagTl[1]).replace(/^#/, '').toLowerCase();
111
- const all = api.store.getStatuses();
112
- const items = all.filter(s => s.kind === 'tag' && s.tag === name)
113
- .sort((a, b) => String(b.published || '').localeCompare(String(a.published || '')));
114
- const { items: page, headers } = api.page(items, url);
115
- return send(200, page.map(s => api.statusOrBoost(s, { all })), headers);
116
- }
117
-
118
- // Lists, filters and scheduled posts are the client's own arrangements —
119
- // kept in local state, nothing federates.
120
- const listJson = (l) => ({ id: l.id, title: l.title, replies_policy: l.repliesPolicy || 'list', exclusive: false });
121
- if (pathname === '/api/v1/lists' && req.method === 'GET') {
122
- return send(200, api.store.getLists().map(listJson));
123
- }
124
- if (pathname === '/api/v1/lists' && req.method === 'POST') {
125
- const body = await readBody(req);
126
- const title = String(body.title || '').trim();
127
- if (!title) return send(422, { error: 'a title is required' });
128
- const lists = api.store.getLists();
129
- const l = { id: crypto.randomBytes(8).toString('hex'), title, repliesPolicy: body.replies_policy || 'list', members: [] };
130
- lists.push(l);
131
- api.store.setLists(lists);
132
- return send(200, listJson(l));
133
- }
134
- const mList = /^\/api\/v1\/lists\/([a-f0-9]+)$/.exec(pathname);
135
- if (mList) {
136
- const lists = api.store.getLists();
137
- const l = lists.find(x => x.id === mList[1]);
138
- if (!l) return send(404, { error: 'Record not found' });
139
- if (req.method === 'DELETE') {
140
- api.store.setLists(lists.filter(x => x.id !== mList[1]));
141
- return send(200, {});
142
- }
143
- if (req.method === 'PUT') {
144
- const body = await readBody(req);
145
- if (body.title) l.title = String(body.title);
146
- if (body.replies_policy) l.repliesPolicy = body.replies_policy;
147
- api.store.setLists(lists);
148
- }
149
- return send(200, listJson(l));
150
- }
151
- const mListAcc = /^\/api\/v1\/lists\/([a-f0-9]+)\/accounts$/.exec(pathname);
152
- if (mListAcc) {
153
- const lists = api.store.getLists();
154
- const l = lists.find(x => x.id === mListAcc[1]);
155
- if (!l) return send(404, { error: 'Record not found' });
156
- if (req.method === 'GET') return send(200, (l.members || []).map(a => api.account(a)));
157
- const body = await readBody(req).catch(() => ({}));
158
- const ids = [].concat(body.account_ids || body['account_ids[]'] || url.searchParams.getAll('account_ids[]')).filter(Boolean);
159
- const actors = ids.map(id => api.store.urlFor(id)).filter(Boolean);
160
- if (req.method === 'POST') l.members = [...new Set([...(l.members || []), ...actors])];
161
- if (req.method === 'DELETE') l.members = (l.members || []).filter(a => !actors.includes(a));
162
- api.store.setLists(lists);
163
- return send(200, {});
164
- }
165
- const mListTl = /^\/api\/v1\/timelines\/list\/([a-f0-9]+)$/.exec(pathname);
166
- if (mListTl) {
167
- const l = api.store.getLists().find(x => x.id === mListTl[1]);
168
- if (!l) return send(404, { error: 'Record not found' });
169
- const members = new Set(l.members || []);
170
- const all = api.store.getStatuses();
171
- const items = all.filter(s => members.has(s.actor) || members.has(s.via))
172
- .sort((a, b) => String(b.published || '').localeCompare(String(a.published || '')));
173
- const { items: page, headers } = api.page(items, url);
174
- return send(200, page.map(s => api.statusOrBoost(s, { all })), headers);
175
- }
176
-
177
- // v2 filters: stored, served, and APPLIED — every status carries what it
178
- // matched in `filtered` (see filtersFor), because no client matches
179
- // keywords for itself.
180
- const filterJson = (f) => ({
181
- id: f.id, title: f.title, context: f.context || ['home'],
182
- expires_at: f.expiresAt || null, filter_action: f.action || 'warn',
183
- keywords: (f.keywords || []).map((k, i) => ({ id: `${f.id}-${i}`, keyword: k.keyword, whole_word: !!k.wholeWord })),
184
- statuses: [],
185
- });
186
- const keywordsOf = (attrs) => [].concat(attrs || [])
187
- .filter(k => k?.keyword && !(k._destroy === true || k._destroy === 'true'))
188
- .map(k => ({ keyword: String(k.keyword), wholeWord: k.whole_word === true || k.whole_word === 'true' }));
189
- if (pathname === '/api/v2/filters' && req.method === 'GET') {
190
- return send(200, api.store.getFilters().map(filterJson));
191
- }
192
- if (pathname === '/api/v2/filters' && req.method === 'POST') {
193
- const body = await readBody(req);
194
- const title = String(body.title || '').trim();
195
- if (!title) return send(422, { error: 'a title is required' });
196
- const filters = api.store.getFilters();
197
- const f = {
198
- id: crypto.randomBytes(8).toString('hex'), title,
199
- context: [].concat(body.context || ['home']),
200
- action: body.filter_action || 'warn', expiresAt: null,
201
- keywords: keywordsOf(body.keywords_attributes),
202
- };
203
- filters.push(f);
204
- api.store.setFilters(filters);
205
- return send(200, filterJson(f));
206
- }
207
- const mFilter = /^\/api\/v2\/filters\/([a-f0-9]+)$/.exec(pathname);
208
- if (mFilter) {
209
- const filters = api.store.getFilters();
210
- const f = filters.find(x => x.id === mFilter[1]);
211
- if (!f) return send(404, { error: 'Record not found' });
212
- if (req.method === 'DELETE') {
213
- api.store.setFilters(filters.filter(x => x.id !== mFilter[1]));
214
- return send(200, {});
215
- }
216
- if (req.method === 'PUT') {
217
- const body = await readBody(req);
218
- if (body.title) f.title = String(body.title);
219
- if (body.context) f.context = [].concat(body.context);
220
- if (body.filter_action) f.action = body.filter_action;
221
- if (body.keywords_attributes) f.keywords = keywordsOf(body.keywords_attributes);
222
- api.store.setFilters(filters);
223
- }
224
- return send(200, filterJson(f));
225
- }
226
-
227
- // Conversations: direct posts, grouped by who is in them.
228
- if (pathname === '/api/v1/conversations' && req.method === 'GET') {
229
- const all = api.store.getStatuses();
230
- const me = api.urls.actor;
231
- const convos = new Map();
232
- for (const s of all) {
233
- if (!(s.direct || s.visibility === 'direct')) continue;
234
- const others = [...new Set([
235
- ...(s.actor !== me ? [s.actor] : []),
236
- ...((s.mentions || []).map(m => m.href).filter(a => a && a !== me)),
237
- ])];
238
- const key = others.sort().join(' ') || me;
239
- const c = convos.get(key) || { accounts: new Set(), last: s };
240
- for (const o of others) c.accounts.add(o);
241
- if (String(s.published || '') > String(c.last.published || '')) c.last = s;
242
- convos.set(key, c);
243
- }
244
- const items = [...convos.entries()]
245
- .sort((a, b) => String(b[1].last.published || '').localeCompare(String(a[1].last.published || '')));
246
- return send(200, items.map(([key, c]) => ({
247
- id: api.store.idFor('conversation:' + key),
248
- unread: false,
249
- accounts: [...(c.accounts.size ? c.accounts : [me])].map(a => api.account(a)),
250
- last_status: api.status(c.last, { all }),
251
- })));
252
- }
253
-
254
- // The direct timeline: every direct post, newest first, paged like the rest.
255
- if (pathname === '/api/v1/timelines/direct' && req.method === 'GET') {
256
- const all = api.store.getStatuses();
257
- const items = all.filter(s => s.direct || s.visibility === 'direct');
258
- const { items: page, headers } = api.page(items, url);
259
- return send(200, page.map(s => api.status(s, { all })), headers);
260
- }
261
-
262
- // One notification by id — what a push notification tap opens — and the
263
- // two ways of being rid of them.
264
- if (pathname === '/api/v1/notifications/clear' && req.method === 'POST') {
265
- api.store.removeNotifications(() => true);
266
- return send(200, {});
267
- }
268
- const mNotif = /^\/api\/v1\/notifications\/([A-Za-z0-9_-]+)(\/dismiss)?$/.exec(pathname);
269
- if (mNotif) {
270
- const n = api.store.getNotifications().find(x => x.id === mNotif[1]);
271
- if (!n) return send(404, { error: 'Record not found' });
272
- if (mNotif[2] && req.method === 'POST') { api.store.removeNotifications(x => x.id === n.id); return send(200, {}); }
273
- if (!mNotif[2] && req.method === 'GET') return send(200, api.notification(n));
274
- }
275
-
276
- // Grouped notifications, the shape a client reads from API version 2 on.
277
- // One group per notification: the client does its own gathering of
278
- // favourites and boosts on a post, and every field it joins on — the
279
- // account and status ids — is here.
280
- const mGroup = /^\/api\/v2\/notifications\/([A-Za-z0-9_-]+)(\/accounts|\/dismiss)?$/.exec(pathname);
281
- if (mGroup) {
282
- const n = api.store.getNotifications().find(x => x.id === mGroup[1]);
283
- if (!n) return send(404, { error: 'Record not found' });
284
- if (mGroup[2] === '/dismiss' && req.method === 'POST') { api.store.removeNotifications(x => x.id === n.id); return send(200, {}); }
285
- if (mGroup[2] === '/accounts' && req.method === 'GET') return send(200, [api.account(n.actor)]);
286
- if (!mGroup[2] && req.method === 'GET') return send(200, groupedNotifications(api, [n]));
287
- }
288
- if (pathname === '/api/v2/notifications') {
289
- const { page, headers } = notificationPage(api, ctx);
290
- return send(200, groupedNotifications(api, page), headers);
291
- }
292
-
293
- if (pathname === '/api/v1/notifications') {
294
- const { page, headers } = notificationPage(api, ctx);
295
- return send(200, page.map((n) => api.notification(n)), headers);
296
- }
297
-
298
- // Search: @user@host → webfinger resolve; URL → actor or note ingest;
299
- // plain text → local mirror + actor-cache scan.
300
- if (pathname === '/api/v2/search' || pathname === '/api/v1/search') {
301
- return searchHandler(api, ctx);
302
- }
303
- return false;
304
- }
305
-
306
- function groupedNotifications(api, page) {
307
- const accounts = new Map();
308
- const statuses = new Map();
309
- const groups = page.map((n) => {
310
- const acct = api.account(n.actor);
311
- accounts.set(acct.id, acct);
312
- const s = n.noteId && api.store.getStatuses().find(x => x.noteId === n.noteId);
313
- let statusId = null;
314
- if (s) { const j = api.status(s); statuses.set(j.id, j); statusId = j.id; }
315
- return {
316
- group_key: n.id, notifications_count: 1, type: api.notificationType(n.type),
317
- most_recent_notification_id: n.id, page_min_id: n.id, page_max_id: n.id,
318
- latest_page_notification_at: n.at, sample_account_ids: [acct.id],
319
- ...(statusId ? { status_id: statusId } : {}),
320
- ...(n.type === 'reaction' ? { emoji: n.emoji, ...(n.url ? { emoji_url: n.url } : {}) } : {}),
321
- };
322
- });
323
- return { accounts: [...accounts.values()], statuses: [...statuses.values()], notification_groups: groups };
324
- }
325
-
326
- // The slice of notifications a request asks for, and the Link header that
327
- // pages it. Shared by the flat list and the grouped one.
328
- function notificationPage(api, ctx) {
329
- const { req, pathname, url } = ctx;
330
- {
331
- const limit = Math.min(Number(url.searchParams.get('limit')) || 30, 60);
332
- const q = url.searchParams;
333
- // Newest first, which is what every id rule below assumes.
334
- let items = api.store.getNotifications();
335
-
336
- // Which KINDS. A client that asks for mentions and is handed favourites
337
- // and boosts renders them as mentions — Phanpy's Mentions column did
338
- // exactly that, and re-pilled on every poll. Both spellings: clients send
339
- // `types[]=mention` and some send bare `types=mention`.
340
- const listParam = (name) => {
341
- const all = [...q.getAll(`${name}[]`), ...q.getAll(name)]
342
- .flatMap((v) => String(v).split(',')).map((v) => v.trim()).filter(Boolean);
343
- return all.length ? new Set(all) : null;
344
- };
345
- const want = listParam('types');
346
- const skip = listParam('exclude_types');
347
- // Mastodon spells the type with an underscore; ours is stored with a
348
- // hyphen, and notification() translates on the way out — so match on what
349
- // the client would have been given, not on what is on disk.
350
- const shown = (n) => api.notificationType(n.type);
351
- if (want) items = items.filter((n) => want.has(shown(n)));
352
- if (skip) items = items.filter((n) => !skip.has(shown(n)));
353
-
354
- // Which SLICE. `max_id` walks backwards into history; `since_id` and
355
- // `min_id` both mean "newer than this", which is how a client asks
356
- // "anything since I last looked?". Ignoring them meant every poll
357
- // returned the whole list, so the client always saw unread items and the
358
- // bell never went out.
359
- const cut = (id, keepNewer) => {
360
- const i = items.findIndex((n) => n.id === id);
361
- if (i < 0) return;
362
- items = keepNewer ? items.slice(0, i) : items.slice(i + 1);
363
- };
364
- const maxId = q.get('max_id');
365
- if (maxId) cut(maxId, false);
366
- const sinceId = q.get('since_id') || q.get('min_id');
367
- if (sinceId) cut(sinceId, true);
368
- // min_id asks for the ones IMMEDIATELY newer, i.e. the oldest end of
369
- // what is newer; since_id asks for the newest. Only the window differs.
370
- const page = q.get('min_id') && !q.get('since_id')
371
- ? items.slice(Math.max(0, items.length - limit))
372
- : items.slice(0, limit);
373
-
374
- // A client pages by following these rather than by guessing ids.
375
- if (page.length) {
376
- const base = `${api.scheme || (req.socket?.encrypted ? 'https' : 'http')}://${req.headers.host}${api.mount || ''}${pathname}`;
377
- const link = (params) => {
378
- const u = new URL(base);
379
- for (const [k, v] of q) if (k !== 'max_id' && k !== 'since_id' && k !== 'min_id') u.searchParams.append(k, v);
380
- for (const [k, v] of Object.entries(params)) u.searchParams.set(k, v);
381
- return u.href;
382
- };
383
- return { page, headers: {
384
- link: `<${link({ max_id: page[page.length - 1].id })}>; rel="next", `
385
- + `<${link({ min_id: page[0].id })}>; rel="prev"`,
386
- } };
387
- }
388
- return { page: [], headers: undefined };
389
- }
390
- }
391
-
392
- // Search: @user@host → webfinger resolve; URL → actor or note ingest;
393
- // plain text → local mirror + actor-cache scan.
394
- async function searchHandler(api, ctx) {
395
- const { req, res, pathname, url, send } = ctx; // eslint-disable-line no-unused-vars
396
- {
397
- const q = String(url.searchParams.get('q') || '').trim();
398
- const type = url.searchParams.get('type');
399
- const out = { accounts: [], statuses: [], hashtags: [] };
400
- const asHandle = /^@?[^@\s]+@[^@\s.]+\.[^@\s]+$/.test(q);
401
- if (asHandle && type !== 'statuses') {
402
- try {
403
- const doc = await social.resolveHandle(api.agent, q);
404
- out.accounts.push(api.account(doc.id));
405
- } catch (e) { api.log(`search resolve ${q}: ${e.message}`); }
406
- } else if (/^https?:\/\//.test(q)) {
407
- const doc = await api.agent.intake.fetchAP(q).catch(() => null);
408
- if (doc?.type === 'Person' && doc.id) out.accounts.push(api.account(doc.id));
409
- else if (doc?.type === 'Note' && doc.id) {
410
- let s = api.store.getStatuses().find(x => x.noteId === doc.id);
411
- // Third site of the same rule: the note's own origin has to vouch for
412
- // the author before we store one. The owner chose the URL, but the
413
- // document at it still names whoever it likes.
414
- const author = authorOf(doc);
415
- if (!s && author) {
416
- s = {
417
- noteId: doc.id, actor: author, content: sanitizeHtml(doc.content),
418
- published: doc.published, inReplyTo: doc.inReplyTo, kind: 'remote',
419
- };
420
- api.store.addStatus(s);
421
- }
422
- if (s) out.statuses.push(api.status(s));
423
- }
424
- } else if (q) {
425
- const needle = q.toLowerCase();
426
- if (type !== 'accounts') {
427
- const all = api.store.getStatuses();
428
- out.statuses = all
429
- .filter(s => (s.content || '').toLowerCase().includes(needle)).slice(0, 20)
430
- .map(s => api.status(s, { all }));
431
- }
432
- if (type !== 'statuses') out.accounts = await api.accountSearch(q);
433
- }
434
- return send(200, out);
435
- }
436
-
437
- return false;
438
- }
@@ -1,62 +0,0 @@
1
- // oidc-auth.mjs — who may drive the C2S surface. Two proofs are accepted:
2
- // the Mastodon facade's bearer (minted on this machine, so its holder is the
3
- // operator by construction), or a Solid-OIDC token with its DPoP proof,
4
- // which Solid-OIDC requires, verified by the same library the wider
5
- // Solid world uses. The token alone is not enough: the WebID it names must
6
- // be THIS identity's owner, an authorization step the token does not carry.
7
- //
8
- // The verifier is injected so offline tests stub it, and wrapped so the
9
- // library (CJS, older jose) can be replaced without touching any caller.
10
-
11
- export function makeC2sAuth({ agent, masto = null, verifier = null, log = () => {}, scheme = null, mount = '' }) {
12
- let verify = verifier;
13
- const loadVerifier = async () => {
14
- if (!verify) {
15
- const { createSolidTokenVerifier } = await import('@solid/access-token-verifier');
16
- verify = createSolidTokenVerifier();
17
- }
18
- return verify;
19
- };
20
-
21
- return async function authenticate(req, pathname) {
22
- if (masto?.authed(req)) {
23
- return { ok: true, webid: agent.remote?.webId || null, via: 'bearer' };
24
- }
25
- if (!req.headers.authorization) {
26
- return { ok: false, status: 401, error: 'authentication required: a Solid-OIDC token (DPoP) or this agent\'s own bearer' };
27
- }
28
- // Solid-OIDC binds the token to a key and has the client prove it holds
29
- // that key on every request. A token shown without its proof is a token
30
- // anyone who saw it could show.
31
- if (!req.headers.dpop) {
32
- return { ok: false, status: 401, error: 'a Solid-OIDC token must come with its DPoP proof',
33
- headers: { 'www-authenticate': 'DPoP' } };
34
- }
35
- let webid;
36
- try {
37
- const v = await loadVerifier();
38
- // The URL the client signed its proof over. The Host header already
39
- // passed the Authorities firewall, so whichever alias the client used
40
- // (localhost, 127.0.0.1, the named origin) is one this agent answers on;
41
- // the scheme is whichever listener the request arrived on. `pathname` is
42
- // relative to this identity's mount, so a suffix pod folds the mount back
43
- // in — the client signed over the full path it actually requested.
44
- const htu = `${scheme ? scheme.replace(/:$/u, '') : req.socket?.encrypted ? 'https' : 'http'
45
- }://${req.headers.host}${mount}${pathname}`;
46
- ({ webid } = await v(
47
- req.headers.authorization,
48
- { header: req.headers.dpop, method: req.method, url: htu },
49
- ));
50
- } catch (e) {
51
- log(`c2s auth: token rejected — ${e.message}`);
52
- return { ok: false, status: 401, error: `token rejected: ${e.message}` };
53
- }
54
- const owner = agent.remote?.webId;
55
- if (!owner || webid !== owner) {
56
- // Verified is not authorized: a valid token from ANY WebID must not
57
- // post as this actor. (The gap jg10's outbox leaves open.)
58
- return { ok: false, status: 403, error: 'authenticated, but this outbox belongs to its owner alone' };
59
- }
60
- return { ok: true, webid, via: 'oidc' };
61
- };
62
- }
@@ -1,154 +0,0 @@
1
- // streaming.mjs — the Mastodon streaming API over a hand-rolled WebSocket
2
- // server (send-mostly; we answer pings and ignore everything else a client
3
- // frames at us). No dependencies — the handshake is one SHA-1 and text
4
- // frames are trivial to emit. Clients connect to
5
- // ws://host/api/v1/streaming?access_token=…&stream=user
6
- // and receive {stream, event, payload} messages for new statuses and
7
- // notifications the moment the agent learns of them.
8
-
9
- import crypto from 'node:crypto';
10
-
11
- const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
12
-
13
- // One identity's live feed is a handful of that owner's clients, so these are
14
- // generous. Without them a single token could hold every file descriptor open,
15
- // and one client that stops reading would grow its write buffer forever.
16
- const MAX_SOCKETS = 20; // matches the client-token cap
17
- const MAX_BUFFERED_BYTES = 1024 * 1024; // a consumer this far behind is gone
18
-
19
- import { checkRequest } from '../shared/guard.mjs';
20
-
21
- export class Streaming {
22
- constructor({ masto, log = console.log, allowed = null, gate = null, gateOptional = false }) {
23
- this.masto = masto;
24
- this.log = log;
25
- this.allowed = allowed;
26
- this.gate = gate;
27
- // A pod-hosted identity is a public instance: a phone app arrives with its
28
- // bearer and no way to send the operator's header, so there the bearer is
29
- // the credential. Locally the gate stands in front of the socket as well.
30
- this.gateOptional = gateOptional;
31
- this.sockets = new Set(); // entries: { send(string), close() }
32
- }
33
-
34
- // Whether a client may open the live feed, asked of the upgrade request
35
- // alone. Shared with the pod-server front, which completes the handshake
36
- // before any of this runs and so must answer by closing rather than refusing.
37
- authorizeUpgrade(req, url) {
38
- const proto = String(req.headers['sec-websocket-protocol'] || '').split(',')[0].trim();
39
- const token = url.searchParams.get('access_token') || proto;
40
- const hasToken = this.masto.tokens().includes(token);
41
- // WebSockets are exempt from CORS. A live bearer is the credential — a
42
- // web client on its own origin connects the way it does to any Mastodon
43
- // server — so with one, only the Host check stands (it is what stops DNS
44
- // rebinding); without one the full Host/Origin firewall applies.
45
- const bad = this.allowed ? checkRequest(req, this.allowed, { ignoreOrigin: hasToken }) : null;
46
- if (bad) return `streaming refused: ${bad}`;
47
- // A gate token covers every other route; an upgrade that skipped it was a
48
- // hole in a control the operator had switched on. A client bearer is the
49
- // other way in — a phone app has no way to send the operator's header.
50
- if (this.gate && !this.gate.upgradeOk(req) && !(this.gateOptional && hasToken)) {
51
- return 'streaming refused: no gate token';
52
- }
53
- if (!hasToken) return 'streaming refused: not a live client token';
54
- if (this.sockets.size >= MAX_SOCKETS) return 'streaming refused: too many live connections';
55
- return null;
56
- }
57
-
58
- // Adopt a WebSocket somebody else finished the handshake for — the pod
59
- // server owns the socket, and hands over an object that already frames.
60
- adopt(ws) {
61
- const entry = {
62
- send: (str) => {
63
- // A consumer that stops reading must not keep costing memory.
64
- if ((ws.bufferedAmount ?? 0) > MAX_BUFFERED_BYTES) {
65
- this.sockets.delete(entry);
66
- try { ws.close(); } catch { /* already gone */ }
67
- return;
68
- }
69
- ws.send(str);
70
- },
71
- close: () => ws.close(),
72
- };
73
- this.sockets.add(entry);
74
- const drop = () => this.sockets.delete(entry);
75
- ws.on('close', drop);
76
- ws.on('error', drop);
77
- return entry;
78
- }
79
-
80
- // Let go of every client: the server is stopping, not the connection failing.
81
- stop() {
82
- for (const s of this.sockets) { try { s.close(); } catch { /* already gone */ } }
83
- this.sockets.clear();
84
- }
85
-
86
- // Attach the upgrade handler to an http.Server.
87
- attach(server) {
88
- server.on('upgrade', (req, socket) => {
89
- const url = new URL(req.url, 'http://localhost');
90
- if (!url.pathname.startsWith('/api/v1/streaming')) { socket.destroy(); return; }
91
- // WebSockets are exempt from CORS, so the Host/Origin firewall is the
92
- // only thing standing between a visited page and the live feed.
93
- const refusal = this.authorizeUpgrade(req, url);
94
- if (refusal) {
95
- this.log(refusal);
96
- socket.write(refusal.includes('refused:') && !refusal.includes('token')
97
- ? 'HTTP/1.1 403 Forbidden\r\n\r\n'
98
- : 'HTTP/1.1 401 Unauthorized\r\n\r\n');
99
- socket.destroy();
100
- return;
101
- }
102
- const proto = String(req.headers['sec-websocket-protocol'] || '').split(',')[0].trim();
103
- const accept = crypto.createHash('sha1')
104
- .update(req.headers['sec-websocket-key'] + WS_MAGIC).digest('base64');
105
- socket.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n'
106
- + `Sec-WebSocket-Accept: ${accept}\r\n`
107
- + (proto ? `Sec-WebSocket-Protocol: ${proto}\r\n` : '') + '\r\n');
108
- socket.setNoDelay(true);
109
- const entry = {
110
- send: (str) => {
111
- // A consumer that stops reading must not keep costing memory.
112
- if (socket.writableLength > MAX_BUFFERED_BYTES) {
113
- this.sockets.delete(entry);
114
- try { socket.destroy(); } catch { /* already gone */ }
115
- return;
116
- }
117
- socket.write(this.frame(str));
118
- },
119
- close: () => { try { socket.end(); } catch { /* already gone */ } },
120
- };
121
- this.sockets.add(entry);
122
- const drop = () => this.sockets.delete(entry);
123
- socket.on('close', drop);
124
- socket.on('error', drop);
125
- socket.on('data', (buf) => {
126
- if (buf.length && (buf[0] & 0x0f) === 0x9) { // ping → pong
127
- try { socket.write(Buffer.from([0x8a, 0x00])); } catch { drop(); }
128
- }
129
- if (buf.length && (buf[0] & 0x0f) === 0x8) { drop(); try { socket.end(); } catch {} } // close
130
- });
131
- });
132
- }
133
-
134
- // Server→client unmasked text frame.
135
- frame(str) {
136
- const payload = Buffer.from(str);
137
- let header;
138
- if (payload.length < 126) header = Buffer.from([0x81, payload.length]);
139
- else if (payload.length < 65536) {
140
- header = Buffer.alloc(4); header[0] = 0x81; header[1] = 126; header.writeUInt16BE(payload.length, 2);
141
- } else {
142
- header = Buffer.alloc(10); header[0] = 0x81; header[1] = 127; header.writeBigUInt64BE(BigInt(payload.length), 2);
143
- }
144
- return Buffer.concat([header, payload]);
145
- }
146
-
147
- broadcast(event, payloadObj, streams = ['user']) {
148
- if (!this.sockets.size) return;
149
- const message = JSON.stringify({ stream: streams, event, payload: JSON.stringify(payloadObj) });
150
- for (const s of this.sockets) {
151
- try { s.send(message); } catch { this.sockets.delete(s); }
152
- }
153
- }
154
- }