fedipod-server 0.28.1 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (438) hide show
  1. package/README.md +38 -34
  2. package/config/server.json +22 -0
  3. package/dist/claims.js +3 -2
  4. package/dist/directory.d.ts +2 -0
  5. package/dist/door.d.ts +4 -0
  6. package/dist/door.js +31 -0
  7. package/dist/fedipod.d.ts +16 -0
  8. package/dist/fedipod.js +13 -0
  9. package/dist/front.d.ts +3 -0
  10. package/dist/front.js +94 -0
  11. package/dist/handler.d.ts +39 -101
  12. package/dist/handler.js +86 -617
  13. package/dist/handler.jsonld +6 -30
  14. package/dist/identities.d.ts +62 -0
  15. package/dist/identities.js +236 -0
  16. package/dist/mounts.d.ts +43 -0
  17. package/dist/mounts.js +123 -0
  18. package/dist/opt-in.d.ts +46 -0
  19. package/dist/opt-in.js +176 -0
  20. package/dist/store-pod.js +37 -0
  21. package/package.json +10 -18
  22. package/lib/client/c2s.mjs +0 -582
  23. package/lib/client/localapi.mjs +0 -87
  24. package/lib/client/masto/accounts.mjs +0 -309
  25. package/lib/client/masto/body.mjs +0 -156
  26. package/lib/client/masto/index.mjs +0 -206
  27. package/lib/client/masto/instance.mjs +0 -112
  28. package/lib/client/masto/media.mjs +0 -79
  29. package/lib/client/masto/oauth.mjs +0 -656
  30. package/lib/client/masto/render.mjs +0 -518
  31. package/lib/client/masto/statuses.mjs +0 -372
  32. package/lib/client/masto/timelines.mjs +0 -438
  33. package/lib/client/oidc-auth.mjs +0 -62
  34. package/lib/client/streaming.mjs +0 -154
  35. package/lib/client/webpush.mjs +0 -98
  36. package/lib/connections/acctfeed.mjs +0 -257
  37. package/lib/connections/atproto.mjs +0 -309
  38. package/lib/connections/bskyfeed.mjs +0 -243
  39. package/lib/connections/bskygroup.mjs +0 -162
  40. package/lib/connections/fediacct.mjs +0 -253
  41. package/lib/connections/import.mjs +0 -357
  42. package/lib/connections/tagfeed.mjs +0 -212
  43. package/lib/connections/vault.mjs +0 -114
  44. package/lib/core/as2.mjs +0 -206
  45. package/lib/core/contexts/activitystreams.json +0 -379
  46. package/lib/core/contexts/anno.json +0 -126
  47. package/lib/core/contexts/did-v1.json +0 -57
  48. package/lib/core/contexts/fep-5711.json +0 -36
  49. package/lib/core/contexts/gotosocial.json +0 -86
  50. package/lib/core/contexts/identity-v1.json +0 -152
  51. package/lib/core/contexts/index.mjs +0 -63
  52. package/lib/core/contexts/join-lemmy.json +0 -33
  53. package/lib/core/contexts/joinmastodon.json +0 -28
  54. package/lib/core/contexts/litepub-0.1.json +0 -60
  55. package/lib/core/contexts/map.json +0 -19
  56. package/lib/core/contexts/miscellany.json +0 -19
  57. package/lib/core/contexts/quotes.json +0 -18
  58. package/lib/core/contexts/schemaorg.json +0 -8845
  59. package/lib/core/contexts/security-data-integrity-v1.json +0 -78
  60. package/lib/core/contexts/security-data-integrity-v2.json +0 -81
  61. package/lib/core/contexts/security-multikey-v1.json +0 -35
  62. package/lib/core/contexts/security-v1.json +0 -74
  63. package/lib/core/contexts/webfinger.json +0 -10
  64. package/lib/core/deliver.mjs +0 -371
  65. package/lib/core/graphview.mjs +0 -327
  66. package/lib/core/intake/activities.mjs +0 -633
  67. package/lib/core/intake/activity.mjs +0 -318
  68. package/lib/core/intake/channel.mjs +0 -144
  69. package/lib/core/intake/group.mjs +0 -256
  70. package/lib/core/intake/index.mjs +0 -701
  71. package/lib/core/intake/notes.mjs +0 -360
  72. package/lib/core/intake/verify.mjs +0 -142
  73. package/lib/core/keys.mjs +0 -153
  74. package/lib/core/lease.mjs +0 -206
  75. package/lib/core/polls.mjs +0 -105
  76. package/lib/core/profile-page.mjs +0 -104
  77. package/lib/core/proof.mjs +0 -79
  78. package/lib/core/publisher/collections.mjs +0 -237
  79. package/lib/core/publisher/index.mjs +0 -500
  80. package/lib/core/publisher/notes.mjs +0 -453
  81. package/lib/core/publisher/questions.mjs +0 -239
  82. package/lib/core/publisher/restore.mjs +0 -224
  83. package/lib/core/shapes/activitystreams.ttl +0 -157
  84. package/lib/core/shapes/index.mjs +0 -132
  85. package/lib/core/shapes/shapes-text.mjs +0 -13
  86. package/lib/core/social.mjs +0 -531
  87. package/lib/core/storage.mjs +0 -176
  88. package/lib/core/store.mjs +0 -568
  89. package/lib/core/wire-quotes.mjs +0 -71
  90. package/lib/core/wire.mjs +0 -889
  91. package/lib/device/account.mjs +0 -87
  92. package/lib/device/admin/index.mjs +0 -13
  93. package/lib/device/admin/origins.mjs +0 -35
  94. package/lib/device/admin/routes/connections.mjs +0 -144
  95. package/lib/device/admin/routes/gateway.mjs +0 -199
  96. package/lib/device/admin/routes/lifecycle.mjs +0 -200
  97. package/lib/device/admin/routes/owner.mjs +0 -322
  98. package/lib/device/admin/routes/setup.mjs +0 -412
  99. package/lib/device/admin/routes/social.mjs +0 -188
  100. package/lib/device/admin/server.mjs +0 -95
  101. package/lib/device/admin/static.mjs +0 -247
  102. package/lib/device/admin/surface.mjs +0 -315
  103. package/lib/device/certs.mjs +0 -281
  104. package/lib/device/cli/commands/account.mjs +0 -586
  105. package/lib/device/cli/commands/run.mjs +0 -278
  106. package/lib/device/cli/commands/service.mjs +0 -221
  107. package/lib/device/cli/commands/setup.mjs +0 -447
  108. package/lib/device/cli/commands/state.mjs +0 -561
  109. package/lib/device/cli/context.mjs +0 -288
  110. package/lib/device/export-collections.mjs +0 -161
  111. package/lib/device/gateway-move.mjs +0 -58
  112. package/lib/device/home.mjs +0 -161
  113. package/lib/device/migrate.mjs +0 -115
  114. package/lib/device/ports.mjs +0 -27
  115. package/lib/device/remote.mjs +0 -54
  116. package/lib/device/setup.mjs +0 -459
  117. package/lib/device/update.mjs +0 -96
  118. package/lib/gateway/caches.mjs +0 -36
  119. package/lib/gateway/directory.mjs +0 -90
  120. package/lib/gateway/front-core.mjs +0 -970
  121. package/lib/gateway/gateway-core.mjs +0 -165
  122. package/lib/gateway/headers.mjs +0 -49
  123. package/lib/gateway/httpsig.mjs +0 -159
  124. package/lib/gateway/notices.mjs +0 -84
  125. package/lib/gateway/quiet.mjs +0 -189
  126. package/lib/pod/README.md +0 -61
  127. package/lib/pod/actor.mjs +0 -96
  128. package/lib/pod/collection.mjs +0 -77
  129. package/lib/pod/containers.mjs +0 -140
  130. package/lib/pod/discovery.mjs +0 -46
  131. package/lib/pod/featured.mjs +0 -18
  132. package/lib/pod/followers.mjs +0 -24
  133. package/lib/pod/following.mjs +0 -10
  134. package/lib/pod/http.mjs +0 -74
  135. package/lib/pod/inbox.mjs +0 -166
  136. package/lib/pod/links.mjs +0 -35
  137. package/lib/pod/media.mjs +0 -27
  138. package/lib/pod/notes.mjs +0 -80
  139. package/lib/pod/notifications.mjs +0 -89
  140. package/lib/pod/outbox.mjs +0 -18
  141. package/lib/pod/package.json +0 -19
  142. package/lib/pod/policy.mjs +0 -58
  143. package/lib/pod/private.mjs +0 -19
  144. package/lib/pod/root.mjs +0 -104
  145. package/lib/pod/state.mjs +0 -40
  146. package/lib/pod/transport.mjs +0 -525
  147. package/lib/pod/urls.mjs +0 -85
  148. package/lib/server/embed.mjs +0 -423
  149. package/lib/session/README.md +0 -137
  150. package/lib/session/demo.html +0 -94
  151. package/lib/session/fedi-account.mjs +0 -408
  152. package/lib/session/fedi-login.mjs +0 -123
  153. package/lib/session/oidc-session.mjs +0 -233
  154. package/lib/session/package.json +0 -49
  155. package/lib/shared/guard.mjs +0 -213
  156. package/lib/shared/links.mjs +0 -9
  157. package/lib/shared/safefetch.mjs +0 -235
  158. package/lib/shared/ua.mjs +0 -29
  159. package/phanpy/dist/404.html +0 -40
  160. package/phanpy/dist/_headers +0 -2
  161. package/phanpy/dist/apple-touch-icon.png +0 -0
  162. package/phanpy/dist/assets/ICONS-DIPJeU0C.js +0 -2
  163. package/phanpy/dist/assets/ICONS-DIPJeU0C.js.map +0 -1
  164. package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js +0 -183
  165. package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
  166. package/phanpy/dist/assets/Temml-CCjoacWt.woff2 +0 -0
  167. package/phanpy/dist/assets/__vite-browser-external-C7iut881.js +0 -2
  168. package/phanpy/dist/assets/__vite-browser-external-C7iut881.js.map +0 -1
  169. package/phanpy/dist/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  170. package/phanpy/dist/assets/catch-up-O5q5C75Z.png +0 -0
  171. package/phanpy/dist/assets/chunk-aKtaBQYM.js +0 -1
  172. package/phanpy/dist/assets/compose-CFEx7wb4.js +0 -2
  173. package/phanpy/dist/assets/compose-CFEx7wb4.js.map +0 -1
  174. package/phanpy/dist/assets/compose-ZJN5xSb4.js +0 -35
  175. package/phanpy/dist/assets/compose-ZJN5xSb4.js.map +0 -1
  176. package/phanpy/dist/assets/debug.module-YFQgwnIr.js +0 -52
  177. package/phanpy/dist/assets/debug.module-YFQgwnIr.js.map +0 -1
  178. package/phanpy/dist/assets/dom-p1YcJ5Rw.js +0 -2
  179. package/phanpy/dist/assets/dom-p1YcJ5Rw.js.map +0 -1
  180. package/phanpy/dist/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  181. package/phanpy/dist/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  182. package/phanpy/dist/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  183. package/phanpy/dist/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  184. package/phanpy/dist/assets/icons/add-circle-line-WD0mwORD.js +0 -1
  185. package/phanpy/dist/assets/icons/alert-line-CpFUMf9s.js +0 -1
  186. package/phanpy/dist/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
  187. package/phanpy/dist/assets/icons/announcement-line-5nwaJFJm.js +0 -1
  188. package/phanpy/dist/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
  189. package/phanpy/dist/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
  190. package/phanpy/dist/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
  191. package/phanpy/dist/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
  192. package/phanpy/dist/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
  193. package/phanpy/dist/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
  194. package/phanpy/dist/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
  195. package/phanpy/dist/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
  196. package/phanpy/dist/assets/icons/at-line-CF66Bznk.js +0 -1
  197. package/phanpy/dist/assets/icons/attachment-line-BY8It-l9.js +0 -1
  198. package/phanpy/dist/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
  199. package/phanpy/dist/assets/icons/board-line-C0UWXyA6.js +0 -1
  200. package/phanpy/dist/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
  201. package/phanpy/dist/assets/icons/building-5-line-Dd59MzbO.js +0 -1
  202. package/phanpy/dist/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
  203. package/phanpy/dist/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
  204. package/phanpy/dist/assets/icons/calendar-month-line-vommts2C.js +0 -1
  205. package/phanpy/dist/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
  206. package/phanpy/dist/assets/icons/camera-line-Cv69MEei.js +0 -1
  207. package/phanpy/dist/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
  208. package/phanpy/dist/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
  209. package/phanpy/dist/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
  210. package/phanpy/dist/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
  211. package/phanpy/dist/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
  212. package/phanpy/dist/assets/icons/clipboard-line-BBp364gJ.js +0 -1
  213. package/phanpy/dist/assets/icons/close-circle-line-CICdiuGz.js +0 -1
  214. package/phanpy/dist/assets/icons/close-line-DYrFWcjv.js +0 -1
  215. package/phanpy/dist/assets/icons/cloud-line-tiQAbah8.js +0 -1
  216. package/phanpy/dist/assets/icons/code-line-fAl8iiV3.js +0 -1
  217. package/phanpy/dist/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
  218. package/phanpy/dist/assets/icons/copy-2-line-CLW69yF5.js +0 -1
  219. package/phanpy/dist/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
  220. package/phanpy/dist/assets/icons/document-3-line-DBGNhAg2.js +0 -1
  221. package/phanpy/dist/assets/icons/document-line-C7mb9sb7.js +0 -1
  222. package/phanpy/dist/assets/icons/down-line-BGqSSThd.js +0 -1
  223. package/phanpy/dist/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
  224. package/phanpy/dist/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
  225. package/phanpy/dist/assets/icons/exit-line-Dc07BS-x.js +0 -1
  226. package/phanpy/dist/assets/icons/external-link-line-BvpSkmjO.js +0 -1
  227. package/phanpy/dist/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
  228. package/phanpy/dist/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
  229. package/phanpy/dist/assets/icons/filter-2-line-B_mR2usy.js +0 -1
  230. package/phanpy/dist/assets/icons/filter-line-C_nm-3cU.js +0 -1
  231. package/phanpy/dist/assets/icons/flag-1-line-C2HB05I0.js +0 -1
  232. package/phanpy/dist/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
  233. package/phanpy/dist/assets/icons/formula-line-DLrt2d6j.js +0 -1
  234. package/phanpy/dist/assets/icons/grid-line-CgUmEikf.js +0 -1
  235. package/phanpy/dist/assets/icons/group-line-C5fnX0wU.js +0 -1
  236. package/phanpy/dist/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
  237. package/phanpy/dist/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
  238. package/phanpy/dist/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
  239. package/phanpy/dist/assets/icons/heart-line-CJ7K47J4.js +0 -1
  240. package/phanpy/dist/assets/icons/history-2-line-kWWvFzqv.js +0 -1
  241. package/phanpy/dist/assets/icons/history-line-bGQiwbTH.js +0 -1
  242. package/phanpy/dist/assets/icons/home-3-line-fRU1zeLG.js +0 -1
  243. package/phanpy/dist/assets/icons/information-line-iNk6-sOY.js +0 -1
  244. package/phanpy/dist/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
  245. package/phanpy/dist/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
  246. package/phanpy/dist/assets/icons/layout-5-line-B_znt5wO.js +0 -1
  247. package/phanpy/dist/assets/icons/left-line-eUw0tya4.js +0 -1
  248. package/phanpy/dist/assets/icons/lightning-line-DaKoDdix.js +0 -1
  249. package/phanpy/dist/assets/icons/link-2-line-DCqLmMA7.js +0 -1
  250. package/phanpy/dist/assets/icons/list-check-line-BAxosBh9.js +0 -1
  251. package/phanpy/dist/assets/icons/lock-line-CPoaXeUW.js +0 -1
  252. package/phanpy/dist/assets/icons/mail-line-B0P8aa1L.js +0 -1
  253. package/phanpy/dist/assets/icons/moon-line-CEqNAToP.js +0 -1
  254. package/phanpy/dist/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
  255. package/phanpy/dist/assets/icons/more-3-line-BIiVjYTz.js +0 -1
  256. package/phanpy/dist/assets/icons/notification-line-BZZq2Gtu.js +0 -1
  257. package/phanpy/dist/assets/icons/pencil-line-CKh8-A1v.js +0 -1
  258. package/phanpy/dist/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
  259. package/phanpy/dist/assets/icons/pin-line-DpTolyhs.js +0 -1
  260. package/phanpy/dist/assets/icons/play-fill-B0-G_8lX.js +0 -1
  261. package/phanpy/dist/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
  262. package/phanpy/dist/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
  263. package/phanpy/dist/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
  264. package/phanpy/dist/assets/icons/radar-line-CKyroTth.js +0 -1
  265. package/phanpy/dist/assets/icons/react-line-CoIKhhtK.js +0 -1
  266. package/phanpy/dist/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
  267. package/phanpy/dist/assets/icons/right-line-B3KWJaqc.js +0 -1
  268. package/phanpy/dist/assets/icons/rocket-line-4KZl537D.js +0 -1
  269. package/phanpy/dist/assets/icons/round-fill-C69NTrZ0.js +0 -1
  270. package/phanpy/dist/assets/icons/round-line-B9k_1uo-.js +0 -1
  271. package/phanpy/dist/assets/icons/route-line-B28ReHcE.js +0 -1
  272. package/phanpy/dist/assets/icons/rows-4-line-CrBniagM.js +0 -1
  273. package/phanpy/dist/assets/icons/scan-line-DSf74YdP.js +0 -1
  274. package/phanpy/dist/assets/icons/search-2-line-B6geIU1z.js +0 -1
  275. package/phanpy/dist/assets/icons/settings-3-line-azJwK47w.js +0 -1
  276. package/phanpy/dist/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
  277. package/phanpy/dist/assets/icons/share-2-line-D5MxWjrm.js +0 -1
  278. package/phanpy/dist/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
  279. package/phanpy/dist/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
  280. package/phanpy/dist/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
  281. package/phanpy/dist/assets/icons/time-line-BF3eet1v.js +0 -1
  282. package/phanpy/dist/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
  283. package/phanpy/dist/assets/icons/translate-line-BFWol2Ce.js +0 -1
  284. package/phanpy/dist/assets/icons/unlock-line-X_x5vwv3.js +0 -1
  285. package/phanpy/dist/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
  286. package/phanpy/dist/assets/icons/user-4-line-BVSynZCp.js +0 -1
  287. package/phanpy/dist/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
  288. package/phanpy/dist/assets/icons/user-add-line-BbXdHG4z.js +0 -1
  289. package/phanpy/dist/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
  290. package/phanpy/dist/assets/icons/user-follow-line-BXux0wSd.js +0 -1
  291. package/phanpy/dist/assets/icons/user-star-line-DdYlpHLU.js +0 -1
  292. package/phanpy/dist/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
  293. package/phanpy/dist/assets/icons/user-x-line-DIDgNlPH.js +0 -1
  294. package/phanpy/dist/assets/icons/volume-line-Bs390KQt.js +0 -1
  295. package/phanpy/dist/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
  296. package/phanpy/dist/assets/icons/walk-line-yNXfB-md.js +0 -1
  297. package/phanpy/dist/assets/icons/world-2-line-CenkGIwj.js +0 -1
  298. package/phanpy/dist/assets/icons/zoom-in-line-CaENUphf.js +0 -1
  299. package/phanpy/dist/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
  300. package/phanpy/dist/assets/instances-BNPptnn-.json +0 -333
  301. package/phanpy/dist/assets/locales/ar-SA-CfheRbCu.js +0 -1
  302. package/phanpy/dist/assets/locales/ca-ES-DHRW86Tz.js +0 -1
  303. package/phanpy/dist/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
  304. package/phanpy/dist/assets/locales/de-DE-ByRPMRFy.js +0 -1
  305. package/phanpy/dist/assets/locales/eo-UY-CPC7wLCS.js +0 -1
  306. package/phanpy/dist/assets/locales/es-ES-dJa0Fd9t.js +0 -1
  307. package/phanpy/dist/assets/locales/eu-ES-C3djlPR2.js +0 -1
  308. package/phanpy/dist/assets/locales/fa-IR-DTT490Ck.js +0 -1
  309. package/phanpy/dist/assets/locales/fi-FI-B23MCPuz.js +0 -1
  310. package/phanpy/dist/assets/locales/fr-FR-BcaQsO5C.js +0 -1
  311. package/phanpy/dist/assets/locales/gl-ES-BR-CVKBN.js +0 -1
  312. package/phanpy/dist/assets/locales/he-IL-CfheRbCu.js +0 -1
  313. package/phanpy/dist/assets/locales/hu-HU-CfheRbCu.js +0 -1
  314. package/phanpy/dist/assets/locales/it-IT-C1136UFV.js +0 -1
  315. package/phanpy/dist/assets/locales/ja-JP-CRZmRAhD.js +0 -1
  316. package/phanpy/dist/assets/locales/kab-Cz8xH_TT.js +0 -1
  317. package/phanpy/dist/assets/locales/ko-KR-CG9UiY4o.js +0 -1
  318. package/phanpy/dist/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
  319. package/phanpy/dist/assets/locales/nb-NO-CfheRbCu.js +0 -1
  320. package/phanpy/dist/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
  321. package/phanpy/dist/assets/locales/oc-FR-CfheRbCu.js +0 -1
  322. package/phanpy/dist/assets/locales/pl-PL-DkwGXf6P.js +0 -1
  323. package/phanpy/dist/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
  324. package/phanpy/dist/assets/locales/pt-BR-nGL0HdFS.js +0 -1
  325. package/phanpy/dist/assets/locales/pt-PT-Cp17aj2z.js +0 -1
  326. package/phanpy/dist/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
  327. package/phanpy/dist/assets/locales/th-TH-CfheRbCu.js +0 -1
  328. package/phanpy/dist/assets/locales/tok-CfheRbCu.js +0 -1
  329. package/phanpy/dist/assets/locales/tr-TR-CfheRbCu.js +0 -1
  330. package/phanpy/dist/assets/locales/uk-UA-C5E3dFBf.js +0 -1
  331. package/phanpy/dist/assets/locales/zh-CN-BHJi7_et.js +0 -1
  332. package/phanpy/dist/assets/locales/zh-TW-CfheRbCu.js +0 -1
  333. package/phanpy/dist/assets/main-BdqNbG-a.js +0 -26
  334. package/phanpy/dist/assets/main-BdqNbG-a.js.map +0 -1
  335. package/phanpy/dist/assets/mock-home-DGhR__He.js +0 -2
  336. package/phanpy/dist/assets/mock-home-DGhR__He.js.map +0 -1
  337. package/phanpy/dist/assets/multi-column-ROck0NVt.jpg +0 -0
  338. package/phanpy/dist/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  339. package/phanpy/dist/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  340. package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js +0 -2
  341. package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js.map +0 -1
  342. package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js +0 -20
  343. package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js.map +0 -1
  344. package/phanpy/dist/assets/style-B1iLPTPR.css +0 -2
  345. package/phanpy/dist/assets/temml-DuyIQMei.js +0 -97
  346. package/phanpy/dist/assets/temml-DuyIQMei.js.map +0 -1
  347. package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js +0 -2
  348. package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
  349. package/phanpy/dist/assets/year-in-posts-B2lywqdx.png +0 -0
  350. package/phanpy/dist/assets/year-in-posts-DJv_star.js +0 -3
  351. package/phanpy/dist/assets/year-in-posts-DJv_star.js.map +0 -1
  352. package/phanpy/dist/compose/index.html +0 -20
  353. package/phanpy/dist/favicon.ico +0 -0
  354. package/phanpy/dist/index.html +0 -73
  355. package/phanpy/dist/logo-192.png +0 -0
  356. package/phanpy/dist/logo-512.png +0 -0
  357. package/phanpy/dist/logo-badge-72.png +0 -0
  358. package/phanpy/dist/logo-maskable-512.png +0 -0
  359. package/phanpy/dist/logo-monochrome-512.png +0 -0
  360. package/phanpy/dist/logo-monochrome-maskable-512.png +0 -0
  361. package/phanpy/dist/manifest.webmanifest +0 -1
  362. package/phanpy/dist/og-image-2.jpg +0 -0
  363. package/phanpy/dist/og-image.png +0 -0
  364. package/phanpy/dist/robots.txt +0 -2
  365. package/phanpy/dist/sw.js +0 -2
  366. package/phanpy/dist/sw.js.map +0 -1
  367. package/phanpy/dist/version.json +0 -1
  368. package/run-agent.mjs +0 -897
  369. package/vendor/gate.cjs +0 -138
  370. package/vendor/idp-grant.cjs +0 -394
  371. package/vendor/jfetch.cjs +0 -16
  372. package/web/admin/actors.js +0 -145
  373. package/web/admin/bar.css +0 -126
  374. package/web/admin/bar.js +0 -99
  375. package/web/admin/client/client.js +0 -66
  376. package/web/admin/client/index.html +0 -69
  377. package/web/admin/client-pick.js +0 -79
  378. package/web/admin/common.js +0 -23
  379. package/web/admin/connections.js +0 -112
  380. package/web/admin/gateway.js +0 -133
  381. package/web/admin/group.js +0 -258
  382. package/web/admin/index.html +0 -603
  383. package/web/admin/notices-bar.js +0 -90
  384. package/web/admin/oauth-signin.mjs +0 -37
  385. package/web/admin/record.js +0 -390
  386. package/web/admin/setup/index.html +0 -241
  387. package/web/admin/setup/setup.js +0 -326
  388. package/web/admin/tokens.css +0 -68
  389. package/web/admin/upkeep.js +0 -177
  390. package/web/admin/window.css +0 -55
  391. package/web/admin/window.js +0 -136
  392. package/web/app/README.md +0 -81
  393. package/web/app/admin-facade.mjs +0 -588
  394. package/web/app/agent.mjs +0 -663
  395. package/web/app/atproto-browser.mjs +0 -86
  396. package/web/app/authorize_interaction.html +0 -56
  397. package/web/app/authorize_interaction.js +0 -69
  398. package/web/app/boot.mjs +0 -576
  399. package/web/app/deliver-relay.mjs +0 -129
  400. package/web/app/dist/boot.js +0 -34218
  401. package/web/app/dist/boot.js.map +0 -7
  402. package/web/app/dist/sw.js +0 -72810
  403. package/web/app/dist/sw.js.map +0 -7
  404. package/web/app/fediacct-browser.mjs +0 -146
  405. package/web/app/gateway-move.mjs +0 -60
  406. package/web/app/idb-kv.mjs +0 -35
  407. package/web/app/index.html +0 -242
  408. package/web/app/keys-browser.mjs +0 -71
  409. package/web/app/keystore.mjs +0 -82
  410. package/web/app/oidc-session.mjs +0 -6
  411. package/web/app/pod-auth.mjs +0 -248
  412. package/web/app/pod-remote.mjs +0 -84
  413. package/web/app/shims/_globals.mjs +0 -2
  414. package/web/app/shims/fedify-sig.mjs +0 -77
  415. package/web/app/shims/node-crypto.mjs +0 -113
  416. package/web/app/shims/node-fs.mjs +0 -17
  417. package/web/app/shims/node-path.mjs +0 -19
  418. package/web/app/shims/node-url.mjs +0 -7
  419. package/web/app/shims/prelude.js +0 -58
  420. package/web/app/shims/safefetch.mjs +0 -58
  421. package/web/app/shims/shapes-text.mjs +0 -8
  422. package/web/app/shims/web-push.mjs +0 -14
  423. package/web/app/signup.mjs +0 -318
  424. package/web/app/sw-src.mjs +0 -220
  425. package/web/app/update.js +0 -70
  426. package/web/front/admin.html +0 -55
  427. package/web/front/admin.js +0 -89
  428. package/web/front/install.sh +0 -69
  429. package/web/front/new-account.html +0 -35
  430. package/web/front/new-account.js +0 -13
  431. package/web/front/notices.html +0 -69
  432. package/web/front/notices.js +0 -107
  433. package/web/front/run.html +0 -58
  434. package/web/front/run.js +0 -195
  435. package/web/front/solid-oidc-client.js +0 -6
  436. package/web/icons/forum.svg +0 -11
  437. package/web/icons/gateway.svg +0 -14
  438. package/web/icons/local.svg +0 -11
