fedipod-server 0.21.0 → 0.28.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 (332) hide show
  1. package/README.md +5 -4
  2. package/dist/claims.d.ts +3 -1
  3. package/dist/claims.js +20 -7
  4. package/dist/components/context.jsonld +6 -0
  5. package/dist/handler.d.ts +29 -1
  6. package/dist/handler.js +89 -4
  7. package/dist/handler.jsonld +36 -1
  8. package/lib/client/c2s.mjs +100 -3
  9. package/lib/client/masto/accounts.mjs +44 -4
  10. package/lib/client/masto/body.mjs +90 -3
  11. package/lib/client/masto/index.mjs +5 -1
  12. package/lib/client/masto/instance.mjs +11 -3
  13. package/lib/client/masto/media.mjs +3 -57
  14. package/lib/client/masto/oauth.mjs +66 -9
  15. package/lib/client/masto/render.mjs +62 -5
  16. package/lib/client/masto/statuses.mjs +25 -1
  17. package/lib/client/masto/timelines.mjs +128 -6
  18. package/lib/connections/tagfeed.mjs +42 -5
  19. package/lib/core/as2.mjs +36 -5
  20. package/lib/core/contexts/index.mjs +14 -0
  21. package/lib/core/contexts/litepub-0.1.json +60 -0
  22. package/lib/core/contexts/map.json +3 -1
  23. package/lib/core/contexts/quotes.json +18 -0
  24. package/lib/core/graphview.mjs +41 -3
  25. package/lib/core/intake/activities.mjs +136 -2
  26. package/lib/core/intake/activity.mjs +79 -1
  27. package/lib/core/intake/group.mjs +37 -3
  28. package/lib/core/intake/index.mjs +32 -2
  29. package/lib/core/intake/notes.mjs +60 -3
  30. package/lib/core/lease.mjs +12 -2
  31. package/lib/core/profile-page.mjs +104 -0
  32. package/lib/core/publisher/collections.mjs +6 -1
  33. package/lib/core/publisher/index.mjs +35 -17
  34. package/lib/core/publisher/notes.mjs +104 -3
  35. package/lib/core/shapes/activitystreams.ttl +28 -0
  36. package/lib/core/shapes/index.mjs +26 -1
  37. package/lib/core/wire-quotes.mjs +71 -0
  38. package/lib/core/wire.mjs +59 -103
  39. package/lib/device/admin/routes/lifecycle.mjs +9 -0
  40. package/lib/device/admin/static.mjs +4 -1
  41. package/lib/device/admin/surface.mjs +26 -2
  42. package/lib/device/certs.mjs +18 -2
  43. package/lib/device/cli/commands/setup.mjs +6 -2
  44. package/lib/device/cli/commands/state.mjs +3 -1
  45. package/lib/device/gateway-move.mjs +58 -0
  46. package/lib/device/migrate.mjs +3 -1
  47. package/lib/device/setup.mjs +67 -7
  48. package/lib/gateway/front-core.mjs +307 -36
  49. package/lib/gateway/gateway-core.mjs +15 -6
  50. package/lib/gateway/notices.mjs +84 -0
  51. package/lib/gateway/quiet.mjs +184 -0
  52. package/lib/pod/root.mjs +6 -3
  53. package/lib/pod/state.mjs +8 -7
  54. package/lib/pod/transport.mjs +5 -1
  55. package/lib/session/README.md +123 -0
  56. package/lib/session/demo.html +94 -0
  57. package/lib/session/fedi-account.mjs +346 -0
  58. package/lib/session/fedi-login.mjs +123 -0
  59. package/lib/session/oidc-session.mjs +233 -0
  60. package/lib/session/package.json +41 -0
  61. package/package.json +1 -1
  62. package/run-agent.mjs +28 -0
  63. package/vendor/gate.cjs +6 -3
  64. package/web/admin/bar.css +66 -0
  65. package/web/admin/bar.js +38 -0
  66. package/web/admin/client/index.html +31 -3
  67. package/web/admin/client-pick.js +79 -0
  68. package/web/admin/gateway.js +22 -0
  69. package/web/admin/index.html +54 -13
  70. package/web/admin/notices-bar.js +90 -0
  71. package/web/admin/oauth-signin.mjs +37 -0
  72. package/web/admin/record.js +13 -1
  73. package/web/admin/setup/index.html +31 -3
  74. package/web/admin/tokens.css +15 -3
  75. package/web/admin/upkeep.js +4 -5
  76. package/web/app/README.md +14 -10
  77. package/web/app/admin-facade.mjs +51 -12
  78. package/web/app/agent.mjs +157 -19
  79. package/web/app/authorize_interaction.html +56 -0
  80. package/web/app/authorize_interaction.js +69 -0
  81. package/web/app/boot.mjs +233 -95
  82. package/web/app/dist/boot.js +477 -569
  83. package/web/app/dist/boot.js.map +4 -4
  84. package/web/app/dist/sw.js +1861 -532
  85. package/web/app/dist/sw.js.map +4 -4
  86. package/web/app/gateway-move.mjs +60 -0
  87. package/web/app/index.html +40 -49
  88. package/web/app/keys-browser.mjs +12 -15
  89. package/web/app/keystore.mjs +19 -36
  90. package/web/app/oidc-session.mjs +5 -188
  91. package/web/app/signup.mjs +172 -132
  92. package/web/app/sw-src.mjs +1 -1
  93. package/web/app/update.js +66 -0
  94. package/web/front/admin.html +5 -3
  95. package/web/front/new-account.html +2 -2
  96. package/web/front/notices.html +69 -0
  97. package/web/front/notices.js +107 -0
  98. package/web/front/run.html +15 -8
  99. package/web/front/run.js +124 -36
  100. package/web/icons/forum.svg +11 -0
  101. package/web/icons/gateway.svg +14 -0
  102. package/web/icons/local.svg +11 -0
  103. package/web/app/index.html~ +0 -152
  104. package/web/app/site/_headers +0 -9
  105. package/web/app/site/_redirects +0 -22
  106. package/web/app/site/admin/actors.js +0 -145
  107. package/web/app/site/admin/bar.css +0 -60
  108. package/web/app/site/admin/bar.js +0 -61
  109. package/web/app/site/admin/client/client.js +0 -66
  110. package/web/app/site/admin/client/index.html +0 -43
  111. package/web/app/site/admin/common.js +0 -23
  112. package/web/app/site/admin/connections.js +0 -112
  113. package/web/app/site/admin/gateway.js +0 -111
  114. package/web/app/site/admin/group.js +0 -258
  115. package/web/app/site/admin/index.html +0 -564
  116. package/web/app/site/admin/record.js +0 -378
  117. package/web/app/site/admin/setup/index.html +0 -215
  118. package/web/app/site/admin/setup/setup.js +0 -326
  119. package/web/app/site/admin/tokens.css +0 -56
  120. package/web/app/site/admin/upkeep.js +0 -178
  121. package/web/app/site/admin/window.css +0 -55
  122. package/web/app/site/admin/window.js +0 -136
  123. package/web/app/site/app/404.html +0 -40
  124. package/web/app/site/app/_headers +0 -2
  125. package/web/app/site/app/apple-touch-icon.png +0 -0
  126. package/web/app/site/app/assets/ICONS-DIPJeU0C.js +0 -2
  127. package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +0 -1
  128. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +0 -183
  129. package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
  130. package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
  131. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +0 -2
  132. package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +0 -1
  133. package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
  134. package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
  135. package/web/app/site/app/assets/chunk-aKtaBQYM.js +0 -1
  136. package/web/app/site/app/assets/compose-CFEx7wb4.js +0 -2
  137. package/web/app/site/app/assets/compose-CFEx7wb4.js.map +0 -1
  138. package/web/app/site/app/assets/compose-ZJN5xSb4.js +0 -35
  139. package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +0 -1
  140. package/web/app/site/app/assets/debug.module-YFQgwnIr.js +0 -52
  141. package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +0 -1
  142. package/web/app/site/app/assets/dom-p1YcJ5Rw.js +0 -2
  143. package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +0 -1
  144. package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
  145. package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
  146. package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
  147. package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
  148. package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +0 -1
  149. package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +0 -1
  150. package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
  151. package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +0 -1
  152. package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
  153. package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
  154. package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
  155. package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
  156. package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
  157. package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
  158. package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
  159. package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
  160. package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +0 -1
  161. package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +0 -1
  162. package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
  163. package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +0 -1
  164. package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
  165. package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +0 -1
  166. package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
  167. package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
  168. package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +0 -1
  169. package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
  170. package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +0 -1
  171. package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
  172. package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
  173. package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
  174. package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
  175. package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
  176. package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +0 -1
  177. package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +0 -1
  178. package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +0 -1
  179. package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +0 -1
  180. package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +0 -1
  181. package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
  182. package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +0 -1
  183. package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
  184. package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +0 -1
  185. package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +0 -1
  186. package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +0 -1
  187. package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
  188. package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
  189. package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +0 -1
  190. package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +0 -1
  191. package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
  192. package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
  193. package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +0 -1
  194. package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +0 -1
  195. package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +0 -1
  196. package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
  197. package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +0 -1
  198. package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +0 -1
  199. package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +0 -1
  200. package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
  201. package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
  202. package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
  203. package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +0 -1
  204. package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +0 -1
  205. package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +0 -1
  206. package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +0 -1
  207. package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +0 -1
  208. package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
  209. package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
  210. package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +0 -1
  211. package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +0 -1
  212. package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +0 -1
  213. package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +0 -1
  214. package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +0 -1
  215. package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +0 -1
  216. package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +0 -1
  217. package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +0 -1
  218. package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
  219. package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +0 -1
  220. package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +0 -1
  221. package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +0 -1
  222. package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
  223. package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +0 -1
  224. package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +0 -1
  225. package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
  226. package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
  227. package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
  228. package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +0 -1
  229. package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +0 -1
  230. package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
  231. package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +0 -1
  232. package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +0 -1
  233. package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +0 -1
  234. package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +0 -1
  235. package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +0 -1
  236. package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +0 -1
  237. package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +0 -1
  238. package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +0 -1
  239. package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +0 -1
  240. package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
  241. package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +0 -1
  242. package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
  243. package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
  244. package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
  245. package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +0 -1
  246. package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
  247. package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +0 -1
  248. package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +0 -1
  249. package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
  250. package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +0 -1
  251. package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
  252. package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +0 -1
  253. package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
  254. package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +0 -1
  255. package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +0 -1
  256. package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
  257. package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +0 -1
  258. package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +0 -1
  259. package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
  260. package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +0 -1
  261. package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +0 -1
  262. package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +0 -1
  263. package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
  264. package/web/app/site/app/assets/instances-BNPptnn-.json +0 -333
  265. package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +0 -1
  266. package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +0 -1
  267. package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
  268. package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +0 -1
  269. package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +0 -1
  270. package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +0 -1
  271. package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +0 -1
  272. package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +0 -1
  273. package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +0 -1
  274. package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +0 -1
  275. package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +0 -1
  276. package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +0 -1
  277. package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +0 -1
  278. package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +0 -1
  279. package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +0 -1
  280. package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +0 -1
  281. package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +0 -1
  282. package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
  283. package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +0 -1
  284. package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
  285. package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +0 -1
  286. package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +0 -1
  287. package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
  288. package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +0 -1
  289. package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +0 -1
  290. package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
  291. package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +0 -1
  292. package/web/app/site/app/assets/locales/tok-CfheRbCu.js +0 -1
  293. package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +0 -1
  294. package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +0 -1
  295. package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +0 -1
  296. package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +0 -1
  297. package/web/app/site/app/assets/main-BdqNbG-a.js +0 -26
  298. package/web/app/site/app/assets/main-BdqNbG-a.js.map +0 -1
  299. package/web/app/site/app/assets/mock-home-DGhR__He.js +0 -2
  300. package/web/app/site/app/assets/mock-home-DGhR__He.js.map +0 -1
  301. package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
  302. package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
  303. package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
  304. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +0 -2
  305. package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +0 -1
  306. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +0 -20
  307. package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +0 -1
  308. package/web/app/site/app/assets/style-B1iLPTPR.css +0 -2
  309. package/web/app/site/app/assets/temml-DuyIQMei.js +0 -97
  310. package/web/app/site/app/assets/temml-DuyIQMei.js.map +0 -1
  311. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +0 -2
  312. package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
  313. package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
  314. package/web/app/site/app/assets/year-in-posts-DJv_star.js +0 -3
  315. package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +0 -1
  316. package/web/app/site/app/compose/index.html +0 -20
  317. package/web/app/site/app/favicon.ico +0 -0
  318. package/web/app/site/app/index.html +0 -73
  319. package/web/app/site/app/logo-192.png +0 -0
  320. package/web/app/site/app/logo-512.png +0 -0
  321. package/web/app/site/app/logo-badge-72.png +0 -0
  322. package/web/app/site/app/logo-maskable-512.png +0 -0
  323. package/web/app/site/app/logo-monochrome-512.png +0 -0
  324. package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
  325. package/web/app/site/app/manifest.webmanifest +0 -1
  326. package/web/app/site/app/og-image-2.jpg +0 -0
  327. package/web/app/site/app/og-image.png +0 -0
  328. package/web/app/site/app/robots.txt +0 -2
  329. package/web/app/site/app/version.json +0 -1
  330. package/web/app/site/boot.js +0 -34277
  331. package/web/app/site/index.html +0 -251
  332. package/web/app/site/sw.js +0 -71331
