fedipod-server 0.10.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/deliver.mjs +6 -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 +335 -118
- package/lib/links.mjs +6 -32
- package/lib/mastoapi.mjs +372 -35
- 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/publisher.mjs +112 -183
- package/lib/remote.mjs +30 -395
- package/lib/social.mjs +18 -9
- package/lib/storage.mjs +2 -2
- package/lib/store.mjs +8 -1
- package/lib/tagfeed.mjs +19 -2
- package/lib/wire.mjs +48 -67
- package/package.json +1 -1
- package/run-agent.mjs +14 -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/lib/intake.mjs
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
// a poll every POLL_MS as fallback, plus a drain at startup.
|
|
16
16
|
|
|
17
17
|
import * as $rdf from 'rdflib';
|
|
18
|
+
import * as podInbox from './pod/inbox.mjs';
|
|
19
|
+
import * as podNotifications from './pod/notifications.mjs';
|
|
20
|
+
import * as podNotes from './pod/notes.mjs';
|
|
18
21
|
import { USER_AGENT } from './ua.mjs';
|
|
19
22
|
import { PUBLIC } from './wire.mjs';
|
|
20
23
|
import { HTTP_TIMEOUT_MS, readCapped } from './safefetch.mjs';
|
|
@@ -96,10 +99,39 @@ const CONTENT_TYPES = new Set(['Note', 'Article', 'Question', 'Page', 'Video', '
|
|
|
96
99
|
// several), each carrying the tally its author's server maintains.
|
|
97
100
|
// Custom emojis ride the tag list; the images live at the author's server and
|
|
98
101
|
// the client fetches them from there.
|
|
102
|
+
// Content, name and summary were capped; nothing else was. One remote Question
|
|
103
|
+
// with thousands of options, or thousands of emoji, mention or attachment
|
|
104
|
+
// entries, put megabytes into a single statuses.json row — a document rewritten
|
|
105
|
+
// whole on every change. These are display lists: past a few dozen, nothing can
|
|
106
|
+
// render them and nobody meant them to be rendered.
|
|
107
|
+
// http(s) only. An emoji, attachment or mention URL is written straight into
|
|
108
|
+
// the client's markup, so `javascript:` and `data:` have no business in one.
|
|
109
|
+
// The store has guarded avatars this way all along (safeUrl, lib/store.mjs);
|
|
110
|
+
// these three lists were simply never put through it.
|
|
111
|
+
const httpOnly = (u) => {
|
|
112
|
+
if (!u) return null;
|
|
113
|
+
try {
|
|
114
|
+
const p = new URL(String(u));
|
|
115
|
+
return (p.protocol === 'https:' || p.protocol === 'http:') ? String(u) : null;
|
|
116
|
+
} catch { return null; }
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const MAX_MODQUEUE = 200;
|
|
120
|
+
const MAX_EMOJIS = 60;
|
|
121
|
+
const MAX_POLL_OPTIONS = 50;
|
|
122
|
+
const MAX_OPTION_CHARS = 200;
|
|
123
|
+
const MAX_MENTIONS = 60;
|
|
124
|
+
const MAX_URL_CHARS = 2048;
|
|
125
|
+
|
|
99
126
|
function emojisOf(note) {
|
|
100
127
|
return [].concat(note?.tag || [])
|
|
101
128
|
.filter(t => t?.type === 'Emoji' && t.icon?.url && t.name)
|
|
102
|
-
.
|
|
129
|
+
.slice(0, MAX_EMOJIS)
|
|
130
|
+
.map(t => ({
|
|
131
|
+
shortcode: String(t.name).replace(/^:|:$/g, '').slice(0, 64),
|
|
132
|
+
url: httpOnly(String(t.icon.url).slice(0, MAX_URL_CHARS)),
|
|
133
|
+
}))
|
|
134
|
+
.filter(e => e.url);
|
|
103
135
|
}
|
|
104
136
|
|
|
105
137
|
function pollOf(note) {
|
|
@@ -109,8 +141,8 @@ function pollOf(note) {
|
|
|
109
141
|
multiple: !!note.anyOf,
|
|
110
142
|
expiresAt: note.endTime || null,
|
|
111
143
|
closed: !!note.closed,
|
|
112
|
-
options: opts.map(o => ({
|
|
113
|
-
title: String(o?.name ?? ''),
|
|
144
|
+
options: opts.slice(0, MAX_POLL_OPTIONS).map(o => ({
|
|
145
|
+
title: String(o?.name ?? '').slice(0, MAX_OPTION_CHARS),
|
|
114
146
|
votes: Number(o?.replies?.totalItems) || 0,
|
|
115
147
|
})),
|
|
116
148
|
};
|
|
@@ -128,8 +160,77 @@ const isActorType = (t) => typesOf(t).some(x => ACTOR_TYPES.has(x));
|
|
|
128
160
|
// stranger could have anything at all, of a type nothing here reads,
|
|
129
161
|
// re-delivered to every follower over our signature.
|
|
130
162
|
const FORWARDABLE = new Set(['Create', 'Update', 'Delete', 'Like', 'Announce', 'Undo']);
|
|
163
|
+
// Of those, the ones we may re-deliver to our own followers over our own
|
|
164
|
+
// signature (§7.1.2). Deliberately narrower than FORWARDABLE: these three are
|
|
165
|
+
// the ones whose object this drain fetched from the author's origin and checked
|
|
166
|
+
// before accepting. A Like, an Announce or an Undo is taken on the envelope's
|
|
167
|
+
// word alone — relaying one is signing for a claim nothing corroborated.
|
|
168
|
+
const FORWARD_TYPES = new Set(['Create', 'Update', 'Delete']);
|
|
169
|
+
// How many forwards one drain may send. A reply into a busy thread of ours is a
|
|
170
|
+
// handful; anything near this is a flood using us as an amplifier.
|
|
171
|
+
const MAX_FORWARDS_PER_DRAIN = 20;
|
|
131
172
|
const ACCEPT_AP = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
|
|
132
173
|
|
|
174
|
+
// What is worth keeping of an activity we are filing rather than acting on.
|
|
175
|
+
//
|
|
176
|
+
// A dead letter, a moderation entry and a waiting follow request each used to
|
|
177
|
+
// hold the WHOLE delivered activity — up to the 512 KB item cap — in documents
|
|
178
|
+
// that are serialized and PUT whole on every change. A stranger could inflate
|
|
179
|
+
// deadletter.json to ~100 MB and requests.json to ~250 MB and make us rewrite
|
|
180
|
+
// them on every batch.
|
|
181
|
+
//
|
|
182
|
+
// Everything the consumers need survives: `acceptActivity` wraps this as the
|
|
183
|
+
// Accept's object (and {id, type, actor, object} IS the standard Follow shape),
|
|
184
|
+
// `applyModeration` reads only `object`, and a dead letter is read by a human
|
|
185
|
+
// who wants to know what arrived, not to replay it.
|
|
186
|
+
export function trimActivity(a) {
|
|
187
|
+
if (!a || typeof a !== 'object') return a ?? null;
|
|
188
|
+
const idOf = (v) => (typeof v === 'string' ? v : v?.id ?? null);
|
|
189
|
+
const out = {};
|
|
190
|
+
for (const k of ['id', 'type', 'actor', 'target']) {
|
|
191
|
+
const v = idOf(a[k]);
|
|
192
|
+
if (v) out[k] = String(v).slice(0, 2048);
|
|
193
|
+
}
|
|
194
|
+
const obj = idOf(a.object);
|
|
195
|
+
if (obj) out.object = String(obj).slice(0, 2048);
|
|
196
|
+
// A typed object with no id still says what it was — a Block of nobody, an
|
|
197
|
+
// Undo of a Follow — and that is the whole of what the queue reads.
|
|
198
|
+
else if (a.object && typeof a.object === 'object' && a.object.type) {
|
|
199
|
+
out.object = { type: String(a.object.type).slice(0, 64) };
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Same origin AND, where the origin holds more than one identity, the same
|
|
205
|
+
// identity within it.
|
|
206
|
+
//
|
|
207
|
+
// Origin alone is the right test when an origin is one person — the ordinary
|
|
208
|
+
// fediverse server, and a subdomain pod. It is the WRONG test on a path-based
|
|
209
|
+
// host, and above all on a multi-tenant front: every tenant of fedipod.net has
|
|
210
|
+
// ids under `https://fedipod.net/u/<name>/`, so origin-equality made every
|
|
211
|
+
// tenant able to vouch for every other. One tenant could publish posts
|
|
212
|
+
// attributed to another, Update a neighbour's post, or Delete it — and a
|
|
213
|
+
// receiving Mastodon would believe it for the same reason we did.
|
|
214
|
+
//
|
|
215
|
+
// So when both ids carry an identity prefix, the prefixes must match too. The
|
|
216
|
+
// two shapes that exist here are the front's `/u/<name>/` and a pod's own AP
|
|
217
|
+
// root (`…/<root>/ap/…`); anything else has no prefix and falls back to origin,
|
|
218
|
+
// which is what a plain remote server should be judged by.
|
|
219
|
+
function identityPrefix(u) {
|
|
220
|
+
const m = /^(https?:\/\/[^/]+\/u\/[^/]+\/)/u.exec(u);
|
|
221
|
+
if (m) return m[1];
|
|
222
|
+
const ap = /^(https?:\/\/[^/]+\/(?:[^/]+\/)*?)ap\//u.exec(u);
|
|
223
|
+
return ap ? ap[1] : null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function sameIdentity(a, b) {
|
|
227
|
+
if (!sameOrigin(a, b)) return false;
|
|
228
|
+
const pa = identityPrefix(String(a));
|
|
229
|
+
const pb = identityPrefix(String(b));
|
|
230
|
+
if (!pa || !pb) return true; // no prefix to compare: origin is the answer
|
|
231
|
+
return pa === pb;
|
|
232
|
+
}
|
|
233
|
+
|
|
133
234
|
export function sameOrigin(a, b) {
|
|
134
235
|
try { return new URL(a).origin === new URL(b).origin; } catch { return false; }
|
|
135
236
|
}
|
|
@@ -149,13 +250,21 @@ export function sameOrigin(a, b) {
|
|
|
149
250
|
// and two labels minimum so `.live` cannot pose as everyone's parent. Not a
|
|
150
251
|
// public-suffix list — that is a dependency and a data file to keep current,
|
|
151
252
|
// and the party this guards against is the pod you already chose to trust.
|
|
253
|
+
//
|
|
254
|
+
// `localhost` is the one single-label parent allowed, because it is the one that
|
|
255
|
+
// cannot be anybody else: it is reserved to the loopback interface (RFC 6761),
|
|
256
|
+
// so `alice.localhost` and `localhost` are the same machine by definition and
|
|
257
|
+
// there is no stranger for the rule to keep out. Without this a pod served from
|
|
258
|
+
// a subdomain of localhost — which is how a Solid server with subdomain pods
|
|
259
|
+
// runs on a developer's machine — refused its own socket and fell back to
|
|
260
|
+
// polling, so every delivery waited up to two minutes.
|
|
152
261
|
export function sameSocketOrigin(socketUrl, podBase) {
|
|
153
262
|
let s, p;
|
|
154
263
|
try { s = new URL(socketUrl); p = new URL(podBase); } catch { return false; }
|
|
155
264
|
if (s.protocol !== (p.protocol === 'https:' ? 'wss:' : 'ws:')) return false;
|
|
156
265
|
if (s.host === p.host) return true;
|
|
157
266
|
const parent = s.hostname.toLowerCase();
|
|
158
|
-
return parent.split('.').length >= 2
|
|
267
|
+
return (parent.split('.').length >= 2 || parent === 'localhost')
|
|
159
268
|
&& s.port === p.port
|
|
160
269
|
&& p.hostname.toLowerCase().endsWith('.' + parent);
|
|
161
270
|
}
|
|
@@ -186,16 +295,20 @@ export function authorOf(note, delivered = null) {
|
|
|
186
295
|
.map(a => (typeof a === 'string' ? a : a?.id)).find(Boolean) || null;
|
|
187
296
|
const author = claimed || delivered;
|
|
188
297
|
if (!author) return null;
|
|
189
|
-
|
|
298
|
+
// sameIdentity, not sameOrigin: on a multi-tenant front every tenant shares
|
|
299
|
+
// an origin, so origin-equality let any of them be credited with any other's
|
|
300
|
+
// post. See sameIdentity.
|
|
301
|
+
return sameIdentity(note?.id, author) ? author : null;
|
|
190
302
|
}
|
|
191
303
|
|
|
192
304
|
export class Intake {
|
|
193
|
-
constructor({ config, urls, remote,
|
|
194
|
-
Object.assign(this, { config, urls, remote,
|
|
305
|
+
constructor({ config, urls, remote, store, deliverer, publisher, log = console.log, lease = null, archive = null, push = true, pollSeconds = null }) {
|
|
306
|
+
Object.assign(this, { config, urls, remote, store, deliverer, publisher, log, lease, archive, push, pollSeconds });
|
|
195
307
|
this.serial = Date.now();
|
|
196
308
|
this.stopped = false;
|
|
197
309
|
// (attempt counts are kept in pod state — see _bumpAttempt)
|
|
198
310
|
this.lastDrain = null;
|
|
311
|
+
this._forwardBudget = MAX_FORWARDS_PER_DRAIN;
|
|
199
312
|
this.lastDrainAtMs = 0;
|
|
200
313
|
this.reconnectTries = 0;
|
|
201
314
|
this.drainCooldownUntil = 0;
|
|
@@ -315,16 +428,8 @@ export class Intake {
|
|
|
315
428
|
* pod that says nothing has always used.
|
|
316
429
|
*/
|
|
317
430
|
async _storageDescriptionUrl() {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
method: 'HEAD',
|
|
321
|
-
headers: { 'user-agent': USER_AGENT },
|
|
322
|
-
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
|
|
323
|
-
});
|
|
324
|
-
const [found] = linkTargets(head.headers.get('link'), REL.storageDescription, this.urls.base);
|
|
325
|
-
if (found) return found;
|
|
326
|
-
} catch { /* the well-known path below */ }
|
|
327
|
-
return this.urls.base + '.well-known/solid';
|
|
431
|
+
return podNotifications.storageDescriptionUrl(this.urls.base,
|
|
432
|
+
{ headers: { 'user-agent': USER_AGENT }, timeoutMs: HTTP_TIMEOUT_MS });
|
|
328
433
|
}
|
|
329
434
|
|
|
330
435
|
async _subscribeOnce() {
|
|
@@ -335,35 +440,16 @@ export class Intake {
|
|
|
335
440
|
return;
|
|
336
441
|
}
|
|
337
442
|
const descUrl = await this._storageDescriptionUrl();
|
|
338
|
-
const
|
|
339
|
-
headers: {
|
|
340
|
-
|
|
341
|
-
});
|
|
342
|
-
// The service description is RDF; ask rdflib which subject is the
|
|
343
|
-
// WebSocketChannel2023 service rather than pattern-matching the document.
|
|
344
|
-
const g = $rdf.graph();
|
|
345
|
-
try { $rdf.parse(await readCapped(descRes), g, descUrl, 'text/turtle'); }
|
|
346
|
-
catch (e) { this.wsState = 'unavailable'; this.log(`service description unparsable (${e.message}) — polling only`); return; }
|
|
347
|
-
const channel = g.each(null, RDF('type'), NOTIFY('WebSocketChannel2023'), null)
|
|
348
|
-
.map(n => n.value)
|
|
349
|
-
.find(Boolean)
|
|
350
|
-
|| g.each(null, NOTIFY('channelType'), NOTIFY('WebSocketChannel2023'), null)
|
|
351
|
-
.map(n => n.value).find(Boolean);
|
|
352
|
-
if (!channel) { this.wsState = 'unavailable'; this.log('no WebSocketChannel2023 service — polling only'); return; }
|
|
443
|
+
const { channel, error } = await podNotifications.readWebSocketChannel(descUrl,
|
|
444
|
+
{ headers: { 'user-agent': USER_AGENT }, timeoutMs: HTTP_TIMEOUT_MS });
|
|
445
|
+
if (!channel) { this.wsState = 'unavailable'; this.log(`${error} — polling only`); return; }
|
|
353
446
|
// The topic is a POD resource, and it travels in the BODY — so the url map
|
|
354
447
|
// RemotePod applies to the request line never reaches it. A fronted
|
|
355
448
|
// identity's inbox url names the front, which the pod cannot grant read on,
|
|
356
449
|
// and the subscription came back 403. A no-op when unfronted.
|
|
357
450
|
const topic = this.urls.toPod ? this.urls.toPod(this.urls.inbox) : this.urls.inbox;
|
|
358
|
-
const sub = await this.remote
|
|
359
|
-
|
|
360
|
-
headers: { 'content-type': 'application/ld+json' },
|
|
361
|
-
body: JSON.stringify({
|
|
362
|
-
'@context': ['https://www.w3.org/ns/solid/notification/v1'],
|
|
363
|
-
type: 'http://www.w3.org/ns/solid/notifications#WebSocketChannel2023',
|
|
364
|
-
topic,
|
|
365
|
-
}),
|
|
366
|
-
});
|
|
451
|
+
const sub = await podNotifications.subscribeToInbox(this.remote,
|
|
452
|
+
{ channelUrl: channel, podTopicUrl: topic });
|
|
367
453
|
const body = await readCapped(sub).then(JSON.parse).catch(() => null);
|
|
368
454
|
if (!body?.receiveFrom) {
|
|
369
455
|
this.wsState = `subscribe-failed-${sub.status}`;
|
|
@@ -445,6 +531,9 @@ export class Intake {
|
|
|
445
531
|
// invariant — commit() flushes whatever is pending either way — so a store
|
|
446
532
|
// that does not implement it behaves exactly as before.
|
|
447
533
|
this._inSweep = true;
|
|
534
|
+
// Fresh per sweep: the cap is on how much this drain may amplify, not a
|
|
535
|
+
// lifetime total (see _maybeForward).
|
|
536
|
+
this._forwardBudget = MAX_FORWARDS_PER_DRAIN;
|
|
448
537
|
this.store.hold?.();
|
|
449
538
|
this._draining = this._drainOnce().finally(async () => {
|
|
450
539
|
this._inSweep = false;
|
|
@@ -481,7 +570,7 @@ export class Intake {
|
|
|
481
570
|
async prune({ before, keepConcerning = false } = {}) {
|
|
482
571
|
const cutoff = Date.parse(before);
|
|
483
572
|
if (!Number.isFinite(cutoff)) throw new Error(`"${before}" is not a date`);
|
|
484
|
-
const all = await this.remote
|
|
573
|
+
const all = await podInbox.list(this.remote, this.urls);
|
|
485
574
|
const older = all.filter(e => !e.url.endsWith('.keep')
|
|
486
575
|
&& e.modified && Date.parse(e.modified) < cutoff);
|
|
487
576
|
const out = { considered: older.length, applied: 0, dropped: 0, discarded: 0, failed: 0 };
|
|
@@ -489,14 +578,14 @@ export class Intake {
|
|
|
489
578
|
for (const item of older) {
|
|
490
579
|
try {
|
|
491
580
|
if (item.size > MAX_ITEM_BYTES) {
|
|
492
|
-
await this.remote
|
|
581
|
+
await podInbox.dropHandledItem(this.remote, item.url); // unreadable by the drain either way
|
|
493
582
|
out.discarded++;
|
|
494
583
|
} else {
|
|
495
|
-
const res = await this.remote.fetch(item.url, { headers: { accept: '*/*' } });
|
|
496
584
|
// Same rule as the drain: a read we could not make is not a Create to
|
|
497
|
-
// be dropped.
|
|
498
|
-
|
|
499
|
-
const
|
|
585
|
+
// be dropped. readItem throws on anything but 404, so it counts as
|
|
586
|
+
// failed and stays for the next pass.
|
|
587
|
+
const got = await podInbox.readItem(this.remote, item.url, { maxBytes: MAX_ITEM_BYTES, readCapped });
|
|
588
|
+
const activity = got.raw === null ? null : (() => { try { return JSON.parse(got.raw); } catch { return null; } })();
|
|
500
589
|
// A Create is the content the owner just asked to be rid of. Anything
|
|
501
590
|
// else changes state and is applied exactly as a drain would.
|
|
502
591
|
if (keepConcerning) {
|
|
@@ -512,7 +601,7 @@ export class Intake {
|
|
|
512
601
|
this.log(`state not written — stopping the prune with ${older.length - out.applied - out.dropped - out.discarded} left`);
|
|
513
602
|
break;
|
|
514
603
|
}
|
|
515
|
-
await this.remote
|
|
604
|
+
await podInbox.dropHandledItem(this.remote, item.url);
|
|
516
605
|
}
|
|
517
606
|
this._clearAttempt(item.url);
|
|
518
607
|
await new Promise(r => setTimeout(r, DELETE_GAP_MS));
|
|
@@ -595,7 +684,7 @@ export class Intake {
|
|
|
595
684
|
this._pruneAttempts();
|
|
596
685
|
let all;
|
|
597
686
|
try {
|
|
598
|
-
all = await this.remote
|
|
687
|
+
all = await podInbox.list(this.remote, this.urls);
|
|
599
688
|
this.drainFailures = 0;
|
|
600
689
|
} catch (e) {
|
|
601
690
|
this._backOff(`inbox unreadable (${e.message})`);
|
|
@@ -637,7 +726,7 @@ export class Intake {
|
|
|
637
726
|
return false;
|
|
638
727
|
}
|
|
639
728
|
for (const url of pending.splice(0)) {
|
|
640
|
-
if (!await this.remote
|
|
729
|
+
if (!await podInbox.dropHandledItem(this.remote, url)) {
|
|
641
730
|
// Still in the mailbox. Handling is idempotent so seeing it again is
|
|
642
731
|
// harmless, but counting it would clear the attempt record and report
|
|
643
732
|
// progress that did not happen.
|
|
@@ -667,17 +756,14 @@ export class Intake {
|
|
|
667
756
|
}
|
|
668
757
|
let activity = null;
|
|
669
758
|
try {
|
|
670
|
-
const
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
|
|
678
|
-
// Capped rather than res.text(): the listing's size is advisory, and the
|
|
679
|
-
// inbox is public-Append, so the body is whatever a stranger chose.
|
|
680
|
-
const raw = res.status < 400 ? await readCapped(res, MAX_ITEM_BYTES) : null;
|
|
759
|
+
const got = await podInbox.readItem(this.remote, url, { maxBytes: MAX_ITEM_BYTES, readCapped });
|
|
760
|
+
// readItem carries the rule that matters here: a pod that would not
|
|
761
|
+
// GIVE us the item has told us nothing about it, so anything but a 404
|
|
762
|
+
// throws rather than reading as an empty body. Reading a 500 as empty
|
|
763
|
+
// made it "unparsable JSON" — a REJECTION, dead-lettered with both
|
|
764
|
+
// `activity` and `raw` null and then DELETEd, destroying a delivery on
|
|
765
|
+
// a transient fault with no record of what it had been.
|
|
766
|
+
const raw = got.raw;
|
|
681
767
|
try { activity = raw ? JSON.parse(raw) : null; } catch { /* kept raw for the dead letter */ }
|
|
682
768
|
// A gateway that verified this delivery left a receipt beside it. Read
|
|
683
769
|
// it only when a gateway is configured (no config → no fetch, so an
|
|
@@ -690,7 +776,7 @@ export class Intake {
|
|
|
690
776
|
if (!rejection) await this._maybeForward(activity); // §7.1.2, only what we accepted
|
|
691
777
|
if (rejection) {
|
|
692
778
|
this.store.addDeadLetter({
|
|
693
|
-
inboxUrl: url, reason: rejection, activity,
|
|
779
|
+
inboxUrl: url, reason: rejection, activity: trimActivity(activity),
|
|
694
780
|
...(activity ? {} : { raw: raw?.slice(0, 2000) ?? null }),
|
|
695
781
|
});
|
|
696
782
|
this.log(`rejected (${rejection}) — dead-lettered: ${url}`);
|
|
@@ -700,7 +786,7 @@ export class Intake {
|
|
|
700
786
|
const n = this._bumpAttempt(url, e.message);
|
|
701
787
|
this.log(`inbox item ${url} attempt ${n}/${MAX_ITEM_ATTEMPTS}: ${e.message}`);
|
|
702
788
|
if (n >= MAX_ITEM_ATTEMPTS) {
|
|
703
|
-
this.store.addDeadLetter({ inboxUrl: url, reason: `failed ${n}x: ${e.message}`, activity });
|
|
789
|
+
this.store.addDeadLetter({ inboxUrl: url, reason: `failed ${n}x: ${e.message}`, activity: trimActivity(activity) });
|
|
704
790
|
// The dead letter IS the record of this item — deleting before it is
|
|
705
791
|
// written down would lose the only evidence it ever arrived, so it
|
|
706
792
|
// goes through the same commit-then-delete batch as everything else.
|
|
@@ -765,6 +851,23 @@ export class Intake {
|
|
|
765
851
|
let doc = null;
|
|
766
852
|
try { doc = JSON.parse(await readCapped(res)); }
|
|
767
853
|
catch (e) { this.log(`fetch ${url}: unreadable as JSON — ${e.message}`); return null; }
|
|
854
|
+
// A document is only evidence about its OWN origin.
|
|
855
|
+
//
|
|
856
|
+
// signedFetch follows redirects and hands back only the final response, and
|
|
857
|
+
// every caller checks `doc.id` against the URL it ASKED for — so an open
|
|
858
|
+
// redirector on a victim's origin let an attacker's server answer for a
|
|
859
|
+
// victim's URL and be believed by that check.
|
|
860
|
+
//
|
|
861
|
+
// Only a redirect that CROSSED AN ORIGIN is refused here. A same-origin
|
|
862
|
+
// redirect is ordinary (canonicalisation, trailing slashes), and a document
|
|
863
|
+
// fetched with no redirect at all is still returned whatever it claims —
|
|
864
|
+
// the callers reject it on id, and they say something more useful about it
|
|
865
|
+
// than this could ("actor id mismatch", "object not verifiable content").
|
|
866
|
+
const landed = res.finalUrl || url;
|
|
867
|
+
if (!sameOrigin(landed, url) && doc && doc.id && !sameOrigin(doc.id, landed)) {
|
|
868
|
+
this.log(`fetch ${url}: redirected to ${landed}, which is not where ${doc.id} lives — refused`);
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
768
871
|
// Every actor type, not just Person. A Group was fetched, used and thrown
|
|
769
872
|
// away, so nothing knew its preferredUsername — and a client rendering it
|
|
770
873
|
// fell back to the last path segment of the actor URL, which is the literal
|
|
@@ -785,6 +888,8 @@ export class Intake {
|
|
|
785
888
|
}
|
|
786
889
|
|
|
787
890
|
sameOrigin(a, b) { return sameOrigin(a, b); }
|
|
891
|
+
// Overridable in tests the same way sameOrigin is.
|
|
892
|
+
sameIdentity(a, b) { return sameIdentity(a, b); }
|
|
788
893
|
|
|
789
894
|
// Have we ever heard of this actor or object? Answered entirely from local
|
|
790
895
|
// state, so asking costs nothing. It is what stops a stranger's Delete or
|
|
@@ -823,15 +928,26 @@ export class Intake {
|
|
|
823
928
|
const secret = this.gatewaySecret();
|
|
824
929
|
if (!secret) return null;
|
|
825
930
|
try {
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const receipt = JSON.parse(await readCapped(res, 64 * 1024));
|
|
931
|
+
const { readCapped: cap } = await import('./safefetch.mjs');
|
|
932
|
+
const receipt = await podInbox.readDeliveryReceipt(this.remote, itemUrl, { maxBytes: 64 * 1024, readCapped: cap });
|
|
933
|
+
if (!receipt) return null;
|
|
830
934
|
const { verifyReceipt } = await import('./httpsig.mjs');
|
|
831
935
|
return verifyReceipt(receipt, secret) ? receipt : null;
|
|
832
936
|
} catch { return null; }
|
|
833
937
|
}
|
|
834
938
|
|
|
939
|
+
// Whether a receipt says anything about THIS actor. Verified-and-about-someone
|
|
940
|
+
// -else is worth exactly as much as unverified, and is treated the same way:
|
|
941
|
+
// the drain's verify-by-dereference still stands behind it.
|
|
942
|
+
receiptVouchesFor(receipt, actor) {
|
|
943
|
+
if (!receipt?.verified) return false;
|
|
944
|
+
if (!receipt.actor || receipt.actor !== actor) return false;
|
|
945
|
+
// A missing keyId with verified:true cannot come from our own door
|
|
946
|
+
// (makeReceipt fills both from the same key), so refuse rather than waive.
|
|
947
|
+
if (!receipt.keyId || !sameOrigin(receipt.keyId, actor)) return false;
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
|
|
835
951
|
async handle(activity, receipt = null) { // eslint-disable-line no-unused-vars
|
|
836
952
|
const actor = typeof activity.actor === 'string' ? activity.actor : activity.actor?.id;
|
|
837
953
|
if (!actor) return 'no actor';
|
|
@@ -844,6 +960,12 @@ export class Intake {
|
|
|
844
960
|
if (!httpUrl(actor)) return `actor is not an http(s) URL (${actor})`;
|
|
845
961
|
if (this.store.isBlocked(actor)) return `blocked sender (${actor})`;
|
|
846
962
|
|
|
963
|
+
// Whether the door vouched for this sender — read by the moderation queue
|
|
964
|
+
// just below as well as by the arms further down, so it is settled here,
|
|
965
|
+
// before its first use.
|
|
966
|
+
const trusted = this.receiptVouchesFor(receipt, actor)
|
|
967
|
+
&& this.store.getConfig()?.gateway?.mode === 'trust';
|
|
968
|
+
|
|
847
969
|
// FEP-1b12 moderation from a LISTED moderator. A delivery proves nothing
|
|
848
970
|
// about its sender, which is why these are QUEUED for the operator rather
|
|
849
971
|
// than run on arrival — the queue is where a claimed moderator's word
|
|
@@ -852,18 +974,26 @@ export class Intake {
|
|
|
852
974
|
if (this.config.kind === 'group'
|
|
853
975
|
&& (this.config.moderators || []).includes(actor)
|
|
854
976
|
&& this.isModerationAsk(activity)) {
|
|
855
|
-
return this.queueModeration(activity, actor);
|
|
977
|
+
return this.queueModeration(activity, actor, { trusted });
|
|
856
978
|
}
|
|
857
979
|
|
|
858
|
-
//
|
|
859
|
-
//
|
|
860
|
-
|
|
980
|
+
// `trusted` was settled above, before the moderation queue reads it. Why it
|
|
981
|
+
// is not simply `receipt.verified`: that says the door checked a signature
|
|
982
|
+
// and the signature was good; it does NOT say whose. The door reports the
|
|
983
|
+
// signing key's owner separately, in `receipt.actor`, and nothing here used
|
|
984
|
+
// to read it — so ANY valid fediverse signing key, over an activity whose
|
|
985
|
+
// `actor` field named someone else entirely, arrived as trusted. In trust
|
|
986
|
+
// mode that is one delivery to evict any of your followers, or to have a
|
|
987
|
+
// Follow naming a third party auto-accepted. See receiptVouchesFor, which
|
|
988
|
+
// also holds the keyId to the actor's origin: a key document is fetched
|
|
989
|
+
// from wherever its id points, so one hosted elsewhere that merely CLAIMS
|
|
990
|
+
// `owner: <you>` would otherwise bind.
|
|
861
991
|
|
|
862
992
|
switch (activity.type) {
|
|
863
993
|
case 'Follow': return this.onFollow(activity, actor, { trusted });
|
|
864
994
|
case 'Undo': return this.onUndo(activity, actor, { trusted });
|
|
865
995
|
case 'Create': return this.onCreate(activity, actor);
|
|
866
|
-
case 'Accept': return this.onAccept(activity, actor);
|
|
996
|
+
case 'Accept': return this.onAccept(activity, actor, { trusted });
|
|
867
997
|
case 'Like': case 'Announce': {
|
|
868
998
|
// FEP-1b12: a group Announces the member's whole Create, not the note.
|
|
869
999
|
// Without unwrapping we try to ingest a Create as if it were a Note and
|
|
@@ -897,7 +1027,7 @@ export class Intake {
|
|
|
897
1027
|
}
|
|
898
1028
|
case 'Delete': return this.onDelete(activity, actor);
|
|
899
1029
|
case 'Update': return this.onUpdate(activity, actor);
|
|
900
|
-
case 'Reject': return this.onReject(activity, actor);
|
|
1030
|
+
case 'Reject': return this.onReject(activity, actor, { trusted });
|
|
901
1031
|
case 'Move': return this.onMove(activity, actor);
|
|
902
1032
|
case 'Add': case 'Remove': return this.onAddRemove(activity, actor);
|
|
903
1033
|
default: this.log(`ignored ${activity.type} from ${actor}`);
|
|
@@ -929,17 +1059,42 @@ export class Intake {
|
|
|
929
1059
|
|
|
930
1060
|
// Held, not run: one entry per distinct ask, capped, waiting for the
|
|
931
1061
|
// operator to apply or dismiss it (social.applyModeration).
|
|
932
|
-
|
|
1062
|
+
// A moderator's WORD, not their proof. `actor` is a field in an unsigned
|
|
1063
|
+
// body and a moderator's URL is public, so anyone can claim to be one — which
|
|
1064
|
+
// is exactly why these are QUEUED for the operator rather than run. What was
|
|
1065
|
+
// missing is that the queue did not say which is which, and a stranger could
|
|
1066
|
+
// fill all 200 slots and push the real asks out.
|
|
1067
|
+
//
|
|
1068
|
+
// So: the entry records whether the door vouched for the sender, and when the
|
|
1069
|
+
// queue is full the UNVERIFIED entries are what get dropped. A real
|
|
1070
|
+
// moderator's ask cannot be crowded out by someone impersonating them.
|
|
1071
|
+
queueModeration(activity, actor, { trusted = false } = {}) {
|
|
933
1072
|
const q = this.store.read('modqueue.json', []);
|
|
934
1073
|
const objectId = typeof activity.object === 'string' ? activity.object : activity.object?.id;
|
|
935
1074
|
const key = [activity.type, actor, objectId || JSON.stringify(activity.object || null)].join(' ');
|
|
936
|
-
|
|
1075
|
+
const seen = q.find(e => e.key === key);
|
|
1076
|
+
if (seen) {
|
|
1077
|
+
// The same ask arriving verified is worth more than the copy we hold.
|
|
1078
|
+
if (trusted && !seen.verified) {
|
|
1079
|
+
seen.verified = true;
|
|
1080
|
+
this.store.write('modqueue.json', q);
|
|
1081
|
+
}
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
937
1084
|
q.unshift({
|
|
938
1085
|
key, id: (this.serial++).toString(36) + '-' + q.length,
|
|
939
|
-
type: activity.type, moderator: actor, activity
|
|
1086
|
+
type: activity.type, moderator: actor, activity: trimActivity(activity),
|
|
1087
|
+
verified: !!trusted, at: new Date().toISOString(),
|
|
940
1088
|
});
|
|
941
|
-
|
|
942
|
-
|
|
1089
|
+
let kept = q;
|
|
1090
|
+
if (kept.length > MAX_MODQUEUE) {
|
|
1091
|
+
const verified = kept.filter(e => e.verified);
|
|
1092
|
+
const rest = kept.filter(e => !e.verified);
|
|
1093
|
+
// Verified first, then the newest unverified up to the cap.
|
|
1094
|
+
kept = [...verified, ...rest].slice(0, MAX_MODQUEUE);
|
|
1095
|
+
}
|
|
1096
|
+
this.store.write('modqueue.json', kept);
|
|
1097
|
+
this.log(`moderation queued from ${actor}${trusted ? '' : ' (unverified)'}: ${activity.type} ${objectId || ''}`);
|
|
943
1098
|
}
|
|
944
1099
|
|
|
945
1100
|
// §7.6 Add / §7.9 Remove. The side effect would be to add or remove the object
|
|
@@ -992,7 +1147,7 @@ export class Intake {
|
|
|
992
1147
|
if (!reqs.some(r => r.actor === actor)) {
|
|
993
1148
|
reqs.unshift({
|
|
994
1149
|
actor, inbox: doc.inbox, sharedInbox: doc.endpoints?.sharedInbox,
|
|
995
|
-
activity, at: new Date().toISOString(),
|
|
1150
|
+
activity: trimActivity(activity), at: new Date().toISOString(),
|
|
996
1151
|
});
|
|
997
1152
|
this.store.setRequests(reqs.slice(0, 500));
|
|
998
1153
|
this.store.addNotification({ type: 'follow-request', actor });
|
|
@@ -1068,7 +1223,18 @@ export class Intake {
|
|
|
1068
1223
|
// operator's queue forever asking about someone who left.
|
|
1069
1224
|
if (!rec) {
|
|
1070
1225
|
const reqs = this.store.getRequests();
|
|
1071
|
-
|
|
1226
|
+
const pending = reqs.find(r => r.actor === actor);
|
|
1227
|
+
if (pending) {
|
|
1228
|
+
// Bound the same way the follower eviction below is, and for the same
|
|
1229
|
+
// reason: `actor` is a field in an unsigned body, so without a match
|
|
1230
|
+
// anyone could withdraw anyone else's waiting request. The victim's
|
|
1231
|
+
// server believes the Follow is still pending and never resends, so
|
|
1232
|
+
// they simply never get followed and nobody sees why.
|
|
1233
|
+
const theirs = pending.activity?.id;
|
|
1234
|
+
if (!trusted && (!theirs || named !== theirs)) {
|
|
1235
|
+
this.log(`Undo from ${actor} does not name the request we hold — ignored`);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1072
1238
|
this.store.setRequests(reqs.filter(r => r.actor !== actor));
|
|
1073
1239
|
await this.republish({ pending: true });
|
|
1074
1240
|
this.log(`join request withdrawn: ${actor}`);
|
|
@@ -1161,6 +1327,21 @@ export class Intake {
|
|
|
1161
1327
|
async _maybeForward(activity) {
|
|
1162
1328
|
if (!activity || typeof activity !== 'object') return;
|
|
1163
1329
|
if (!FORWARDABLE.has(activity.type)) return; // see FORWARDABLE
|
|
1330
|
+
// Forwarding signs somebody else's activity with OUR key and pushes it to
|
|
1331
|
+
// every follower we have. That is a lot to do on the word of an unsigned
|
|
1332
|
+
// POST, so it is narrowed three ways:
|
|
1333
|
+
//
|
|
1334
|
+
// 1. Only the types whose object this drain actually DEREFERENCED at the
|
|
1335
|
+
// author's own origin. A Create/Update/Delete went through onCreate /
|
|
1336
|
+
// onUpdate / onDelete, which fetch and check the author; a Like, an
|
|
1337
|
+
// Announce or an Undo is believed on the envelope alone, so relaying
|
|
1338
|
+
// one made us a signed relay for anything a stranger cared to write.
|
|
1339
|
+
// 2. The actor has to be someone we already know of. A complete stranger
|
|
1340
|
+
// addressing our followers collection is not a conversation we are
|
|
1341
|
+
// party to.
|
|
1342
|
+
// 3. A budget per drain, so a flood cannot turn one sweep into thousands
|
|
1343
|
+
// of outbound deliveries under our signature.
|
|
1344
|
+
if (!FORWARD_TYPES.has(activity.type)) return;
|
|
1164
1345
|
try {
|
|
1165
1346
|
const audience = []
|
|
1166
1347
|
.concat(activity.to || [], activity.cc || [], activity.audience || [])
|
|
@@ -1169,6 +1350,14 @@ export class Intake {
|
|
|
1169
1350
|
if (!this._referencesOurObject(activity)) return; // not into a thread of ours
|
|
1170
1351
|
const actor = typeof activity.actor === 'string' ? activity.actor : activity.actor?.id;
|
|
1171
1352
|
if (actor === this.urls.actor) return; // our own; nothing to forward
|
|
1353
|
+
if (!this.known(actor)) {
|
|
1354
|
+
this.log(`not forwarding ${activity.type} from ${actor}: nobody we know of`);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
if (this._forwardBudget <= 0) {
|
|
1358
|
+
this.log(`not forwarding ${activity.type}: this drain's forwarding budget is spent`);
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1172
1361
|
const id = typeof activity.id === 'string' ? activity.id : null;
|
|
1173
1362
|
if (!id) return;
|
|
1174
1363
|
const forwarded = this.store.read('forwarded.json', []);
|
|
@@ -1179,6 +1368,7 @@ export class Intake {
|
|
|
1179
1368
|
.map(f => f.sharedInbox || f.inbox)
|
|
1180
1369
|
.filter(Boolean))];
|
|
1181
1370
|
if (!inboxes.length) return;
|
|
1371
|
+
this._forwardBudget -= 1;
|
|
1182
1372
|
await this.deliverer.deliverToAll(inboxes, activity);
|
|
1183
1373
|
this.store.write('forwarded.json', [...forwarded, id].slice(-MAX_FORWARDED));
|
|
1184
1374
|
this.log(`forwarded ${activity.type} ${id} to ${inboxes.length} follower inbox(es)`);
|
|
@@ -1206,7 +1396,7 @@ export class Intake {
|
|
|
1206
1396
|
const objectId = typeof activity.object === 'string' ? activity.object : activity.object?.id;
|
|
1207
1397
|
if (!objectId) return 'Create without object id';
|
|
1208
1398
|
if (this.store.isBlocked(objectId)) return `blocked domain (${objectId})`;
|
|
1209
|
-
if (!this.
|
|
1399
|
+
if (!this.sameIdentity(objectId, actor)) return `object/actor identity mismatch (${objectId})`;
|
|
1210
1400
|
// The delivered copy is untrusted for CONTENT, but its addressing is
|
|
1211
1401
|
// enough to decide whether to bother fetching the origin's copy.
|
|
1212
1402
|
const envelope = typeof activity.object === 'object' ? { ...activity, ...activity.object } : activity;
|
|
@@ -1296,8 +1486,22 @@ export class Intake {
|
|
|
1296
1486
|
// Wrap the member's own activity when we have it; a bare note URL is the
|
|
1297
1487
|
// fallback, and renders as a plain boost rather than a group carry. The
|
|
1298
1488
|
// group names itself as the audience (FEP-1b12).
|
|
1489
|
+
//
|
|
1490
|
+
// `activity` is the envelope as DELIVERED — a document the sender wrote,
|
|
1491
|
+
// which the group would otherwise re-sign and hand to every follower with
|
|
1492
|
+
// whatever addressing, tags and object body it carried. Only the note id was
|
|
1493
|
+
// ever verified (ingestNote fetched it from the author's origin and checked
|
|
1494
|
+
// the attribution), so only the note id is safe to pass on: send the bare
|
|
1495
|
+
// id unless the wrapper's own object id agrees with what we verified.
|
|
1496
|
+
const wrapperObject = (a) => {
|
|
1497
|
+
const inner = a?.object;
|
|
1498
|
+
const id = typeof inner === 'string' ? inner : inner?.id;
|
|
1499
|
+
return id === noteId ? a : null;
|
|
1500
|
+
};
|
|
1299
1501
|
const act = announceActivity({
|
|
1300
|
-
urls: this.urls,
|
|
1502
|
+
urls: this.urls,
|
|
1503
|
+
object: wrapperObject(activity) || wrapperObject(held?.activity) || noteId,
|
|
1504
|
+
serial: this.serial++,
|
|
1301
1505
|
audience: this.urls.actor,
|
|
1302
1506
|
});
|
|
1303
1507
|
await this.deliverer.deliverToAll(inboxes, act);
|
|
@@ -1432,6 +1636,20 @@ export class Intake {
|
|
|
1432
1636
|
// somebody else's boost, or through a hashtag feed.
|
|
1433
1637
|
if (this.store.isBlocked(author)) return `blocked author (${author})`;
|
|
1434
1638
|
|
|
1639
|
+
// The ENVELOPE said this concerns us. The envelope is a document a stranger
|
|
1640
|
+
// wrote: onCreate reads addressing off the delivered copy to decide whether
|
|
1641
|
+
// fetching is worth it, and nothing re-asked the question of the copy that
|
|
1642
|
+
// came back from the author's own server. So anyone could take any public
|
|
1643
|
+
// post, address the delivery to us, and have it filed as "X mentioned you"
|
|
1644
|
+
// — or, to a group, have a member's post carried to every follower when the
|
|
1645
|
+
// member never sent it there.
|
|
1646
|
+
//
|
|
1647
|
+
// A boost is exempt (`via`): someone we follow deliberately putting a post
|
|
1648
|
+
// in front of us is the whole point, and the note will not address us.
|
|
1649
|
+
if (!via && !this.concernsUs(note, author)) {
|
|
1650
|
+
return `the note its own server serves does not address us (${objectId})`;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1435
1653
|
// An answer to one of our polls is a number on a document, not a post. It
|
|
1436
1654
|
// arrives as an ordinary reply naming an option and carrying nothing else,
|
|
1437
1655
|
// so filing it as one would put a blank entry in the thread and ring the
|
|
@@ -1467,24 +1685,13 @@ export class Intake {
|
|
|
1467
1685
|
const followed = via || known || (!known && await this.isCoMember(author));
|
|
1468
1686
|
const kind = followed ? 'timeline' : 'mention';
|
|
1469
1687
|
|
|
1470
|
-
// `published` comes from a document at someone else's origin, and its first
|
|
1471
|
-
// ten characters become the leading component of a storage path. A date is
|
|
1472
|
-
// a date or it is not used: a value carrying `../` would otherwise be a
|
|
1473
|
-
// remote party choosing where we write.
|
|
1474
|
-
const day = String(note.published || '').slice(0, 10);
|
|
1475
|
-
const slug = (/^\d{4}-\d{2}-\d{2}$/.test(day) ? day : new Date().toISOString().slice(0, 10)) + '-' +
|
|
1476
|
-
(await import('node:crypto')).createHash('sha256').update(note.id).digest('hex').slice(0, 8);
|
|
1477
|
-
if (kind === 'timeline') {
|
|
1478
|
-
await this.local.writeNote('timeline', slug, {
|
|
1479
|
-
noteId: note.id, actor: author, published: note.published, content,
|
|
1480
|
-
inReplyTo: note.inReplyTo, attachments,
|
|
1481
|
-
});
|
|
1482
|
-
}
|
|
1483
1688
|
// Mastodon carries a thread's mentions into every reply, which is the only
|
|
1484
1689
|
// reason a reply ever reaches a group. Keep them so our composer can too.
|
|
1485
1690
|
const mentions = [].concat(note.tag || [])
|
|
1486
1691
|
.filter(t => t?.type === 'Mention' && t.href && t.name)
|
|
1487
|
-
.
|
|
1692
|
+
.slice(0, MAX_MENTIONS)
|
|
1693
|
+
.map(t => ({ href: httpOnly(String(t.href).slice(0, MAX_URL_CHARS)), name: String(t.name).slice(0, 256) }))
|
|
1694
|
+
.filter(m => m.href);
|
|
1488
1695
|
const emojis = emojisOf(note);
|
|
1489
1696
|
const poll = pollOf(note);
|
|
1490
1697
|
// Explicitly addressed, but to nobody public and to no followers
|
|
@@ -1507,7 +1714,6 @@ export class Intake {
|
|
|
1507
1714
|
...(poll ? { poll } : {}),
|
|
1508
1715
|
...(emojis.length ? { emojis } : {}),
|
|
1509
1716
|
...(mentions.length ? { mentions } : {}),
|
|
1510
|
-
...(kind === 'timeline' ? { slug } : {}),
|
|
1511
1717
|
...(attachments.length ? { attachments } : {}),
|
|
1512
1718
|
...(via ? { via } : {}),
|
|
1513
1719
|
});
|
|
@@ -1546,7 +1752,7 @@ export class Intake {
|
|
|
1546
1752
|
async onDelete(activity, actor) {
|
|
1547
1753
|
const objectId = typeof activity.object === 'string' ? activity.object : activity.object?.id;
|
|
1548
1754
|
if (!objectId) return 'Delete without object id';
|
|
1549
|
-
if (!this.
|
|
1755
|
+
if (!this.sameIdentity(objectId, actor)) return `Delete crosses identities (${objectId})`;
|
|
1550
1756
|
// `objectId === actor` is NOT evidence we care: it is true of EVERY account
|
|
1551
1757
|
// deletion, and Mastodon broadcasts those constantly. Taking it as known
|
|
1552
1758
|
// meant a signed dereference to a stranger's server for each one.
|
|
@@ -1592,13 +1798,6 @@ export class Intake {
|
|
|
1592
1798
|
if (s.announceActivity) {
|
|
1593
1799
|
await this.retract(s.noteId, { collect }).catch(e => this.log(`retract: ${e.message}`));
|
|
1594
1800
|
}
|
|
1595
|
-
if (s.slug) {
|
|
1596
|
-
// Said out loud rather than swallowed: the status row is about to go, so
|
|
1597
|
-
// a failure here leaves the note document in the private tree with
|
|
1598
|
-
// nothing indexing it, and no other record that it is there.
|
|
1599
|
-
await this.local.delete(this.local.fedi + 'timeline/' + s.slug)
|
|
1600
|
-
.catch(e => this.log(`forget ${s.noteId}: its RDF note could NOT be removed (${e.message})`));
|
|
1601
|
-
}
|
|
1602
1801
|
this.store.removeStatus(s.noteId);
|
|
1603
1802
|
}
|
|
1604
1803
|
|
|
@@ -1629,7 +1828,7 @@ export class Intake {
|
|
|
1629
1828
|
async onUpdate(activity, actor) {
|
|
1630
1829
|
const objectId = typeof activity.object === 'string' ? activity.object : activity.object?.id;
|
|
1631
1830
|
if (!objectId) return 'Update without object id';
|
|
1632
|
-
if (!this.
|
|
1831
|
+
if (!this.sameIdentity(objectId, actor)) return `Update crosses identities (${objectId})`;
|
|
1633
1832
|
if (objectId === actor) { // display name, avatar, bio
|
|
1634
1833
|
// The same guard onDelete has, for the same reason: the inbox is
|
|
1635
1834
|
// public-Append, so without it anyone can name any host and make us spend
|
|
@@ -1664,12 +1863,6 @@ export class Intake {
|
|
|
1664
1863
|
poll: { ...freshPoll, voted: !!s.poll?.voted, ownVotes: s.poll?.ownVotes || [] },
|
|
1665
1864
|
} : {}),
|
|
1666
1865
|
});
|
|
1667
|
-
if (s.slug) {
|
|
1668
|
-
await this.local.writeNote('timeline', s.slug, {
|
|
1669
|
-
noteId: note.id, actor: s.actor, published: note.published, content,
|
|
1670
|
-
inReplyTo: note.inReplyTo, attachments,
|
|
1671
|
-
}).catch(e => this.log(`rewrite ${s.slug}: ${e.message}`));
|
|
1672
|
-
}
|
|
1673
1866
|
this.log(`edited upstream: ${objectId}`);
|
|
1674
1867
|
}
|
|
1675
1868
|
|
|
@@ -1695,11 +1888,11 @@ export class Intake {
|
|
|
1695
1888
|
// collection, and rewriting on top of one erases every reply already
|
|
1696
1889
|
// recorded. getJson returns null only for a genuine 404 — the document does
|
|
1697
1890
|
// not exist yet — and throws otherwise, which the caller logs and retries.
|
|
1698
|
-
const cur = await this.remote
|
|
1891
|
+
const cur = await podNotes.readReplies(this.remote, url);
|
|
1699
1892
|
const items = Array.isArray(cur?.items) ? cur.items : [];
|
|
1700
1893
|
if (items.includes(replyId)) return;
|
|
1701
1894
|
items.push(replyId);
|
|
1702
|
-
await this.remote
|
|
1895
|
+
await podNotes.writeReplies(this.remote, url, collection(url, items.slice(-MAX_REPLIES_RECORDED)));
|
|
1703
1896
|
this.log(`reply recorded on ${parentId}`);
|
|
1704
1897
|
}
|
|
1705
1898
|
|
|
@@ -1707,11 +1900,32 @@ export class Intake {
|
|
|
1707
1900
|
// has recorded that we do not follow them; ours went on saying we did, and
|
|
1708
1901
|
// published it — so the two disagreed permanently, and a retry would never
|
|
1709
1902
|
// come because as far as they are concerned the question was answered.
|
|
1710
|
-
|
|
1903
|
+
//
|
|
1904
|
+
// It has to answer the Follow we actually SENT. Only the type was checked, so
|
|
1905
|
+
// one Append per account you follow — from anyone, naming no particular
|
|
1906
|
+
// follow — severed every one of them at once, and silently: their server
|
|
1907
|
+
// never hears about it, so nothing ever retries and nothing looks wrong until
|
|
1908
|
+
// the timeline goes quiet. `followActivity` is stored by followActor
|
|
1909
|
+
// (lib/social.mjs) for exactly this kind of comparison.
|
|
1910
|
+
async onReject(activity, actor, { trusted = false } = {}) {
|
|
1711
1911
|
if (activity.object?.type && activity.object.type !== 'Follow') return;
|
|
1712
1912
|
const contacts = this.store.getContacts();
|
|
1713
1913
|
const rec = contacts.following.find(f => f.actor === actor);
|
|
1714
1914
|
if (!rec) return; // nothing of ours to undo
|
|
1915
|
+
const named = typeof activity.object === 'string' ? activity.object : activity.object?.id;
|
|
1916
|
+
const ours = rec.followActivity?.id;
|
|
1917
|
+
// A gateway receipt bound to this actor is the other way to believe it —
|
|
1918
|
+
// the door checked a signature and said whose (see receiptVouchesFor).
|
|
1919
|
+
if (!trusted) {
|
|
1920
|
+
if (!ours) {
|
|
1921
|
+
this.log(`Reject from ${actor}: no follow id on record to match it against — ignored`);
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
if (named !== ours) {
|
|
1925
|
+
this.log(`Reject from ${actor} answers ${named || 'nothing'}, not the follow we sent — ignored`);
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1715
1929
|
contacts.following = contacts.following.filter(f => f.actor !== actor);
|
|
1716
1930
|
this.store.setContacts(contacts);
|
|
1717
1931
|
await this.republish({ following: true, pending: true });
|
|
@@ -1741,7 +1955,7 @@ export class Intake {
|
|
|
1741
1955
|
this.log(`${actor} moved to ${target} — follow the new account to keep seeing them`);
|
|
1742
1956
|
}
|
|
1743
1957
|
|
|
1744
|
-
async onAccept(activity, actor) {
|
|
1958
|
+
async onAccept(activity, actor, { trusted = false } = {}) {
|
|
1745
1959
|
const contacts = this.store.getContacts();
|
|
1746
1960
|
const rec = contacts.following.find(f => f.actor === actor);
|
|
1747
1961
|
// It has to answer the Follow we actually sent. followActor stores that
|
|
@@ -1750,8 +1964,11 @@ export class Intake {
|
|
|
1750
1964
|
// including one answering a Follow we never made.
|
|
1751
1965
|
const named = typeof activity.object === 'string' ? activity.object : activity.object?.id;
|
|
1752
1966
|
const ours = rec?.followActivity?.id;
|
|
1753
|
-
|
|
1754
|
-
|
|
1967
|
+
// `named &&` used to be part of this, so an Accept naming NOTHING sailed
|
|
1968
|
+
// past — which is the easy one to send, and it marks a request to a locked
|
|
1969
|
+
// account as accepted when it is still sitting in their queue.
|
|
1970
|
+
if (ours && named !== ours && !trusted) {
|
|
1971
|
+
this.log(`Accept from ${actor} answers ${named || 'nothing'}, not the follow we sent — ignored`);
|
|
1755
1972
|
return;
|
|
1756
1973
|
}
|
|
1757
1974
|
if (rec && !rec.accepted) {
|