@@ -1,656 +0,0 @@
1
- // oauth.mjs — how a client gets in: registered apps and client documents,
2
- // authorization codes and PKCE, the bearer tokens and their scopes, the
3
- // password gate, and the three /oauth endpoints plus app registration.
4
-
5
- import crypto from 'node:crypto';
6
- import { isCrossSiteNavigation } from '../../shared/guard.mjs';
7
- import { safeFetch, readCapped } from '../../shared/safefetch.mjs';
8
- import { readBody } from './body.mjs';
9
-
10
- const TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1000; // tokens age out after 90 days
11
- const AUTHZ_WINDOW_MS = 60_000;
12
- const AUTHZ_MAX_ATTEMPTS = 5;
13
- const CODE_TTL_MS = 5 * 60_000; // an authorization code is short-lived
14
- const MAX_APPS = 200; // registered third-party clients, capped
15
- const CLIENT_DOC_TTL_MS = 10 * 60_000; // how long a fetched client document is trusted
16
- const CLIENT_DOC_MAX_CACHED = 200; // ids remembered at once; oldest out
17
- const CLIENT_DOC_WINDOW_MS = 60_000;
18
- const CLIENT_DOC_MAX_FETCHES = 20; // outbound lookups per window
19
- const CLIENT_DOC_MAX = 64 * 1024; // it names a client; it is not a payload
20
-
21
- // scrypt check for the optional UI password ({ saltHex, hashHex } record —
22
- // see hashPassword, used by the passwd CLI).
23
- export function hashPassword(password) {
24
- const salt = crypto.randomBytes(16);
25
- const hash = crypto.scryptSync(String(password), salt, 32);
26
- return { saltHex: salt.toString('hex'), hashHex: hash.toString('hex') };
27
- }
28
- export function checkPassword(rec, password) {
29
- try {
30
- const hash = crypto.scryptSync(String(password), Buffer.from(rec.saltHex, 'hex'), 32);
31
- return crypto.timingSafeEqual(hash, Buffer.from(rec.hashHex, 'hex'));
32
- } catch { return false; }
33
- }
34
-
35
- const escapeHtml = (s) => String(s).replace(/[&<>"']/g, c =>
36
- ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
37
-
38
- // The authorize login page: every original OAuth param rides along as a
39
- // hidden field so the POST can complete the flow.
40
- // redirect_uris arrive as an array or a whitespace-separated string (Mastodon
41
- // accepts both); normalise to a trimmed, non-empty list.
42
- const parseRedirects = (v) => (Array.isArray(v) ? v : String(v || '').split(/\s+/))
43
- .map(s => s.trim()).filter(Boolean);
44
-
45
- // The sign-in page for an account whose owner signs in at their pod: no
46
- // password of ours exists, and none is asked for. The page carries the
47
- // client's request in its address, signs the owner in at the pod with the
48
- // fediverse-account library, and proves that sign-in back to /oauth/authorize.
49
- function sendPodSigninPage(res, client, webId, mount = '') {
50
- let asking = '';
51
- if (client && (client.name || client.redirect)) {
52
- let where = '';
53
- try { where = client.redirect ? new URL(client.redirect).host : ''; } catch { /* oob or blank */ }
54
- const who = client.name ? escapeHtml(client.name) : (where ? escapeHtml(where) : 'A client');
55
- asking = `<p><strong>${who}</strong> is asking to use your account`
56
- + `${where ? `, and will be sent back to <code>${escapeHtml(where)}</code>` : ''}.</p>`;
57
- }
58
- res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' });
59
- res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
60
- <title>Sign in with your pod</title>
61
- <style>:root{color-scheme:light dark;--heading:#1a4f8a;--btn:#3d6b35;--line:#9a9a9a}
62
- body{font:20px Arial,Helvetica,sans-serif;max-width:28rem;margin:12vh auto;padding:0 1rem}
63
- h1{color:var(--heading);font-size:1.5rem}
64
- code{word-break:break-all;background:rgba(120,120,120,.12);padding:.05em .35em;border-radius:4px}
65
- button{font:inherit;padding:.6rem 1.1rem;background:var(--btn);color:#fff;border:none;border-radius:9px;cursor:pointer}
66
- button[hidden]{display:none}
67
- #signin-status{min-height:1.5em;padding:.6rem .8rem;border:1px solid var(--line);border-radius:8px;margin-top:1rem}
68
- @media (prefers-color-scheme:dark){:root{--heading:#7fb3e8;--btn:#5a8a4f}}</style></head><body>
69
- <main>
70
- <h1>Sign in with your pod</h1>
71
- ${asking}
72
- <p>This account belongs to <a id="webid" href="${escapeHtml(webId)}">${escapeHtml(webId)}</a>. Sign in at that pod to allow it.</p>
73
- <p><button type="button" id="signin" hidden>Sign in with your pod</button></p>
74
- <p id="signin-status" role="status" aria-live="polite"></p>
75
- </main>
76
- <script type="module" src="${escapeHtml(mount)}/oauth/session/signin.mjs"></script>
77
- </body></html>`);
78
- return true;
79
- }
80
-
81
- function sendLoginForm(res, params, error = '', client = null, status = null, headers = {}) {
82
- const hidden = [...params.entries()].filter(([k]) => k !== 'password')
83
- .map(([k, v]) => `<input type="hidden" name="${escapeHtml(k)}" value="${escapeHtml(v)}">`).join('\n');
84
- // Name what is asking, so the owner approves a client they can see rather
85
- // than an opaque one. The destination the code will be sent to is the fact
86
- // that matters for a cross-site client.
87
- let asking = '<p>Enter the agent password to authorize this client.</p>';
88
- if (client && (client.name || client.redirect)) {
89
- let where = '';
90
- try { where = client.redirect ? new URL(client.redirect).host : ''; } catch { /* oob or blank */ }
91
- const who = client.name ? escapeHtml(client.name) : (where ? escapeHtml(where) : 'A client');
92
- asking = `<p><strong>${who}</strong> is asking to access your account`
93
- + `${where ? `, sending the authorization to <code>${escapeHtml(where)}</code>` : ''}.</p>`
94
- + `<p>Scope: <code>${escapeHtml(client.scope || 'read')}</code>. Enter the agent password to allow it.</p>`;
95
- }
96
- res.writeHead(status || (error ? 401 : 200),
97
- { 'content-type': 'text/html; charset=utf-8', ...headers });
98
- res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
99
- <title>FediPod — authorize</title>
100
- <style>:root{color-scheme:light dark;font-size:125%;--heading:#1a4f8a}
101
- body{font:1rem system-ui,sans-serif;max-width:22rem;margin:15vh auto;padding:0 1rem}
102
- h1{color:var(--heading)}
103
- code{word-break:break-all}
104
- @media (prefers-color-scheme:dark){:root{--heading:#7fb3e8}}
105
- input,button{font:inherit;width:100%;padding:.5rem;margin:.3rem 0;box-sizing:border-box}
106
- .err{color:#b00020}
107
- @media (prefers-color-scheme:dark){.err{color:#ff8a8a}}</style></head><body>
108
- <main>
109
- <h1>FediPod</h1>
110
- ${asking}
111
- ${error ? `<p class="err" id="login-err" role="alert">${escapeHtml(error)}</p>` : ''}
112
- <form method="POST" action="/oauth/authorize">
113
- ${hidden}
114
- <label for="password">Agent password</label>
115
- <input type="password" id="password" name="password" autofocus autocomplete="current-password"
116
- ${error ? 'aria-invalid="true" aria-describedby="login-err"' : ''}>
117
- <button type="submit">Authorize</button>
118
- </form>
119
- </main></body></html>`);
120
- return true;
121
- }
122
-
123
- // What one request needs. Mastodon's four coarse scopes; a client that was
124
- // granted a granular `write:statuses` satisfies `write` here, which is the
125
- // direction that cannot let anything through that `write` would not.
126
- //
127
- // Reads are `read`, writes are `write`, and the two Mastodon carves out are
128
- // kept: relationship changes accept the legacy `follow`, and push
129
- // subscriptions want `push`. Nothing here grants across: `write` does NOT
130
- // imply `read`, exactly as on Mastodon, so a write-only client cannot read
131
- // the owner's direct messages.
132
- export function scopeFor(method, pathname) {
133
- if (/^\/api\/v\d\/push\//u.test(pathname)) return 'push';
134
- const relationship = /^\/api\/v1\/(accounts\/[a-f0-9]+\/(follow|unfollow|block|unblock|mute|unmute|remove_from_followers)|follow_requests\/)/u;
135
- if (method !== 'GET' && method !== 'HEAD') {
136
- return relationship.test(pathname) ? 'follow' : 'write';
137
- }
138
- return 'read';
139
- }
140
-
141
- // Whether a token's granted scopes satisfy `need`.
142
- //
143
- // A record with NO scope is a token minted before scopes were kept — it is
144
- // full authority, because that is what it was granted, and quietly demoting
145
- // live 90-day tokens would sign people out of working clients for a bug that
146
- // was ours. New tokens all carry one.
147
- export function scopeAllows(granted, need) {
148
- if (granted == null) return true; // pre-scope token
149
- const have = String(granted).split(/[\s,+]+/u).filter(Boolean);
150
- if (!have.length) return true;
151
- if (need === 'follow') {
152
- // Mastodon's `follow` is the legacy spelling; `write` covers it too.
153
- return have.some((g) => g === 'follow' || g === 'write' || g.startsWith('write:'));
154
- }
155
- return have.some((g) => g === need || g.startsWith(`${need}:`));
156
- }
157
-
158
- /**
159
- * Whether a redirect the client asked for is one it published.
160
- *
161
- * A native client listens on whatever port the machine gave it, so it can
162
- * only publish the loopback address without one (RFC 8252). The port is
163
- * therefore not part of the match there, and nowhere else.
164
- */
165
- export function redirectMatches(published, asked) {
166
- if (published === asked) return true;
167
- try {
168
- const a = new URL(published);
169
- const b = new URL(asked);
170
- const loopback = (h) => h === '127.0.0.1' || h === '[::1]' || h === 'localhost';
171
- if (!loopback(a.hostname) || a.hostname !== b.hostname) return false;
172
- return a.protocol === b.protocol
173
- && a.pathname.replace(/\/$/u, '') === b.pathname.replace(/\/$/u, '');
174
- } catch { return false; }
175
- }
176
-
177
- /**
178
- * Whether this verifier is the one the challenge was made from (RFC 7636).
179
- * Length is checked because a short verifier is guessable, which is the
180
- * whole thing this is here to prevent.
181
- */
182
- export function provesCode(rec, verifier) {
183
- const v = String(verifier || '');
184
- if (v.length < 43 || v.length > 128) return false;
185
- if ((rec.challengeMethod || 'plain') === 'S256') {
186
- const made = crypto.createHash('sha256').update(v).digest('base64url');
187
- const given = Buffer.from(made);
188
- const known = Buffer.from(String(rec.challenge));
189
- return given.length === known.length && crypto.timingSafeEqual(given, known);
190
- }
191
- const given = Buffer.from(v);
192
- const known = Buffer.from(String(rec.challenge));
193
- return given.length === known.length && crypto.timingSafeEqual(given, known);
194
- }
195
-
196
- // ---- tokens ----
197
- // Tokens are records {token, createdAt} and expire; legacy bare strings
198
- // are read as undated and treated as expired-on-sight only if older
199
- // formats can't be dated (they get an epoch of now on first migration).
200
- export function tokenRecords(api) {
201
- const raw = api.store.read('masto-tokens.json', []);
202
- return raw.map(r => (typeof r === 'string' ? { token: r, createdAt: Date.now() } : r));
203
- }
204
-
205
- export function tokens(api) {
206
- const now = Date.now();
207
- return api.tokenRecords().filter(r => now - (r.createdAt || 0) < TOKEN_TTL_MS).map(r => r.token);
208
- }
209
-
210
- // `scope` is what the owner actually granted at /oauth/authorize. It used to
211
- // be discarded: every token was full authority, so a client that asked for
212
- // `read` could post, delete, and edit the profile. Recorded now, and enforced
213
- // at the one gate every client route passes (see scopeFor / authed).
214
- export function mintToken(api, scope = null) {
215
- const t = crypto.randomBytes(24).toString('hex');
216
- const now = Date.now();
217
- const kept = api.tokenRecords().filter(r => now - (r.createdAt || 0) < TOKEN_TTL_MS);
218
- api.store.write('masto-tokens.json',
219
- [...kept, { token: t, createdAt: now, ...(scope ? { scope } : {}) }].slice(-20));
220
- return t;
221
- }
222
-
223
- // Registered OAuth apps. A third-party (browser) client registers here, and
224
- // the authorization code it later receives is bound to the client_id and the
225
- // redirect_uri it registered — so the code reaches only where that client
226
- // said, and only that client, presenting its secret, can exchange it for a
227
- // bearer. A redirect back to this agent's own origin keeps the local flow.
228
- export function apps(api) { return api.store.read('oauth-apps.json', []); }
229
-
230
- /**
231
- * What a client needs to know before it can sign in, at the address RFC 8414
232
- * puts it. The actor carries the same two endpoints; a client that looks
233
- * here first finds everything rather than the minimum.
234
- *
235
- * `none` among the authentication methods is what says a client keeping no
236
- * secret is welcome, which is the whole of what a browser app needs to hear.
237
- */
238
- export function authorizationServerMetadata(api, origin) {
239
- const at = (p) => `${origin.replace(/\/$/u, '')}${p}`;
240
- return {
241
- issuer: origin.replace(/\/$/u, ''),
242
- authorization_endpoint: at('/oauth/authorize'),
243
- token_endpoint: at('/oauth/token'),
244
- revocation_endpoint: at('/oauth/revoke'),
245
- registration_endpoint: at('/api/v1/apps'),
246
- response_types_supported: [ 'code' ],
247
- grant_types_supported: [ 'authorization_code' ],
248
- code_challenge_methods_supported: [ 'S256', 'plain' ],
249
- token_endpoint_auth_methods_supported: [ 'client_secret_post', 'none' ],
250
- scopes_supported: [ 'read', 'write', 'follow', 'push' ],
251
- };
252
- }
253
-
254
- export function findApp(api, clientId) { return clientId ? api.apps().find(a => a.clientId === clientId) || null : null; }
255
-
256
- /**
257
- * A client that publishes its own metadata document is named by that
258
- * document's URL and registers nothing here: the document says who it is
259
- * and where it may be sent back to. Such a client keeps no secret, so it
260
- * always proves itself with a challenge instead.
261
- *
262
- * The fetch is the guarded one — a client id is a URL a stranger chose, and
263
- * an unguarded fetch of it would ask this machine to reach wherever they
264
- * pointed.
265
- */
266
- export async function resolveClientDocument(api, clientId) {
267
- if (!/^https:\/\//iu.test(String(clientId || ''))) return null; // cleartext is refused
268
- api.clientDocs = api.clientDocs || new Map();
269
- const seen = api.clientDocs.get(clientId);
270
- // A cached FAILURE counts. Only successes were remembered, so a client id
271
- // that 404s (or is not a client document at all) was re-fetched on every
272
- // single call — and this runs on an unauthenticated GET, so a page could
273
- // fire these in a loop and have the owner's machine hammer an address of
274
- // the attacker's choosing, from the owner's IP, indefinitely.
275
- if (seen && Date.now() - seen.at < CLIENT_DOC_TTL_MS) return seen.client;
276
- // And a budget, because caching alone still lets a fresh id per request
277
- // through. `safeFetch` keeps every one of these to a public address, so
278
- // this is not internal SSRF — it is amplification, and a cap is what
279
- // amplification needs.
280
- if (!api._clientDocFetches || Date.now() - api._clientDocWindow > CLIENT_DOC_WINDOW_MS) {
281
- api._clientDocWindow = Date.now();
282
- api._clientDocFetches = 0;
283
- }
284
- if (api._clientDocFetches >= CLIENT_DOC_MAX_FETCHES) {
285
- api.log(`client document ${clientId} not fetched: too many lookups this minute`);
286
- return null;
287
- }
288
- api._clientDocFetches += 1;
289
- // The Map has a TTL but had no ceiling, so it grew for as long as an
290
- // attacker cared to serve distinct documents. Oldest out when it is full;
291
- // Map iterates in insertion order, so the first key is the oldest.
292
- if (api.clientDocs.size >= CLIENT_DOC_MAX_CACHED) {
293
- api.clientDocs.delete(api.clientDocs.keys().next().value);
294
- }
295
- const remember = (client) => { api.clientDocs.set(clientId, { at: Date.now(), client }); return client; };
296
- let doc;
297
- try {
298
- const res = await safeFetch(clientId, { headers: { accept: 'application/json' } });
299
- if (res.status >= 400) { api.log(`client document ${clientId} → ${res.status}`); return remember(null); }
300
- doc = JSON.parse(await readCapped(res, CLIENT_DOC_MAX));
301
- } catch (e) {
302
- api.log(`client document ${clientId} could not be read: ${e.message}`);
303
- return remember(null);
304
- }
305
- // It must claim to be itself: a document naming some other id would let
306
- // one client borrow another's name.
307
- if (doc?.client_id !== clientId) {
308
- api.log(`client document ${clientId} names ${doc?.client_id ?? 'nothing'} — refused`);
309
- return remember(null);
310
- }
311
- const redirectUris = [].concat(doc.redirect_uris || []).filter((u) => typeof u === 'string');
312
- if (!redirectUris.length) { api.log(`client document ${clientId} names no redirect — refused`); return remember(null); }
313
- const client = {
314
- clientId, redirectUris,
315
- name: String(doc.client_name || clientId).slice(0, 200),
316
- scopes: 'read write follow',
317
- };
318
- return remember(client);
319
- }
320
-
321
- export function registerApp(api, { name, website, redirectUris, scopes }) {
322
- const app = {
323
- clientId: crypto.randomBytes(16).toString('hex'),
324
- clientSecret: crypto.randomBytes(32).toString('base64url'),
325
- name: String(name || 'client').slice(0, 200),
326
- website: String(website || '').slice(0, 500),
327
- redirectUris, scopes: String(scopes || 'read'), createdAt: Date.now(),
328
- };
329
- api.store.write('oauth-apps.json', [...api.apps(), app].slice(-MAX_APPS));
330
- return app;
331
- }
332
-
333
- // A short-lived, single-use authorization code for a registered client, kept
334
- // apart from masto-tokens.json so the code is NOT a bearer until it is
335
- // exchanged with the client secret.
336
- export function mintCode(api, { clientId, redirectUri, scope, challenge = null, challengeMethod = null }) {
337
- const code = crypto.randomBytes(24).toString('hex');
338
- const now = Date.now();
339
- const kept = api.store.read('oauth-codes.json', []).filter(c => now - c.createdAt < CODE_TTL_MS);
340
- api.store.write('oauth-codes.json', [ ...kept, {
341
- code, clientId, redirectUri, scope, createdAt: now,
342
- // What the client promised to prove when it comes back for the token.
343
- // A client that cannot keep a secret — anything running in a browser —
344
- // has this instead, and it is the only thing standing between a stolen
345
- // code and a token.
346
- ...(challenge ? { challenge, challengeMethod: challengeMethod || 'plain' } : {}),
347
- } ].slice(-50));
348
- return code;
349
- }
350
-
351
- export function consumeCode(api, code) {
352
- const now = Date.now();
353
- const all = api.store.read('oauth-codes.json', []);
354
- const rec = all.find(c => c.code === code && now - c.createdAt < CODE_TTL_MS);
355
- if (rec) api.store.write('oauth-codes.json', all.filter(c => c.code !== code)); // single-use
356
- return rec || null;
357
- }
358
-
359
- // The live record for the bearer on this request, or null.
360
- export function tokenOf(api, req) {
361
- const m = /^Bearer (.+)$/.exec(req.headers.authorization || '');
362
- if (!m) return null;
363
- const now = Date.now();
364
- return api.tokenRecords().find(
365
- (r) => r.token === m[1] && now - (r.createdAt || 0) < TOKEN_TTL_MS) || null;
366
- }
367
-
368
- export function authed(api, req) { return !!api.tokenOf(req); }
369
-
370
- // A redirect_uri must name an authority this agent answers on — otherwise
371
- // a visited page could navigate to /oauth/authorize and have the freshly
372
- // minted code delivered to itself.
373
- export function redirectAllowed(api, redirect) {
374
- if (!redirect || redirect === 'urn:ietf:wg:oauth:2.0:oob') return true;
375
- if (!api.allowed) return true; // no policy configured (tests)
376
- try {
377
- const u = new URL(redirect);
378
- if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
379
- return api.allowed.has(u.host.toLowerCase());
380
- } catch { return false; }
381
- }
382
-
383
- export function rateLimited(api) {
384
- const now = Date.now();
385
- api.authzAttempts = api.authzAttempts.filter(t => now - t < AUTHZ_WINDOW_MS);
386
- if (api.authzAttempts.length >= AUTHZ_MAX_ATTEMPTS) return true;
387
- api.authzAttempts.push(now);
388
- return false;
389
- }
390
-
391
- export async function handle(api, ctx) {
392
- const { req, res, pathname, url, send } = ctx; // eslint-disable-line no-unused-vars
393
-
394
- // --- oauth ---
395
- // With no UI password configured this is "theater": whoever can reach the
396
- // loopback surface IS the trusted user, and authorize redirects at once.
397
- // With a password set (required before any non-loopback exposure), the
398
- // authorize step becomes a real login form.
399
- if (pathname === '/api/v1/apps' && req.method === 'POST') {
400
- const body = await readBody(req);
401
- const redirectUris = parseRedirects(body.redirect_uris);
402
- const app = api.registerApp({ name: body.client_name, website: body.website,
403
- redirectUris, scopes: body.scopes });
404
- return send(200, {
405
- id: app.clientId, name: app.name, website: app.website,
406
- client_id: app.clientId, client_secret: app.clientSecret,
407
- redirect_uri: redirectUris.join(' ') || 'urn:ietf:wg:oauth:2.0:oob',
408
- ...(api.webPush ? { vapid_key: api.push.publicKey() } : {}),
409
- });
410
- }
411
- if (pathname === '/oauth/authorize' && (req.method === 'GET' || req.method === 'POST')) {
412
- const pw = api.store.getConfig()?.uiPassword; // { saltHex, hashHex } scrypt record
413
- let params = url.searchParams;
414
- let body = null;
415
- if (req.method === 'POST') { body = await readBody(req); params = new URLSearchParams(body); }
416
- const redirect = params.get('redirect_uri') || '';
417
- // The owner's pod sign-in, proved: a Solid-OIDC token with its DPoP proof,
418
- // from the WebID this account belongs to. It stands in for the password
419
- // everywhere below.
420
- let proved = false;
421
- if (req.method === 'POST' && req.headers.authorization && req.headers.dpop && api.podAuth) {
422
- const proof = await api.podAuth(req, pathname);
423
- if (!proof.ok) return send(proof.status || 401, { error: proof.error });
424
- proved = true;
425
- }
426
- const ownerWebId = api.ownerWebId?.() || null;
427
- // The way back from the pod's login: no client named, only the pod's
428
- // code. The page finishes the sign-in and returns to the client's request.
429
- if (req.method === 'GET' && !params.get('client_id') && params.get('code') && params.get('state') && ownerWebId) {
430
- return sendPodSigninPage(res, null, ownerWebId, api.mount || '');
431
- }
432
- const app = api.findApp(params.get('client_id') || '');
433
- // A client that published its own metadata document needs no
434
- // registration here: the document is its name and says where it may be
435
- // sent back to.
436
- const doc = app ? null : await api.resolveClientDocument(params.get('client_id') || '');
437
- // A REGISTERED client is always the third-party flow — its code is
438
- // bound to it and exchanged with its secret — even when its redirect
439
- // points back at this very agent (a web client served from our own
440
- // origin registers itself exactly like a phone app does). The local
441
- // code-is-the-token flow is only for the built-in client, which never
442
- // registers.
443
- const external = !!app || !!doc;
444
-
445
- // S36. Two doors were open at once, and together they handed a bearer for
446
- // this account to any page the owner happened to be reading.
447
- //
448
- // /api/v1/apps takes any redirect_uri from anyone — as it must, that is
449
- // how a fediverse client registers — and the instant-authorize path below
450
- // mints without a password whenever the request "is this machine". A page
451
- // in another tab is this machine. So: register a client whose redirect is
452
- // your own server, navigate the owner's browser to /oauth/authorize, and
453
- // the code arrives at your address. The agent's own CA is in the browser's
454
- // trust store, so https://localhost:<port> loads without a murmur.
455
- //
456
- // Two locks, either of which alone would do, both cheap:
457
- //
458
- // 1. A cross-site NAVIGATION may not reach the mint. A client signing in
459
- // navigates from its own page and is same-site; a page on somebody
460
- // else's site is not. (Absent Sec-Fetch-Site — curl, an old browser —
461
- // is not cross-site and still passes, as everywhere else in this
462
- // project; the second lock is what covers that case.)
463
- if (isCrossSiteNavigation(req)) {
464
- api.log(`authorize refused: cross-site navigation to the mint from ${req.headers.referer || 'nowhere'}`);
465
- return send(403, { error: 'a cross-site navigation may not authorize a client' });
466
- }
467
- // 2. With NO password set there is nothing to authorize a third party
468
- // WITH. The password-less path is honest only for a client on an
469
- // address this agent itself answers on — where "whoever reaches the
470
- // port is the owner" is a statement about the machine rather than
471
- // about a web page. Anything pointing elsewhere has to be approved by
472
- // somebody who knows the password.
473
- const client = { name: app?.name || doc?.name || null, redirect, scope: params.get('scope') || 'read' };
474
- if (external && !proved && !api.store.getConfig()?.uiPassword && !api.redirectAllowed(redirect)) {
475
- // With a pod to sign in at, the owner approves the client there.
476
- if (req.method === 'GET' && ownerWebId) return sendPodSigninPage(res, client, ownerWebId, api.mount || '');
477
- api.log(`authorize refused: no UI password, and "${redirect}" is not an address of this agent`);
478
- return send(403, {
479
- error: 'this client asks to be sent somewhere other than this agent, and no password is '
480
- + 'set to approve that with. Run `fedipod passwd` and try again.',
481
- });
482
- }
483
- if (app) {
484
- if (!app.redirectUris.includes(redirect)) {
485
- api.log(`authorize refused: redirect_uri "${redirect}" not registered for ${app.clientId}`);
486
- return send(400, { error: 'redirect_uri was not registered by this client' });
487
- }
488
- } else if (doc) {
489
- if (!doc.redirectUris.some((u) => redirectMatches(u, redirect))) {
490
- api.log(`authorize refused: redirect_uri "${redirect}" is not one ${doc.clientId} published`);
491
- return send(400, { error: 'redirect_uri is not one this client published' });
492
- }
493
- // It keeps no secret, so the challenge is the only thing that will
494
- // stand between its code and a token. Refuse now rather than mint a
495
- // code nothing can prove.
496
- if (!params.get('code_challenge')) {
497
- api.log(`authorize refused: ${doc.clientId} keeps no secret and offered no challenge`);
498
- return send(400, { error: 'a client identified by its own document must send a code_challenge' });
499
- }
500
- } else if (!api.redirectAllowed(redirect)) {
501
- api.log(`authorize refused: redirect_uri "${redirect}" is not this agent`);
502
- return send(400, { error: 'redirect_uri must be an address of this agent' });
503
- }
504
- if (req.method === 'POST' && !proved) {
505
- if (api.rateLimited()) {
506
- api.log('authorize rate limited');
507
- // 429, not 401: a client that reads this as a wrong password will
508
- // ask the person to type it again, which is the one thing that
509
- // cannot help. Retry-After says how long the wait actually is.
510
- return sendLoginForm(res, params, 'too many attempts — wait a minute', client,
511
- 429, { 'retry-after': String(Math.ceil(AUTHZ_WINDOW_MS / 1000)) });
512
- }
513
- if (!pw || !checkPassword(pw, body.password || '')) {
514
- return sendLoginForm(res, params, 'wrong password — try again', client);
515
- }
516
- } else if (pw && !proved) {
517
- // The login/approve screen names what is asking before the owner types
518
- // the password — a cross-site client cannot forge past that.
519
- return sendLoginForm(res, params, '', client);
520
- } else if (!proved && api.allowed && !api.allowed.isLocalRequest(req) && !api.throughDoor?.(req)) {
521
- // With a pod to sign in at, the owner does that instead of a password.
522
- if (req.method === 'GET' && ownerWebId) return sendPodSigninPage(res, client, ownerWebId, api.mount || '');
523
- // No password set, and this request did not come from this machine.
524
- //
525
- // The instant-authorize path is honest theatre on loopback: whoever can
526
- // reach it IS the trusted user. It stops being theatre the moment an
527
- // operator takes the documented AP_ALLOWED_HOSTS route and puts the
528
- // agent on a tailnet name or behind a reverse proxy, because anyone who
529
- // reaches that name is then handed a 90-day bearer for the whole facade
530
- // — update_credentials included, which is precisely the authority the
531
- // isLocal check on /setup and /config exists to withhold. /oauth is
532
- // dispatched before that check ever runs, so it needs its own.
533
- api.log(`authorize refused: no UI password, and "${req.headers.host}" is not this machine`);
534
- return send(403, {
535
- error: api.embedded
536
- ? 'Open your account\'s management page first, then sign in from there.'
537
- : 'Signing in from another machine needs a password. On the machine that runs your account, run: fedipod passwd',
538
- });
539
- }
540
- // External clients get a bound code; the local flow keeps code==token.
541
- const code = external
542
- ? api.mintCode({ clientId: (app || doc).clientId, redirectUri: redirect, scope: client.scope,
543
- challenge: params.get('code_challenge') || null,
544
- challengeMethod: params.get('code_challenge_method') || null })
545
- : api.mintToken(client.scope);
546
- if (!redirect || redirect === 'urn:ietf:wg:oauth:2.0:oob') return send(200, { code });
547
- const target = new URL(redirect);
548
- target.searchParams.set('code', code);
549
- if (params.get('state')) target.searchParams.set('state', params.get('state'));
550
- // A proved sign-in came from the page's own request, which cannot follow
551
- // a redirect to a client's address; the page is told where to go.
552
- if (proved) return send(200, { redirect: target.href });
553
- res.writeHead(302, { location: target.href });
554
- res.end();
555
- return true;
556
- }
557
- if (pathname === '/oauth/token' && req.method === 'POST') {
558
- const body = await readBody(req);
559
- // A registered third-party client exchanges its bound code, proving its
560
- // secret, for a real bearer — the code alone is not a token.
561
- const app = api.findApp(body.client_id || '');
562
- // A client named by its own document keeps no secret at all, so the
563
- // challenge is the whole of its proof. The code carries the document's
564
- // URL as the client it was bound to.
565
- if (!app && body.code_verifier && /^https:\/\//iu.test(String(body.client_id || ''))) {
566
- const rec = api.consumeCode(body.code || '');
567
- if (!rec || rec.clientId !== body.client_id
568
- || (body.redirect_uri && rec.redirectUri !== body.redirect_uri)) {
569
- api.log('token refused: code is not a live authorization for that client document');
570
- return send(400, { error: 'invalid_grant' });
571
- }
572
- if (!rec.challenge || !provesCode(rec, body.code_verifier)) {
573
- api.log('token refused: the verifier does not answer the challenge this code was made with');
574
- return send(400, { error: 'invalid_grant' });
575
- }
576
- return send(200, { access_token: api.mintToken(rec.scope || 'read'), token_type: 'Bearer',
577
- scope: rec.scope || 'read', created_at: Math.floor(Date.now() / 1000),
578
- ...(api.urls?.actor ? { activitypub_actor_id: api.urls.actor } : {}) });
579
- }
580
- // A client that runs in a browser cannot keep a secret, so it proves it
581
- // is the same caller that asked instead: it sends the verifier for the
582
- // challenge it presented at authorize (RFC 7636). Sending a verifier is
583
- // what says which of the two flows this is.
584
- if (app && body.code_verifier) {
585
- const rec = api.consumeCode(body.code || '');
586
- if (!rec || rec.clientId !== app.clientId || (body.redirect_uri && rec.redirectUri !== body.redirect_uri)) {
587
- api.log('token refused: code is not a live authorization for this client');
588
- return send(400, { error: 'invalid_grant' });
589
- }
590
- // A code minted without a challenge cannot be redeemed with one: that
591
- // would let anyone holding a stolen code invent the proof for it.
592
- if (!rec.challenge || !provesCode(rec, body.code_verifier)) {
593
- api.log('token refused: the verifier does not answer the challenge this code was made with');
594
- return send(400, { error: 'invalid_grant' });
595
- }
596
- return send(200, { access_token: api.mintToken(rec.scope || 'read'), token_type: 'Bearer',
597
- scope: rec.scope || 'read', created_at: Math.floor(Date.now() / 1000),
598
- ...(api.urls?.actor ? { activitypub_actor_id: api.urls.actor } : {}) });
599
- }
600
- if (app && body.client_secret) {
601
- const given = Buffer.from(String(body.client_secret));
602
- const known = Buffer.from(app.clientSecret);
603
- const okSecret = given.length === known.length && crypto.timingSafeEqual(given, known);
604
- if (!okSecret) { api.log('token refused: client secret mismatch'); return send(401, { error: 'invalid_client' }); }
605
- const rec = api.consumeCode(body.code || '');
606
- if (!rec || rec.clientId !== app.clientId || (body.redirect_uri && rec.redirectUri !== body.redirect_uri)) {
607
- api.log('token refused: code is not a live authorization for this client');
608
- return send(400, { error: 'invalid_grant' });
609
- }
610
- // A challenge, once made, is not optional: without this a client could
611
- // present one and then skip past it with the secret alone.
612
- if (rec.challenge && !provesCode(rec, body.code_verifier)) {
613
- api.log('token refused: this code was made with a challenge and the verifier does not answer it');
614
- return send(400, { error: 'invalid_grant' });
615
- }
616
- return send(200, { access_token: api.mintToken(rec.scope || 'read'), token_type: 'Bearer',
617
- scope: rec.scope || 'read', created_at: Math.floor(Date.now() / 1000),
618
- // Which actor the token acts for. A Mastodon client ignores it; an
619
- // ActivityPub API client needs it, and asking for it separately
620
- // would mean a second round trip before it knows who it is.
621
- ...(api.urls?.actor ? { activitypub_actor_id: api.urls.actor } : {}) });
622
- }
623
- // Local flow: the code IS the token, minted by /oauth/authorize after the
624
- // password gate. Minting one here for an unrecognised code handed a
625
- // bearer to anyone who could reach the port — and a non-browser client
626
- // sends no Origin, so the firewall never saw it. That defeated `passwd`
627
- // on any agent deliberately exposed through AP_ALLOWED_HOSTS.
628
- if (!body.code || !api.tokens().includes(body.code)) {
629
- api.log('token refused: code is not a live authorization');
630
- return send(400, { error: 'invalid_grant' });
631
- }
632
- // The scope this token was actually minted with, not whatever the client
633
- // asks to be told — reporting one and enforcing another is how a client
634
- // ends up surprised by a 403 it was promised it would not get.
635
- const granted = api.tokenRecords().find((r) => r.token === body.code)?.scope
636
- || 'read write follow push';
637
- return send(200, { access_token: body.code, token_type: 'Bearer',
638
- scope: granted, created_at: Math.floor(Date.now() / 1000),
639
- ...(api.urls?.actor ? { activitypub_actor_id: api.urls.actor } : {}) });
640
- }
641
- if (pathname === '/oauth/revoke' && req.method === 'POST') {
642
- // It used to answer 200 and keep the token, so logging out of a client
643
- // left a working 90-day bearer behind. Mastodon's endpoint takes `token`;
644
- // an unknown one is still a 200, which is what the spec asks for.
645
- const body = await readBody(req).catch(() => ({}));
646
- const gone = body?.token;
647
- if (gone) {
648
- const kept = api.tokenRecords().filter(r => r.token !== gone);
649
- api.store.write('masto-tokens.json', kept);
650
- api.log('client token revoked');
651
- }
652
- return send(200, {});
653
- }
654
-
655
- return false;
656
- }