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,970 +0,0 @@
1
- // front-core.mjs — the multi-user front: one always-on box, run by a host, that
2
- // fronts many independent FediPod users under a shared domain (@name@fedipod.net).
3
- // It answers name lookups, serves each user's public face, and is the verifying
4
- // inbox for all of them — routing every verified delivery into that user's own
5
- // pod inbox. It is KEYLESS and holds no user data: only a directory (handle →
6
- // where that user's pod is + their public key) and the per-user secrets needed
7
- // to append verified mail to their inbox.
8
- //
9
- // The identity trick is rewrite-on-read (jg10's pattern): a user's public IRI
10
- // space at `<front>/u/<handle>/` is a 1:1 path map of their pod's own tree, so
11
- // the actor id and every id under it live on the shared domain while the bytes
12
- // still come from the user's pod. The inbox maps the same way, so delivery to
13
- // the fronted inbox is delivery this box verifies, then forwards to the pod.
14
- //
15
- // Runtime-agnostic: `netlify/functions/front.mjs` is one adapter; any always-on
16
- // HTTPS box is another. UN-DEPLOYED — nothing in FediPod runs it.
17
-
18
- import crypto from 'node:crypto';
19
- import { handleDelivery, handleOwnerPost } from './gateway-core.mjs';
20
- import { readCapped, safeFetch, isLoopbackHost } from '../shared/safefetch.mjs';
21
- import * as podRoot from '../pod/root.mjs';
22
- import * as podPolicy from '../pod/policy.mjs';
23
- import { podBaseOfWebId } from '../pod/urls.mjs';
24
- import { routeQuietApi, noteOpened, noteReceived, closedState, accountState, closedAnswer } from './quiet.mjs';
25
- import { routeNoticesApi } from './notices.mjs';
26
- import { withSecurityHeaders } from './headers.mjs';
27
- import { podTokenVerifier } from './caches.mjs';
28
-
29
- // The one WebFinger document, spelled out here rather than imported from
30
- // wire.mjs: wire drags the agent's whole HTML pipeline (sanitize-html and
31
- // friends), which a serverless front must never carry — it crashed the
32
- // deployed function before it answered its first request.
33
- const jrd = ({ handle, host, actor, aliases = [], page = null, subscribe = null }) => ({
34
- subject: `acct:${handle}@${host}`,
35
- ...(aliases.length ? { aliases } : {}),
36
- links: [
37
- { rel: 'self', type: 'application/activity+json', href: actor },
38
- ...(page ? [{ rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: page }] : []),
39
- // Remote follow. Somebody reading this account on another server presses
40
- // Follow, says they are here, and that server looks for this link to find
41
- // out where to send them. `{uri}` is a literal the other server fills in
42
- // with whatever they are following — it must not be escaped.
43
- ...(subscribe ? [{ rel: 'http://ostatus.org/schema/1.0/subscribe', template: subscribe }] : []),
44
- ],
45
- });
46
- // The door that link names, which the site staged beside this function serves.
47
- const subscribeTemplate = (origin) => (origin
48
- ? `${String(origin).replace(/\/$/u, '')}/authorize_interaction?uri={uri}` : null);
49
- // The public profile page of any account the front answers for lives on
50
- // its pod, whether the address is the front's or the pod's own.
51
- const profilePageOf = (rec) => (rec?.podHome ? rec.podHome + 'ap/profile.html' : null);
52
-
53
- const AP_CT = 'application/activity+json';
54
-
55
- // Verify a Solid-OIDC token (DPoP-bound) and return its WebID, or null. The
56
- // verifier is injected so tests stub it; in production it is the same library
57
- // the agent's own C2S auth uses.
58
- async function verifyPodToken(request, pathname, verifier) {
59
- const authz = request.headers.get('authorization');
60
- if (!authz) return null;
61
- try {
62
- const dpop = request.headers.get('dpop');
63
- // Solid-OIDC binds the token to a key the client proves on every request;
64
- // a token shown without the proof is one anyone who saw it could show.
65
- if (!dpop) { console.log(`front: pod token without a DPoP proof refused on ${pathname}`); return null; }
66
- const v = verifier || await podTokenVerifier();
67
- const url = request.url;
68
- const { webid } = await v(authz, { header: dpop, method: request.method, url });
69
- return webid || null;
70
- } catch (e) {
71
- // Said aloud: a token the front will not take is otherwise a bare 401 to
72
- // the caller and nothing at all here.
73
- console.log(`front: pod token refused on ${pathname}: ${e?.message || e}`);
74
- return null;
75
- }
76
- }
77
-
78
- // Is this WebID served by the claimed pod? A pod owner's WebID lives on the pod
79
- // origin — that is the whole proof: a token for a WebID under podHome could
80
- // only be minted by someone who controls that pod's identity provider.
81
- // A pod that will not answer must not hold up the person opting in; without
82
- // an answer the older check stands on its own.
83
- const OWNER_LOOKUP_MS = 5_000;
84
-
85
- /**
86
- * Who the pod server says owns the pod. The server that hosts it is the
87
- * authority on that, so when it answers, its answer decides. A server that
88
- * says nothing leaves where the WebID lives as the only evidence there is.
89
- */
90
- const podOwners = (podBase, fetchImpl = fetch) =>
91
- podRoot.readOwnerLinks(fetchImpl, podBase, { timeoutMs: OWNER_LOOKUP_MS });
92
-
93
- // Whether a token's WebID vouches for this pod, on the evidence of where it
94
- // lives. Weak on purpose, and never used alone any more — see provesPod.
95
- function webidUnderPod(webid, podHome) {
96
- try { return new URL(webid).origin === new URL(podHome).origin; } catch { return false; }
97
- }
98
-
99
- /**
100
- * Whether this WebID may speak for this pod.
101
- *
102
- * Sharing an origin was the whole test, which is right where an origin is one
103
- * person (a subdomain pod) and wrong where it is not. On a path-based server
104
- * every account is on the same origin, so any account holder could attach
105
- * somebody else's pod under a name of their choosing — and, worse, re-attach a
106
- * name that already existed and be handed its receipt secret (S7).
107
- *
108
- * The pod server is the authority on who owns the pod, and it will say so in a
109
- * `solid:owner` link if asked. When it answers, its answer decides. Only when
110
- * it says nothing does this fall back to where the WebID lives, which is the
111
- * best a silent server leaves available. This is the rule /api/agent's opt-in
112
- * has used all along; attach simply never asked.
113
- */
114
- async function provesPod(webid, podHome, fetchImpl) {
115
- const owners = await podOwners(podHome, fetchImpl || fetch);
116
- if (owners.length) return owners.includes(webid);
117
- return webidUnderPod(webid, podHome);
118
- }
119
-
120
- // A pod this front will read from at all.
121
- //
122
- // The front reads `rec.podHome + <the rest of the request path>` and, on the
123
- // CSS server component, that read goes straight into the resource store with
124
- // no access control of its own — so a row whose podHome names the FRONT is a
125
- // row that can read the server's own internals, `/.internal/fedipod/directory/`
126
- // included, where every user's receipt secret is kept (S2). The front is not a
127
- // pod and never was one; saying so outright is the fix, and it closes the
128
- // self-recursion of S8 with the same line.
129
- function podHomeProblem(podHome, frontOrigin) {
130
- let u;
131
- try { u = new URL(podHome); } catch { return 'podHome is not a URL'; }
132
- if (frontOrigin) {
133
- try {
134
- if (u.origin === new URL(frontOrigin).origin) {
135
- return 'a pod cannot live on this gateway — name the pod that holds your data';
136
- }
137
- } catch { /* no usable frontOrigin: the other checks still apply */ }
138
- }
139
- // Nothing may reach a server's private trees, whatever origin they are on.
140
- if (/(^|\/)\.internal(\/|$)/u.test(u.pathname) || /(^|\/)\.\.(\/|$)/u.test(u.pathname)) {
141
- return 'that is not a pod address';
142
- }
143
- return null;
144
- }
145
-
146
- const RELAY_MAX_REQUESTS = 20;
147
- const RELAY_MAX_BODY = 1024 * 1024;
148
- const RELAY_TIMEOUT_MS = 8_000;
149
- // The only headers a relayed request may carry to the remote server. Host
150
- // comes from the URL; the user agent from safeFetch.
151
- const SERVER_PROBE_MS = 6000;
152
- const SERVER_PROBE_BYTES = 64 * 1024;
153
- const RELAY_HEADERS = new Set(['date', 'digest', 'signature', 'content-type', 'accept']);
154
-
155
- const keyIdOf = (signature) => (/keyId="([^"]+)"/.exec(signature || '') || [])[1] || null;
156
-
157
- // The owner's APIs a browser at ANOTHER origin may call: an address moving
158
- // to a new gateway is driven from the new gateway's page, and it has to tell
159
- // this one it moved and send the Move through this one's relay. Both take a
160
- // pod token as proof, the way the outbox door does, so any origin may ask.
161
- const API_CORS = {
162
- 'access-control-allow-origin': '*',
163
- 'access-control-allow-methods': 'POST, OPTIONS',
164
- 'access-control-allow-headers': 'Authorization, DPoP, Content-Type',
165
- 'access-control-max-age': '86400',
166
- };
167
- const withApiCors = (out) => ({ ...out, headers: { ...(out.headers || {}), ...API_CORS } });
168
- const apiPreflight = () => ({ status: 204, headers: { ...API_CORS, allow: 'POST, OPTIONS' }, body: null });
169
-
170
- async function relayOne(item, rec, fetchImpl) {
171
- const url = String(item?.url || '');
172
- const method = String(item?.method || 'POST').toUpperCase();
173
- if (method !== 'GET' && method !== 'POST') return { url, status: 0, error: 'method must be GET or POST' };
174
- let u;
175
- try { u = new URL(url); } catch { return { url, status: 0, error: 'not a URL' }; }
176
- if (u.protocol !== 'https:' && !(u.protocol === 'http:' && process.env.AP_ALLOW_PRIVATE_TARGETS === '1')) {
177
- return { url, status: 0, error: 'https only' };
178
- }
179
- const headers = {};
180
- for (const [k, v] of Object.entries(item?.headers || {})) {
181
- const name = k.toLowerCase();
182
- if (RELAY_HEADERS.has(name) && typeof v === 'string') headers[name] = v;
183
- }
184
- const body = method === 'POST' ? String(item?.body ?? '') : undefined;
185
- if (body !== undefined && Buffer.byteLength(body) > RELAY_MAX_BODY) return { url, status: 0, error: 'body too large' };
186
- const keyId = keyIdOf(headers.signature);
187
- if (method === 'POST' && !keyId) return { url, status: 0, error: 'a delivery must be signed' };
188
- if (keyId && !keyId.startsWith(rec.actorUrl + '#')) return { url, status: 0, error: "signed with a key that is not this account's" };
189
- if (headers.digest) {
190
- const want = 'SHA-256=' + crypto.createHash('sha256').update(body || '').digest('base64');
191
- if (headers.digest !== want) return { url, status: 0, error: 'digest does not match the body' };
192
- }
193
- try {
194
- const res = await safeFetch(url, { method, headers, body, signal: AbortSignal.timeout(RELAY_TIMEOUT_MS) }, fetchImpl);
195
- const out = { url, method, status: res.status };
196
- // The far server asking to be left alone has to reach the agent that will
197
- // do the asking again. Without this the browser build could not honour a
198
- // Retry-After at all — every delivery it makes goes through here — and fell
199
- // back to its own ladder against a server that had already said how long.
200
- const retryAfter = res.headers.get('retry-after');
201
- if (retryAfter) out.retryAfter = retryAfter;
202
- if (method === 'GET') {
203
- out.contentType = res.headers.get('content-type') || null;
204
- out.body = await readCapped(res, RELAY_MAX_BODY);
205
- }
206
- return out;
207
- } catch (e) { return { url, method, status: 0, error: e.message }; }
208
- }
209
-
210
- const j = (status, obj, ct = 'application/json') =>
211
- ({ status, headers: { 'content-type': ct, 'cache-control': 'no-store' }, body: JSON.stringify(obj) });
212
- // Held at the edge like any other missing document: a bot's scan, a dead
213
- // handle, a typo in a WebFinger query each cost one call, not one per asker.
214
- const notFound = () => ({ status: 404, headers: { 'content-type': 'text/plain', ...publicFor(MISSING_EDGE_SECONDS) }, body: 'not found\n' });
215
-
216
- // A user's public base on the front, a 1:1 mirror of their pod home.
217
- // The directory key is the FULL fediverse address — handle@host — never the
218
- // bare handle, which is not unique: two different pods can each have a "me".
219
- // A fronted identity answers at this front's host; a mail-door identity at its
220
- // own pod's host. The `/u/` path segment carries that same key.
221
- export const addressKey = (handle, host) => `${handle}@${host}`;
222
- export const userBase = (frontOrigin, key) => `${frontOrigin}/u/${encodeURIComponent(key)}/`;
223
-
224
- // Rewrite every occurrence of the pod home to the front base (or back). The
225
- // bodies are JSON, so a whole-string swap rewrites ids wherever they appear —
226
- // actor id, collection ids, note ids — keeping the whole space self-consistent.
227
- const swap = (text, from, to) => text.split(from).join(to);
228
-
229
- // Which handle a /u/<handle>/... path names, and the pod path under it.
230
- function parseUserPath(pathname) {
231
- const m = /^\/u\/([^/]+)\/(.*)$/.exec(pathname);
232
- return m ? { handle: decodeURIComponent(m[1]), rest: m[2] } : null;
233
- }
234
-
235
- // A public document is the same for everybody who asks, so the edge may hold
236
- // it and answer the next reader itself. Without this every read of an actor,
237
- // a topic, a post or a card was a function call: one visit to a forum is
238
- // twenty of them, and every visitor and every refresh paid again.
239
- //
240
- // `netlify-cdn-cache-control` is the edge's own instruction; `cache-control`
241
- // is what the reader's browser is told. The edge may serve a stale copy while
242
- // it fetches a fresh one, so nobody waits on the pod for a document that is
243
- // merely minutes old. The edge holds a document far longer than a browser
244
- // is asked to: the edge is what turns a thousand readers into one function
245
- // call, while a browser that held an actor for ten minutes would show one
246
- // person a stale profile they had just edited.
247
- const BROWSER_MAX_AGE = 60;
248
- const publicFor = (seconds, stale = seconds * 4) => ({
249
- 'cache-control': `public, max-age=${Math.min(seconds, BROWSER_MAX_AGE)}`,
250
- // `durable`: one copy for every region, and one that a deploy does not
251
- // empty. Without it each region held its own and every deploy started cold.
252
- 'netlify-cdn-cache-control': `public, durable, s-maxage=${seconds}, stale-while-revalidate=${stale}`,
253
- });
254
- // How long the edge holds a public document, and a handle's WebFinger
255
- // answer. Every server that has heard of an account asks for its actor and
256
- // its handle again and again; a post or a collection changes when its owner
257
- // acts, and ten minutes of edge is what the plan's budget of function calls
258
- // can afford.
259
- const PUBLIC_EDGE_SECONDS = 600;
260
- const WEBFINGER_EDGE_SECONDS = 3600;
261
- // A closed or moved address stays gone; a picture's address stays where it is.
262
- const GONE_EDGE_SECONDS = 3600;
263
- const MEDIA_EDGE_SECONDS = 86400;
264
- // A public document the pod would not give (missing, or not public) is asked
265
- // for again and again by every server that shows the account — a pinned-posts
266
- // collection an old account never wrote, a forum count nobody published. Held
267
- // briefly, so the edge absorbs the asking; briefly, so a document published a
268
- // moment later is not "missing" for long.
269
- const MISSING_EDGE_SECONDS = 120;
270
-
271
- // Where a pod owner opts their identity in. An operator may name it, because
272
- // the path it takes is one their pod server can no longer serve; the dot says
273
- // it belongs to the server rather than to anybody's data, the way
274
- // `.well-known` and `.account` do.
275
- export const DEFAULT_RUN_PATH = '/.fediverse-account';
276
- export const runPath = (ctx) => {
277
- const said = String(ctx?.runPath || '').trim().replace(/\/+$/u, '');
278
- if (!said) return DEFAULT_RUN_PATH;
279
- return said.startsWith('/') ? said : `/${said}`;
280
- };
281
-
282
- // A request the browser itself says came from this very origin. `Sec-Fetch-Site`
283
- // is the browser's word and a page cannot forge it; an `Origin` that matches is
284
- // the older way of saying the same. This is what makes it safe to read a
285
- // session cookie as proof: a page on another site gets neither.
286
- const sameOriginRequest = (request, origin) => {
287
- const site = request.headers.get('sec-fetch-site');
288
- if (site) return site === 'same-origin';
289
- const said = request.headers.get('origin');
290
- if (!said || !origin) return false;
291
- try { return new URL(said).origin === new URL(origin).origin; } catch { return false; }
292
- };
293
-
294
- // Top-level paths a handle may not take, so a name never shadows a route.
295
- const RESERVED = new Set(['u', 'api', 'signup', 'new-account', 'run', 'admin', 'roster', 'gateway',
296
- 'gw', 'well-known', 'inbox', 'outbox', 'actor', 'install', 'notices']);
297
-
298
- // Why a handle is unusable, or null when it is fine. Lowercase letters, digits
299
- // and hyphens; 2–30 chars; not edge-hyphenated; not a reserved route.
300
- function handleProblem(h) {
301
- if (!h) return 'empty';
302
- if (!/^[a-z0-9-]{2,30}$/.test(h)) return 'letters, digits and hyphens only, 2–30 characters';
303
- if (h.startsWith('-') || h.endsWith('-')) return 'cannot start or end with a hyphen';
304
- if (RESERVED.has(h)) return 'that name is reserved';
305
- return null;
306
- }
307
-
308
- // The person's own filtering policy, published on their pod and readable by
309
- // anyone: their accepted following list and a mirror of their blocklist. A
310
- // directory row carries neither — the row is written at attach time, before
311
- // the agent has published anything — so without this the door would judge by
312
- // addressing alone and forward the junk a blocklist exists to stop.
313
- //
314
- // Cached briefly: a delivery flood must not become a read per delivery on the
315
- // person's pod, which is the load this whole door exists to spare them.
316
- const POLICY_TTL_MS = 5 * 60_000;
317
- const policyCache = new Map(); // podHome -> { at, policy }
318
-
319
- // The cache is OURS, not the library's: on a warm serverless container one Map
320
- // is shared across every user this front serves, so what it is keyed by is a
321
- // correctness property. Keyed by podHome, it is one person's policy under one
322
- // person's key.
323
- const policyFor = (rec, fetchImpl = fetch) =>
324
- podPolicy.read(fetchImpl, rec.podHome, { cache: policyCache, ttlMs: POLICY_TTL_MS });
325
-
326
- // The verified-delivery identity for a directory record: what gateway-core
327
- // needs to check what concerns the user and to append to their pod inbox.
328
- // `policy` is the person's published filtering policy when there is one.
329
- function identFor(rec, policy = null) {
330
- return {
331
- // A row may name the inbox its deliveries are written to: a forum's
332
- // categories share one, on the forum's pod, above their own trees.
333
- inboxUrl: rec.inboxUrl || rec.podHome + 'ap/inbox/',
334
- actorUrl: rec.actorUrl, // the FRONT actor id (what mail is addressed to)
335
- followersUrl: rec.actorUrl.replace(/actor$/, 'followers'),
336
- notesPrefix: rec.actorUrl.replace(/ap\/actor$/, 'ap/notes/'),
337
- following: policy?.following || rec.following || [],
338
- blocklist: policy?.blocklist || rec.blocklist || { domains: [], actors: [] },
339
- kind: policy?.kind || rec.kind || 'person',
340
- gatewayWebId: rec.gatewayWebId,
341
- hmacSecret: rec.hmacSecret,
342
- };
343
- }
344
-
345
- // The single entry point. `ctx`:
346
- // host the front's own host, e.g. "fedipod.net" (for WebFinger subjects)
347
- // frontOrigin "https://fedipod.net"
348
- // lookup(handle) -> record | null the directory
349
- // listDirectory() -> { handle: record } every row, for the admin roster
350
- // removeDirectory(handle) -> boolean drop a row; false when a seeded row remains
351
- // podPut(url, body, ct) -> boolean append to a user's pod (per-user cred inside)
352
- // readReceived / writeReceived / dropReceived, pauseItems, closeDays — see quiet.mjs
353
- // podGet(url, { podHome }) -> Response read a user's pod (public reads; plain fetch
354
- // is fine). `podHome` is the row's own pod, for
355
- // an adapter that reads a store directly and so
356
- // must confine the read itself.
357
- export async function routeFront(request, ctx) {
358
- const out = await route(request, ctx);
359
- return { ...out, headers: withSecurityHeaders(out.headers, out.body) };
360
- }
361
-
362
- async function route(request, ctx) {
363
- const url = new URL(request.url);
364
- const { pathname } = url;
365
-
366
- // The new-account page: the host's front door, forking solo / group /
367
- // manage. Served as a static page the adapter supplies; the page itself
368
- // calls the availability API below. It only COLLECTS intent — creating an
369
- // account is the Solid-OIDC attach step (proving the pod is the user's).
370
- if (pathname === '/' || pathname === '/signup' || pathname === '/new-account') {
371
- if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
372
- if (!ctx.signupPage) return notFound();
373
- return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: ctx.signupPage };
374
- }
375
-
376
- // The run page: a pod owner opting their identity in or out of being run by
377
- // this server. Meaningful only on a host that embeds the agent; a host that
378
- // does not simply supplies no page.
379
- if (pathname === runPath(ctx)) {
380
- if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
381
- if (!ctx.runPage) return notFound();
382
- return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' }, body: ctx.runPage };
383
- }
384
-
385
- // The roster page (/roster, off /admin, which is the owner's own record
386
- // page on every agent) is served with the notices page, in notices.mjs.
387
-
388
- // Notices from the operator to every account here, and the page they are
389
- // written on (notices.mjs).
390
- const notices = await routeNoticesApi(request, pathname, ctx, { j, verifyPodToken, publicFor, notFound });
391
- if (notices) return notices;
392
-
393
- // The roster: every directory row, secrets stripped, for the host's own
394
- // eyes. The reader proves themself the way attach proves a pod — a
395
- // Solid-OIDC token — and must be the WebID the deploy names as admin.
396
- if (pathname === '/api/roster') {
397
- if (!ctx.listDirectory || !ctx.adminWebId) return j(501, { error: 'this front has no roster to offer' });
398
- const webid = await verifyPodToken(request, pathname, ctx.verifier);
399
- if (!webid) return j(401, { error: 'a Solid-OIDC token is required' });
400
- if (webid !== ctx.adminWebId) return j(403, { error: 'that WebID is not the admin of this front' });
401
- const rows = await ctx.listDirectory();
402
- const accounts = Object.values(rows)
403
- .map((r) => ({
404
- handle: r.handle, kind: r.kind || 'person', fronted: !r.inboxOnly,
405
- podHome: r.podHome, webId: r.webId || null, actorUrl: r.actorUrl,
406
- address: r.address || `@${r.handle}@${ctx.host}`,
407
- openedAt: r.openedAt || null, pausedAt: r.pausedAt || null, closedAt: r.closedAt || null,
408
- }))
409
- .sort((a, b) => (a.address || a.handle).localeCompare(b.address || b.handle));
410
- return j(200, { host: ctx.host, accounts });
411
- }
412
-
413
- // Revoke: the admin removes an account's row, so the server stops answering
414
- // for the name. Nothing on the user's pod is touched. Attach-created rows go
415
- // for good; a row seeded in the deploy's environment can only be removed there.
416
- if (pathname === '/api/revoke' && request.method === 'POST') {
417
- if (!ctx.removeDirectory || !ctx.adminWebId) return j(501, { error: 'this front cannot revoke accounts' });
418
- const webid = await verifyPodToken(request, pathname, ctx.verifier);
419
- if (!webid) return j(401, { error: 'a Solid-OIDC token is required' });
420
- if (webid !== ctx.adminWebId) return j(403, { error: 'that WebID is not the admin of this front' });
421
- let body;
422
- try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
423
- // Revoke by full address (what the roster lists); a bare handle still works
424
- // for any legacy row keyed that way.
425
- const key = String(body.address || body.handle || '').replace(/^@/, '').toLowerCase();
426
- if (!key || !(await ctx.lookup(key))) return j(404, { error: 'no such account' });
427
- const removed = (await ctx.removeDirectory(key)) === true;
428
- return j(200, removed ? { account: key, removed }
429
- : { account: key, removed: false,
430
- reason: 'this row is seeded in the deploy environment (FEDIPOD_DIRECTORY_JSON) — remove it there and redeploy' });
431
- }
432
-
433
- // Live handle check for the page: valid shape AND not already in the
434
- // directory. Also tells the page whether this host offers pods.
435
- if (pathname === '/api/handle') {
436
- const h = (url.searchParams.get('handle') || '').toLowerCase();
437
- const problem = handleProblem(h);
438
- if (problem) {
439
- return j(200, { handle: h, available: false, reason: problem,
440
- offersPods: !!ctx.offersPods, version: ctx.version || null });
441
- }
442
- const taken = !!(await ctx.lookup(h));
443
- return j(200, { handle: h, available: !taken, reason: taken ? 'that name is taken' : null,
444
- offersPods: !!ctx.offersPods, version: ctx.version || null });
445
- }
446
-
447
- // What a reader's own server speaks, asked here rather than in their
448
- // browser: a page cannot probe a stranger's server (no CORS), and the
449
- // answer decides how that reader takes part in a forum.
450
- if (pathname === '/api/server') {
451
- const host = (url.searchParams.get('host') || '').trim().toLowerCase();
452
- const open = { 'access-control-allow-origin': '*' };
453
- const said = (kind, extra = {}) => ({ ...j(200, { host, kind, ...extra }), headers: { ...j(200, {}).headers, ...open } });
454
- // An address at this front is a FediPod account: its client is the
455
- // browser here, and no other server holds it. Asked before the shape of
456
- // the host, since a front may answer at a name with no dot in it.
457
- if (host && host === String(ctx.host || '').toLowerCase()) {
458
- const handle = (url.searchParams.get('handle') || '').toLowerCase();
459
- const rec = handle ? await ctx.lookup(handle) : null;
460
- if (!rec) return said('fedipod');
461
- // Where the owner of this pod signs in: the pod says so itself, at the
462
- // well-known document every Solid server answers (or redirects to).
463
- let issuer = null;
464
- try {
465
- const res = await safeFetch(new URL('/.well-known/openid-configuration', rec.podHome).href,
466
- { headers: { accept: 'application/json' }, redirect: 'follow', signal: AbortSignal.timeout(SERVER_PROBE_MS) },
467
- ctx.fetchImpl || fetch);
468
- if (res.ok) issuer = JSON.parse(await readCapped(res, SERVER_PROBE_BYTES)).issuer || null;
469
- } catch { /* a pod that will not say leaves the reader to type it */ }
470
- return said('fedipod', { podHome: rec.podHome, actor: rec.actorUrl, issuer, inbox: identFor(rec).inboxUrl });
471
- }
472
- if (!/^[a-z0-9.-]+\.[a-z0-9-]+(:\d+)?$/u.test(host)) return said('invalid');
473
- const f = ctx.fetchImpl || fetch;
474
- const ask = async (path) => {
475
- try {
476
- const res = await safeFetch(`https://${host}${path}`,
477
- { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(SERVER_PROBE_MS) }, f);
478
- if (!res.ok) return null;
479
- return JSON.parse(await readCapped(res, SERVER_PROBE_BYTES));
480
- } catch { return null; }
481
- };
482
- const masto = await ask('/api/v1/instance');
483
- if (masto) return said('mastodon-api', { name: masto.title || masto.domain || host });
484
- const lemmy = await ask('/api/v3/site');
485
- if (lemmy) return said('lemmy', { name: lemmy.site_view?.site?.name || host });
486
- return said('unknown');
487
- }
488
-
489
- // Attach: create the account. The user proves control of the pod they claim
490
- // by presenting a Solid-OIDC token whose WebID lives on that pod — no
491
- // password reaches the front. On success the directory gets a row and the
492
- // agent gets a one-time HMAC secret to stamp the receipts its front writes.
493
- if (pathname === '/api/attach' && request.method === 'POST') {
494
- if (!ctx.putDirectory) return j(501, { error: 'this front does not accept signups' });
495
- let body;
496
- try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
497
- const handle = String(body.handle || '').toLowerCase();
498
- const podHome = String(body.podHome || '');
499
- const problem = handleProblem(handle);
500
- if (problem) return j(400, { error: problem });
501
- const podLoopback = (() => { try { return isLoopbackHost(new URL(podHome).hostname); } catch { return false; } })();
502
- if (!(/^https:\/\/\S+\/$/.test(podHome) || (/^http:\/\/\S+\/$/.test(podHome) && podLoopback))) {
503
- return j(400, { error: 'podHome must be an https URL ending in /' });
504
- }
505
- const badPod = podHomeProblem(podHome, ctx.frontOrigin);
506
- if (badPod) return j(400, { error: badPod });
507
- const webid = await verifyPodToken(request, pathname, ctx.verifier);
508
- if (!webid) return j(401, { error: 'a Solid-OIDC token proving the pod is required' });
509
- if (!await provesPod(webid, podHome, ctx.fetchImpl)) {
510
- return j(403, { error: 'the token proves a different pod than the one you listed' });
511
- }
512
- // The usual attachment keeps the user's identity on their pod and moves
513
- // only the mail door here (@me@mypod). Fronted identity — actor ids on
514
- // this domain — remains available to an explicit `fronted: true`.
515
- const fronted = body.fronted === true;
516
- // The full address is the key: a fronted name answers at this front's host,
517
- // a mail-door name at its pod's host. The same handle on two pods is two
518
- // distinct addresses, so it never collides.
519
- let addressHost; try { addressHost = fronted ? ctx.host : new URL(podHome).host; } catch { return j(400, { error: 'bad podHome' }); }
520
- // A fronted name answers at this front's host, where the handle alone is the
521
- // whole address and is unique — so it keeps keying by bare handle (and its
522
- // actor id stays /u/<handle>/, unchanged for existing accounts). A mail-door
523
- // name answers at its own pod, where "me" is not unique, so it keys by the
524
- // full address handle@podhost, which the /u/ segment then carries.
525
- const key = fronted ? handle : addressKey(handle, addressHost);
526
- const address = `@${handle}@${addressHost}`;
527
- // "Taken" only when this exact address already belongs to a DIFFERENT pod.
528
- // Re-attaching your own pod — a retry, a re-provision, a second browser — is
529
- // idempotent: it updates the row in place and keeps the same door secret.
530
- const prior = await ctx.lookup(key);
531
- // The same account on the same pod may correct where its tree lives — a
532
- // row made by an older sign-up named the pod root, and every delivery
533
- // to it was written where nothing reads. A different pod is still taken.
534
- const sameOwner = (a, b) => { try { return new URL(a).origin === new URL(b).origin; } catch { return false; } };
535
- if (prior && prior.podHome !== podHome
536
- && !(prior.webId && prior.webId === webid && sameOwner(prior.podHome, podHome))) {
537
- return j(409, { error: 'that name is taken' });
538
- }
539
- // Re-attaching is idempotent — a retry, a re-provision, a second browser —
540
- // and it hands back the EXISTING receipt secret. So it has to be the same
541
- // person: matching podHome was enough on a subdomain server and, on a
542
- // path-based one, meant any co-tenant could re-attach your row, flip its
543
- // kind, and be given your secret (S7). A row that predates `webId` has
544
- // nothing to compare and falls back to the pod check above.
545
- if (prior?.webId && prior.webId !== webid) {
546
- return j(409, { error: 'that name belongs to a different account on this pod' });
547
- }
548
- // The pod's actor lives under its AP root container, which the pod owner
549
- // knows and the front does not — so take the caller's actorUrl when it is on
550
- // this pod, and fall back only when none is given.
551
- const claimedActor = typeof body.actorUrl === 'string' && body.actorUrl.startsWith(podHome) ? body.actorUrl : null;
552
- const actorUrl = fronted ? `${userBase(ctx.frontOrigin, key)}ap/actor` : (claimedActor || podHome + 'ap/actor');
553
- const hmacSecret = prior?.hmacSecret || crypto.randomBytes(32).toString('base64');
554
- // Where the door writes: the row's own tree unless the caller names an
555
- // inbox on the same pod — a forum's categories name the forum's.
556
- let inboxUrl = null;
557
- if (typeof body.inboxUrl === 'string' && body.inboxUrl) {
558
- let ok = false;
559
- try { ok = new URL(body.inboxUrl).origin === new URL(podHome).origin && body.inboxUrl.endsWith('/'); } catch { ok = false; }
560
- if (!ok) return j(400, { error: 'inboxUrl must be a container on the same pod' });
561
- inboxUrl = body.inboxUrl;
562
- }
563
- const record = {
564
- handle, address, podHome, webId: webid, actorUrl,
565
- kind: body.kind === 'group' ? 'group' : body.kind === 'application' ? 'application' : 'person',
566
- gatewayWebId: ctx.gatewayWebId || null, hmacSecret,
567
- ...(inboxUrl ? { inboxUrl } : {}),
568
- ...(fronted ? {} : { inboxOnly: true }),
569
- };
570
- await ctx.putDirectory(key, record);
571
- // The secret is returned ONCE, with the one command the user runs to
572
- // point their agent at this gateway.
573
- const doorInbox = `${userBase(ctx.frontOrigin, key)}ap/inbox/`;
574
- if (fronted) {
575
- return j(201, { ok: true, handle, address, actorUrl, frontActor: actorUrl, hmacSecret,
576
- command: `fedipod gateway ${actorUrl} --secret ${hmacSecret}` });
577
- }
578
- return j(201, { ok: true, handle, address, doorInbox, hmacSecret,
579
- command: `fedipod gateway ${doorInbox} --secret ${hmacSecret} --inbox-only` });
580
- }
581
-
582
- // An address at this gateway moving to another one. Its owner, proved the
583
- // way attach proves a pod, says where it went. From then on this front
584
- // answers the old actor as a moved stub — the same key under the old id,
585
- // `movedTo` the new one, so the Move the owner sends from the old address
586
- // verifies — every other old id redirects to its new address, and the
587
- // mail door is shut: deliveries belong at the new one. Repeating it with
588
- // the same target changes nothing.
589
- if (pathname === '/api/move' && request.method === 'OPTIONS') return apiPreflight();
590
- if (pathname === '/api/move' && request.method === 'POST') {
591
- if (!ctx.putDirectory) return withApiCors(j(501, { error: 'this front keeps no directory' }));
592
- let body;
593
- try { body = JSON.parse(await request.clone().text()); } catch { return withApiCors(j(400, { error: 'bad JSON' })); }
594
- const handle = String(body.handle || '').toLowerCase();
595
- const rec = await ctx.lookup(handle);
596
- if (!rec) return withApiCors(j(404, { error: 'no such account' }));
597
- if (rec.inboxOnly) return withApiCors(j(400, { error: 'this address lives on its pod; only an address at this gateway can move' }));
598
- const movedTo = String(body.movedTo || '');
599
- let mu;
600
- try { mu = new URL(movedTo); } catch { return withApiCors(j(400, { error: 'movedTo must be the new actor URL' })); }
601
- if (!((mu.protocol === 'https:' || (mu.protocol === 'http:' && isLoopbackHost(mu.hostname))) && /\/ap\/actor$/u.test(mu.pathname))) {
602
- return withApiCors(j(400, { error: 'movedTo must be an https …/ap/actor URL' }));
603
- }
604
- if (ctx.frontOrigin && mu.origin === new URL(ctx.frontOrigin).origin) return withApiCors(j(400, { error: 'that address is at this gateway' }));
605
- const webid = await verifyPodToken(request, pathname, ctx.verifier);
606
- if (!webid) return withApiCors(j(401, { error: 'a Solid-OIDC token proving the pod is required' }));
607
- const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
608
- if (!owner) return withApiCors(j(403, { error: "the token proves a different pod than this account's" }));
609
- const movedAt = rec.movedTo === movedTo && rec.movedAt ? rec.movedAt : new Date().toISOString();
610
- await ctx.putDirectory(handle, { ...rec, movedTo, movedAt });
611
- console.log(`front @${handle}: moved to ${movedTo}`);
612
- return withApiCors(j(200, { ok: true, handle, movedTo, movedAt }));
613
- }
614
-
615
- // The owner's say over an account that goes quiet (quiet.mjs).
616
- const quiet = await routeQuietApi(request, pathname, ctx, { j, verifyPodToken, webidUnderPod, apiPreflight });
617
- if (quiet) return withApiCors(quiet);
618
-
619
- // The relay: the front sends requests a browser has already signed. A page
620
- // may not set the Date or Host header, and both are inside an HTTP
621
- // signature, so a browser-run agent signs and hands the request here; the
622
- // front sends it with exactly the headers that were signed. It can forward
623
- // a signature and cannot make one. Only the account's own key may sign,
624
- // only the account's own pod may call, and nothing unsigned-for reaches
625
- // the remote server.
626
- if (pathname === '/api/relay' && request.method === 'OPTIONS') return apiPreflight();
627
- if (pathname === '/api/relay' && request.method === 'POST') {
628
- let body;
629
- try { body = JSON.parse(await request.clone().text()); } catch { return withApiCors(j(400, { error: 'bad JSON' })); }
630
- const handle = String(body.handle || '').toLowerCase();
631
- const rec = await ctx.lookup(handle);
632
- if (!rec) { console.log(`relay: no account named ${handle}`); return withApiCors(j(404, { error: 'no such account' })); }
633
- const webid = await verifyPodToken(request, pathname, ctx.verifier);
634
- if (!webid) { console.log(`relay @${handle}: no usable pod token`); return withApiCors(j(401, { error: 'a Solid-OIDC token proving the pod is required' })); }
635
- const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
636
- if (!owner) { console.log(`relay @${handle}: token is for ${webid}, not this account's pod`); return withApiCors(j(403, { error: "the token proves a different pod than this account's" })); }
637
- const items = Array.isArray(body.requests) ? body.requests : [];
638
- if (!items.length) return withApiCors(j(400, { error: 'requests must be a non-empty list' }));
639
- // Acting through the relay is being here. Hourly at most; see noteOpened.
640
- await noteOpened(ctx, handle, rec).catch((e) => console.log(`relay @${handle}: stamp not written: ${e?.message || e}`));
641
- if (items.length > RELAY_MAX_REQUESTS) return withApiCors(j(400, { error: `at most ${RELAY_MAX_REQUESTS} requests per call` }));
642
- const results = await Promise.all(items.map((it) => relayOne(it, rec, ctx.fetchImpl || fetch)));
643
- // One line per relayed request, so a lookup that fails on the far side is
644
- // visible here and not only as an empty result in someone's browser.
645
- for (const r of results) console.log(`relay @${handle}: ${r.method || ''} ${r.url} → ${r.status}${r.error ? ` (${r.error})` : ''}`);
646
- return withApiCors(j(200, { results }));
647
- }
648
-
649
- // Who is here, and which pod of theirs could opt in. The opt-in page asks
650
- // this first: on a pod server that issued the reader's session, the answer
651
- // names their pod and its address, and the page shows one button instead of
652
- // a form. A Gateway holds no such session and says so; the page then asks
653
- // for the pod by hand and proves it with a sign-in, as before.
654
- if (pathname === '/api/agent' && request.method === 'GET') {
655
- if (!ctx.agentControl) return j(501, { error: 'this server does not run identities' });
656
- if (!ctx.webIdsFromSession) return j(200, { offered: true, session: 'unknown' });
657
- if (!sameOriginRequest(request, ctx.frontOrigin)) return j(200, { offered: true, session: 'none' });
658
- const webIds = await ctx.webIdsFromSession(request).catch(() => []);
659
- if (!webIds.length) return j(200, { offered: true, session: 'none', loginUrl: '/.account/login/password/' });
660
- const pods = [];
661
- for (const webId of webIds) {
662
- let podBase;
663
- try { podBase = podBaseOfWebId(webId); } catch { continue; }
664
- let about = null;
665
- try { about = ctx.agentControl.describe ? await ctx.agentControl.describe({ podBase }) : null; } catch { about = null; }
666
- pods.push({ webId, podBase, ...(about || {}) });
667
- }
668
- return j(200, { offered: true, session: 'ok', pods });
669
- }
670
-
671
- // Runtime opt-in: a pod owner asks the server this front sits on to RUN
672
- // their identity. Same proof as attach — a Solid-OIDC token — but the
673
- // ownership check is stricter: the WebID must live UNDER the pod base, not
674
- // merely on its origin, or on a path-pod server one user could opt in
675
- // another's pod. Offered only where the deployment wired agentControl in
676
- // (a pod server running the CSS component); everywhere else it is 501.
677
- if (pathname === '/api/agent' && request.method === 'POST') {
678
- if (!ctx.agentControl) return j(501, { error: 'this server does not run identities' });
679
- let body;
680
- try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
681
- const action = String(body.action || '');
682
- const podBase = String(body.podBase || '');
683
- if (!/^https?:\/\/\S+\/$/.test(podBase)) {
684
- return j(400, { error: 'podBase must be a URL ending in /' });
685
- }
686
- let podUrl;
687
- try { podUrl = new URL(podBase); } catch { return j(400, { error: 'podBase is not a URL' }); }
688
- // A pod may be host-root (its own origin) or on a PATH of a shared host — a
689
- // suffix pod, which the Server runs at `https://server/aisha/`. What is
690
- // refused is the gateway's own origin root itself: that is where the
691
- // gateway lives, not a pod, and admitting it would (with the ownership
692
- // fallback below) let a co-tenant claim the whole origin.
693
- if (ctx.frontOrigin) {
694
- try {
695
- if (podUrl.origin === new URL(ctx.frontOrigin).origin && podUrl.pathname === '/') {
696
- return j(403, { error: 'that is this gateway, not a pod — name the pod that holds your data' });
697
- }
698
- } catch { /* no usable frontOrigin: the checks below still apply */ }
699
- }
700
- if (/(^|\/)\.internal(\/|$)/u.test(podUrl.pathname) || /(^|\/)\.\.(\/|$)/u.test(podUrl.pathname)) {
701
- return j(403, { error: 'that is not a pod address' });
702
- }
703
- // The pod's own server names its owner when it can, and where it does that
704
- // is the proof. Where it does not, the WebID must live in EXACTLY this pod
705
- // — its own pod base equal to podBase, not merely starting with it, which on
706
- // a path server an ancestor of another's pod would.
707
- const owners = await podOwners(podBase, ctx.fetchImpl || fetch);
708
- const ownsIt = (who) => (owners.length ? owners.includes(who) : podBaseOfWebId(who) === podBase);
709
- const token = await verifyPodToken(request, pathname, ctx.verifier);
710
- // A server that issued the reader's own session can simply read it. On a
711
- // pod server the page, the session and the pod are one origin, and a token
712
- // proves nothing the session does not — so opting in cost a round trip to
713
- // an identity provider that had just said who they were. A Gateway fronting
714
- // somebody else's pod holds no such session and this does nothing there.
715
- //
716
- // One account can hold several WebIDs, so it is the one that owns this pod
717
- // that is taken, not whichever came back first.
718
- const session = !token && ctx.webIdsFromSession && sameOriginRequest(request, ctx.frontOrigin)
719
- ? (await ctx.webIdsFromSession(request).catch(() => [])).find(ownsIt) || null
720
- : null;
721
- const webid = token || session;
722
- if (!webid) return j(401, { error: 'a Solid-OIDC token proving the pod is required' });
723
- if (!ownsIt(webid)) {
724
- return j(403, { error: 'the token proves a different pod than the one you listed' });
725
- }
726
- if (action === 'opt-in') {
727
- const { httpStatus, ...reply } = await ctx.agentControl.optIn({ podBase, webId: webid });
728
- if (reply.doorSecret) {
729
- // The secret appears here and nowhere else; the command is the
730
- // paste-and-run way to open the owner door once.
731
- reply.command = `curl -H 'x-dk-token: ${reply.doorSecret}' ${podBase.replace(/\/$/, '')}${reply.doorPath}status`;
732
- }
733
- return j(httpStatus, reply);
734
- }
735
- if (action === 'opt-out') {
736
- const { httpStatus, ...reply } = await ctx.agentControl.optOut({ podBase });
737
- return j(httpStatus, reply);
738
- }
739
- return j(400, { error: 'action must be opt-in or opt-out' });
740
- }
741
-
742
- // Each page's own script. They were inline until 2026-09-09; out here the
743
- // pages can be served under `script-src 'self'`, which is the directive that
744
- // tells running code from injected markup — and inline script defeats it by
745
- // construction. Same mechanism the vendored library below has always used.
746
- //
747
- // The name is matched against a literal list, so nothing about the request
748
- // chooses a file.
749
- const mPageScript = /^\/(new-account|run|admin|notices)\.js$/u.exec(pathname);
750
- if (mPageScript) {
751
- const body = ctx.pageScripts?.[`${mPageScript[1]}.js`];
752
- if (!body) return notFound();
753
- // Revalidated on every load: a page served fresh with a script the
754
- // browser kept from last time is the mismatch nobody can see.
755
- return { status: 200, headers: { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-cache' }, body };
756
- }
757
-
758
- // The vendored Solid-OIDC browser library the opt-in and admin pages load —
759
- // served here because this function owns every path on the domain.
760
- if (pathname === '/solid-oidc-client.js') {
761
- if (!ctx.authBundle) return notFound();
762
- return { status: 200, headers: { 'content-type': 'text/javascript' }, body: ctx.authBundle };
763
- }
764
-
765
- // The installer: curl -fsSL https://<host>/install | sh
766
- if (pathname === '/install') {
767
- if (!ctx.installScript) return notFound();
768
- return { status: 200, headers: { 'content-type': 'text/plain; charset=utf-8' }, body: ctx.installScript };
769
- }
770
-
771
- // WebFinger: acct:<handle>@<host> → the fronted actor. This is what makes
772
- // @name@fedipod.net a real handle the fediverse can look up.
773
- if (pathname === '/.well-known/webfinger') {
774
- const resource = url.searchParams.get('resource') || '';
775
- const m = /^acct:([^@]+)@(.+)$/.exec(resource);
776
- if (!m || m[2] !== ctx.host) return notFound();
777
- const rec = await ctx.lookup(m[1]);
778
- if (!rec) return notFound();
779
- // A closed address is gone, and says so rather than pretending never to
780
- // have existed: the name stays taken.
781
- if ((await closedState(ctx, m[1], rec)).closed) return closedAnswer({ 'access-control-allow-origin': '*', ...publicFor(GONE_EDGE_SECONDS) });
782
- // A fronted identity's documents live on its pod; the pod's own actor id
783
- // is the alias, so a client signing in by the fronted address can find
784
- // the pod (and its login) without a lookup only the host could answer.
785
- const podActor = rec.inboxOnly ? [] : [rec.podHome + 'ap/actor'];
786
- const out = j(200, jrd({ handle: m[1], host: ctx.host, actor: rec.actorUrl, aliases: podActor,
787
- page: profilePageOf(rec), subscribe: subscribeTemplate(ctx.frontOrigin) }),
788
- 'application/jrd+json');
789
- // RFC 7033: a JRD is read from any origin.
790
- out.headers['access-control-allow-origin'] = '*';
791
- // A handle answers the same for everybody, and changes only when a row
792
- // does. Every server that has heard of this account asks for it; the edge
793
- // can answer most of them.
794
- Object.assign(out.headers, publicFor(WEBFINGER_EDGE_SECONDS));
795
- return out;
796
- }
797
-
798
- // The short address a person types: https://<host>/@handle opens the
799
- // profile page, the way most Fediverse servers spell it. /@handle@pod-host
800
- // is the same for an account whose address is its pod's, which the front
801
- // holds under that full address — the form other servers use for a
802
- // profile that lives elsewhere.
803
- const at = /^\/@([^/@]+)(?:@([^/@]+))?$/u.exec(pathname);
804
- if (at) {
805
- const handle = decodeURIComponent(at[1]).toLowerCase();
806
- const host = at[2] ? decodeURIComponent(at[2]).toLowerCase() : null;
807
- const key = host && host !== String(ctx.host).toLowerCase() ? `${handle}@${host}` : handle;
808
- const rec = await ctx.lookup(key);
809
- const page = rec && profilePageOf(rec);
810
- if (!page) return notFound();
811
- return { status: 302, headers: { location: page, 'cache-control': 'no-store' }, body: '' };
812
- }
813
-
814
- const up = parseUserPath(pathname);
815
- if (!up) return notFound();
816
- const rec = await ctx.lookup(up.handle);
817
- if (!rec) return notFound();
818
- const base = userBase(ctx.frontOrigin, up.handle);
819
- // An account that moved to another gateway (see /api/move): where its ids
820
- // live now. The actor below answers as a stub; everything else redirects.
821
- const movedBase = rec.movedTo ? rec.movedTo.replace(/ap\/actor$/u, '') : null;
822
- // A closed address (see "accounts that go quiet" above): every id under it
823
- // is gone. Asked after moved, which a close does not undo.
824
- const closed = movedBase ? false : (await closedState(ctx, up.handle, rec)).closed;
825
- const gone = (headers = {}, why = `this account has moved to ${rec.movedTo}`) => closedAnswer(headers, why);
826
- const CLOSED = 'this address is closed';
827
-
828
- // Inbox: verify at the door, forward to the user's pod inbox. This is the
829
- // gateway, per user.
830
- if (up.rest === 'ap/inbox/' || up.rest === 'ap/inbox') {
831
- if (request.method !== 'POST') return { status: 405, headers: {}, body: '' };
832
- if (movedBase) { console.log(`door @${up.handle}: delivery → 410 (moved)`); return gone(); }
833
- if (closed) { console.log(`door @${up.handle}: delivery → 410 (closed)`); return gone({}, CLOSED); }
834
- const policy = await policyFor(rec, ctx.fetchImpl || fetch);
835
- const standing = await accountState(ctx, up.handle, rec);
836
- const { status, reason, content, bytes } = await handleDelivery(request, identFor(rec, policy),
837
- { podPut: (u, b, ct) => ctx.podPut(up.handle, u, b, ct), fetchImpl: ctx.fetchImpl, paused: standing.paused });
838
- // One line per delivery, so "did it arrive at the door" has an answer.
839
- console.log(`door @${up.handle}: delivery → ${status} (${reason})`);
840
- if (status === 202 && content) await noteReceived(ctx, up.handle, rec, bytes);
841
- return { status, headers: {}, body: '' };
842
- }
843
-
844
- // Outbox: the owner's own post, from any client that speaks ActivityPub
845
- // client-to-server (dokieli, say). Cross-origin by nature — such a client is
846
- // a page on another site — so this route answers a preflight and carries
847
- // CORS headers; the owner's pod token is the credential, so any origin may
848
- // ask. GET stays a read: the pod's document for a fronted account, sent on
849
- // to the pod for a door account whose documents live at their own address.
850
- if (up.rest === 'ap/outbox') {
851
- const cors = {
852
- 'access-control-allow-origin': '*',
853
- 'access-control-allow-methods': 'GET, POST, OPTIONS',
854
- 'access-control-allow-headers': 'Authorization, DPoP, Content-Type, Slug, Link',
855
- 'access-control-expose-headers': 'Location, Accept-Post',
856
- 'access-control-max-age': '86400',
857
- };
858
- if (request.method === 'OPTIONS') {
859
- // Accept-Post names JSON only: a client that reads it (dokieli) sends
860
- // JSON-LD when HTML is not offered.
861
- // body null, not '': a Response refuses any body on a 204, and the
862
- // function adapter hands `body` straight to one.
863
- return { status: 204, headers: { ...cors, allow: 'GET, POST, OPTIONS',
864
- 'accept-post': 'application/ld+json, application/activity+json' }, body: null };
865
- }
866
- if (movedBase && request.method === 'POST') return gone(cors);
867
- if (closed && request.method === 'POST') return gone(cors, CLOSED);
868
- if (movedBase && (request.method === 'GET' || request.method === 'HEAD')) {
869
- return { status: 301, headers: { ...cors, location: movedBase + up.rest, 'cache-control': 'no-store' }, body: '' };
870
- }
871
- if (request.method === 'POST') {
872
- const json = (status, obj, extra = {}) => ({ status,
873
- headers: { ...cors, 'content-type': 'application/json', 'cache-control': 'no-store', ...extra },
874
- body: JSON.stringify(obj) });
875
- const webid = await verifyPodToken(request, pathname, ctx.verifier);
876
- if (!webid) return json(401, { error: 'a Solid-OIDC token proving this account\'s owner is required' });
877
- const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
878
- if (!owner) return json(403, { error: 'this outbox belongs to its owner alone' });
879
- const { status, reason, location } = await handleOwnerPost(request, identFor(rec),
880
- { podPut: (u, b, ct) => ctx.podPut(up.handle, u, b, ct), ownerWebId: webid });
881
- console.log(`door @${up.handle}: owner post → ${status} (${reason})`);
882
- if (status !== 201) return json(status, { error: reason });
883
- return json(201, { accepted: true, ...(location ? { object: location } : {}),
884
- note: 'it goes out when your FediPod agent next runs' }, location ? { location } : {});
885
- }
886
- if (rec.inboxOnly && (request.method === 'GET' || request.method === 'HEAD')) {
887
- return { status: 303, headers: { ...cors, location: rec.podHome + 'ap/outbox', 'cache-control': 'no-store' }, body: '' };
888
- }
889
- }
890
-
891
- // Everything else is a public GET, served by reading the user's pod and
892
- // rewriting pod ids to the front. The actor also gets its handle and inbox
893
- // fixed to the front so a consumer cross-checks it consistently.
894
- // Public documents are read from any origin — a page on another site, the
895
- // forum's own host among them — the way a pod serves them, so the read
896
- // carries the open origin and a preflight is answered.
897
- const open = { 'access-control-allow-origin': '*' };
898
- if (request.method === 'OPTIONS') {
899
- return { status: 204, headers: { ...open, 'access-control-allow-methods': 'GET, HEAD, OPTIONS',
900
- 'access-control-allow-headers': 'Accept', 'access-control-max-age': '86400', allow: 'GET, HEAD, OPTIONS' }, body: null };
901
- }
902
- if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
903
- // Moved: every old id but the actor's answers with where it is now, so a
904
- // reply or a boost another server still holds under the old id resolves.
905
- if (movedBase && up.rest !== 'ap/actor') {
906
- return { status: 301, headers: { ...open, location: movedBase + up.rest, 'cache-control': 'no-store' }, body: '' };
907
- }
908
- if (closed) return gone({ ...open, ...publicFor(GONE_EDGE_SECONDS) }, CLOSED);
909
- const podTarget = rec.podHome + up.rest;
910
- // Media stays on the pod (lib/pod/urls.mjs keeps `media` off the front), but
911
- // the id rewrite below turns media links onto the front like every other
912
- // pod url in a document. Answer those by pointing at the pod: bytes are not
913
- // a document to cap and relabel, and remotes follow a redirect for a picture.
914
- if (up.rest.startsWith('ap/media/')) {
915
- return { status: 302, headers: { location: podTarget, ...publicFor(MEDIA_EDGE_SECONDS) }, body: '' };
916
- }
917
- // The pod this read belongs to travels with it: an adapter reading a store
918
- // directly (the CSS server component) has no access control of its own and
919
- // needs to be told what it may reach. See podHomeProblem above for the other
920
- // half — a row that names something which is not a pod.
921
- // A container is Turtle, a profile page is HTML, everything else here is
922
- // ActivityPub. Ask for what the thing can be, not for what most of them are.
923
- const container = podTarget.endsWith('/');
924
- const page = /\.html?$/u.test(podTarget);
925
- const accept = container ? 'text/turtle, */*;q=0.8'
926
- : page ? 'text/html, */*;q=0.8'
927
- : AP_CT;
928
- const got = await podRoot.readPublicDocument(
929
- ctx.podGet || ((u) => fetch(u, { headers: { accept } })),
930
- podTarget, { podHome: rec.podHome, accept });
931
- if (got.text === null) {
932
- const hold = [401, 403, 404, 410].includes(got.status) ? publicFor(MISSING_EDGE_SECONDS) : {};
933
- return { status: got.status, headers: { ...open, ...hold }, body: '' };
934
- }
935
- let text = got.text;
936
- text = swap(text, rec.podHome, base);
937
- // The moved stub: the pod's actor now carries the NEW gateway's ids, and a
938
- // server verifying the Move fetches the OLD id and needs a document whose
939
- // id is that old id — with the same key under it, and `movedTo` saying
940
- // where the account went. Swapping the new base back to this one gives
941
- // exactly that; alsoKnownAs and movedTo are set below.
942
- if (movedBase) text = swap(text, movedBase, base);
943
- if (up.rest === 'ap/actor') {
944
- try {
945
- const doc = JSON.parse(text);
946
- doc.preferredUsername = up.handle; // so @handle@front cross-checks
947
- doc.inbox = base + 'ap/inbox/'; // deliveries come to the front to be verified
948
- doc.endpoints = { ...(doc.endpoints || {}), sharedInbox: base + 'ap/inbox/' };
949
- if (movedBase) {
950
- doc.movedTo = rec.movedTo;
951
- const aka = new Set([].concat(doc.alsoKnownAs || []).filter((a) => typeof a === 'string' && a !== doc.id));
952
- aka.add(rec.movedTo);
953
- doc.alsoKnownAs = [...aka];
954
- const ctxs = [].concat(doc['@context'] || []);
955
- const declared = (term) => ctxs.some((c) => c && typeof c === 'object' && term in c);
956
- if (!declared('alsoKnownAs')) ctxs.push({ alsoKnownAs: { '@id': 'as:alsoKnownAs', '@type': '@id' } });
957
- if (!declared('movedTo')) ctxs.push({ movedTo: { '@id': 'as:movedTo', '@type': '@id' } });
958
- doc['@context'] = ctxs;
959
- }
960
- text = JSON.stringify(doc);
961
- } catch { /* leave the rewritten text as-is if it will not parse */ }
962
- }
963
- // `readPublicDocument` asked for it with no credential, so a document that
964
- // came back is one anybody can read — which is what makes it cacheable.
965
- return { status: 200,
966
- headers: { ...open, 'content-type': got.type || (container ? 'text/turtle' : page ? 'text/html' : AP_CT), ...publicFor(PUBLIC_EDGE_SECONDS) },
967
- body: text };
968
- }
969
-
970
- export const _internal = { parseUserPath, identFor, swap, handleProblem, policyFor, policyCache };