fedipod-server 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (444) hide show
  1. package/README.md +16 -6
  2. package/dist/claims.d.ts +2 -0
  3. package/dist/claims.js +13 -4
  4. package/dist/door.d.ts +4 -0
  5. package/dist/door.js +31 -0
  6. package/dist/fedipod.d.ts +15 -0
  7. package/dist/fedipod.js +13 -0
  8. package/dist/front.d.ts +3 -0
  9. package/dist/front.js +92 -0
  10. package/dist/handler.d.ts +36 -101
  11. package/dist/handler.js +93 -644
  12. package/dist/handler.jsonld +2 -30
  13. package/dist/identities.d.ts +71 -0
  14. package/dist/identities.js +236 -0
  15. package/dist/mounts.d.ts +43 -0
  16. package/dist/mounts.js +123 -0
  17. package/dist/opt-in.d.ts +46 -0
  18. package/dist/opt-in.js +176 -0
  19. package/dist/private-read.d.ts +4 -0
  20. package/dist/private-read.js +34 -0
  21. package/package.json +12 -20
  22. package/lib/client/c2s.mjs +0 -636
  23. package/lib/client/localapi.mjs +0 -87
  24. package/lib/client/masto/accounts.mjs +0 -310
  25. package/lib/client/masto/body.mjs +0 -156
  26. package/lib/client/masto/index.mjs +0 -206
  27. package/lib/client/masto/instance.mjs +0 -112
  28. package/lib/client/masto/media.mjs +0 -79
  29. package/lib/client/masto/oauth.mjs +0 -656
  30. package/lib/client/masto/render.mjs +0 -518
  31. package/lib/client/masto/statuses.mjs +0 -372
  32. package/lib/client/masto/timelines.mjs +0 -438
  33. package/lib/client/oidc-auth.mjs +0 -62
  34. package/lib/client/streaming.mjs +0 -154
  35. package/lib/client/webpush.mjs +0 -98
  36. package/lib/connections/acctfeed.mjs +0 -257
  37. package/lib/connections/atproto.mjs +0 -309
  38. package/lib/connections/bskyfeed.mjs +0 -243
  39. package/lib/connections/bskygroup.mjs +0 -162
  40. package/lib/connections/fediacct.mjs +0 -253
  41. package/lib/connections/import.mjs +0 -357
  42. package/lib/connections/tagfeed.mjs +0 -212
  43. package/lib/connections/vault.mjs +0 -114
  44. package/lib/core/as2.mjs +0 -206
  45. package/lib/core/contexts/activitystreams.json +0 -379
  46. package/lib/core/contexts/anno.json +0 -126
  47. package/lib/core/contexts/did-v1.json +0 -57
  48. package/lib/core/contexts/fep-5711.json +0 -36
  49. package/lib/core/contexts/gotosocial.json +0 -86
  50. package/lib/core/contexts/identity-v1.json +0 -152
  51. package/lib/core/contexts/index.mjs +0 -63
  52. package/lib/core/contexts/join-lemmy.json +0 -33
  53. package/lib/core/contexts/joinmastodon.json +0 -28
  54. package/lib/core/contexts/litepub-0.1.json +0 -60
  55. package/lib/core/contexts/map.json +0 -19
  56. package/lib/core/contexts/miscellany.json +0 -19
  57. package/lib/core/contexts/quotes.json +0 -18
  58. package/lib/core/contexts/schemaorg.json +0 -8845
  59. package/lib/core/contexts/security-data-integrity-v1.json +0 -78
  60. package/lib/core/contexts/security-data-integrity-v2.json +0 -81
  61. package/lib/core/contexts/security-multikey-v1.json +0 -35
  62. package/lib/core/contexts/security-v1.json +0 -74
  63. package/lib/core/contexts/webfinger.json +0 -10
  64. package/lib/core/deliver.mjs +0 -387
  65. package/lib/core/graphview.mjs +0 -327
  66. package/lib/core/intake/activities.mjs +0 -649
  67. package/lib/core/intake/activity.mjs +0 -318
  68. package/lib/core/intake/channel.mjs +0 -144
  69. package/lib/core/intake/group.mjs +0 -258
  70. package/lib/core/intake/index.mjs +0 -732
  71. package/lib/core/intake/notes.mjs +0 -367
  72. package/lib/core/intake/verify.mjs +0 -142
  73. package/lib/core/keys.mjs +0 -153
  74. package/lib/core/lease.mjs +0 -206
  75. package/lib/core/place.mjs +0 -82
  76. package/lib/core/polls.mjs +0 -105
  77. package/lib/core/profile-page.mjs +0 -104
  78. package/lib/core/proof.mjs +0 -79
  79. package/lib/core/publisher/collections.mjs +0 -272
  80. package/lib/core/publisher/index.mjs +0 -522
  81. package/lib/core/publisher/notes.mjs +0 -519
  82. package/lib/core/publisher/own.mjs +0 -143
  83. package/lib/core/publisher/questions.mjs +0 -241
  84. package/lib/core/publisher/restore.mjs +0 -224
  85. package/lib/core/shapes/activitystreams.ttl +0 -157
  86. package/lib/core/shapes/index.mjs +0 -132
  87. package/lib/core/shapes/shapes-text.mjs +0 -13
  88. package/lib/core/social.mjs +0 -561
  89. package/lib/core/storage.mjs +0 -176
  90. package/lib/core/store.mjs +0 -578
  91. package/lib/core/wire-quotes.mjs +0 -71
  92. package/lib/core/wire.mjs +0 -925
  93. package/lib/device/account.mjs +0 -87
  94. package/lib/device/admin/index.mjs +0 -13
  95. package/lib/device/admin/origins.mjs +0 -35
  96. package/lib/device/admin/routes/connections.mjs +0 -144
  97. package/lib/device/admin/routes/gateway.mjs +0 -199
  98. package/lib/device/admin/routes/lifecycle.mjs +0 -200
  99. package/lib/device/admin/routes/owner.mjs +0 -322
  100. package/lib/device/admin/routes/setup.mjs +0 -428
  101. package/lib/device/admin/routes/social.mjs +0 -188
  102. package/lib/device/admin/server.mjs +0 -95
  103. package/lib/device/admin/static.mjs +0 -247
  104. package/lib/device/admin/surface.mjs +0 -315
  105. package/lib/device/certs.mjs +0 -281
  106. package/lib/device/cli/commands/account.mjs +0 -586
  107. package/lib/device/cli/commands/run.mjs +0 -278
  108. package/lib/device/cli/commands/service.mjs +0 -221
  109. package/lib/device/cli/commands/setup.mjs +0 -489
  110. package/lib/device/cli/commands/state.mjs +0 -561
  111. package/lib/device/cli/context.mjs +0 -288
  112. package/lib/device/export-collections.mjs +0 -161
  113. package/lib/device/gateway-move.mjs +0 -58
  114. package/lib/device/home.mjs +0 -161
  115. package/lib/device/migrate.mjs +0 -115
  116. package/lib/device/ports.mjs +0 -27
  117. package/lib/device/remote.mjs +0 -54
  118. package/lib/device/setup.mjs +0 -496
  119. package/lib/device/update.mjs +0 -96
  120. package/lib/gateway/caches.mjs +0 -36
  121. package/lib/gateway/directory.mjs +0 -90
  122. package/lib/gateway/front-core.mjs +0 -966
  123. package/lib/gateway/gateway-core.mjs +0 -244
  124. package/lib/gateway/headers.mjs +0 -49
  125. package/lib/gateway/httpsig.mjs +0 -159
  126. package/lib/gateway/notices.mjs +0 -84
  127. package/lib/gateway/quiet.mjs +0 -189
  128. package/lib/gateway/relay-extras.mjs +0 -89
  129. package/lib/gateway/token-claims.mjs +0 -16
  130. package/lib/pod/README.md +0 -61
  131. package/lib/pod/actor.mjs +0 -96
  132. package/lib/pod/collection.mjs +0 -77
  133. package/lib/pod/containers.mjs +0 -140
  134. package/lib/pod/discovery.mjs +0 -46
  135. package/lib/pod/featured.mjs +0 -18
  136. package/lib/pod/followers.mjs +0 -24
  137. package/lib/pod/following.mjs +0 -10
  138. package/lib/pod/http.mjs +0 -74
  139. package/lib/pod/inbox.mjs +0 -166
  140. package/lib/pod/links.mjs +0 -35
  141. package/lib/pod/location.mjs +0 -52
  142. package/lib/pod/media.mjs +0 -27
  143. package/lib/pod/notes.mjs +0 -78
  144. package/lib/pod/notifications.mjs +0 -89
  145. package/lib/pod/outbox.mjs +0 -18
  146. package/lib/pod/package.json +0 -19
  147. package/lib/pod/policy.mjs +0 -58
  148. package/lib/pod/private.mjs +0 -19
  149. package/lib/pod/root.mjs +0 -104
  150. package/lib/pod/state.mjs +0 -40
  151. package/lib/pod/transport.mjs +0 -674
  152. package/lib/pod/type-index.mjs +0 -101
  153. package/lib/pod/urls.mjs +0 -91
  154. package/lib/server/embed.mjs +0 -424
  155. package/lib/session/README.md +0 -137
  156. package/lib/session/demo.html +0 -94
  157. package/lib/session/fedi-account.mjs +0 -417
  158. package/lib/session/fedi-login.mjs +0 -123
  159. package/lib/session/oidc-session.mjs +0 -233
  160. package/lib/session/package.json +0 -49
  161. package/lib/shared/guard.mjs +0 -213
  162. package/lib/shared/links.mjs +0 -9
  163. package/lib/shared/safefetch.mjs +0 -235
  164. package/lib/shared/ua.mjs +0 -29
  165. package/phanpy/dist/404.html +0 -40
  166. package/phanpy/dist/_headers +0 -2
  167. package/phanpy/dist/apple-touch-icon.png +0 -0
  168. package/phanpy/dist/assets/ICONS-DIPJeU0C.js +0 -2
  169. package/phanpy/dist/assets/ICONS-DIPJeU0C.js.map +0 -1
  170. package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js +0 -183
  171. package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
  172. package/phanpy/dist/assets/Temml-CCjoacWt.woff2 +0 -0
  173. package/phanpy/dist/assets/__vite-browser-external-C7iut881.js +0 -2
  174. package/phanpy/dist/assets/__vite-browser-external-C7iut881.js.map +0 -1
  175. package/phanpy/dist/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  176. package/phanpy/dist/assets/catch-up-O5q5C75Z.png +0 -0
  177. package/phanpy/dist/assets/chunk-aKtaBQYM.js +0 -1
  178. package/phanpy/dist/assets/compose-CFEx7wb4.js +0 -2
  179. package/phanpy/dist/assets/compose-CFEx7wb4.js.map +0 -1
  180. package/phanpy/dist/assets/compose-ZJN5xSb4.js +0 -35
  181. package/phanpy/dist/assets/compose-ZJN5xSb4.js.map +0 -1
  182. package/phanpy/dist/assets/debug.module-YFQgwnIr.js +0 -52
  183. package/phanpy/dist/assets/debug.module-YFQgwnIr.js.map +0 -1
  184. package/phanpy/dist/assets/dom-p1YcJ5Rw.js +0 -2
  185. package/phanpy/dist/assets/dom-p1YcJ5Rw.js.map +0 -1
  186. package/phanpy/dist/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  187. package/phanpy/dist/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  188. package/phanpy/dist/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  189. package/phanpy/dist/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  190. package/phanpy/dist/assets/icons/add-circle-line-WD0mwORD.js +0 -1
  191. package/phanpy/dist/assets/icons/alert-line-CpFUMf9s.js +0 -1
  192. package/phanpy/dist/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
  193. package/phanpy/dist/assets/icons/announcement-line-5nwaJFJm.js +0 -1
  194. package/phanpy/dist/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
  195. package/phanpy/dist/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
  196. package/phanpy/dist/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
  197. package/phanpy/dist/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
  198. package/phanpy/dist/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
  199. package/phanpy/dist/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
  200. package/phanpy/dist/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
  201. package/phanpy/dist/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
  202. package/phanpy/dist/assets/icons/at-line-CF66Bznk.js +0 -1
  203. package/phanpy/dist/assets/icons/attachment-line-BY8It-l9.js +0 -1
  204. package/phanpy/dist/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
  205. package/phanpy/dist/assets/icons/board-line-C0UWXyA6.js +0 -1
  206. package/phanpy/dist/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
  207. package/phanpy/dist/assets/icons/building-5-line-Dd59MzbO.js +0 -1
  208. package/phanpy/dist/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
  209. package/phanpy/dist/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
  210. package/phanpy/dist/assets/icons/calendar-month-line-vommts2C.js +0 -1
  211. package/phanpy/dist/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
  212. package/phanpy/dist/assets/icons/camera-line-Cv69MEei.js +0 -1
  213. package/phanpy/dist/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
  214. package/phanpy/dist/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
  215. package/phanpy/dist/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
  216. package/phanpy/dist/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
  217. package/phanpy/dist/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
  218. package/phanpy/dist/assets/icons/clipboard-line-BBp364gJ.js +0 -1
  219. package/phanpy/dist/assets/icons/close-circle-line-CICdiuGz.js +0 -1
  220. package/phanpy/dist/assets/icons/close-line-DYrFWcjv.js +0 -1
  221. package/phanpy/dist/assets/icons/cloud-line-tiQAbah8.js +0 -1
  222. package/phanpy/dist/assets/icons/code-line-fAl8iiV3.js +0 -1
  223. package/phanpy/dist/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
  224. package/phanpy/dist/assets/icons/copy-2-line-CLW69yF5.js +0 -1
  225. package/phanpy/dist/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
  226. package/phanpy/dist/assets/icons/document-3-line-DBGNhAg2.js +0 -1
  227. package/phanpy/dist/assets/icons/document-line-C7mb9sb7.js +0 -1
  228. package/phanpy/dist/assets/icons/down-line-BGqSSThd.js +0 -1
  229. package/phanpy/dist/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
  230. package/phanpy/dist/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
  231. package/phanpy/dist/assets/icons/exit-line-Dc07BS-x.js +0 -1
  232. package/phanpy/dist/assets/icons/external-link-line-BvpSkmjO.js +0 -1
  233. package/phanpy/dist/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
  234. package/phanpy/dist/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
  235. package/phanpy/dist/assets/icons/filter-2-line-B_mR2usy.js +0 -1
  236. package/phanpy/dist/assets/icons/filter-line-C_nm-3cU.js +0 -1
  237. package/phanpy/dist/assets/icons/flag-1-line-C2HB05I0.js +0 -1
  238. package/phanpy/dist/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
  239. package/phanpy/dist/assets/icons/formula-line-DLrt2d6j.js +0 -1
  240. package/phanpy/dist/assets/icons/grid-line-CgUmEikf.js +0 -1
  241. package/phanpy/dist/assets/icons/group-line-C5fnX0wU.js +0 -1
  242. package/phanpy/dist/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
  243. package/phanpy/dist/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
  244. package/phanpy/dist/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
  245. package/phanpy/dist/assets/icons/heart-line-CJ7K47J4.js +0 -1
  246. package/phanpy/dist/assets/icons/history-2-line-kWWvFzqv.js +0 -1
  247. package/phanpy/dist/assets/icons/history-line-bGQiwbTH.js +0 -1
  248. package/phanpy/dist/assets/icons/home-3-line-fRU1zeLG.js +0 -1
  249. package/phanpy/dist/assets/icons/information-line-iNk6-sOY.js +0 -1
  250. package/phanpy/dist/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
  251. package/phanpy/dist/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
  252. package/phanpy/dist/assets/icons/layout-5-line-B_znt5wO.js +0 -1
  253. package/phanpy/dist/assets/icons/left-line-eUw0tya4.js +0 -1
  254. package/phanpy/dist/assets/icons/lightning-line-DaKoDdix.js +0 -1
  255. package/phanpy/dist/assets/icons/link-2-line-DCqLmMA7.js +0 -1
  256. package/phanpy/dist/assets/icons/list-check-line-BAxosBh9.js +0 -1
  257. package/phanpy/dist/assets/icons/lock-line-CPoaXeUW.js +0 -1
  258. package/phanpy/dist/assets/icons/mail-line-B0P8aa1L.js +0 -1
  259. package/phanpy/dist/assets/icons/moon-line-CEqNAToP.js +0 -1
  260. package/phanpy/dist/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
  261. package/phanpy/dist/assets/icons/more-3-line-BIiVjYTz.js +0 -1
  262. package/phanpy/dist/assets/icons/notification-line-BZZq2Gtu.js +0 -1
  263. package/phanpy/dist/assets/icons/pencil-line-CKh8-A1v.js +0 -1
  264. package/phanpy/dist/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
  265. package/phanpy/dist/assets/icons/pin-line-DpTolyhs.js +0 -1
  266. package/phanpy/dist/assets/icons/play-fill-B0-G_8lX.js +0 -1
  267. package/phanpy/dist/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
  268. package/phanpy/dist/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
  269. package/phanpy/dist/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
  270. package/phanpy/dist/assets/icons/radar-line-CKyroTth.js +0 -1
  271. package/phanpy/dist/assets/icons/react-line-CoIKhhtK.js +0 -1
  272. package/phanpy/dist/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
  273. package/phanpy/dist/assets/icons/right-line-B3KWJaqc.js +0 -1
  274. package/phanpy/dist/assets/icons/rocket-line-4KZl537D.js +0 -1
  275. package/phanpy/dist/assets/icons/round-fill-C69NTrZ0.js +0 -1
  276. package/phanpy/dist/assets/icons/round-line-B9k_1uo-.js +0 -1
  277. package/phanpy/dist/assets/icons/route-line-B28ReHcE.js +0 -1
  278. package/phanpy/dist/assets/icons/rows-4-line-CrBniagM.js +0 -1
  279. package/phanpy/dist/assets/icons/scan-line-DSf74YdP.js +0 -1
  280. package/phanpy/dist/assets/icons/search-2-line-B6geIU1z.js +0 -1
  281. package/phanpy/dist/assets/icons/settings-3-line-azJwK47w.js +0 -1
  282. package/phanpy/dist/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
  283. package/phanpy/dist/assets/icons/share-2-line-D5MxWjrm.js +0 -1
  284. package/phanpy/dist/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
  285. package/phanpy/dist/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
  286. package/phanpy/dist/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
  287. package/phanpy/dist/assets/icons/time-line-BF3eet1v.js +0 -1
  288. package/phanpy/dist/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
  289. package/phanpy/dist/assets/icons/translate-line-BFWol2Ce.js +0 -1
  290. package/phanpy/dist/assets/icons/unlock-line-X_x5vwv3.js +0 -1
  291. package/phanpy/dist/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
  292. package/phanpy/dist/assets/icons/user-4-line-BVSynZCp.js +0 -1
  293. package/phanpy/dist/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
  294. package/phanpy/dist/assets/icons/user-add-line-BbXdHG4z.js +0 -1
  295. package/phanpy/dist/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
  296. package/phanpy/dist/assets/icons/user-follow-line-BXux0wSd.js +0 -1
  297. package/phanpy/dist/assets/icons/user-star-line-DdYlpHLU.js +0 -1
  298. package/phanpy/dist/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
  299. package/phanpy/dist/assets/icons/user-x-line-DIDgNlPH.js +0 -1
  300. package/phanpy/dist/assets/icons/volume-line-Bs390KQt.js +0 -1
  301. package/phanpy/dist/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
  302. package/phanpy/dist/assets/icons/walk-line-yNXfB-md.js +0 -1
  303. package/phanpy/dist/assets/icons/world-2-line-CenkGIwj.js +0 -1
  304. package/phanpy/dist/assets/icons/zoom-in-line-CaENUphf.js +0 -1
  305. package/phanpy/dist/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
  306. package/phanpy/dist/assets/instances-BNPptnn-.json +0 -333
  307. package/phanpy/dist/assets/locales/ar-SA-CfheRbCu.js +0 -1
  308. package/phanpy/dist/assets/locales/ca-ES-DHRW86Tz.js +0 -1
  309. package/phanpy/dist/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
  310. package/phanpy/dist/assets/locales/de-DE-ByRPMRFy.js +0 -1
  311. package/phanpy/dist/assets/locales/eo-UY-CPC7wLCS.js +0 -1
  312. package/phanpy/dist/assets/locales/es-ES-dJa0Fd9t.js +0 -1
  313. package/phanpy/dist/assets/locales/eu-ES-C3djlPR2.js +0 -1
  314. package/phanpy/dist/assets/locales/fa-IR-DTT490Ck.js +0 -1
  315. package/phanpy/dist/assets/locales/fi-FI-B23MCPuz.js +0 -1
  316. package/phanpy/dist/assets/locales/fr-FR-BcaQsO5C.js +0 -1
  317. package/phanpy/dist/assets/locales/gl-ES-BR-CVKBN.js +0 -1
  318. package/phanpy/dist/assets/locales/he-IL-CfheRbCu.js +0 -1
  319. package/phanpy/dist/assets/locales/hu-HU-CfheRbCu.js +0 -1
  320. package/phanpy/dist/assets/locales/it-IT-C1136UFV.js +0 -1
  321. package/phanpy/dist/assets/locales/ja-JP-CRZmRAhD.js +0 -1
  322. package/phanpy/dist/assets/locales/kab-Cz8xH_TT.js +0 -1
  323. package/phanpy/dist/assets/locales/ko-KR-CG9UiY4o.js +0 -1
  324. package/phanpy/dist/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
  325. package/phanpy/dist/assets/locales/nb-NO-CfheRbCu.js +0 -1
  326. package/phanpy/dist/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
  327. package/phanpy/dist/assets/locales/oc-FR-CfheRbCu.js +0 -1
  328. package/phanpy/dist/assets/locales/pl-PL-DkwGXf6P.js +0 -1
  329. package/phanpy/dist/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
  330. package/phanpy/dist/assets/locales/pt-BR-nGL0HdFS.js +0 -1
  331. package/phanpy/dist/assets/locales/pt-PT-Cp17aj2z.js +0 -1
  332. package/phanpy/dist/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
  333. package/phanpy/dist/assets/locales/th-TH-CfheRbCu.js +0 -1
  334. package/phanpy/dist/assets/locales/tok-CfheRbCu.js +0 -1
  335. package/phanpy/dist/assets/locales/tr-TR-CfheRbCu.js +0 -1
  336. package/phanpy/dist/assets/locales/uk-UA-C5E3dFBf.js +0 -1
  337. package/phanpy/dist/assets/locales/zh-CN-BHJi7_et.js +0 -1
  338. package/phanpy/dist/assets/locales/zh-TW-CfheRbCu.js +0 -1
  339. package/phanpy/dist/assets/main-BdqNbG-a.js +0 -26
  340. package/phanpy/dist/assets/main-BdqNbG-a.js.map +0 -1
  341. package/phanpy/dist/assets/mock-home-DGhR__He.js +0 -2
  342. package/phanpy/dist/assets/mock-home-DGhR__He.js.map +0 -1
  343. package/phanpy/dist/assets/multi-column-ROck0NVt.jpg +0 -0
  344. package/phanpy/dist/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  345. package/phanpy/dist/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  346. package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js +0 -2
  347. package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js.map +0 -1
  348. package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js +0 -20
  349. package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js.map +0 -1
  350. package/phanpy/dist/assets/style-B1iLPTPR.css +0 -2
  351. package/phanpy/dist/assets/temml-DuyIQMei.js +0 -97
  352. package/phanpy/dist/assets/temml-DuyIQMei.js.map +0 -1
  353. package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js +0 -2
  354. package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
  355. package/phanpy/dist/assets/year-in-posts-B2lywqdx.png +0 -0
  356. package/phanpy/dist/assets/year-in-posts-DJv_star.js +0 -3
  357. package/phanpy/dist/assets/year-in-posts-DJv_star.js.map +0 -1
  358. package/phanpy/dist/compose/index.html +0 -20
  359. package/phanpy/dist/favicon.ico +0 -0
  360. package/phanpy/dist/index.html +0 -73
  361. package/phanpy/dist/logo-192.png +0 -0
  362. package/phanpy/dist/logo-512.png +0 -0
  363. package/phanpy/dist/logo-badge-72.png +0 -0
  364. package/phanpy/dist/logo-maskable-512.png +0 -0
  365. package/phanpy/dist/logo-monochrome-512.png +0 -0
  366. package/phanpy/dist/logo-monochrome-maskable-512.png +0 -0
  367. package/phanpy/dist/manifest.webmanifest +0 -1
  368. package/phanpy/dist/og-image-2.jpg +0 -0
  369. package/phanpy/dist/og-image.png +0 -0
  370. package/phanpy/dist/robots.txt +0 -2
  371. package/phanpy/dist/sw.js +0 -2
  372. package/phanpy/dist/sw.js.map +0 -1
  373. package/phanpy/dist/version.json +0 -1
  374. package/run-agent.mjs +0 -899
  375. package/vendor/gate.cjs +0 -138
  376. package/vendor/idp-grant.cjs +0 -394
  377. package/vendor/jfetch.cjs +0 -16
  378. package/web/admin/actors.js +0 -147
  379. package/web/admin/bar.css +0 -126
  380. package/web/admin/bar.js +0 -99
  381. package/web/admin/client/client.js +0 -66
  382. package/web/admin/client/index.html +0 -69
  383. package/web/admin/client-pick.js +0 -79
  384. package/web/admin/common.js +0 -23
  385. package/web/admin/connections.js +0 -112
  386. package/web/admin/gateway.js +0 -133
  387. package/web/admin/group.js +0 -258
  388. package/web/admin/index.html +0 -612
  389. package/web/admin/notices-bar.js +0 -90
  390. package/web/admin/oauth-signin.mjs +0 -37
  391. package/web/admin/record.js +0 -393
  392. package/web/admin/setup/index.html +0 -257
  393. package/web/admin/setup/setup.js +0 -339
  394. package/web/admin/tokens.css +0 -68
  395. package/web/admin/upkeep.js +0 -177
  396. package/web/admin/window.css +0 -55
  397. package/web/admin/window.js +0 -136
  398. package/web/app/README.md +0 -81
  399. package/web/app/admin-facade.mjs +0 -588
  400. package/web/app/agent.mjs +0 -672
  401. package/web/app/atproto-browser.mjs +0 -86
  402. package/web/app/authorize_interaction.html +0 -56
  403. package/web/app/authorize_interaction.js +0 -69
  404. package/web/app/boot.mjs +0 -616
  405. package/web/app/deliver-relay.mjs +0 -129
  406. package/web/app/dist/boot.js +0 -34562
  407. package/web/app/dist/boot.js.map +0 -7
  408. package/web/app/dist/sw.js +0 -73456
  409. package/web/app/dist/sw.js.map +0 -7
  410. package/web/app/fediacct-browser.mjs +0 -146
  411. package/web/app/gateway-move.mjs +0 -60
  412. package/web/app/idb-kv.mjs +0 -35
  413. package/web/app/index.html +0 -258
  414. package/web/app/keys-browser.mjs +0 -71
  415. package/web/app/keystore.mjs +0 -82
  416. package/web/app/oidc-session.mjs +0 -6
  417. package/web/app/pod-auth.mjs +0 -248
  418. package/web/app/pod-remote.mjs +0 -84
  419. package/web/app/shims/_globals.mjs +0 -2
  420. package/web/app/shims/fedify-sig.mjs +0 -77
  421. package/web/app/shims/node-crypto.mjs +0 -113
  422. package/web/app/shims/node-fs.mjs +0 -17
  423. package/web/app/shims/node-path.mjs +0 -19
  424. package/web/app/shims/node-url.mjs +0 -7
  425. package/web/app/shims/prelude.js +0 -58
  426. package/web/app/shims/safefetch.mjs +0 -58
  427. package/web/app/shims/shapes-text.mjs +0 -8
  428. package/web/app/shims/web-push.mjs +0 -14
  429. package/web/app/signup.mjs +0 -357
  430. package/web/app/sw-src.mjs +0 -220
  431. package/web/app/update.js +0 -70
  432. package/web/front/admin.html +0 -55
  433. package/web/front/admin.js +0 -89
  434. package/web/front/install.sh +0 -69
  435. package/web/front/new-account.html +0 -35
  436. package/web/front/new-account.js +0 -13
  437. package/web/front/notices.html +0 -69
  438. package/web/front/notices.js +0 -107
  439. package/web/front/run.html +0 -64
  440. package/web/front/run.js +0 -221
  441. package/web/front/solid-oidc-client.js +0 -6
  442. package/web/icons/forum.svg +0 -11
  443. package/web/icons/gateway.svg +0 -14
  444. package/web/icons/local.svg +0 -11