@@ -0,0 +1,60 @@
1
+ // gateway-move.mjs — the second half of moving an address from one gateway
2
+ // to another (signup.mjs moveIn is the first). It runs on the agent's first
3
+ // active boot under the new ids, once the new actor is published with the
4
+ // old one as an alias, and it is safe to run again: nothing here is done
5
+ // twice with the same result changed.
6
+ //
7
+ // 1. The old gateway is told the address moved (POST /api/move, proved
8
+ // with the pod session as attach was). It serves the old actor as a
9
+ // moved stub from then on — same key, old id, `movedTo` the new one.
10
+ // 2. A Move goes to every follower, FROM the old actor, signed under the
11
+ // old key id, through the old gateway's relay: that is the only
12
+ // signature a follower's server can verify against the old actor.
13
+ //
14
+ // The followers' servers fetch the old actor, find `movedTo`, fetch the new
15
+ // one, find the old among its `alsoKnownAs`, and move the follow.
16
+ import { RelayDeliverer } from './deliver-relay.mjs';
17
+ import { moveActivity } from '../../lib/core/wire.mjs';
18
+
19
+ export async function completeGatewayMove(agent) {
20
+ const cfg = agent.store.getConfig();
21
+ const mv = cfg?.movedFrom;
22
+ if (!mv || mv.completedAt) return null;
23
+ const log = agent.log;
24
+ const newActor = agent.urls.actor;
25
+
26
+ // 1. the old gateway
27
+ let told;
28
+ try {
29
+ told = await agent.sessionFetch(`${mv.gateway}/api/move`, {
30
+ method: 'POST', headers: { 'content-type': 'application/json' },
31
+ body: JSON.stringify({ handle: mv.handle, movedTo: newActor }),
32
+ });
33
+ } catch (e) { told = { status: 0, statusText: e.message }; }
34
+ if (told.status !== 200) {
35
+ // Left pending: the next active boot tries again. Followers are not told
36
+ // until the old actor can vouch for the Move.
37
+ log(`gateway move: ${mv.gateway} did not mark @${mv.handle} as moved (${told.status || told.statusText}) — will retry`);
38
+ return { told: false };
39
+ }
40
+
41
+ // 2. the Move, from the old address
42
+ const contacts = agent.store.getContacts();
43
+ const inboxes = [...new Set(contacts.followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean))];
44
+ const activity = moveActivity({ actor: mv.actor }, newActor, Date.now());
45
+ const sender = new RelayDeliverer({
46
+ passive: true, store: agent.store, rsaPrivate: agent.deliverer.rsaPrivate,
47
+ keyId: `${mv.actor}#main-key`, actorId: mv.actor, log,
48
+ relayUrl: `${mv.gateway}/api/relay`, handle: mv.handle, sessionFetch: agent.sessionFetch,
49
+ });
50
+ let sent = 0; let failed = 0;
51
+ for (const inbox of inboxes) {
52
+ try { await sender.deliverNow(inbox, activity); sent++; }
53
+ catch (e) { failed++; log(`gateway move: Move to ${inbox} failed: ${e.message}`); }
54
+ }
55
+ const done = { ...mv, completedAt: new Date().toISOString(), moveSent: sent, moveFailed: failed };
56
+ agent.store.setConfig({ ...agent.store.getConfig(), movedFrom: done });
57
+ await agent.store.flush?.();
58
+ log(`moved from ${mv.actor} to ${newActor}: Move sent to ${sent} inbox(es)${failed ? `, ${failed} failed` : ''}`);
59
+ return done;
60
+ }
@@ -5,12 +5,13 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https:; form-action 'self'; base-uri 'none'">
7
7
  <title>FediPod</title>
