fedipod-server 0.21.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/claims.d.ts +3 -1
- package/dist/claims.js +20 -7
- package/dist/components/context.jsonld +6 -0
- package/dist/handler.d.ts +29 -1
- package/dist/handler.js +89 -4
- package/dist/handler.jsonld +36 -1
- package/lib/client/c2s.mjs +100 -3
- package/lib/client/masto/accounts.mjs +44 -4
- package/lib/client/masto/body.mjs +90 -3
- package/lib/client/masto/index.mjs +5 -1
- package/lib/client/masto/instance.mjs +11 -3
- package/lib/client/masto/media.mjs +3 -57
- package/lib/client/masto/oauth.mjs +66 -9
- package/lib/client/masto/render.mjs +62 -5
- package/lib/client/masto/statuses.mjs +25 -1
- package/lib/client/masto/timelines.mjs +128 -6
- package/lib/connections/tagfeed.mjs +42 -5
- package/lib/core/as2.mjs +36 -5
- package/lib/core/contexts/index.mjs +14 -0
- package/lib/core/contexts/litepub-0.1.json +60 -0
- package/lib/core/contexts/map.json +3 -1
- package/lib/core/contexts/quotes.json +18 -0
- package/lib/core/graphview.mjs +41 -3
- package/lib/core/intake/activities.mjs +136 -2
- package/lib/core/intake/activity.mjs +79 -1
- package/lib/core/intake/group.mjs +37 -3
- package/lib/core/intake/index.mjs +32 -2
- package/lib/core/intake/notes.mjs +60 -3
- package/lib/core/lease.mjs +12 -2
- package/lib/core/profile-page.mjs +104 -0
- package/lib/core/publisher/collections.mjs +6 -1
- package/lib/core/publisher/index.mjs +35 -17
- package/lib/core/publisher/notes.mjs +104 -3
- package/lib/core/shapes/activitystreams.ttl +28 -0
- package/lib/core/shapes/index.mjs +26 -1
- package/lib/core/wire-quotes.mjs +71 -0
- package/lib/core/wire.mjs +59 -103
- package/lib/device/admin/routes/lifecycle.mjs +9 -0
- package/lib/device/admin/static.mjs +4 -1
- package/lib/device/admin/surface.mjs +26 -2
- package/lib/device/certs.mjs +18 -2
- package/lib/device/cli/commands/setup.mjs +6 -2
- package/lib/device/cli/commands/state.mjs +3 -1
- package/lib/device/gateway-move.mjs +58 -0
- package/lib/device/migrate.mjs +3 -1
- package/lib/device/setup.mjs +67 -7
- package/lib/gateway/front-core.mjs +307 -36
- package/lib/gateway/gateway-core.mjs +15 -6
- package/lib/gateway/notices.mjs +84 -0
- package/lib/gateway/quiet.mjs +184 -0
- package/lib/pod/root.mjs +6 -3
- package/lib/pod/state.mjs +8 -7
- package/lib/pod/transport.mjs +5 -1
- package/lib/session/README.md +123 -0
- package/lib/session/demo.html +94 -0
- package/lib/session/fedi-account.mjs +346 -0
- package/lib/session/fedi-login.mjs +123 -0
- package/lib/session/oidc-session.mjs +233 -0
- package/lib/session/package.json +41 -0
- package/package.json +1 -1
- package/run-agent.mjs +28 -0
- package/vendor/gate.cjs +6 -3
- package/web/admin/bar.css +66 -0
- package/web/admin/bar.js +38 -0
- package/web/admin/client/index.html +31 -3
- package/web/admin/client-pick.js +79 -0
- package/web/admin/gateway.js +22 -0
- package/web/admin/index.html +54 -13
- package/web/admin/notices-bar.js +90 -0
- package/web/admin/oauth-signin.mjs +37 -0
- package/web/admin/record.js +13 -1
- package/web/admin/setup/index.html +31 -3
- package/web/admin/tokens.css +15 -3
- package/web/admin/upkeep.js +4 -5
- package/web/app/README.md +14 -10
- package/web/app/admin-facade.mjs +51 -12
- package/web/app/agent.mjs +157 -19
- package/web/app/authorize_interaction.html +56 -0
- package/web/app/authorize_interaction.js +69 -0
- package/web/app/boot.mjs +233 -95
- package/web/app/dist/boot.js +477 -569
- package/web/app/dist/boot.js.map +4 -4
- package/web/app/dist/sw.js +1861 -532
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/gateway-move.mjs +60 -0
- package/web/app/index.html +40 -49
- package/web/app/keys-browser.mjs +12 -15
- package/web/app/keystore.mjs +19 -36
- package/web/app/oidc-session.mjs +5 -188
- package/web/app/signup.mjs +172 -132
- package/web/app/sw-src.mjs +1 -1
- package/web/app/update.js +66 -0
- package/web/front/admin.html +5 -3
- package/web/front/new-account.html +2 -2
- package/web/front/notices.html +69 -0
- package/web/front/notices.js +107 -0
- package/web/front/run.html +15 -8
- package/web/front/run.js +124 -36
- package/web/icons/forum.svg +11 -0
- package/web/icons/gateway.svg +14 -0
- package/web/icons/local.svg +11 -0
- package/web/app/index.html~ +0 -152
- package/web/app/site/_headers +0 -9
- package/web/app/site/_redirects +0 -22
- package/web/app/site/admin/actors.js +0 -145
- package/web/app/site/admin/bar.css +0 -60
- package/web/app/site/admin/bar.js +0 -61
- package/web/app/site/admin/client/client.js +0 -66
- package/web/app/site/admin/client/index.html +0 -43
- package/web/app/site/admin/common.js +0 -23
- package/web/app/site/admin/connections.js +0 -112
- package/web/app/site/admin/gateway.js +0 -111
- package/web/app/site/admin/group.js +0 -258
- package/web/app/site/admin/index.html +0 -564
- package/web/app/site/admin/record.js +0 -378
- package/web/app/site/admin/setup/index.html +0 -215
- package/web/app/site/admin/setup/setup.js +0 -326
- package/web/app/site/admin/tokens.css +0 -56
- package/web/app/site/admin/upkeep.js +0 -178
- package/web/app/site/admin/window.css +0 -55
- package/web/app/site/admin/window.js +0 -136
- package/web/app/site/app/404.html +0 -40
- package/web/app/site/app/_headers +0 -2
- package/web/app/site/app/apple-touch-icon.png +0 -0
- package/web/app/site/app/assets/ICONS-DIPJeU0C.js +0 -2
- package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +0 -1
- package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +0 -183
- package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
- package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
- package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +0 -2
- package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +0 -1
- 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 +0 -1
- package/web/app/site/app/assets/compose-CFEx7wb4.js +0 -2
- package/web/app/site/app/assets/compose-CFEx7wb4.js.map +0 -1
- package/web/app/site/app/assets/compose-ZJN5xSb4.js +0 -35
- package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +0 -1
- package/web/app/site/app/assets/debug.module-YFQgwnIr.js +0 -52
- package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +0 -1
- package/web/app/site/app/assets/dom-p1YcJ5Rw.js +0 -2
- package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +0 -1
- 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 +0 -1
- package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +0 -1
- package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
- package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +0 -1
- package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
- package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
- package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
- package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
- package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
- package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
- package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
- package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
- package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +0 -1
- package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +0 -1
- package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
- package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +0 -1
- package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
- package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +0 -1
- package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
- package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
- package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +0 -1
- package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
- package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +0 -1
- package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
- package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
- package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
- package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
- package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
- package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +0 -1
- package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +0 -1
- package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +0 -1
- package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +0 -1
- package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +0 -1
- package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
- package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +0 -1
- package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
- package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +0 -1
- package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +0 -1
- package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +0 -1
- package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
- package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
- package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +0 -1
- package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +0 -1
- package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
- package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
- package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +0 -1
- package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +0 -1
- package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +0 -1
- package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
- package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +0 -1
- package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +0 -1
- package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +0 -1
- package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
- package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
- package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
- package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +0 -1
- package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +0 -1
- package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +0 -1
- package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +0 -1
- package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +0 -1
- package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
- package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
- package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +0 -1
- package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +0 -1
- package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +0 -1
- package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +0 -1
- package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +0 -1
- package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +0 -1
- package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +0 -1
- package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +0 -1
- package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
- package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +0 -1
- package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +0 -1
- package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +0 -1
- package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
- package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +0 -1
- package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +0 -1
- package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
- package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
- package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
- package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +0 -1
- package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +0 -1
- package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
- package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +0 -1
- package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +0 -1
- package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +0 -1
- package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +0 -1
- package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +0 -1
- package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +0 -1
- package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +0 -1
- package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +0 -1
- package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +0 -1
- package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
- package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +0 -1
- package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
- package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
- package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
- package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +0 -1
- package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
- package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +0 -1
- package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +0 -1
- package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
- package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +0 -1
- package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
- package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +0 -1
- package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
- package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +0 -1
- package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +0 -1
- package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
- package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +0 -1
- package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +0 -1
- package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
- package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +0 -1
- package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +0 -1
- package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +0 -1
- package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
- package/web/app/site/app/assets/instances-BNPptnn-.json +0 -333
- package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +0 -1
- package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
- package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +0 -1
- package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +0 -1
- package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +0 -1
- package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +0 -1
- package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +0 -1
- package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +0 -1
- package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +0 -1
- package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +0 -1
- package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +0 -1
- package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +0 -1
- package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +0 -1
- package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +0 -1
- package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
- package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
- package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +0 -1
- package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
- package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +0 -1
- package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +0 -1
- package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
- package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/tok-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +0 -1
- package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +0 -1
- package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/main-BdqNbG-a.js +0 -26
- package/web/app/site/app/assets/main-BdqNbG-a.js.map +0 -1
- package/web/app/site/app/assets/mock-home-DGhR__He.js +0 -2
- package/web/app/site/app/assets/mock-home-DGhR__He.js.map +0 -1
- 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 +0 -2
- package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +0 -1
- package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +0 -20
- package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +0 -1
- package/web/app/site/app/assets/style-B1iLPTPR.css +0 -2
- package/web/app/site/app/assets/temml-DuyIQMei.js +0 -97
- package/web/app/site/app/assets/temml-DuyIQMei.js.map +0 -1
- package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +0 -2
- package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
- 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 +0 -3
- package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +0 -1
- package/web/app/site/app/compose/index.html +0 -20
- package/web/app/site/app/favicon.ico +0 -0
- package/web/app/site/app/index.html +0 -73
- 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 +0 -1
- 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 +0 -2
- package/web/app/site/app/version.json +0 -1
- package/web/app/site/boot.js +0 -34277
- package/web/app/site/index.html +0 -251
- package/web/app/site/sw.js +0 -71331
|
@@ -21,19 +21,29 @@ import { readCapped, safeFetch, isLoopbackHost } from '../shared/safefetch.mjs';
|
|
|
21
21
|
import * as podRoot from '../pod/root.mjs';
|
|
22
22
|
import * as podPolicy from '../pod/policy.mjs';
|
|
23
23
|
import { podBaseOfWebId } from '../pod/urls.mjs';
|
|
24
|
+
import { routeQuietApi, noteOpened, noteReceived, closedState, accountState, closedAnswer } from './quiet.mjs';
|
|
25
|
+
import { routeNoticesApi } from './notices.mjs';
|
|
24
26
|
|
|
25
27
|
// The one WebFinger document, spelled out here rather than imported from
|
|
26
28
|
// wire.mjs: wire drags the agent's whole HTML pipeline (sanitize-html and
|
|
27
29
|
// friends), which a serverless front must never carry — it crashed the
|
|
28
30
|
// deployed function before it answered its first request.
|
|
29
|
-
const jrd = ({ handle, host, actor, aliases = [], page = null }) => ({
|
|
31
|
+
const jrd = ({ handle, host, actor, aliases = [], page = null, subscribe = null }) => ({
|
|
30
32
|
subject: `acct:${handle}@${host}`,
|
|
31
33
|
...(aliases.length ? { aliases } : {}),
|
|
32
34
|
links: [
|
|
33
35
|
{ rel: 'self', type: 'application/activity+json', href: actor },
|
|
34
36
|
...(page ? [{ rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: page }] : []),
|
|
37
|
+
// Remote follow. Somebody reading this account on another server presses
|
|
38
|
+
// Follow, says they are here, and that server looks for this link to find
|
|
39
|
+
// out where to send them. `{uri}` is a literal the other server fills in
|
|
40
|
+
// with whatever they are following — it must not be escaped.
|
|
41
|
+
...(subscribe ? [{ rel: 'http://ostatus.org/schema/1.0/subscribe', template: subscribe }] : []),
|
|
35
42
|
],
|
|
36
43
|
});
|
|
44
|
+
// The door that link names, which the site staged beside this function serves.
|
|
45
|
+
const subscribeTemplate = (origin) => (origin
|
|
46
|
+
? `${String(origin).replace(/\/$/u, '')}/authorize_interaction?uri={uri}` : null);
|
|
37
47
|
// The public profile page of any account the front answers for lives on
|
|
38
48
|
// its pod, whether the address is the front's or the pod's own.
|
|
39
49
|
const profilePageOf = (rec) => (rec?.podHome ? rec.podHome + 'ap/profile.html' : null);
|
|
@@ -136,10 +146,25 @@ const RELAY_MAX_BODY = 1024 * 1024;
|
|
|
136
146
|
const RELAY_TIMEOUT_MS = 8_000;
|
|
137
147
|
// The only headers a relayed request may carry to the remote server. Host
|
|
138
148
|
// comes from the URL; the user agent from safeFetch.
|
|
149
|
+
const SERVER_PROBE_MS = 6000;
|
|
150
|
+
const SERVER_PROBE_BYTES = 64 * 1024;
|
|
139
151
|
const RELAY_HEADERS = new Set(['date', 'digest', 'signature', 'content-type', 'accept']);
|
|
140
152
|
|
|
141
153
|
const keyIdOf = (signature) => (/keyId="([^"]+)"/.exec(signature || '') || [])[1] || null;
|
|
142
154
|
|
|
155
|
+
// The owner's APIs a browser at ANOTHER origin may call: an address moving
|
|
156
|
+
// to a new gateway is driven from the new gateway's page, and it has to tell
|
|
157
|
+
// this one it moved and send the Move through this one's relay. Both take a
|
|
158
|
+
// pod token as proof, the way the outbox door does, so any origin may ask.
|
|
159
|
+
const API_CORS = {
|
|
160
|
+
'access-control-allow-origin': '*',
|
|
161
|
+
'access-control-allow-methods': 'POST, OPTIONS',
|
|
162
|
+
'access-control-allow-headers': 'Authorization, DPoP, Content-Type',
|
|
163
|
+
'access-control-max-age': '86400',
|
|
164
|
+
};
|
|
165
|
+
const withApiCors = (out) => ({ ...out, headers: { ...(out.headers || {}), ...API_CORS } });
|
|
166
|
+
const apiPreflight = () => ({ status: 204, headers: { ...API_CORS, allow: 'POST, OPTIONS' }, body: null });
|
|
167
|
+
|
|
143
168
|
async function relayOne(item, rec, fetchImpl) {
|
|
144
169
|
const url = String(item?.url || '');
|
|
145
170
|
const method = String(item?.method || 'POST').toUpperCase();
|
|
@@ -203,9 +228,57 @@ function parseUserPath(pathname) {
|
|
|
203
228
|
return m ? { handle: decodeURIComponent(m[1]), rest: m[2] } : null;
|
|
204
229
|
}
|
|
205
230
|
|
|
231
|
+
// A public document is the same for everybody who asks, so the edge may hold
|
|
232
|
+
// it and answer the next reader itself. Without this every read of an actor,
|
|
233
|
+
// a topic, a post or a card was a function call: one visit to a forum is
|
|
234
|
+
// twenty of them, and every visitor and every refresh paid again.
|
|
235
|
+
//
|
|
236
|
+
// `netlify-cdn-cache-control` is the edge's own instruction; `cache-control`
|
|
237
|
+
// is what the reader's browser is told. The edge may serve a stale copy while
|
|
238
|
+
// it fetches a fresh one, so nobody waits on the pod for a document that is
|
|
239
|
+
// merely minutes old. The edge holds a document far longer than a browser
|
|
240
|
+
// is asked to: the edge is what turns a thousand readers into one function
|
|
241
|
+
// call, while a browser that held an actor for ten minutes would show one
|
|
242
|
+
// person a stale profile they had just edited.
|
|
243
|
+
const BROWSER_MAX_AGE = 60;
|
|
244
|
+
const publicFor = (seconds, stale = seconds * 4) => ({
|
|
245
|
+
'cache-control': `public, max-age=${Math.min(seconds, BROWSER_MAX_AGE)}`,
|
|
246
|
+
'netlify-cdn-cache-control': `public, s-maxage=${seconds}, stale-while-revalidate=${stale}`,
|
|
247
|
+
});
|
|
248
|
+
// How long the edge holds a public document, and a handle's WebFinger
|
|
249
|
+
// answer. Every server that has heard of an account asks for its actor and
|
|
250
|
+
// its handle again and again; a post or a collection changes when its owner
|
|
251
|
+
// acts, and ten minutes of edge is what the plan's budget of function calls
|
|
252
|
+
// can afford.
|
|
253
|
+
const PUBLIC_EDGE_SECONDS = 600;
|
|
254
|
+
const WEBFINGER_EDGE_SECONDS = 3600;
|
|
255
|
+
|
|
256
|
+
// Where a pod owner opts their identity in. An operator may name it, because
|
|
257
|
+
// the path it takes is one their pod server can no longer serve; the dot says
|
|
258
|
+
// it belongs to the server rather than to anybody's data, the way
|
|
259
|
+
// `.well-known` and `.account` do.
|
|
260
|
+
export const DEFAULT_RUN_PATH = '/.fediverse-account';
|
|
261
|
+
export const runPath = (ctx) => {
|
|
262
|
+
const said = String(ctx?.runPath || '').trim().replace(/\/+$/u, '');
|
|
263
|
+
if (!said) return DEFAULT_RUN_PATH;
|
|
264
|
+
return said.startsWith('/') ? said : `/${said}`;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// A request the browser itself says came from this very origin. `Sec-Fetch-Site`
|
|
268
|
+
// is the browser's word and a page cannot forge it; an `Origin` that matches is
|
|
269
|
+
// the older way of saying the same. This is what makes it safe to read a
|
|
270
|
+
// session cookie as proof: a page on another site gets neither.
|
|
271
|
+
const sameOriginRequest = (request, origin) => {
|
|
272
|
+
const site = request.headers.get('sec-fetch-site');
|
|
273
|
+
if (site) return site === 'same-origin';
|
|
274
|
+
const said = request.headers.get('origin');
|
|
275
|
+
if (!said || !origin) return false;
|
|
276
|
+
try { return new URL(said).origin === new URL(origin).origin; } catch { return false; }
|
|
277
|
+
};
|
|
278
|
+
|
|
206
279
|
// Top-level paths a handle may not take, so a name never shadows a route.
|
|
207
280
|
const RESERVED = new Set(['u', 'api', 'signup', 'new-account', 'run', 'admin', 'roster', 'gateway',
|
|
208
|
-
'gw', 'well-known', 'inbox', 'outbox', 'actor', 'install']);
|
|
281
|
+
'gw', 'well-known', 'inbox', 'outbox', 'actor', 'install', 'notices']);
|
|
209
282
|
|
|
210
283
|
// Why a handle is unusable, or null when it is fine. Lowercase letters, digits
|
|
211
284
|
// and hyphens; 2–30 chars; not edge-hyphenated; not a reserved route.
|
|
@@ -240,7 +313,9 @@ const policyFor = (rec, fetchImpl = fetch) =>
|
|
|
240
313
|
// `policy` is the person's published filtering policy when there is one.
|
|
241
314
|
function identFor(rec, policy = null) {
|
|
242
315
|
return {
|
|
243
|
-
|
|
316
|
+
// A row may name the inbox its deliveries are written to: a forum's
|
|
317
|
+
// categories share one, on the forum's pod, above their own trees.
|
|
318
|
+
inboxUrl: rec.inboxUrl || rec.podHome + 'ap/inbox/',
|
|
244
319
|
actorUrl: rec.actorUrl, // the FRONT actor id (what mail is addressed to)
|
|
245
320
|
followersUrl: rec.actorUrl.replace(/actor$/, 'followers'),
|
|
246
321
|
notesPrefix: rec.actorUrl.replace(/ap\/actor$/, 'ap/notes/'),
|
|
@@ -259,6 +334,7 @@ function identFor(rec, policy = null) {
|
|
|
259
334
|
// listDirectory() -> { handle: record } every row, for the admin roster
|
|
260
335
|
// removeDirectory(handle) -> boolean drop a row; false when a seeded row remains
|
|
261
336
|
// podPut(url, body, ct) -> boolean append to a user's pod (per-user cred inside)
|
|
337
|
+
// readReceived / writeReceived / dropReceived, pauseItems, closeDays — see quiet.mjs
|
|
262
338
|
// podGet(url, { podHome }) -> Response read a user's pod (public reads; plain fetch
|
|
263
339
|
// is fine). `podHome` is the row's own pod, for
|
|
264
340
|
// an adapter that reads a store directly and so
|
|
@@ -330,20 +406,19 @@ async function route(request, ctx) {
|
|
|
330
406
|
// The run page: a pod owner opting their identity in or out of being run by
|
|
331
407
|
// this server. Meaningful only on a host that embeds the agent; a host that
|
|
332
408
|
// does not simply supplies no page.
|
|
333
|
-
if (pathname ===
|
|
409
|
+
if (pathname === runPath(ctx)) {
|
|
334
410
|
if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
|
|
335
411
|
if (!ctx.runPage) return notFound();
|
|
336
|
-
return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: ctx.runPage };
|
|
412
|
+
return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' }, body: ctx.runPage };
|
|
337
413
|
}
|
|
338
414
|
|
|
339
|
-
// The roster page
|
|
340
|
-
//
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
415
|
+
// The roster page (/roster, off /admin, which is the owner's own record
|
|
416
|
+
// page on every agent) is served with the notices page, in notices.mjs.
|
|
417
|
+
|
|
418
|
+
// Notices from the operator to every account here, and the page they are
|
|
419
|
+
// written on (notices.mjs).
|
|
420
|
+
const notices = await routeNoticesApi(request, pathname, ctx, { j, verifyPodToken, publicFor, notFound });
|
|
421
|
+
if (notices) return notices;
|
|
347
422
|
|
|
348
423
|
// The roster: every directory row, secrets stripped, for the host's own
|
|
349
424
|
// eyes. The reader proves themself the way attach proves a pod — a
|
|
@@ -359,6 +434,7 @@ async function route(request, ctx) {
|
|
|
359
434
|
handle: r.handle, kind: r.kind || 'person', fronted: !r.inboxOnly,
|
|
360
435
|
podHome: r.podHome, webId: r.webId || null, actorUrl: r.actorUrl,
|
|
361
436
|
address: r.address || `@${r.handle}@${ctx.host}`,
|
|
437
|
+
openedAt: r.openedAt || null, pausedAt: r.pausedAt || null, closedAt: r.closedAt || null,
|
|
362
438
|
}))
|
|
363
439
|
.sort((a, b) => (a.address || a.handle).localeCompare(b.address || b.handle));
|
|
364
440
|
return j(200, { host: ctx.host, accounts });
|
|
@@ -398,6 +474,48 @@ async function route(request, ctx) {
|
|
|
398
474
|
offersPods: !!ctx.offersPods, version: ctx.version || null });
|
|
399
475
|
}
|
|
400
476
|
|
|
477
|
+
// What a reader's own server speaks, asked here rather than in their
|
|
478
|
+
// browser: a page cannot probe a stranger's server (no CORS), and the
|
|
479
|
+
// answer decides how that reader takes part in a forum.
|
|
480
|
+
if (pathname === '/api/server') {
|
|
481
|
+
const host = (url.searchParams.get('host') || '').trim().toLowerCase();
|
|
482
|
+
const open = { 'access-control-allow-origin': '*' };
|
|
483
|
+
const said = (kind, extra = {}) => ({ ...j(200, { host, kind, ...extra }), headers: { ...j(200, {}).headers, ...open } });
|
|
484
|
+
// An address at this front is a FediPod account: its client is the
|
|
485
|
+
// browser here, and no other server holds it. Asked before the shape of
|
|
486
|
+
// the host, since a front may answer at a name with no dot in it.
|
|
487
|
+
if (host && host === String(ctx.host || '').toLowerCase()) {
|
|
488
|
+
const handle = (url.searchParams.get('handle') || '').toLowerCase();
|
|
489
|
+
const rec = handle ? await ctx.lookup(handle) : null;
|
|
490
|
+
if (!rec) return said('fedipod');
|
|
491
|
+
// Where the owner of this pod signs in: the pod says so itself, at the
|
|
492
|
+
// well-known document every Solid server answers (or redirects to).
|
|
493
|
+
let issuer = null;
|
|
494
|
+
try {
|
|
495
|
+
const res = await safeFetch(new URL('/.well-known/openid-configuration', rec.podHome).href,
|
|
496
|
+
{ headers: { accept: 'application/json' }, redirect: 'follow', signal: AbortSignal.timeout(SERVER_PROBE_MS) },
|
|
497
|
+
ctx.fetchImpl || fetch);
|
|
498
|
+
if (res.ok) issuer = JSON.parse(await readCapped(res, SERVER_PROBE_BYTES)).issuer || null;
|
|
499
|
+
} catch { /* a pod that will not say leaves the reader to type it */ }
|
|
500
|
+
return said('fedipod', { podHome: rec.podHome, actor: rec.actorUrl, issuer, inbox: identFor(rec).inboxUrl });
|
|
501
|
+
}
|
|
502
|
+
if (!/^[a-z0-9.-]+\.[a-z0-9-]+(:\d+)?$/u.test(host)) return said('invalid');
|
|
503
|
+
const f = ctx.fetchImpl || fetch;
|
|
504
|
+
const ask = async (path) => {
|
|
505
|
+
try {
|
|
506
|
+
const res = await safeFetch(`https://${host}${path}`,
|
|
507
|
+
{ headers: { accept: 'application/json' }, signal: AbortSignal.timeout(SERVER_PROBE_MS) }, f);
|
|
508
|
+
if (!res.ok) return null;
|
|
509
|
+
return JSON.parse(await readCapped(res, SERVER_PROBE_BYTES));
|
|
510
|
+
} catch { return null; }
|
|
511
|
+
};
|
|
512
|
+
const masto = await ask('/api/v1/instance');
|
|
513
|
+
if (masto) return said('mastodon-api', { name: masto.title || masto.domain || host });
|
|
514
|
+
const lemmy = await ask('/api/v3/site');
|
|
515
|
+
if (lemmy) return said('lemmy', { name: lemmy.site_view?.site?.name || host });
|
|
516
|
+
return said('unknown');
|
|
517
|
+
}
|
|
518
|
+
|
|
401
519
|
// Attach: create the account. The user proves control of the pod they claim
|
|
402
520
|
// by presenting a Solid-OIDC token whose WebID lives on that pod — no
|
|
403
521
|
// password reaches the front. On success the directory gets a row and the
|
|
@@ -463,10 +581,20 @@ async function route(request, ctx) {
|
|
|
463
581
|
const claimedActor = typeof body.actorUrl === 'string' && body.actorUrl.startsWith(podHome) ? body.actorUrl : null;
|
|
464
582
|
const actorUrl = fronted ? `${userBase(ctx.frontOrigin, key)}ap/actor` : (claimedActor || podHome + 'ap/actor');
|
|
465
583
|
const hmacSecret = prior?.hmacSecret || crypto.randomBytes(32).toString('base64');
|
|
584
|
+
// Where the door writes: the row's own tree unless the caller names an
|
|
585
|
+
// inbox on the same pod — a forum's categories name the forum's.
|
|
586
|
+
let inboxUrl = null;
|
|
587
|
+
if (typeof body.inboxUrl === 'string' && body.inboxUrl) {
|
|
588
|
+
let ok = false;
|
|
589
|
+
try { ok = new URL(body.inboxUrl).origin === new URL(podHome).origin && body.inboxUrl.endsWith('/'); } catch { ok = false; }
|
|
590
|
+
if (!ok) return j(400, { error: 'inboxUrl must be a container on the same pod' });
|
|
591
|
+
inboxUrl = body.inboxUrl;
|
|
592
|
+
}
|
|
466
593
|
const record = {
|
|
467
594
|
handle, address, podHome, webId: webid, actorUrl,
|
|
468
|
-
kind: body.kind === 'group' ? 'group' : 'person',
|
|
595
|
+
kind: body.kind === 'group' ? 'group' : body.kind === 'application' ? 'application' : 'person',
|
|
469
596
|
gatewayWebId: ctx.gatewayWebId || null, hmacSecret,
|
|
597
|
+
...(inboxUrl ? { inboxUrl } : {}),
|
|
470
598
|
...(fronted ? {} : { inboxOnly: true }),
|
|
471
599
|
};
|
|
472
600
|
await ctx.putDirectory(key, record);
|
|
@@ -481,6 +609,43 @@ async function route(request, ctx) {
|
|
|
481
609
|
command: `fedipod gateway ${doorInbox} --secret ${hmacSecret} --inbox-only` });
|
|
482
610
|
}
|
|
483
611
|
|
|
612
|
+
// An address at this gateway moving to another one. Its owner, proved the
|
|
613
|
+
// way attach proves a pod, says where it went. From then on this front
|
|
614
|
+
// answers the old actor as a moved stub — the same key under the old id,
|
|
615
|
+
// `movedTo` the new one, so the Move the owner sends from the old address
|
|
616
|
+
// verifies — every other old id redirects to its new address, and the
|
|
617
|
+
// mail door is shut: deliveries belong at the new one. Repeating it with
|
|
618
|
+
// the same target changes nothing.
|
|
619
|
+
if (pathname === '/api/move' && request.method === 'OPTIONS') return apiPreflight();
|
|
620
|
+
if (pathname === '/api/move' && request.method === 'POST') {
|
|
621
|
+
if (!ctx.putDirectory) return withApiCors(j(501, { error: 'this front keeps no directory' }));
|
|
622
|
+
let body;
|
|
623
|
+
try { body = JSON.parse(await request.clone().text()); } catch { return withApiCors(j(400, { error: 'bad JSON' })); }
|
|
624
|
+
const handle = String(body.handle || '').toLowerCase();
|
|
625
|
+
const rec = await ctx.lookup(handle);
|
|
626
|
+
if (!rec) return withApiCors(j(404, { error: 'no such account' }));
|
|
627
|
+
if (rec.inboxOnly) return withApiCors(j(400, { error: 'this address lives on its pod; only an address at this gateway can move' }));
|
|
628
|
+
const movedTo = String(body.movedTo || '');
|
|
629
|
+
let mu;
|
|
630
|
+
try { mu = new URL(movedTo); } catch { return withApiCors(j(400, { error: 'movedTo must be the new actor URL' })); }
|
|
631
|
+
if (!((mu.protocol === 'https:' || (mu.protocol === 'http:' && isLoopbackHost(mu.hostname))) && /\/ap\/actor$/u.test(mu.pathname))) {
|
|
632
|
+
return withApiCors(j(400, { error: 'movedTo must be an https …/ap/actor URL' }));
|
|
633
|
+
}
|
|
634
|
+
if (ctx.frontOrigin && mu.origin === new URL(ctx.frontOrigin).origin) return withApiCors(j(400, { error: 'that address is at this gateway' }));
|
|
635
|
+
const webid = await verifyPodToken(request, pathname, ctx.verifier);
|
|
636
|
+
if (!webid) return withApiCors(j(401, { error: 'a Solid-OIDC token proving the pod is required' }));
|
|
637
|
+
const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
|
|
638
|
+
if (!owner) return withApiCors(j(403, { error: "the token proves a different pod than this account's" }));
|
|
639
|
+
const movedAt = rec.movedTo === movedTo && rec.movedAt ? rec.movedAt : new Date().toISOString();
|
|
640
|
+
await ctx.putDirectory(handle, { ...rec, movedTo, movedAt });
|
|
641
|
+
console.log(`front @${handle}: moved to ${movedTo}`);
|
|
642
|
+
return withApiCors(j(200, { ok: true, handle, movedTo, movedAt }));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// The owner's say over an account that goes quiet (quiet.mjs).
|
|
646
|
+
const quiet = await routeQuietApi(request, pathname, ctx, { j, verifyPodToken, webidUnderPod });
|
|
647
|
+
if (quiet) return quiet;
|
|
648
|
+
|
|
484
649
|
// The relay: the front sends requests a browser has already signed. A page
|
|
485
650
|
// may not set the Date or Host header, and both are inside an HTTP
|
|
486
651
|
// signature, so a browser-run agent signs and hands the request here; the
|
|
@@ -488,24 +653,49 @@ async function route(request, ctx) {
|
|
|
488
653
|
// a signature and cannot make one. Only the account's own key may sign,
|
|
489
654
|
// only the account's own pod may call, and nothing unsigned-for reaches
|
|
490
655
|
// the remote server.
|
|
656
|
+
if (pathname === '/api/relay' && request.method === 'OPTIONS') return apiPreflight();
|
|
491
657
|
if (pathname === '/api/relay' && request.method === 'POST') {
|
|
492
658
|
let body;
|
|
493
|
-
try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
|
|
659
|
+
try { body = JSON.parse(await request.clone().text()); } catch { return withApiCors(j(400, { error: 'bad JSON' })); }
|
|
494
660
|
const handle = String(body.handle || '').toLowerCase();
|
|
495
661
|
const rec = await ctx.lookup(handle);
|
|
496
|
-
if (!rec) { console.log(`relay: no account named ${handle}`); return j(404, { error: 'no such account' }); }
|
|
662
|
+
if (!rec) { console.log(`relay: no account named ${handle}`); return withApiCors(j(404, { error: 'no such account' })); }
|
|
497
663
|
const webid = await verifyPodToken(request, pathname, ctx.verifier);
|
|
498
|
-
if (!webid) { console.log(`relay @${handle}: no usable pod token`); return j(401, { error: 'a Solid-OIDC token proving the pod is required' }); }
|
|
664
|
+
if (!webid) { console.log(`relay @${handle}: no usable pod token`); return withApiCors(j(401, { error: 'a Solid-OIDC token proving the pod is required' })); }
|
|
499
665
|
const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
|
|
500
|
-
if (!owner) { console.log(`relay @${handle}: token is for ${webid}, not this account's pod`); return j(403, { error: "the token proves a different pod than this account's" }); }
|
|
666
|
+
if (!owner) { console.log(`relay @${handle}: token is for ${webid}, not this account's pod`); return withApiCors(j(403, { error: "the token proves a different pod than this account's" })); }
|
|
501
667
|
const items = Array.isArray(body.requests) ? body.requests : [];
|
|
502
|
-
if (!items.length) return j(400, { error: 'requests must be a non-empty list' });
|
|
503
|
-
|
|
668
|
+
if (!items.length) return withApiCors(j(400, { error: 'requests must be a non-empty list' }));
|
|
669
|
+
// Acting through the relay is being here. Hourly at most; see noteOpened.
|
|
670
|
+
await noteOpened(ctx, handle, rec).catch((e) => console.log(`relay @${handle}: stamp not written: ${e?.message || e}`));
|
|
671
|
+
if (items.length > RELAY_MAX_REQUESTS) return withApiCors(j(400, { error: `at most ${RELAY_MAX_REQUESTS} requests per call` }));
|
|
504
672
|
const results = await Promise.all(items.map((it) => relayOne(it, rec, ctx.fetchImpl || fetch)));
|
|
505
673
|
// One line per relayed request, so a lookup that fails on the far side is
|
|
506
674
|
// visible here and not only as an empty result in someone's browser.
|
|
507
675
|
for (const r of results) console.log(`relay @${handle}: ${r.method || ''} ${r.url} → ${r.status}${r.error ? ` (${r.error})` : ''}`);
|
|
508
|
-
return j(200, { results });
|
|
676
|
+
return withApiCors(j(200, { results }));
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Who is here, and which pod of theirs could opt in. The opt-in page asks
|
|
680
|
+
// this first: on a pod server that issued the reader's session, the answer
|
|
681
|
+
// names their pod and its address, and the page shows one button instead of
|
|
682
|
+
// a form. A Gateway holds no such session and says so; the page then asks
|
|
683
|
+
// for the pod by hand and proves it with a sign-in, as before.
|
|
684
|
+
if (pathname === '/api/agent' && request.method === 'GET') {
|
|
685
|
+
if (!ctx.agentControl) return j(501, { error: 'this server does not run identities' });
|
|
686
|
+
if (!ctx.webIdsFromSession) return j(200, { offered: true, session: 'unknown' });
|
|
687
|
+
if (!sameOriginRequest(request, ctx.frontOrigin)) return j(200, { offered: true, session: 'none' });
|
|
688
|
+
const webIds = await ctx.webIdsFromSession(request).catch(() => []);
|
|
689
|
+
if (!webIds.length) return j(200, { offered: true, session: 'none', loginUrl: '/.account/login/password/' });
|
|
690
|
+
const pods = [];
|
|
691
|
+
for (const webId of webIds) {
|
|
692
|
+
let podBase;
|
|
693
|
+
try { podBase = podBaseOfWebId(webId); } catch { continue; }
|
|
694
|
+
let about = null;
|
|
695
|
+
try { about = ctx.agentControl.describe ? await ctx.agentControl.describe({ podBase }) : null; } catch { about = null; }
|
|
696
|
+
pods.push({ webId, podBase, ...(about || {}) });
|
|
697
|
+
}
|
|
698
|
+
return j(200, { offered: true, session: 'ok', pods });
|
|
509
699
|
}
|
|
510
700
|
|
|
511
701
|
// Runtime opt-in: a pod owner asks the server this front sits on to RUN
|
|
@@ -540,15 +730,27 @@ async function route(request, ctx) {
|
|
|
540
730
|
if (/(^|\/)\.internal(\/|$)/u.test(podUrl.pathname) || /(^|\/)\.\.(\/|$)/u.test(podUrl.pathname)) {
|
|
541
731
|
return j(403, { error: 'that is not a pod address' });
|
|
542
732
|
}
|
|
543
|
-
const webid = await verifyPodToken(request, pathname, ctx.verifier);
|
|
544
|
-
if (!webid) return j(401, { error: 'a Solid-OIDC token proving the pod is required' });
|
|
545
733
|
// The pod's own server names its owner when it can, and where it does that
|
|
546
734
|
// is the proof. Where it does not, the WebID must live in EXACTLY this pod
|
|
547
735
|
// — its own pod base equal to podBase, not merely starting with it, which on
|
|
548
736
|
// a path server an ancestor of another's pod would.
|
|
549
737
|
const owners = await podOwners(podBase, ctx.fetchImpl || fetch);
|
|
550
|
-
const
|
|
551
|
-
|
|
738
|
+
const ownsIt = (who) => (owners.length ? owners.includes(who) : podBaseOfWebId(who) === podBase);
|
|
739
|
+
const token = await verifyPodToken(request, pathname, ctx.verifier);
|
|
740
|
+
// A server that issued the reader's own session can simply read it. On a
|
|
741
|
+
// pod server the page, the session and the pod are one origin, and a token
|
|
742
|
+
// proves nothing the session does not — so opting in cost a round trip to
|
|
743
|
+
// an identity provider that had just said who they were. A Gateway fronting
|
|
744
|
+
// somebody else's pod holds no such session and this does nothing there.
|
|
745
|
+
//
|
|
746
|
+
// One account can hold several WebIDs, so it is the one that owns this pod
|
|
747
|
+
// that is taken, not whichever came back first.
|
|
748
|
+
const session = !token && ctx.webIdsFromSession && sameOriginRequest(request, ctx.frontOrigin)
|
|
749
|
+
? (await ctx.webIdsFromSession(request).catch(() => [])).find(ownsIt) || null
|
|
750
|
+
: null;
|
|
751
|
+
const webid = token || session;
|
|
752
|
+
if (!webid) return j(401, { error: 'a Solid-OIDC token proving the pod is required' });
|
|
753
|
+
if (!ownsIt(webid)) {
|
|
552
754
|
return j(403, { error: 'the token proves a different pod than the one you listed' });
|
|
553
755
|
}
|
|
554
756
|
if (action === 'opt-in') {
|
|
@@ -574,14 +776,16 @@ async function route(request, ctx) {
|
|
|
574
776
|
//
|
|
575
777
|
// The name is matched against a literal list, so nothing about the request
|
|
576
778
|
// chooses a file.
|
|
577
|
-
const mPageScript = /^\/(new-account|run|admin)\.js$/u.exec(pathname);
|
|
779
|
+
const mPageScript = /^\/(new-account|run|admin|notices)\.js$/u.exec(pathname);
|
|
578
780
|
if (mPageScript) {
|
|
579
781
|
const body = ctx.pageScripts?.[`${mPageScript[1]}.js`];
|
|
580
782
|
if (!body) return notFound();
|
|
581
|
-
|
|
783
|
+
// Revalidated on every load: a page served fresh with a script the
|
|
784
|
+
// browser kept from last time is the mismatch nobody can see.
|
|
785
|
+
return { status: 200, headers: { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-cache' }, body };
|
|
582
786
|
}
|
|
583
787
|
|
|
584
|
-
// The vendored Solid-OIDC browser library the
|
|
788
|
+
// The vendored Solid-OIDC browser library the opt-in and admin pages load —
|
|
585
789
|
// served here because this function owns every path on the domain.
|
|
586
790
|
if (pathname === '/solid-oidc-client.js') {
|
|
587
791
|
if (!ctx.authBundle) return notFound();
|
|
@@ -602,14 +806,22 @@ async function route(request, ctx) {
|
|
|
602
806
|
if (!m || m[2] !== ctx.host) return notFound();
|
|
603
807
|
const rec = await ctx.lookup(m[1]);
|
|
604
808
|
if (!rec) return notFound();
|
|
809
|
+
// A closed address is gone, and says so rather than pretending never to
|
|
810
|
+
// have existed: the name stays taken.
|
|
811
|
+
if ((await closedState(ctx, m[1], rec)).closed) return closedAnswer({ 'access-control-allow-origin': '*' });
|
|
605
812
|
// A fronted identity's documents live on its pod; the pod's own actor id
|
|
606
813
|
// is the alias, so a client signing in by the fronted address can find
|
|
607
814
|
// the pod (and its login) without a lookup only the host could answer.
|
|
608
815
|
const podActor = rec.inboxOnly ? [] : [rec.podHome + 'ap/actor'];
|
|
609
|
-
const out = j(200, jrd({ handle: m[1], host: ctx.host, actor: rec.actorUrl, aliases: podActor,
|
|
610
|
-
|
|
816
|
+
const out = j(200, jrd({ handle: m[1], host: ctx.host, actor: rec.actorUrl, aliases: podActor,
|
|
817
|
+
page: profilePageOf(rec), subscribe: subscribeTemplate(ctx.frontOrigin) }),
|
|
818
|
+
'application/jrd+json');
|
|
611
819
|
// RFC 7033: a JRD is read from any origin.
|
|
612
820
|
out.headers['access-control-allow-origin'] = '*';
|
|
821
|
+
// A handle answers the same for everybody, and changes only when a row
|
|
822
|
+
// does. Every server that has heard of this account asks for it; the edge
|
|
823
|
+
// can answer most of them.
|
|
824
|
+
Object.assign(out.headers, publicFor(WEBFINGER_EDGE_SECONDS));
|
|
613
825
|
return out;
|
|
614
826
|
}
|
|
615
827
|
|
|
@@ -634,16 +846,28 @@ async function route(request, ctx) {
|
|
|
634
846
|
const rec = await ctx.lookup(up.handle);
|
|
635
847
|
if (!rec) return notFound();
|
|
636
848
|
const base = userBase(ctx.frontOrigin, up.handle);
|
|
849
|
+
// An account that moved to another gateway (see /api/move): where its ids
|
|
850
|
+
// live now. The actor below answers as a stub; everything else redirects.
|
|
851
|
+
const movedBase = rec.movedTo ? rec.movedTo.replace(/ap\/actor$/u, '') : null;
|
|
852
|
+
// A closed address (see "accounts that go quiet" above): every id under it
|
|
853
|
+
// is gone. Asked after moved, which a close does not undo.
|
|
854
|
+
const closed = movedBase ? false : (await closedState(ctx, up.handle, rec)).closed;
|
|
855
|
+
const gone = (headers = {}, why = `this account has moved to ${rec.movedTo}`) => closedAnswer(headers, why);
|
|
856
|
+
const CLOSED = 'this address is closed';
|
|
637
857
|
|
|
638
858
|
// Inbox: verify at the door, forward to the user's pod inbox. This is the
|
|
639
859
|
// gateway, per user.
|
|
640
860
|
if (up.rest === 'ap/inbox/' || up.rest === 'ap/inbox') {
|
|
641
861
|
if (request.method !== 'POST') return { status: 405, headers: {}, body: '' };
|
|
862
|
+
if (movedBase) { console.log(`door @${up.handle}: delivery → 410 (moved)`); return gone(); }
|
|
863
|
+
if (closed) { console.log(`door @${up.handle}: delivery → 410 (closed)`); return gone({}, CLOSED); }
|
|
642
864
|
const policy = await policyFor(rec, ctx.fetchImpl || fetch);
|
|
643
|
-
const
|
|
644
|
-
|
|
865
|
+
const standing = await accountState(ctx, up.handle, rec);
|
|
866
|
+
const { status, reason, content, bytes } = await handleDelivery(request, identFor(rec, policy),
|
|
867
|
+
{ podPut: (u, b, ct) => ctx.podPut(up.handle, u, b, ct), fetchImpl: ctx.fetchImpl, paused: standing.paused });
|
|
645
868
|
// One line per delivery, so "did it arrive at the door" has an answer.
|
|
646
869
|
console.log(`door @${up.handle}: delivery → ${status} (${reason})`);
|
|
870
|
+
if (status === 202 && content) await noteReceived(ctx, up.handle, rec, bytes);
|
|
647
871
|
return { status, headers: {}, body: '' };
|
|
648
872
|
}
|
|
649
873
|
|
|
@@ -669,6 +893,11 @@ async function route(request, ctx) {
|
|
|
669
893
|
return { status: 204, headers: { ...cors, allow: 'GET, POST, OPTIONS',
|
|
670
894
|
'accept-post': 'application/ld+json, application/activity+json' }, body: null };
|
|
671
895
|
}
|
|
896
|
+
if (movedBase && request.method === 'POST') return gone(cors);
|
|
897
|
+
if (closed && request.method === 'POST') return gone(cors, CLOSED);
|
|
898
|
+
if (movedBase && (request.method === 'GET' || request.method === 'HEAD')) {
|
|
899
|
+
return { status: 301, headers: { ...cors, location: movedBase + up.rest, 'cache-control': 'no-store' }, body: '' };
|
|
900
|
+
}
|
|
672
901
|
if (request.method === 'POST') {
|
|
673
902
|
const json = (status, obj, extra = {}) => ({ status,
|
|
674
903
|
headers: { ...cors, 'content-type': 'application/json', 'cache-control': 'no-store', ...extra },
|
|
@@ -692,7 +921,21 @@ async function route(request, ctx) {
|
|
|
692
921
|
// Everything else is a public GET, served by reading the user's pod and
|
|
693
922
|
// rewriting pod ids to the front. The actor also gets its handle and inbox
|
|
694
923
|
// fixed to the front so a consumer cross-checks it consistently.
|
|
924
|
+
// Public documents are read from any origin — a page on another site, the
|
|
925
|
+
// forum's own host among them — the way a pod serves them, so the read
|
|
926
|
+
// carries the open origin and a preflight is answered.
|
|
927
|
+
const open = { 'access-control-allow-origin': '*' };
|
|
928
|
+
if (request.method === 'OPTIONS') {
|
|
929
|
+
return { status: 204, headers: { ...open, 'access-control-allow-methods': 'GET, HEAD, OPTIONS',
|
|
930
|
+
'access-control-allow-headers': 'Accept', 'access-control-max-age': '86400', allow: 'GET, HEAD, OPTIONS' }, body: null };
|
|
931
|
+
}
|
|
695
932
|
if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
|
|
933
|
+
// Moved: every old id but the actor's answers with where it is now, so a
|
|
934
|
+
// reply or a boost another server still holds under the old id resolves.
|
|
935
|
+
if (movedBase && up.rest !== 'ap/actor') {
|
|
936
|
+
return { status: 301, headers: { ...open, location: movedBase + up.rest, 'cache-control': 'no-store' }, body: '' };
|
|
937
|
+
}
|
|
938
|
+
if (closed) return gone(open, CLOSED);
|
|
696
939
|
const podTarget = rec.podHome + up.rest;
|
|
697
940
|
// Media stays on the pod (lib/pod/urls.mjs keeps `media` off the front), but
|
|
698
941
|
// the id rewrite below turns media links onto the front like every other
|
|
@@ -705,22 +948,50 @@ async function route(request, ctx) {
|
|
|
705
948
|
// directly (the CSS server component) has no access control of its own and
|
|
706
949
|
// needs to be told what it may reach. See podHomeProblem above for the other
|
|
707
950
|
// half — a row that names something which is not a pod.
|
|
951
|
+
// A container is Turtle, a profile page is HTML, everything else here is
|
|
952
|
+
// ActivityPub. Ask for what the thing can be, not for what most of them are.
|
|
953
|
+
const container = podTarget.endsWith('/');
|
|
954
|
+
const page = /\.html?$/u.test(podTarget);
|
|
955
|
+
const accept = container ? 'text/turtle, */*;q=0.8'
|
|
956
|
+
: page ? 'text/html, */*;q=0.8'
|
|
957
|
+
: AP_CT;
|
|
708
958
|
const got = await podRoot.readPublicDocument(
|
|
709
|
-
ctx.podGet || ((u) => fetch(u, { headers: { accept
|
|
710
|
-
podTarget, { podHome: rec.podHome });
|
|
711
|
-
if (got.text === null) return { status: got.status, headers:
|
|
959
|
+
ctx.podGet || ((u) => fetch(u, { headers: { accept } })),
|
|
960
|
+
podTarget, { podHome: rec.podHome, accept });
|
|
961
|
+
if (got.text === null) return { status: got.status, headers: open, body: '' };
|
|
712
962
|
let text = got.text;
|
|
713
963
|
text = swap(text, rec.podHome, base);
|
|
964
|
+
// The moved stub: the pod's actor now carries the NEW gateway's ids, and a
|
|
965
|
+
// server verifying the Move fetches the OLD id and needs a document whose
|
|
966
|
+
// id is that old id — with the same key under it, and `movedTo` saying
|
|
967
|
+
// where the account went. Swapping the new base back to this one gives
|
|
968
|
+
// exactly that; alsoKnownAs and movedTo are set below.
|
|
969
|
+
if (movedBase) text = swap(text, movedBase, base);
|
|
714
970
|
if (up.rest === 'ap/actor') {
|
|
715
971
|
try {
|
|
716
972
|
const doc = JSON.parse(text);
|
|
717
973
|
doc.preferredUsername = up.handle; // so @handle@front cross-checks
|
|
718
974
|
doc.inbox = base + 'ap/inbox/'; // deliveries come to the front to be verified
|
|
719
975
|
doc.endpoints = { ...(doc.endpoints || {}), sharedInbox: base + 'ap/inbox/' };
|
|
976
|
+
if (movedBase) {
|
|
977
|
+
doc.movedTo = rec.movedTo;
|
|
978
|
+
const aka = new Set([].concat(doc.alsoKnownAs || []).filter((a) => typeof a === 'string' && a !== doc.id));
|
|
979
|
+
aka.add(rec.movedTo);
|
|
980
|
+
doc.alsoKnownAs = [...aka];
|
|
981
|
+
const ctxs = [].concat(doc['@context'] || []);
|
|
982
|
+
const declared = (term) => ctxs.some((c) => c && typeof c === 'object' && term in c);
|
|
983
|
+
if (!declared('alsoKnownAs')) ctxs.push({ alsoKnownAs: { '@id': 'as:alsoKnownAs', '@type': '@id' } });
|
|
984
|
+
if (!declared('movedTo')) ctxs.push({ movedTo: { '@id': 'as:movedTo', '@type': '@id' } });
|
|
985
|
+
doc['@context'] = ctxs;
|
|
986
|
+
}
|
|
720
987
|
text = JSON.stringify(doc);
|
|
721
988
|
} catch { /* leave the rewritten text as-is if it will not parse */ }
|
|
722
989
|
}
|
|
723
|
-
|
|
990
|
+
// `readPublicDocument` asked for it with no credential, so a document that
|
|
991
|
+
// came back is one anybody can read — which is what makes it cacheable.
|
|
992
|
+
return { status: 200,
|
|
993
|
+
headers: { ...open, 'content-type': got.type || (container ? 'text/turtle' : page ? 'text/html' : AP_CT), ...publicFor(PUBLIC_EDGE_SECONDS) },
|
|
994
|
+
body: text };
|
|
724
995
|
}
|
|
725
996
|
|
|
726
997
|
export const _internal = { parseUserPath, identFor, swap, handleProblem, policyFor, policyCache };
|
|
@@ -19,9 +19,11 @@ import * as inbox from '../pod/inbox.mjs';
|
|
|
19
19
|
const DEFAULT_MAX_BYTES = 512 * 1024; // mirror intake.mjs MAX_ITEM_BYTES
|
|
20
20
|
|
|
21
21
|
// Control activities are the message itself and must always pass — you want
|
|
22
|
-
// Follows from strangers. Only CONTENT is subject to the concerns-us drop
|
|
23
|
-
|
|
22
|
+
// Follows from strangers. Only CONTENT is subject to the concerns-us drop,
|
|
23
|
+
// and to a pause: an account nobody is reading still takes its follows.
|
|
24
|
+
export const CONTROL = new Set(['Follow', 'Undo', 'Accept', 'Reject', 'Delete', 'Move',
|
|
24
25
|
'Add', 'Remove', 'Block']);
|
|
26
|
+
export const isControl = (type) => CONTROL.has(type);
|
|
25
27
|
|
|
26
28
|
const idOf = (v) => (typeof v === 'string' ? v : v?.id) || null;
|
|
27
29
|
const sha256hex = (s) => crypto.createHash('sha256').update(s).digest('hex');
|
|
@@ -64,8 +66,13 @@ function concernsUsAtEdge(activity, ident) {
|
|
|
64
66
|
// resolved identity policy: { inboxUrl, actorUrl, followersUrl, notesPrefix,
|
|
65
67
|
// following, blocklist, kind, gatewayWebId, hmacSecret }. `podPut(url, body,
|
|
66
68
|
// contentType) → boolean` appends to the pod with the gateway's credential.
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
+
// `paused` says nobody is reading this inbox: content is accepted and
|
|
70
|
+
// discarded (the same quiet 202 a blocked sender gets, so nothing retries
|
|
71
|
+
// and nothing counts a failure against this host), control still lands.
|
|
72
|
+
// Returns { status, reason } — the adapter turns it into an HTTP response —
|
|
73
|
+
// and, for a delivery that reached the pod, `content` (whether it was
|
|
74
|
+
// content rather than control) and `bytes`, so the caller can keep count.
|
|
75
|
+
export async function handleDelivery(request, ident, { podPut, fetchImpl = fetch, maxBytes = DEFAULT_MAX_BYTES, paused = false } = {}) {
|
|
69
76
|
// Read the body once from a clone; the original, unconsumed, goes to the
|
|
70
77
|
// verifier (which needs the body for the Digest check).
|
|
71
78
|
let raw;
|
|
@@ -80,7 +87,9 @@ export async function handleDelivery(request, ident, { podPut, fetchImpl = fetch
|
|
|
80
87
|
// Edge drops — silent 202 so a rejected sender does not retry a delivery we
|
|
81
88
|
// will never accept. None of these becomes a pod write.
|
|
82
89
|
if (isBlocked(actor, ident.blocklist)) return { status: 202, reason: 'blocked' };
|
|
83
|
-
|
|
90
|
+
const content = !CONTROL.has(activity.type);
|
|
91
|
+
if (content && paused) return { status: 202, reason: 'paused' };
|
|
92
|
+
if (content && !concernsUsAtEdge(activity, ident)) {
|
|
84
93
|
return { status: 202, reason: 'does not concern us' };
|
|
85
94
|
}
|
|
86
95
|
|
|
@@ -107,7 +116,7 @@ export async function handleDelivery(request, ident, { podPut, fetchImpl = fetch
|
|
|
107
116
|
// holding any state.
|
|
108
117
|
if (!okA) return { status: 502, reason: 'pod inbox write failed' };
|
|
109
118
|
if (receipt) await inbox.writeReceiptBeside(podPut, ident.inboxUrl, hash, receipt);
|
|
110
|
-
return { status: 202, reason: v.verified ? 'verified' : 'buffered-unverified' };
|
|
119
|
+
return { status: 202, reason: v.verified ? 'verified' : 'buffered-unverified', content, bytes: Buffer.byteLength(raw) };
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
// The outbox door: the owner's own post, taken on their behalf.
|