fedipod-server 0.9.0 → 0.11.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 (319) hide show
  1. package/dist/claims.js +1 -1
  2. package/dist/handler.d.ts +1 -1
  3. package/dist/handler.js +35 -2
  4. package/dist/handler.jsonld +1 -1
  5. package/lib/admin.mjs +3 -3
  6. package/lib/c2s.mjs +27 -2
  7. package/lib/deliver.mjs +6 -0
  8. package/lib/embed.mjs +1 -0
  9. package/lib/export-collections.mjs +1 -1
  10. package/lib/front-core.mjs +272 -49
  11. package/lib/gateway-core.mjs +3 -7
  12. package/lib/httpsig.mjs +33 -2
  13. package/lib/import.mjs +1 -1
  14. package/lib/intake.mjs +357 -118
  15. package/lib/links.mjs +6 -32
  16. package/lib/mastoapi.mjs +449 -38
  17. package/lib/migrate.mjs +4 -18
  18. package/lib/pod/README.md +61 -0
  19. package/lib/pod/actor.mjs +96 -0
  20. package/lib/pod/collection.mjs +77 -0
  21. package/lib/pod/containers.mjs +136 -0
  22. package/lib/pod/discovery.mjs +46 -0
  23. package/lib/pod/featured.mjs +18 -0
  24. package/lib/pod/followers.mjs +24 -0
  25. package/lib/pod/following.mjs +10 -0
  26. package/lib/pod/http.mjs +74 -0
  27. package/lib/pod/inbox.mjs +144 -0
  28. package/lib/pod/links.mjs +35 -0
  29. package/lib/pod/media.mjs +27 -0
  30. package/lib/pod/notes.mjs +80 -0
  31. package/lib/pod/notifications.mjs +89 -0
  32. package/lib/pod/outbox.mjs +18 -0
  33. package/lib/pod/package.json +19 -0
  34. package/lib/pod/policy.mjs +58 -0
  35. package/lib/pod/private.mjs +19 -0
  36. package/lib/pod/root.mjs +63 -0
  37. package/lib/pod/state.mjs +39 -0
  38. package/lib/pod/transport.mjs +477 -0
  39. package/lib/pod/urls.mjs +71 -0
  40. package/lib/polls.mjs +105 -0
  41. package/lib/publisher.mjs +357 -188
  42. package/lib/remote.mjs +30 -395
  43. package/lib/social.mjs +20 -9
  44. package/lib/storage.mjs +2 -2
  45. package/lib/store.mjs +8 -1
  46. package/lib/tagfeed.mjs +19 -2
  47. package/lib/wire.mjs +87 -67
  48. package/package.json +1 -1
  49. package/run-agent.mjs +20 -39
  50. package/web/admin/admin.js +141 -74
  51. package/web/admin/bar.css +9 -13
  52. package/web/admin/bar.js +2 -2
  53. package/web/admin/client/client.js +1 -1
  54. package/web/admin/client/index.html +5 -6
  55. package/web/admin/index.html +53 -65
  56. package/web/admin/setup/index.html +2 -1
  57. package/web/admin/setup/setup.js +1 -1
  58. package/web/admin/tokens.css +56 -0
  59. package/web/app/README.md +77 -0
  60. package/web/app/admin-facade.mjs +549 -0
  61. package/web/app/agent.mjs +490 -0
  62. package/web/app/atproto-browser.mjs +86 -0
  63. package/web/app/boot.mjs +340 -0
  64. package/web/app/deliver-relay.mjs +72 -0
  65. package/web/app/dist/boot.js +34067 -0
  66. package/web/app/dist/boot.js.map +7 -0
  67. package/web/app/dist/sw.js +53745 -0
  68. package/web/app/dist/sw.js.map +7 -0
  69. package/web/app/fediacct-browser.mjs +146 -0
  70. package/web/app/idb-kv.mjs +35 -0
  71. package/web/app/index.html +203 -0
  72. package/web/app/index.html~ +152 -0
  73. package/web/app/keys-browser.mjs +46 -0
  74. package/web/app/keystore.mjs +99 -0
  75. package/web/app/oidc-session.mjs +189 -0
  76. package/web/app/pod-auth.mjs +248 -0
  77. package/web/app/pod-remote.mjs +84 -0
  78. package/web/app/shims/_globals.mjs +2 -0
  79. package/web/app/shims/fedify-sig.mjs +77 -0
  80. package/web/app/shims/node-crypto.mjs +113 -0
  81. package/web/app/shims/node-fs.mjs +17 -0
  82. package/web/app/shims/node-path.mjs +19 -0
  83. package/web/app/shims/node-url.mjs +7 -0
  84. package/web/app/shims/prelude.js +58 -0
  85. package/web/app/shims/safefetch.mjs +58 -0
  86. package/web/app/shims/web-push.mjs +14 -0
  87. package/web/app/signup.mjs +248 -0
  88. package/web/app/site/_headers +9 -0
  89. package/web/app/site/_redirects +21 -0
  90. package/web/app/site/admin/admin.js +1181 -0
  91. package/web/app/site/admin/bar.css +60 -0
  92. package/web/app/site/admin/bar.js +61 -0
  93. package/web/app/site/admin/client/client.js +66 -0
  94. package/web/app/site/admin/client/index.html +43 -0
  95. package/web/app/site/admin/index.html +556 -0
  96. package/web/app/site/admin/setup/index.html +191 -0
  97. package/web/app/site/admin/setup/setup.js +308 -0
  98. package/web/app/site/admin/tokens.css +56 -0
  99. package/web/app/site/admin/window.css +55 -0
  100. package/web/app/site/admin/window.js +136 -0
  101. package/web/app/site/app/404.html +40 -0
  102. package/web/app/site/app/_headers +2 -0
  103. package/web/app/site/app/apple-touch-icon.png +0 -0
  104. package/web/app/site/app/assets/ICONS-DIPJeU0C.js +2 -0
  105. package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +1 -0
  106. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +183 -0
  107. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +1 -0
  108. package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
  109. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +2 -0
  110. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +1 -0
  111. package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  112. package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
  113. package/web/app/site/app/assets/chunk-aKtaBQYM.js +1 -0
  114. package/web/app/site/app/assets/compose-CFEx7wb4.js +2 -0
  115. package/web/app/site/app/assets/compose-CFEx7wb4.js.map +1 -0
  116. package/web/app/site/app/assets/compose-ZJN5xSb4.js +35 -0
  117. package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +1 -0
  118. package/web/app/site/app/assets/debug.module-YFQgwnIr.js +52 -0
  119. package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +1 -0
  120. package/web/app/site/app/assets/dom-p1YcJ5Rw.js +2 -0
  121. package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +1 -0
  122. package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  123. package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  124. package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  125. package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  126. package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +1 -0
  127. package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +1 -0
  128. package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +1 -0
  129. package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +1 -0
  130. package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +1 -0
  131. package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +1 -0
  132. package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +1 -0
  133. package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +1 -0
  134. package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +1 -0
  135. package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +1 -0
  136. package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +1 -0
  137. package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +1 -0
  138. package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +1 -0
  139. package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +1 -0
  140. package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +1 -0
  141. package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +1 -0
  142. package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +1 -0
  143. package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +1 -0
  144. package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +1 -0
  145. package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +1 -0
  146. package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +1 -0
  147. package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +1 -0
  148. package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +1 -0
  149. package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +1 -0
  150. package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +1 -0
  151. package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +1 -0
  152. package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +1 -0
  153. package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +1 -0
  154. package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +1 -0
  155. package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +1 -0
  156. package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +1 -0
  157. package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +1 -0
  158. package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +1 -0
  159. package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +1 -0
  160. package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +1 -0
  161. package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +1 -0
  162. package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +1 -0
  163. package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +1 -0
  164. package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +1 -0
  165. package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +1 -0
  166. package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +1 -0
  167. package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +1 -0
  168. package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +1 -0
  169. package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +1 -0
  170. package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +1 -0
  171. package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +1 -0
  172. package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +1 -0
  173. package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +1 -0
  174. package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +1 -0
  175. package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +1 -0
  176. package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +1 -0
  177. package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +1 -0
  178. package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +1 -0
  179. package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +1 -0
  180. package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +1 -0
  181. package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +1 -0
  182. package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +1 -0
  183. package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +1 -0
  184. package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +1 -0
  185. package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +1 -0
  186. package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +1 -0
  187. package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +1 -0
  188. package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +1 -0
  189. package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +1 -0
  190. package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +1 -0
  191. package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +1 -0
  192. package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +1 -0
  193. package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +1 -0
  194. package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +1 -0
  195. package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +1 -0
  196. package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +1 -0
  197. package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +1 -0
  198. package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +1 -0
  199. package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +1 -0
  200. package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +1 -0
  201. package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +1 -0
  202. package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +1 -0
  203. package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +1 -0
  204. package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +1 -0
  205. package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +1 -0
  206. package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +1 -0
  207. package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +1 -0
  208. package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +1 -0
  209. package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +1 -0
  210. package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +1 -0
  211. package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +1 -0
  212. package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +1 -0
  213. package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +1 -0
  214. package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +1 -0
  215. package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +1 -0
  216. package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +1 -0
  217. package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +1 -0
  218. package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +1 -0
  219. package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +1 -0
  220. package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +1 -0
  221. package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +1 -0
  222. package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +1 -0
  223. package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +1 -0
  224. package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +1 -0
  225. package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +1 -0
  226. package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +1 -0
  227. package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +1 -0
  228. package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +1 -0
  229. package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +1 -0
  230. package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +1 -0
  231. package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +1 -0
  232. package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +1 -0
  233. package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +1 -0
  234. package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +1 -0
  235. package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +1 -0
  236. package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +1 -0
  237. package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +1 -0
  238. package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +1 -0
  239. package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +1 -0
  240. package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +1 -0
  241. package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +1 -0
  242. package/web/app/site/app/assets/instances-BNPptnn-.json +333 -0
  243. package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +1 -0
  244. package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +1 -0
  245. package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +1 -0
  246. package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +1 -0
  247. package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +1 -0
  248. package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +1 -0
  249. package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +1 -0
  250. package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +1 -0
  251. package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +1 -0
  252. package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +1 -0
  253. package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +1 -0
  254. package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +1 -0
  255. package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +1 -0
  256. package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +1 -0
  257. package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +1 -0
  258. package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +1 -0
  259. package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +1 -0
  260. package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +1 -0
  261. package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +1 -0
  262. package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +1 -0
  263. package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +1 -0
  264. package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +1 -0
  265. package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +1 -0
  266. package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +1 -0
  267. package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +1 -0
  268. package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +1 -0
  269. package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +1 -0
  270. package/web/app/site/app/assets/locales/tok-CfheRbCu.js +1 -0
  271. package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +1 -0
  272. package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +1 -0
  273. package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +1 -0
  274. package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +1 -0
  275. package/web/app/site/app/assets/main-BdqNbG-a.js +26 -0
  276. package/web/app/site/app/assets/main-BdqNbG-a.js.map +1 -0
  277. package/web/app/site/app/assets/mock-home-DGhR__He.js +2 -0
  278. package/web/app/site/app/assets/mock-home-DGhR__He.js.map +1 -0
  279. package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
  280. package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  281. package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  282. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +2 -0
  283. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +1 -0
  284. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +20 -0
  285. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +1 -0
  286. package/web/app/site/app/assets/style-B1iLPTPR.css +2 -0
  287. package/web/app/site/app/assets/temml-DuyIQMei.js +97 -0
  288. package/web/app/site/app/assets/temml-DuyIQMei.js.map +1 -0
  289. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +2 -0
  290. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +1 -0
  291. package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
  292. package/web/app/site/app/assets/year-in-posts-DJv_star.js +3 -0
  293. package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +1 -0
  294. package/web/app/site/app/compose/index.html +20 -0
  295. package/web/app/site/app/favicon.ico +0 -0
  296. package/web/app/site/app/index.html +73 -0
  297. package/web/app/site/app/logo-192.png +0 -0
  298. package/web/app/site/app/logo-512.png +0 -0
  299. package/web/app/site/app/logo-badge-72.png +0 -0
  300. package/web/app/site/app/logo-maskable-512.png +0 -0
  301. package/web/app/site/app/logo-monochrome-512.png +0 -0
  302. package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
  303. package/web/app/site/app/manifest.webmanifest +1 -0
  304. package/web/app/site/app/og-image-2.jpg +0 -0
  305. package/web/app/site/app/og-image.png +0 -0
  306. package/web/app/site/app/robots.txt +2 -0
  307. package/web/app/site/app/version.json +1 -0
  308. package/web/app/site/boot.js +34067 -0
  309. package/web/app/site/index.html +203 -0
  310. package/web/app/site/sw.js +53745 -0
  311. package/web/app/sw-src.mjs +205 -0
  312. package/web/front/#new-account.html# +43 -0
  313. package/web/front/admin.html +1 -81
  314. package/web/front/admin.js +85 -0
  315. package/web/front/new-account.html +1 -9
  316. package/web/front/new-account.js +13 -0
  317. package/web/front/run.html +1 -92
  318. package/web/front/run.js +107 -0
  319. package/lib/podrdf.mjs +0 -132
