fedipod-server 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claims.js +1 -1
- package/dist/handler.d.ts +1 -1
- package/dist/handler.js +35 -2
- package/dist/handler.jsonld +1 -1
- package/lib/admin.mjs +3 -3
- package/lib/c2s.mjs +27 -2
- package/lib/deliver.mjs +6 -0
- package/lib/embed.mjs +1 -0
- package/lib/export-collections.mjs +1 -1
- package/lib/front-core.mjs +272 -49
- package/lib/gateway-core.mjs +3 -7
- package/lib/httpsig.mjs +33 -2
- package/lib/import.mjs +1 -1
- package/lib/intake.mjs +357 -118
- package/lib/links.mjs +6 -32
- package/lib/mastoapi.mjs +449 -38
- package/lib/migrate.mjs +4 -18
- package/lib/pod/README.md +61 -0
- package/lib/pod/actor.mjs +96 -0
- package/lib/pod/collection.mjs +77 -0
- package/lib/pod/containers.mjs +136 -0
- package/lib/pod/discovery.mjs +46 -0
- package/lib/pod/featured.mjs +18 -0
- package/lib/pod/followers.mjs +24 -0
- package/lib/pod/following.mjs +10 -0
- package/lib/pod/http.mjs +74 -0
- package/lib/pod/inbox.mjs +144 -0
- package/lib/pod/links.mjs +35 -0
- package/lib/pod/media.mjs +27 -0
- package/lib/pod/notes.mjs +80 -0
- package/lib/pod/notifications.mjs +89 -0
- package/lib/pod/outbox.mjs +18 -0
- package/lib/pod/package.json +19 -0
- package/lib/pod/policy.mjs +58 -0
- package/lib/pod/private.mjs +19 -0
- package/lib/pod/root.mjs +63 -0
- package/lib/pod/state.mjs +39 -0
- package/lib/pod/transport.mjs +477 -0
- package/lib/pod/urls.mjs +71 -0
- package/lib/polls.mjs +105 -0
- package/lib/publisher.mjs +357 -188
- package/lib/remote.mjs +30 -395
- package/lib/social.mjs +20 -9
- package/lib/storage.mjs +2 -2
- package/lib/store.mjs +8 -1
- package/lib/tagfeed.mjs +19 -2
- package/lib/wire.mjs +87 -67
- package/package.json +1 -1
- package/run-agent.mjs +20 -39
- package/web/admin/admin.js +141 -74
- package/web/admin/bar.css +9 -13
- package/web/admin/bar.js +2 -2
- package/web/admin/client/client.js +1 -1
- package/web/admin/client/index.html +5 -6
- package/web/admin/index.html +53 -65
- package/web/admin/setup/index.html +2 -1
- package/web/admin/setup/setup.js +1 -1
- package/web/admin/tokens.css +56 -0
- package/web/app/README.md +77 -0
- package/web/app/admin-facade.mjs +549 -0
- package/web/app/agent.mjs +490 -0
- package/web/app/atproto-browser.mjs +86 -0
- package/web/app/boot.mjs +340 -0
- package/web/app/deliver-relay.mjs +72 -0
- package/web/app/dist/boot.js +34067 -0
- package/web/app/dist/boot.js.map +7 -0
- package/web/app/dist/sw.js +53745 -0
- package/web/app/dist/sw.js.map +7 -0
- package/web/app/fediacct-browser.mjs +146 -0
- package/web/app/idb-kv.mjs +35 -0
- package/web/app/index.html +203 -0
- package/web/app/index.html~ +152 -0
- package/web/app/keys-browser.mjs +46 -0
- package/web/app/keystore.mjs +99 -0
- package/web/app/oidc-session.mjs +189 -0
- package/web/app/pod-auth.mjs +248 -0
- package/web/app/pod-remote.mjs +84 -0
- package/web/app/shims/_globals.mjs +2 -0
- package/web/app/shims/fedify-sig.mjs +77 -0
- package/web/app/shims/node-crypto.mjs +113 -0
- package/web/app/shims/node-fs.mjs +17 -0
- package/web/app/shims/node-path.mjs +19 -0
- package/web/app/shims/node-url.mjs +7 -0
- package/web/app/shims/prelude.js +58 -0
- package/web/app/shims/safefetch.mjs +58 -0
- package/web/app/shims/web-push.mjs +14 -0
- package/web/app/signup.mjs +248 -0
- package/web/app/site/_headers +9 -0
- package/web/app/site/_redirects +21 -0
- package/web/app/site/admin/admin.js +1181 -0
- package/web/app/site/admin/bar.css +60 -0
- package/web/app/site/admin/bar.js +61 -0
- package/web/app/site/admin/client/client.js +66 -0
- package/web/app/site/admin/client/index.html +43 -0
- package/web/app/site/admin/index.html +556 -0
- package/web/app/site/admin/setup/index.html +191 -0
- package/web/app/site/admin/setup/setup.js +308 -0
- package/web/app/site/admin/tokens.css +56 -0
- package/web/app/site/admin/window.css +55 -0
- package/web/app/site/admin/window.js +136 -0
- package/web/app/site/app/404.html +40 -0
- package/web/app/site/app/_headers +2 -0
- package/web/app/site/app/apple-touch-icon.png +0 -0
- package/web/app/site/app/assets/ICONS-DIPJeU0C.js +2 -0
- package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +1 -0
- package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +183 -0
- package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +1 -0
- package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
- package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +2 -0
- package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +1 -0
- package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
- package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
- package/web/app/site/app/assets/chunk-aKtaBQYM.js +1 -0
- package/web/app/site/app/assets/compose-CFEx7wb4.js +2 -0
- package/web/app/site/app/assets/compose-CFEx7wb4.js.map +1 -0
- package/web/app/site/app/assets/compose-ZJN5xSb4.js +35 -0
- package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +1 -0
- package/web/app/site/app/assets/debug.module-YFQgwnIr.js +52 -0
- package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +1 -0
- package/web/app/site/app/assets/dom-p1YcJ5Rw.js +2 -0
- package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +1 -0
- package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
- package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
- package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
- package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
- package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +1 -0
- package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +1 -0
- package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +1 -0
- package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +1 -0
- package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +1 -0
- package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +1 -0
- package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +1 -0
- package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +1 -0
- package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +1 -0
- package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +1 -0
- package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +1 -0
- package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +1 -0
- package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +1 -0
- package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +1 -0
- package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +1 -0
- package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +1 -0
- package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +1 -0
- package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +1 -0
- package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +1 -0
- package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +1 -0
- package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +1 -0
- package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +1 -0
- package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +1 -0
- package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +1 -0
- package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +1 -0
- package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +1 -0
- package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +1 -0
- package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +1 -0
- package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +1 -0
- package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +1 -0
- package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +1 -0
- package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +1 -0
- package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +1 -0
- package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +1 -0
- package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +1 -0
- package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +1 -0
- package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +1 -0
- package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +1 -0
- package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +1 -0
- package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +1 -0
- package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +1 -0
- package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +1 -0
- package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +1 -0
- package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +1 -0
- package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +1 -0
- package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +1 -0
- package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +1 -0
- package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +1 -0
- package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +1 -0
- package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +1 -0
- package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +1 -0
- package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +1 -0
- package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +1 -0
- package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +1 -0
- package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +1 -0
- package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +1 -0
- package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +1 -0
- package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +1 -0
- package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +1 -0
- package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +1 -0
- package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +1 -0
- package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +1 -0
- package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +1 -0
- package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +1 -0
- package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +1 -0
- package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +1 -0
- package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +1 -0
- package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +1 -0
- package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +1 -0
- package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +1 -0
- package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +1 -0
- package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +1 -0
- package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +1 -0
- package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +1 -0
- package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +1 -0
- package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +1 -0
- package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +1 -0
- package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +1 -0
- package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +1 -0
- package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +1 -0
- package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +1 -0
- package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +1 -0
- package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +1 -0
- package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +1 -0
- package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +1 -0
- package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +1 -0
- package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +1 -0
- package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +1 -0
- package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +1 -0
- package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +1 -0
- package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +1 -0
- package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +1 -0
- package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +1 -0
- package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +1 -0
- package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +1 -0
- package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +1 -0
- package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +1 -0
- package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +1 -0
- package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +1 -0
- package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +1 -0
- package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +1 -0
- package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +1 -0
- package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +1 -0
- package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +1 -0
- package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +1 -0
- package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +1 -0
- package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +1 -0
- package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +1 -0
- package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +1 -0
- package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +1 -0
- package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +1 -0
- package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +1 -0
- package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +1 -0
- package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +1 -0
- package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +1 -0
- package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +1 -0
- package/web/app/site/app/assets/instances-BNPptnn-.json +333 -0
- package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +1 -0
- package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +1 -0
- package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +1 -0
- package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +1 -0
- package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +1 -0
- package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +1 -0
- package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +1 -0
- package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +1 -0
- package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +1 -0
- package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +1 -0
- package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +1 -0
- package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +1 -0
- package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +1 -0
- package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +1 -0
- package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +1 -0
- package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +1 -0
- package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +1 -0
- package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +1 -0
- package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +1 -0
- package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +1 -0
- package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +1 -0
- package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/tok-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +1 -0
- package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +1 -0
- package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/main-BdqNbG-a.js +26 -0
- package/web/app/site/app/assets/main-BdqNbG-a.js.map +1 -0
- package/web/app/site/app/assets/mock-home-DGhR__He.js +2 -0
- package/web/app/site/app/assets/mock-home-DGhR__He.js.map +1 -0
- package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
- package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
- package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
- package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +2 -0
- package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +1 -0
- package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +20 -0
- package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +1 -0
- package/web/app/site/app/assets/style-B1iLPTPR.css +2 -0
- package/web/app/site/app/assets/temml-DuyIQMei.js +97 -0
- package/web/app/site/app/assets/temml-DuyIQMei.js.map +1 -0
- package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +2 -0
- package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +1 -0
- package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
- package/web/app/site/app/assets/year-in-posts-DJv_star.js +3 -0
- package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +1 -0
- package/web/app/site/app/compose/index.html +20 -0
- package/web/app/site/app/favicon.ico +0 -0
- package/web/app/site/app/index.html +73 -0
- package/web/app/site/app/logo-192.png +0 -0
- package/web/app/site/app/logo-512.png +0 -0
- package/web/app/site/app/logo-badge-72.png +0 -0
- package/web/app/site/app/logo-maskable-512.png +0 -0
- package/web/app/site/app/logo-monochrome-512.png +0 -0
- package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
- package/web/app/site/app/manifest.webmanifest +1 -0
- package/web/app/site/app/og-image-2.jpg +0 -0
- package/web/app/site/app/og-image.png +0 -0
- package/web/app/site/app/robots.txt +2 -0
- package/web/app/site/app/version.json +1 -0
- package/web/app/site/boot.js +34067 -0
- package/web/app/site/index.html +203 -0
- package/web/app/site/sw.js +53745 -0
- package/web/app/sw-src.mjs +205 -0
- package/web/front/#new-account.html# +43 -0
- package/web/front/admin.html +1 -81
- package/web/front/admin.js +85 -0
- package/web/front/new-account.html +1 -9
- package/web/front/new-account.js +13 -0
- package/web/front/run.html +1 -92
- package/web/front/run.js +107 -0
- package/lib/podrdf.mjs +0 -132
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
// agent.mjs — FediPod's agent, running in the browser.
|
|
2
|
+
//
|
|
3
|
+
// The Node agent (run-agent.mjs) wires the same pieces; this is that wiring with
|
|
4
|
+
// the browser's edges: the pod is reached over the DPoP session (pod-remote),
|
|
5
|
+
// the signing key comes from WebCrypto (keys-browser), and delivery goes through
|
|
6
|
+
// the relay (deliver-relay). Everything between — wire, the store, the
|
|
7
|
+
// publisher, intake, the Mastodon facade — is lib/, unchanged.
|
|
8
|
+
import { apUrls } from '../../lib/wire.mjs';
|
|
9
|
+
import * as containers from '../../lib/pod/containers.mjs';
|
|
10
|
+
import * as podState from '../../lib/pod/state.mjs';
|
|
11
|
+
import { PodStore } from '../../lib/store.mjs';
|
|
12
|
+
import { HttpStorage } from '../../lib/storage.mjs';
|
|
13
|
+
import { Publisher } from '../../lib/publisher.mjs';
|
|
14
|
+
import { Intake } from '../../lib/intake.mjs';
|
|
15
|
+
import { Lease } from '../../lib/lease.mjs';
|
|
16
|
+
import { MastoApi } from '../../lib/mastoapi.mjs';
|
|
17
|
+
import { TagFeed } from '../../lib/tagfeed.mjs';
|
|
18
|
+
import { makeDpopSession } from './pod-auth.mjs';
|
|
19
|
+
import { BrowserRemotePod } from './pod-remote.mjs';
|
|
20
|
+
import { importSigningKey, loadKeysFromPod, keyCacheKey } from './keys-browser.mjs';
|
|
21
|
+
import { generateKeys, wrapKeys } from './keystore.mjs';
|
|
22
|
+
import { kvPut } from './idb-kv.mjs';
|
|
23
|
+
import { RelayDeliverer } from './deliver-relay.mjs';
|
|
24
|
+
import { AdminFacade } from './admin-facade.mjs';
|
|
25
|
+
import { BrowserAtproto } from './atproto-browser.mjs';
|
|
26
|
+
import { BskyFeed } from '../../lib/bskyfeed.mjs';
|
|
27
|
+
import { BrowserFediAccounts } from './fediacct-browser.mjs';
|
|
28
|
+
import { AcctFeed } from '../../lib/acctfeed.mjs';
|
|
29
|
+
import { followActor, unfollowActor } from '../../lib/social.mjs';
|
|
30
|
+
import { ImportWorker } from '../../lib/import.mjs';
|
|
31
|
+
|
|
32
|
+
// The authorities this identity answers on: exactly one, this origin. The Node
|
|
33
|
+
// agent gets this from lib/guard.mjs, which is not in the browser bundle and
|
|
34
|
+
// would not fit if it were — its whole subject is loopback and named local
|
|
35
|
+
// origins, neither of which means anything here.
|
|
36
|
+
//
|
|
37
|
+
// Two questions are asked of it. `has()` decides whether an OAuth redirect_uri
|
|
38
|
+
// is somewhere this agent actually answers. `isLocalRequest()` decides whether
|
|
39
|
+
// a caller is the owner rather than a stranger, and in a browser that question
|
|
40
|
+
// has exactly one honest answer: whether the request came from this origin —
|
|
41
|
+
// which the service worker has already established before anything reaches the
|
|
42
|
+
// facade, and stamps on the request it builds (see sw-src.mjs).
|
|
43
|
+
const originAuthorities = (host) => ({
|
|
44
|
+
set: new Set([String(host || '').toLowerCase()]),
|
|
45
|
+
has(authority) { return this.set.has(String(authority || '').toLowerCase()); },
|
|
46
|
+
isLocalRequest(req) { return req?.sameOrigin === true; },
|
|
47
|
+
isLocal(h) { return this.has(h); },
|
|
48
|
+
wsAuthorities() { return []; }, // fetch-only: this build serves no socket
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export class BrowserAgent {
|
|
52
|
+
constructor({ log = console.log } = {}) {
|
|
53
|
+
this.log = log;
|
|
54
|
+
this.viewer = false;
|
|
55
|
+
this.fediaccts = null;
|
|
56
|
+
this.atproto = null;
|
|
57
|
+
this.intake = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
configured() { return !!this.store?.getConfig(); }
|
|
61
|
+
|
|
62
|
+
// Asked by every write / destructive path before it acts. If we already hold
|
|
63
|
+
// the lease, proceed. If we are a viewer, the owner acting HERE outranks the
|
|
64
|
+
// idle active device: claim the lease outright and become active.
|
|
65
|
+
async requestTakeover() {
|
|
66
|
+
if (!this.viewer) return true;
|
|
67
|
+
if (!(await this.lease.takeover())) return false;
|
|
68
|
+
clearTimeout(this._viewerTimer); this._viewerTimer = null;
|
|
69
|
+
await this.goActive();
|
|
70
|
+
this.log('took over from the other device (action here)');
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Become the active agent: renew the lease, then start what a viewer skips —
|
|
75
|
+
// publish the face, drain the inbox, run the mirrors.
|
|
76
|
+
async goActive() {
|
|
77
|
+
this.viewer = false;
|
|
78
|
+
this.lease.onLost = () => this.demote();
|
|
79
|
+
this.lease.startRenewal();
|
|
80
|
+
try {
|
|
81
|
+
// Forced, not revalidated. While this device watched, the ACTIVE one was
|
|
82
|
+
// writing; our cache is however stale the last viewer poll left it, and
|
|
83
|
+
// the store is write-through — so the first write from here would push a
|
|
84
|
+
// whole document back over newer state. Read what is actually there
|
|
85
|
+
// before acting on it.
|
|
86
|
+
await this.store.load({ force: true }).catch((e) => this.log(`re-reading state: ${e.message}`));
|
|
87
|
+
// And start delivering again, since demote() stopped it. startQueue() is
|
|
88
|
+
// idempotent, so a goActive() that was already active costs nothing.
|
|
89
|
+
this.deliverer?.startQueue?.();
|
|
90
|
+
await this.publisher.publishProfile();
|
|
91
|
+
await this.store.flush?.();
|
|
92
|
+
await this.intake.start();
|
|
93
|
+
this.startBsky();
|
|
94
|
+
this.startAccts();
|
|
95
|
+
this.tagfeed?.start();
|
|
96
|
+
// Resumes a run that was staged on another device, or here before a
|
|
97
|
+
// restart — the state lives on the pod, not in this process.
|
|
98
|
+
this.importer?.start();
|
|
99
|
+
} catch (e) { this.log(`going active: ${e.message}`); }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Another device took the lease: stop acting (the drain also self-checks the
|
|
103
|
+
// lease), keep serving the feed read-only, and poll to take it back if freed.
|
|
104
|
+
demote() {
|
|
105
|
+
if (this.viewer) return;
|
|
106
|
+
this.viewer = true;
|
|
107
|
+
this.log('another device took over — read-only here');
|
|
108
|
+
this.lease.stopRenewal();
|
|
109
|
+
this.intake?.stop?.();
|
|
110
|
+
// The delivery queue as well. Its timer starts in the Deliverer's
|
|
111
|
+
// constructor and nothing here ever switched it off, so a demoted device
|
|
112
|
+
// went on sending from a queue the active device is also sending from —
|
|
113
|
+
// the same activity delivered twice, from two browsers, over one key.
|
|
114
|
+
this.deliverer?.stop?.();
|
|
115
|
+
this.importer?.stop?.();
|
|
116
|
+
this.stopBsky(); this.stopAccts(); this.tagfeed?.stop?.();
|
|
117
|
+
this.startViewerPoll();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// A viewer refreshes the feed from the pod (the active device updates it), and
|
|
121
|
+
// promotes the moment the lease is free.
|
|
122
|
+
// Five minutes with jitter, matching the Node agent (run-agent.mjs). Two
|
|
123
|
+
// minutes flat was ~60 pod requests an hour from a device that is not acting,
|
|
124
|
+
// and unjittered meant several idle devices on one pod knocking in lockstep.
|
|
125
|
+
static VIEWER_POLL_MS = 5 * 60_000;
|
|
126
|
+
|
|
127
|
+
startViewerPoll() {
|
|
128
|
+
clearTimeout(this._viewerTimer);
|
|
129
|
+
const every = () => BrowserAgent.VIEWER_POLL_MS * (0.85 + Math.random() * 0.3);
|
|
130
|
+
const tick = () => {
|
|
131
|
+
this._viewerTimer = setTimeout(async () => {
|
|
132
|
+
try {
|
|
133
|
+
if (!this.viewer) return;
|
|
134
|
+
await this.store.load().catch(() => {}); // revalidating is enough while watching
|
|
135
|
+
if (await this.lease.acquire()) { await this.goActive(); this.log('lease freed — now active'); return; }
|
|
136
|
+
} catch (e) { this.log(`viewer poll: ${e.message}`); }
|
|
137
|
+
if (this.viewer) tick();
|
|
138
|
+
}, every());
|
|
139
|
+
};
|
|
140
|
+
tick();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Boot from what sign-up (or sign-in) produced.
|
|
145
|
+
* @param credential the pod credential { clientId, secret, tokenEndpoint, webId, remotePod, root }
|
|
146
|
+
* @param keysRecord the plaintext signing keys { rsa:{privatePem,publicPem} }
|
|
147
|
+
* @param config { remotePod, root, handle, name, issuer, gateway? }
|
|
148
|
+
* @param opts.frontOrigin where the relay lives (for delivery)
|
|
149
|
+
*/
|
|
150
|
+
/**
|
|
151
|
+
* Boot the agent. Two ways in:
|
|
152
|
+
* - { oidc }: a Solid-OIDC session from oidc-session.getSession() (the real,
|
|
153
|
+
* redirect-login path). Config and keys are read from the pod, which the
|
|
154
|
+
* session is authenticated to; a fresh sign-up writes them first.
|
|
155
|
+
* - { credential, keysRecord, config }: a client-credential session with the
|
|
156
|
+
* material in hand (the offline test path).
|
|
157
|
+
*/
|
|
158
|
+
async boot({ oidc, credential, keysRecord, config, frontOrigin }) {
|
|
159
|
+
let session; let webId; let remotePod;
|
|
160
|
+
if (oidc) {
|
|
161
|
+
session = { fetch: (u, i) => oidc.fetch(u, i) };
|
|
162
|
+
webId = oidc.webId;
|
|
163
|
+
remotePod = new URL(webId).origin + '/';
|
|
164
|
+
} else {
|
|
165
|
+
const dpop = await makeDpopSession(credential);
|
|
166
|
+
session = { fetch: (u, i) => dpop.fetch(u, i) };
|
|
167
|
+
webId = credential.webId;
|
|
168
|
+
remotePod = credential.remotePod;
|
|
169
|
+
}
|
|
170
|
+
this.webId = webId;
|
|
171
|
+
const root = (config && config.root) || 'fedipod/';
|
|
172
|
+
this.remote = new BrowserRemotePod(session, { webId, log: this.log });
|
|
173
|
+
this.urls = apUrls(remotePod, root);
|
|
174
|
+
|
|
175
|
+
// State store, on the pod.
|
|
176
|
+
// Through the transport, not the raw session: state writes are pod writes,
|
|
177
|
+
// and going round it skipped the Retry-After cooldown, the retry ladder and
|
|
178
|
+
// the deletion deny-list that every other write on this agent observes.
|
|
179
|
+
// The Node agent has always passed remote.fetch here.
|
|
180
|
+
const podFetch = (u, i) => this.remote.fetch(u, i);
|
|
181
|
+
this.store = new PodStore({ storage: new HttpStorage(this.urls.state, podFetch), log: this.log });
|
|
182
|
+
await this.store.load().catch(() => { /* first boot: nothing there yet */ });
|
|
183
|
+
// Config: handed in on sign-up, or read from the pod on a returning sign-in.
|
|
184
|
+
const cfg = config || this.store.getConfig();
|
|
185
|
+
if (!cfg) throw new Error('no account config on this pod — sign up first');
|
|
186
|
+
// `root` is written INTO the config, not just used above. The Publisher
|
|
187
|
+
// builds its own urls from `config.root` (publisher.mjs), and a config
|
|
188
|
+
// without one falls to the Node default — so an account set up elsewhere
|
|
189
|
+
// and signed into here would keep its state under `fedipod/` while every
|
|
190
|
+
// document it published landed under `activitypods-js/`. One root, decided
|
|
191
|
+
// once, carried by the config everything downstream reads.
|
|
192
|
+
this.store.setConfig({ ...(this.store.getConfig() || {}), ...cfg, root });
|
|
193
|
+
// Keys: handed in (offline), or the owner-only keys.json read from the pod.
|
|
194
|
+
const keys = keysRecord ? await importSigningKey(keysRecord) : await loadKeysFromPod(this.remote, this.urls);
|
|
195
|
+
config = this.store.getConfig();
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
// `passive`: no queue-drain timer until this device is the active one.
|
|
199
|
+
// Whether it IS the active one is not known here — the lease is acquired
|
|
200
|
+
// further down, after everything is constructed — and the timer starts in
|
|
201
|
+
// the Deliverer's constructor, so a viewer used to boot delivering. Start
|
|
202
|
+
// it in goActive() instead, which is the one place that means "act".
|
|
203
|
+
this.deliverer = new RelayDeliverer({
|
|
204
|
+
passive: true,
|
|
205
|
+
store: this.store, rsaPrivate: keys.rsaPrivate, keyId: this.urls.actor + '#main-key',
|
|
206
|
+
actorId: this.urls.actor, log: this.log,
|
|
207
|
+
relayUrl: `${frontOrigin.replace(/\/$/, '')}/api/relay`, handle: config.handle, sessionFetch: session.fetch,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
this.publisher = new Publisher({
|
|
211
|
+
config: this.store.getConfig(), remote: this.remote, store: this.store,
|
|
212
|
+
deliverer: this.deliverer, publicKeyPem: keys.rsaPublicPem, assertionKey: null, log: this.log,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// The Bluesky connection, stamped to this actor. The same client the Node
|
|
216
|
+
// agent uses; here the credential lives in the owner-only pod state (see
|
|
217
|
+
// atproto-browser.mjs). The publisher cross-posts public notes through it
|
|
218
|
+
// when one is connected and crossPost is on; BskyFeed mirrors Bluesky back.
|
|
219
|
+
this.atproto = new BrowserAtproto({ store: this.store, actorId: this.urls.actor, log: this.log });
|
|
220
|
+
this.publisher.atproto = this.atproto;
|
|
221
|
+
|
|
222
|
+
// Fediverse accounts the owner holds on OTHER servers, mirrored into the
|
|
223
|
+
// feed read here. Their full-access tokens live in this browser (IndexedDB),
|
|
224
|
+
// never on the pod — see fediacct-browser.mjs. Records are loaded, and the
|
|
225
|
+
// poll started, in the background provisioning below.
|
|
226
|
+
this.fediaccts = new BrowserFediAccounts({ store: this.store, actorId: this.urls.actor, log: this.log });
|
|
227
|
+
|
|
228
|
+
// The facade and the intake object exist synchronously, so the client can be
|
|
229
|
+
// served the instant boot returns. The heavy pod I/O — provisioning the
|
|
230
|
+
// owner-only containers, publishing the public face, and starting the inbox
|
|
231
|
+
// drain — is a burst of ~20 writes that a pod behind an edge (Cloudflare on
|
|
232
|
+
// solidcommunity.net) throttles. Reaching the feed must NOT wait on it, or a
|
|
233
|
+
// throttled request hangs the whole sign-in. So it runs in the background,
|
|
234
|
+
// where per-request retry rides out the throttling; `this.provisioning`
|
|
235
|
+
// resolves when it is done (a test, or a later call, can await it).
|
|
236
|
+
// Single-active-agent lease (lib/lease.mjs): the inbox drain is a
|
|
237
|
+
// destructive read and the state store is write-through-cached, so exactly
|
|
238
|
+
// one browser/device may ACT on a pod at a time — a later arrival runs
|
|
239
|
+
// read-only until the owner acts on it and it takes over. Written with fresh
|
|
240
|
+
// fetches, never the cached store. Passed into Intake so the drain checks it.
|
|
241
|
+
this.lease = new Lease({ url: this.urls.state + 'lease.json', fetchImpl: podFetch, log: this.log });
|
|
242
|
+
this.intake = new Intake({
|
|
243
|
+
config: this.store.getConfig(), urls: this.urls, remote: this.remote,
|
|
244
|
+
store: this.store, deliverer: this.deliverer, publisher: this.publisher, log: this.log, push: true, lease: this.lease,
|
|
245
|
+
});
|
|
246
|
+
// The Mastodon facade the service worker serves.
|
|
247
|
+
//
|
|
248
|
+
// It used to be handed `allowed: null`, which reads as "no policy" — and
|
|
249
|
+
// `redirectAllowed()` waves through EVERY redirect_uri when there is no
|
|
250
|
+
// policy (lib/mastoapi.mjs:315). So any page could have a freshly minted
|
|
251
|
+
// 90-day bearer delivered to an address of its own. One origin, ours, is
|
|
252
|
+
// the whole of the policy here.
|
|
253
|
+
// Three capabilities this build does not have, declared rather than
|
|
254
|
+
// pretended: no streaming socket (a worker is fetch-only), no web push
|
|
255
|
+
// (shims/web-push.mjs is a no-op), and no scheduling (nothing here runs
|
|
256
|
+
// between now and the scheduled time to publish). Each is omitted from the
|
|
257
|
+
// instance document, so the client hides the control instead of offering
|
|
258
|
+
// one that quietly does nothing.
|
|
259
|
+
this.masto = new MastoApi({
|
|
260
|
+
agent: this, log: this.log, scheme: 'https',
|
|
261
|
+
streaming: false, webPush: false, scheduling: false,
|
|
262
|
+
allowed: originAuthorities(self.location.host),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// The owner's record/manage surface (the same web/admin page the Node agent
|
|
266
|
+
// serves), answered by this agent over the worker. Personal only — the
|
|
267
|
+
// group and multi-actor endpoints do not exist here. See admin-facade.mjs.
|
|
268
|
+
this.admin = new AdminFacade({ agent: this, log: this.log });
|
|
269
|
+
|
|
270
|
+
// The CSV follow/block/mute importer — the same worker the Node agent runs.
|
|
271
|
+
// It needs only `agent.store` and social.mjs, both of which are in this
|
|
272
|
+
// bundle, so the browser answering 501 was a gap rather than a limitation.
|
|
273
|
+
// Started only by goActive(): it follows people over minutes, which is
|
|
274
|
+
// exactly the kind of writing a viewer must not do.
|
|
275
|
+
this.importer = new ImportWorker({ agent: this, log: this.log });
|
|
276
|
+
|
|
277
|
+
// The topical (hashtag) feed, so a fresh account is not a blank wall. It
|
|
278
|
+
// polls public tag timelines and mirrors NEW notes as view-only statuses —
|
|
279
|
+
// nothing is written to the pod. A browser cannot fetch arbitrary fediverse
|
|
280
|
+
// servers cross-origin, so every fetch it makes (the tag timeline and each
|
|
281
|
+
// note's verification via intake.fetchAP) goes through the same relay the
|
|
282
|
+
// rest of the agent's remote reads use. The user controls it from the
|
|
283
|
+
// client's Followed Hashtags surface (see MastoApi's tag endpoints).
|
|
284
|
+
const relayGet = (u, i = {}) => this.deliverer.signedFetch(u, { ...i, method: 'GET' });
|
|
285
|
+
this.tagfeed = new TagFeed({ store: this.store, intake: this.intake, log: this.log, fetcher: relayGet });
|
|
286
|
+
// Started only when this browser is the ACTIVE agent (goActive), so two
|
|
287
|
+
// devices do not double-mirror the same hashtags into the pod state.
|
|
288
|
+
|
|
289
|
+
this.provisioning = (async () => {
|
|
290
|
+
// Owner-only containers first; idempotent, so a second device provisioning
|
|
291
|
+
// them too is harmless. Each public document sets its own Read ACL inside
|
|
292
|
+
// publishProfile. Mirrors run-agent bootstrap.
|
|
293
|
+
await containers.provisionPrivate(this.remote, this.urls);
|
|
294
|
+
// Connected-account records are reads — safe whether we act or view.
|
|
295
|
+
await this.atproto.load(); // the Bluesky credential, if it is browser-stored
|
|
296
|
+
await this.fediaccts.load(); // connected fediverse accounts, from both backends
|
|
297
|
+
// The lease decides: this device ACTS on the pod, or reads it read-only.
|
|
298
|
+
this.viewer = !(await this.lease.acquire());
|
|
299
|
+
if (this.viewer) {
|
|
300
|
+
this.log(`read-only viewer: another device is active on @${config.handle}`);
|
|
301
|
+
this.startViewerPoll(); // reload the feed, and promote if the lease frees
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
await this.goActive();
|
|
305
|
+
this.log(`browser agent fully provisioned (active): @${config.handle}`);
|
|
306
|
+
})().catch((e) => { this.log(`background provisioning: ${e.message}`); throw e; });
|
|
307
|
+
// Never let an unhandled rejection escape when nobody awaits it.
|
|
308
|
+
this.provisioning.catch(() => {});
|
|
309
|
+
|
|
310
|
+
this.log(`browser agent up: @${config.handle} on ${remotePod}`);
|
|
311
|
+
return this;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// What the record page and the bar read (GET /status). The same shape the
|
|
315
|
+
// Node agent returns (run-agent.mjs), minus what a browser cannot have — no
|
|
316
|
+
// process, no local checkout, no atproto yet, so those are null.
|
|
317
|
+
status() {
|
|
318
|
+
const cfg = this.store?.getConfig();
|
|
319
|
+
const contacts = this.store?.getContacts() || { followers: [], following: [] };
|
|
320
|
+
return {
|
|
321
|
+
configured: this.configured(),
|
|
322
|
+
mode: !this.configured() ? 'unconfigured' : this.viewer ? 'viewer' : 'active',
|
|
323
|
+
kind: cfg?.kind || 'person',
|
|
324
|
+
handle: cfg?.handle || null,
|
|
325
|
+
actor: this.urls?.actor || null,
|
|
326
|
+
followers: contacts.followers.length,
|
|
327
|
+
following: contacts.following.length,
|
|
328
|
+
queue: this.store?.getQueue().length || 0,
|
|
329
|
+
deadLetters: this.store?.getDeadLetters().length || 0,
|
|
330
|
+
blockedDomains: this.store?.getBlocklist().domains.length || 0,
|
|
331
|
+
push: this.intake?.wsState || 'n/a',
|
|
332
|
+
inbox: this.intake?.inboxStats || null,
|
|
333
|
+
lastDrain: this.intake?.lastDrain || null,
|
|
334
|
+
tagfeed: this.tagfeed
|
|
335
|
+
? { ...this.tagfeed.config(), lastSweep: this.tagfeed.lastSweep, lastAdded: this.tagfeed.lastAdded }
|
|
336
|
+
: null,
|
|
337
|
+
atproto: this.atproto?.status() || null,
|
|
338
|
+
podRequests: this.remote?.stats?.() || null,
|
|
339
|
+
update: null,
|
|
340
|
+
inboxCooldownFor: 0,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Rotate the signing key (POST /rotate-key). Mint a fresh keypair, replace the
|
|
345
|
+
// pod's copy, swap it into the live publisher/deliverer, and republish the
|
|
346
|
+
// actor so the new public key is on the wire. The old key stops signing the
|
|
347
|
+
// moment this returns — same one-way change as the Node agent's rotateKey
|
|
348
|
+
// (run-agent.mjs).
|
|
349
|
+
//
|
|
350
|
+
// The pod's copy is wrapped, so this needs the account password. There is
|
|
351
|
+
// nowhere to get it from without asking: the worker boots from a stored
|
|
352
|
+
// session and holds no password, and caching one to save a prompt on a
|
|
353
|
+
// once-in-a-while action would put the account password in storage to avoid
|
|
354
|
+
// typing it. So the caller supplies it, and rotating without one is refused
|
|
355
|
+
// rather than quietly writing a bare key back where a wrapped one was.
|
|
356
|
+
async rotateKey({ password } = {}) {
|
|
357
|
+
if (!password) {
|
|
358
|
+
const e = new Error('rotating the signing key needs your account password — '
|
|
359
|
+
+ 'it is what the new key is locked under on the pod');
|
|
360
|
+
e.code = 'key-password-needed';
|
|
361
|
+
throw e;
|
|
362
|
+
}
|
|
363
|
+
const before = this.publisher.publicKeyPem;
|
|
364
|
+
const rec = await generateKeys();
|
|
365
|
+
rec.mintedFor = this.urls.actor; // one key, one actor (lib/keys.mjs)
|
|
366
|
+
await podState.writeWrappedKeys(this.remote, this.urls, await wrapKeys(rec, password));
|
|
367
|
+
await kvPut(keyCacheKey(this.urls.actor), rec).catch(() => {});
|
|
368
|
+
const keys = await importSigningKey(rec);
|
|
369
|
+
this.publisher.publicKeyPem = keys.rsaPublicPem;
|
|
370
|
+
this.deliverer.rsaPrivate = keys.rsaPrivate;
|
|
371
|
+
await this.publisher.publishProfile();
|
|
372
|
+
return { changed: before !== keys.rsaPublicPem, publicKeyPem: keys.rsaPublicPem };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// The Bluesky mirror poll — Bluesky notifications and replies into the pod —
|
|
376
|
+
// running only while an account is connected. Reused across calls, not
|
|
377
|
+
// replaced, so a reconnect never leaves an orphaned timer (as in run-agent).
|
|
378
|
+
// A person's mirror only; a group's Bluesky presence is not built here.
|
|
379
|
+
startBsky() {
|
|
380
|
+
if (!this.atproto?.feedActive()) return; // connected AND not paused
|
|
381
|
+
if (this.store.getConfig()?.quiescedAt) return;
|
|
382
|
+
this.bskyfeed ||= new BskyFeed({ store: this.store, atproto: this.atproto, log: this.log });
|
|
383
|
+
this.bskyfeed.start();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
stopBsky() { this.bskyfeed?.stop(); }
|
|
387
|
+
|
|
388
|
+
// Pausing/resuming or moving the Bluesky connection changes whether the mirror
|
|
389
|
+
// should run — re-evaluate it.
|
|
390
|
+
restartBsky() { this.stopBsky(); this.bskyfeed = null; this.startBsky(); }
|
|
391
|
+
|
|
392
|
+
// The connected-fediverse-accounts mirror — their home timelines and
|
|
393
|
+
// notifications polled into the feed read here — running only while at least
|
|
394
|
+
// one is connected. Reused, not replaced, for the same orphaned-timer reason
|
|
395
|
+
// as the others (run-agent.mjs).
|
|
396
|
+
startAccts() {
|
|
397
|
+
if (!this.fediaccts?.connected()) return;
|
|
398
|
+
if (this.store.getConfig()?.quiescedAt) return;
|
|
399
|
+
this.acctfeed ||= new AcctFeed({ store: this.store, accounts: this.fediaccts, log: this.log });
|
|
400
|
+
this.acctfeed.start();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
stopAccts() { this.acctfeed?.stop(); }
|
|
404
|
+
|
|
405
|
+
// Connecting or disconnecting one changes what there is to poll.
|
|
406
|
+
restartAccts() { this.stopAccts(); this.acctfeed = null; this.startAccts(); }
|
|
407
|
+
|
|
408
|
+
// The follow graph, written down before it is torn down.
|
|
409
|
+
//
|
|
410
|
+
// Both parking and moving away unfollow everyone, and the record page offers
|
|
411
|
+
// "active" again after either — so both have to leave something to come back
|
|
412
|
+
// from. moveTo used to report `snapshot: following.length` without writing
|
|
413
|
+
// one, so a browser account set back to active re-followed nobody and said so
|
|
414
|
+
// only in a count of zero. Same file, same name, same shape as the Node
|
|
415
|
+
// agent's `_snapshotFollowing` (run-agent.mjs).
|
|
416
|
+
async _snapshotFollowing() {
|
|
417
|
+
const following = this.store.getContacts().following;
|
|
418
|
+
this.store.write('parked.json', {
|
|
419
|
+
parkedAt: new Date().toISOString(),
|
|
420
|
+
following: following.map(f => ({ actor: f.actor, handle: f.handle || null })),
|
|
421
|
+
});
|
|
422
|
+
await this.store.flush();
|
|
423
|
+
return following;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Keep the handle, take no more mail.
|
|
427
|
+
//
|
|
428
|
+
// Unfollowing is what actually stops the volume — every account you follow
|
|
429
|
+
// pushes its posts into your inbox — and closing the inbox handles what no
|
|
430
|
+
// follow graph can gate: stranger mentions, new follow requests, spam.
|
|
431
|
+
//
|
|
432
|
+
// This matters more here than it does on Node. A Node agent runs as a service
|
|
433
|
+
// and keeps draining; close the tab on a browser agent and nothing drains,
|
|
434
|
+
// while the gateway goes on delivering into the pod inbox regardless (it
|
|
435
|
+
// writes straight to the pod — see lib/gateway-core.mjs). An unattended
|
|
436
|
+
// browser account is a pod quietly filling up with nobody collecting, and
|
|
437
|
+
// this is the only control that stops it at the source.
|
|
438
|
+
async quiesce() {
|
|
439
|
+
const following = this.store.getContacts().following.map(f => f.actor).filter(Boolean);
|
|
440
|
+
let unfollowed = 0;
|
|
441
|
+
for (const actor of following) {
|
|
442
|
+
try { await unfollowActor(this, actor); unfollowed++; }
|
|
443
|
+
catch (e) { this.log(`unfollow ${actor} failed: ${e.message}`); }
|
|
444
|
+
}
|
|
445
|
+
const quiescedAt = await this.publisher.closeInbox();
|
|
446
|
+
this.log(`quiesced: unfollowed ${unfollowed}/${following.length}, inbox closed`);
|
|
447
|
+
return { unfollowed, following: following.length, quiescedAt };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Park (POST /park). The snapshot is taken FIRST: unfollowing is what stops
|
|
451
|
+
// the traffic, but it also destroys the only record of who was being
|
|
452
|
+
// followed, and "until I want this back" needs that record.
|
|
453
|
+
async park() {
|
|
454
|
+
const following = await this._snapshotFollowing();
|
|
455
|
+
const r = await this.quiesce();
|
|
456
|
+
this.log(`parked: ${r.unfollowed} unfollow(s) recorded for revival, inbox closed`);
|
|
457
|
+
return { ...r, snapshot: following.length };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Undo a park (POST /revive). Re-open the inbox, then re-follow everyone in
|
|
461
|
+
// the snapshot. Each Follow needs the far end to Accept, so this is a request
|
|
462
|
+
// rather than a restoration — some will not come back, which is the nature of
|
|
463
|
+
// the thing, and why the result counts both numbers.
|
|
464
|
+
async revive() {
|
|
465
|
+
const parked = this.store.read('parked.json', null);
|
|
466
|
+
await this.publisher.openInbox();
|
|
467
|
+
let refollowed = 0;
|
|
468
|
+
for (const f of parked?.following || []) {
|
|
469
|
+
try { await followActor(this, f.actor); refollowed++; }
|
|
470
|
+
catch (e) { this.log(`re-follow ${f.actor} failed: ${e.message}`); }
|
|
471
|
+
}
|
|
472
|
+
if (parked) this.store.remove('parked.json').catch(() => {});
|
|
473
|
+
await this.store.flush();
|
|
474
|
+
this.log(`revived: inbox open, ${refollowed}/${parked?.following?.length || 0} follow(s) re-sent`);
|
|
475
|
+
return { refollowed, of: parked?.following?.length || 0, parkedAt: parked?.parkedAt || null };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Hand the identity to another account (POST /move). The federated act is the
|
|
479
|
+
// Move to every follower's server; then this account quiesces — unfollows
|
|
480
|
+
// everyone and closes its inbox — since it is being left behind. Same as the
|
|
481
|
+
// Node agent's moveTo + quiesce (run-agent.mjs). `target` is an actor URL,
|
|
482
|
+
// already resolved by the caller.
|
|
483
|
+
async moveTo(target) {
|
|
484
|
+
const moved = await this.publisher.publishMove(target);
|
|
485
|
+
const following = await this._snapshotFollowing();
|
|
486
|
+
const r = await this.quiesce();
|
|
487
|
+
this.log(`moved to ${target}: unfollowed ${r.unfollowed}/${r.following}, inbox closed`);
|
|
488
|
+
return { ...moved, ...r, snapshot: following.length };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// atproto-browser.mjs — the Bluesky connection, in the browser.
|
|
2
|
+
//
|
|
3
|
+
// Same Atproto client the Node agent uses (lib/atproto.mjs). Two browser edges:
|
|
4
|
+
//
|
|
5
|
+
// 1. Storage is the owner's choice, per connection. "browser" keeps the
|
|
6
|
+
// credential in IndexedDB on this device (default — a full-access token
|
|
7
|
+
// stays here); "pod" keeps it in the owner-only pod state so it follows the
|
|
8
|
+
// owner to any browser they sign in from. The record carries which, and
|
|
9
|
+
// read/write route to that backend; moving between them is just a re-write.
|
|
10
|
+
// 2. The mirror can be paused (feedPaused) without disconnecting.
|
|
11
|
+
//
|
|
12
|
+
// Bluesky's API answers CORS, so the fetches go direct, not through the relay.
|
|
13
|
+
import { Atproto } from '../../lib/atproto.mjs';
|
|
14
|
+
import { kvGet, kvPut, kvDel } from './idb-kv.mjs';
|
|
15
|
+
|
|
16
|
+
const IDB_KEY = 'atproto';
|
|
17
|
+
const POD_DOC = 'atproto.json';
|
|
18
|
+
|
|
19
|
+
export class BrowserAtproto extends Atproto {
|
|
20
|
+
constructor({ store, actorId = null, log = console.log } = {}) {
|
|
21
|
+
super({ localDir: '/atproto', actorId, log }); // localDir unused; storage below
|
|
22
|
+
this.store = store;
|
|
23
|
+
this._idbRec = null; // the browser-stored record, cached at boot
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The IndexedDB copy into memory, so read() stays synchronous as the base
|
|
27
|
+
// class expects. The pod copy is already in the loaded store.
|
|
28
|
+
async load() { this._idbRec = await kvGet(IDB_KEY).catch(() => null); return this; }
|
|
29
|
+
|
|
30
|
+
// Whichever backend holds it, tagged with where it lives. The stamped-record
|
|
31
|
+
// guard is the base class's.
|
|
32
|
+
read() {
|
|
33
|
+
let rec = null; let where = null;
|
|
34
|
+
if (this._idbRec) { rec = this._idbRec; where = 'browser'; }
|
|
35
|
+
else { const p = this.store.read(POD_DOC, null); if (p) { rec = p; where = 'pod'; } }
|
|
36
|
+
if (!rec) return null;
|
|
37
|
+
if (rec.mintedFor && this.actorId && rec.mintedFor !== this.actorId) {
|
|
38
|
+
this.log(`atproto belongs to ${rec.mintedFor} — not reusing it for ${this.actorId}`);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return { ...rec, storage: where };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Write to the record's chosen backend (default browser), and clear the other
|
|
45
|
+
// so a moved credential leaves no copy behind.
|
|
46
|
+
write(rec) {
|
|
47
|
+
const where = rec.storage || 'browser';
|
|
48
|
+
const clean = { ...rec, storage: where };
|
|
49
|
+
if (where === 'pod') {
|
|
50
|
+
this.store.write(POD_DOC, clean); this.store.flush?.().catch(() => {});
|
|
51
|
+
this._idbRec = null; kvDel(IDB_KEY).catch(() => {});
|
|
52
|
+
} else {
|
|
53
|
+
this._idbRec = clean; kvPut(IDB_KEY, clean).catch((e) => this.log(`atproto persist: ${e.message}`));
|
|
54
|
+
this.store.write(POD_DOC, null); this.store.flush?.().catch(() => {});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
status() {
|
|
59
|
+
const rec = this.read();
|
|
60
|
+
return {
|
|
61
|
+
connected: !!rec?.did, service: rec?.service || null, handle: rec?.handle || null,
|
|
62
|
+
did: rec?.did || null, lastError: this.lastError,
|
|
63
|
+
cooldownFor: Math.max(0, Math.round((this.pausedUntil - Date.now()) / 1000)),
|
|
64
|
+
storage: rec?.storage || null, feedPaused: !!rec?.feedPaused,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The mirror runs unless paused; the account stays connected either way.
|
|
69
|
+
feedActive() { return this.connected() && !this.read()?.feedPaused; }
|
|
70
|
+
setFeedPaused(on) { const rec = this.read(); if (rec) this.write({ ...rec, feedPaused: !!on }); }
|
|
71
|
+
|
|
72
|
+
// Move the credential between backends (the Options menu's Storage control).
|
|
73
|
+
setStorage(where) { const rec = this.read(); if (rec && (where === 'pod' || where === 'browser')) this.write({ ...rec, storage: where }); }
|
|
74
|
+
|
|
75
|
+
async disconnect() {
|
|
76
|
+
const rec = this.read();
|
|
77
|
+
if (rec?.refreshJwt) {
|
|
78
|
+
await this._fetch(`${rec.service}/xrpc/com.atproto.server.deleteSession`, {
|
|
79
|
+
method: 'POST', headers: { authorization: `Bearer ${rec.refreshJwt}` },
|
|
80
|
+
}).catch(() => {});
|
|
81
|
+
}
|
|
82
|
+
this._idbRec = null; await kvDel(IDB_KEY).catch(() => {});
|
|
83
|
+
this.store.write(POD_DOC, null); await this.store.flush?.().catch(() => {});
|
|
84
|
+
this.log('bluesky disconnected');
|
|
85
|
+
}
|
|
86
|
+
}
|