8
+ <link rel="icon" href="/icons/local.svg" type="image/svg+xml">
8
9
  <style>
9
10
  :root {
10
11
  color-scheme: light dark;
11
12
  font-size: 125%; /* 20px default (125% of 16) */
12
13
  --bg: #ffffff; --fg: #1a1a1a;
13
- --heading: #123f74; --sub: #565656; --line: #b0b0b0; --accent: #12467e;
14
+ --heading: #2c4a35; --sub: #565656; --line: #b0b0b0; --accent: #12467e;
14
15
  --btn: #3a5f43; --btn-hover: #30503a; --btn-text: #ffffff;
15
16
  --field-bg: #ececec; --field-text: #141414; --field-border: #6f6f6f;
16
17
  --select-bg: #dce7f3; --select-border: #12467e;
@@ -19,14 +20,14 @@
19
20
  @media (prefers-color-scheme: dark) {
20
21
  :root {
21
22
  --bg: #191919; --fg: #f3f3f3;
22
- --heading: #8fc2f2; --sub: #d2d2d2; --line: #6d6d6d; --accent: #8fc2f2;
23
+ --heading: #a7d4b5; --sub: #d2d2d2; --line: #6d6d6d; --accent: #8fc2f2;
23
24
  --field-bg: #474747; --field-text: #f5f5f5; --field-border: #9aa0a6;
24
25
  --select-bg: #2f4a66; --select-border: #8fc2f2;
25
26
  --btn: #4a7a54; --btn-hover: #3f6a49;
26
27
  --err: #ff9b9b; --warn: #ffb366; --focus: #8fc2f2;
27
28
  }
28
29
  }
29
- body { font: 1rem/1.55 system-ui, sans-serif; margin: 0; background: var(--bg); color: var(--fg); }
30
+ body { font: 1rem/1.55 Arial, Helvetica, sans-serif; margin: 0; background: var(--bg); color: var(--fg); }
30
31
  #page { max-width: 42rem; margin: 0 auto; padding: 1.75rem 1rem 4rem; box-sizing: border-box; }
31
32
  h1, h2, h3 { color: var(--heading); }
32
33
  h1 { font-size: 1.6rem; margin: 0 0 .5rem; }
@@ -103,36 +104,35 @@ a { color: var(--accent); }
103
104
  <p><button type="button" class="primary" id="create">create an account</button></p>
104
105
  <p>Fedipod.net is a FediPod Gateway operated by Jeff Zucker. You can detach from the gateway at any
105
106
  time and transfer your account either to your own FediPod Gateway (easy to set up on a free tier Netlify)
106
- or to any Fediverse server. All of your data lives on your pod and your Fediverse identity is yours
107
- forever independent of which gateway you attach to.</p>
107
+ or to any Fediverse server. All of your data lives on your pod and is yours regardless of which
108
+ gateway you are attached to.</p>
108
109
  <p>For more information, see
109
110
  <a href="https://github.com/jeff-zucker/FediPod">github.com/jeff-zucker/FediPod</a></p>
110
111
  </section>
111
112
 
112
- <!-- Shown only on a browser that has signed in but has never opened the signing
113
- key. The key is kept on the pod encrypted under the sign-up password, so
114
- the pod's host cannot sign as you; a browser opens it once and remembers.
115
- The second block is for someone who no longer has that password: a new
116
- key, locked under the password they use now. -->
113
+ <!-- Shown only for an account made before 2026-09-22, whose signing key on
114
+ the pod is still under the password given at sign-up. It is opened once
115
+ and stored as it is from then on. The second block is for someone who no
116
+ longer has that password: a new key. -->
117
117
  <section id="unlock" hidden>
118
- <h2>Unlock your signing key</h2>
119
- <p>Your signing key is stored on your pod, locked under the password you gave
120
- when you signed up, so nobody who can read your pod — your pod's host included
121
- — can post as you. This browser has not opened it yet.</p>
118
+ <h2>Open your signing key</h2>
119
+ <p>Your account was made when this site kept your signing key under the
120
+ password you gave at sign-up. Enter that password once. Your key is then
121
+ stored on your pod as it is, behind your pod's own login, and no browser
122
+ asks again.</p>
122
123
  <p class="row">
123
124
  <input type="password" id="unlock-password" autocomplete="current-password"
124
- placeholder="Your password" aria-label="Your password">
125
- <button type="button" class="primary" id="unlock-go">unlock</button>
125
+ placeholder="The password you gave at sign-up" aria-label="The password you gave at sign-up">
126
+ <button type="button" class="primary" id="unlock-go">open</button>
126
127
  </p>
127
128
  <p class="err" id="unlock-error" role="alert"></p>
128
129
  <p class="hint">This browser only — it is not sent anywhere.</p>
129
- <h2>Changed your pod password, or lost the old one?</h2>
130
- <p>Make a new signing key, locked under the password you use now. Your account,
131
- address, posts and followers stay as they are; other servers learn the new key
132
- the next time they hear from you.</p>
130
+ <h2>Lost that password?</h2>
131
+ <p>Make a new signing key instead. Your account, address, posts and followers
132
+ stay as they are; other servers learn the new key the next time they hear
133
+ from you.</p>
133
134
  <p><button type="button" id="unlock-newkey">make a new signing key</button></p>
134
- <p id="unlock-newkey-confirm" hidden>This replaces the key on your pod. Type the
135
- password you use now in the field above, then
135
+ <p id="unlock-newkey-confirm" hidden>This replaces the key on your pod.
136
136
  <button type="button" id="unlock-newkey-go">make the new key</button></p>
137
137
  </section>
138
138
 
@@ -153,7 +153,7 @@ a { color: var(--accent); }
153
153
 
154
154
  <label for="provider">Pod provider</label>
155
155
  <!-- The providers listed at https://solidproject.org/get_a_pod that run Community
156
- Solid Server, whose account API is what "A new pod" uses. Checked 2026-09-14. -->
156
+ Solid Server, whose sign-up page "Create your pod" links to. Checked 2026-09-14. -->
157
157
  <select id="provider" name="provider">
158
158
  <option value="https://solidcommunity.net">solidcommunity.net</option>
159
159
  <option value="https://privatedatapod.com">privatedatapod.com</option>
@@ -168,44 +168,35 @@ a { color: var(--accent); }
168
168
  <input type="url" id="providerOther" name="providerOther" autocomplete="url" placeholder="https://pods.example.org">
169
169
  </div>
170
170
 
171
- <div id="podname-field">
172
- <label for="podName">Pod username</label>
173
- <input type="text" id="podName" name="podName" autocomplete="off" aria-describedby="podname-hint">
174
- <p class="hint" id="podname-hint">Depending on your provider's setup, your pod will be at
175
- either https://USERNAME.podprovider or https://podprovider/USERNAME.</p>
171
+ <div id="newpod-field">
172
+ <p>Make your pod on your provider's own page, then come back here.</p>
173
+ <p><a id="register-link" href="#" target="_blank" rel="noopener">Create your pod at your provider</a></p>
176
174
  </div>
177
175
 
178
- <div id="pod-field" hidden>
179
- <label for="pod">Pod address</label>
180
- <input type="url" id="pod" name="pod" autocomplete="url" aria-describedby="pod-hint"
181
- placeholder="https://alice.solidcommunity.net/ or https://server.example/alice/">
182
- <p class="hint" id="pod-hint">The address of your pod: its own host, or a path on a
183
- suffix-based host.</p>
184
- </div>
185
-
186
- <label for="email">Email (used as pod id)</label>
187
- <input type="email" id="email" name="email" autocomplete="email" placeholder="you@example.org">
188
-
189
- <label for="password">Pod password</label>
190
- <input type="password" id="password" name="password" autocomplete="new-password"
191
- aria-describedby="password-hint">
192
- <p class="hint" id="password-hint">Your password is sent once, to make your account and to
193
- lock your signing key. It is never stored. The credential used to set things up is
194
- deleted when setup finishes — from then on this browser signs in through your pod,
195
- the way any other Solid app does.</p>
176
+ <p class="hint" id="signin-hint">Your password is typed at your pod only. This site
177
+ never sees it: you sign in at your pod, and it sends you back here.</p>
196
178
  </fieldset>
197
179
  <p class="err" id="err-1" role="alert"></p>
198
180
  <p>
199
181
  <button type="button" id="cancel">Cancel</button>
200
- <button type="button" class="primary" id="to-2">Continue</button>
182
+ <button type="button" class="primary" id="to-2">Sign in at your pod</button>
201
183
  </p>
202
184
  </div>
203
185
 
204
- <!-- Screen 2: Your Fediverse Identity -->
186
+ <!-- Screen 2: Your Fediverse Identity — shown on return from the pod's login -->
205
187
  <div class="step" id="step-2" hidden>
206
188
  <fieldset>
207
189
  <legend>Your Fediverse Identity</legend>
208
190
 
191
+ <span id="signed-pod-label" class="grouplabel">Your pod</span>
192
+ <p id="signed-pod" role="status" aria-labelledby="signed-pod-label"></p>
193
+
194
+ <!-- Shown when the pod's account lives at another gateway: this is a
195
+ move, not a new account. -->
196
+ <p id="movein-note" hidden>Moving <strong id="movein-from"></strong> to this site.
197
+ Your posts, followers and key come with you, and your followers are told the
198
+ new address. Keep the same handle, or choose a new one here.</p>
199
+
209
200
  <label for="handle">Fediverse handle</label>
210
201
  <input type="text" id="handle" name="handle" autocomplete="off"
211
202
  placeholder="the handle part of your address: @HANDLE@server" aria-describedby="handle-hint">
@@ -228,7 +219,7 @@ a { color: var(--accent); }
228
219
  <p class="err" id="form-error" role="alert"></p>
229
220
  <p>
230
221
  <button type="button" id="back-1">Back</button>
231
- <button type="submit" class="primary" id="submit">Create Pod and Fediverse accounts</button>
222
+ <button type="submit" class="primary" id="submit">Create your Fediverse account</button>
232
223
  </p>
233
224
  </div>
234
225
 
@@ -29,7 +29,7 @@ const isOpenedKey = (c) => c?.rsaPrivate?.type === 'private' && typeof c.rsaPubl
29
29
  /**
30
30
  * Keep this browser's opened copy: import the record and store the CryptoKey.
31
31
  * Best effort on the store — a browser that refuses IndexedDB (private mode)
32
- * asks for the password on the way back in. Returns the imported keys.
32
+ * reads the pod's copy again on the way back in. Returns the imported keys.
33
33
  */
34
34
  export async function cacheOpenedKeys(actorUrl, keysRecord) {
35
35
  const keys = await importSigningKey(keysRecord);
@@ -38,24 +38,22 @@ export async function cacheOpenedKeys(actorUrl, keysRecord) {
38
38
  return keys;
39
39
  }
40
40
 
41
- // Where this browser keeps its own opened copy of the signing key. The pod's
42
- // copy is wrapped under the account password; this one is not, because it never
43
- // leaves the origin — the same protection the OIDC session in the next IDB row
44
- // already relies on. It is what lets the worker boot itself after an idle kill
45
- // without a soul around to type anything.
41
+ // Where this browser keeps its own opened copy of the signing key: a
42
+ // non-extractable CryptoKey that never leaves the origin — the same protection
43
+ // the OIDC session in the next IDB row already relies on. It is what lets the
44
+ // worker boot itself after an idle kill without a network read.
46
45
  export const keyCacheKey = (actorUrl) => `signing-keys:${actorUrl}`;
47
46
  // The cache is keyed by the POD actor, whatever the identity advertises: a
48
47
  // fronted identity's advertised actor is the gateway's, and keying by that
49
- // would hide the opened copy sign-up stored and ask for the password again.
48
+ // would hide the opened copy sign-up stored and read the pod again.
50
49
  export const podActorOf = (urls) => (urls.toPod ? urls.toPod(urls.actor) : urls.actor);
51
50
 
52
51
  // Read the signing key for this actor and import it for signing.
53
52
  //
54
- // Two places hold one: this browser (opened, in IndexedDB) and the pod (wrapped).
55
- // The browser's is tried first — it is the only one that can be read with no
56
- // password, and a service worker has nobody to ask. Falling through to the pod
57
- // covers the browser that has just signed in and has no copy yet: a bare record
58
- // there is from before wrapping and is used as it stands, while an envelope
53
+ // Two places hold one: this browser (opened, in IndexedDB) and the pod. The
54
+ // browser's is tried first — no network read. Falling through to the pod covers
55
+ // the browser that has just signed in and has no copy yet: the record is read
56
+ // with the pod session and used as it stands. An envelope from before 1.28.0
59
57
  // needs the password and so has to go back to the page.
60
58
  export async function loadKeysFromPod(remote, urls) {
61
59
  const podActor = podActorOf(urls);
@@ -65,10 +63,9 @@ export async function loadKeysFromPod(remote, urls) {
65
63
  // in its place, so the PEM is gone from storage after one boot.
66
64
  if (cached?.rsa?.privatePem) return cacheOpenedKeys(podActor, cached);
67
65
 
68
- const doc = await podState.readWrappedKeys(remote, urls);
66
+ const doc = await podState.readKeys(remote, urls);
69
67
  if (isKeyEnvelope(doc)) throw new KeyPasswordNeeded();
70
68
  if (!doc || !doc.rsa) throw new Error('no signing key on the pod — sign up did not finish');
71
- // A pre-wrapping install. Cache it so the next boot is one read, and leave
72
- // the pod's copy alone: re-wrapping it would need the password we do not have.
69
+ // Cache it so the next boot is no read at all.
73
70
  return cacheOpenedKeys(podActor, doc);
74
71
  }
@@ -1,22 +1,19 @@
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.
1
+ // keystore.mjs — the signing keys, made in the browser and stored on the pod.
3
2
  //
4
3
  // The Node agent keeps keys.json (RSA + Ed25519 PEMs) on disk, 0600. A browser
5
4
  // 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.
5
+ // the pod, in an owner-only container: the same access rule every private
6
+ // document on the pod lives under, reachable through the pod's own login and
7
+ // by nobody else. The shape is exactly the agent's keys.json, so it is used
8
+ // unchanged. A new browser reads it with its pod session and asks for nothing.
11
9
  //
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.
10
+ // Until 2026-09-22 the pod's copy was encrypted under the pod password, so a
11
+ // new browser had to ask for the password once. That sealed the key against
12
+ // the pod's host, which the access rule cannot do, at the price of fedipod.net
13
+ // asking for a pod password. Jeff decided the host is trusted the way it is
14
+ // for every other document, and the password went. `unwrapKeys`,
15
+ // `isKeyEnvelope` and `KeyPasswordNeeded` remain for accounts made before
16
+ // then: their key is opened once with the password and stored as it is.
20
17
 
21
18
  const PEM = (der, label) => {
22
19
  const b64 = btoa(String.fromCharCode(...new Uint8Array(der)));
@@ -56,38 +53,24 @@ async function deriveAesKey(password, salt, iterations) {
56
53
  base, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
57
54
  }
58
55
 
59
- const toB64 = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)));
60
56
  const fromB64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
61
57
 
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. */
58
+ /** Whether a document read off the pod is an envelope from before 1.28.0
59
+ * rather than the keys record. The shape is what tells them apart. */
65
60
  export function isKeyEnvelope(doc) {
66
61
  return !!doc && doc.v === 1 && typeof doc.ct === 'string' && typeof doc.salt === 'string';
67
62
  }
68
63
 
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. */
64
+ /** Thrown when the pod holds a key from before 1.28.0, under a password, and
65
+ * nothing in this browser can open it. The page catches it by `code` and asks
66
+ * for the password once; the worker cannot ask anyone anything. */
72
67
  export class KeyPasswordNeeded extends Error {
73
68
  constructor() { super('this browser needs your account password to unlock the signing key'); }
74
69
  code = 'key-password-needed';
75
70
  }
76
71
 
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. */
72
+ /** Open an envelope from before 1.28.0. Throws on a wrong password (AES-GCM
73
+ * auth failure) — which is how the page tells a typo from a real problem. */
91
74
  export async function unwrapKeys(envelope, password) {
92
75
  if (!envelope || envelope.v !== 1) throw new Error('not a key envelope');
93
76
  const aes = await deriveAesKey(password, fromB64(envelope.salt), envelope.iterations);
@@ -1,189 +1,6 @@
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
1
+ // oidc-session.mjs — FediPod's Solid-OIDC session: the solid-oidc-session
2
+ // library (lib/session/) bound to this app's database and client name. The
3
+ // page and the service worker both import this file, so both read one session.
4
+ import { solidOidcSession } from '../../lib/session/oidc-session.mjs';
17
5
 
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
- }
6
+ export const { beginLogin, completeLogin, getSession, signOut } = solidOidcSession({ dbName: 'fedipod-oidc', clientName: 'FediPod' });