@@ -0,0 +1,77 @@
1
+ // fedify-sig.mjs — a browser stand-in for @fedify/fedify/sig.
2
+ //
3
+ // Fedify itself will not run in a browser: it reaches for node:dns, node:net
4
+ // and node:process to resolve and fetch actors. Only two of its functions are
5
+ // on the agent's path:
6
+ //
7
+ // signRequest — outbound HTTP Signatures (draft-cavage), what a remote
8
+ // server verifies. Implemented here with WebCrypto and
9
+ // proven byte-identical to Fedify's own output for the
10
+ // same request and key (claude/validation/signing-shim/).
11
+ // verifyRequestDetailed — inbound verification. The browser agent never runs
12
+ // it: it trusts the gateway's receipt, exactly as the
13
+ // agent does in the gateway's `trust` mode. Stubbed so a
14
+ // bundled import resolves.
15
+ //
16
+ // A browser cannot set Date or Host on a request (both are forbidden header
17
+ // names) and does not add them itself, so a signature that covers them can only
18
+ // travel through the relay, which sends the signed headers verbatim. That is why
19
+ // the real work is `sign()`, which returns the signed headers as data. The
20
+ // browser deliverer hands that to the relay; `signRequest` wraps it into a
21
+ // Request for the Fedify-shaped callers and the byte-equality proof.
22
+
23
+ const b64 = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)));
24
+
25
+ /**
26
+ * Sign a request and return the parts to send: the covered header names, the
27
+ * header values (Date/Host/Digest filled in), the body, and the target. Host is
28
+ * always signed from the URL; the relay's own fetch sends the matching Host.
29
+ */
30
+ export async function sign({ url, method = 'POST', headers = {}, body }, privateKey, keyId) {
31
+ const u = new URL(url);
32
+ const m = method.toUpperCase();
33
+ const signed = {};
34
+ for (const [k, v] of Object.entries(headers)) {
35
+ const name = k.toLowerCase();
36
+ if (name !== 'signature' && name !== 'host') signed[name] = v;
37
+ }
38
+
39
+ let bodyBytes;
40
+ if (m !== 'GET' && m !== 'HEAD' && body != null) {
41
+ bodyBytes = typeof body === 'string' ? new TextEncoder().encode(body) : new Uint8Array(body);
42
+ if (!('digest' in signed)) signed.digest = `SHA-256=${b64(await crypto.subtle.digest('SHA-256', bodyBytes))}`;
43
+ }
44
+ if (!('date' in signed)) signed.date = new Date().toUTCString();
45
+ signed.host = u.host;
46
+
47
+ const names = ['(request-target)', ...Object.keys(signed).sort()];
48
+ const signingString = names.map((n) => (n === '(request-target)'
49
+ ? `(request-target): ${m.toLowerCase()} ${u.pathname}${u.search}`
50
+ : `${n}: ${signed[n]}`)).join('\n');
51
+ const sig = await crypto.subtle.sign('RSASSA-PKCS1-v1_5', privateKey, new TextEncoder().encode(signingString));
52
+ signed.signature = `keyId="${keyId.href || keyId}",algorithm="rsa-sha256",headers="${names.join(' ')}",signature="${b64(sig)}"`;
53
+
54
+ return { url: u.href, method: m, headers: signed, body: bodyBytes ?? null, covered: names };
55
+ }
56
+
57
+ /** Fedify's signRequest shape: sign, then return a Request. Forbidden headers
58
+ * (Host, Date) survive here only where the runtime allows them (Node); the
59
+ * browser delivery path uses sign() and the relay instead. */
60
+ export async function signRequest(request, privateKey, keyId) {
61
+ const headers = {};
62
+ for (const [k, v] of request.headers) headers[k] = v;
63
+ const body = (request.method === 'GET' || request.method === 'HEAD')
64
+ ? undefined : new Uint8Array(await request.clone().arrayBuffer());
65
+ const s = await sign({ url: request.url, method: request.method, headers, body }, privateKey, keyId);
66
+ const out = new Headers();
67
+ for (const [k, v] of Object.entries(s.headers)) out.set(k, v);
68
+ const init = { method: s.method, headers: out, signal: request.signal };
69
+ if (s.body) init.body = s.body;
70
+ return new Request(request.url, init);
71
+ }
72
+
73
+ // The browser agent verifies nothing itself — the gateway already did, and its
74
+ // receipt is what the drain trusts. Present so the import resolves.
75
+ export async function verifyRequestDetailed() {
76
+ return { success: false, reason: 'the browser agent does not verify; it trusts the gateway receipt' };
77
+ }
@@ -0,0 +1,113 @@
1
+ // node-crypto.mjs — a browser stand-in for node:crypto, for the agent bundle.
2
+ //
3
+ // The agent uses only a small synchronous slice: SHA-256 (and HMAC-SHA-256)
4
+ // hashing, random bytes, constant-time compare — plus WebCrypto (crypto.subtle),
5
+ // which the browser has natively. Rather than pull in crypto-browserify (which
6
+ // drags Node streams into the bundle), SHA-256 is implemented here in pure JS.
7
+ // Key generation and KeyObject calls are never reached: keys are made by
8
+ // keystore.mjs and loaded straight into WebCrypto.
9
+
10
+ const K = new Uint32Array([
11
+ 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
12
+ 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
13
+ 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
14
+ 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
15
+ 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
16
+ 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
17
+ 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
18
+ 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2]);
19
+ const rotr = (x, n) => (x >>> n) | (x << (32 - n));
20
+
21
+ function sha256(bytes) {
22
+ const l = bytes.length;
23
+ const withOne = l + 1;
24
+ const k = (56 - (withOne % 64) + 64) % 64;
25
+ const total = withOne + k + 8;
26
+ const m = new Uint8Array(total);
27
+ m.set(bytes); m[l] = 0x80;
28
+ const bits = l * 8;
29
+ const dv = new DataView(m.buffer);
30
+ dv.setUint32(total - 4, bits >>> 0);
31
+ dv.setUint32(total - 8, Math.floor(bits / 0x100000000));
32
+ const H = new Uint32Array([0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]);
33
+ const w = new Uint32Array(64);
34
+ for (let i = 0; i < total; i += 64) {
35
+ for (let t = 0; t < 16; t++) w[t] = dv.getUint32(i + t * 4);
36
+ for (let t = 16; t < 64; t++) {
37
+ const s0 = rotr(w[t-15],7) ^ rotr(w[t-15],18) ^ (w[t-15] >>> 3);
38
+ const s1 = rotr(w[t-2],17) ^ rotr(w[t-2],19) ^ (w[t-2] >>> 10);
39
+ w[t] = (w[t-16] + s0 + w[t-7] + s1) >>> 0;
40
+ }
41
+ let [a,b,c,d,e,f,g,h] = H;
42
+ for (let t = 0; t < 64; t++) {
43
+ const S1 = rotr(e,6) ^ rotr(e,11) ^ rotr(e,25);
44
+ const ch = (e & f) ^ (~e & g);
45
+ const t1 = (h + S1 + ch + K[t] + w[t]) >>> 0;
46
+ const S0 = rotr(a,2) ^ rotr(a,13) ^ rotr(a,22);
47
+ const maj = (a & b) ^ (a & c) ^ (b & c);
48
+ const t2 = (S0 + maj) >>> 0;
49
+ h=g; g=f; f=e; e=(d+t1)>>>0; d=c; c=b; b=a; a=(t1+t2)>>>0;
50
+ }
51
+ H[0]=(H[0]+a)>>>0; H[1]=(H[1]+b)>>>0; H[2]=(H[2]+c)>>>0; H[3]=(H[3]+d)>>>0;
52
+ H[4]=(H[4]+e)>>>0; H[5]=(H[5]+f)>>>0; H[6]=(H[6]+g)>>>0; H[7]=(H[7]+h)>>>0;
53
+ }
54
+ const out = new Uint8Array(32);
55
+ new DataView(out.buffer).setUint32(0, H[0]); new DataView(out.buffer).setUint32(4, H[1]);
56
+ new DataView(out.buffer).setUint32(8, H[2]); new DataView(out.buffer).setUint32(12, H[3]);
57
+ new DataView(out.buffer).setUint32(16, H[4]); new DataView(out.buffer).setUint32(20, H[5]);
58
+ new DataView(out.buffer).setUint32(24, H[6]); new DataView(out.buffer).setUint32(28, H[7]);
59
+ return out;
60
+ }
61
+ function hmacSha256(key, msg) {
62
+ if (key.length > 64) key = sha256(key);
63
+ const pad = new Uint8Array(64); pad.set(key);
64
+ const ipad = new Uint8Array(64); const opad = new Uint8Array(64);
65
+ for (let i = 0; i < 64; i++) { ipad[i] = pad[i] ^ 0x36; opad[i] = pad[i] ^ 0x5c; }
66
+ const inner = sha256(concat(ipad, msg));
67
+ return sha256(concat(opad, inner));
68
+ }
69
+ const concat = (a, b) => { const o = new Uint8Array(a.length + b.length); o.set(a); o.set(b, a.length); return o; };
70
+ const toBytes = (data, enc) => {
71
+ if (data == null) return new Uint8Array(0);
72
+ if (typeof data === 'string') {
73
+ if (enc === 'hex') return Uint8Array.from(data.match(/.{1,2}/g) || [], (h) => parseInt(h, 16));
74
+ if (enc === 'base64') return Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
75
+ return new TextEncoder().encode(data);
76
+ }
77
+ return data instanceof Uint8Array ? data : new Uint8Array(data);
78
+ };
79
+ const encode = (bytes, enc) => {
80
+ if (!enc || enc === 'buffer') return bytes;
81
+ if (enc === 'hex') return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');
82
+ const b64 = btoa(String.fromCharCode(...bytes));
83
+ if (enc === 'base64url') return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
84
+ return b64; // base64
85
+ };
86
+
87
+ export function createHash(alg) {
88
+ if (alg !== 'sha256') throw new Error(`node-crypto shim: only sha256 is implemented (got ${alg})`);
89
+ let acc = new Uint8Array(0);
90
+ return { update(d, e) { acc = concat(acc, toBytes(d, e)); return this; }, digest(enc) { return encode(sha256(acc), enc); } };
91
+ }
92
+ export function createHmac(alg, key) {
93
+ if (alg !== 'sha256') throw new Error(`node-crypto shim: only hmac-sha256 is implemented (got ${alg})`);
94
+ const k = toBytes(key); let acc = new Uint8Array(0);
95
+ return { update(d, e) { acc = concat(acc, toBytes(d, e)); return this; }, digest(enc) { return encode(hmacSha256(k, acc), enc); } };
96
+ }
97
+ // A Buffer, not a bare Uint8Array, so callers' `.toString('hex')` works (the
98
+ // prelude defines Buffer before any bundle code runs).
99
+ export function randomBytes(n) { const b = new Uint8Array(n); crypto.getRandomValues(b); return globalThis.Buffer ? globalThis.Buffer.from(b) : b; }
100
+ export const webcrypto = globalThis.crypto;
101
+ export const randomUUID = () => globalThis.crypto.randomUUID();
102
+ export function timingSafeEqual(a, b) {
103
+ if (a.length !== b.length) return false;
104
+ let out = 0; for (let i = 0; i < a.length; i++) out |= a[i] ^ b[i];
105
+ return out === 0;
106
+ }
107
+ const unavailable = (name) => () => { throw new Error(`node:crypto ${name} is not available in the browser agent`); };
108
+ export const generateKeyPairSync = unavailable('generateKeyPairSync');
109
+ export const createPrivateKey = unavailable('createPrivateKey');
110
+ export const createPublicKey = unavailable('createPublicKey');
111
+ export const scryptSync = unavailable('scryptSync');
112
+ export default { createHash, createHmac, randomBytes, webcrypto, randomUUID, timingSafeEqual,
113
+ generateKeyPairSync, createPrivateKey, createPublicKey, scryptSync };
@@ -0,0 +1,17 @@
1
+ // node-fs.mjs — the tiny synchronous slice of node:fs touched at import time.
2
+ // ua.mjs and publisher.mjs read package.json for the version; everything else
3
+ // (FileStorage, home, certs) is never constructed in the browser.
4
+ const PKG = JSON.stringify({ version: '0.18.0', name: 'fedipod' });
5
+ export const readFileSync = (p) => {
6
+ if (String(p).endsWith('package.json')) return PKG;
7
+ throw new Error(`node:fs readFileSync(${p}) is not available in the browser agent`);
8
+ };
9
+ export const existsSync = () => false;
10
+ const nope = (name) => () => { throw new Error(`node:fs ${name} is not available in the browser agent`); };
11
+ export const writeFileSync = nope('writeFileSync');
12
+ export const mkdirSync = nope('mkdirSync');
13
+ export const readdirSync = () => [];
14
+ export const statSync = nope('statSync');
15
+ export const unlinkSync = nope('unlinkSync');
16
+ export const rmSync = nope('rmSync');
17
+ export default { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync, rmSync };
@@ -0,0 +1,19 @@
1
+ // node-path.mjs — the POSIX slice of node:path the agent uses in the browser.
2
+ const normalize = (p) => {
3
+ const up = p.startsWith('/');
4
+ const parts = [];
5
+ for (const seg of p.split('/')) {
6
+ if (!seg || seg === '.') continue;
7
+ if (seg === '..') { if (parts.length && parts[parts.length - 1] !== '..') parts.pop(); else if (!up) parts.push('..'); }
8
+ else parts.push(seg);
9
+ }
10
+ return (up ? '/' : '') + parts.join('/') || (up ? '/' : '.');
11
+ };
12
+ const join = (...segs) => normalize(segs.filter((s) => s != null && s !== '').join('/'));
13
+ const dirname = (p) => { const i = p.replace(/\/$/, '').lastIndexOf('/'); return i <= 0 ? (i === 0 ? '/' : '.') : p.slice(0, i); };
14
+ const basename = (p, ext) => { let b = p.slice(p.lastIndexOf('/') + 1); if (ext && b.endsWith(ext)) b = b.slice(0, -ext.length); return b; };
15
+ const extname = (p) => { const b = basename(p); const i = b.lastIndexOf('.'); return i > 0 ? b.slice(i) : ''; };
16
+ const resolve = (...segs) => join(...segs);
17
+ const posix = { join, dirname, basename, extname, resolve, normalize, sep: '/' };
18
+ export { join, dirname, basename, extname, resolve, normalize, posix };
19
+ export default { join, dirname, basename, extname, resolve, normalize, posix, sep: '/' };
@@ -0,0 +1,7 @@
1
+ // node-url.mjs — the node:url slice the agent uses. URL is native; the two
2
+ // file-path converters are only ever called with data: or http URLs in the
3
+ // browser build, so plain string surgery suffices.
4
+ export const fileURLToPath = (u) => { const s = typeof u === 'string' ? u : u.href; return s.replace(/^file:\/\//, ''); };
5
+ export const pathToFileURL = (p) => new URL('file://' + (p.startsWith('/') ? p : '/' + p));
6
+ export { URL, URLSearchParams } from './_globals.mjs';
7
+ export default { URL: globalThis.URL, URLSearchParams: globalThis.URLSearchParams, fileURLToPath, pathToFileURL };
@@ -0,0 +1,58 @@
1
+ // prelude.js — globals the bundled Node code expects, set once before it runs.
2
+ // Injected as the bundle's banner. Kept minimal and local: Buffer is a subclass
3
+ // of Uint8Array (not a prototype patch), so nothing else in the page is touched.
4
+ globalThis.process ??= { env: {}, argv: [], platform: 'browser', cwd: () => '/', nextTick: (f, ...a) => queueMicrotask(() => f(...a)) };
5
+ globalThis.Buffer ??= (() => {
6
+ const b64 = (u) => btoa(String.fromCharCode(...u));
7
+ const hex = (u) => { let s = ''; for (const b of u) s += b.toString(16).padStart(2, '0'); return s; };
8
+ class Buf extends Uint8Array {
9
+ // Uint8Array.indexOf finds a single BYTE; Node's Buffer finds a whole
10
+ // subsequence, and lib/mastoapi.mjs's readMultipart walks a multipart body
11
+ // by searching for the boundary and for the CRLFCRLF that ends each part's
12
+ // headers. Without this every search returned -1, so an upload came back
13
+ // "file required" however good the bytes were.
14
+ indexOf(needle, from = 0) {
15
+ const pat = typeof needle === 'string' ? new TextEncoder().encode(needle)
16
+ : needle instanceof Uint8Array ? needle : null;
17
+ if (pat === null) return super.indexOf(needle, from); // a single byte
18
+ if (pat.length === 0) return Math.min(from, this.length);
19
+ const last = this.length - pat.length;
20
+ outer: for (let i = Math.max(0, from); i <= last; i++) {
21
+ for (let j = 0; j < pat.length; j++) if (this[i + j] !== pat[j]) continue outer;
22
+ return i;
23
+ }
24
+ return -1;
25
+ }
26
+ toString(enc) {
27
+ if (enc === 'hex') return hex(this);
28
+ if (enc === 'base64') return b64(this);
29
+ if (enc === 'base64url') return b64(this).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
30
+ return new TextDecoder().decode(this);
31
+ }
32
+ }
33
+ const fromString = (s, enc) => {
34
+ if (enc === 'hex') return Buf.from(s.match(/.{1,2}/g) || [], (h) => parseInt(h, 16));
35
+ if (enc === 'base64' || enc === 'base64url') {
36
+ const n = s.replace(/-/g, '+').replace(/_/g, '/');
37
+ return Buf.from(atob(n + '==='.slice((n.length + 3) % 4)), (c) => c.charCodeAt(0));
38
+ }
39
+ return Buf.from(new TextEncoder().encode(s));
40
+ };
41
+ const from = (d, enc) => {
42
+ if (typeof d === 'string') return fromString(d, enc);
43
+ if (typeof d === 'function') { const args = arguments; return Uint8Array.from.call(Buf, args[0], args[1]); }
44
+ const b = new Buf(d.length); b.set(d); return b;
45
+ };
46
+ const fromAny = (d, e) => {
47
+ if (typeof d === 'string') return fromString(d, e);
48
+ if (typeof e === 'function') { const arr = Array.from(d, e); const b = new Buf(arr.length); b.set(arr); return b; }
49
+ if (d instanceof Uint8Array || Array.isArray(d)) { const b = new Buf(d.length); b.set(d); return b; }
50
+ return new Buf(d);
51
+ };
52
+ return {
53
+ from: fromAny,
54
+ concat: (list) => { const n = list.reduce((s, x) => s + x.length, 0); const o = new Buf(n); let i = 0; for (const x of list) { o.set(x, i); i += x.length; } return o; },
55
+ alloc: (n) => new Buf(n),
56
+ isBuffer: (x) => x instanceof Uint8Array,
57
+ };
58
+ })();
@@ -0,0 +1,58 @@
1
+ // safefetch.mjs — browser stand-in for lib/safefetch.mjs.
2
+ //
3
+ // The Node version pins connections to a validated IP to stop DNS-rebinding and
4
+ // refuses private targets, using undici and node:dns. A browser cannot open raw
5
+ // sockets and is bound by the same-origin policy and CORS, so the rebinding
6
+ // attack this defends against is not reachable from here; the browser agent's
7
+ // only cross-origin writes go through the relay, which runs the Node checks.
8
+ // This keeps the same surface with plain fetch and no top-level await.
9
+ export const HTTP_TIMEOUT_MS = 20_000;
10
+ const MAX_BYTES = 5 * 1024 * 1024;
11
+
12
+ export function retryAfterMs(res, max = 30 * 60_000) {
13
+ const h = res?.headers?.get?.('retry-after');
14
+ if (!h) return null;
15
+ const secs = /^\d+$/.test(h.trim()) ? Number(h) * 1000 : (Date.parse(h) - Date.now());
16
+ return Number.isFinite(secs) ? Math.max(0, Math.min(secs, max)) : null;
17
+ }
18
+ export function isPrivateAddress() { return false; }
19
+ export function isLoopbackHost(host) { return /^(localhost|127\.|\[?::1)/.test(String(host)); }
20
+ export function insecureUrlReason(url, what = 'address') {
21
+ try { const u = new URL(url); if (u.protocol === 'https:' || isLoopbackHost(u.hostname)) return null;
22
+ if (u.protocol !== 'http:') return `the ${what} must be http(s)`; return null; } catch { return `"${url}" is not a ${what}`; }
23
+ }
24
+ export async function assertPublicUrl() { return null; } // no socket pinning in a browser
25
+ export async function pinnedFor() { return undefined; }
26
+ // The one thing in this file that is NOT made unnecessary by being in a
27
+ // browser. Pinning and the private-address checks answer DNS rebinding, which
28
+ // a browser closes on its own; a byte budget answers a server that simply
29
+ // keeps sending, which it does not. This used to check `content-length` and
30
+ // then hand back `res.text()` — and a chunked response carries no
31
+ // `content-length` at all, so every caller below dereferences a URL a stranger
32
+ // chose (the note, the actor, the receipt, the Tombstone check, WebFinger)
33
+ // with no bound on what comes back. Stream it and stop at the budget, the way
34
+ // the Node version does. TextDecoder rather than Buffer, and `stream: true` so
35
+ // a UTF-8 sequence split across two chunks still decodes.
36
+ export async function readCapped(res, max = MAX_BYTES) {
37
+ const len = Number(res.headers?.get?.('content-length') || 0);
38
+ if (len > max) throw new Error(`response too large (${len} bytes)`);
39
+ if (!res.body) return res.text();
40
+ const reader = res.body.getReader();
41
+ const decoder = new TextDecoder('utf-8');
42
+ let out = '';
43
+ let total = 0;
44
+ for (;;) {
45
+ const { done, value } = await reader.read();
46
+ if (done) break;
47
+ total += value.length;
48
+ if (total > max) {
49
+ await reader.cancel();
50
+ throw new Error(`response exceeded ${max} bytes`);
51
+ }
52
+ out += decoder.decode(value, { stream: true });
53
+ }
54
+ return out + decoder.decode();
55
+ }
56
+ export async function safeFetch(url, init = {}, fetchImpl = fetch) {
57
+ return fetchImpl(url, { ...init, signal: init.signal || AbortSignal.timeout(HTTP_TIMEOUT_MS) });
58
+ }
@@ -0,0 +1,14 @@
1
+ // web-push.mjs — browser stand-in. The facade needs a VAPID public key to put
2
+ // in its instance document; server-sent push while the tab is closed is not
3
+ // part of the browser build, so sendNotification is a no-op. The keypair is a
4
+ // well-formed placeholder (a real one needs a matching private key, which the
5
+ // browser push model does not use this way).
6
+ const b64u = (u) => btoa(String.fromCharCode(...u)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
7
+ export function generateVAPIDKeys() {
8
+ const pub = new Uint8Array(65); pub[0] = 0x04; crypto.getRandomValues(pub.subarray(1));
9
+ const priv = new Uint8Array(32); crypto.getRandomValues(priv);
10
+ return { publicKey: b64u(pub), privateKey: b64u(priv) };
11
+ }
12
+ export function setVapidDetails() { /* no-op */ }
13
+ export async function sendNotification() { return { statusCode: 201, body: '', headers: {} }; }
14
+ export default { generateVAPIDKeys, setVapidDetails, sendNotification };
@@ -0,0 +1,248 @@
1
+ // signup.mjs — the setup flow of `fedipod setup`, run in the browser. It does
2
+ // what lib/setup.mjs's runSetup does up to the publish step: create the account
3
+ // and pod (or accept one you bring), mint a credential, make the signing keys
4
+ // and lock them on the pod under the password. Publishing the actor is the
5
+ // agent's first-run job, exactly as `connect()` publishes after `setup` mints —
6
+ // so this module needs nothing from lib.
7
+ //
8
+ // It returns the two things a browser must keep for itself: the credential and
9
+ // the config. Neither is secret to the pod owner, but the credential is a pod
10
+ // write key, so the caller stores it in this browser only, never on the pod.
11
+ // The only thing this writes to the pod is the password-wrapped signing key.
12
+
13
+ import { createAccountWithPod, mintCredential, makeDpopSession, revokeCredential } from './pod-auth.mjs';
14
+ import { generateKeys, wrapKeys } from './keystore.mjs';
15
+ import { BrowserRemotePod } from './pod-remote.mjs';
16
+ import * as podState from '../../lib/pod/state.mjs';
17
+ import { kvPut } from './idb-kv.mjs';
18
+ import { keyCacheKey } from './keys-browser.mjs';
19
+
20
+ // The container everything the agent publishes hangs under. New pods made here
21
+ // use `fedipod/`; the agent's own default stays `activitypods-js/` for installs
22
+ // that predate this, so those pods are untouched. The name is stored on the
23
+ // credential and the config, so the agent reads it rather than guessing.
24
+ export const AP_ROOT = 'fedipod/';
25
+ const actorUrlFor = (pod) => `${pod}${AP_ROOT}ap/actor`;
26
+ const keysDocFor = (pod) => `${pod}${AP_ROOT}ap-state/keys.json`;
27
+
28
+ const HANDLE_RE = /^[a-z0-9-]{2,30}$/;
29
+
30
+ export function handleProblem(handle) {
31
+ if (!handle) return 'a handle is required';
32
+ if (!HANDLE_RE.test(handle)) return 'letters, digits and hyphens only, 2–30 characters';
33
+ if (handle.startsWith('-') || handle.endsWith('-')) return 'cannot start or end with a hyphen';
34
+ return null;
35
+ }
36
+
37
+ // Resume across a failed attempt. A run that throws part-way keeps what it
38
+ // already achieved in this page session — the pod it made, the credential it
39
+ // minted, the key it wrote — so a second attempt (the failure screen's Try
40
+ // again) continues from the first unfinished step instead of restarting. The
41
+ // record lives only in memory here: no secret is written to disk, and it is
42
+ // dropped the moment the run completes. Keyed by the identity being built, so
43
+ // changing the handle or pod on the form starts a clean attempt.
44
+ const PROGRESS = new Map();
45
+ const progressKey = (a) => [a.issuer, a.mode, a.handle, a.mode === 'new' ? (a.podName || a.handle) : a.pod].join('|');
46
+
47
+ /**
48
+ * Run sign-up. Steps are reported through onStep(key, state, note) so a page can
49
+ * draw the same tick list the CLI setup shows. Resumable: see PROGRESS above.
50
+ *
51
+ * answers: { mode:'new'|'existing', issuer, email, password, handle,
52
+ * podName?, pod?, gateway? }
53
+ * returns: { credential, config, actorUrl, address, keysPublic }
54
+ */
55
+ export async function signUp(answers, { onStep = () => {}, frontOrigin = null } = {}) {
56
+ const { mode, issuer, email, password, handle } = answers;
57
+ const bad = handleProblem(handle);
58
+ if (bad) throw new Error(bad);
59
+ if (!email) throw new Error('an email is required'); // recovery + account login
60
+ if (!password) throw new Error('a password is required');
61
+ if (mode === 'existing' && !answers.pod) throw new Error('a pod address is required');
62
+
63
+ const key = progressKey(answers);
64
+ const prog = PROGRESS.get(key) || {};
65
+ PROGRESS.set(key, prog); // resume record for this identity
66
+
67
+ const step = (key) => ({
68
+ running: (note) => onStep(key, 'running', note),
69
+ ok: (note) => onStep(key, 'ok', note),
70
+ skip: (note) => onStep(key, 'skipped', note),
71
+ });
72
+
73
+ // --- account + pod --- (skipped outright once a prior attempt has made it)
74
+ let accountToken = null; // valid only within this call
75
+ const acct = step('account');
76
+ if (!prog.pod) {
77
+ if (mode === 'new') {
78
+ acct.running('creating the account and pod');
79
+ const made = await createAccountWithPod({ issuer, email, password, podName: answers.podName || handle });
80
+ prog.pod = made.pod; prog.webId = made.webId; accountToken = made.accountToken;
81
+ acct.ok(made.pod);
82
+ } else {
83
+ const brought = answers.pod.endsWith('/') ? answers.pod : answers.pod + '/';
84
+ acct.running('checking your pod');
85
+ const head = await fetch(brought, { method: 'HEAD' }).catch(() => null);
86
+ if (!head || head.status >= 400) throw new Error(`the pod at ${brought} did not answer (HTTP ${head?.status || 'no response'})`);
87
+ prog.pod = brought;
88
+ acct.skip('using the pod you brought');
89
+ }
90
+ } else {
91
+ acct.ok(prog.pod); // resumed: the pod is already there
92
+ }
93
+ const pod = prog.pod; const webId = prog.webId || null;
94
+
95
+ // WebFinger is answered at a host root, so the address only works if the pod
96
+ // is the root of its own host. Refuse a path pod rather than making an
97
+ // account nobody can find.
98
+ const podUrl = new URL(pod);
99
+ if (podUrl.pathname !== '/') {
100
+ throw new Error(`${pod} is a path on ${podUrl.host}, not its own host. `
101
+ + 'A Fediverse address lives at a host root, so this pod cannot carry one. '
102
+ + 'Use a pod that is the root of its own subdomain.');
103
+ }
104
+
105
+ const actorUrl = actorUrlFor(pod);
106
+
107
+ // --- credential --- (a fresh attempt re-mints; a resumed one reuses it)
108
+ const cred = step('credential');
109
+ let credential;
110
+ if (!prog.credential) {
111
+ cred.running('minting a credential for this browser');
112
+ // accountToken is null when the account step was skipped (a resume), so
113
+ // mintCredential logs in fresh with email+password rather than reusing a
114
+ // token that a prior attempt may have let go stale.
115
+ credential = await mintCredential({ issuer, email, password, webId, podUrl: pod, accountToken });
116
+ credential.remotePod = pod;
117
+ credential.root = AP_ROOT; // the agent reads the container name from here
118
+ prog.credential = credential;
119
+ cred.ok();
120
+ } else {
121
+ credential = prog.credential;
122
+ cred.ok();
123
+ }
124
+
125
+ // A pod-writing session for the remaining pod writes and the gateway proof.
126
+ const session = await makeDpopSession(credential);
127
+
128
+ // --- keys, encrypted, in an owner-only container on the pod ---
129
+ //
130
+ // The durable copy is on the pod, because a browser has no disk you can carry
131
+ // to the next machine. That copy used to be the bare record, on the argument
132
+ // that the pod's owner-only ACL is protection enough and the pod host already
133
+ // holds your data. It is not the same thing: your data is your data, and the
134
+ // signing key IS you — whoever holds it is you to every server in the
135
+ // fediverse, for as long as the key lives, and no ACL reaches the host itself.
136
+ // So it is wrapped under the account password first, and the host stores
137
+ // ciphertext. (Three documents already said this was happening. Now it is.)
138
+ //
139
+ // Two things in that order, deliberately: the ACL BEFORE the key. Writing the
140
+ // key first leaves a window where a pod whose root is world-readable serves it
141
+ // to anyone who asks, and a brought pod is exactly the case where that root
142
+ // may be public.
143
+ //
144
+ // The opened copy is kept in this browser (IndexedDB) so the worker can boot
145
+ // itself after an idle kill with nobody there to type a password. A browser
146
+ // that has no copy asks for the password once — see boot.mjs.
147
+ const keysStep = step('keys');
148
+ let keys;
149
+ if (!prog.keysStored) {
150
+ keysStep.running('making your signing key and locking it under your password');
151
+ keys = await generateKeys();
152
+ keys.mintedFor = actorUrl; // one key, one actor (lib/keys.mjs)
153
+ const remote = new BrowserRemotePod(session, { webId: credential.webId, role: 'signup', log: () => {} });
154
+ // Owner-only, and THEN the key — one operation, so the order cannot be got
155
+ // wrong here or anywhere else. A pod that refuses the ACL write is not a
156
+ // pod this key may sit on, wrapped or not.
157
+ try {
158
+ await podState.provisionKey(remote, {
159
+ stateUrl: `${pod}${AP_ROOT}ap-state/`,
160
+ keysUrl: keysDocFor(pod),
161
+ envelope: await wrapKeys(keys, password),
162
+ });
163
+ } catch (e) {
164
+ throw new Error(`could not store the signing key on the pod (${e.message}). `
165
+ + `The credential is for ${credential.webId} — that WebID must own ${pod} and its ${AP_ROOT} must be writable by it.`);
166
+ }
167
+ // This browser's own opened copy, so the boot after the login redirect
168
+ // needs no password. Best effort: a browser that refuses IndexedDB (private
169
+ // mode) simply asks for the password on the way back in.
170
+ await kvPut(keyCacheKey(actorUrl), keys).catch(() => {});
171
+ prog.keys = keys; prog.keysStored = true;
172
+ keysStep.ok();
173
+ } else {
174
+ keys = prog.keys;
175
+ keysStep.ok();
176
+ }
177
+
178
+ // Connect the mail door: attach to the gateway (fedipod.net), inbox-only, so
179
+ // the actor advertises the door as its inbox. The door verifies each delivery
180
+ // and forwards the clean mail to the pod; the agent trusts the door's receipt.
181
+ // The same DPoP session proves the pod to the gateway — no password reaches it.
182
+ let gateway = answers.gateway || prog.gateway || null;
183
+ if (frontOrigin && !gateway) {
184
+ const gw = step('gateway');
185
+ gw.running(`connecting your mail door on ${new URL(frontOrigin).host}`);
186
+ const res = await session.fetch(`${frontOrigin.replace(/\/$/, '')}/api/attach`, {
187
+ method: 'POST', headers: { 'content-type': 'application/json' },
188
+ // podHome is the AP CONTAINER, not the pod root: the front builds the
189
+ // delivery target as `podHome + 'ap/inbox/'` (lib/front-core.mjs), so a
190
+ // bare pod root sends this identity's mail to <pod>/ap/inbox/ — outside
191
+ // the container the agent drains, where nothing would ever read it. The
192
+ // manage surface has always sent `urls.home`; this is the same value.
193
+ body: JSON.stringify({ handle, podHome: `${pod}${AP_ROOT}`, actorUrl, kind: 'person' }),
194
+ });
195
+ const d = await res.json().catch(() => ({}));
196
+ if (res.status !== 201 || !d.hmacSecret) {
197
+ throw new Error(`could not connect the mail door (HTTP ${res.status}): ${d.error || ''}`);
198
+ }
199
+ gateway = { url: d.doorInbox, hmacSecret: d.hmacSecret, mode: 'trust' };
200
+ prog.gateway = gateway;
201
+ gw.ok();
202
+ } else if (frontOrigin && gateway) {
203
+ step('gateway').ok(); // resumed
204
+ }
205
+
206
+ const config = {
207
+ remotePod: pod, root: AP_ROOT, handle, name: handle, issuer: credential.issuerOrigin,
208
+ ...(gateway ? { gateway } : {}),
209
+ };
210
+ // Write the config to the pod (owner-only, beside the key) so a returning
211
+ // sign-in — which arrives with only an OIDC session — can read the account's
212
+ // config and key from the pod and boot with no password.
213
+ const cfgRemote = new BrowserRemotePod(session, { webId: credential.webId, role: 'signup', log: () => {} });
214
+ try {
215
+ await podState.writeConfig(cfgRemote, { state: `${pod}${AP_ROOT}ap-state/` }, config);
216
+ } catch (e) {
217
+ throw new Error(`could not store the config on the pod (${e.message})`);
218
+ }
219
+
220
+ // The credential was for sign-up, and sign-up is over. The agent runs on the
221
+ // Solid-OIDC session from here on and never needs it again, so leaving it
222
+ // alive would leave permanent full access to the pod in a key nothing holds.
223
+ // Best effort: a server that will not delete it is a credential the owner can
224
+ // still revoke from their pod's account page, and not a reason to fail a
225
+ // sign-up that otherwise worked.
226
+ const revoked = await revokeCredential({
227
+ resource: credential.resource, accountToken: credential.accountToken,
228
+ });
229
+ delete credential.accountToken; // never leaves this function
230
+ if (!revoked && credential.resource) {
231
+ onStep('credential', 'ok', 'this browser is ready (the setup credential could not be '
232
+ + 'revoked automatically — you can remove it from your pod\'s account page)');
233
+ }
234
+
235
+ PROGRESS.delete(key); // finished — nothing left to resume
236
+
237
+ const host = new URL(pod).host;
238
+ return {
239
+ credential, config, actorUrl,
240
+ address: `@${handle}@${host}`,
241
+ // The opened keys, for booting the agent in THIS browser session right away.
242
+ // The durable copy on the pod is wrapped under the account password; this
243
+ // browser also holds an opened one in IndexedDB (above), which is what the
244
+ // worker reads. A fresh browser asks for the password once and makes its own.
245
+ keys,
246
+ keysPublic: { rsa: keys.rsa.publicPem, ed25519: keys.ed25519?.publicPem || null },
247
+ };
248
+ }
@@ -0,0 +1,9 @@
1
+ /app/*
2
+ Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data: blob:; media-src 'self' https: blob:; connect-src 'self' https:; font-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'self'; form-action 'self'
3
+ X-Content-Type-Options: nosniff
4
+ Referrer-Policy: same-origin
5
+
6
+ /admin/*
7
+ Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data: blob:; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'; form-action 'self'
8
+ X-Content-Type-Options: nosniff
9
+ Referrer-Policy: same-origin
@@ -0,0 +1,21 @@
1
+ # gateway + front API → the function
2
+ /api/handle /.netlify/functions/front 200
3
+ /api/attach /.netlify/functions/front 200
4
+ /api/relay /.netlify/functions/front 200
5
+ /api/roster /.netlify/functions/front 200
6
+ /api/revoke /.netlify/functions/front 200
7
+ /api/agent /.netlify/functions/front 200
8
+ /u/* /.netlify/functions/front 200
9
+ /.well-known/* /.netlify/functions/front 200
10
+ /install /.netlify/functions/front 200
11
+ /run /.netlify/functions/front 200
12
+ /roster /.netlify/functions/front 200
13
+ /solid-oidc-client.js /.netlify/functions/front 200
14
+ # the in-browser app, served static (these win over the project catch-all)
15
+ / /index.html 200
16
+ /sw.js /sw.js 200
17
+ /boot.js /boot.js 200
18
+ /app /app/ 301
19
+ /app/* /app/:splat 200
20
+ /admin /admin/ 301
21
+ /admin/* /admin/:splat 200