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.
- package/dist/claims.js +1 -1
- package/dist/handler.d.ts +1 -1
- package/dist/handler.js +35 -2
- package/dist/handler.jsonld +1 -1
- package/lib/admin.mjs +3 -3
- package/lib/c2s.mjs +27 -2
- package/lib/deliver.mjs +6 -0
- package/lib/embed.mjs +1 -0
- package/lib/export-collections.mjs +1 -1
- package/lib/front-core.mjs +272 -49
- package/lib/gateway-core.mjs +3 -7
- package/lib/httpsig.mjs +33 -2
- package/lib/import.mjs +1 -1
- package/lib/intake.mjs +357 -118
- package/lib/links.mjs +6 -32
- package/lib/mastoapi.mjs +449 -38
- package/lib/migrate.mjs +4 -18
- package/lib/pod/README.md +61 -0
- package/lib/pod/actor.mjs +96 -0
- package/lib/pod/collection.mjs +77 -0
- package/lib/pod/containers.mjs +136 -0
- package/lib/pod/discovery.mjs +46 -0
- package/lib/pod/featured.mjs +18 -0
- package/lib/pod/followers.mjs +24 -0
- package/lib/pod/following.mjs +10 -0
- package/lib/pod/http.mjs +74 -0
- package/lib/pod/inbox.mjs +144 -0
- package/lib/pod/links.mjs +35 -0
- package/lib/pod/media.mjs +27 -0
- package/lib/pod/notes.mjs +80 -0
- package/lib/pod/notifications.mjs +89 -0
- package/lib/pod/outbox.mjs +18 -0
- package/lib/pod/package.json +19 -0
- package/lib/pod/policy.mjs +58 -0
- package/lib/pod/private.mjs +19 -0
- package/lib/pod/root.mjs +63 -0
- package/lib/pod/state.mjs +39 -0
- package/lib/pod/transport.mjs +477 -0
- package/lib/pod/urls.mjs +71 -0
- package/lib/polls.mjs +105 -0
- package/lib/publisher.mjs +357 -188
- package/lib/remote.mjs +30 -395
- package/lib/social.mjs +20 -9
- package/lib/storage.mjs +2 -2
- package/lib/store.mjs +8 -1
- package/lib/tagfeed.mjs +19 -2
- package/lib/wire.mjs +87 -67
- package/package.json +1 -1
- package/run-agent.mjs +20 -39
- package/web/admin/admin.js +141 -74
- package/web/admin/bar.css +9 -13
- package/web/admin/bar.js +2 -2
- package/web/admin/client/client.js +1 -1
- package/web/admin/client/index.html +5 -6
- package/web/admin/index.html +53 -65
- package/web/admin/setup/index.html +2 -1
- package/web/admin/setup/setup.js +1 -1
- package/web/admin/tokens.css +56 -0
- package/web/app/README.md +77 -0
- package/web/app/admin-facade.mjs +549 -0
- package/web/app/agent.mjs +490 -0
- package/web/app/atproto-browser.mjs +86 -0
- package/web/app/boot.mjs +340 -0
- package/web/app/deliver-relay.mjs +72 -0
- package/web/app/dist/boot.js +34067 -0
- package/web/app/dist/boot.js.map +7 -0
- package/web/app/dist/sw.js +53745 -0
- package/web/app/dist/sw.js.map +7 -0
- package/web/app/fediacct-browser.mjs +146 -0
- package/web/app/idb-kv.mjs +35 -0
- package/web/app/index.html +203 -0
- package/web/app/index.html~ +152 -0
- package/web/app/keys-browser.mjs +46 -0
- package/web/app/keystore.mjs +99 -0
- package/web/app/oidc-session.mjs +189 -0
- package/web/app/pod-auth.mjs +248 -0
- package/web/app/pod-remote.mjs +84 -0
- package/web/app/shims/_globals.mjs +2 -0
- package/web/app/shims/fedify-sig.mjs +77 -0
- package/web/app/shims/node-crypto.mjs +113 -0
- package/web/app/shims/node-fs.mjs +17 -0
- package/web/app/shims/node-path.mjs +19 -0
- package/web/app/shims/node-url.mjs +7 -0
- package/web/app/shims/prelude.js +58 -0
- package/web/app/shims/safefetch.mjs +58 -0
- package/web/app/shims/web-push.mjs +14 -0
- package/web/app/signup.mjs +248 -0
- package/web/app/site/_headers +9 -0
- package/web/app/site/_redirects +21 -0
- package/web/app/site/admin/admin.js +1181 -0
- package/web/app/site/admin/bar.css +60 -0
- package/web/app/site/admin/bar.js +61 -0
- package/web/app/site/admin/client/client.js +66 -0
- package/web/app/site/admin/client/index.html +43 -0
- package/web/app/site/admin/index.html +556 -0
- package/web/app/site/admin/setup/index.html +191 -0
- package/web/app/site/admin/setup/setup.js +308 -0
- package/web/app/site/admin/tokens.css +56 -0
- package/web/app/site/admin/window.css +55 -0
- package/web/app/site/admin/window.js +136 -0
- package/web/app/site/app/404.html +40 -0
- package/web/app/site/app/_headers +2 -0
- package/web/app/site/app/apple-touch-icon.png +0 -0
- package/web/app/site/app/assets/ICONS-DIPJeU0C.js +2 -0
- package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +1 -0
- package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +183 -0
- package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +1 -0
- package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
- package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +2 -0
- package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +1 -0
- package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
- package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
- package/web/app/site/app/assets/chunk-aKtaBQYM.js +1 -0
- package/web/app/site/app/assets/compose-CFEx7wb4.js +2 -0
- package/web/app/site/app/assets/compose-CFEx7wb4.js.map +1 -0
- package/web/app/site/app/assets/compose-ZJN5xSb4.js +35 -0
- package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +1 -0
- package/web/app/site/app/assets/debug.module-YFQgwnIr.js +52 -0
- package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +1 -0
- package/web/app/site/app/assets/dom-p1YcJ5Rw.js +2 -0
- package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +1 -0
- package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
- package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
- package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
- package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
- package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +1 -0
- package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +1 -0
- package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +1 -0
- package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +1 -0
- package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +1 -0
- package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +1 -0
- package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +1 -0
- package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +1 -0
- package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +1 -0
- package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +1 -0
- package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +1 -0
- package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +1 -0
- package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +1 -0
- package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +1 -0
- package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +1 -0
- package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +1 -0
- package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +1 -0
- package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +1 -0
- package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +1 -0
- package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +1 -0
- package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +1 -0
- package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +1 -0
- package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +1 -0
- package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +1 -0
- package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +1 -0
- package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +1 -0
- package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +1 -0
- package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +1 -0
- package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +1 -0
- package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +1 -0
- package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +1 -0
- package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +1 -0
- package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +1 -0
- package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +1 -0
- package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +1 -0
- package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +1 -0
- package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +1 -0
- package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +1 -0
- package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +1 -0
- package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +1 -0
- package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +1 -0
- package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +1 -0
- package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +1 -0
- package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +1 -0
- package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +1 -0
- package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +1 -0
- package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +1 -0
- package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +1 -0
- package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +1 -0
- package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +1 -0
- package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +1 -0
- package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +1 -0
- package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +1 -0
- package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +1 -0
- package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +1 -0
- package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +1 -0
- package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +1 -0
- package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +1 -0
- package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +1 -0
- package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +1 -0
- package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +1 -0
- package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +1 -0
- package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +1 -0
- package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +1 -0
- package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +1 -0
- package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +1 -0
- package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +1 -0
- package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +1 -0
- package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +1 -0
- package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +1 -0
- package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +1 -0
- package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +1 -0
- package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +1 -0
- package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +1 -0
- package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +1 -0
- package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +1 -0
- package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +1 -0
- package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +1 -0
- package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +1 -0
- package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +1 -0
- package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +1 -0
- package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +1 -0
- package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +1 -0
- package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +1 -0
- package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +1 -0
- package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +1 -0
- package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +1 -0
- package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +1 -0
- package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +1 -0
- package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +1 -0
- package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +1 -0
- package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +1 -0
- package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +1 -0
- package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +1 -0
- package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +1 -0
- package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +1 -0
- package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +1 -0
- package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +1 -0
- package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +1 -0
- package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +1 -0
- package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +1 -0
- package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +1 -0
- package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +1 -0
- package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +1 -0
- package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +1 -0
- package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +1 -0
- package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +1 -0
- package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +1 -0
- package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +1 -0
- package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +1 -0
- package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +1 -0
- package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +1 -0
- package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +1 -0
- package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +1 -0
- package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +1 -0
- package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +1 -0
- package/web/app/site/app/assets/instances-BNPptnn-.json +333 -0
- package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +1 -0
- package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +1 -0
- package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +1 -0
- package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +1 -0
- package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +1 -0
- package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +1 -0
- package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +1 -0
- package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +1 -0
- package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +1 -0
- package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +1 -0
- package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +1 -0
- package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +1 -0
- package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +1 -0
- package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +1 -0
- package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +1 -0
- package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +1 -0
- package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +1 -0
- package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +1 -0
- package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +1 -0
- package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +1 -0
- package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +1 -0
- package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/tok-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +1 -0
- package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +1 -0
- package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/main-BdqNbG-a.js +26 -0
- package/web/app/site/app/assets/main-BdqNbG-a.js.map +1 -0
- package/web/app/site/app/assets/mock-home-DGhR__He.js +2 -0
- package/web/app/site/app/assets/mock-home-DGhR__He.js.map +1 -0
- package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
- package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
- package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
- package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +2 -0
- package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +1 -0
- package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +20 -0
- package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +1 -0
- package/web/app/site/app/assets/style-B1iLPTPR.css +2 -0
- package/web/app/site/app/assets/temml-DuyIQMei.js +97 -0
- package/web/app/site/app/assets/temml-DuyIQMei.js.map +1 -0
- package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +2 -0
- package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +1 -0
- package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
- package/web/app/site/app/assets/year-in-posts-DJv_star.js +3 -0
- package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +1 -0
- package/web/app/site/app/compose/index.html +20 -0
- package/web/app/site/app/favicon.ico +0 -0
- package/web/app/site/app/index.html +73 -0
- package/web/app/site/app/logo-192.png +0 -0
- package/web/app/site/app/logo-512.png +0 -0
- package/web/app/site/app/logo-badge-72.png +0 -0
- package/web/app/site/app/logo-maskable-512.png +0 -0
- package/web/app/site/app/logo-monochrome-512.png +0 -0
- package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
- package/web/app/site/app/manifest.webmanifest +1 -0
- package/web/app/site/app/og-image-2.jpg +0 -0
- package/web/app/site/app/og-image.png +0 -0
- package/web/app/site/app/robots.txt +2 -0
- package/web/app/site/app/version.json +1 -0
- package/web/app/site/boot.js +34067 -0
- package/web/app/site/index.html +203 -0
- package/web/app/site/sw.js +53745 -0
- package/web/app/sw-src.mjs +205 -0
- package/web/front/#new-account.html# +43 -0
- package/web/front/admin.html +1 -81
- package/web/front/admin.js +85 -0
- package/web/front/new-account.html +1 -9
- package/web/front/new-account.js +13 -0
- package/web/front/run.html +1 -92
- package/web/front/run.js +107 -0
- package/lib/podrdf.mjs +0 -132
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// sw-src.mjs — the service worker that hosts the in-browser agent.
|
|
2
|
+
//
|
|
3
|
+
// Phanpy (served static on this origin) calls the Mastodon API on its own
|
|
4
|
+
// origin. This worker answers those calls from the agent running inside it, so
|
|
5
|
+
// there is no server behind the client. The page boots the agent by posting it
|
|
6
|
+
// the sign-up material (credential, config, plaintext keys); the worker holds
|
|
7
|
+
// the agent for the life of the worker and intercepts the facade paths. Static
|
|
8
|
+
// files fall through to the network.
|
|
9
|
+
import { BrowserAgent } from './agent.mjs';
|
|
10
|
+
import { getSession } from './oidc-session.mjs';
|
|
11
|
+
import { ADMIN_PATHS } from './admin-facade.mjs';
|
|
12
|
+
|
|
13
|
+
let agent = null;
|
|
14
|
+
let booting = null;
|
|
15
|
+
|
|
16
|
+
const FACADE = /^\/(api\/v[12]\/|oauth\/|nodeinfo\/)/; // Mastodon paths only — never the front's own /api/handle, /api/attach, /api/relay
|
|
17
|
+
const FACADE_EXACT = new Set(['/.well-known/nodeinfo', '/.well-known/oauth-authorization-server', '/nodeinfo/2.0']);
|
|
18
|
+
const isFacade = (p) => FACADE.test(p) || FACADE_EXACT.has(p);
|
|
19
|
+
// The owner's record/manage data endpoints (see admin-facade.mjs). The web/admin
|
|
20
|
+
// PAGES are static files Netlify serves; only these data calls are ours.
|
|
21
|
+
const isAdmin = (p) => ADMIN_PATHS.has(p);
|
|
22
|
+
|
|
23
|
+
// ---- who may drive these routes ----
|
|
24
|
+
//
|
|
25
|
+
// Same-origin is the WHOLE trust boundary for this build. The worker answers
|
|
26
|
+
// the owner's admin routes with no password and no token — the only interlock
|
|
27
|
+
// on the destructive ones is `confirm === handle`, and the handle is public —
|
|
28
|
+
// so without this gate any page the owner merely visited could move every
|
|
29
|
+
// follower to an attacker's account, retire the identity, rotate the key,
|
|
30
|
+
// point the inbox at an attacker's gateway, or set a UI password the owner
|
|
31
|
+
// does not know. The Node agent is covered by lib/guard.mjs; nothing from
|
|
32
|
+
// that file is in this bundle, and it could not do the job here anyway —
|
|
33
|
+
// `Sec-Fetch-*` headers are NOT visible inside a service worker's fetch
|
|
34
|
+
// event, and neither is `Origin`. So the gate is built from what a worker
|
|
35
|
+
// CAN see: the request's mode, its referrer, and its headers.
|
|
36
|
+
//
|
|
37
|
+
// The header is what does the work, and it does not need to be a secret to do
|
|
38
|
+
// it. A cross-site FORM cannot set a header at all. A cross-origin fetch()
|
|
39
|
+
// that sets one is a preflighted request, and nothing here ever answers a
|
|
40
|
+
// preflight with CORS headers, so the browser never sends the real one. That
|
|
41
|
+
// leaves a top-level navigation, which also carries no custom headers.
|
|
42
|
+
//
|
|
43
|
+
// (The review suggested making it a per-session secret handed to the page by
|
|
44
|
+
// postMessage. Deliberately not carried: the header already refuses every
|
|
45
|
+
// cross-ORIGIN caller, and a secret would only add something against an
|
|
46
|
+
// attacker already running script on this origin — who could read it straight
|
|
47
|
+
// out of the page. The defence at that layer is the CSP, in the site's
|
|
48
|
+
// _headers.)
|
|
49
|
+
const PAGE_HEADER = 'x-fedipod-page';
|
|
50
|
+
|
|
51
|
+
// The one route that MUST survive a cross-site navigation: the other server's
|
|
52
|
+
// OAuth redirect lands the browser here, from their origin, by design. Its
|
|
53
|
+
// `state` parameter is what stands in for the header.
|
|
54
|
+
const NAV_ALLOWED = new Set(['/fediacct/callback']);
|
|
55
|
+
|
|
56
|
+
const sameOriginReferrer = (request) => {
|
|
57
|
+
const r = request.referrer;
|
|
58
|
+
// 'about:client' is the browser saying "the context that asked", which for a
|
|
59
|
+
// request this worker intercepts is a page on this origin.
|
|
60
|
+
if (!r || r === 'about:client') return r === 'about:client';
|
|
61
|
+
try { return new URL(r).origin === self.location.origin; } catch { return false; }
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Returns null when the request may proceed, else the reason to refuse with.
|
|
65
|
+
function notAllowed(request, url) {
|
|
66
|
+
const p = url.pathname;
|
|
67
|
+
|
|
68
|
+
// The Mastodon client API is deliberately open, exactly as it is on the Node
|
|
69
|
+
// agent and on any real instance: a bearer is its credential, and a client
|
|
70
|
+
// has to be able to call it. It mints nothing without one.
|
|
71
|
+
if (!isAdmin(p) && !p.startsWith('/oauth/')) return null;
|
|
72
|
+
|
|
73
|
+
if (request.mode === 'navigate') {
|
|
74
|
+
if (NAV_ALLOWED.has(p)) return null;
|
|
75
|
+
// The bundled client signs in by navigating its frame to /oauth/authorize
|
|
76
|
+
// from a page on this origin. A navigation from anywhere else — or from
|
|
77
|
+
// nowhere, which is what a page that suppressed its referrer looks like —
|
|
78
|
+
// is not that, and /oauth/authorize is where a 90-day bearer is minted.
|
|
79
|
+
if (p.startsWith('/oauth/') && sameOriginReferrer(request)) return null;
|
|
80
|
+
return 'a navigation may not drive this route';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// /oauth/* is not open the way /api/* is: the only client here is the one
|
|
84
|
+
// served from this origin, so a call from another origin is never legitimate.
|
|
85
|
+
if (p.startsWith('/oauth/')) {
|
|
86
|
+
return sameOriginReferrer(request) ? null : 'this route answers this origin only';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (request.headers.get(PAGE_HEADER) !== '1') return `missing ${PAGE_HEADER}`;
|
|
90
|
+
// Belt and braces on top of the header: the facade parses a body as JSON
|
|
91
|
+
// whatever its content type, and `text/plain` is the one a cross-site form
|
|
92
|
+
// can send. Nothing that got past the header sends anything else anyway.
|
|
93
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
94
|
+
const ct = (request.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
|
|
95
|
+
if (ct && ct !== 'application/json') return `unexpected content type "${ct}"`;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
self.addEventListener('install', () => self.skipWaiting());
|
|
101
|
+
self.addEventListener('activate', (e) => e.waitUntil(self.clients.claim()));
|
|
102
|
+
|
|
103
|
+
// Boot the agent from the stored Solid-OIDC session. The sign-in page posts a
|
|
104
|
+
// 'boot' to prime it, but the worker ALSO boots itself on demand (see serve):
|
|
105
|
+
// browsers kill an idle service worker and restart it fresh, and the client at
|
|
106
|
+
// /app/ never posts 'boot' — so without on-demand boot every request after a
|
|
107
|
+
// restart 503s. On a boot failure `booting` is cleared so the next request retries.
|
|
108
|
+
async function bootFromSession(frontOrigin) {
|
|
109
|
+
const oidc = await getSession();
|
|
110
|
+
if (!oidc) throw new Error('no session — sign in first');
|
|
111
|
+
const a = new BrowserAgent({ log: (m) => console.log('[sw-agent]', m) });
|
|
112
|
+
await a.boot({ oidc, frontOrigin: frontOrigin || self.location.origin });
|
|
113
|
+
agent = a;
|
|
114
|
+
return a;
|
|
115
|
+
}
|
|
116
|
+
function ensureBooting(frontOrigin) {
|
|
117
|
+
if (agent) return Promise.resolve(agent);
|
|
118
|
+
if (!booting) booting = bootFromSession(frontOrigin).catch((err) => { booting = null; throw err; });
|
|
119
|
+
return booting;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
self.addEventListener('message', (e) => {
|
|
123
|
+
// Drop the identity this worker is holding. A worker outlives a page, so
|
|
124
|
+
// signing in as somebody else left the FIRST account's agent live and serving
|
|
125
|
+
// — the new session sat in IndexedDB while every request was still answered
|
|
126
|
+
// by the old actor, with the old key. The page posts this before it boots the
|
|
127
|
+
// new one; the next request boots from whatever session is stored now.
|
|
128
|
+
if (e.data?.type === 'reset') {
|
|
129
|
+
const had = !!agent;
|
|
130
|
+
agent = null; booting = null;
|
|
131
|
+
e.source?.postMessage({ type: 'reset-done', had });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (e.data?.type !== 'boot') return;
|
|
135
|
+
ensureBooting(e.data.frontOrigin)
|
|
136
|
+
.then(() => e.source?.postMessage({ type: 'booted' }))
|
|
137
|
+
// `code` matters: a boot that failed only because this browser has never
|
|
138
|
+
// opened the signing key is not a failure the page should show as one — it
|
|
139
|
+
// is a question the page can answer (boot.mjs). Everything else is shown.
|
|
140
|
+
.catch((err) => e.source?.postMessage({ type: 'boot-error', error: err.message,
|
|
141
|
+
code: err.code || null, stack: String(err.stack || '') }));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
self.addEventListener('fetch', (e) => {
|
|
145
|
+
const url = new URL(e.request.url);
|
|
146
|
+
if (url.origin !== self.location.origin || !(isFacade(url.pathname) || isAdmin(url.pathname))) return; // static → network
|
|
147
|
+
e.respondWith(serve(e.request, url));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
async function serve(request, url) {
|
|
151
|
+
// Before anything is booted or read: is this caller allowed to be here at all?
|
|
152
|
+
const refuse = notAllowed(request, url);
|
|
153
|
+
if (refuse) {
|
|
154
|
+
console.warn(`[sw-agent] refused ${request.method} ${url.pathname}: ${refuse}`);
|
|
155
|
+
return json(403, { error: `refused: ${refuse}` });
|
|
156
|
+
}
|
|
157
|
+
// Boot on demand from the stored session, so the client works even when the
|
|
158
|
+
// worker was restarted (idle-killed) and nobody posted 'boot' this time.
|
|
159
|
+
if (!agent) { try { await ensureBooting(); } catch { /* no session → 503 below */ } }
|
|
160
|
+
if (!agent) return json(503, { error: 'the agent is not booted yet — open the app from the sign-in page' });
|
|
161
|
+
// Bytes, not text. A multipart upload is binary — read as text it comes back
|
|
162
|
+
// through a UTF-8 round trip that replaces every byte that is not valid UTF-8,
|
|
163
|
+
// which is most of a JPEG, so the boundary search found nothing and every
|
|
164
|
+
// media and avatar upload answered "422 file required". readBody() does
|
|
165
|
+
// `data += chunk`, which decodes a Buffer the same way it always did, so the
|
|
166
|
+
// JSON and form paths are unchanged.
|
|
167
|
+
const bodyBytes = (request.method === 'GET' || request.method === 'HEAD')
|
|
168
|
+
? null : Buffer.from(new Uint8Array(await request.arrayBuffer()));
|
|
169
|
+
// The admin facade is JSON-only (the gate above enforces the content type),
|
|
170
|
+
// and takes its body already decoded.
|
|
171
|
+
const bodyText = bodyBytes ? new TextDecoder().decode(bodyBytes) : '';
|
|
172
|
+
const reqHeaders = {}; for (const [k, v] of request.headers) reqHeaders[k.toLowerCase()] = v;
|
|
173
|
+
// `host` is a forbidden header name, so a fetch Request never carries one and
|
|
174
|
+
// the loop above cannot produce it — but it is a header every real request
|
|
175
|
+
// arrives with, and the agent reads it to say where it lives. Without it the
|
|
176
|
+
// notifications `Link` header named `https://undefined/`, so a client that
|
|
177
|
+
// paged by following it (which is how a client is meant to page) walked off
|
|
178
|
+
// the origin and saw nothing past the first screen.
|
|
179
|
+
reqHeaders.host = url.host;
|
|
180
|
+
const listeners = {};
|
|
181
|
+
const req = { method: request.method, url: url.pathname + url.search, headers: reqHeaders,
|
|
182
|
+
// notAllowed() above let this through, so it came from this origin. Said
|
|
183
|
+
// out loud rather than left implicit: MastoApi asks (through the
|
|
184
|
+
// authorities object in agent.mjs) whether a request is the owner's own,
|
|
185
|
+
// and in a browser that question means exactly this.
|
|
186
|
+
sameOrigin: true,
|
|
187
|
+
socket: { encrypted: url.protocol === 'https:' }, on(ev, cb) { (listeners[ev] ||= []).push(cb); return req; }, destroy() {} };
|
|
188
|
+
queueMicrotask(() => { if (bodyBytes?.length) (listeners.data || []).forEach((cb) => cb(bodyBytes)); (listeners.end || []).forEach((cb) => cb()); });
|
|
189
|
+
let status = 200; const outHeaders = {}; const chunks = [];
|
|
190
|
+
const res = {
|
|
191
|
+
writeHead(s, h) { status = s; if (h) Object.assign(outHeaders, h); return res; },
|
|
192
|
+
setHeader(k, v) { outHeaders[k] = v; }, getHeader(k) { return outHeaders[k]; },
|
|
193
|
+
write(c) { chunks.push(c); }, end(c) { if (c) chunks.push(c); },
|
|
194
|
+
};
|
|
195
|
+
try {
|
|
196
|
+
const handled = isAdmin(url.pathname)
|
|
197
|
+
? await agent.admin.handle(req, res, url.pathname, url, bodyText)
|
|
198
|
+
: await agent.masto.handle(req, res, url.pathname, url);
|
|
199
|
+
if (!handled) return fetch(request);
|
|
200
|
+
return new Response(chunks.join(''), { status, headers: { 'content-type': 'application/json', ...outHeaders } });
|
|
201
|
+
} catch (err) {
|
|
202
|
+
return json(500, { error: err.message });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const json = (status, obj) => new Response(JSON.stringify(obj), { status, headers: { 'content-type': 'application/json' } });
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>FediPod — access the Fediverse and ATProto from a Solid pod</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: light dark; --line:#d9d4c7; --dim:#45443d; --accent:#6a8a5a; }
|
|
9
|
+
@media (prefers-color-scheme: dark) { :root { --dim:#c2c0b4; } }
|
|
10
|
+
body { font: 19px/1.6 system-ui, sans-serif; max-width: 44rem; margin: 2rem auto; padding: 0 1.1rem; }
|
|
11
|
+
h1 { font-size: 1.7rem; margin: 0 0 .2rem; }
|
|
12
|
+
h2 { font-size: 1.2rem; margin: 1.4rem 0 .4rem; }
|
|
13
|
+
p.lede { color: var(--dim); margin-top: 0; }
|
|
14
|
+
.hint { color: var(--dim); font-size: .95rem; }
|
|
15
|
+
code { background: rgba(120,120,120,.12); padding: .05em .35em; border-radius: 4px; }
|
|
16
|
+
pre { background: rgba(120,120,120,.12); padding: .7rem .9rem; border-radius: 8px; overflow-x: auto; }
|
|
17
|
+
ol.steps li { margin: .5rem 0; }
|
|
18
|
+
[hidden] { display: none !important; }
|
|
19
|
+
</style>
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<h1>FediPod - join the Fediverse from a Solid pod.</h1>
|
|
23
|
+
<p class="lede">Welcome to <b>FediPod</b>! Here, you can get a free pod and Fediverse account or link to your existing pod/account.</p>
|
|
24
|
+
|
|
25
|
+
<p class="hint" id="current-version" hidden>Current FediPod: <strong id="current-version-num"></strong><br>
|
|
26
|
+
— install or update an existing install with <code id="current-version-cmd"></code></p>
|
|
27
|
+
|
|
28
|
+
<p>
|
|
29
|
+
FediPod is experimental and free to use. See the <a href="https://github.com/jeff-zucker/FediPod">FediPod repository</a> for details.
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
<script>
|
|
33
|
+
fetch('/api/handle?handle=__probe__').then(r => r.json()).then(d => {
|
|
34
|
+
if (d.version) {
|
|
35
|
+
document.getElementById('current-version-num').textContent = d.version;
|
|
36
|
+
document.getElementById('current-version-cmd').textContent = `curl -fsSL ${location.origin}/install | sh`;
|
|
37
|
+
document.getElementById('current-version').hidden = false;
|
|
38
|
+
}
|
|
39
|
+
}).catch(() => {});
|
|
40
|
+
</script>
|
|
41
|
+
<i style="text-align:right">(cc) 4.0 By, Jeff Zucker, 2026</i>
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|
package/web/front/admin.html
CHANGED
|
@@ -47,87 +47,7 @@ accounts that only use it as their gateway. Sign in as the server's admin to see
|
|
|
47
47
|
<tbody id="roster-rows"></tbody>
|
|
48
48
|
</table>
|
|
49
49
|
|
|
50
|
-
<script type="module">
|
|
51
|
-
const $ = (id) => document.getElementById(id);
|
|
52
|
-
const note = (text) => { const n = $('roster-note'); n.hidden = !text; n.textContent = text || ''; };
|
|
53
|
-
|
|
54
|
-
// The Solid-OIDC client for the sign-in round trip. Constructed once; the
|
|
55
|
-
// redirect state lives in this tab's sessionStorage, so the same session
|
|
56
|
-
// finishes the login on the way back.
|
|
57
|
-
let session = null;
|
|
58
|
-
try {
|
|
59
|
-
const { SessionCore } = await import('/solid-oidc-client.js');
|
|
60
|
-
session = new SessionCore({ redirect_uris: [location.origin + '/admin'], client_name: 'FediPod admin' });
|
|
61
|
-
} catch { /* leave null — the "did not load" note fires */ }
|
|
62
|
-
|
|
63
|
-
$('roster-issuer').addEventListener('input', () => {
|
|
64
|
-
$('roster-signin').disabled = !/^https?:\/\/\S+/.test($('roster-issuer').value.trim());
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
$('roster-form').addEventListener('submit', (ev) => {
|
|
68
|
-
ev.preventDefault();
|
|
69
|
-
if (!session) { note('the sign-in library did not load — reload and try again'); return; }
|
|
70
|
-
session.login($('roster-issuer').value.trim(), location.origin + '/admin')
|
|
71
|
-
.catch((e) => note('sign-in failed to start: ' + e.message));
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Back from the identity provider: read the roster with the proven login.
|
|
75
|
-
(async () => {
|
|
76
|
-
if (!session) return;
|
|
77
|
-
await session.handleRedirectFromLogin().catch(() => {});
|
|
78
|
-
if (!session.isActive) return;
|
|
79
|
-
const webId = session.webId;
|
|
80
|
-
|
|
81
|
-
// Signed fetches build a DPoP proof from the URL, so it must be absolute.
|
|
82
|
-
const load = async () => {
|
|
83
|
-
let res;
|
|
84
|
-
try { res = await session.authFetch(location.origin + '/api/roster'); }
|
|
85
|
-
catch (e) { note('the roster request failed: ' + e.message); return; }
|
|
86
|
-
const d = await res.json().catch(() => ({}));
|
|
87
|
-
if (res.status === 403) { note('signed in as ' + webId + ', which is not this server’s admin'); return; }
|
|
88
|
-
if (res.status === 501) { note('this server names no admin — set FEDIPOD_ADMIN_WEBID and redeploy'); return; }
|
|
89
|
-
if (res.status !== 200) { note('roster unavailable: ' + (d.error || 'HTTP ' + res.status)); return; }
|
|
90
|
-
const rows = $('roster-rows');
|
|
91
|
-
rows.textContent = '';
|
|
92
|
-
for (const a of d.accounts || []) {
|
|
93
|
-
const tr = document.createElement('tr');
|
|
94
|
-
const cell = (child) => { const td = document.createElement('td'); td.append(child); tr.append(td); };
|
|
95
|
-
cell(a.address);
|
|
96
|
-
cell(a.kind);
|
|
97
|
-
cell(a.fronted ? 'lives here' : 'gateway only');
|
|
98
|
-
const pod = document.createElement('a');
|
|
99
|
-
pod.href = a.podHome; pod.textContent = new URL(a.podHome).host;
|
|
100
|
-
cell(pod);
|
|
101
|
-
const rm = document.createElement('button');
|
|
102
|
-
rm.type = 'button'; rm.textContent = 'Remove';
|
|
103
|
-
rm.onclick = () => revoke(a);
|
|
104
|
-
cell(rm);
|
|
105
|
-
rows.append(tr);
|
|
106
|
-
}
|
|
107
|
-
$('roster-table').hidden = false;
|
|
108
|
-
note((d.accounts || []).length + ' account(s) on ' + d.host);
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const revoke = async (a) => {
|
|
112
|
-
if (!confirm('Remove ' + a.address + ' from this server? The name stops resolving here; nothing on their pod is touched.')) return;
|
|
113
|
-
let res;
|
|
114
|
-
try {
|
|
115
|
-
res = await session.authFetch(location.origin + '/api/revoke', {
|
|
116
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
117
|
-
body: JSON.stringify({ handle: a.handle }),
|
|
118
|
-
});
|
|
119
|
-
} catch (e) { note('the remove request failed: ' + e.message); return; }
|
|
120
|
-
const d = await res.json().catch(() => ({}));
|
|
121
|
-
if (res.status !== 200) { note('remove refused: ' + (d.error || 'HTTP ' + res.status)); return; }
|
|
122
|
-
if (!d.removed) { note(a.address + ' stays: ' + d.reason); return; }
|
|
123
|
-
await load();
|
|
124
|
-
note('removed ' + a.address);
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
note('signed in as ' + webId + ' — reading the roster…');
|
|
128
|
-
await load();
|
|
129
|
-
})();
|
|
130
|
-
</script>
|
|
50
|
+
<script type="module" src="/admin.js"></script>
|
|
131
51
|
<i>(cc) 4.0 By, Jeff Zucker, 2026</i>
|
|
132
52
|
</body>
|
|
133
53
|
</html>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// admin.js — the roster page's script, in a file of its own.
|
|
2
|
+
//
|
|
3
|
+
// It lived inline in admin.html until 2026-09-09. Out here the page can be
|
|
4
|
+
// served under `script-src 'self'`, which is what stops a stray bit of markup
|
|
5
|
+
// from becoming running code; inline script cannot be told apart from injected
|
|
6
|
+
// script by any policy. Served by the front at /admin.js.
|
|
7
|
+
const $ = (id) => document.getElementById(id);
|
|
8
|
+
const note = (text) => { const n = $('roster-note'); n.hidden = !text; n.textContent = text || ''; };
|
|
9
|
+
|
|
10
|
+
// The Solid-OIDC client for the sign-in round trip. Constructed once; the
|
|
11
|
+
// redirect state lives in this tab's sessionStorage, so the same session
|
|
12
|
+
// finishes the login on the way back.
|
|
13
|
+
let session = null;
|
|
14
|
+
try {
|
|
15
|
+
const { SessionCore } = await import('/solid-oidc-client.js');
|
|
16
|
+
session = new SessionCore({ redirect_uris: [location.origin + '/roster'], client_name: 'FediPod admin' });
|
|
17
|
+
} catch { /* leave null — the "did not load" note fires */ }
|
|
18
|
+
|
|
19
|
+
$('roster-issuer').addEventListener('input', () => {
|
|
20
|
+
$('roster-signin').disabled = !/^https?:\/\/\S+/.test($('roster-issuer').value.trim());
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
$('roster-form').addEventListener('submit', (ev) => {
|
|
24
|
+
ev.preventDefault();
|
|
25
|
+
if (!session) { note('the sign-in library did not load — reload and try again'); return; }
|
|
26
|
+
session.login($('roster-issuer').value.trim(), location.origin + '/roster')
|
|
27
|
+
.catch((e) => note('sign-in failed to start: ' + e.message));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Back from the identity provider: read the roster with the proven login.
|
|
31
|
+
(async () => {
|
|
32
|
+
if (!session) return;
|
|
33
|
+
await session.handleRedirectFromLogin().catch(() => {});
|
|
34
|
+
if (!session.isActive) return;
|
|
35
|
+
const webId = session.webId;
|
|
36
|
+
|
|
37
|
+
// Signed fetches build a DPoP proof from the URL, so it must be absolute.
|
|
38
|
+
const load = async () => {
|
|
39
|
+
let res;
|
|
40
|
+
try { res = await session.authFetch(location.origin + '/api/roster'); }
|
|
41
|
+
catch (e) { note('the roster request failed: ' + e.message); return; }
|
|
42
|
+
const d = await res.json().catch(() => ({}));
|
|
43
|
+
if (res.status === 403) { note('signed in as ' + webId + ', which is not this server’s admin'); return; }
|
|
44
|
+
if (res.status === 501) { note('this server names no admin — set FEDIPOD_ADMIN_WEBID and redeploy'); return; }
|
|
45
|
+
if (res.status !== 200) { note('roster unavailable: ' + (d.error || 'HTTP ' + res.status)); return; }
|
|
46
|
+
const rows = $('roster-rows');
|
|
47
|
+
rows.textContent = '';
|
|
48
|
+
for (const a of d.accounts || []) {
|
|
49
|
+
const tr = document.createElement('tr');
|
|
50
|
+
const cell = (child) => { const td = document.createElement('td'); td.append(child); tr.append(td); };
|
|
51
|
+
cell(a.address);
|
|
52
|
+
cell(a.kind);
|
|
53
|
+
cell(a.fronted ? 'lives here' : 'gateway only');
|
|
54
|
+
const pod = document.createElement('a');
|
|
55
|
+
pod.href = a.podHome; pod.textContent = new URL(a.podHome).host;
|
|
56
|
+
cell(pod);
|
|
57
|
+
const rm = document.createElement('button');
|
|
58
|
+
rm.type = 'button'; rm.textContent = 'Remove';
|
|
59
|
+
rm.onclick = () => revoke(a);
|
|
60
|
+
cell(rm);
|
|
61
|
+
rows.append(tr);
|
|
62
|
+
}
|
|
63
|
+
$('roster-table').hidden = false;
|
|
64
|
+
note((d.accounts || []).length + ' account(s) on ' + d.host);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const revoke = async (a) => {
|
|
68
|
+
if (!confirm('Remove ' + a.address + ' from this server? The name stops resolving here; nothing on their pod is touched.')) return;
|
|
69
|
+
let res;
|
|
70
|
+
try {
|
|
71
|
+
res = await session.authFetch(location.origin + '/api/revoke', {
|
|
72
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({ handle: a.handle }),
|
|
74
|
+
});
|
|
75
|
+
} catch (e) { note('the remove request failed: ' + e.message); return; }
|
|
76
|
+
const d = await res.json().catch(() => ({}));
|
|
77
|
+
if (res.status !== 200) { note('remove refused: ' + (d.error || 'HTTP ' + res.status)); return; }
|
|
78
|
+
if (!d.removed) { note(a.address + ' stays: ' + d.reason); return; }
|
|
79
|
+
await load();
|
|
80
|
+
note('removed ' + a.address);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
note('signed in as ' + webId + ' — reading the roster…');
|
|
84
|
+
await load();
|
|
85
|
+
})();
|
|
@@ -29,15 +29,7 @@
|
|
|
29
29
|
FediPod is experimental and free to use. See the <a href="https://github.com/jeff-zucker/FediPod">FediPod repository</a> for details.
|
|
30
30
|
</p>
|
|
31
31
|
|
|
32
|
-
<script>
|
|
33
|
-
fetch('/api/handle?handle=__probe__').then(r => r.json()).then(d => {
|
|
34
|
-
if (d.version) {
|
|
35
|
-
document.getElementById('current-version-num').textContent = d.version;
|
|
36
|
-
document.getElementById('current-version-cmd').textContent = `curl -fsSL ${location.origin}/install | sh`;
|
|
37
|
-
document.getElementById('current-version').hidden = false;
|
|
38
|
-
}
|
|
39
|
-
}).catch(() => {});
|
|
40
|
-
</script>
|
|
32
|
+
<script src="/new-account.js"></script>
|
|
41
33
|
<i style="text-align:right">(cc) 4.0 By, Jeff Zucker, 2026</i>
|
|
42
34
|
</body>
|
|
43
35
|
</html>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// new-account.js — the sign-up page's script, in a file of its own.
|
|
2
|
+
//
|
|
3
|
+
// It lived inline in new-account.html until 2026-09-09. Out here the page can
|
|
4
|
+
// be served under `script-src 'self'`, which is what stops a stray bit of
|
|
5
|
+
// markup from becoming running code; inline script cannot be told apart from
|
|
6
|
+
// injected script by any policy. Served by the front at /new-account.js.
|
|
7
|
+
fetch('/api/handle?handle=__probe__').then(r => r.json()).then(d => {
|
|
8
|
+
if (d.version) {
|
|
9
|
+
document.getElementById('current-version-num').textContent = d.version;
|
|
10
|
+
document.getElementById('current-version-cmd').textContent = `curl -fsSL ${location.origin}/install | sh`;
|
|
11
|
+
document.getElementById('current-version').hidden = false;
|
|
12
|
+
}
|
|
13
|
+
}).catch(() => {});
|
package/web/front/run.html
CHANGED
|
@@ -45,98 +45,7 @@ you to keep running. Sign in with your pod to prove it is yours.</p>
|
|
|
45
45
|
<p class="hint" id="run-note" hidden></p>
|
|
46
46
|
</form>
|
|
47
47
|
|
|
48
|
-
<script type="module">
|
|
49
|
-
const $ = (id) => document.getElementById(id);
|
|
50
|
-
|
|
51
|
-
// The Solid-OIDC client for the sign-in round trip. Constructed once; the
|
|
52
|
-
// redirect state (PKCE, csrf) lives in this tab's sessionStorage, so the same
|
|
53
|
-
// session finishes the login on the way back.
|
|
54
|
-
let session = null;
|
|
55
|
-
try {
|
|
56
|
-
const { SessionCore } = await import('/solid-oidc-client.js');
|
|
57
|
-
session = new SessionCore({ redirect_uris: [location.origin + '/run'], client_name: 'FediPod gateway' });
|
|
58
|
-
} catch { /* leave null — the "did not load" notes below fire */ }
|
|
59
|
-
|
|
60
|
-
// Offered only where this server actually runs identities. An
|
|
61
|
-
// unauthenticated probe tells the two apart: 501 = not offered here,
|
|
62
|
-
// anything else = offered (a real opt-in still needs the signed-in proof).
|
|
63
|
-
let offered = true;
|
|
64
|
-
(async () => {
|
|
65
|
-
const res = await fetch('/api/agent', { method: 'POST',
|
|
66
|
-
headers: { 'content-type': 'application/json' }, body: '{}' }).catch(() => null);
|
|
67
|
-
if (!res || res.status === 501) {
|
|
68
|
-
offered = false;
|
|
69
|
-
const n = $('run-note');
|
|
70
|
-
n.hidden = false;
|
|
71
|
-
n.textContent = 'This server does not run identities. Your agent stays where it is.';
|
|
72
|
-
runFormCheck();
|
|
73
|
-
}
|
|
74
|
-
})();
|
|
75
|
-
|
|
76
|
-
function runFormCheck() {
|
|
77
|
-
let ok = false;
|
|
78
|
-
try { ok = /^https?:$/.test(new URL($('run-pod-url').value.trim()).protocol); } catch { /* not yet */ }
|
|
79
|
-
const issuerOk = /^https?:\/\/\S+/.test($('run-issuer').value.trim());
|
|
80
|
-
$('run-continue').disabled = !(offered && ok && issuerOk);
|
|
81
|
-
$('run-leave').disabled = !(offered && ok && issuerOk);
|
|
82
|
-
}
|
|
83
|
-
$('run-pod-url').addEventListener('input', () => {
|
|
84
|
-
runFormCheck();
|
|
85
|
-
// The identity provider is usually the server itself — still editable.
|
|
86
|
-
try {
|
|
87
|
-
const u = new URL($('run-pod-url').value.trim());
|
|
88
|
-
const apex = u.hostname.split('.').slice(1).join('.');
|
|
89
|
-
if (!$('run-issuer').value && apex) $('run-issuer').value = `${u.protocol}//${apex}${u.port ? ':' + u.port : ''}`;
|
|
90
|
-
} catch { /* not a URL yet */ }
|
|
91
|
-
runFormCheck();
|
|
92
|
-
});
|
|
93
|
-
$('run-issuer').addEventListener('input', runFormCheck);
|
|
94
|
-
|
|
95
|
-
const runStart = (action) => {
|
|
96
|
-
const n = $('run-note');
|
|
97
|
-
if (!session) { n.hidden = false; n.textContent = 'the sign-in library did not load — reload and try again'; return; }
|
|
98
|
-
const u = new URL($('run-pod-url').value.trim());
|
|
99
|
-
if (u.pathname === '') u.pathname = '/';
|
|
100
|
-
if (!u.pathname.endsWith('/')) u.pathname += '/';
|
|
101
|
-
u.search = ''; u.hash = '';
|
|
102
|
-
sessionStorage.setItem('fp-run', JSON.stringify({ podBase: u.href, action }));
|
|
103
|
-
session.login($('run-issuer').value.trim(), location.origin + '/run')
|
|
104
|
-
.catch((e) => { n.hidden = false; n.textContent = 'sign-in failed to start: ' + e.message; });
|
|
105
|
-
};
|
|
106
|
-
$('run-continue').onclick = () => runStart('opt-in');
|
|
107
|
-
$('run-leave').onclick = () => runStart('opt-out');
|
|
108
|
-
|
|
109
|
-
// Back from the identity provider: finish the opt-in with the proven login.
|
|
110
|
-
(async () => {
|
|
111
|
-
if (!session) return;
|
|
112
|
-
await session.handleRedirectFromLogin().catch(() => {});
|
|
113
|
-
const pending = sessionStorage.getItem('fp-run');
|
|
114
|
-
if (!session.isActive || !pending) return;
|
|
115
|
-
sessionStorage.removeItem('fp-run');
|
|
116
|
-
const p = JSON.parse(pending);
|
|
117
|
-
const n = $('run-note');
|
|
118
|
-
n.hidden = false;
|
|
119
|
-
n.textContent = 'signed in as ' + session.webId + ' — asking the server…';
|
|
120
|
-
// The signed fetch builds a DPoP proof from the URL, so it must be absolute.
|
|
121
|
-
const res = await session.authFetch(location.origin + '/api/agent', {
|
|
122
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
123
|
-
body: JSON.stringify({ action: p.action, podBase: p.podBase }),
|
|
124
|
-
}).catch(() => null);
|
|
125
|
-
const d = res ? await res.json().catch(() => ({})) : {};
|
|
126
|
-
if (res && res.status === 201 && d.doorSecret) {
|
|
127
|
-
n.innerHTML = 'Your identity runs here now. This is your door secret — <b>save it now</b>, '
|
|
128
|
-
+ 'it is shown only this once. It opens your admin pages at <code>' + p.podBase.replace(/\/$/, '') + d.doorPath + '</code>:'
|
|
129
|
-
+ '<pre>' + d.doorSecret + '</pre>'
|
|
130
|
-
+ 'To check it works:<pre>' + d.command + '</pre>'
|
|
131
|
-
+ 'Lost it? Opt in again — that mints a fresh one and retires this one.';
|
|
132
|
-
} else if (res && res.status === 200) {
|
|
133
|
-
n.textContent = 'Done — this server no longer runs that identity. Your pod and its data are untouched.';
|
|
134
|
-
} else {
|
|
135
|
-
n.textContent = (p.action === 'opt-in' ? 'opt-in' : 'opt-out') + ' failed: '
|
|
136
|
-
+ (d.error || (res ? 'HTTP ' + res.status : 'no response'));
|
|
137
|
-
}
|
|
138
|
-
})();
|
|
139
|
-
</script>
|
|
48
|
+
<script type="module" src="/run.js"></script>
|
|
140
49
|
<i>(cc) 4.0 By, Jeff Zucker, 2026</i>
|
|
141
50
|
</body>
|
|
142
51
|
</html>
|