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,98 +0,0 @@
1
- // webpush.mjs — Mastodon-style Web Push from the agent itself. The VAPID
2
- // keypair is minted once into state; each client login keeps one subscription;
3
- // payloads go straight from this process to the browser's push service, so a
4
- // closed client still hears about mentions. No relay, no third party account,
5
- // and nothing here touches the pod.
6
- import crypto from 'node:crypto';
7
- import webpush from 'web-push';
8
-
9
- export class Push {
10
- constructor({ store, subject, log = () => {} }) {
11
- this.store = store;
12
- this.subject = subject; // () => the actor URL, VAPID's contact
13
- this.log = log;
14
- }
15
-
16
- state() { return this.store.read('webpush.json', { subs: {} }); }
17
- save(s) { this.store.write('webpush.json', s); }
18
-
19
- vapid() {
20
- const st = this.state();
21
- if (!st.vapid) {
22
- st.vapid = webpush.generateVAPIDKeys();
23
- this.save(st);
24
- this.log('webpush: VAPID keypair minted');
25
- }
26
- return st.vapid;
27
- }
28
-
29
- publicKey() { return this.vapid().publicKey; }
30
-
31
- // One subscription per client login, keyed by a hash of its bearer token —
32
- // the token itself is never written down twice.
33
- keyOf(token) { return crypto.createHash('sha256').update(String(token)).digest('hex').slice(0, 16); }
34
-
35
- get(token) { return this.state().subs[this.keyOf(token)] || null; }
36
-
37
- set(token, { endpoint, keys, alerts }) {
38
- if (!/^https:\/\//.test(String(endpoint || ''))) return null;
39
- if (!keys?.p256dh || !keys?.auth) return null;
40
- const st = this.state();
41
- st.subs[this.keyOf(token)] = { endpoint, keys: { p256dh: keys.p256dh, auth: keys.auth }, alerts: alerts || {} };
42
- this.save(st);
43
- return st.subs[this.keyOf(token)];
44
- }
45
-
46
- setAlerts(token, alerts) {
47
- const st = this.state();
48
- const sub = st.subs[this.keyOf(token)];
49
- if (!sub) return null;
50
- sub.alerts = { ...sub.alerts, ...(alerts || {}) };
51
- this.save(st);
52
- return sub;
53
- }
54
-
55
- drop(token) {
56
- const st = this.state();
57
- delete st.subs[this.keyOf(token)];
58
- this.save(st);
59
- }
60
-
61
- json(token, sub) {
62
- return {
63
- id: this.keyOf(token), endpoint: sub.endpoint,
64
- alerts: sub.alerts || {}, policy: 'all',
65
- server_key: this.publicKey(),
66
- };
67
- }
68
-
69
- // Push one notification to every subscription whose alerts allow the type.
70
- // A push service answering 404/410 means the browser dropped the
71
- // subscription — it is removed rather than retried forever.
72
- async notify(n, payload) {
73
- const st = this.state();
74
- const entries = Object.entries(st.subs);
75
- if (!entries.length) return;
76
- const { publicKey, privateKey } = this.vapid();
77
- const details = { subject: this.subject(), publicKey, privateKey };
78
- let dropped = false;
79
- for (const [key, sub] of entries) {
80
- if (sub.alerts && sub.alerts[n.type] === false) continue;
81
- try {
82
- await webpush.sendNotification(
83
- { endpoint: sub.endpoint, keys: sub.keys },
84
- JSON.stringify(payload),
85
- { vapidDetails: details, contentEncoding: 'aes128gcm', TTL: 3600 },
86
- );
87
- } catch (e) {
88
- if (e?.statusCode === 404 || e?.statusCode === 410) {
89
- delete st.subs[key];
90
- dropped = true;
91
- } else {
92
- this.log(`webpush: ${e?.statusCode || ''} ${e?.message || e}`);
93
- }
94
- }
95
- }
96
- if (dropped) this.save(st);
97
- }
98
- }
@@ -1,257 +0,0 @@
1
- // acctfeed.mjs — polls each connected fediverse account's home timeline and
2
- // notifications and merges them into the statuses index as kind 'acct'. View
3
- // cache only: none of it is written to the pod, which holds followed and own
4
- // content. Modeled on bskyfeed.mjs, which is modeled on tagfeed.mjs.
5
- //
6
- // A note is NOT re-fetched at its origin before being stored. The dereference
7
- // rule guards material an unauthenticated stranger chose for us; this is the
8
- // owner's own authenticated account answering with the result of the owner's
9
- // own follow decisions there — the same trust anchor that lets bskyfeed skip
10
- // it. A local block still applies, because that is the owner's decision.
11
- //
12
- // Config in fediacctfeed.json: { intervalMin, accounts: { <id>: marks } }.
13
-
14
- import { sanitizeHtml } from '../core/wire.mjs';
15
-
16
- const DEFAULTS = { intervalMin: 5 };
17
- const PER_SWEEP = 40;
18
- const MAX_PER_ACCT = 150;
19
- const MAX_ACCT_ENTRIES = 400;
20
- const BACKOFF_MIN_MS = 5 * 60_000;
21
- const BACKOFF_MAX_MS = 2 * 60 * 60_000;
22
- // Mastodon allows 300 requests per five minutes per token. Four go per sweep,
23
- // so the floor is only ever reached by something else using the same token.
24
- const RATE_FLOOR = 20;
25
-
26
- const MEDIA_TYPES = {
27
- image: 'image/jpeg', gifv: 'image/gif', video: 'video/mp4', audio: 'audio/mpeg',
28
- };
29
-
30
- export class AcctFeed {
31
- constructor({ store, accounts, log = console.log, onNotification = null }) {
32
- Object.assign(this, { store, accounts, log, onNotification });
33
- this.state = new Map(); // account id → { failures, quietUntil }
34
- this.lastSweep = null;
35
- this.lastAdded = 0;
36
- }
37
-
38
- config() { return { ...DEFAULTS, ...this.store.read('fediacctfeed.json', {}) }; }
39
-
40
- setConfig(patch) {
41
- const clean = {};
42
- if (patch.intervalMin) clean.intervalMin = Math.max(1, Number(patch.intervalMin) || DEFAULTS.intervalMin);
43
- this.store.write('fediacctfeed.json', { ...this.config(), ...clean });
44
- this.stop();
45
- this.start();
46
- return this.config();
47
- }
48
-
49
- marks(id) { return this.config().accounts?.[id] || {}; }
50
-
51
- setMarks(id, patch) {
52
- const cfg = this.config();
53
- this.store.write('fediacctfeed.json', {
54
- ...cfg, accounts: { ...(cfg.accounts || {}), [id]: { ...(cfg.accounts?.[id] || {}), ...patch } },
55
- });
56
- }
57
-
58
- start() {
59
- this.stopped = false;
60
- this.sweep().catch(e => this.log(`acctfeed: ${e.message}`));
61
- const tick = () => {
62
- this.timer = setTimeout(() => {
63
- this.sweep()
64
- .catch(e => this.log(`acctfeed: ${e.message}`))
65
- .finally(() => { if (!this.stopped) tick(); });
66
- }, Math.round(this.config().intervalMin * 60_000 * (0.85 + Math.random() * 0.3)));
67
- this.timer.unref?.();
68
- };
69
- tick();
70
- }
71
-
72
- stop() { this.stopped = true; clearTimeout(this.timer); }
73
-
74
- // Per account, not per feed: one server being down must not silence the rest.
75
- _stateOf(id) {
76
- if (!this.state.has(id)) this.state.set(id, { failures: 0, quietUntil: 0 });
77
- return this.state.get(id);
78
- }
79
-
80
- _backOff(id, handle, status, retryAfter) {
81
- const st = this._stateOf(id);
82
- st.failures += 1;
83
- const ladder = Math.min(BACKOFF_MIN_MS * 2 ** (st.failures - 1), BACKOFF_MAX_MS);
84
- const wait = retryAfter || Math.round(ladder * (0.85 + Math.random() * 0.3));
85
- st.quietUntil = Date.now() + wait;
86
- this.log(`acctfeed: ${handle} ${status ? `answered ${status}` : 'did not answer'} — not asking again for ${Math.round(wait / 60_000)} min`);
87
- }
88
-
89
- // One author into the shared actor cache. `uri` is the ActivityPub id, so a
90
- // person the pod already knows from its own inbox stays one actor here.
91
- _rememberAuthor(a) {
92
- const url = a?.uri || a?.url;
93
- if (!url) return null;
94
- if (!this.store.getActors()[url]) {
95
- this.store.cacheActor(url, {
96
- name: a.display_name || a.username, preferredUsername: a.username,
97
- icon: a.avatar || null, type: a.bot ? 'Service' : 'Person',
98
- });
99
- }
100
- return url;
101
- }
102
-
103
- // One Mastodon status into the statuses index. Never pre-checks for a
104
- // duplicate: a repeat sighting is what carries the second account's
105
- // provenance, and addStatus is the only thing that knows how to merge it.
106
- _mirror(st, { acct, via = null, parents = null }) {
107
- const noteId = st.uri;
108
- if (!noteId) return false;
109
- const actor = this._rememberAuthor(st.account);
110
- if (!actor || this.store.isBlocked(actor)) return false;
111
- const inReplyTo = st.in_reply_to_id ? parents?.get(String(st.in_reply_to_id)) : null;
112
- const out = this.store.addStatus({
113
- noteId, actor,
114
- content: sanitizeHtml(st.content || ''),
115
- published: st.created_at,
116
- kind: 'acct',
117
- sourceAccts: [{ acct, remoteId: String(st.id) }],
118
- ...(st.url && st.url !== noteId ? { link: st.url } : {}),
119
- ...(inReplyTo ? { inReplyTo } : {}),
120
- ...(via ? { via } : {}),
121
- ...(st.spoiler_text ? { spoiler: st.spoiler_text } : {}),
122
- ...(st.media_attachments?.length ? {
123
- attachments: st.media_attachments.map(m => ({
124
- url: m.url, mediaType: MEDIA_TYPES[m.type] || 'image/jpeg', description: m.description || '',
125
- })),
126
- } : {}),
127
- });
128
- return !!out?.added;
129
- }
130
-
131
- // Mastodon threads by its own local ids, which mean nothing anywhere else.
132
- // A parent we already hold from this same account can be resolved locally,
133
- // and one we do not hold simply has no parent here rather than a fetch.
134
- _parentMap(acct) {
135
- const map = new Map();
136
- for (const s of this.store.getStatuses()) {
137
- for (const v of s.sourceAccts || []) {
138
- if (v.acct === acct && v.remoteId) map.set(String(v.remoteId), s.noteId);
139
- }
140
- }
141
- return map;
142
- }
143
-
144
- _rateGuard(id, handle, res) {
145
- const left = Number(res.headers.get('x-ratelimit-remaining'));
146
- if (!Number.isFinite(left) || left > RATE_FLOOR) return false;
147
- const reset = Date.parse(res.headers.get('x-ratelimit-reset') || '');
148
- const wait = Number.isFinite(reset) ? Math.max(0, reset - Date.now()) : BACKOFF_MIN_MS;
149
- this._stateOf(id).quietUntil = Date.now() + wait;
150
- this.log(`acctfeed: ${handle} has ${left} requests left — waiting ${Math.round(wait / 1000)}s`);
151
- return true;
152
- }
153
-
154
- async sweep() {
155
- const rows = (this.accounts?.list() || []).filter(r => r.token && r.enabled !== false);
156
- if (!rows.length) return;
157
- this.lastSweep = new Date().toISOString();
158
- let added = 0;
159
- // The 300ms debounce cannot coalesce a sweep that awaits between items —
160
- // every fetch outlives it — so the whole sweep is one commit boundary.
161
- // Without this, statuses.json is serialized whole once per post.
162
- this.store.hold();
163
- try {
164
- for (const rec of rows) {
165
- if (this.stopped) break;
166
- added += await this._sweepOne(rec).catch((e) => {
167
- this._backOff(rec.id, rec.handle, e.status || 0, null);
168
- return 0;
169
- });
170
- }
171
- this._prune();
172
- } finally {
173
- this.store.release();
174
- }
175
- this.lastAdded = added;
176
- if (added) this.log(`acctfeed: +${added} from ${rows.length} connected account(s)`);
177
- }
178
-
179
- async _sweepOne(rec) {
180
- const st = this._stateOf(rec.id);
181
- if (st.quietUntil && Date.now() < st.quietUntil) return 0;
182
- const marks = this.marks(rec.id);
183
- const parents = this._parentMap(rec.id);
184
- let added = 0;
185
-
186
- const homeQ = marks.homeSinceId ? `&since_id=${encodeURIComponent(marks.homeSinceId)}` : '';
187
- const homeRes = await this.accounts.api(rec.id, `/api/v1/timelines/home?limit=${PER_SWEEP}${homeQ}`);
188
- if (this._rateGuard(rec.id, rec.handle, homeRes)) return 0;
189
- const home = await homeRes.json().catch(() => []);
190
- if (!Array.isArray(home)) throw new Error(`${rec.handle} answered with no timeline`);
191
- for (const item of home) {
192
- if (item?.reblog) {
193
- // A boost: the inner post is the content, the booster is the carrier —
194
- // the same envelope statusOrBoost already renders for our own timeline.
195
- const via = this._rememberAuthor(item.account);
196
- if (this._mirror(item.reblog, { acct: rec.id, via, parents })) added++;
197
- } else if (item?.uri) {
198
- if (this._mirror(item, { acct: rec.id, parents })) added++;
199
- }
200
- }
201
- // Mastodon returns newest first, so the first row is the new mark.
202
- // Ids are 19-digit snowflakes: opaque strings, never numbers.
203
- if (home.length && home[0]?.id) this.setMarks(rec.id, { homeSinceId: String(home[0].id) });
204
-
205
- const notifQ = marks.notifSinceId ? `&since_id=${encodeURIComponent(marks.notifSinceId)}` : '';
206
- const notifRes = await this.accounts.api(rec.id, `/api/v1/notifications?limit=${PER_SWEEP}${notifQ}`);
207
- if (this._rateGuard(rec.id, rec.handle, notifRes)) return added;
208
- const notes = await notifRes.json().catch(() => []);
209
- if (Array.isArray(notes)) {
210
- for (const n of notes) {
211
- if (!n?.account || String(n.account.id) === String(rec.accountId)) continue;
212
- const actor = this._rememberAuthor(n.account);
213
- if (!actor || this.store.isBlocked(actor)) continue;
214
- // The post a favourite or boost is about is one of this account's own,
215
- // so it is mirrored first — a notification pointing at nothing is a
216
- // row the client drops.
217
- if (n.status?.uri) this._mirror(n.status, { acct: rec.id, parents });
218
- if (n.type === 'favourite' || n.type === 'reblog') {
219
- this.store.addNotification({
220
- type: n.type === 'favourite' ? 'favourite' : 'reblog',
221
- actor, noteId: n.status?.uri, via: rec.id,
222
- });
223
- } else if (n.type === 'follow' || n.type === 'follow_request') {
224
- this.store.addNotification({ type: 'follow', actor, via: rec.id });
225
- await this.onNotification?.(n, { actor, acct: rec.id });
226
- } else if (n.type === 'mention') {
227
- this.store.addNotification({ type: 'mention', actor, noteId: n.status?.uri, via: rec.id });
228
- await this.onNotification?.(n, { actor, acct: rec.id });
229
- }
230
- }
231
- if (notes.length && notes[0]?.id) this.setMarks(rec.id, { notifSinceId: String(notes[0].id) });
232
- }
233
-
234
- st.failures = 0;
235
- return added;
236
- }
237
-
238
- // Two caps. The per-account one keeps a busy account from crowding out a
239
- // quiet one; the total keeps every connected account together from evicting
240
- // the pod's own posts and the timeline it already had.
241
- _prune() {
242
- const all = this.store.getStatuses();
243
- const mine = all.filter(s => s.kind === 'acct');
244
- if (!mine.length) return;
245
- const drop = new Set();
246
- const seen = new Map();
247
- for (const s of mine) {
248
- const acct = s.sourceAccts?.[0]?.acct || '?';
249
- const n = (seen.get(acct) || 0) + 1;
250
- seen.set(acct, n);
251
- if (n > MAX_PER_ACCT) drop.add(s.noteId);
252
- }
253
- for (const s of mine.slice(MAX_ACCT_ENTRIES)) drop.add(s.noteId);
254
- if (!drop.size) return;
255
- this.store.write('statuses.json', all.filter(s => !drop.has(s.noteId)));
256
- }
257
- }
@@ -1,309 +0,0 @@
1
- // atproto.mjs — the agent's connection to an ATProto (Bluesky) account. The
2
- // agent is an API CLIENT of an existing account on bsky.social or any PDS —
3
- // the same relationship a Mastodon app has to this agent. Nothing here hosts
4
- // ATProto; the pod stays the only public surface.
5
- //
6
- // Credential at AP_HOME/atproto.json (0600, atomic, stamped with the actor it
7
- // was connected for — the keys.json rules). The app password is kept so an
8
- // expired refresh token re-logins instead of demanding a reconnect.
9
-
10
- import { fileVault } from './vault.mjs';
11
- import { safeFetch, retryAfterMs } from '../shared/safefetch.mjs';
12
-
13
- const FILE = 'atproto.json';
14
- const POST_NSID = 'app.bsky.feed.post';
15
- const MAX_GRAPHEMES = 300;
16
- const MAX_IMAGES = 4;
17
- const MAX_BLOB_BYTES = 950_000;
18
-
19
- const enc = new TextEncoder();
20
- const seg = new Intl.Segmenter();
21
- const graphemes = (s) => { let n = 0; for (const _ of seg.segment(s)) n++; return n; };
22
- const byteLen = (s) => enc.encode(s).length;
23
-
24
- // Facet byte offsets are UTF-8 BYTE indexes into the text, not code units.
25
- function detectFacets(text) {
26
- const facets = [];
27
- for (const m of text.matchAll(/https?:\/\/[^\s<>"'\])]+/g)) {
28
- facets.push({
29
- index: { byteStart: byteLen(text.slice(0, m.index)), byteEnd: byteLen(text.slice(0, m.index + m[0].length)) },
30
- features: [{ $type: 'app.bsky.richtext.facet#link', uri: m[0] }],
31
- });
32
- }
33
- for (const m of text.matchAll(/(^|\s)#([\p{L}\p{N}_]+)/gu)) {
34
- const start = m.index + m[1].length;
35
- const tagText = '#' + m[2];
36
- facets.push({
37
- index: { byteStart: byteLen(text.slice(0, start)), byteEnd: byteLen(text.slice(0, start + tagText.length)) },
38
- features: [{ $type: 'app.bsky.richtext.facet#tag', tag: m[2] }],
39
- });
40
- }
41
- return facets;
42
- }
43
-
44
- // The post text Bluesky gets: the note's own text when it fits, otherwise a
45
- // grapheme-safe cut with a link to the pod note, which holds the whole thing.
46
- export function bskyText(text, noteUrl) {
47
- const clean = String(text || '').trim();
48
- if (graphemes(clean) <= MAX_GRAPHEMES) {
49
- return { text: clean, facets: detectFacets(clean), truncated: false };
50
- }
51
- const tail = '… ' + noteUrl;
52
- const budget = MAX_GRAPHEMES - graphemes(tail);
53
- let cut = '';
54
- let used = 0;
55
- for (const s of seg.segment(clean)) {
56
- if (used >= budget) break;
57
- cut += s.segment;
58
- used++;
59
- }
60
- const out = cut.trimEnd() + tail;
61
- const facets = detectFacets(out);
62
- return { text: out, facets, truncated: true };
63
- }
64
-
65
- export class Atproto {
66
- /**
67
- * `vault` is where the app password is kept: this machine when the identity
68
- * runs on a laptop, its own pod when it runs inside the pod server. Absent,
69
- * it is the file beside the credential, which is what a laptop wants.
70
- */
71
- constructor({ localDir, vault = null, actorId = null, log = console.log, fetcher = null }) {
72
- this.vault = vault || fileVault(localDir);
73
- this.name = vault ? 'bluesky' : FILE.replace(/\.json$/u, '');
74
- this.actorId = actorId;
75
- this.log = log;
76
- // Injectable for tests; the default is the politeness stack.
77
- this.fetcher = fetcher || ((url, init) => safeFetch(url, init));
78
- this.pausedUntil = 0;
79
- this.lastError = null;
80
- }
81
-
82
- // The stamped-record guard: a credential minted for another actor is treated
83
- // as absent, never silently adopted across identities.
84
- read() {
85
- const rec = this.vault.read(this.name);
86
- if (!rec) return null;
87
- if (rec?.mintedFor && this.actorId && rec.mintedFor !== this.actorId) {
88
- this.log(`the Bluesky connection belongs to ${rec.mintedFor} — not reusing it for ${this.actorId}`);
89
- return null;
90
- }
91
- return rec;
92
- }
93
-
94
- write(rec) { this.vault.write(this.name, rec); }
95
-
96
- connected() { return !!this.read()?.did; }
97
-
98
- status() {
99
- const rec = this.read();
100
- return {
101
- connected: !!rec?.did,
102
- service: rec?.service || null,
103
- handle: rec?.handle || null,
104
- did: rec?.did || null,
105
- lastError: this.lastError,
106
- cooldownFor: Math.max(0, Math.round((this.pausedUntil - Date.now()) / 1000)),
107
- };
108
- }
109
-
110
- async _fetch(url, init) {
111
- const left = this.pausedUntil - Date.now();
112
- if (left > 0) throw new Error(`bluesky asked us to back off — ${Math.ceil(left / 1000)}s left`);
113
- const res = await this.fetcher(url, init);
114
- if (res.status === 429 || res.status === 503) {
115
- const ms = retryAfterMs(res) ?? 60_000;
116
- this.pausedUntil = Date.now() + ms;
117
- this.log(`bluesky answered ${res.status} — pausing for ${Math.round(ms / 1000)}s`);
118
- }
119
- return res;
120
- }
121
-
122
- async _json(res) {
123
- const body = await res.json().catch(() => ({}));
124
- if (res.status >= 400) {
125
- const err = new Error(body.message || body.error || `bluesky answered ${res.status}`);
126
- err.status = res.status;
127
- err.code = body.error || null;
128
- throw err;
129
- }
130
- return body;
131
- }
132
-
133
- // One session mint, straight from service + identifier + app password.
134
- // The identifier reaches the server bare: a fediverse-habit leading @ makes
135
- // Bluesky parse the handle as an email and refuse it confusingly.
136
- async connect({ service, identifier, appPassword }) {
137
- const base = String(service || 'https://bsky.social').replace(/\/+$/, '');
138
- const id = String(identifier || '').trim().replace(/^@/, '');
139
- const res = await this._fetch(`${base}/xrpc/com.atproto.server.createSession`, {
140
- method: 'POST',
141
- headers: { 'content-type': 'application/json' },
142
- body: JSON.stringify({ identifier: id, password: appPassword }),
143
- });
144
- const s = await this._json(res);
145
- this.write({
146
- service: base, identifier: id, appPassword,
147
- did: s.did, handle: s.handle,
148
- accessJwt: s.accessJwt, refreshJwt: s.refreshJwt,
149
- ...(this.actorId ? { mintedFor: this.actorId } : {}),
150
- });
151
- this.lastError = null;
152
- this.log(`bluesky connected: @${s.handle} (${s.did})`);
153
- return { did: s.did, handle: s.handle, service: base };
154
- }
155
-
156
- // Best-effort session teardown, then the local record is gone regardless.
157
- async disconnect() {
158
- const rec = this.read();
159
- if (rec?.refreshJwt) {
160
- await this._fetch(`${rec.service}/xrpc/com.atproto.server.deleteSession`, {
161
- method: 'POST', headers: { authorization: `Bearer ${rec.refreshJwt}` },
162
- }).catch(() => {});
163
- }
164
- await this.vault.remove(this.name);
165
- this.log('bluesky disconnected');
166
- }
167
-
168
- async _refresh(rec) {
169
- const res = await this._fetch(`${rec.service}/xrpc/com.atproto.server.refreshSession`, {
170
- method: 'POST', headers: { authorization: `Bearer ${rec.refreshJwt}` },
171
- });
172
- if (res.status >= 400) {
173
- // The refresh token died; the stored app password re-mints the session.
174
- await this._json(res).catch(() => {});
175
- return this.connect({ service: rec.service, identifier: rec.identifier, appPassword: rec.appPassword })
176
- .then(() => this.read());
177
- }
178
- const s = await this._json(res);
179
- const next = { ...rec, accessJwt: s.accessJwt, refreshJwt: s.refreshJwt };
180
- this.write(next);
181
- return next;
182
- }
183
-
184
- // Mirror one public note. Returns { uri, cid } — the caller records it on
185
- // the status so a later delete can find the mirror.
186
- async crossPost({ text, published, attachments = [] }, { noteUrl }) {
187
- const rec = this.read();
188
- if (!rec?.did) throw new Error('no bluesky account connected');
189
- const body = bskyText(text, noteUrl);
190
- const images = [];
191
- for (const a of attachments.filter(x => /^image\//.test(x.mediaType || '')).slice(0, MAX_IMAGES)) {
192
- const res = await this._fetch(a.url, {});
193
- if (res.status >= 400) throw new Error(`attachment fetch failed (${res.status}): ${a.url}`);
194
- const bytes = Buffer.from(await res.arrayBuffer());
195
- if (bytes.length > MAX_BLOB_BYTES) { this.log(`bluesky: image too large, skipped (${a.url})`); continue; }
196
- const up = await this.xrpc('com.atproto.repo.uploadBlob', {
197
- body: bytes, contentType: a.mediaType,
198
- });
199
- images.push({ image: up.blob, alt: a.description || '' });
200
- }
201
- const record = {
202
- $type: POST_NSID,
203
- text: body.text,
204
- ...(body.facets.length ? { facets: body.facets } : {}),
205
- ...(images.length ? { embed: { $type: 'app.bsky.embed.images', images } } : {}),
206
- createdAt: published || new Date().toISOString(),
207
- };
208
- const out = await this.xrpc('com.atproto.repo.createRecord', {
209
- body: { repo: rec.did, collection: POST_NSID, record },
210
- });
211
- return { uri: out.uri, cid: out.cid, ...(body.truncated ? { truncated: true } : {}) };
212
- }
213
-
214
- // at://did/collection/rkey → deleteRecord. Absence on the far side is fine.
215
- async deleteCrossPost(uri) {
216
- const m = String(uri).match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
217
- if (!m) throw new Error(`not an at:// uri: ${uri}`);
218
- await this.xrpc('com.atproto.repo.deleteRecord', {
219
- body: { repo: m[1], collection: m[2], rkey: m[3] },
220
- });
221
- }
222
-
223
- // The post as Bluesky holds it — for a cid an old mirror never stored, and
224
- // for the thread root a reply must carry.
225
- async _post(uri) {
226
- const out = await this.xrpc('app.bsky.feed.getPosts', { params: { uris: uri } });
227
- const p = (out.posts || [])[0];
228
- if (!p) throw new Error('that post is gone on Bluesky');
229
- return p;
230
- }
231
-
232
- // A native like or repost of someone's post — the record every Bluesky app
233
- // writes. Returns the minted record's uri, the only handle an undo has.
234
- async like(uri, cid = null) { return this._subjectRecord('app.bsky.feed.like', uri, cid); }
235
- async repost(uri, cid = null) { return this._subjectRecord('app.bsky.feed.repost', uri, cid); }
236
-
237
- async _subjectRecord(collection, uri, cid) {
238
- const rec = this.read();
239
- if (!rec?.did) throw new Error('no bluesky account connected');
240
- const subject = cid ? { uri, cid } : await this._post(uri).then(p => ({ uri: p.uri, cid: p.cid }));
241
- const out = await this.xrpc('com.atproto.repo.createRecord', {
242
- body: {
243
- repo: rec.did, collection,
244
- record: { $type: collection, subject, createdAt: new Date().toISOString() },
245
- },
246
- });
247
- return { uri: out.uri };
248
- }
249
-
250
- // A native reply under someone's post, threaded at the root their post
251
- // hangs from. A reply exists only on Bluesky — the fediverse cannot see the
252
- // parent — so there is no AP object and nothing to cross-post.
253
- async reply(text, parentUri) {
254
- const rec = this.read();
255
- if (!rec?.did) throw new Error('no bluesky account connected');
256
- const clean = String(text || '').trim();
257
- const n = graphemes(clean);
258
- if (n > MAX_GRAPHEMES) throw new Error(`a Bluesky reply fits ${MAX_GRAPHEMES} characters — this one is ${n}`);
259
- const p = await this._post(parentUri);
260
- const parent = { uri: p.uri, cid: p.cid };
261
- const facets = detectFacets(clean);
262
- const out = await this.xrpc('com.atproto.repo.createRecord', {
263
- body: {
264
- repo: rec.did, collection: POST_NSID,
265
- record: {
266
- $type: POST_NSID, text: clean,
267
- ...(facets.length ? { facets } : {}),
268
- reply: { root: p.record?.reply?.root || parent, parent },
269
- createdAt: new Date().toISOString(),
270
- },
271
- },
272
- });
273
- return { uri: out.uri, cid: out.cid };
274
- }
275
-
276
- // The one door every call goes through. GET when there is no body.
277
- // Retries exactly once after an expired-token refresh.
278
- async xrpc(nsid, { params = null, body = null, contentType = 'application/json', method = null } = {}) {
279
- let rec = this.read();
280
- if (!rec?.did) throw new Error('no bluesky account connected');
281
- const qs = params
282
- ? '?' + new URLSearchParams(Object.entries(params).filter(([, v]) => v != null)).toString()
283
- : '';
284
- const doCall = (r) => this._fetch(`${r.service}/xrpc/${nsid}${qs}`, {
285
- method: method || (body != null ? 'POST' : 'GET'),
286
- headers: {
287
- authorization: `Bearer ${r.accessJwt}`,
288
- ...(body != null ? { 'content-type': contentType } : {}),
289
- },
290
- ...(body != null ? { body: contentType === 'application/json' ? JSON.stringify(body) : body } : {}),
291
- });
292
- let res = await doCall(rec);
293
- if (res.status === 400 || res.status === 401) {
294
- const peek = await res.clone().json().catch(() => ({}));
295
- if (peek.error === 'ExpiredToken' || peek.error === 'InvalidToken') {
296
- rec = await this._refresh(rec);
297
- res = await doCall(rec);
298
- }
299
- }
300
- try {
301
- const out = await this._json(res);
302
- this.lastError = null;
303
- return out;
304
- } catch (e) {
305
- this.lastError = e.message;
306
- throw e;
307
- }
308
- }
309
- }