@@ -1,966 +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
- import { claimedWebId } from './token-claims.mjs';
29
- import { withdrawn, overLimit, relayOne } from './relay-extras.mjs';
30
-
31
- // The one WebFinger document, spelled out here rather than imported from
32
- // wire.mjs: wire drags the agent's whole HTML pipeline (sanitize-html and
33
- // friends), which a serverless front must never carry — it crashed the
34
- // deployed function before it answered its first request.
35
- const jrd = ({ handle, host, actor, aliases = [], page = null, subscribe = null }) => ({
36
- subject: `acct:${handle}@${host}`,
37
- ...(aliases.length ? { aliases } : {}),
38
- links: [
39
- { rel: 'self', type: 'application/activity+json', href: actor },
40
- ...(page ? [{ rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: page }] : []),
41
- // Remote follow. Somebody reading this account on another server presses
42
- // Follow, says they are here, and that server looks for this link to find
43
- // out where to send them. `{uri}` is a literal the other server fills in
44
- // with whatever they are following — it must not be escaped.
45
- ...(subscribe ? [{ rel: 'http://ostatus.org/schema/1.0/subscribe', template: subscribe }] : []),
46
- ],
47
- });
48
- // The door that link names, which the site staged beside this function serves.
49
- const subscribeTemplate = (origin) => (origin
50
- ? `${String(origin).replace(/\/$/u, '')}/authorize_interaction?uri={uri}` : null);
51
- // The public profile page of any account the front answers for lives on
52
- // its pod, whether the address is the front's or the pod's own.
53
- const profilePageOf = (rec) => (rec?.podHome ? rec.podHome + 'ap/profile.html' : null);
54
-
55
- const AP_CT = 'application/activity+json';
56
-
57
- // Verify a Solid-OIDC token (DPoP-bound) and return its WebID, or null. The
58
- // verifier is injected so tests stub it; in production it is the same library
59
- // the agent's own C2S auth uses.
60
- async function verifyPodToken(request, pathname, verifier) {
61
- const authz = request.headers.get('authorization');
62
- if (!authz) return null;
63
- try {
64
- const dpop = request.headers.get('dpop');
65
- // Solid-OIDC binds the token to a key the client proves on every request;
66
- // a token shown without the proof is one anyone who saw it could show.
67
- if (!dpop) { console.log(`front: pod token without a DPoP proof refused on ${pathname}`); return null; }
68
- const v = verifier || await podTokenVerifier();
69
- const url = request.url;
70
- const { webid } = await v(authz, { header: dpop, method: request.method, url });
71
- return webid || null;
72
- } catch (e) {
73
- // Said aloud: a token the front will not take is otherwise a bare 401 to
74
- // the caller and nothing at all here.
75
- console.log(`front: pod token refused on ${pathname}: ${e?.message || e}`);
76
- return null;
77
- }
78
- }
79
-
80
- // Is this WebID served by the claimed pod? A pod owner's WebID lives on the pod
81
- // origin — that is the whole proof: a token for a WebID under podHome could
82
- // only be minted by someone who controls that pod's identity provider.
83
- // A pod that will not answer must not hold up the person opting in; without
84
- // an answer the older check stands on its own.
85
- const OWNER_LOOKUP_MS = 5_000;
86
-
87
- /**
88
- * Who the pod server says owns the pod. The server that hosts it is the
89
- * authority on that, so when it answers, its answer decides. A server that
90
- * says nothing leaves where the WebID lives as the only evidence there is.
91
- */
92
- const podOwners = (podBase, fetchImpl = fetch) =>
93
- podRoot.readOwnerLinks(fetchImpl, podBase, { timeoutMs: OWNER_LOOKUP_MS });
94
-
95
- // Whether a token's WebID vouches for this pod, on the evidence of where it
96
- // lives: the same origin, and — for a suffixed pod — inside
97
- // the WebID's own pod, wherever in it the account was put. Weak on purpose,
98
- // and never used alone for an attach — see provesPod.
99
- function webidUnderPod(webid, podHome) {
100
- try {
101
- return new URL(webid).origin === new URL(podHome).origin && String(podHome).startsWith(podBaseOfWebId(webid));
102
- } catch { return false; }
103
- }
104
-
105
- /**
106
- * Whether this WebID may speak for this pod.
107
- *
108
- * Sharing an origin was the whole test, which is right where an origin is one
109
- * person (a subdomain pod) and wrong where it is not. On a path-based server
110
- * every account is on the same origin, so any account holder could attach
111
- * somebody else's pod under a name of their choosing — and, worse, re-attach a
112
- * name that already existed and be handed its receipt secret (S7).
113
- *
114
- * The pod server is the authority on who owns the pod, and it will say so in a
115
- * `solid:owner` link if asked. When it answers, its answer decides. Only when
116
- * it says nothing does this fall back to where the WebID lives, which is the
117
- * best a silent server leaves available. This is the rule /api/agent's opt-in
118
- * has used all along; attach simply never asked.
119
- */
120
- async function provesPod(webid, podHome, fetchImpl) {
121
- const owners = await podOwners(podHome, fetchImpl || fetch);
122
- if (owners.length) return owners.includes(webid);
123
- return webidUnderPod(webid, podHome);
124
- }
125
-
126
- // A pod this front will read from at all.
127
- //
128
- // The front reads `rec.podHome + <the rest of the request path>` and, on the
129
- // CSS server component, that read goes straight into the resource store with
130
- // no access control of its own — so a row whose podHome names the FRONT is a
131
- // row that can read the server's own internals, `/.internal/fedipod/directory/`
132
- // included, where every user's receipt secret is kept (S2). The front is not a
133
- // pod and never was one; saying so outright is the fix, and it closes the
134
- // self-recursion of S8 with the same line.
135
- function podHomeProblem(podHome, frontOrigin) {
136
- let u;
137
- try { u = new URL(podHome); } catch { return 'podHome is not a URL'; }
138
- if (frontOrigin) {
139
- try {
140
- if (u.origin === new URL(frontOrigin).origin) {
141
- return 'a pod cannot live on this gateway — name the pod that holds your data';
142
- }
143
- } catch { /* no usable frontOrigin: the other checks still apply */ }
144
- }
145
- // Nothing may reach a server's private trees, whatever origin they are on.
146
- if (/(^|\/)\.internal(\/|$)/u.test(u.pathname) || /(^|\/)\.\.(\/|$)/u.test(u.pathname)) {
147
- return 'that is not a pod address';
148
- }
149
- return null;
150
- }
151
-
152
- const RELAY_MAX_REQUESTS = 20;
153
- const SERVER_PROBE_MS = 6000;
154
- const SERVER_PROBE_BYTES = 64 * 1024;
155
-
156
- // The owner's APIs a browser at ANOTHER origin may call: an address moving
157
- // to a new gateway is driven from the new gateway's page, and it has to tell
158
- // this one it moved and send the Move through this one's relay. Both take a
159
- // pod token as proof, the way the outbox door does, so any origin may ask.
160
- const API_CORS = {
161
- 'access-control-allow-origin': '*',
162
- 'access-control-allow-methods': 'POST, OPTIONS',
163
- 'access-control-allow-headers': 'Authorization, DPoP, Content-Type',
164
- 'access-control-max-age': '86400',
165
- };
166
- const withApiCors = (out) => ({ ...out, headers: { ...(out.headers || {}), ...API_CORS } });
167
- const apiPreflight = () => ({ status: 204, headers: { ...API_CORS, allow: 'POST, OPTIONS' }, body: null });
168
-
169
- const j = (status, obj, ct = 'application/json') =>
170
- ({ status, headers: { 'content-type': ct, 'cache-control': 'no-store' }, body: JSON.stringify(obj) });
171
- // Held at the edge like any other missing document: a bot's scan, a dead
172
- // handle, a typo in a WebFinger query each cost one call, not one per asker.
173
- const notFound = () => ({ status: 404, headers: { 'content-type': 'text/plain', ...publicFor(MISSING_EDGE_SECONDS) }, body: 'not found\n' });
174
-
175
- // A user's public base on the front, a 1:1 mirror of their pod home.
176
- // The directory key is the FULL fediverse address — handle@host — never the
177
- // bare handle, which is not unique: two different pods can each have a "me".
178
- // A fronted identity answers at this front's host; a mail-door identity at its
179
- // own pod's host. The `/u/` path segment carries that same key.
180
- export const addressKey = (handle, host) => `${handle}@${host}`;
181
- export const userBase = (frontOrigin, key) => `${frontOrigin}/u/${encodeURIComponent(key)}/`;
182
-
183
- // Rewrite every occurrence of the pod home to the front base (or back). The
184
- // bodies are JSON, so a whole-string swap rewrites ids wherever they appear —
185
- // actor id, collection ids, note ids — keeping the whole space self-consistent.
186
- const swap = (text, from, to) => text.split(from).join(to);
187
-
188
- // Which handle a /u/<handle>/... path names, and the pod path under it.
189
- function parseUserPath(pathname) {
190
- const m = /^\/u\/([^/]+)\/(.*)$/.exec(pathname);
191
- return m ? { handle: decodeURIComponent(m[1]), rest: m[2] } : null;
192
- }
193
-
194
- // A public document is the same for everybody who asks, so the edge may hold
195
- // it and answer the next reader itself. Without this every read of an actor,
196
- // a topic, a post or a card was a function call: one visit to a forum is
197
- // twenty of them, and every visitor and every refresh paid again.
198
- //
199
- // `netlify-cdn-cache-control` is the edge's own instruction; `cache-control`
200
- // is what the reader's browser is told. The edge may serve a stale copy while
201
- // it fetches a fresh one, so nobody waits on the pod for a document that is
202
- // merely minutes old. The edge holds a document far longer than a browser
203
- // is asked to: the edge is what turns a thousand readers into one function
204
- // call, while a browser that held an actor for ten minutes would show one
205
- // person a stale profile they had just edited.
206
- const BROWSER_MAX_AGE = 60;
207
- const publicFor = (seconds, stale = seconds * 4) => ({
208
- 'cache-control': `public, max-age=${Math.min(seconds, BROWSER_MAX_AGE)}`,
209
- // `durable`: one copy for every region, and one that a deploy does not
210
- // empty. Without it each region held its own and every deploy started cold.
211
- 'netlify-cdn-cache-control': `public, durable, s-maxage=${seconds}, stale-while-revalidate=${stale}`,
212
- });
213
- // How long the edge holds a public document, and a handle's WebFinger
214
- // answer. Every server that has heard of an account asks for its actor and
215
- // its handle again and again; a post or a collection changes when its owner
216
- // acts, and ten minutes of edge is what the plan's budget of function calls
217
- // can afford.
218
- const PUBLIC_EDGE_SECONDS = 600;
219
- const WEBFINGER_EDGE_SECONDS = 3600;
220
- // A closed or moved address stays gone; a picture's address stays where it is.
221
- const GONE_EDGE_SECONDS = 3600;
222
- const MEDIA_EDGE_SECONDS = 86400;
223
- // A public document the pod would not give (missing, or not public) is asked
224
- // for again and again by every server that shows the account — a pinned-posts
225
- // collection an old account never wrote, a forum count nobody published. Held
226
- // briefly, so the edge absorbs the asking; briefly, so a document published a
227
- // moment later is not "missing" for long.
228
- const MISSING_EDGE_SECONDS = 120;
229
-
230
- // Where a pod owner opts their identity in. An operator may name it, because
231
- // the path it takes is one their pod server can no longer serve; the dot says
232
- // it belongs to the server rather than to anybody's data, the way
233
- // `.well-known` and `.account` do.
234
- export const DEFAULT_RUN_PATH = '/.fediverse-account';
235
- export const runPath = (ctx) => {
236
- const said = String(ctx?.runPath || '').trim().replace(/\/+$/u, '');
237
- if (!said) return DEFAULT_RUN_PATH;
238
- return said.startsWith('/') ? said : `/${said}`;
239
- };
240
-
241
- // A request the browser itself says came from this very origin. `Sec-Fetch-Site`
242
- // is the browser's word and a page cannot forge it; an `Origin` that matches is
243
- // the older way of saying the same. This is what makes it safe to read a
244
- // session cookie as proof: a page on another site gets neither.
245
- const sameOriginRequest = (request, origin) => {
246
- const site = request.headers.get('sec-fetch-site');
247
- if (site) return site === 'same-origin';
248
- const said = request.headers.get('origin');
249
- if (!said || !origin) return false;
250
- try { return new URL(said).origin === new URL(origin).origin; } catch { return false; }
251
- };
252
-
253
- // Top-level paths a handle may not take, so a name never shadows a route.
254
- const RESERVED = new Set(['u', 'api', 'signup', 'new-account', 'run', 'admin', 'roster', 'gateway',
255
- 'gw', 'well-known', 'inbox', 'outbox', 'actor', 'install', 'notices']);
256
-
257
- // Why a handle is unusable, or null when it is fine. Lowercase letters, digits
258
- // and hyphens; 2–30 chars; not edge-hyphenated; not a reserved route.
259
- function handleProblem(h) {
260
- if (!h) return 'empty';
261
- if (!/^[a-z0-9-]{2,30}$/.test(h)) return 'letters, digits and hyphens only, 2–30 characters';
262
- if (h.startsWith('-') || h.endsWith('-')) return 'cannot start or end with a hyphen';
263
- if (RESERVED.has(h)) return 'that name is reserved';
264
- return null;
265
- }
266
-
267
- // The person's own filtering policy, published on their pod and readable by
268
- // anyone: their accepted following list and a mirror of their blocklist. A
269
- // directory row carries neither — the row is written at attach time, before
270
- // the agent has published anything — so without this the door would judge by
271
- // addressing alone and forward the junk a blocklist exists to stop.
272
- //
273
- // Cached briefly: a delivery flood must not become a read per delivery on the
274
- // person's pod, which is the load this whole door exists to spare them.
275
- const POLICY_TTL_MS = 5 * 60_000;
276
- const policyCache = new Map(); // podHome -> { at, policy }
277
-
278
- // The cache is OURS, not the library's: on a warm serverless container one Map
279
- // is shared across every user this front serves, so what it is keyed by is a
280
- // correctness property. Keyed by podHome, it is one person's policy under one
281
- // person's key.
282
- const policyFor = (rec, fetchImpl = fetch) =>
283
- podPolicy.read(fetchImpl, rec.podHome, { cache: policyCache, ttlMs: POLICY_TTL_MS });
284
-
285
- // The verified-delivery identity for a directory record: what gateway-core
286
- // needs to check what concerns the user and to append to their pod inbox.
287
- // `policy` is the person's published filtering policy when there is one.
288
- function identFor(rec, policy = null) {
289
- return {
290
- // A row may name the inbox its deliveries are written to: a forum's
291
- // categories share one, on the forum's pod, above their own trees.
292
- inboxUrl: rec.inboxUrl || rec.podHome + 'ap/inbox/',
293
- actorUrl: rec.actorUrl, // the FRONT actor id (what mail is addressed to)
294
- followersUrl: rec.actorUrl.replace(/actor$/, 'followers'),
295
- notesPrefix: rec.actorUrl.replace(/ap\/actor$/, 'ap/notes/'),
296
- following: policy?.following || rec.following || [],
297
- blocklist: policy?.blocklist || rec.blocklist || { domains: [], actors: [] },
298
- kind: policy?.kind || rec.kind || 'person',
299
- gatewayWebId: rec.gatewayWebId,
300
- hmacSecret: rec.hmacSecret,
301
- };
302
- }
303
-
304
- // The single entry point. `ctx`:
305
- // host the front's own host, e.g. "fedipod.net" (for WebFinger subjects)
306
- // frontOrigin "https://fedipod.net"
307
- // lookup(handle) -> record | null the directory
308
- // listDirectory() -> { handle: record } every row, for the admin roster
309
- // removeDirectory(handle) -> boolean drop a row; false when a seeded row remains
310
- // podPut(url, body, ct) -> boolean append to a user's pod (per-user cred inside)
311
- // readReceived / writeReceived / dropReceived, pauseItems, closeDays — see quiet.mjs
312
- // podGet(url, { podHome }) -> Response read a user's pod (public reads; plain fetch
313
- // is fine). `podHome` is the row's own pod, for
314
- // an adapter that reads a store directly and so
315
- // must confine the read itself.
316
- export async function routeFront(request, ctx) {
317
- const out = await route(request, ctx);
318
- return { ...out, headers: withSecurityHeaders(out.headers, out.body) };
319
- }
320
-
321
- async function route(request, ctx) {
322
- const url = new URL(request.url);
323
- const { pathname } = url;
324
-
325
- // The new-account page: the host's front door, forking solo / group /
326
- // manage. Served as a static page the adapter supplies; the page itself
327
- // calls the availability API below. It only COLLECTS intent — creating an
328
- // account is the Solid-OIDC attach step (proving the pod is the user's).
329
- if (pathname === '/' || pathname === '/signup' || pathname === '/new-account') {
330
- if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
331
- if (!ctx.signupPage) return notFound();
332
- return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: ctx.signupPage };
333
- }
334
-
335
- // The run page: a pod owner opting their identity in or out of being run by
336
- // this server. Meaningful only on a host that embeds the agent; a host that
337
- // does not simply supplies no page.
338
- if (pathname === runPath(ctx)) {
339
- if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
340
- if (!ctx.runPage) return notFound();
341
- return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' }, body: ctx.runPage };
342
- }
343
-
344
- // The roster page (/roster, off /admin, which is the owner's own record
345
- // page on every agent) is served with the notices page, in notices.mjs.
346
-
347
- // Notices from the operator to every account here, and the page they are
348
- // written on (notices.mjs).
349
- const notices = await routeNoticesApi(request, pathname, ctx, { j, verifyPodToken, publicFor, notFound });
350
- if (notices) return notices;
351
-
352
- // The roster: every directory row, secrets stripped, for the host's own
353
- // eyes. The reader proves themself the way attach proves a pod — a
354
- // Solid-OIDC token — and must be the WebID the deploy names as admin.
355
- if (pathname === '/api/roster') {
356
- if (!ctx.listDirectory || !ctx.adminWebId) return j(501, { error: 'this front has no roster to offer' });
357
- const webid = await verifyPodToken(request, pathname, ctx.verifier);
358
- if (!webid) return j(401, { error: 'a Solid-OIDC token is required' });
359
- if (webid !== ctx.adminWebId) return j(403, { error: 'that WebID is not the admin of this front' });
360
- const rows = await ctx.listDirectory();
361
- const accounts = Object.values(rows)
362
- .map((r) => ({
363
- handle: r.handle, kind: r.kind || 'person', fronted: !r.inboxOnly,
364
- podHome: r.podHome, webId: r.webId || null, actorUrl: r.actorUrl,
365
- address: r.address || `@${r.handle}@${ctx.host}`,
366
- openedAt: r.openedAt || null, pausedAt: r.pausedAt || null, closedAt: r.closedAt || null,
367
- }))
368
- .sort((a, b) => (a.address || a.handle).localeCompare(b.address || b.handle));
369
- return j(200, { host: ctx.host, accounts });
370
- }
371
-
372
- // Revoke: the admin removes an account's row, so the server stops answering
373
- // for the name. Nothing on the user's pod is touched. Attach-created rows go
374
- // for good; a row seeded in the deploy's environment can only be removed there.
375
- if (pathname === '/api/revoke' && request.method === 'POST') {
376
- if (!ctx.removeDirectory || !ctx.adminWebId) return j(501, { error: 'this front cannot revoke accounts' });
377
- const webid = await verifyPodToken(request, pathname, ctx.verifier);
378
- if (!webid) return j(401, { error: 'a Solid-OIDC token is required' });
379
- if (webid !== ctx.adminWebId) return j(403, { error: 'that WebID is not the admin of this front' });
380
- let body;
381
- try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
382
- // Revoke by full address (what the roster lists); a bare handle still works
383
- // for any legacy row keyed that way.
384
- const key = String(body.address || body.handle || '').replace(/^@/, '').toLowerCase();
385
- if (!key || !(await ctx.lookup(key))) return j(404, { error: 'no such account' });
386
- const removed = (await ctx.removeDirectory(key)) === true;
387
- return j(200, removed ? { account: key, removed }
388
- : { account: key, removed: false,
389
- reason: 'this row is seeded in the deploy environment (FEDIPOD_DIRECTORY_JSON) — remove it there and redeploy' });
390
- }
391
-
392
- // Live handle check for the page: valid shape AND not already in the
393
- // directory. Also tells the page whether this host offers pods.
394
- if (pathname === '/api/handle') {
395
- const h = (url.searchParams.get('handle') || '').toLowerCase();
396
- const problem = handleProblem(h);
397
- if (problem) {
398
- return j(200, { handle: h, available: false, reason: problem,
399
- offersPods: !!ctx.offersPods, version: ctx.version || null });
400
- }
401
- const taken = !!(await ctx.lookup(h));
402
- return j(200, { handle: h, available: !taken, reason: taken ? 'that name is taken' : null,
403
- offersPods: !!ctx.offersPods, version: ctx.version || null });
404
- }
405
-
406
- // What a reader's own server speaks, asked here rather than in their
407
- // browser: a page cannot probe a stranger's server (no CORS), and the
408
- // answer decides how that reader takes part in a forum.
409
- if (pathname === '/api/server') {
410
- const host = (url.searchParams.get('host') || '').trim().toLowerCase();
411
- const open = { 'access-control-allow-origin': '*' };
412
- const said = (kind, extra = {}) => ({ ...j(200, { host, kind, ...extra }), headers: { ...j(200, {}).headers, ...open } });
413
- // An address at this front is a FediPod account: its client is the
414
- // browser here, and no other server holds it. Asked before the shape of
415
- // the host, since a front may answer at a name with no dot in it.
416
- if (host && host === String(ctx.host || '').toLowerCase()) {
417
- const handle = (url.searchParams.get('handle') || '').toLowerCase();
418
- const rec = handle ? await ctx.lookup(handle) : null;
419
- if (!rec) return said('fedipod');
420
- // Where the owner of this pod signs in: the pod says so itself, at the
421
- // well-known document every Solid server answers (or redirects to).
422
- let issuer = null;
423
- try {
424
- const res = await safeFetch(new URL('/.well-known/openid-configuration', rec.podHome).href,
425
- { headers: { accept: 'application/json' }, redirect: 'follow', signal: AbortSignal.timeout(SERVER_PROBE_MS) },
426
- ctx.fetchImpl || fetch);
427
- if (res.ok) issuer = JSON.parse(await readCapped(res, SERVER_PROBE_BYTES)).issuer || null;
428
- } catch { /* a pod that will not say leaves the reader to type it */ }
429
- return said('fedipod', { podHome: rec.podHome, actor: rec.actorUrl, issuer, inbox: identFor(rec).inboxUrl });
430
- }
431
- if (!/^[a-z0-9.-]+\.[a-z0-9-]+(:\d+)?$/u.test(host)) return said('invalid');
432
- const f = ctx.fetchImpl || fetch;
433
- const ask = async (path) => {
434
- try {
435
- const res = await safeFetch(`https://${host}${path}`,
436
- { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(SERVER_PROBE_MS) }, f);
437
- if (!res.ok) return null;
438
- return JSON.parse(await readCapped(res, SERVER_PROBE_BYTES));
439
- } catch { return null; }
440
- };
441
- const masto = await ask('/api/v1/instance');
442
- if (masto) return said('mastodon-api', { name: masto.title || masto.domain || host });
443
- const lemmy = await ask('/api/v3/site');
444
- if (lemmy) return said('lemmy', { name: lemmy.site_view?.site?.name || host });
445
- return said('unknown');
446
- }
447
-
448
- // Attach: create the account. The user proves control of the pod they claim
449
- // by presenting a Solid-OIDC token whose WebID lives on that pod — no
450
- // password reaches the front. On success the directory gets a row and the
451
- // agent gets a one-time HMAC secret to stamp the receipts its front writes.
452
- if (pathname === '/api/attach' && request.method === 'POST') {
453
- if (!ctx.putDirectory) return j(501, { error: 'this front does not accept signups' });
454
- let body;
455
- try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
456
- const handle = String(body.handle || '').toLowerCase();
457
- const podHome = String(body.podHome || '');
458
- const problem = handleProblem(handle);
459
- if (problem) return j(400, { error: problem });
460
- const podLoopback = (() => { try { return isLoopbackHost(new URL(podHome).hostname); } catch { return false; } })();
461
- if (!(/^https:\/\/\S+\/$/.test(podHome) || (/^http:\/\/\S+\/$/.test(podHome) && podLoopback))) {
462
- return j(400, { error: 'podHome must be an https URL ending in /' });
463
- }
464
- const badPod = podHomeProblem(podHome, ctx.frontOrigin);
465
- if (badPod) return j(400, { error: badPod });
466
- const webid = await verifyPodToken(request, pathname, ctx.verifier);
467
- if (!webid) return j(401, { error: 'a Solid-OIDC token proving the pod is required' });
468
- if (!await provesPod(webid, podHome, ctx.fetchImpl)) {
469
- return j(403, { error: 'the token proves a different pod than the one you listed' });
470
- }
471
- // The usual attachment keeps the user's identity on their pod and moves
472
- // only the mail door here (@me@mypod). Fronted identity — actor ids on
473
- // this domain — remains available to an explicit `fronted: true`.
474
- const fronted = body.fronted === true;
475
- // The full address is the key: a fronted name answers at this front's host,
476
- // a mail-door name at its pod's host. The same handle on two pods is two
477
- // distinct addresses, so it never collides.
478
- let addressHost; try { addressHost = fronted ? ctx.host : new URL(podHome).host; } catch { return j(400, { error: 'bad podHome' }); }
479
- // A fronted name answers at this front's host, where the handle alone is the
480
- // whole address and is unique — so it keeps keying by bare handle (and its
481
- // actor id stays /u/<handle>/, unchanged for existing accounts). A mail-door
482
- // name answers at its own pod, where "me" is not unique, so it keys by the
483
- // full address handle@podhost, which the /u/ segment then carries.
484
- const key = fronted ? handle : addressKey(handle, addressHost);
485
- const address = `@${handle}@${addressHost}`;
486
- // "Taken" only when this exact address already belongs to a DIFFERENT pod.
487
- // Re-attaching your own pod — a retry, a re-provision, a second browser — is
488
- // idempotent: it updates the row in place and keeps the same door secret.
489
- const prior = await ctx.lookup(key);
490
- // The same account on the same pod may correct where its tree lives — a
491
- // row made by an older sign-up named the pod root, and every delivery
492
- // to it was written where nothing reads. A different pod is still taken.
493
- const sameOwner = (a, b) => { try { return new URL(a).origin === new URL(b).origin; } catch { return false; } };
494
- if (prior && prior.podHome !== podHome
495
- && !(prior.webId && prior.webId === webid && sameOwner(prior.podHome, podHome))) {
496
- return j(409, { error: 'that name is taken' });
497
- }
498
- // Re-attaching is idempotent — a retry, a re-provision, a second browser —
499
- // and it hands back the EXISTING receipt secret. So it has to be the same
500
- // person: matching podHome was enough on a subdomain server and, on a
501
- // path-based one, meant any co-tenant could re-attach your row, flip its
502
- // kind, and be given your secret (S7). A row that predates `webId` has
503
- // nothing to compare and falls back to the pod check above.
504
- if (prior?.webId && prior.webId !== webid) {
505
- return j(409, { error: 'that name belongs to a different account on this pod' });
506
- }
507
- // The pod's actor lives under its AP root container, which the pod owner
508
- // knows and the front does not — so take the caller's actorUrl when it is on
509
- // this pod, and fall back only when none is given.
510
- const claimedActor = typeof body.actorUrl === 'string' && body.actorUrl.startsWith(podHome) ? body.actorUrl : null;
511
- const actorUrl = fronted ? `${userBase(ctx.frontOrigin, key)}ap/actor` : (claimedActor || podHome + 'ap/actor');
512
- const hmacSecret = prior?.hmacSecret || crypto.randomBytes(32).toString('base64');
513
- // Where the door writes: the row's own tree unless the caller names an
514
- // inbox on the same pod — a forum's categories name the forum's.
515
- let inboxUrl = null;
516
- if (typeof body.inboxUrl === 'string' && body.inboxUrl) {
517
- let ok = false;
518
- try { ok = new URL(body.inboxUrl).origin === new URL(podHome).origin && body.inboxUrl.endsWith('/'); } catch { ok = false; }
519
- if (!ok) return j(400, { error: 'inboxUrl must be a container on the same pod' });
520
- inboxUrl = body.inboxUrl;
521
- }
522
- const record = {
523
- handle, address, podHome, webId: webid, actorUrl,
524
- kind: body.kind === 'group' ? 'group' : body.kind === 'application' ? 'application' : 'person',
525
- gatewayWebId: ctx.gatewayWebId || null, hmacSecret,
526
- ...(inboxUrl ? { inboxUrl } : {}),
527
- ...(fronted ? {} : { inboxOnly: true }),
528
- };
529
- await ctx.putDirectory(key, record);
530
- // The secret is returned ONCE, with the one command the user runs to
531
- // point their agent at this gateway.
532
- const doorInbox = `${userBase(ctx.frontOrigin, key)}ap/inbox/`;
533
- if (fronted) {
534
- return j(201, { ok: true, handle, address, actorUrl, frontActor: actorUrl, hmacSecret,
535
- command: `fedipod gateway ${actorUrl} --secret ${hmacSecret}` });
536
- }
537
- return j(201, { ok: true, handle, address, doorInbox, hmacSecret,
538
- command: `fedipod gateway ${doorInbox} --secret ${hmacSecret} --inbox-only` });
539
- }
540
-
541
- // An address at this gateway moving to another one. Its owner, proved the
542
- // way attach proves a pod, says where it went. From then on this front
543
- // answers the old actor as a moved stub — the same key under the old id,
544
- // `movedTo` the new one, so the Move the owner sends from the old address
545
- // verifies — every other old id redirects to its new address, and the
546
- // mail door is shut: deliveries belong at the new one. Repeating it with
547
- // the same target changes nothing.
548
- if (pathname === '/api/move' && request.method === 'OPTIONS') return apiPreflight();
549
- if (pathname === '/api/move' && request.method === 'POST') {
550
- if (!ctx.putDirectory) return withApiCors(j(501, { error: 'this front keeps no directory' }));
551
- let body;
552
- try { body = JSON.parse(await request.clone().text()); } catch { return withApiCors(j(400, { error: 'bad JSON' })); }
553
- const handle = String(body.handle || '').toLowerCase();
554
- const rec = await ctx.lookup(handle);
555
- if (!rec) return withApiCors(j(404, { error: 'no such account' }));
556
- if (rec.inboxOnly) return withApiCors(j(400, { error: 'this address lives on its pod; only an address at this gateway can move' }));
557
- const movedTo = String(body.movedTo || '');
558
- let mu;
559
- try { mu = new URL(movedTo); } catch { return withApiCors(j(400, { error: 'movedTo must be the new actor URL' })); }
560
- if (!((mu.protocol === 'https:' || (mu.protocol === 'http:' && isLoopbackHost(mu.hostname))) && /\/ap\/actor$/u.test(mu.pathname))) {
561
- return withApiCors(j(400, { error: 'movedTo must be an https …/ap/actor URL' }));
562
- }
563
- if (ctx.frontOrigin && mu.origin === new URL(ctx.frontOrigin).origin) return withApiCors(j(400, { error: 'that address is at this gateway' }));
564
- const webid = await verifyPodToken(request, pathname, ctx.verifier);
565
- if (!webid) return withApiCors(j(401, { error: 'a Solid-OIDC token proving the pod is required' }));
566
- const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
567
- if (!owner) return withApiCors(j(403, { error: "the token proves a different pod than this account's" }));
568
- const movedAt = rec.movedTo === movedTo && rec.movedAt ? rec.movedAt : new Date().toISOString();
569
- await ctx.putDirectory(handle, { ...rec, movedTo, movedAt });
570
- console.log(`front @${handle}: moved to ${movedTo}`);
571
- return withApiCors(j(200, { ok: true, handle, movedTo, movedAt }));
572
- }
573
-
574
- // The owner's say over an account that goes quiet (quiet.mjs).
575
- const quiet = await routeQuietApi(request, pathname, ctx, { j, verifyPodToken, webidUnderPod, apiPreflight });
576
- if (quiet) return withApiCors(quiet);
577
-
578
- // The relay: the front sends requests a browser has already signed. A page
579
- // may not set the Date or Host header, and both are inside an HTTP
580
- // signature, so a browser-run agent signs and hands the request here; the
581
- // front sends it with exactly the headers that were signed. It can forward
582
- // a signature and cannot make one. Only the account's own key may sign,
583
- // only the account's own pod may call, and nothing unsigned-for reaches
584
- // the remote server.
585
- if (pathname === '/api/relay' && request.method === 'OPTIONS') return apiPreflight();
586
- if (pathname === '/api/relay' && request.method === 'POST') {
587
- let body;
588
- try { body = JSON.parse(await request.clone().text()); } catch { return withApiCors(j(400, { error: 'bad JSON' })); }
589
- const handle = String(body.handle || '').toLowerCase();
590
- const rec = await ctx.lookup(handle);
591
- if (!rec) { console.log(`relay: no account named ${handle}`); return withApiCors(j(404, { error: 'no such account' })); }
592
- const webid = await verifyPodToken(request, pathname, ctx.verifier);
593
- if (!webid) { console.log(`relay @${handle}: no usable pod token`); return withApiCors(j(401, { error: 'a Solid-OIDC token proving the pod is required' })); }
594
- const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
595
- 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" })); }
596
- const items = Array.isArray(body.requests) ? body.requests : [];
597
- if (!items.length) return withApiCors(j(400, { error: 'requests must be a non-empty list' }));
598
- // Acting through the relay is being here. Hourly at most; see noteOpened.
599
- await noteOpened(ctx, handle, rec).catch((e) => console.log(`relay @${handle}: stamp not written: ${e?.message || e}`));
600
- if (items.length > RELAY_MAX_REQUESTS) return withApiCors(j(400, { error: `at most ${RELAY_MAX_REQUESTS} requests per call` }));
601
- const slow = overLimit(handle, items.length) && j(429, { error: 'too many relayed requests — slow down' });
602
- if (slow) return withApiCors({ ...slow, headers: { ...slow.headers, 'retry-after': '60' } });
603
- const results = await Promise.all(items.map((it) => relayOne(it, rec, ctx.fetchImpl || fetch)));
604
- // Something of the account's withdrawn: the edge's copies go.
605
- if (withdrawn(items, rec.actorUrl)) await ctx.purge?.([`u-${handle}`]).catch((e) => console.log(`purge @${handle}: ${e?.message || e}`));
606
- // One line per relayed request, so a lookup that fails on the far side is
607
- // visible here and not only as an empty result in someone's browser.
608
- for (const r of results) console.log(`relay @${handle}: ${r.method || ''} ${r.url} → ${r.status}${r.error ? ` (${r.error})` : ''}`);
609
- return withApiCors(j(200, { results }));
610
- }
611
-
612
- // Who is here, and which pod of theirs could opt in. The opt-in page asks
613
- // this first: on a pod server that issued the reader's session, the answer
614
- // names their pod and its address, and the page shows one button instead of
615
- // a form. A Gateway holds no such session and says so; the page then asks
616
- // for the pod by hand and proves it with a sign-in, as before.
617
- if (pathname === '/api/agent' && request.method === 'GET') {
618
- if (!ctx.agentControl) return j(501, { error: 'this server does not run identities' });
619
- if (!ctx.webIdsFromSession) return j(200, { offered: true, session: 'unknown' });
620
- if (!sameOriginRequest(request, ctx.frontOrigin)) return j(200, { offered: true, session: 'none' });
621
- const webIds = await ctx.webIdsFromSession(request).catch(() => []);
622
- if (!webIds.length) return j(200, { offered: true, session: 'none', loginUrl: '/.account/login/password/' });
623
- const pods = [];
624
- for (const webId of webIds) {
625
- let podBase;
626
- try { podBase = podBaseOfWebId(webId); } catch { continue; }
627
- let about = null;
628
- try { about = ctx.agentControl.describe ? await ctx.agentControl.describe({ podBase }) : null; } catch { about = null; }
629
- pods.push({ webId, podBase, ...(about || {}) });
630
- }
631
- return j(200, { offered: true, session: 'ok', pods });
632
- }
633
-
634
- // Runtime opt-in: a pod owner asks the server this front sits on to RUN
635
- // their identity. Same proof as attach — a Solid-OIDC token — but the
636
- // ownership check is stricter: the WebID must live UNDER the pod base, not
637
- // merely on its origin, or on a path-pod server one user could opt in
638
- // another's pod. Offered only where the deployment wired agentControl in
639
- // (a pod server running the CSS component); everywhere else it is 501.
640
- if (pathname === '/api/agent' && request.method === 'POST') {
641
- if (!ctx.agentControl) return j(501, { error: 'this server does not run identities' });
642
- let body;
643
- try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
644
- const action = String(body.action || '');
645
- const podBase = String(body.podBase || '');
646
- if (!/^https?:\/\/\S+\/$/.test(podBase)) {
647
- return j(400, { error: 'podBase must be a URL ending in /' });
648
- }
649
- let podUrl;
650
- try { podUrl = new URL(podBase); } catch { return j(400, { error: 'podBase is not a URL' }); }
651
- // A pod may be host-root (its own origin) or on a PATH of a shared host — a
652
- // suffix pod, which the Server runs at `https://server/aisha/`. What is
653
- // refused is the gateway's own origin root itself: that is where the
654
- // gateway lives, not a pod, and admitting it would (with the ownership
655
- // fallback below) let a co-tenant claim the whole origin.
656
- if (ctx.frontOrigin) {
657
- try {
658
- if (podUrl.origin === new URL(ctx.frontOrigin).origin && podUrl.pathname === '/') {
659
- return j(403, { error: 'that is this gateway, not a pod — name the pod that holds your data' });
660
- }
661
- } catch { /* no usable frontOrigin: the checks below still apply */ }
662
- }
663
- if (/(^|\/)\.internal(\/|$)/u.test(podUrl.pathname) || /(^|\/)\.\.(\/|$)/u.test(podUrl.pathname)) {
664
- return j(403, { error: 'that is not a pod address' });
665
- }
666
- // The pod's own server names its owner when it can, and where it does that
667
- // is the proof. Where it does not, the WebID must live in EXACTLY this pod
668
- // — its own pod base equal to podBase, not merely starting with it, which on
669
- // a path server an ancestor of another's pod would.
670
- const owners = await podOwners(podBase, ctx.fetchImpl || fetch);
671
- const ownsIt = (who) => (owners.length ? owners.includes(who) : podBaseOfWebId(who) === podBase);
672
- const token = await verifyPodToken(request, pathname, ctx.verifier);
673
- // A server that issued the reader's own session can simply read it. On a
674
- // pod server the page, the session and the pod are one origin, and a token
675
- // proves nothing the session does not — so opting in cost a round trip to
676
- // an identity provider that had just said who they were. A Gateway fronting
677
- // somebody else's pod holds no such session and this does nothing there.
678
- //
679
- // One account can hold several WebIDs, so it is the one that owns this pod
680
- // that is taken, not whichever came back first.
681
- const session = !token && ctx.webIdsFromSession && sameOriginRequest(request, ctx.frontOrigin)
682
- ? (await ctx.webIdsFromSession(request).catch(() => [])).find(ownsIt) || null
683
- : null;
684
- const webid = token || session;
685
- if (!webid) return j(401, { error: 'a Solid-OIDC token proving the pod is required' });
686
- if (!ownsIt(webid)) {
687
- return j(403, { error: 'the token proves a different pod than the one you listed' });
688
- }
689
- if (action === 'opt-in') {
690
- const { httpStatus, ...reply } = await ctx.agentControl.optIn({ podBase, webId: webid,
691
- container: String(body.container || ''), createIndex: body.createIndex === true });
692
- if (reply.doorSecret) {
693
- // The secret appears here and nowhere else; the command is the
694
- // paste-and-run way to open the owner door once.
695
- reply.command = `curl -H 'x-dk-token: ${reply.doorSecret}' ${podBase.replace(/\/$/, '')}${reply.doorPath}status`;
696
- }
697
- return j(httpStatus, reply);
698
- }
699
- if (action === 'opt-out') {
700
- const { httpStatus, ...reply } = await ctx.agentControl.optOut({ podBase });
701
- return j(httpStatus, reply);
702
- }
703
- return j(400, { error: 'action must be opt-in or opt-out' });
704
- }
705
-
706
- // Each page's own script. They were inline until 2026-09-09; out here the
707
- // pages can be served under `script-src 'self'`, which is the directive that
708
- // tells running code from injected markup — and inline script defeats it by
709
- // construction. Same mechanism the vendored library below has always used.
710
- //
711
- // The name is matched against a literal list, so nothing about the request
712
- // chooses a file.
713
- const mPageScript = /^\/(new-account|run|admin|notices)\.js$/u.exec(pathname);
714
- if (mPageScript) {
715
- const body = ctx.pageScripts?.[`${mPageScript[1]}.js`];
716
- if (!body) return notFound();
717
- // Revalidated on every load: a page served fresh with a script the
718
- // browser kept from last time is the mismatch nobody can see.
719
- return { status: 200, headers: { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-cache' }, body };
720
- }
721
-
722
- // The vendored Solid-OIDC browser library the opt-in and admin pages load —
723
- // served here because this function owns every path on the domain.
724
- if (pathname === '/solid-oidc-client.js') {
725
- if (!ctx.authBundle) return notFound();
726
- return { status: 200, headers: { 'content-type': 'text/javascript' }, body: ctx.authBundle };
727
- }
728
-
729
- // The installer: curl -fsSL https://<host>/install | sh
730
- if (pathname === '/install') {
731
- if (!ctx.installScript) return notFound();
732
- return { status: 200, headers: { 'content-type': 'text/plain; charset=utf-8' }, body: ctx.installScript };
733
- }
734
-
735
- // WebFinger: acct:<handle>@<host> → the fronted actor. This is what makes
736
- // @name@fedipod.net a real handle the fediverse can look up.
737
- if (pathname === '/.well-known/webfinger') {
738
- const resource = url.searchParams.get('resource') || '';
739
- const m = /^acct:([^@]+)@(.+)$/.exec(resource);
740
- if (!m || m[2] !== ctx.host) return notFound();
741
- const rec = await ctx.lookup(m[1]);
742
- if (!rec) return notFound();
743
- // A closed address is gone, and says so rather than pretending never to
744
- // have existed: the name stays taken.
745
- if ((await closedState(ctx, m[1], rec)).closed) return closedAnswer({ 'access-control-allow-origin': '*', ...publicFor(GONE_EDGE_SECONDS) });
746
- // A fronted identity's documents live on its pod; the pod's own actor id
747
- // is the alias, so a client signing in by the fronted address can find
748
- // the pod (and its login) without a lookup only the host could answer.
749
- const podActor = rec.inboxOnly ? [] : [rec.podHome + 'ap/actor'];
750
- const out = j(200, jrd({ handle: m[1], host: ctx.host, actor: rec.actorUrl, aliases: podActor,
751
- page: profilePageOf(rec), subscribe: subscribeTemplate(ctx.frontOrigin) }),
752
- 'application/jrd+json');
753
- // RFC 7033: a JRD is read from any origin.
754
- out.headers['access-control-allow-origin'] = '*';
755
- // A handle answers the same for everybody, and changes only when a row
756
- // does. Every server that has heard of this account asks for it; the edge
757
- // can answer most of them.
758
- Object.assign(out.headers, publicFor(WEBFINGER_EDGE_SECONDS));
759
- return out;
760
- }
761
-
762
- // The short address a person types: https://<host>/@handle opens the
763
- // profile page, the way most Fediverse servers spell it. /@handle@pod-host
764
- // is the same for an account whose address is its pod's, which the front
765
- // holds under that full address — the form other servers use for a
766
- // profile that lives elsewhere.
767
- const at = /^\/@([^/@]+)(?:@([^/@]+))?$/u.exec(pathname);
768
- if (at) {
769
- const handle = decodeURIComponent(at[1]).toLowerCase();
770
- const host = at[2] ? decodeURIComponent(at[2]).toLowerCase() : null;
771
- const key = host && host !== String(ctx.host).toLowerCase() ? `${handle}@${host}` : handle;
772
- const rec = await ctx.lookup(key);
773
- const page = rec && profilePageOf(rec);
774
- if (!page) return notFound();
775
- return { status: 302, headers: { location: page, 'cache-control': 'no-store' }, body: '' };
776
- }
777
-
778
- const up = parseUserPath(pathname);
779
- if (!up) return notFound();
780
- const rec = await ctx.lookup(up.handle);
781
- if (!rec) return notFound();
782
- const base = userBase(ctx.frontOrigin, up.handle);
783
- // An account that moved to another gateway (see /api/move): where its ids
784
- // live now. The actor below answers as a stub; everything else redirects.
785
- const movedBase = rec.movedTo ? rec.movedTo.replace(/ap\/actor$/u, '') : null;
786
- // A closed address (see "accounts that go quiet" above): every id under it
787
- // is gone. Asked after moved, which a close does not undo.
788
- const closed = movedBase ? false : (await closedState(ctx, up.handle, rec)).closed;
789
- const gone = (headers = {}, why = `this account has moved to ${rec.movedTo}`) => closedAnswer(headers, why);
790
- const CLOSED = 'this address is closed';
791
-
792
- // Inbox: verify at the door, forward to the user's pod inbox. This is the
793
- // gateway, per user.
794
- if (up.rest === 'ap/inbox/' || up.rest === 'ap/inbox') {
795
- if (request.method !== 'POST') return { status: 405, headers: {}, body: '' };
796
- if (movedBase) { console.log(`door @${up.handle}: delivery → 410 (moved)`); return gone(); }
797
- if (closed) { console.log(`door @${up.handle}: delivery → 410 (closed)`); return gone({}, CLOSED); }
798
- const policy = await policyFor(rec, ctx.fetchImpl || fetch);
799
- const standing = await accountState(ctx, up.handle, rec);
800
- const { status, reason, content, bytes } = await handleDelivery(request, identFor(rec, policy),
801
- { podPut: (u, b, ct) => ctx.podPut(up.handle, u, b, ct), fetchImpl: ctx.fetchImpl, paused: standing.paused });
802
- // One line per delivery, so "did it arrive at the door" has an answer.
803
- console.log(`door @${up.handle}: delivery → ${status} (${reason})`);
804
- if (status === 202 && content) await noteReceived(ctx, up.handle, rec, bytes);
805
- return { status, headers: {}, body: '' };
806
- }
807
-
808
- // Outbox: the owner's own post, from any client that speaks ActivityPub
809
- // client-to-server (dokieli, say). Cross-origin by nature — such a client is
810
- // a page on another site — so this route answers a preflight and carries
811
- // CORS headers; the owner's pod token is the credential, so any origin may
812
- // ask. GET stays a read: the pod's document for a fronted account, sent on
813
- // to the pod for a door account whose documents live at their own address.
814
- if (up.rest === 'ap/outbox') {
815
- const cors = {
816
- 'access-control-allow-origin': '*',
817
- 'access-control-allow-methods': 'GET, POST, OPTIONS',
818
- 'access-control-allow-headers': 'Authorization, DPoP, Content-Type, Slug, Link',
819
- 'access-control-expose-headers': 'Location, Accept-Post',
820
- 'access-control-max-age': '86400',
821
- };
822
- if (request.method === 'OPTIONS') {
823
- // Accept-Post names JSON only: a client that reads it (dokieli) sends
824
- // JSON-LD when HTML is not offered.
825
- // body null, not '': a Response refuses any body on a 204, and the
826
- // function adapter hands `body` straight to one.
827
- return { status: 204, headers: { ...cors, allow: 'GET, POST, OPTIONS',
828
- 'accept-post': 'application/ld+json, application/activity+json' }, body: null };
829
- }
830
- if (movedBase && request.method === 'POST') return gone(cors);
831
- if (closed && request.method === 'POST') return gone(cors, CLOSED);
832
- if (movedBase && (request.method === 'GET' || request.method === 'HEAD')) {
833
- return { status: 301, headers: { ...cors, location: movedBase + up.rest, 'cache-control': 'no-store' }, body: '' };
834
- }
835
- if (request.method === 'POST') {
836
- const json = (status, obj, extra = {}) => ({ status,
837
- headers: { ...cors, 'content-type': 'application/json', 'cache-control': 'no-store', ...extra },
838
- body: JSON.stringify(obj) });
839
- const webid = await verifyPodToken(request, pathname, ctx.verifier);
840
- if (!webid) return json(401, { error: 'a Solid-OIDC token proving this account\'s owner is required' });
841
- const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
842
- if (!owner) return json(403, { error: 'this outbox belongs to its owner alone' });
843
- const onPod = (u) => u.replace(rec.actorUrl.replace(/ap\/actor$/u, ''), rec.podHome);
844
- const { status, reason, location, object } = await handleOwnerPost(request, identFor(rec),
845
- { podPut: (u, b, ct) => ctx.podPut(up.handle, u, b, ct), ownerWebId: webid,
846
- exists: async (u) => ((await (ctx.podGet || fetch)(onPod(u), { podHome: rec.podHome })).status !== 404) });
847
- console.log(`door @${up.handle}: owner post → ${status} (${reason})`);
848
- if (status !== 201) return json(status, { error: reason });
849
- return json(201, { accepted: true, ...(location ? { id: location } : {}), ...(object ? { object } : {}),
850
- note: 'it goes out when your FediPod agent next runs' }, location ? { location } : {});
851
- }
852
- // Its owner, signed in, reads every message the account produced (§5.1:
853
- // the outbox is filtered by who asks); that view is on the pod, under the
854
- // owner's own rule. Anyone else reads the public one.
855
- // The token is only read here, not verified: the redirect grants nothing,
856
- // the pod checks the credential when the owner arrives there, and a
857
- // verification per read would let anyone make the front fetch keys from a
858
- // server they name.
859
- if ((request.method === 'GET' || request.method === 'HEAD') && request.headers.get('authorization')) {
860
- const webid = claimedWebId(request);
861
- if (webid && (rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome))) {
862
- return { status: 303, headers: { ...cors, location: rec.podHome + 'ap/private/outbox', 'cache-control': 'no-store' }, body: '' };
863
- }
864
- }
865
- if (rec.inboxOnly && (request.method === 'GET' || request.method === 'HEAD')) {
866
- return { status: 303, headers: { ...cors, location: rec.podHome + 'ap/outbox', 'cache-control': 'no-store' }, body: '' };
867
- }
868
- }
869
-
870
- // Everything else is a public GET, served by reading the user's pod and
871
- // rewriting pod ids to the front. The actor also gets its handle and inbox
872
- // fixed to the front so a consumer cross-checks it consistently.
873
- // Public documents are read from any origin — a page on another site, the
874
- // forum's own host among them — the way a pod serves them, so the read
875
- // carries the open origin and a preflight is answered.
876
- const open = { 'access-control-allow-origin': '*' };
877
- if (request.method === 'OPTIONS') {
878
- return { status: 204, headers: { ...open, 'access-control-allow-methods': 'GET, HEAD, OPTIONS',
879
- 'access-control-allow-headers': 'Accept', 'access-control-max-age': '86400', allow: 'GET, HEAD, OPTIONS' }, body: null };
880
- }
881
- if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
882
- // Moved: every old id but the actor's answers with where it is now, so a
883
- // reply or a boost another server still holds under the old id resolves.
884
- if (movedBase && up.rest !== 'ap/actor') {
885
- return { status: 301, headers: { ...open, location: movedBase + up.rest, 'cache-control': 'no-store' }, body: '' };
886
- }
887
- if (closed) return gone({ ...open, ...publicFor(GONE_EDGE_SECONDS) }, CLOSED);
888
- const podTarget = rec.podHome + up.rest;
889
- // The owner's own documents — the full outbox, the liked list, the pending
890
- // lists — are read at the pod, which decides who may; never held here,
891
- // where one reader's answer would be served to the next.
892
- if (up.rest.startsWith('ap/private/')) {
893
- return { status: 303, headers: { ...open, location: podTarget, 'cache-control': 'no-store' }, body: '' };
894
- }
895
- // Media stays on the pod (lib/pod/urls.mjs keeps `media` off the front), but
896
- // the id rewrite below turns media links onto the front like every other
897
- // pod url in a document. Answer those by pointing at the pod: bytes are not
898
- // a document to cap and relabel, and remotes follow a redirect for a picture.
899
- if (up.rest.startsWith('ap/media/')) {
900
- return { status: 302, headers: { location: podTarget, ...publicFor(MEDIA_EDGE_SECONDS) }, body: '' };
901
- }
902
- // The pod this read belongs to travels with it: an adapter reading a store
903
- // directly (the CSS server component) has no access control of its own and
904
- // needs to be told what it may reach. See podHomeProblem above for the other
905
- // half — a row that names something which is not a pod.
906
- // A container is Turtle, a profile page is HTML, everything else here is
907
- // ActivityPub. Ask for what the thing can be, not for what most of them are.
908
- const container = podTarget.endsWith('/');
909
- const page = /\.html?$/u.test(podTarget);
910
- const accept = container ? 'text/turtle, */*;q=0.8'
911
- : page ? 'text/html, */*;q=0.8'
912
- : AP_CT;
913
- const got = await podRoot.readPublicDocument(
914
- ctx.podGet || ((u) => fetch(u, { headers: { accept } })),
915
- podTarget, { podHome: rec.podHome, accept });
916
- if (got.text === null) {
917
- const hold = [401, 403, 404, 410].includes(got.status) ? { ...publicFor(MISSING_EDGE_SECONDS), 'netlify-cache-tag': `u-${up.handle}` } : {};
918
- // A stranger is not told a document exists that they may not read (§3.2).
919
- return { status: got.status === 401 || got.status === 403 ? 404 : got.status, headers: { ...open, ...hold }, body: '' };
920
- }
921
- let text = got.text;
922
- text = swap(text, rec.podHome, base);
923
- // The moved stub: the pod's actor now carries the NEW gateway's ids, and a
924
- // server verifying the Move fetches the OLD id and needs a document whose
925
- // id is that old id — with the same key under it, and `movedTo` saying
926
- // where the account went. Swapping the new base back to this one gives
927
- // exactly that; alsoKnownAs and movedTo are set below.
928
- if (movedBase) text = swap(text, movedBase, base);
929
- if (up.rest === 'ap/actor') {
930
- try {
931
- const doc = JSON.parse(text);
932
- doc.preferredUsername = up.handle; // so @handle@front cross-checks
933
- doc.inbox = base + 'ap/inbox/'; // deliveries come to the front to be verified
934
- doc.endpoints = { ...(doc.endpoints || {}), sharedInbox: base + 'ap/inbox/' };
935
- // The owner's own documents are read at the pod, where their credential
936
- // works; they keep the pod's address.
937
- for (const k of ['liked', 'ownerOutbox']) if (typeof doc[k] === 'string') doc[k] = doc[k].replace(base, rec.podHome);
938
- if (movedBase) {
939
- doc.movedTo = rec.movedTo;
940
- const aka = new Set([].concat(doc.alsoKnownAs || []).filter((a) => typeof a === 'string' && a !== doc.id));
941
- aka.add(rec.movedTo);
942
- doc.alsoKnownAs = [...aka];
943
- const ctxs = [].concat(doc['@context'] || []);
944
- const declared = (term) => ctxs.some((c) => c && typeof c === 'object' && term in c);
945
- if (!declared('alsoKnownAs')) ctxs.push({ alsoKnownAs: { '@id': 'as:alsoKnownAs', '@type': '@id' } });
946
- if (!declared('movedTo')) ctxs.push({ movedTo: { '@id': 'as:movedTo', '@type': '@id' } });
947
- doc['@context'] = ctxs;
948
- }
949
- text = JSON.stringify(doc);
950
- } catch { /* leave the rewritten text as-is if it will not parse */ }
951
- }
952
- // `readPublicDocument` asked for it with no credential, so a document that
953
- // came back is one anybody can read — which is what makes it cacheable. A
954
- // deleted post's Tombstone answers 410 Gone, and still says what it was.
955
- let kind = null;
956
- try { kind = JSON.parse(text)?.type; } catch { /* not JSON: a page or a container */ }
957
- return { status: kind === 'Tombstone' ? 410 : 200,
958
- headers: { ...open, 'content-type': got.type || (container ? 'text/turtle' : page ? 'text/html' : AP_CT), ...publicFor(PUBLIC_EDGE_SECONDS),
959
- // The outbox answers its signed-in owner differently, so the edge keeps
960
- // the public copy for those who show no credential.
961
- ...(up.rest === 'ap/outbox' ? { 'netlify-vary': 'header=Authorization', vary: 'Authorization' } : {}),
962
- 'netlify-cache-tag': `u-${up.handle}` },
963
- body: text };
964
- }
965
-
966
- export const _internal = { parseUserPath, identFor, swap, handleProblem, policyFor, policyCache };