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,99 @@
1
+ // keystore.mjs — the signing keys, made in the browser and locked under the
2
+ // account password, so the copy that lives on the pod is ciphertext.
3
+ //
4
+ // The Node agent keeps keys.json (RSA + Ed25519 PEMs) on disk, 0600. A browser
5
+ // has no disk it can carry to the next machine, so the durable copy lives on
6
+ // the pod, wrapped here first: the pod's owner-only ACL keeps strangers out,
7
+ // and the password keeps the pod's HOST out — which the ACL cannot do, and
8
+ // which matters more for a key than for any other document, because whoever
9
+ // holds it is you to every server in the fediverse. The shape inside is exactly
10
+ // the agent's keys.json, so once unwrapped it is used unchanged.
11
+ //
12
+ // Each browser opens it once and keeps the opened copy in its own IndexedDB
13
+ // (see keys-browser.mjs). That is what lets the service worker boot itself
14
+ // after an idle kill with nobody present to type anything, and it is why a NEW
15
+ // browser asks for the password and a returning one does not.
16
+ //
17
+ // Until 2026-09-09 the wrap existed here but nothing called it: signup PUT the
18
+ // bare record and the loader read it plain, while this header, web/app/README.md
19
+ // and claude/plans/browser-agent.md all said otherwise. Now they agree.
20
+
21
+ const PEM = (der, label) => {
22
+ const b64 = btoa(String.fromCharCode(...new Uint8Array(der)));
23
+ const lines = b64.match(/.{1,64}/g).join('\n');
24
+ return `-----BEGIN ${label}-----\n${lines}\n-----END ${label}-----\n`;
25
+ };
26
+
27
+ /** RSA (what Mastodon verifies) and, where the browser supports it, Ed25519
28
+ * (FEP-8b32 proofs). A missing Ed25519 half is not fatal — the agent mints one
29
+ * the same way it does for a pre-proofs keys.json, since nothing has published it. */
30
+ export async function generateKeys() {
31
+ const rsa = await crypto.subtle.generateKey(
32
+ { name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' },
33
+ true, ['sign', 'verify']);
34
+ const rec = {
35
+ rsa: {
36
+ publicPem: PEM(await crypto.subtle.exportKey('spki', rsa.publicKey), 'PUBLIC KEY'),
37
+ privatePem: PEM(await crypto.subtle.exportKey('pkcs8', rsa.privateKey), 'PRIVATE KEY'),
38
+ },
39
+ };
40
+ try {
41
+ const ed = await crypto.subtle.generateKey('Ed25519', true, ['sign', 'verify']);
42
+ rec.ed25519 = {
43
+ publicPem: PEM(await crypto.subtle.exportKey('spki', ed.publicKey), 'PUBLIC KEY'),
44
+ privatePem: PEM(await crypto.subtle.exportKey('pkcs8', ed.privateKey), 'PRIVATE KEY'),
45
+ };
46
+ } catch { /* no Ed25519 in this browser — the agent mints it on first run */ }
47
+ return rec;
48
+ }
49
+
50
+ const PBKDF2_ITERATIONS = 310_000; // OWASP 2023 floor for PBKDF2-HMAC-SHA256
51
+
52
+ async function deriveAesKey(password, salt, iterations) {
53
+ const base = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveKey']);
54
+ return crypto.subtle.deriveKey(
55
+ { name: 'PBKDF2', salt, iterations, hash: 'SHA-256' },
56
+ base, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
57
+ }
58
+
59
+ const toB64 = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)));
60
+ const fromB64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
61
+
62
+ /** Whether a document read off the pod is a wrapped envelope rather than a bare
63
+ * keys record. Installs made before wrapping have the bare record there, and
64
+ * those still have to boot — the shape is what tells them apart. */
65
+ export function isKeyEnvelope(doc) {
66
+ return !!doc && doc.v === 1 && typeof doc.ct === 'string' && typeof doc.salt === 'string';
67
+ }
68
+
69
+ /** Thrown when the pod holds a wrapped key and nothing in this browser can open
70
+ * it. The page catches it by `code` and asks for the account password; the
71
+ * worker cannot ask anyone anything. */
72
+ export class KeyPasswordNeeded extends Error {
73
+ constructor() { super('this browser needs your account password to unlock the signing key'); }
74
+ code = 'key-password-needed';
75
+ }
76
+
77
+ /** Wrap the keys record under the password. The envelope names its own KDF and
78
+ * parameters, so unwrap needs only the password and this document. */
79
+ export async function wrapKeys(keysRecord, password) {
80
+ const salt = crypto.getRandomValues(new Uint8Array(16));
81
+ const iv = crypto.getRandomValues(new Uint8Array(12));
82
+ const aes = await deriveAesKey(password, salt, PBKDF2_ITERATIONS);
83
+ const plaintext = new TextEncoder().encode(JSON.stringify(keysRecord));
84
+ const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, aes, plaintext);
85
+ return { v: 1, kdf: 'PBKDF2-SHA256', iterations: PBKDF2_ITERATIONS,
86
+ salt: toB64(salt), iv: toB64(iv), ct: toB64(ct) };
87
+ }
88
+
89
+ /** Unwrap an envelope wrapKeys made. Throws on a wrong password (AES-GCM auth
90
+ * failure) — which is how a new browser tells a typo from a real key. */
91
+ export async function unwrapKeys(envelope, password) {
92
+ if (!envelope || envelope.v !== 1) throw new Error('not a key envelope');
93
+ const aes = await deriveAesKey(password, fromB64(envelope.salt), envelope.iterations);
94
+ let plaintext;
95
+ try {
96
+ plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: fromB64(envelope.iv) }, aes, fromB64(envelope.ct));
97
+ } catch { throw new Error('wrong password'); }
98
+ return JSON.parse(new TextDecoder().decode(plaintext));
99
+ }
@@ -0,0 +1,189 @@
1
+ // oidc-session.mjs — Solid-OIDC login for the browser agent, with a secure,
2
+ // durable, worker-visible session.
3
+ //
4
+ // The redirect handshake (authorization-code + PKCE) runs in the page. The
5
+ // session it yields — a DPoP keypair whose private half is NON-EXTRACTABLE, plus
6
+ // the refresh token, the token endpoint and the WebID — is kept in IndexedDB.
7
+ // IndexedDB survives a browser restart and is readable by the service worker
8
+ // where the agent runs, and a non-extractable key can sign but never be read
9
+ // out, even by injected script. That is the security this buys over uvdsl's
10
+ // sessionStorage (which must hold an extractable key as a string, per-tab).
11
+ //
12
+ // Page: const { authorizationUrl } = await beginLogin({ issuer, redirectUri });
13
+ // location.href = authorizationUrl; // ... redirect ...
14
+ // await completeLogin({ currentUrl: location.href }); // on return
15
+ // Anywhere (page or worker):
16
+ // const session = await getSession(); // { webId, fetch, signOut } | null
17
+
18
+ const DB = 'fedipod-oidc';
19
+ const STORE = 'session';
20
+ const b64u = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
21
+ const enc = (o) => b64u(new TextEncoder().encode(JSON.stringify(o)));
22
+ const sha256 = (s) => crypto.subtle.digest('SHA-256', new TextEncoder().encode(s));
23
+ const rand = (n = 32) => b64u(crypto.getRandomValues(new Uint8Array(n)));
24
+
25
+ function idb() {
26
+ return new Promise((res, rej) => {
27
+ const r = indexedDB.open(DB, 1);
28
+ r.onupgradeneeded = () => r.result.createObjectStore(STORE);
29
+ r.onsuccess = () => res(r.result);
30
+ r.onerror = () => rej(r.error);
31
+ });
32
+ }
33
+ async function idbGet(key) {
34
+ const db = await idb();
35
+ return new Promise((res, rej) => { const t = db.transaction(STORE).objectStore(STORE).get(key); t.onsuccess = () => res(t.result); t.onerror = () => rej(t.error); });
36
+ }
37
+ async function idbPut(key, val) {
38
+ const db = await idb();
39
+ return new Promise((res, rej) => { const t = db.transaction(STORE, 'readwrite').objectStore(STORE).put(val, key); t.onsuccess = () => res(); t.onerror = () => rej(t.error); });
40
+ }
41
+ async function idbDel(key) {
42
+ const db = await idb();
43
+ return new Promise((res, rej) => { const t = db.transaction(STORE, 'readwrite').objectStore(STORE).delete(key); t.onsuccess = () => res(); t.onerror = () => rej(t.error); });
44
+ }
45
+
46
+ async function dpopKey() { return crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']); }
47
+ async function publicJwk(pair) { const j = await crypto.subtle.exportKey('jwk', pair.publicKey); return { kty: j.kty, crv: j.crv, x: j.x, y: j.y }; }
48
+ async function dpopProof(pair, jwk, htm, htu, ath) {
49
+ const header = { typ: 'dpop+jwt', alg: 'ES256', jwk };
50
+ const payload = { htm, htu: htu.split('#')[0], jti: crypto.randomUUID(), iat: Math.floor(Date.now() / 1000), ...(ath ? { ath } : {}) };
51
+ const data = `${enc(header)}.${enc(payload)}`;
52
+ const sig = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, pair.privateKey, new TextEncoder().encode(data));
53
+ return `${data}.${b64u(sig)}`;
54
+ }
55
+ const jwtPayload = (jwt) => { try { return JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(jwt.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0)))); } catch { return {}; } };
56
+
57
+ async function discover(issuer) {
58
+ const res = await fetch(`${issuer.replace(/\/+$/, '')}/.well-known/openid-configuration`, { headers: { accept: 'application/json' } });
59
+ if (!res.ok) throw new Error(`OIDC discovery failed at ${issuer} (HTTP ${res.status})`);
60
+ return res.json();
61
+ }
62
+ async function registerClient(cfg, redirectUri, clientName) {
63
+ const res = await fetch(cfg.registration_endpoint, {
64
+ method: 'POST', headers: { 'content-type': 'application/json' },
65
+ body: JSON.stringify({ client_name: clientName, redirect_uris: [redirectUri], token_endpoint_auth_method: 'none',
66
+ grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], application_type: 'web', scope: 'openid webid offline_access' }),
67
+ });
68
+ if (!res.ok) throw new Error(`client registration failed (HTTP ${res.status})`);
69
+ return (await res.json()).client_id;
70
+ }
71
+
72
+ /** Begin login: prepare PKCE + a non-extractable DPoP key, stash the pending
73
+ * state, and return the authorization URL for the caller to navigate to. */
74
+ export async function beginLogin({ issuer, redirectUri, clientName = 'FediPod' }) {
75
+ const cfg = await discover(issuer);
76
+ const client_id = await registerClient(cfg, redirectUri, clientName);
77
+ const pair = await dpopKey();
78
+ const verifier = rand(48);
79
+ const challenge = b64u(await sha256(verifier));
80
+ const state = rand(16);
81
+ await idbPut('pending', { issuer, client_id, redirectUri, verifier, state, pair,
82
+ tokenEndpoint: cfg.token_endpoint, authorizationEndpoint: cfg.authorization_endpoint,
83
+ revocationEndpoint: cfg.revocation_endpoint || null });
84
+ const url = new URL(cfg.authorization_endpoint);
85
+ for (const [k, v] of Object.entries({ client_id, redirect_uri: redirectUri, response_type: 'code',
86
+ scope: 'openid webid offline_access', code_challenge: challenge, code_challenge_method: 'S256', state, prompt: 'consent' })) url.searchParams.set(k, v);
87
+ return { authorizationUrl: url.href };
88
+ }
89
+
90
+ /** Complete login from the redirect back. Exchanges the code (DPoP + PKCE),
91
+ * stores the durable session, and returns it. Returns null if there is no code. */
92
+ export async function completeLogin({ currentUrl }) {
93
+ const u = new URL(currentUrl);
94
+ const code = u.searchParams.get('code');
95
+ const state = u.searchParams.get('state');
96
+ if (!code) return null;
97
+ const p = await idbGet('pending');
98
+ if (!p || p.state !== state) throw new Error('login state mismatch');
99
+ const jwk = await publicJwk(p.pair);
100
+ const res = await fetch(p.tokenEndpoint, {
101
+ method: 'POST',
102
+ headers: { 'content-type': 'application/x-www-form-urlencoded', dpop: await dpopProof(p.pair, jwk, 'POST', p.tokenEndpoint) },
103
+ body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: p.redirectUri, client_id: p.client_id, code_verifier: p.verifier }),
104
+ });
105
+ const tok = await res.json().catch(() => ({}));
106
+ if (!res.ok || !tok.access_token) throw new Error(`token exchange failed (HTTP ${res.status}): ${tok.error || ''}`);
107
+ const webId = jwtPayload(tok.access_token).webid || jwtPayload(tok.id_token || '').webid || null;
108
+ const session = { issuer: p.issuer, client_id: p.client_id, tokenEndpoint: p.tokenEndpoint, pair: p.pair,
109
+ // Where to hand the refresh token back at sign-out. Kept on the session
110
+ // because discovery is a network round trip and sign-out should not need
111
+ // one — see signOut().
112
+ revocationEndpoint: p.revocationEndpoint || null,
113
+ refreshToken: tok.refresh_token || null, accessToken: tok.access_token,
114
+ expiresAt: Date.now() + Math.max(30, (tok.expires_in || 300)) * 1000, webId };
115
+ await idbPut('session', session);
116
+ await idbDel('pending');
117
+ return sessionHandle(session);
118
+ }
119
+
120
+ /** Restore the session from IndexedDB — works in the page and the worker. */
121
+ export async function getSession() {
122
+ const s = await idbGet('session');
123
+ return s ? sessionHandle(s) : null;
124
+ }
125
+
126
+ /**
127
+ * Sign out, and tell the issuer.
128
+ *
129
+ * Dropping the row locally left the REFRESH TOKEN alive at the pod for its full
130
+ * life — signing out of a shared or borrowed browser cleared the screen and
131
+ * nothing else, and anyone who had copied the token could go on minting access
132
+ * tokens with it. RFC 7009: hand it back.
133
+ *
134
+ * Best effort, and the local teardown happens either way: an issuer that is
135
+ * unreachable, or that publishes no revocation endpoint, must not be able to
136
+ * keep somebody signed in.
137
+ */
138
+ export async function signOut() {
139
+ const s = await idbGet('session').catch(() => null);
140
+ if (s?.revocationEndpoint && s.refreshToken) {
141
+ try {
142
+ await fetch(s.revocationEndpoint, {
143
+ method: 'POST',
144
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
145
+ body: new URLSearchParams({
146
+ token: s.refreshToken, token_type_hint: 'refresh_token', client_id: s.client_id,
147
+ }),
148
+ });
149
+ } catch { /* told if we could; leaving is not conditional on being heard */ }
150
+ }
151
+ await idbDel('session');
152
+ await idbDel('pending');
153
+ }
154
+
155
+ function sessionHandle(s) {
156
+ let { accessToken, expiresAt, refreshToken } = s;
157
+ const jwkP = publicJwk(s.pair);
158
+ const refresh = async () => {
159
+ if (!refreshToken) throw new Error('session expired and there is no refresh token — sign in again');
160
+ const jwk = await jwkP;
161
+ const res = await fetch(s.tokenEndpoint, {
162
+ method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', dpop: await dpopProof(s.pair, jwk, 'POST', s.tokenEndpoint) },
163
+ body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: s.client_id, scope: 'openid webid offline_access' }),
164
+ });
165
+ const tok = await res.json().catch(() => ({}));
166
+ if (!res.ok || !tok.access_token) { await idbDel('session'); throw new Error('refresh failed — sign in again'); }
167
+ accessToken = tok.access_token;
168
+ expiresAt = Date.now() + Math.max(30, (tok.expires_in || 300)) * 1000;
169
+ if (tok.refresh_token) refreshToken = tok.refresh_token;
170
+ // Re-read before writing back. There is ONE 'session' row, and this handle
171
+ // closed over the session it was made from — so a refresh that was already
172
+ // in flight when the user signed in as somebody else would put the OLD
173
+ // identity back, silently, over the new one. Only write if the row is still
174
+ // the session this handle belongs to.
175
+ const now = await idbGet('session');
176
+ if (now && now.webId !== s.webId) {
177
+ throw new Error('signed in as somebody else while this session was refreshing');
178
+ }
179
+ await idbPut('session', { ...s, accessToken, expiresAt, refreshToken });
180
+ };
181
+ const authFetch = async (url, init = {}) => {
182
+ if (Date.now() > expiresAt - 30_000) await refresh();
183
+ const jwk = await jwkP;
184
+ const ath = b64u(await sha256(accessToken));
185
+ const headers = { ...(init.headers || {}), authorization: `DPoP ${accessToken}`, dpop: await dpopProof(s.pair, jwk, (init.method || 'GET'), url, ath) };
186
+ return fetch(url, { ...init, headers });
187
+ };
188
+ return { webId: s.webId, fetch: authFetch, refresh, signOut };
189
+ }
@@ -0,0 +1,248 @@
1
+ // pod-auth.mjs — the pod side of sign-in, done in the browser with no server
2
+ // in between. It is the browser-native twin of lib/account.mjs and
3
+ // vendor/idp-grant.cjs: create a Community Solid Server account and pod, mint a
4
+ // client credential, and turn that credential into a DPoP-bound session whose
5
+ // fetch writes to the pod. Everything a Node agent did against a CSS account
6
+ // API, a page does here — proven cross-origin against solidcommunity.net on
7
+ // 2026-09-06 (claude/validation/scn-cross-origin-spike.html).
8
+ //
9
+ // No dependency on lib/: lib is Node (node:crypto, undici, @fedify). This uses
10
+ // only WebCrypto and fetch, so it runs in a tab and in a service worker.
11
+
12
+ const b64u = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)))
13
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
14
+ const enc = (o) => b64u(new TextEncoder().encode(JSON.stringify(o)));
15
+ const sha256 = async (str) => crypto.subtle.digest('SHA-256', new TextEncoder().encode(str));
16
+
17
+ // A CSS account-API call: JSON in, JSON out, the account token carried the way
18
+ // CSS wants it (a bearer with its own scheme, not a cookie, in v7).
19
+ async function accountFetch(url, { method = 'GET', body, token } = {}) {
20
+ const headers = { accept: 'application/json' };
21
+ if (body !== undefined) headers['content-type'] = 'application/json';
22
+ if (token) headers.authorization = `CSS-Account-Token ${token}`;
23
+ const res = await fetch(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
24
+ let json = null;
25
+ try { json = await res.json(); } catch { /* non-JSON error bodies exist */ }
26
+ return { status: res.status, json };
27
+ }
28
+
29
+ /** Is this origin a CSS v7 account API at all? Cheap pre-flight for the form. */
30
+ export async function accountApiControls(issuer) {
31
+ const origin = issuer.replace(/\/+$/, '');
32
+ const { status, json } = await accountFetch(`${origin}/.account/`);
33
+ if (status >= 400 || !json?.controls) return null;
34
+ return json.controls;
35
+ }
36
+
37
+ /**
38
+ * Create an account and a pod named `podName`. Idempotent on re-run: an
39
+ * existing account logs in, an existing pod of the same name is reused. Refuses
40
+ * to make a second pod on an account that already has one, which would give it
41
+ * a second WebID a single credential could not match. Mirrors
42
+ * lib/account.mjs's guards exactly, because the failure it prevents (a
43
+ * credential bound to the wrong WebID that 403s every write) is the same here.
44
+ */
45
+ export async function createAccountWithPod({ issuer, email, password, podName }) {
46
+ const origin = issuer.replace(/\/+$/, '');
47
+ const accountRoot = `${origin}/.account/`;
48
+
49
+ let token;
50
+ const login = await accountFetch(`${accountRoot}login/password/`, { method: 'POST', body: { email, password } });
51
+ if (login.status < 400 && login.json?.authorization) {
52
+ token = login.json.authorization;
53
+ } else {
54
+ const create = await accountFetch(`${accountRoot}account/`, { method: 'POST' });
55
+ if (!create.json?.authorization) {
56
+ throw new Error(`could not create an account at ${origin} (HTTP ${create.status})`
57
+ + ` — is sign-up open there? ${create.json?.message || ''}`);
58
+ }
59
+ token = create.json.authorization;
60
+ const pwCreate = (await accountFetch(accountRoot, { token })).json?.controls?.password?.create;
61
+ if (!pwCreate) throw new Error('this server is not a CSS v7 account API (no password control)');
62
+ const pw = await accountFetch(pwCreate, { method: 'POST', token, body: { email, password } });
63
+ if (pw.status >= 400) throw new Error(`could not set the password (HTTP ${pw.status}): ${pw.json?.message || ''}`);
64
+ }
65
+
66
+ const podCreate = (await accountFetch(accountRoot, { token })).json?.controls?.account?.pod;
67
+ if (!podCreate) throw new Error('this server does not offer pod creation through its account API');
68
+
69
+ const findOwn = async () => {
70
+ const pods = (await accountFetch(podCreate, { token })).json?.pods || {};
71
+ return Object.entries(pods).find(([url]) => {
72
+ try {
73
+ const u = new URL(url);
74
+ return u.hostname === podName || u.hostname.startsWith(`${podName}.`)
75
+ || u.pathname.split('/').filter(Boolean).includes(podName);
76
+ } catch { return false; }
77
+ }) || null;
78
+ };
79
+ const owned = Object.keys((await accountFetch(podCreate, { token })).json?.pods || {});
80
+ if (owned.length && !(await findOwn())) {
81
+ throw new Error(`${email} already has a pod on ${new URL(origin).host} — one account, one pod.`
82
+ + ' A second identity needs its own account. Nothing was created.');
83
+ }
84
+ const made = await accountFetch(podCreate, { method: 'POST', token, body: { name: podName } });
85
+ let pod = made.json?.pod || made.json?.podBaseUrl || null;
86
+ let webId = made.json?.webId || null;
87
+ if (made.status >= 400 || !pod) {
88
+ const own = await findOwn();
89
+ if (own) { pod = own[0]; webId = webId || own[1]?.webId || null; }
90
+ else if (made.status >= 400) throw new Error(`pod creation failed (HTTP ${made.status}): ${made.json?.message || ''}`);
91
+ }
92
+ if (!pod) throw new Error('the server did not report a pod URL');
93
+ return { pod: pod.endsWith('/') ? pod : pod + '/', webId, accountToken: token };
94
+ }
95
+
96
+ /**
97
+ * Mint a client credential against the account. `accountToken` reuses a login
98
+ * from createAccountWithPod; otherwise email+password logs in. Returns the
99
+ * credential record the agent expects, minus remotePod (the caller adds it).
100
+ */
101
+ export async function mintCredential({ issuer, email, password, webId, podUrl, accountToken, name = 'fedipod' }) {
102
+ const origin = issuer.replace(/\/+$/, '');
103
+ const accountRoot = `${origin}/.account/`;
104
+
105
+ let token = accountToken;
106
+ if (!token) {
107
+ // Sign in with the identifier the form gave (an email). If that account does
108
+ // not own the target pod — or the sign-in fails outright — fall back to the
109
+ // pod's subdomain as a username: older solidcommunity.net accounts sign in by
110
+ // username, not email. Done behind the scenes so the form only asks for an email.
111
+ const tryLogin = async (id) => {
112
+ const r = await accountFetch(`${accountRoot}login/password/`, { method: 'POST', body: { email: id, password } });
113
+ return (r.status < 400 && r.json?.authorization) ? r.json.authorization : null;
114
+ };
115
+ const ownsPod = async (tok) => {
116
+ if (!podUrl) return true;
117
+ try {
118
+ const ctl = (await accountFetch(accountRoot, { token: tok })).json?.controls?.account?.webId;
119
+ const links = ctl ? (await accountFetch(ctl, { token: tok })).json?.webIdLinks : null;
120
+ return Object.keys(links || {}).some((w) => new URL(w).origin === new URL(podUrl).origin);
121
+ } catch { return false; }
122
+ };
123
+ token = await tryLogin(email);
124
+ let sub = null; try { sub = podUrl ? new URL(podUrl).host.split('.')[0] : null; } catch { /* podUrl not a URL */ }
125
+ if (sub && sub !== email && (!token || !(await ownsPod(token)))) {
126
+ const alt = await tryLogin(sub);
127
+ if (alt) token = alt;
128
+ }
129
+ if (!token) throw new Error('account login failed — check the email or username and the password');
130
+ }
131
+
132
+ const controls = (await accountFetch(accountRoot, { token })).json?.controls;
133
+ const ccUrl = controls?.account?.clientCredentials;
134
+ if (!ccUrl) throw new Error('this server is not a CSS account API (no clientCredentials control)');
135
+
136
+ let wid = webId;
137
+ if (!wid) {
138
+ const linkCtl = controls?.account?.webId;
139
+ const links = linkCtl ? (await accountFetch(linkCtl, { token })).json?.webIdLinks : null;
140
+ const all = links ? Object.keys(links) : [];
141
+ if (!all.length) throw new Error('no WebID is linked to this account');
142
+ if (podUrl) {
143
+ // The credential MUST be for a WebID that owns the target pod, or every
144
+ // write to it is rejected. Never fall back to an unrelated WebID: a
145
+ // credential bound to the wrong identity 401s/403s on the pod, which is
146
+ // the opaque "could not store the key" people then see.
147
+ let origins = [];
148
+ try { origins = all.filter((w) => new URL(w).origin === new URL(podUrl).origin); } catch { /* podUrl not a URL */ }
149
+ if (!origins.length) {
150
+ throw new Error(`the account you signed in with does not own ${new URL(podUrl).origin}. `
151
+ + `It is linked to ${all.length} WebID${all.length > 1 ? 's' : ''} — on `
152
+ + `${all.map((w) => new URL(w).host).join(', ')} — none of them this pod. `
153
+ + 'Sign in with the account that owns this pod, or let FediPod make you a new one.');
154
+ }
155
+ wid = origins[0];
156
+ } else {
157
+ wid = all[0];
158
+ }
159
+ }
160
+
161
+ const made = await accountFetch(ccUrl, { method: 'POST', token, body: { name, webId: wid } });
162
+ if (made.status >= 400 || !made.json?.secret) throw new Error(`mint failed (HTTP ${made.status}): ${made.json?.message || ''}`);
163
+ const tokenEndpoint = await discoverTokenEndpoint(origin);
164
+ return { clientId: made.json.id, secret: made.json.secret, webId: wid, tokenEndpoint,
165
+ resource: made.json.resource, issuerOrigin: origin,
166
+ // Carried so sign-up can hand it straight back to revokeCredential. It is a
167
+ // session token for the ACCOUNT — never stored, never written to the pod,
168
+ // and gone with the tab.
169
+ accountToken: token };
170
+ }
171
+
172
+ /**
173
+ * Delete a client credential.
174
+ *
175
+ * The one sign-up mints is full, permanent access to the pod, and it is needed
176
+ * only while sign-up is running — the agent itself runs on the Solid-OIDC
177
+ * session. Leaving it behind meant every account carried an orphaned key that
178
+ * nothing held and nobody would think to revoke, while the page told the user
179
+ * it was "stored in this browser", which it never was.
180
+ *
181
+ * Best effort by design: a server that will not delete it leaves a credential
182
+ * the owner can still revoke from their pod's own account page, and saying so
183
+ * is better than failing a sign-up that otherwise worked.
184
+ */
185
+ export async function revokeCredential({ resource, accountToken }) {
186
+ if (!resource || !accountToken) return false;
187
+ try {
188
+ const r = await accountFetch(resource, { method: 'DELETE', token: accountToken });
189
+ return r.status < 400;
190
+ } catch { return false; }
191
+ }
192
+
193
+ /** The OIDC token endpoint, from the issuer's discovery document. */
194
+ export async function discoverTokenEndpoint(issuer) {
195
+ const origin = issuer.replace(/\/+$/, '');
196
+ try {
197
+ const res = await fetch(`${origin}/.well-known/openid-configuration`, { headers: { accept: 'application/json' } });
198
+ if (res.ok) {
199
+ const doc = await res.json();
200
+ if (doc.token_endpoint) return doc.token_endpoint;
201
+ }
202
+ } catch { /* fall through to the CSS default */ }
203
+ return `${origin}/.oidc/token`;
204
+ }
205
+
206
+ /**
207
+ * A DPoP-bound session: one ES256 proof key, a client-credentials access token
208
+ * bound to it, and a `fetch` that signs every request. The same key must sign
209
+ * the token request and every resource request, or the token's binding fails,
210
+ * so the key is made once and kept.
211
+ */
212
+ export async function makeDpopSession({ clientId, secret, tokenEndpoint }) {
213
+ const proofKey = await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']);
214
+ const jwk = await crypto.subtle.exportKey('jwk', proofKey.publicKey);
215
+ const publicJwk = { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y };
216
+
217
+ const proof = async (htm, htu, ath) => {
218
+ const header = { typ: 'dpop+jwt', alg: 'ES256', jwk: publicJwk };
219
+ const payload = { htm, htu: htu.split('#')[0], jti: crypto.randomUUID(), iat: Math.floor(Date.now() / 1000), ...(ath ? { ath } : {}) };
220
+ const data = `${enc(header)}.${enc(payload)}`;
221
+ const sig = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, proofKey.privateKey, new TextEncoder().encode(data));
222
+ return `${data}.${b64u(sig)}`;
223
+ };
224
+
225
+ let token = null; let expiresAt = 0;
226
+ const refresh = async () => {
227
+ const basic = btoa(`${encodeURIComponent(clientId)}:${encodeURIComponent(secret)}`);
228
+ const res = await fetch(tokenEndpoint, {
229
+ method: 'POST',
230
+ headers: { authorization: `Basic ${basic}`, 'content-type': 'application/x-www-form-urlencoded', dpop: await proof('POST', tokenEndpoint) },
231
+ body: 'grant_type=client_credentials&scope=webid',
232
+ });
233
+ const tok = await res.json().catch(() => ({}));
234
+ if (!tok.access_token) throw new Error(`token request failed (HTTP ${res.status}): ${tok.error || ''}`);
235
+ token = tok.access_token;
236
+ expiresAt = Date.now() + Math.max(30, (tok.expires_in || 300) - 30) * 1000;
237
+ return token;
238
+ };
239
+
240
+ const authFetch = async (url, init = {}) => {
241
+ if (!token || Date.now() > expiresAt) await refresh();
242
+ const ath = b64u(await sha256(token));
243
+ const headers = { ...(init.headers || {}), authorization: `DPoP ${token}`, dpop: await proof((init.method || 'GET'), url, ath) };
244
+ return fetch(url, { ...init, headers });
245
+ };
246
+
247
+ return { fetch: authFetch, refresh };
248
+ }
@@ -0,0 +1,84 @@
1
+ // pod-remote.mjs — the browser's pod transport, over a DPoP/OIDC session.
2
+ //
3
+ // The conversation with the pod — verbs, ACL documents, the deletion deny-list,
4
+ // the Retry-After cooldown, container listings — is lib/pod/transport.mjs, the
5
+ // same code the Node agent runs. This used to be a hand-kept fork of that file,
6
+ // and the fork had drifted: no cooldown at all for a 429 without a Retry-After
7
+ // header, no byte cap on a listing from a public-Append inbox, a swallowed
8
+ // parse failure, and probes that were neither counted nor held back. Those are
9
+ // gone with the fork.
10
+ //
11
+ // What genuinely differs, and all that is left here: this side retries a
12
+ // dropped connection, and waits a throttle out instead of failing fast.
13
+
14
+ import { PodTransport } from '../../lib/pod/transport.mjs';
15
+
16
+ export { protectedFromDeletion } from '../../lib/pod/transport.mjs';
17
+
18
+ // Retry transient edge throttling (dropped connections, 429/503) with backoff.
19
+ const RETRY_MAX = 5;
20
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
21
+ const backoff = (n) => Math.min(5000, 400 * 2 ** n) + Math.floor(Math.random() * 250);
22
+
23
+ // A tighter ceiling than the Node side's thirty minutes: a tab is a person
24
+ // waiting, and a hostile or confused header must not park them for half an hour.
25
+ const COOLDOWN_MAX_MS = 5 * 60_000;
26
+
27
+ export class BrowserRemotePod extends PodTransport {
28
+ constructor(session, { webId, log = () => {}, role = 'agent' } = {}) {
29
+ super(session, {
30
+ webId,
31
+ log,
32
+ role,
33
+ runtime: 'browser',
34
+ // Wait the throttle out rather than refusing: there is a person here, and
35
+ // an operation that resumes in four seconds beats one that fails and asks
36
+ // them to try again.
37
+ cooldownMode: 'wait',
38
+ maxCooldownMs: COOLDOWN_MAX_MS,
39
+ });
40
+ }
41
+
42
+ async warmup() { /* the DPoP session refreshes lazily */ }
43
+
44
+ /**
45
+ * solidcommunity.net sits behind an edge (Cloudflare) that throttles a burst
46
+ * of requests: during first-boot provisioning the agent makes ~15 rapid
47
+ * calls, and one comes back as a dropped connection ("Failed to fetch") or a
48
+ * 429/503. A single dropped write used to abort the whole boot. Retry those
49
+ * with escalating backoff — the throttle window clears in under a few seconds.
50
+ * Safe to retry: a thrown request never reached the server, and a 429/503 was
51
+ * refused, not applied.
52
+ *
53
+ * This overrides `_send`, not `fetch`, so it cannot skip the url map, the
54
+ * cooldown accounting or the deletion deny-list — all of which sit in `fetch`
55
+ * above it. The pod-wide pause is armed there too, so a retry here waits for
56
+ * the whole pod rather than each call climbing its own ladder.
57
+ */
58
+ async _send(url, init) {
59
+ let attempt = 0;
60
+ for (;;) {
61
+ try {
62
+ const res = await this.session.fetch(url, init);
63
+ if ((res.status === 429 || res.status === 503) && attempt < RETRY_MAX) {
64
+ // Arm the pod-wide pause from THIS answer before retrying. The retry
65
+ // happens inside `fetch`, so waiting for the outer `_observe` would
66
+ // mean climbing the whole ladder while ignoring the Retry-After the
67
+ // server just gave us. `_cooldownGate` then sleeps whatever was
68
+ // armed; when the server named no delay, nothing is armed and the
69
+ // backoff below paces it instead.
70
+ this._observe(res);
71
+ if (this.pausedUntil > Date.now()) await this._cooldownGate();
72
+ else await sleep(backoff(attempt));
73
+ attempt++;
74
+ continue;
75
+ }
76
+ return res;
77
+ } catch (e) {
78
+ if (attempt >= RETRY_MAX) throw e;
79
+ this.log(`[${this.label}] ${init?.method || 'GET'} ${url} failed (${e.message}); retry ${attempt + 1}/${RETRY_MAX}`);
80
+ await sleep(backoff(attempt++));
81
+ }
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,2 @@
1
+ export const URL = globalThis.URL;
2
+ export const URLSearchParams = globalThis.URLSearchParams;