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
package/dist/claims.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.claims = claims;
|
|
8
8
|
exports.agentClaims = agentClaims;
|
|
9
|
-
const FRONT_PATHS = new Set(['/', '/signup', '/new-account', '/run', '/
|
|
9
|
+
const FRONT_PATHS = new Set(['/', '/signup', '/new-account', '/run', '/roster',
|
|
10
10
|
'/.well-known/webfinger', '/api/handle', '/api/attach', '/api/agent',
|
|
11
11
|
'/api/roster', '/api/revoke',
|
|
12
12
|
// What the pages load: the sign-in library the /run and /admin pages use, and
|
package/dist/handler.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface FediPodServerArgs {
|
|
|
18
18
|
signupPage?: string;
|
|
19
19
|
/** The run-your-identity page HTML served at /run. */
|
|
20
20
|
runPage?: string;
|
|
21
|
-
/** The accounts-roster page HTML served at /
|
|
21
|
+
/** The accounts-roster page HTML served at /roster. */
|
|
22
22
|
adminPage?: string;
|
|
23
23
|
/** Directory holding each agent identity's signing key and log. Required when runtime opt-in is on. */
|
|
24
24
|
agentDataDir?: string;
|
package/dist/handler.js
CHANGED
|
@@ -441,15 +441,48 @@ class FediPodServerHandler extends community_server_1.HttpHandler {
|
|
|
441
441
|
signupPage: this.args.signupPage || webFile('new-account.html'),
|
|
442
442
|
runPage: this.args.runPage || webFile('run.html'),
|
|
443
443
|
adminPage: this.args.adminPage || webFile('admin.html'),
|
|
444
|
-
// The /run and /
|
|
444
|
+
// The /run and /roster pages load the sign-in library, and the signup page
|
|
445
445
|
// hands out the installer command; without these the pages render but
|
|
446
446
|
// cannot be used.
|
|
447
447
|
authBundle: webFile('solid-oidc-client.js'),
|
|
448
|
+
// Each page's own script — inline until 2026-09-09, so that the pages can
|
|
449
|
+
// be served under `script-src 'self'` (see lib/front-core.mjs).
|
|
450
|
+
pageScripts: {
|
|
451
|
+
'new-account.js': webFile('new-account.js'),
|
|
452
|
+
'run.js': webFile('run.js'),
|
|
453
|
+
'admin.js': webFile('admin.js'),
|
|
454
|
+
},
|
|
448
455
|
installScript: webFile('install.sh'),
|
|
449
456
|
lookup: (h) => this.dir.lookup(h),
|
|
450
457
|
putDirectory: (h, rec) => this.dir.putDirectory(h, rec),
|
|
451
458
|
podPut: (_handle, url, body, ct) => this.podPut(url, body, ct),
|
|
452
|
-
|
|
459
|
+
// Reads for the public proxy. `this.io.read` goes STRAIGHT into the
|
|
460
|
+
// resource store, which applies no access control of its own — so this is
|
|
461
|
+
// the one place that has to say what may be read, and it says: only
|
|
462
|
+
// inside the pod of the handle being served, and never a server-internal
|
|
463
|
+
// tree. Without it a row could name any location and this would fetch it
|
|
464
|
+
// (the directory, with every user's receipt secret, included).
|
|
465
|
+
//
|
|
466
|
+
// The front now refuses to attach such a row at all (podHomeProblem in
|
|
467
|
+
// lib/front-core.mjs); this is the same refusal at the other end, because
|
|
468
|
+
// a row can also arrive from a seed or an older deploy.
|
|
469
|
+
podGet: async (url, opts) => {
|
|
470
|
+
const denied = { status: 403, text: async () => '', headers: { get: () => null } };
|
|
471
|
+
let target;
|
|
472
|
+
try {
|
|
473
|
+
target = new URL(url);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
return denied;
|
|
477
|
+
}
|
|
478
|
+
if (/(^|\/)\.internal(\/|$)/u.test(target.pathname))
|
|
479
|
+
return denied;
|
|
480
|
+
// Which pod asked: routeFront reads `rec.podHome + rest` and passes
|
|
481
|
+
// that podHome here, so the confinement is a value on the call rather
|
|
482
|
+
// than state on the handler. No podHome, no read.
|
|
483
|
+
const home = opts?.podHome;
|
|
484
|
+
if (!home || !url.startsWith(home))
|
|
485
|
+
return denied;
|
|
453
486
|
const raw = await this.io.read(url);
|
|
454
487
|
return raw == null
|
|
455
488
|
? { status: 404, text: async () => '', headers: { get: () => null } }
|
package/dist/handler.jsonld
CHANGED
package/lib/admin.mjs
CHANGED
|
@@ -1379,15 +1379,15 @@ export function buildAdminSurface({ agent, gate, allowed, log = console.log,
|
|
|
1379
1379
|
await agent.store.flush();
|
|
1380
1380
|
const destCred = { ...cred, privateRoot: target };
|
|
1381
1381
|
const copied = await copyPrivateHalf({
|
|
1382
|
-
from: { state: agent.privateStorage(cred, 'state')
|
|
1383
|
-
to: { state: agent.privateStorage(destCred, 'state')
|
|
1382
|
+
from: { state: agent.privateStorage(cred, 'state') },
|
|
1383
|
+
to: { state: agent.privateStorage(destCred, 'state') },
|
|
1384
1384
|
log,
|
|
1385
1385
|
});
|
|
1386
1386
|
if (target) cred.privateRoot = target; else delete cred.privateRoot;
|
|
1387
1387
|
if (isCurrent(cred)) cred.layout = CURRENT_LAYOUT; else delete cred.layout;
|
|
1388
1388
|
writeJsonAtomic(path.join(agent.home, 'credential.json'), cred);
|
|
1389
1389
|
// connect() rebuilds everything that pointed at the old location —
|
|
1390
|
-
// store,
|
|
1390
|
+
// store, publisher, intake — and restarts them.
|
|
1391
1391
|
agent.store.attach(agent.privateStorage(cred, 'state'));
|
|
1392
1392
|
agent.stateLoaded = false;
|
|
1393
1393
|
await agent.connect();
|
package/lib/c2s.mjs
CHANGED
|
@@ -234,8 +234,9 @@ export class C2S {
|
|
|
234
234
|
|
|
235
235
|
switch (activity.type) {
|
|
236
236
|
case 'Create': {
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
const makes = object?.type || 'Note';
|
|
238
|
+
if (!object || (makes !== 'Note' && makes !== 'Question')) {
|
|
239
|
+
return this.send(res, 422, { error: 'only a Note or a Question (or a bare Note) can be created here' });
|
|
239
240
|
}
|
|
240
241
|
const visibility = this.visibilityOf(activity, object);
|
|
241
242
|
if (!visibility) {
|
|
@@ -246,6 +247,30 @@ export class C2S {
|
|
|
246
247
|
// visible characters rather than as markup. Documented v1 limit.
|
|
247
248
|
const text = String(object.source?.content ?? object.content ?? '');
|
|
248
249
|
if (!text.trim()) return this.send(res, 422, { error: 'the note has no content' });
|
|
250
|
+
|
|
251
|
+
// A Question is a poll: the choices are in oneOf (pick one) or anyOf
|
|
252
|
+
// (pick several), each naming itself, and endTime is when it shuts.
|
|
253
|
+
if (makes === 'Question') {
|
|
254
|
+
const one = arr(object.oneOf);
|
|
255
|
+
const many = arr(object.anyOf);
|
|
256
|
+
const titles = (one.length ? one : many).map((c) => String(c?.name ?? '').trim()).filter(Boolean);
|
|
257
|
+
try {
|
|
258
|
+
const question = await agent.publisher.publishQuestion(text, {
|
|
259
|
+
options: titles,
|
|
260
|
+
multiple: !one.length && many.length > 0,
|
|
261
|
+
expiresAt: object.endTime || null,
|
|
262
|
+
inReplyTo: idOf(object.inReplyTo) || undefined,
|
|
263
|
+
visibility,
|
|
264
|
+
spoilerText: object.summary || null,
|
|
265
|
+
});
|
|
266
|
+
return this.send(res, 201,
|
|
267
|
+
{ id: wire.createActivityId(question.id), object: question.id },
|
|
268
|
+
{ location: wire.createActivityId(question.id) });
|
|
269
|
+
} catch (e) {
|
|
270
|
+
return this.send(res, 422, { error: e.message });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
249
274
|
const attachments = arr(object.attachment).map((a) => ({
|
|
250
275
|
url: a?.url, mediaType: a?.mediaType,
|
|
251
276
|
...(a?.name ? { description: a.name } : {}),
|
package/lib/deliver.mjs
CHANGED
|
@@ -107,6 +107,12 @@ export class Deliverer {
|
|
|
107
107
|
current = new URL(res.headers.get('location'), current).href;
|
|
108
108
|
continue;
|
|
109
109
|
}
|
|
110
|
+
// Where this ended up, which is not always where it was asked to go.
|
|
111
|
+
// Callers compare the document's own id against the URL they REQUESTED,
|
|
112
|
+
// so without this an open redirector on a victim's origin let an
|
|
113
|
+
// attacker's server answer for a victim's URL and be believed.
|
|
114
|
+
try { Object.defineProperty(res, 'finalUrl', { value: current, configurable: true }); }
|
|
115
|
+
catch { /* a frozen Response: the caller falls back to the requested URL */ }
|
|
110
116
|
return res;
|
|
111
117
|
}
|
|
112
118
|
throw new Error(`too many redirects from ${url}`);
|
package/lib/embed.mjs
CHANGED
|
@@ -202,6 +202,7 @@ export async function startEmbeddedAgent({
|
|
|
202
202
|
agent.importer?.stop();
|
|
203
203
|
clearInterval(agent.schedTimer);
|
|
204
204
|
clearInterval(agent.refreshTimer);
|
|
205
|
+
agent.publisher?.stopPolls();
|
|
205
206
|
// Same order the standalone agent's shutdown uses: write what is pending,
|
|
206
207
|
// then let go of the lease so the next agent need not wait out the TTL.
|
|
207
208
|
await Promise.allSettled([
|
package/lib/front-core.mjs
CHANGED
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
import crypto from 'node:crypto';
|
|
19
19
|
import { handleDelivery } from './gateway-core.mjs';
|
|
20
|
-
import { readCapped } from './safefetch.mjs';
|
|
21
|
-
import
|
|
20
|
+
import { readCapped, safeFetch, isLoopbackHost } from './safefetch.mjs';
|
|
21
|
+
import * as podRoot from './pod/root.mjs';
|
|
22
|
+
import * as podPolicy from './pod/policy.mjs';
|
|
22
23
|
|
|
23
24
|
// The one WebFinger document, spelled out here rather than imported from
|
|
24
25
|
// wire.mjs: wire drags the agent's whole HTML pipeline (sanitize-html and
|
|
@@ -58,25 +59,122 @@ const OWNER_LOOKUP_MS = 5_000;
|
|
|
58
59
|
* authority on that, so when it answers, its answer decides. A server that
|
|
59
60
|
* says nothing leaves where the WebID lives as the only evidence there is.
|
|
60
61
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const res = await fetchImpl(podBase, {
|
|
64
|
-
method: 'HEAD', signal: AbortSignal.timeout(OWNER_LOOKUP_MS),
|
|
65
|
-
});
|
|
66
|
-
return linkTargets(res?.headers?.get?.('link'), REL.owner, podBase);
|
|
67
|
-
} catch { return []; }
|
|
68
|
-
}
|
|
62
|
+
const podOwners = (podBase, fetchImpl = fetch) =>
|
|
63
|
+
podRoot.readOwnerLinks(fetchImpl, podBase, { timeoutMs: OWNER_LOOKUP_MS });
|
|
69
64
|
|
|
65
|
+
// Whether a token's WebID vouches for this pod, on the evidence of where it
|
|
66
|
+
// lives. Weak on purpose, and never used alone any more — see provesPod.
|
|
70
67
|
function webidUnderPod(webid, podHome) {
|
|
71
68
|
try { return new URL(webid).origin === new URL(podHome).origin; } catch { return false; }
|
|
72
69
|
}
|
|
73
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Whether this WebID may speak for this pod.
|
|
73
|
+
*
|
|
74
|
+
* Sharing an origin was the whole test, which is right where an origin is one
|
|
75
|
+
* person (a subdomain pod) and wrong where it is not. On a path-based server
|
|
76
|
+
* every account is on the same origin, so any account holder could attach
|
|
77
|
+
* somebody else's pod under a name of their choosing — and, worse, re-attach a
|
|
78
|
+
* name that already existed and be handed its receipt secret (S7).
|
|
79
|
+
*
|
|
80
|
+
* The pod server is the authority on who owns the pod, and it will say so in a
|
|
81
|
+
* `solid:owner` link if asked. When it answers, its answer decides. Only when
|
|
82
|
+
* it says nothing does this fall back to where the WebID lives, which is the
|
|
83
|
+
* best a silent server leaves available. This is the rule /api/agent's opt-in
|
|
84
|
+
* has used all along; attach simply never asked.
|
|
85
|
+
*/
|
|
86
|
+
async function provesPod(webid, podHome, fetchImpl) {
|
|
87
|
+
const owners = await podOwners(podHome, fetchImpl || fetch);
|
|
88
|
+
if (owners.length) return owners.includes(webid);
|
|
89
|
+
return webidUnderPod(webid, podHome);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// A pod this front will read from at all.
|
|
93
|
+
//
|
|
94
|
+
// The front reads `rec.podHome + <the rest of the request path>` and, on the
|
|
95
|
+
// CSS server component, that read goes straight into the resource store with
|
|
96
|
+
// no access control of its own — so a row whose podHome names the FRONT is a
|
|
97
|
+
// row that can read the server's own internals, `/.internal/fedipod/directory/`
|
|
98
|
+
// included, where every user's receipt secret is kept (S2). The front is not a
|
|
99
|
+
// pod and never was one; saying so outright is the fix, and it closes the
|
|
100
|
+
// self-recursion of S8 with the same line.
|
|
101
|
+
function podHomeProblem(podHome, frontOrigin) {
|
|
102
|
+
let u;
|
|
103
|
+
try { u = new URL(podHome); } catch { return 'podHome is not a URL'; }
|
|
104
|
+
if (frontOrigin) {
|
|
105
|
+
try {
|
|
106
|
+
if (u.origin === new URL(frontOrigin).origin) {
|
|
107
|
+
return 'a pod cannot live on this gateway — name the pod that holds your data';
|
|
108
|
+
}
|
|
109
|
+
} catch { /* no usable frontOrigin: the other checks still apply */ }
|
|
110
|
+
}
|
|
111
|
+
// Nothing may reach a server's private trees, whatever origin they are on.
|
|
112
|
+
if (/(^|\/)\.internal(\/|$)/u.test(u.pathname) || /(^|\/)\.\.(\/|$)/u.test(u.pathname)) {
|
|
113
|
+
return 'that is not a pod address';
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const RELAY_MAX_REQUESTS = 20;
|
|
119
|
+
const RELAY_MAX_BODY = 1024 * 1024;
|
|
120
|
+
const RELAY_TIMEOUT_MS = 8_000;
|
|
121
|
+
// The only headers a relayed request may carry to the remote server. Host
|
|
122
|
+
// comes from the URL; the user agent from safeFetch.
|
|
123
|
+
const RELAY_HEADERS = new Set(['date', 'digest', 'signature', 'content-type', 'accept']);
|
|
124
|
+
|
|
125
|
+
const keyIdOf = (signature) => (/keyId="([^"]+)"/.exec(signature || '') || [])[1] || null;
|
|
126
|
+
|
|
127
|
+
async function relayOne(item, rec, fetchImpl) {
|
|
128
|
+
const url = String(item?.url || '');
|
|
129
|
+
const method = String(item?.method || 'POST').toUpperCase();
|
|
130
|
+
if (method !== 'GET' && method !== 'POST') return { url, status: 0, error: 'method must be GET or POST' };
|
|
131
|
+
let u;
|
|
132
|
+
try { u = new URL(url); } catch { return { url, status: 0, error: 'not a URL' }; }
|
|
133
|
+
if (u.protocol !== 'https:' && !(u.protocol === 'http:' && process.env.AP_ALLOW_PRIVATE_TARGETS === '1')) {
|
|
134
|
+
return { url, status: 0, error: 'https only' };
|
|
135
|
+
}
|
|
136
|
+
const headers = {};
|
|
137
|
+
for (const [k, v] of Object.entries(item?.headers || {})) {
|
|
138
|
+
const name = k.toLowerCase();
|
|
139
|
+
if (RELAY_HEADERS.has(name) && typeof v === 'string') headers[name] = v;
|
|
140
|
+
}
|
|
141
|
+
const body = method === 'POST' ? String(item?.body ?? '') : undefined;
|
|
142
|
+
if (body !== undefined && Buffer.byteLength(body) > RELAY_MAX_BODY) return { url, status: 0, error: 'body too large' };
|
|
143
|
+
const keyId = keyIdOf(headers.signature);
|
|
144
|
+
if (method === 'POST' && !keyId) return { url, status: 0, error: 'a delivery must be signed' };
|
|
145
|
+
if (keyId && !keyId.startsWith(rec.actorUrl + '#')) return { url, status: 0, error: "signed with a key that is not this account's" };
|
|
146
|
+
if (headers.digest) {
|
|
147
|
+
const want = 'SHA-256=' + crypto.createHash('sha256').update(body || '').digest('base64');
|
|
148
|
+
if (headers.digest !== want) return { url, status: 0, error: 'digest does not match the body' };
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const res = await safeFetch(url, { method, headers, body, signal: AbortSignal.timeout(RELAY_TIMEOUT_MS) }, fetchImpl);
|
|
152
|
+
const out = { url, status: res.status };
|
|
153
|
+
// The far server asking to be left alone has to reach the agent that will
|
|
154
|
+
// do the asking again. Without this the browser build could not honour a
|
|
155
|
+
// Retry-After at all — every delivery it makes goes through here — and fell
|
|
156
|
+
// back to its own ladder against a server that had already said how long.
|
|
157
|
+
const retryAfter = res.headers.get('retry-after');
|
|
158
|
+
if (retryAfter) out.retryAfter = retryAfter;
|
|
159
|
+
if (method === 'GET') {
|
|
160
|
+
out.contentType = res.headers.get('content-type') || null;
|
|
161
|
+
out.body = await readCapped(res, RELAY_MAX_BODY);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
} catch (e) { return { url, status: 0, error: e.message }; }
|
|
165
|
+
}
|
|
166
|
+
|
|
74
167
|
const j = (status, obj, ct = 'application/json') =>
|
|
75
168
|
({ status, headers: { 'content-type': ct, 'cache-control': 'no-store' }, body: JSON.stringify(obj) });
|
|
76
169
|
const notFound = () => ({ status: 404, headers: { 'content-type': 'text/plain' }, body: 'not found\n' });
|
|
77
170
|
|
|
78
171
|
// A user's public base on the front, a 1:1 mirror of their pod home.
|
|
79
|
-
|
|
172
|
+
// The directory key is the FULL fediverse address — handle@host — never the
|
|
173
|
+
// bare handle, which is not unique: two different pods can each have a "me".
|
|
174
|
+
// A fronted identity answers at this front's host; a mail-door identity at its
|
|
175
|
+
// own pod's host. The `/u/` path segment carries that same key.
|
|
176
|
+
export const addressKey = (handle, host) => `${handle}@${host}`;
|
|
177
|
+
export const userBase = (frontOrigin, key) => `${frontOrigin}/u/${encodeURIComponent(key)}/`;
|
|
80
178
|
|
|
81
179
|
// Rewrite every occurrence of the pod home to the front base (or back). The
|
|
82
180
|
// bodies are JSON, so a whole-string swap rewrites ids wherever they appear —
|
|
@@ -90,7 +188,7 @@ function parseUserPath(pathname) {
|
|
|
90
188
|
}
|
|
91
189
|
|
|
92
190
|
// Top-level paths a handle may not take, so a name never shadows a route.
|
|
93
|
-
const RESERVED = new Set(['u', 'api', 'signup', 'new-account', 'run', 'admin', 'gateway',
|
|
191
|
+
const RESERVED = new Set(['u', 'api', 'signup', 'new-account', 'run', 'admin', 'roster', 'gateway',
|
|
94
192
|
'gw', 'well-known', 'inbox', 'outbox', 'actor', 'install']);
|
|
95
193
|
|
|
96
194
|
// Why a handle is unusable, or null when it is fine. Lowercase letters, digits
|
|
@@ -114,19 +212,12 @@ function handleProblem(h) {
|
|
|
114
212
|
const POLICY_TTL_MS = 5 * 60_000;
|
|
115
213
|
const policyCache = new Map(); // podHome -> { at, policy }
|
|
116
214
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const res = await fetchImpl(rec.podHome + 'ap/gateway-policy.json',
|
|
124
|
-
{ headers: { accept: 'application/json' } });
|
|
125
|
-
if (res && res.status < 400) policy = JSON.parse(await readCapped(res, 256 * 1024));
|
|
126
|
-
} catch { /* unpublished or unreachable: the row's own fields stand */ }
|
|
127
|
-
policyCache.set(key, { at: Date.now(), policy });
|
|
128
|
-
return policy;
|
|
129
|
-
}
|
|
215
|
+
// The cache is OURS, not the library's: on a warm serverless container one Map
|
|
216
|
+
// is shared across every user this front serves, so what it is keyed by is a
|
|
217
|
+
// correctness property. Keyed by podHome, it is one person's policy under one
|
|
218
|
+
// person's key.
|
|
219
|
+
const policyFor = (rec, fetchImpl = fetch) =>
|
|
220
|
+
podPolicy.read(fetchImpl, rec.podHome, { cache: policyCache, ttlMs: POLICY_TTL_MS });
|
|
130
221
|
|
|
131
222
|
// The verified-delivery identity for a directory record: what gateway-core
|
|
132
223
|
// needs to check what concerns the user and to append to their pod inbox.
|
|
@@ -152,8 +243,61 @@ function identFor(rec, policy = null) {
|
|
|
152
243
|
// listDirectory() -> { handle: record } every row, for the admin roster
|
|
153
244
|
// removeDirectory(handle) -> boolean drop a row; false when a seeded row remains
|
|
154
245
|
// podPut(url, body, ct) -> boolean append to a user's pod (per-user cred inside)
|
|
155
|
-
// podGet(url) -> Response
|
|
246
|
+
// podGet(url, { podHome }) -> Response read a user's pod (public reads; plain fetch
|
|
247
|
+
// is fine). `podHome` is the row's own pod, for
|
|
248
|
+
// an adapter that reads a store directly and so
|
|
249
|
+
// must confine the read itself.
|
|
156
250
|
export async function routeFront(request, ctx) {
|
|
251
|
+
const out = await route(request, ctx);
|
|
252
|
+
return { ...out, headers: withSecurityHeaders(out.headers, out.body) };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Every response this file makes, hardened in one place rather than in each of
|
|
256
|
+
// the dozen shapes below.
|
|
257
|
+
//
|
|
258
|
+
// `nosniff` matters most: the front serves user-supplied JSON straight from
|
|
259
|
+
// somebody's pod (the proxied actor and object documents), and without it a
|
|
260
|
+
// browser is free to decide for itself that a document is HTML and run what is
|
|
261
|
+
// inside it. The rest is the same posture the app already has — nothing may be
|
|
262
|
+
// framed, no base tag may be rewritten, no plugin content.
|
|
263
|
+
//
|
|
264
|
+
// A content-security-policy goes on the HTML only: it would mean nothing on a
|
|
265
|
+
// JSON document, and `frame-ancestors` has to be a header rather than a meta
|
|
266
|
+
// tag anyway.
|
|
267
|
+
//
|
|
268
|
+
// `script-src 'self'` is the one that matters, and it is only possible because
|
|
269
|
+
// none of these pages carries inline script any more — each has its own file and
|
|
270
|
+
// its own route above. A policy cannot tell an inline block the author wrote
|
|
271
|
+
// from one an attacker injected, so as long as any inline script has to run,
|
|
272
|
+
// every inline script may.
|
|
273
|
+
//
|
|
274
|
+
// `connect-src` allows https: because the pages sign in against the user's own
|
|
275
|
+
// pod, which is a different origin by definition and not one we can name here.
|
|
276
|
+
function withSecurityHeaders(headers = {}, body = null) {
|
|
277
|
+
const ct = String(headers['content-type'] || '');
|
|
278
|
+
const isHtml = ct.startsWith('text/html');
|
|
279
|
+
return {
|
|
280
|
+
...headers,
|
|
281
|
+
'x-content-type-options': 'nosniff',
|
|
282
|
+
'referrer-policy': 'same-origin',
|
|
283
|
+
'x-frame-options': 'SAMEORIGIN',
|
|
284
|
+
...(isHtml && body ? {
|
|
285
|
+
'content-security-policy': [
|
|
286
|
+
"default-src 'self'",
|
|
287
|
+
"script-src 'self'", // no inline script: see above
|
|
288
|
+
"style-src 'self' 'unsafe-inline'",
|
|
289
|
+
"img-src 'self' https: data:",
|
|
290
|
+
"connect-src 'self' https:", // sign-in goes to the user's own pod
|
|
291
|
+
"object-src 'none'", // no plugin content, ever
|
|
292
|
+
"base-uri 'none'", // no rewriting where relative URLs resolve
|
|
293
|
+
"frame-ancestors 'self'", // nobody else may frame these pages
|
|
294
|
+
"form-action 'self'", // a form here submits here
|
|
295
|
+
].join('; '),
|
|
296
|
+
} : {}),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function route(request, ctx) {
|
|
157
301
|
const url = new URL(request.url);
|
|
158
302
|
const { pathname } = url;
|
|
159
303
|
|
|
@@ -176,9 +320,10 @@ export async function routeFront(request, ctx) {
|
|
|
176
320
|
return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: ctx.runPage };
|
|
177
321
|
}
|
|
178
322
|
|
|
179
|
-
// The
|
|
323
|
+
// The roster page: the host reading who has accounts here. The page signs in
|
|
180
324
|
// and calls the roster API below; a deploy with no admin supplies no page.
|
|
181
|
-
|
|
325
|
+
// Off /admin, which is the owner's own record page on every agent.
|
|
326
|
+
if (pathname === '/roster') {
|
|
182
327
|
if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
|
|
183
328
|
if (!ctx.adminPage) return notFound();
|
|
184
329
|
return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: ctx.adminPage };
|
|
@@ -197,9 +342,9 @@ export async function routeFront(request, ctx) {
|
|
|
197
342
|
.map((r) => ({
|
|
198
343
|
handle: r.handle, kind: r.kind || 'person', fronted: !r.inboxOnly,
|
|
199
344
|
podHome: r.podHome, webId: r.webId || null, actorUrl: r.actorUrl,
|
|
200
|
-
address: `@${r.handle}@${ctx.host}`,
|
|
345
|
+
address: r.address || `@${r.handle}@${ctx.host}`,
|
|
201
346
|
}))
|
|
202
|
-
.sort((a, b) => a.handle.localeCompare(b.handle));
|
|
347
|
+
.sort((a, b) => (a.address || a.handle).localeCompare(b.address || b.handle));
|
|
203
348
|
return j(200, { host: ctx.host, accounts });
|
|
204
349
|
}
|
|
205
350
|
|
|
@@ -213,11 +358,13 @@ export async function routeFront(request, ctx) {
|
|
|
213
358
|
if (webid !== ctx.adminWebId) return j(403, { error: 'that WebID is not the admin of this front' });
|
|
214
359
|
let body;
|
|
215
360
|
try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
return j(
|
|
220
|
-
|
|
361
|
+
// Revoke by full address (what the roster lists); a bare handle still works
|
|
362
|
+
// for any legacy row keyed that way.
|
|
363
|
+
const key = String(body.address || body.handle || '').replace(/^@/, '').toLowerCase();
|
|
364
|
+
if (!key || !(await ctx.lookup(key))) return j(404, { error: 'no such account' });
|
|
365
|
+
const removed = (await ctx.removeDirectory(key)) === true;
|
|
366
|
+
return j(200, removed ? { account: key, removed }
|
|
367
|
+
: { account: key, removed: false,
|
|
221
368
|
reason: 'this row is seeded in the deploy environment (FEDIPOD_DIRECTORY_JSON) — remove it there and redeploy' });
|
|
222
369
|
}
|
|
223
370
|
|
|
@@ -247,38 +394,94 @@ export async function routeFront(request, ctx) {
|
|
|
247
394
|
const podHome = String(body.podHome || '');
|
|
248
395
|
const problem = handleProblem(handle);
|
|
249
396
|
if (problem) return j(400, { error: problem });
|
|
250
|
-
|
|
251
|
-
if (
|
|
397
|
+
const podLoopback = (() => { try { return isLoopbackHost(new URL(podHome).hostname); } catch { return false; } })();
|
|
398
|
+
if (!(/^https:\/\/\S+\/$/.test(podHome) || (/^http:\/\/\S+\/$/.test(podHome) && podLoopback))) {
|
|
399
|
+
return j(400, { error: 'podHome must be an https URL ending in /' });
|
|
400
|
+
}
|
|
401
|
+
const badPod = podHomeProblem(podHome, ctx.frontOrigin);
|
|
402
|
+
if (badPod) return j(400, { error: badPod });
|
|
252
403
|
const webid = await verifyPodToken(request, pathname, ctx.verifier);
|
|
253
404
|
if (!webid) return j(401, { error: 'a Solid-OIDC token proving the pod is required' });
|
|
254
|
-
if (!
|
|
405
|
+
if (!await provesPod(webid, podHome, ctx.fetchImpl)) {
|
|
255
406
|
return j(403, { error: 'the token proves a different pod than the one you listed' });
|
|
256
407
|
}
|
|
257
408
|
// The usual attachment keeps the user's identity on their pod and moves
|
|
258
409
|
// only the mail door here (@me@mypod). Fronted identity — actor ids on
|
|
259
410
|
// this domain — remains available to an explicit `fronted: true`.
|
|
260
411
|
const fronted = body.fronted === true;
|
|
261
|
-
|
|
262
|
-
|
|
412
|
+
// The full address is the key: a fronted name answers at this front's host,
|
|
413
|
+
// a mail-door name at its pod's host. The same handle on two pods is two
|
|
414
|
+
// distinct addresses, so it never collides.
|
|
415
|
+
let addressHost; try { addressHost = fronted ? ctx.host : new URL(podHome).host; } catch { return j(400, { error: 'bad podHome' }); }
|
|
416
|
+
// A fronted name answers at this front's host, where the handle alone is the
|
|
417
|
+
// whole address and is unique — so it keeps keying by bare handle (and its
|
|
418
|
+
// actor id stays /u/<handle>/, unchanged for existing accounts). A mail-door
|
|
419
|
+
// name answers at its own pod, where "me" is not unique, so it keys by the
|
|
420
|
+
// full address handle@podhost, which the /u/ segment then carries.
|
|
421
|
+
const key = fronted ? handle : addressKey(handle, addressHost);
|
|
422
|
+
const address = `@${handle}@${addressHost}`;
|
|
423
|
+
// "Taken" only when this exact address already belongs to a DIFFERENT pod.
|
|
424
|
+
// Re-attaching your own pod — a retry, a re-provision, a second browser — is
|
|
425
|
+
// idempotent: it updates the row in place and keeps the same door secret.
|
|
426
|
+
const prior = await ctx.lookup(key);
|
|
427
|
+
if (prior && prior.podHome !== podHome) return j(409, { error: 'that name is taken' });
|
|
428
|
+
// Re-attaching is idempotent — a retry, a re-provision, a second browser —
|
|
429
|
+
// and it hands back the EXISTING receipt secret. So it has to be the same
|
|
430
|
+
// person: matching podHome was enough on a subdomain server and, on a
|
|
431
|
+
// path-based one, meant any co-tenant could re-attach your row, flip its
|
|
432
|
+
// kind, and be given your secret (S7). A row that predates `webId` has
|
|
433
|
+
// nothing to compare and falls back to the pod check above.
|
|
434
|
+
if (prior?.webId && prior.webId !== webid) {
|
|
435
|
+
return j(409, { error: 'that name belongs to a different account on this pod' });
|
|
436
|
+
}
|
|
437
|
+
// The pod's actor lives under its AP root container, which the pod owner
|
|
438
|
+
// knows and the front does not — so take the caller's actorUrl when it is on
|
|
439
|
+
// this pod, and fall back only when none is given.
|
|
440
|
+
const claimedActor = typeof body.actorUrl === 'string' && body.actorUrl.startsWith(podHome) ? body.actorUrl : null;
|
|
441
|
+
const actorUrl = fronted ? `${userBase(ctx.frontOrigin, key)}ap/actor` : (claimedActor || podHome + 'ap/actor');
|
|
442
|
+
const hmacSecret = prior?.hmacSecret || crypto.randomBytes(32).toString('base64');
|
|
263
443
|
const record = {
|
|
264
|
-
handle, podHome, webId: webid, actorUrl,
|
|
444
|
+
handle, address, podHome, webId: webid, actorUrl,
|
|
265
445
|
kind: body.kind === 'group' ? 'group' : 'person',
|
|
266
446
|
gatewayWebId: ctx.gatewayWebId || null, hmacSecret,
|
|
267
447
|
...(fronted ? {} : { inboxOnly: true }),
|
|
268
448
|
};
|
|
269
|
-
await ctx.putDirectory(
|
|
449
|
+
await ctx.putDirectory(key, record);
|
|
270
450
|
// The secret is returned ONCE, with the one command the user runs to
|
|
271
451
|
// point their agent at this gateway.
|
|
272
|
-
const doorInbox = `${ctx.frontOrigin}
|
|
452
|
+
const doorInbox = `${userBase(ctx.frontOrigin, key)}ap/inbox/`;
|
|
273
453
|
if (fronted) {
|
|
274
|
-
return j(201, { ok: true, handle, actorUrl, frontActor: actorUrl, hmacSecret,
|
|
275
|
-
address: `@${handle}@${ctx.host}`,
|
|
454
|
+
return j(201, { ok: true, handle, address, actorUrl, frontActor: actorUrl, hmacSecret,
|
|
276
455
|
command: `fedipod gateway ${actorUrl} --secret ${hmacSecret}` });
|
|
277
456
|
}
|
|
278
|
-
return j(201, { ok: true, handle, doorInbox, hmacSecret,
|
|
457
|
+
return j(201, { ok: true, handle, address, doorInbox, hmacSecret,
|
|
279
458
|
command: `fedipod gateway ${doorInbox} --secret ${hmacSecret} --inbox-only` });
|
|
280
459
|
}
|
|
281
460
|
|
|
461
|
+
// The relay: the front sends requests a browser has already signed. A page
|
|
462
|
+
// may not set the Date or Host header, and both are inside an HTTP
|
|
463
|
+
// signature, so a browser-run agent signs and hands the request here; the
|
|
464
|
+
// front sends it with exactly the headers that were signed. It can forward
|
|
465
|
+
// a signature and cannot make one. Only the account's own key may sign,
|
|
466
|
+
// only the account's own pod may call, and nothing unsigned-for reaches
|
|
467
|
+
// the remote server.
|
|
468
|
+
if (pathname === '/api/relay' && request.method === 'POST') {
|
|
469
|
+
let body;
|
|
470
|
+
try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
|
|
471
|
+
const handle = String(body.handle || '').toLowerCase();
|
|
472
|
+
const rec = await ctx.lookup(handle);
|
|
473
|
+
if (!rec) return j(404, { error: 'no such account' });
|
|
474
|
+
const webid = await verifyPodToken(request, pathname, ctx.verifier);
|
|
475
|
+
if (!webid) return j(401, { error: 'a Solid-OIDC token proving the pod is required' });
|
|
476
|
+
const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
|
|
477
|
+
if (!owner) return j(403, { error: "the token proves a different pod than this account's" });
|
|
478
|
+
const items = Array.isArray(body.requests) ? body.requests : [];
|
|
479
|
+
if (!items.length) return j(400, { error: 'requests must be a non-empty list' });
|
|
480
|
+
if (items.length > RELAY_MAX_REQUESTS) return j(400, { error: `at most ${RELAY_MAX_REQUESTS} requests per call` });
|
|
481
|
+
const results = await Promise.all(items.map((it) => relayOne(it, rec, ctx.fetchImpl || fetch)));
|
|
482
|
+
return j(200, { results });
|
|
483
|
+
}
|
|
484
|
+
|
|
282
485
|
// Runtime opt-in: a pod owner asks the server this front sits on to RUN
|
|
283
486
|
// their identity. Same proof as attach — a Solid-OIDC token — but the
|
|
284
487
|
// ownership check is stricter: the WebID must live UNDER the pod base, not
|
|
@@ -327,6 +530,20 @@ export async function routeFront(request, ctx) {
|
|
|
327
530
|
return j(400, { error: 'action must be opt-in or opt-out' });
|
|
328
531
|
}
|
|
329
532
|
|
|
533
|
+
// Each page's own script. They were inline until 2026-09-09; out here the
|
|
534
|
+
// pages can be served under `script-src 'self'`, which is the directive that
|
|
535
|
+
// tells running code from injected markup — and inline script defeats it by
|
|
536
|
+
// construction. Same mechanism the vendored library below has always used.
|
|
537
|
+
//
|
|
538
|
+
// The name is matched against a literal list, so nothing about the request
|
|
539
|
+
// chooses a file.
|
|
540
|
+
const mPageScript = /^\/(new-account|run|admin)\.js$/u.exec(pathname);
|
|
541
|
+
if (mPageScript) {
|
|
542
|
+
const body = ctx.pageScripts?.[`${mPageScript[1]}.js`];
|
|
543
|
+
if (!body) return notFound();
|
|
544
|
+
return { status: 200, headers: { 'content-type': 'text/javascript; charset=utf-8' }, body };
|
|
545
|
+
}
|
|
546
|
+
|
|
330
547
|
// The vendored Solid-OIDC browser library the /run and /admin pages load —
|
|
331
548
|
// served here because this function owns every path on the domain.
|
|
332
549
|
if (pathname === '/solid-oidc-client.js') {
|
|
@@ -373,9 +590,15 @@ export async function routeFront(request, ctx) {
|
|
|
373
590
|
// fixed to the front so a consumer cross-checks it consistently.
|
|
374
591
|
if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
|
|
375
592
|
const podTarget = rec.podHome + up.rest;
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
593
|
+
// The pod this read belongs to travels with it: an adapter reading a store
|
|
594
|
+
// directly (the CSS server component) has no access control of its own and
|
|
595
|
+
// needs to be told what it may reach. See podHomeProblem above for the other
|
|
596
|
+
// half — a row that names something which is not a pod.
|
|
597
|
+
const got = await podRoot.readPublicDocument(
|
|
598
|
+
ctx.podGet || ((u) => fetch(u, { headers: { accept: AP_CT } })),
|
|
599
|
+
podTarget, { podHome: rec.podHome });
|
|
600
|
+
if (got.text === null) return { status: got.status, headers: {}, body: '' };
|
|
601
|
+
let text = got.text;
|
|
379
602
|
text = swap(text, rec.podHome, base);
|
|
380
603
|
if (up.rest === 'ap/actor') {
|
|
381
604
|
try {
|
package/lib/gateway-core.mjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import crypto from 'node:crypto';
|
|
16
16
|
import { verifyHttpSignature, makeSafeLoader, makeReceipt, signReceipt } from './httpsig.mjs';
|
|
17
|
+
import * as inbox from './pod/inbox.mjs';
|
|
17
18
|
|
|
18
19
|
const DEFAULT_MAX_BYTES = 512 * 1024; // mirror intake.mjs MAX_ITEM_BYTES
|
|
19
20
|
|
|
@@ -100,17 +101,12 @@ export async function handleDelivery(request, ident, { podPut, fetchImpl = fetch
|
|
|
100
101
|
? signReceipt(makeReceipt(v, { gateway: ident.gatewayWebId }), ident.hmacSecret)
|
|
101
102
|
: null;
|
|
102
103
|
const hash = sha256hex(raw);
|
|
103
|
-
const okA = await podPut
|
|
104
|
+
const okA = await inbox.appendVerifiedDelivery(podPut, ident.inboxUrl, hash, raw);
|
|
104
105
|
// A pod-write failure returns 5xx so the ORIGIN retries over its own ladder —
|
|
105
106
|
// that is what preserves the pod's buffer property without the gateway
|
|
106
107
|
// holding any state.
|
|
107
108
|
if (!okA) return { status: 502, reason: 'pod inbox write failed' };
|
|
108
|
-
|
|
109
|
-
// receipt did not is simply unverified, never lost.
|
|
110
|
-
if (receipt) {
|
|
111
|
-
await podPut(ident.inboxUrl + hash + '.receipt.json', JSON.stringify(receipt), 'application/json')
|
|
112
|
-
.catch(() => {});
|
|
113
|
-
}
|
|
109
|
+
if (receipt) await inbox.writeReceiptBeside(podPut, ident.inboxUrl, hash, receipt);
|
|
114
110
|
return { status: 202, reason: v.verified ? 'verified' : 'buffered-unverified' };
|
|
115
111
|
}
|
|
116
112
|
|