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,205 @@
1
+ // sw-src.mjs — the service worker that hosts the in-browser agent.
2
+ //
3
+ // Phanpy (served static on this origin) calls the Mastodon API on its own
4
+ // origin. This worker answers those calls from the agent running inside it, so
5
+ // there is no server behind the client. The page boots the agent by posting it
6
+ // the sign-up material (credential, config, plaintext keys); the worker holds
7
+ // the agent for the life of the worker and intercepts the facade paths. Static
8
+ // files fall through to the network.
9
+ import { BrowserAgent } from './agent.mjs';
10
+ import { getSession } from './oidc-session.mjs';
11
+ import { ADMIN_PATHS } from './admin-facade.mjs';
12
+
13
+ let agent = null;
14
+ let booting = null;
15
+
16
+ const FACADE = /^\/(api\/v[12]\/|oauth\/|nodeinfo\/)/; // Mastodon paths only — never the front's own /api/handle, /api/attach, /api/relay
17
+ const FACADE_EXACT = new Set(['/.well-known/nodeinfo', '/.well-known/oauth-authorization-server', '/nodeinfo/2.0']);
18
+ const isFacade = (p) => FACADE.test(p) || FACADE_EXACT.has(p);
19
+ // The owner's record/manage data endpoints (see admin-facade.mjs). The web/admin
20
+ // PAGES are static files Netlify serves; only these data calls are ours.
21
+ const isAdmin = (p) => ADMIN_PATHS.has(p);
22
+
23
+ // ---- who may drive these routes ----
24
+ //
25
+ // Same-origin is the WHOLE trust boundary for this build. The worker answers
26
+ // the owner's admin routes with no password and no token — the only interlock
27
+ // on the destructive ones is `confirm === handle`, and the handle is public —
28
+ // so without this gate any page the owner merely visited could move every
29
+ // follower to an attacker's account, retire the identity, rotate the key,
30
+ // point the inbox at an attacker's gateway, or set a UI password the owner
31
+ // does not know. The Node agent is covered by lib/guard.mjs; nothing from
32
+ // that file is in this bundle, and it could not do the job here anyway —
33
+ // `Sec-Fetch-*` headers are NOT visible inside a service worker's fetch
34
+ // event, and neither is `Origin`. So the gate is built from what a worker
35
+ // CAN see: the request's mode, its referrer, and its headers.
36
+ //
37
+ // The header is what does the work, and it does not need to be a secret to do
38
+ // it. A cross-site FORM cannot set a header at all. A cross-origin fetch()
39
+ // that sets one is a preflighted request, and nothing here ever answers a
40
+ // preflight with CORS headers, so the browser never sends the real one. That
41
+ // leaves a top-level navigation, which also carries no custom headers.
42
+ //
43
+ // (The review suggested making it a per-session secret handed to the page by
44
+ // postMessage. Deliberately not carried: the header already refuses every
45
+ // cross-ORIGIN caller, and a secret would only add something against an
46
+ // attacker already running script on this origin — who could read it straight
47
+ // out of the page. The defence at that layer is the CSP, in the site's
48
+ // _headers.)
49
+ const PAGE_HEADER = 'x-fedipod-page';
50
+
51
+ // The one route that MUST survive a cross-site navigation: the other server's
52
+ // OAuth redirect lands the browser here, from their origin, by design. Its
53
+ // `state` parameter is what stands in for the header.
54
+ const NAV_ALLOWED = new Set(['/fediacct/callback']);
55
+
56
+ const sameOriginReferrer = (request) => {
57
+ const r = request.referrer;
58
+ // 'about:client' is the browser saying "the context that asked", which for a
59
+ // request this worker intercepts is a page on this origin.
60
+ if (!r || r === 'about:client') return r === 'about:client';
61
+ try { return new URL(r).origin === self.location.origin; } catch { return false; }
62
+ };
63
+
64
+ // Returns null when the request may proceed, else the reason to refuse with.
65
+ function notAllowed(request, url) {
66
+ const p = url.pathname;
67
+
68
+ // The Mastodon client API is deliberately open, exactly as it is on the Node
69
+ // agent and on any real instance: a bearer is its credential, and a client
70
+ // has to be able to call it. It mints nothing without one.
71
+ if (!isAdmin(p) && !p.startsWith('/oauth/')) return null;
72
+
73
+ if (request.mode === 'navigate') {
74
+ if (NAV_ALLOWED.has(p)) return null;
75
+ // The bundled client signs in by navigating its frame to /oauth/authorize
76
+ // from a page on this origin. A navigation from anywhere else — or from
77
+ // nowhere, which is what a page that suppressed its referrer looks like —
78
+ // is not that, and /oauth/authorize is where a 90-day bearer is minted.
79
+ if (p.startsWith('/oauth/') && sameOriginReferrer(request)) return null;
80
+ return 'a navigation may not drive this route';
81
+ }
82
+
83
+ // /oauth/* is not open the way /api/* is: the only client here is the one
84
+ // served from this origin, so a call from another origin is never legitimate.
85
+ if (p.startsWith('/oauth/')) {
86
+ return sameOriginReferrer(request) ? null : 'this route answers this origin only';
87
+ }
88
+
89
+ if (request.headers.get(PAGE_HEADER) !== '1') return `missing ${PAGE_HEADER}`;
90
+ // Belt and braces on top of the header: the facade parses a body as JSON
91
+ // whatever its content type, and `text/plain` is the one a cross-site form
92
+ // can send. Nothing that got past the header sends anything else anyway.
93
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
94
+ const ct = (request.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
95
+ if (ct && ct !== 'application/json') return `unexpected content type "${ct}"`;
96
+ }
97
+ return null;
98
+ }
99
+
100
+ self.addEventListener('install', () => self.skipWaiting());
101
+ self.addEventListener('activate', (e) => e.waitUntil(self.clients.claim()));
102
+
103
+ // Boot the agent from the stored Solid-OIDC session. The sign-in page posts a
104
+ // 'boot' to prime it, but the worker ALSO boots itself on demand (see serve):
105
+ // browsers kill an idle service worker and restart it fresh, and the client at
106
+ // /app/ never posts 'boot' — so without on-demand boot every request after a
107
+ // restart 503s. On a boot failure `booting` is cleared so the next request retries.
108
+ async function bootFromSession(frontOrigin) {
109
+ const oidc = await getSession();
110
+ if (!oidc) throw new Error('no session — sign in first');
111
+ const a = new BrowserAgent({ log: (m) => console.log('[sw-agent]', m) });
112
+ await a.boot({ oidc, frontOrigin: frontOrigin || self.location.origin });
113
+ agent = a;
114
+ return a;
115
+ }
116
+ function ensureBooting(frontOrigin) {
117
+ if (agent) return Promise.resolve(agent);
118
+ if (!booting) booting = bootFromSession(frontOrigin).catch((err) => { booting = null; throw err; });
119
+ return booting;
120
+ }
121
+
122
+ self.addEventListener('message', (e) => {
123
+ // Drop the identity this worker is holding. A worker outlives a page, so
124
+ // signing in as somebody else left the FIRST account's agent live and serving
125
+ // — the new session sat in IndexedDB while every request was still answered
126
+ // by the old actor, with the old key. The page posts this before it boots the
127
+ // new one; the next request boots from whatever session is stored now.
128
+ if (e.data?.type === 'reset') {
129
+ const had = !!agent;
130
+ agent = null; booting = null;
131
+ e.source?.postMessage({ type: 'reset-done', had });
132
+ return;
133
+ }
134
+ if (e.data?.type !== 'boot') return;
135
+ ensureBooting(e.data.frontOrigin)
136
+ .then(() => e.source?.postMessage({ type: 'booted' }))
137
+ // `code` matters: a boot that failed only because this browser has never
138
+ // opened the signing key is not a failure the page should show as one — it
139
+ // is a question the page can answer (boot.mjs). Everything else is shown.
140
+ .catch((err) => e.source?.postMessage({ type: 'boot-error', error: err.message,
141
+ code: err.code || null, stack: String(err.stack || '') }));
142
+ });
143
+
144
+ self.addEventListener('fetch', (e) => {
145
+ const url = new URL(e.request.url);
146
+ if (url.origin !== self.location.origin || !(isFacade(url.pathname) || isAdmin(url.pathname))) return; // static → network
147
+ e.respondWith(serve(e.request, url));
148
+ });
149
+
150
+ async function serve(request, url) {
151
+ // Before anything is booted or read: is this caller allowed to be here at all?
152
+ const refuse = notAllowed(request, url);
153
+ if (refuse) {
154
+ console.warn(`[sw-agent] refused ${request.method} ${url.pathname}: ${refuse}`);
155
+ return json(403, { error: `refused: ${refuse}` });
156
+ }
157
+ // Boot on demand from the stored session, so the client works even when the
158
+ // worker was restarted (idle-killed) and nobody posted 'boot' this time.
159
+ if (!agent) { try { await ensureBooting(); } catch { /* no session → 503 below */ } }
160
+ if (!agent) return json(503, { error: 'the agent is not booted yet — open the app from the sign-in page' });
161
+ // Bytes, not text. A multipart upload is binary — read as text it comes back
162
+ // through a UTF-8 round trip that replaces every byte that is not valid UTF-8,
163
+ // which is most of a JPEG, so the boundary search found nothing and every
164
+ // media and avatar upload answered "422 file required". readBody() does
165
+ // `data += chunk`, which decodes a Buffer the same way it always did, so the
166
+ // JSON and form paths are unchanged.
167
+ const bodyBytes = (request.method === 'GET' || request.method === 'HEAD')
168
+ ? null : Buffer.from(new Uint8Array(await request.arrayBuffer()));
169
+ // The admin facade is JSON-only (the gate above enforces the content type),
170
+ // and takes its body already decoded.
171
+ const bodyText = bodyBytes ? new TextDecoder().decode(bodyBytes) : '';
172
+ const reqHeaders = {}; for (const [k, v] of request.headers) reqHeaders[k.toLowerCase()] = v;
173
+ // `host` is a forbidden header name, so a fetch Request never carries one and
174
+ // the loop above cannot produce it — but it is a header every real request
175
+ // arrives with, and the agent reads it to say where it lives. Without it the
176
+ // notifications `Link` header named `https://undefined/`, so a client that
177
+ // paged by following it (which is how a client is meant to page) walked off
178
+ // the origin and saw nothing past the first screen.
179
+ reqHeaders.host = url.host;
180
+ const listeners = {};
181
+ const req = { method: request.method, url: url.pathname + url.search, headers: reqHeaders,
182
+ // notAllowed() above let this through, so it came from this origin. Said
183
+ // out loud rather than left implicit: MastoApi asks (through the
184
+ // authorities object in agent.mjs) whether a request is the owner's own,
185
+ // and in a browser that question means exactly this.
186
+ sameOrigin: true,
187
+ socket: { encrypted: url.protocol === 'https:' }, on(ev, cb) { (listeners[ev] ||= []).push(cb); return req; }, destroy() {} };
188
+ queueMicrotask(() => { if (bodyBytes?.length) (listeners.data || []).forEach((cb) => cb(bodyBytes)); (listeners.end || []).forEach((cb) => cb()); });
189
+ let status = 200; const outHeaders = {}; const chunks = [];
190
+ const res = {
191
+ writeHead(s, h) { status = s; if (h) Object.assign(outHeaders, h); return res; },
192
+ setHeader(k, v) { outHeaders[k] = v; }, getHeader(k) { return outHeaders[k]; },
193
+ write(c) { chunks.push(c); }, end(c) { if (c) chunks.push(c); },
194
+ };
195
+ try {
196
+ const handled = isAdmin(url.pathname)
197
+ ? await agent.admin.handle(req, res, url.pathname, url, bodyText)
198
+ : await agent.masto.handle(req, res, url.pathname, url);
199
+ if (!handled) return fetch(request);
200
+ return new Response(chunks.join(''), { status, headers: { 'content-type': 'application/json', ...outHeaders } });
201
+ } catch (err) {
202
+ return json(500, { error: err.message });
203
+ }
204
+ }
205
+ const json = (status, obj) => new Response(JSON.stringify(obj), { status, headers: { 'content-type': 'application/json' } });
@@ -0,0 +1,43 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>FediPod — access the Fediverse and ATProto from a Solid pod</title>
7
+ <style>
8
+ :root { color-scheme: light dark; --line:#d9d4c7; --dim:#45443d; --accent:#6a8a5a; }
9
+ @media (prefers-color-scheme: dark) { :root { --dim:#c2c0b4; } }
10
+ body { font: 19px/1.6 system-ui, sans-serif; max-width: 44rem; margin: 2rem auto; padding: 0 1.1rem; }
11
+ h1 { font-size: 1.7rem; margin: 0 0 .2rem; }
12
+ h2 { font-size: 1.2rem; margin: 1.4rem 0 .4rem; }
13
+ p.lede { color: var(--dim); margin-top: 0; }
14
+ .hint { color: var(--dim); font-size: .95rem; }
15
+ code { background: rgba(120,120,120,.12); padding: .05em .35em; border-radius: 4px; }
16
+ pre { background: rgba(120,120,120,.12); padding: .7rem .9rem; border-radius: 8px; overflow-x: auto; }
17
+ ol.steps li { margin: .5rem 0; }
18
+ [hidden] { display: none !important; }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <h1>FediPod - join the Fediverse from a Solid pod.</h1>
23
+ <p class="lede">Welcome to <b>FediPod</b>! Here, you can get a free pod and Fediverse account or link to your existing pod/account.</p>
24
+
25
+ <p class="hint" id="current-version" hidden>Current FediPod: <strong id="current-version-num"></strong><br>
26
+ — install or update an existing install with <code id="current-version-cmd"></code></p>
27
+
28
+ <p>
29
+ FediPod is experimental and free to use. See the <a href="https://github.com/jeff-zucker/FediPod">FediPod repository</a> for details.
30
+ </p>
31
+
32
+ <script>
33
+ fetch('/api/handle?handle=__probe__').then(r => r.json()).then(d => {
34
+ if (d.version) {
35
+ document.getElementById('current-version-num').textContent = d.version;
36
+ document.getElementById('current-version-cmd').textContent = `curl -fsSL ${location.origin}/install | sh`;
37
+ document.getElementById('current-version').hidden = false;
38
+ }
39
+ }).catch(() => {});
40
+ </script>
41
+ <i style="text-align:right">(cc) 4.0 By, Jeff Zucker, 2026</i>
42
+ </body>
43
+ </html>
@@ -47,87 +47,7 @@ accounts that only use it as their gateway. Sign in as the server's admin to see
47
47
  <tbody id="roster-rows"></tbody>
48
48
  </table>
49
49
 
50
- <script type="module">
51
- const $ = (id) => document.getElementById(id);
52
- const note = (text) => { const n = $('roster-note'); n.hidden = !text; n.textContent = text || ''; };
53
-
54
- // The Solid-OIDC client for the sign-in round trip. Constructed once; the
55
- // redirect state lives in this tab's sessionStorage, so the same session
56
- // finishes the login on the way back.
57
- let session = null;
58
- try {
59
- const { SessionCore } = await import('/solid-oidc-client.js');
60
- session = new SessionCore({ redirect_uris: [location.origin + '/admin'], client_name: 'FediPod admin' });
61
- } catch { /* leave null — the "did not load" note fires */ }
62
-
63
- $('roster-issuer').addEventListener('input', () => {
64
- $('roster-signin').disabled = !/^https?:\/\/\S+/.test($('roster-issuer').value.trim());
65
- });
66
-
67
- $('roster-form').addEventListener('submit', (ev) => {
68
- ev.preventDefault();
69
- if (!session) { note('the sign-in library did not load — reload and try again'); return; }
70
- session.login($('roster-issuer').value.trim(), location.origin + '/admin')
71
- .catch((e) => note('sign-in failed to start: ' + e.message));
72
- });
73
-
74
- // Back from the identity provider: read the roster with the proven login.
75
- (async () => {
76
- if (!session) return;
77
- await session.handleRedirectFromLogin().catch(() => {});
78
- if (!session.isActive) return;
79
- const webId = session.webId;
80
-
81
- // Signed fetches build a DPoP proof from the URL, so it must be absolute.
82
- const load = async () => {
83
- let res;
84
- try { res = await session.authFetch(location.origin + '/api/roster'); }
85
- catch (e) { note('the roster request failed: ' + e.message); return; }
86
- const d = await res.json().catch(() => ({}));
87
- if (res.status === 403) { note('signed in as ' + webId + ', which is not this server’s admin'); return; }
88
- if (res.status === 501) { note('this server names no admin — set FEDIPOD_ADMIN_WEBID and redeploy'); return; }
89
- if (res.status !== 200) { note('roster unavailable: ' + (d.error || 'HTTP ' + res.status)); return; }
90
- const rows = $('roster-rows');
91
- rows.textContent = '';
92
- for (const a of d.accounts || []) {
93
- const tr = document.createElement('tr');
94
- const cell = (child) => { const td = document.createElement('td'); td.append(child); tr.append(td); };
95
- cell(a.address);
96
- cell(a.kind);
97
- cell(a.fronted ? 'lives here' : 'gateway only');
98
- const pod = document.createElement('a');
99
- pod.href = a.podHome; pod.textContent = new URL(a.podHome).host;
100
- cell(pod);
101
- const rm = document.createElement('button');
102
- rm.type = 'button'; rm.textContent = 'Remove';
103
- rm.onclick = () => revoke(a);
104
- cell(rm);
105
- rows.append(tr);
106
- }
107
- $('roster-table').hidden = false;
108
- note((d.accounts || []).length + ' account(s) on ' + d.host);
109
- };
110
-
111
- const revoke = async (a) => {
112
- if (!confirm('Remove ' + a.address + ' from this server? The name stops resolving here; nothing on their pod is touched.')) return;
113
- let res;
114
- try {
115
- res = await session.authFetch(location.origin + '/api/revoke', {
116
- method: 'POST', headers: { 'content-type': 'application/json' },
117
- body: JSON.stringify({ handle: a.handle }),
118
- });
119
- } catch (e) { note('the remove request failed: ' + e.message); return; }
120
- const d = await res.json().catch(() => ({}));
121
- if (res.status !== 200) { note('remove refused: ' + (d.error || 'HTTP ' + res.status)); return; }
122
- if (!d.removed) { note(a.address + ' stays: ' + d.reason); return; }
123
- await load();
124
- note('removed ' + a.address);
125
- };
126
-
127
- note('signed in as ' + webId + ' — reading the roster…');
128
- await load();
129
- })();
130
- </script>
50
+ <script type="module" src="/admin.js"></script>
131
51
  <i>(cc) 4.0 By, Jeff Zucker, 2026</i>
132
52
  </body>
133
53
  </html>
@@ -0,0 +1,85 @@
1
+ // admin.js — the roster page's script, in a file of its own.
2
+ //
3
+ // It lived inline in admin.html until 2026-09-09. Out here the page can be
4
+ // served under `script-src 'self'`, which is what stops a stray bit of markup
5
+ // from becoming running code; inline script cannot be told apart from injected
6
+ // script by any policy. Served by the front at /admin.js.
7
+ const $ = (id) => document.getElementById(id);
8
+ const note = (text) => { const n = $('roster-note'); n.hidden = !text; n.textContent = text || ''; };
9
+
10
+ // The Solid-OIDC client for the sign-in round trip. Constructed once; the
11
+ // redirect state lives in this tab's sessionStorage, so the same session
12
+ // finishes the login on the way back.
13
+ let session = null;
14
+ try {
15
+ const { SessionCore } = await import('/solid-oidc-client.js');
16
+ session = new SessionCore({ redirect_uris: [location.origin + '/roster'], client_name: 'FediPod admin' });
17
+ } catch { /* leave null — the "did not load" note fires */ }
18
+
19
+ $('roster-issuer').addEventListener('input', () => {
20
+ $('roster-signin').disabled = !/^https?:\/\/\S+/.test($('roster-issuer').value.trim());
21
+ });
22
+
23
+ $('roster-form').addEventListener('submit', (ev) => {
24
+ ev.preventDefault();
25
+ if (!session) { note('the sign-in library did not load — reload and try again'); return; }
26
+ session.login($('roster-issuer').value.trim(), location.origin + '/roster')
27
+ .catch((e) => note('sign-in failed to start: ' + e.message));
28
+ });
29
+
30
+ // Back from the identity provider: read the roster with the proven login.
31
+ (async () => {
32
+ if (!session) return;
33
+ await session.handleRedirectFromLogin().catch(() => {});
34
+ if (!session.isActive) return;
35
+ const webId = session.webId;
36
+
37
+ // Signed fetches build a DPoP proof from the URL, so it must be absolute.
38
+ const load = async () => {
39
+ let res;
40
+ try { res = await session.authFetch(location.origin + '/api/roster'); }
41
+ catch (e) { note('the roster request failed: ' + e.message); return; }
42
+ const d = await res.json().catch(() => ({}));
43
+ if (res.status === 403) { note('signed in as ' + webId + ', which is not this server’s admin'); return; }
44
+ if (res.status === 501) { note('this server names no admin — set FEDIPOD_ADMIN_WEBID and redeploy'); return; }
45
+ if (res.status !== 200) { note('roster unavailable: ' + (d.error || 'HTTP ' + res.status)); return; }
46
+ const rows = $('roster-rows');
47
+ rows.textContent = '';
48
+ for (const a of d.accounts || []) {
49
+ const tr = document.createElement('tr');
50
+ const cell = (child) => { const td = document.createElement('td'); td.append(child); tr.append(td); };
51
+ cell(a.address);
52
+ cell(a.kind);
53
+ cell(a.fronted ? 'lives here' : 'gateway only');
54
+ const pod = document.createElement('a');
55
+ pod.href = a.podHome; pod.textContent = new URL(a.podHome).host;
56
+ cell(pod);
57
+ const rm = document.createElement('button');
58
+ rm.type = 'button'; rm.textContent = 'Remove';
59
+ rm.onclick = () => revoke(a);
60
+ cell(rm);
61
+ rows.append(tr);
62
+ }
63
+ $('roster-table').hidden = false;
64
+ note((d.accounts || []).length + ' account(s) on ' + d.host);
65
+ };
66
+
67
+ const revoke = async (a) => {
68
+ if (!confirm('Remove ' + a.address + ' from this server? The name stops resolving here; nothing on their pod is touched.')) return;
69
+ let res;
70
+ try {
71
+ res = await session.authFetch(location.origin + '/api/revoke', {
72
+ method: 'POST', headers: { 'content-type': 'application/json' },
73
+ body: JSON.stringify({ handle: a.handle }),
74
+ });
75
+ } catch (e) { note('the remove request failed: ' + e.message); return; }
76
+ const d = await res.json().catch(() => ({}));
77
+ if (res.status !== 200) { note('remove refused: ' + (d.error || 'HTTP ' + res.status)); return; }
78
+ if (!d.removed) { note(a.address + ' stays: ' + d.reason); return; }
79
+ await load();
80
+ note('removed ' + a.address);
81
+ };
82
+
83
+ note('signed in as ' + webId + ' — reading the roster…');
84
+ await load();
85
+ })();
@@ -29,15 +29,7 @@
29
29
  FediPod is experimental and free to use. See the <a href="https://github.com/jeff-zucker/FediPod">FediPod repository</a> for details.
30
30
  </p>
31
31
 
32
- <script>
33
- fetch('/api/handle?handle=__probe__').then(r => r.json()).then(d => {
34
- if (d.version) {
35
- document.getElementById('current-version-num').textContent = d.version;
36
- document.getElementById('current-version-cmd').textContent = `curl -fsSL ${location.origin}/install | sh`;
37
- document.getElementById('current-version').hidden = false;
38
- }
39
- }).catch(() => {});
40
- </script>
32
+ <script src="/new-account.js"></script>
41
33
  <i style="text-align:right">(cc) 4.0 By, Jeff Zucker, 2026</i>
42
34
  </body>
43
35
  </html>
@@ -0,0 +1,13 @@
1
+ // new-account.js — the sign-up page's script, in a file of its own.
2
+ //
3
+ // It lived inline in new-account.html until 2026-09-09. Out here the page can
4
+ // be served under `script-src 'self'`, which is what stops a stray bit of
5
+ // markup from becoming running code; inline script cannot be told apart from
6
+ // injected script by any policy. Served by the front at /new-account.js.
7
+ fetch('/api/handle?handle=__probe__').then(r => r.json()).then(d => {
8
+ if (d.version) {
9
+ document.getElementById('current-version-num').textContent = d.version;
10
+ document.getElementById('current-version-cmd').textContent = `curl -fsSL ${location.origin}/install | sh`;
11
+ document.getElementById('current-version').hidden = false;
12
+ }
13
+ }).catch(() => {});
@@ -45,98 +45,7 @@ you to keep running. Sign in with your pod to prove it is yours.</p>
45
45
  <p class="hint" id="run-note" hidden></p>
46
46
  </form>
47
47
 
48
- <script type="module">
49
- const $ = (id) => document.getElementById(id);
50
-
51
- // The Solid-OIDC client for the sign-in round trip. Constructed once; the
52
- // redirect state (PKCE, csrf) lives in this tab's sessionStorage, so the same
53
- // session finishes the login on the way back.
54
- let session = null;
55
- try {
56
- const { SessionCore } = await import('/solid-oidc-client.js');
57
- session = new SessionCore({ redirect_uris: [location.origin + '/run'], client_name: 'FediPod gateway' });
58
- } catch { /* leave null — the "did not load" notes below fire */ }
59
-
60
- // Offered only where this server actually runs identities. An
61
- // unauthenticated probe tells the two apart: 501 = not offered here,
62
- // anything else = offered (a real opt-in still needs the signed-in proof).
63
- let offered = true;
64
- (async () => {
65
- const res = await fetch('/api/agent', { method: 'POST',
66
- headers: { 'content-type': 'application/json' }, body: '{}' }).catch(() => null);
67
- if (!res || res.status === 501) {
68
- offered = false;
69
- const n = $('run-note');
70
- n.hidden = false;
71
- n.textContent = 'This server does not run identities. Your agent stays where it is.';
72
- runFormCheck();
73
- }
74
- })();
75
-
76
- function runFormCheck() {
77
- let ok = false;
78
- try { ok = /^https?:$/.test(new URL($('run-pod-url').value.trim()).protocol); } catch { /* not yet */ }
79
- const issuerOk = /^https?:\/\/\S+/.test($('run-issuer').value.trim());
80
- $('run-continue').disabled = !(offered && ok && issuerOk);
81
- $('run-leave').disabled = !(offered && ok && issuerOk);
82
- }
83
- $('run-pod-url').addEventListener('input', () => {
84
- runFormCheck();
85
- // The identity provider is usually the server itself — still editable.
86
- try {
87
- const u = new URL($('run-pod-url').value.trim());
88
- const apex = u.hostname.split('.').slice(1).join('.');
89
- if (!$('run-issuer').value && apex) $('run-issuer').value = `${u.protocol}//${apex}${u.port ? ':' + u.port : ''}`;
90
- } catch { /* not a URL yet */ }
91
- runFormCheck();
92
- });
93
- $('run-issuer').addEventListener('input', runFormCheck);
94
-
95
- const runStart = (action) => {
96
- const n = $('run-note');
97
- if (!session) { n.hidden = false; n.textContent = 'the sign-in library did not load — reload and try again'; return; }
98
- const u = new URL($('run-pod-url').value.trim());
99
- if (u.pathname === '') u.pathname = '/';
100
- if (!u.pathname.endsWith('/')) u.pathname += '/';
101
- u.search = ''; u.hash = '';
102
- sessionStorage.setItem('fp-run', JSON.stringify({ podBase: u.href, action }));
103
- session.login($('run-issuer').value.trim(), location.origin + '/run')
104
- .catch((e) => { n.hidden = false; n.textContent = 'sign-in failed to start: ' + e.message; });
105
- };
106
- $('run-continue').onclick = () => runStart('opt-in');
107
- $('run-leave').onclick = () => runStart('opt-out');
108
-
109
- // Back from the identity provider: finish the opt-in with the proven login.
110
- (async () => {
111
- if (!session) return;
112
- await session.handleRedirectFromLogin().catch(() => {});
113
- const pending = sessionStorage.getItem('fp-run');
114
- if (!session.isActive || !pending) return;
115
- sessionStorage.removeItem('fp-run');
116
- const p = JSON.parse(pending);
117
- const n = $('run-note');
118
- n.hidden = false;
119
- n.textContent = 'signed in as ' + session.webId + ' — asking the server…';
120
- // The signed fetch builds a DPoP proof from the URL, so it must be absolute.
121
- const res = await session.authFetch(location.origin + '/api/agent', {
122
- method: 'POST', headers: { 'content-type': 'application/json' },
123
- body: JSON.stringify({ action: p.action, podBase: p.podBase }),
124
- }).catch(() => null);
125
- const d = res ? await res.json().catch(() => ({})) : {};
126
- if (res && res.status === 201 && d.doorSecret) {
127
- n.innerHTML = 'Your identity runs here now. This is your door secret — <b>save it now</b>, '
128
- + 'it is shown only this once. It opens your admin pages at <code>' + p.podBase.replace(/\/$/, '') + d.doorPath + '</code>:'
129
- + '<pre>' + d.doorSecret + '</pre>'
130
- + 'To check it works:<pre>' + d.command + '</pre>'
131
- + 'Lost it? Opt in again — that mints a fresh one and retires this one.';
132
- } else if (res && res.status === 200) {
133
- n.textContent = 'Done — this server no longer runs that identity. Your pod and its data are untouched.';
134
- } else {
135
- n.textContent = (p.action === 'opt-in' ? 'opt-in' : 'opt-out') + ' failed: '
136
- + (d.error || (res ? 'HTTP ' + res.status : 'no response'));
137
- }
138
- })();
139
- </script>
48
+ <script type="module" src="/run.js"></script>
140
49
  <i>(cc) 4.0 By, Jeff Zucker, 2026</i>
141
50
  </body>
142
51
  </html>