fedipod-server 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claims.js +1 -1
- package/dist/handler.d.ts +1 -1
- package/dist/handler.js +35 -2
- package/dist/handler.jsonld +1 -1
- package/lib/admin.mjs +3 -3
- package/lib/c2s.mjs +27 -2
- package/lib/deliver.mjs +6 -0
- package/lib/embed.mjs +1 -0
- package/lib/export-collections.mjs +1 -1
- package/lib/front-core.mjs +272 -49
- package/lib/gateway-core.mjs +3 -7
- package/lib/httpsig.mjs +33 -2
- package/lib/import.mjs +1 -1
- package/lib/intake.mjs +357 -118
- package/lib/links.mjs +6 -32
- package/lib/mastoapi.mjs +449 -38
- package/lib/migrate.mjs +4 -18
- package/lib/pod/README.md +61 -0
- package/lib/pod/actor.mjs +96 -0
- package/lib/pod/collection.mjs +77 -0
- package/lib/pod/containers.mjs +136 -0
- package/lib/pod/discovery.mjs +46 -0
- package/lib/pod/featured.mjs +18 -0
- package/lib/pod/followers.mjs +24 -0
- package/lib/pod/following.mjs +10 -0
- package/lib/pod/http.mjs +74 -0
- package/lib/pod/inbox.mjs +144 -0
- package/lib/pod/links.mjs +35 -0
- package/lib/pod/media.mjs +27 -0
- package/lib/pod/notes.mjs +80 -0
- package/lib/pod/notifications.mjs +89 -0
- package/lib/pod/outbox.mjs +18 -0
- package/lib/pod/package.json +19 -0
- package/lib/pod/policy.mjs +58 -0
- package/lib/pod/private.mjs +19 -0
- package/lib/pod/root.mjs +63 -0
- package/lib/pod/state.mjs +39 -0
- package/lib/pod/transport.mjs +477 -0
- package/lib/pod/urls.mjs +71 -0
- package/lib/polls.mjs +105 -0
- package/lib/publisher.mjs +357 -188
- package/lib/remote.mjs +30 -395
- package/lib/social.mjs +20 -9
- package/lib/storage.mjs +2 -2
- package/lib/store.mjs +8 -1
- package/lib/tagfeed.mjs +19 -2
- package/lib/wire.mjs +87 -67
- package/package.json +1 -1
- package/run-agent.mjs +20 -39
- package/web/admin/admin.js +141 -74
- package/web/admin/bar.css +9 -13
- package/web/admin/bar.js +2 -2
- package/web/admin/client/client.js +1 -1
- package/web/admin/client/index.html +5 -6
- package/web/admin/index.html +53 -65
- package/web/admin/setup/index.html +2 -1
- package/web/admin/setup/setup.js +1 -1
- package/web/admin/tokens.css +56 -0
- package/web/app/README.md +77 -0
- package/web/app/admin-facade.mjs +549 -0
- package/web/app/agent.mjs +490 -0
- package/web/app/atproto-browser.mjs +86 -0
- package/web/app/boot.mjs +340 -0
- package/web/app/deliver-relay.mjs +72 -0
- package/web/app/dist/boot.js +34067 -0
- package/web/app/dist/boot.js.map +7 -0
- package/web/app/dist/sw.js +53745 -0
- package/web/app/dist/sw.js.map +7 -0
- package/web/app/fediacct-browser.mjs +146 -0
- package/web/app/idb-kv.mjs +35 -0
- package/web/app/index.html +203 -0
- package/web/app/index.html~ +152 -0
- package/web/app/keys-browser.mjs +46 -0
- package/web/app/keystore.mjs +99 -0
- package/web/app/oidc-session.mjs +189 -0
- package/web/app/pod-auth.mjs +248 -0
- package/web/app/pod-remote.mjs +84 -0
- package/web/app/shims/_globals.mjs +2 -0
- package/web/app/shims/fedify-sig.mjs +77 -0
- package/web/app/shims/node-crypto.mjs +113 -0
- package/web/app/shims/node-fs.mjs +17 -0
- package/web/app/shims/node-path.mjs +19 -0
- package/web/app/shims/node-url.mjs +7 -0
- package/web/app/shims/prelude.js +58 -0
- package/web/app/shims/safefetch.mjs +58 -0
- package/web/app/shims/web-push.mjs +14 -0
- package/web/app/signup.mjs +248 -0
- package/web/app/site/_headers +9 -0
- package/web/app/site/_redirects +21 -0
- package/web/app/site/admin/admin.js +1181 -0
- package/web/app/site/admin/bar.css +60 -0
- package/web/app/site/admin/bar.js +61 -0
- package/web/app/site/admin/client/client.js +66 -0
- package/web/app/site/admin/client/index.html +43 -0
- package/web/app/site/admin/index.html +556 -0
- package/web/app/site/admin/setup/index.html +191 -0
- package/web/app/site/admin/setup/setup.js +308 -0
- package/web/app/site/admin/tokens.css +56 -0
- package/web/app/site/admin/window.css +55 -0
- package/web/app/site/admin/window.js +136 -0
- package/web/app/site/app/404.html +40 -0
- package/web/app/site/app/_headers +2 -0
- package/web/app/site/app/apple-touch-icon.png +0 -0
- package/web/app/site/app/assets/ICONS-DIPJeU0C.js +2 -0
- package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +1 -0
- package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +183 -0
- package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +1 -0
- package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
- package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +2 -0
- package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +1 -0
- package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
- package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
- package/web/app/site/app/assets/chunk-aKtaBQYM.js +1 -0
- package/web/app/site/app/assets/compose-CFEx7wb4.js +2 -0
- package/web/app/site/app/assets/compose-CFEx7wb4.js.map +1 -0
- package/web/app/site/app/assets/compose-ZJN5xSb4.js +35 -0
- package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +1 -0
- package/web/app/site/app/assets/debug.module-YFQgwnIr.js +52 -0
- package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +1 -0
- package/web/app/site/app/assets/dom-p1YcJ5Rw.js +2 -0
- package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +1 -0
- package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
- package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
- package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
- package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
- package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +1 -0
- package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +1 -0
- package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +1 -0
- package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +1 -0
- package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +1 -0
- package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +1 -0
- package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +1 -0
- package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +1 -0
- package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +1 -0
- package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +1 -0
- package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +1 -0
- package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +1 -0
- package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +1 -0
- package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +1 -0
- package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +1 -0
- package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +1 -0
- package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +1 -0
- package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +1 -0
- package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +1 -0
- package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +1 -0
- package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +1 -0
- package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +1 -0
- package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +1 -0
- package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +1 -0
- package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +1 -0
- package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +1 -0
- package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +1 -0
- package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +1 -0
- package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +1 -0
- package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +1 -0
- package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +1 -0
- package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +1 -0
- package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +1 -0
- package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +1 -0
- package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +1 -0
- package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +1 -0
- package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +1 -0
- package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +1 -0
- package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +1 -0
- package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +1 -0
- package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +1 -0
- package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +1 -0
- package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +1 -0
- package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +1 -0
- package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +1 -0
- package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +1 -0
- package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +1 -0
- package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +1 -0
- package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +1 -0
- package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +1 -0
- package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +1 -0
- package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +1 -0
- package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +1 -0
- package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +1 -0
- package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +1 -0
- package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +1 -0
- package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +1 -0
- package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +1 -0
- package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +1 -0
- package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +1 -0
- package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +1 -0
- package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +1 -0
- package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +1 -0
- package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +1 -0
- package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +1 -0
- package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +1 -0
- package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +1 -0
- package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +1 -0
- package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +1 -0
- package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +1 -0
- package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +1 -0
- package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +1 -0
- package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +1 -0
- package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +1 -0
- package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +1 -0
- package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +1 -0
- package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +1 -0
- package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +1 -0
- package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +1 -0
- package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +1 -0
- package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +1 -0
- package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +1 -0
- package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +1 -0
- package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +1 -0
- package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +1 -0
- package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +1 -0
- package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +1 -0
- package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +1 -0
- package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +1 -0
- package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +1 -0
- package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +1 -0
- package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +1 -0
- package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +1 -0
- package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +1 -0
- package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +1 -0
- package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +1 -0
- package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +1 -0
- package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +1 -0
- package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +1 -0
- package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +1 -0
- package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +1 -0
- package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +1 -0
- package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +1 -0
- package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +1 -0
- package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +1 -0
- package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +1 -0
- package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +1 -0
- package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +1 -0
- package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +1 -0
- package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +1 -0
- package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +1 -0
- package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +1 -0
- package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +1 -0
- package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +1 -0
- package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +1 -0
- package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +1 -0
- package/web/app/site/app/assets/instances-BNPptnn-.json +333 -0
- package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +1 -0
- package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +1 -0
- package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +1 -0
- package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +1 -0
- package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +1 -0
- package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +1 -0
- package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +1 -0
- package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +1 -0
- package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +1 -0
- package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +1 -0
- package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +1 -0
- package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +1 -0
- package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +1 -0
- package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +1 -0
- package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +1 -0
- package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +1 -0
- package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +1 -0
- package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +1 -0
- package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +1 -0
- package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +1 -0
- package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +1 -0
- package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/tok-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +1 -0
- package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +1 -0
- package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/main-BdqNbG-a.js +26 -0
- package/web/app/site/app/assets/main-BdqNbG-a.js.map +1 -0
- package/web/app/site/app/assets/mock-home-DGhR__He.js +2 -0
- package/web/app/site/app/assets/mock-home-DGhR__He.js.map +1 -0
- package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
- package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
- package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
- package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +2 -0
- package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +1 -0
- package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +20 -0
- package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +1 -0
- package/web/app/site/app/assets/style-B1iLPTPR.css +2 -0
- package/web/app/site/app/assets/temml-DuyIQMei.js +97 -0
- package/web/app/site/app/assets/temml-DuyIQMei.js.map +1 -0
- package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +2 -0
- package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +1 -0
- package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
- package/web/app/site/app/assets/year-in-posts-DJv_star.js +3 -0
- package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +1 -0
- package/web/app/site/app/compose/index.html +20 -0
- package/web/app/site/app/favicon.ico +0 -0
- package/web/app/site/app/index.html +73 -0
- package/web/app/site/app/logo-192.png +0 -0
- package/web/app/site/app/logo-512.png +0 -0
- package/web/app/site/app/logo-badge-72.png +0 -0
- package/web/app/site/app/logo-maskable-512.png +0 -0
- package/web/app/site/app/logo-monochrome-512.png +0 -0
- package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
- package/web/app/site/app/manifest.webmanifest +1 -0
- package/web/app/site/app/og-image-2.jpg +0 -0
- package/web/app/site/app/og-image.png +0 -0
- package/web/app/site/app/robots.txt +2 -0
- package/web/app/site/app/version.json +1 -0
- package/web/app/site/boot.js +34067 -0
- package/web/app/site/index.html +203 -0
- package/web/app/site/sw.js +53745 -0
- package/web/app/sw-src.mjs +205 -0
- package/web/front/#new-account.html# +43 -0
- package/web/front/admin.html +1 -81
- package/web/front/admin.js +85 -0
- package/web/front/new-account.html +1 -9
- package/web/front/new-account.js +13 -0
- package/web/front/run.html +1 -92
- package/web/front/run.js +107 -0
- package/lib/podrdf.mjs +0 -132
package/lib/wire.mjs
CHANGED
|
@@ -6,75 +6,40 @@ export const AS_CTX = 'https://www.w3.org/ns/activitystreams';
|
|
|
6
6
|
export const SEC_CTX = 'https://w3id.org/security/v1';
|
|
7
7
|
export const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// the
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
9
|
+
// The pod LAYOUT — which container each document lives in — moved into the pod
|
|
10
|
+
// library (lib/pod/urls.mjs), which knows nothing about FediPod and requires
|
|
11
|
+
// the container root to be stated rather than defaulted. What stays here is
|
|
12
|
+
// FediPod's own answer to "which root", and the handle rule, both of which read
|
|
13
|
+
// this application's config shape.
|
|
14
|
+
import { apUrls as podUrls } from './pod/urls.mjs';
|
|
15
|
+
|
|
16
|
+
export { webfingerHost } from './pod/urls.mjs';
|
|
17
|
+
|
|
18
|
+
// The container the Node agent has always used. Older than the rename to
|
|
19
|
+
// FediPod and kept because it is what every existing Node-side pod actually
|
|
20
|
+
// contains; the browser build states `fedipod/` on its own configs. A config
|
|
21
|
+
// that names a root is always believed — this is only the answer for one that
|
|
22
|
+
// does not.
|
|
23
|
+
export const DEFAULT_ROOT = 'activitypods-js/';
|
|
24
|
+
|
|
24
25
|
// The handle the fediverse sees: a fronted identity's name is the front's.
|
|
25
26
|
export function publicHandle(config) {
|
|
26
27
|
return config?.gateway?.frontActor?.match(/\/u\/([^/]+)\/ap\/actor\/?$/)?.[1]
|
|
27
28
|
|| config?.handle || null;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
following: face + 'ap/following',
|
|
43
|
-
notes: face + 'ap/notes/',
|
|
44
|
-
privateNotes: face + 'ap/private/',
|
|
45
|
-
featured: face + 'ap/featured',
|
|
46
|
-
// FEP-1b12: the moderator roster a recipient validates announced
|
|
47
|
-
// moderation against. Published only when moderators are configured.
|
|
48
|
-
moderators: face + 'ap/moderators',
|
|
49
|
-
// FEP-4ccd and FEP-c648: follows in limbo and the block list, as
|
|
50
|
-
// collections. They live in the private container so the owner-only ACL
|
|
51
|
-
// is inherited, not re-stated per document.
|
|
52
|
-
pendingFollowers: face + 'ap/private/pending-followers',
|
|
53
|
-
pendingFollowing: face + 'ap/private/pending-following',
|
|
54
|
-
blocked: face + 'ap/private/blocked',
|
|
55
|
-
profileHtml: face + 'ap/profile.html',
|
|
56
|
-
// Media stays on the pod even when fronted: attachment urls are not
|
|
57
|
-
// identity-checked by remotes, and proxying blobs would be pure cost.
|
|
58
|
-
media: home + 'ap/media/',
|
|
59
|
-
fediverse: home + 'fediverse/',
|
|
60
|
-
state: home + 'ap-state/',
|
|
61
|
-
};
|
|
62
|
-
if (publicBase) {
|
|
63
|
-
urls.podHome = home;
|
|
64
|
-
urls.publicHome = face;
|
|
65
|
-
urls.toPod = (u) => (typeof u === 'string' && u.startsWith(face) ? home + u.slice(face.length) : u);
|
|
66
|
-
urls.toPublic = (u) => (typeof u === 'string' && u.startsWith(home) ? face + u.slice(home.length) : u);
|
|
67
|
-
}
|
|
68
|
-
return urls;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// The host a handle would resolve through, or null when no handle can point
|
|
72
|
-
// here. @name@host is looked up at https://host/.well-known/webfinger, so only
|
|
73
|
-
// a pod that owns the root of its host can answer for one; a pod living at
|
|
74
|
-
// https://server/name/ may publish the document but nothing will ever ask.
|
|
75
|
-
export function webfingerHost(podUrl) {
|
|
76
|
-
const u = new URL(podUrl);
|
|
77
|
-
return u.pathname === '/' ? u.host : null;
|
|
31
|
+
/**
|
|
32
|
+
* The AP URLs for an identity, with FediPod's default root applied.
|
|
33
|
+
*
|
|
34
|
+
* The library underneath refuses to guess a root. Every caller here goes
|
|
35
|
+
* through this wrapper so there is exactly ONE answer to "which container",
|
|
36
|
+
* which is the thing that was wrong: the browser agent decided `fedipod/` for
|
|
37
|
+
* its own urls while the publisher, handed a config with no `root`, decided
|
|
38
|
+
* `activitypods-js/` — putting an identity's state in one container and its
|
|
39
|
+
* published documents in another.
|
|
40
|
+
*/
|
|
41
|
+
export function apUrls(remotePod, root, opts = {}) {
|
|
42
|
+
return podUrls(remotePod, root || DEFAULT_ROOT, opts);
|
|
78
43
|
}
|
|
79
44
|
|
|
80
45
|
// RFC 6415 host-meta: several fediverse implementations discover WebFinger
|
|
@@ -585,15 +550,31 @@ export function titledContent(note) {
|
|
|
585
550
|
}
|
|
586
551
|
|
|
587
552
|
// Normalize a wire Note's attachment list to { url, mediaType, description }.
|
|
553
|
+
// Capped: this list comes from somebody else's document and lands in a
|
|
554
|
+
// statuses.json row, which is rewritten whole on every change. Twenty is more
|
|
555
|
+
// than any client shows and more than any real post carries.
|
|
556
|
+
const MAX_ATTACHMENTS = 20;
|
|
557
|
+
const MAX_ATTACHMENT_URL = 2048;
|
|
558
|
+
// http(s) only: an attachment URL becomes a client's <img src> or <video src>,
|
|
559
|
+
// and the store has guarded avatars this way all along (safeUrl).
|
|
560
|
+
const attachmentUrl = (u) => {
|
|
561
|
+
if (!u) return null;
|
|
562
|
+
const s = String(u).slice(0, MAX_ATTACHMENT_URL);
|
|
563
|
+
try {
|
|
564
|
+
const p = new URL(s);
|
|
565
|
+
return (p.protocol === 'https:' || p.protocol === 'http:') ? s : null;
|
|
566
|
+
} catch { return null; }
|
|
567
|
+
};
|
|
568
|
+
|
|
588
569
|
export function attachmentsOf(note) {
|
|
589
570
|
const list = Array.isArray(note?.attachment) ? note.attachment : note?.attachment ? [note.attachment] : [];
|
|
590
571
|
// A bare Link carries `href` and no `url` — Lemmy's link posts arrive that
|
|
591
572
|
// way, and reading only `url` dropped the link the post was about.
|
|
592
|
-
return list.map(a => ({
|
|
573
|
+
return list.slice(0, MAX_ATTACHMENTS).map(a => ({
|
|
593
574
|
url: typeof a?.url === 'string' ? a.url : a?.url?.href || (typeof a?.href === 'string' ? a.href : undefined),
|
|
594
|
-
mediaType: a?.mediaType || '',
|
|
595
|
-
...(a?.name ? { description: a.name } : {}),
|
|
596
|
-
})).filter(a => a.url);
|
|
575
|
+
mediaType: String(a?.mediaType || '').slice(0, 128),
|
|
576
|
+
...(a?.name ? { description: String(a.name).slice(0, 1500) } : {}),
|
|
577
|
+
})).map(a => ({ ...a, url: attachmentUrl(a.url) })).filter(a => a.url);
|
|
597
578
|
}
|
|
598
579
|
|
|
599
580
|
const HTML_ESCAPES = { '&': '&', '<': '<', '>': '>' };
|
|
@@ -660,6 +641,45 @@ export function noteDoc({ urls, slug, content, published, inReplyTo, attachments
|
|
|
660
641
|
return note;
|
|
661
642
|
}
|
|
662
643
|
|
|
644
|
+
// A poll is a note that asks something: the same addressing, the same
|
|
645
|
+
// mentions, the same replies collection, with the choices in `oneOf` (pick
|
|
646
|
+
// one) or `anyOf` (pick several). Each choice carries the count as the
|
|
647
|
+
// totalItems of its own replies collection, which is where every server that
|
|
648
|
+
// shows a poll reads it from.
|
|
649
|
+
//
|
|
650
|
+
// `endTime` is when voting stops and `closed` is the stamp saying it has —
|
|
651
|
+
// both plain AS2. `votersCount` is Mastodon's: PEOPLE rather than answers,
|
|
652
|
+
// which differ only once a poll takes several answers each. It is not in the
|
|
653
|
+
// base context, so it is declared inline exactly as Mastodon declares it, and
|
|
654
|
+
// only when we actually carry it.
|
|
655
|
+
export function questionDoc({ options = [], multiple = false, endTime = null,
|
|
656
|
+
closed = null, votersCount = null, ...rest }) {
|
|
657
|
+
const note = noteDoc(rest);
|
|
658
|
+
const choices = options.map(o => ({
|
|
659
|
+
type: 'Note',
|
|
660
|
+
name: String(o.title),
|
|
661
|
+
replies: { type: 'Collection', totalItems: Number(o.votes) || 0 },
|
|
662
|
+
}));
|
|
663
|
+
const question = {
|
|
664
|
+
...note,
|
|
665
|
+
type: 'Question',
|
|
666
|
+
[multiple ? 'anyOf' : 'oneOf']: choices,
|
|
667
|
+
};
|
|
668
|
+
if (endTime) question.endTime = endTime;
|
|
669
|
+
if (closed) question.closed = closed;
|
|
670
|
+
if (votersCount !== null && votersCount !== undefined) {
|
|
671
|
+
question['@context'] = [AS_CTX, {
|
|
672
|
+
toot: 'http://joinmastodon.org/ns#',
|
|
673
|
+
votersCount: {
|
|
674
|
+
'@id': 'toot:votersCount',
|
|
675
|
+
'@type': 'http://www.w3.org/2001/XMLSchema#nonNegativeInteger',
|
|
676
|
+
},
|
|
677
|
+
}];
|
|
678
|
+
question.votersCount = votersCount;
|
|
679
|
+
}
|
|
680
|
+
return question;
|
|
681
|
+
}
|
|
682
|
+
|
|
663
683
|
// The id is a document of its own, not `note.id + '#create'`. A group wraps this
|
|
664
684
|
// whole activity in its Announce (FEP-1b12), and the receiver resolves it by
|
|
665
685
|
// dereferencing this id — a fragment would just serve the Note back under a
|
package/package.json
CHANGED
package/run-agent.mjs
CHANGED
|
@@ -34,7 +34,6 @@ import { ensureTrustedTls } from './lib/certs.mjs';
|
|
|
34
34
|
import { storageFor } from './lib/storage.mjs';
|
|
35
35
|
import { resolveKeys } from './lib/keys.mjs';
|
|
36
36
|
import { RemotePod } from './lib/remote.mjs';
|
|
37
|
-
import { PodRdf } from './lib/podrdf.mjs';
|
|
38
37
|
import { Deliverer } from './lib/deliver.mjs';
|
|
39
38
|
import { Publisher } from './lib/publisher.mjs';
|
|
40
39
|
import { Intake } from './lib/intake.mjs';
|
|
@@ -69,15 +68,14 @@ export class Agent {
|
|
|
69
68
|
|
|
70
69
|
// Where the private half lives. `privateRoot` in the credential file names a
|
|
71
70
|
// container — by default a plain directory beside the credential, and a pod
|
|
72
|
-
// on this machine if you move it there
|
|
73
|
-
// laid out as on the pod. Absent means on the pod, exactly as before, so an
|
|
71
|
+
// on this machine if you move it there. Absent means on the pod, so an
|
|
74
72
|
// existing install is untouched.
|
|
75
73
|
privateUrls(cred, urls = this.urls) {
|
|
76
74
|
if (!cred.privateRoot) {
|
|
77
|
-
return { state: urls.state,
|
|
75
|
+
return { state: urls.state, archive: urls.home + 'inbox-archive/', elsewhere: false };
|
|
78
76
|
}
|
|
79
77
|
const base = cred.privateRoot.endsWith('/') ? cred.privateRoot : cred.privateRoot + '/';
|
|
80
|
-
return { state: base + 'ap-state/',
|
|
78
|
+
return { state: base + 'ap-state/', archive: base + 'inbox-archive/', elsewhere: true };
|
|
81
79
|
}
|
|
82
80
|
|
|
83
81
|
// A container to keep the private half in. On the pod it is reached with the
|
|
@@ -146,14 +144,9 @@ export class Agent {
|
|
|
146
144
|
await this.remote.setAcl(this.urls.home, []);
|
|
147
145
|
await this.remote.setAcl(this.urls.state, []);
|
|
148
146
|
if (priv.elsewhere) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (!r.ok) throw new Error(`private store ${s.base}.keep → ${r.why}`);
|
|
152
|
-
}
|
|
147
|
+
const r = await stateStore.write('.keep', '{"keep":true}\n', 'application/json');
|
|
148
|
+
if (!r.ok) throw new Error(`private store ${stateStore.base}.keep → ${r.why}`);
|
|
153
149
|
this.log(`private state lives at ${cred.privateRoot} (${stateStore.kind})`);
|
|
154
|
-
} else {
|
|
155
|
-
await this.remote.putJson(this.urls.fediverse + '.keep', { keep: true }, 'application/json');
|
|
156
|
-
await this.remote.setAcl(this.urls.fediverse, []);
|
|
157
150
|
}
|
|
158
151
|
this.store.attach(stateStore);
|
|
159
152
|
// Re-running setup must not destroy state set afterwards (the UI
|
|
@@ -314,7 +307,6 @@ export class Agent {
|
|
|
314
307
|
const { rotateKeyOnce, ...rest } = cred;
|
|
315
308
|
writeJsonAtomic(path.join(this.home, 'credential.json'), rest);
|
|
316
309
|
}
|
|
317
|
-
this.local = new PodRdf({ storage: this.privateStorage(cred, 'fediverse') });
|
|
318
310
|
// connect() can run more than once — connectWithRetry retries after a throw,
|
|
319
311
|
// and the CLI connects for real after its confirmation. Deliverer arms a
|
|
320
312
|
// 60s queue timer in its constructor and Intake owns a poll timer and a
|
|
@@ -324,6 +316,7 @@ export class Agent {
|
|
|
324
316
|
this.deliverer?.stop();
|
|
325
317
|
this.tagfeed?.stop();
|
|
326
318
|
clearInterval(this.schedTimer);
|
|
319
|
+
this.publisher?.stopPolls();
|
|
327
320
|
this.deliverer = new Deliverer({
|
|
328
321
|
store: this.store, rsaPrivate: keys.rsaPrivate, keyId: this.urls.actor + '#main-key',
|
|
329
322
|
actorId: this.urls.actor, edPrivate: keys.edPrivate,
|
|
@@ -331,13 +324,10 @@ export class Agent {
|
|
|
331
324
|
log: this.log, passive: this.viewer,
|
|
332
325
|
});
|
|
333
326
|
this.publisher = new Publisher({
|
|
334
|
-
config, remote: this.remote,
|
|
327
|
+
config, remote: this.remote, store: this.store,
|
|
335
328
|
deliverer: this.deliverer, publicKeyPem: keys.rsaPublicPem,
|
|
336
329
|
assertionKey: keys.edPublicMultibase, log: this.log,
|
|
337
330
|
resolveMention: (h) => resolveHandle(this, h),
|
|
338
|
-
// Whether the fediverse tree is on the pod at all, so the ACL check does
|
|
339
|
-
// not probe for something the default layout keeps on local disk.
|
|
340
|
-
privateOnPod: !cred.privateRoot,
|
|
341
331
|
// Inside a pod server the client surface answers on the pod's own
|
|
342
332
|
// origin, so it can be advertised. Standalone it is on loopback, and
|
|
343
333
|
// naming it in a world-readable actor would send clients nowhere.
|
|
@@ -346,7 +336,7 @@ export class Agent {
|
|
|
346
336
|
// Intake is constructed even for viewers — its signed fetchAP powers
|
|
347
337
|
// search/deref; start() (draining) is active-only.
|
|
348
338
|
this.intake = new Intake({
|
|
349
|
-
config, urls: this.urls, remote: this.remote,
|
|
339
|
+
config, urls: this.urls, remote: this.remote, store: this.store,
|
|
350
340
|
deliverer: this.deliverer, publisher: this.publisher, log: this.log, lease: this.lease,
|
|
351
341
|
archive: this.privateStorage(cred, 'archive'),
|
|
352
342
|
push: !this.embedded, pollSeconds: this.pollSeconds || null,
|
|
@@ -547,6 +537,10 @@ export class Agent {
|
|
|
547
537
|
}).then(() => this.log(`scheduled post published (${e.id})`))
|
|
548
538
|
.catch(err => this.log(`scheduled post ${e.id} failed: ${err.message} — dropped`));
|
|
549
539
|
}
|
|
540
|
+
// A poll whose time is up is shut on the same sweep: it stops taking
|
|
541
|
+
// answers here, and everyone holding it is told once.
|
|
542
|
+
this.publisher.closeDuePolls()
|
|
543
|
+
.catch(err => this.log(`closing polls: ${err.message}`));
|
|
550
544
|
}, 30_000);
|
|
551
545
|
this.schedTimer.unref();
|
|
552
546
|
// A CSV import interrupted by a restart or a handoff picks back up here.
|
|
@@ -576,7 +570,7 @@ export class Agent {
|
|
|
576
570
|
this.ensureActorPublished()
|
|
577
571
|
.catch(e => this.log(`actor check failed: ${e.message}`));
|
|
578
572
|
}
|
|
579
|
-
this.
|
|
573
|
+
this.seedFollowNotifications().catch(e => this.log(`notification seeding failed: ${e.message}`));
|
|
580
574
|
}
|
|
581
575
|
|
|
582
576
|
// Re-read state before acting on what we hold. A viewer's cache is kept
|
|
@@ -651,6 +645,7 @@ export class Agent {
|
|
|
651
645
|
this.deliverer?.stop();
|
|
652
646
|
this.importer?.stop();
|
|
653
647
|
clearInterval(this.schedTimer);
|
|
648
|
+
this.publisher?.stopPolls();
|
|
654
649
|
// The lease too: standing down means standing down. Left renewing, a viewer
|
|
655
650
|
// keeps writing to the pod on the active agent's behalf and can win the
|
|
656
651
|
// lease back on a conditional PUT it had no business making.
|
|
@@ -689,27 +684,13 @@ export class Agent {
|
|
|
689
684
|
return true;
|
|
690
685
|
}
|
|
691
686
|
|
|
692
|
-
//
|
|
693
|
-
//
|
|
694
|
-
async
|
|
695
|
-
if (
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
if (this.store.has('statuses.json')) return;
|
|
701
|
-
const entries = [];
|
|
702
|
-
for (const [container, kind] of [['timeline', 'timeline'], ['posts', 'post']]) {
|
|
703
|
-
for (const url of await this.local.listNotes(container)) {
|
|
704
|
-
try {
|
|
705
|
-
const n = await this.local.readNote(url);
|
|
706
|
-
if (n.noteId) entries.push({ ...n, kind, slug: url.split('/').pop() });
|
|
707
|
-
} catch (e) { this.log(`backfill: skipped ${url}: ${e.message}`); }
|
|
708
|
-
}
|
|
687
|
+
// A first run inherits followers without ever having seen them arrive, so
|
|
688
|
+
// the notifications list starts empty where it should start full.
|
|
689
|
+
async seedFollowNotifications() {
|
|
690
|
+
if (this.store.has('notifications.json')) return;
|
|
691
|
+
for (const f of this.store.getContacts().followers) {
|
|
692
|
+
this.store.addNotification({ type: 'follow', actor: f.actor });
|
|
709
693
|
}
|
|
710
|
-
entries.sort((a, b) => String(b.published || '').localeCompare(String(a.published || '')));
|
|
711
|
-
this.store.write('statuses.json', entries.slice(0, 1000));
|
|
712
|
-
this.log(`backfilled ${entries.length} statuses from pod RDF`);
|
|
713
694
|
}
|
|
714
695
|
}
|
|
715
696
|
|
package/web/admin/admin.js
CHANGED
|
@@ -13,8 +13,15 @@ const $ = (id) => document.getElementById(id);
|
|
|
13
13
|
// machine, `/app` when the identity is hosted by its own pod server. Read from
|
|
14
14
|
// the page's own address, so one build serves both.
|
|
15
15
|
const BASE = location.pathname.replace(/\/admin\/.*$/u, '');
|
|
16
|
+
// Every call carries `x-fedipod-page`. On the Node agent it is ignored — the
|
|
17
|
+
// Origin check there already refuses a page on somebody else's site. In the
|
|
18
|
+
// browser build the agent IS this origin's service worker, which cannot see
|
|
19
|
+
// Sec-Fetch-* and answers these routes with no password, so this header is the
|
|
20
|
+
// whole door: a cross-site form cannot set one, and a cross-origin fetch that
|
|
21
|
+
// sets one is stopped at a preflight the worker never grants. See sw-src.mjs.
|
|
22
|
+
const PAGE_HEADER = { 'x-fedipod-page': '1' };
|
|
16
23
|
const api = async (path, init) => {
|
|
17
|
-
const res = await fetch(BASE + path, init);
|
|
24
|
+
const res = await fetch(BASE + path, { ...init, headers: { ...(init?.headers || {}), ...PAGE_HEADER } });
|
|
18
25
|
return { status: res.status, json: await res.json().catch(() => null) };
|
|
19
26
|
};
|
|
20
27
|
const postJson = (path, body) => api(path, {
|
|
@@ -27,14 +34,12 @@ let config = null;
|
|
|
27
34
|
const MODERATION = $('moderation');
|
|
28
35
|
const STATUS = $('status-ctl');
|
|
29
36
|
const STATUS_PICK = $('status-pick'); // held: getElementById can't see it mid-render
|
|
30
|
-
const BSKY_CTL = $('bsky-ctl');
|
|
31
37
|
const IDENT_CTL = $('ident-ctl');
|
|
32
38
|
const FOLLOWS_CTL = $('follows-ctl');
|
|
33
39
|
const FOLLOWS_PICK = $('follows-pick');
|
|
34
40
|
const UPDATE_CTL = $('update-ctl');
|
|
35
41
|
const UPDATE_WORD = $('update-word');
|
|
36
42
|
const UPDATE_GO = $('update-go');
|
|
37
|
-
const BSKY_CROSSPOST = $('bsky-crosspost'); // held: it rides BSKY_CTL into a generated row
|
|
38
43
|
|
|
39
44
|
// #say and #fatal live in the accessibility tree from load and hide by being
|
|
40
45
|
// empty (see the stylesheet). Unhiding a live region and filling it in the same
|
|
@@ -95,10 +100,7 @@ function resetConfirm() {
|
|
|
95
100
|
$('confirm-handle').value = '';
|
|
96
101
|
$('confirm-handle-move').value = '';
|
|
97
102
|
$('move-target').value = '';
|
|
98
|
-
$('
|
|
99
|
-
$('state-url').value = '';
|
|
100
|
-
document.querySelector('input[name=stateWhere][value=path]').checked = true;
|
|
101
|
-
stateRows();
|
|
103
|
+
$('rotate-password').value = '';
|
|
102
104
|
}
|
|
103
105
|
function closePanels(keep = null) {
|
|
104
106
|
if (!keep) solWindow.close();
|
|
@@ -126,34 +128,129 @@ function identityRows() {
|
|
|
126
128
|
dd.append(...nodes);
|
|
127
129
|
out.push([dt, dd]);
|
|
128
130
|
};
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
131
|
+
// One descriptor shape for every connected account — Bluesky and each
|
|
132
|
+
// fediverse account — so the row and its Options menu build the same way.
|
|
133
|
+
const accounts = [];
|
|
134
|
+
if (config.atproto?.connected) accounts.push({
|
|
135
|
+
label: 'Bluesky', handle: `@${config.atproto.handle}`,
|
|
136
|
+
href: `https://bsky.app/profile/${config.atproto.handle}`,
|
|
137
|
+
storage: config.atproto.storage || null,
|
|
138
|
+
feedPaused: 'feedPaused' in config.atproto ? config.atproto.feedPaused : null,
|
|
139
|
+
crossPost: 'crossPost' in config.atproto ? config.atproto.crossPost : null,
|
|
140
|
+
feed: (paused) => ['/atproto', { feedPaused: paused }],
|
|
141
|
+
cross: (on) => ['/atproto', { crossPost: on }],
|
|
142
|
+
store: (w) => ['/atproto', { storage: w }],
|
|
143
|
+
off: () => ['/atproto/disconnect', {}],
|
|
144
|
+
});
|
|
145
|
+
for (const acct of config.fediAccounts || []) accounts.push({
|
|
146
|
+
label: acct.host,
|
|
147
|
+
handle: acct.needsReconnect ? `${acct.handle} — sign in again` : acct.handle,
|
|
148
|
+
warn: acct.needsReconnect,
|
|
149
|
+
storage: acct.storage || null,
|
|
150
|
+
feedPaused: !acct.enabled,
|
|
151
|
+
crossPost: null,
|
|
152
|
+
feed: (paused) => ['/fediacct', { id: acct.id, enabled: !paused }],
|
|
153
|
+
store: (w) => ['/fediacct', { id: acct.id, storage: w }],
|
|
154
|
+
off: () => ['/fediacct/disconnect', { id: acct.id }],
|
|
155
|
+
});
|
|
156
|
+
for (const acct of accounts) {
|
|
157
|
+
const name = document.createElement(acct.href ? 'a' : 'span');
|
|
158
|
+
name.textContent = acct.handle;
|
|
159
|
+
if (acct.href) { name.href = acct.href; name.target = '_blank'; name.rel = 'noopener'; name.title = ' Open this account elsewhere in a new tab'; }
|
|
160
|
+
if (acct.warn) name.className = 'warn';
|
|
161
|
+
// Where storage is a real choice (every browser-build account today), two
|
|
162
|
+
// dropdowns sit in the row itself. Where it isn't (the Node agent's
|
|
163
|
+
// Bluesky, which shows a cross-post toggle instead), the Options
|
|
164
|
+
// disclosure it always has is unchanged.
|
|
165
|
+
row(acct.label, name, ' ', ...(acct.storage ? connectionControls(acct) : [optionsMenu(acct)]));
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Connected, Paused or Disconnected, and where the key lives — one dropdown
|
|
171
|
+
// each, in the row. Replaces the separate feed and connect dropdowns that used
|
|
172
|
+
// to sit behind an Options disclosure; only reached where acct.storage is set.
|
|
173
|
+
function connectionControls(acct) {
|
|
174
|
+
const send = async ([path, payload], msg) => { if (await write(path, payload, msg)) await load(); };
|
|
175
|
+
const pick = (opts, current, onChange, title) => {
|
|
176
|
+
const el = document.createElement('select');
|
|
177
|
+
if (title) el.title = title;
|
|
178
|
+
for (const [v, t] of opts) { const o = document.createElement('option'); o.value = v; o.textContent = t; o.selected = v === current; el.appendChild(o); }
|
|
179
|
+
el.addEventListener('change', () => onChange(el.value));
|
|
180
|
+
return el;
|
|
181
|
+
};
|
|
182
|
+
const conn = pick(
|
|
183
|
+
[['connected', 'Connected'], ['paused', 'Paused'], ['disconnected', 'Disconnected']],
|
|
184
|
+
acct.feedPaused ? 'paused' : 'connected',
|
|
185
|
+
(v) => (v === 'disconnected'
|
|
186
|
+
? send(acct.off(), 'disconnected')
|
|
187
|
+
: send(acct.feed(v === 'paused'), v === 'paused' ? 'paused adding to your feed' : 'added to your home feed')),
|
|
188
|
+
' Connected feeds this account into your home feed here; Paused keeps the account but stops adding its posts; Disconnected removes it.');
|
|
189
|
+
const storage = pick(
|
|
190
|
+
[['pod', 'Store key on Pod'], ['browser', 'Store key in Browser']],
|
|
191
|
+
acct.storage,
|
|
192
|
+
(v) => {
|
|
193
|
+
if (v === 'pod' && !confirm('Store this account on your pod?\n\nIt will follow you to any browser you sign in from — but a full-access token to that account then lives on your pod.')) { load(); return; }
|
|
194
|
+
send(acct.store(v), v === 'pod' ? 'stored on your pod' : 'stored in this browser');
|
|
195
|
+
},
|
|
196
|
+
' On your pod the key follows you to any browser you sign in from, and a full-access token then lives there; in this browser the token never leaves this device.');
|
|
197
|
+
const wrap = document.createElement('span');
|
|
198
|
+
wrap.className = 'conn-controls';
|
|
199
|
+
wrap.append(conn, ' ', storage);
|
|
200
|
+
return [wrap];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Every connected account without a storage choice carries an Options menu
|
|
204
|
+
// instead: pause its feed, its cross-post toggle (the Node agent's Bluesky),
|
|
205
|
+
// disconnect.
|
|
206
|
+
function optionsMenu(acct) {
|
|
207
|
+
const d = document.createElement('details');
|
|
208
|
+
d.className = 'opts';
|
|
209
|
+
const sum = document.createElement('summary');
|
|
210
|
+
sum.textContent = 'Options';
|
|
211
|
+
d.appendChild(sum);
|
|
212
|
+
const body = document.createElement('div');
|
|
213
|
+
d.appendChild(body);
|
|
214
|
+
|
|
215
|
+
// Each control is one dropdown whose options ARE the states, so it needs no
|
|
216
|
+
// separate label — the chosen option reads as the current state.
|
|
217
|
+
const pick = (opts, current, onChange) => {
|
|
218
|
+
const el = document.createElement('select');
|
|
219
|
+
for (const [v, t] of opts) { const o = document.createElement('option'); o.value = v; o.textContent = t; o.selected = v === current; el.appendChild(o); }
|
|
220
|
+
el.addEventListener('change', () => onChange(el.value));
|
|
221
|
+
body.appendChild(el);
|
|
222
|
+
};
|
|
223
|
+
const send = async ([path, payload], msg) => { if (await write(path, payload, msg)) await load(); };
|
|
224
|
+
|
|
225
|
+
// Feed: shown in / hidden from the feed you read here.
|
|
226
|
+
if (acct.feedPaused !== null) {
|
|
227
|
+
pick([['show', 'Add to home feed'], ['hide', 'Pause adding to home feed']], acct.feedPaused ? 'hide' : 'show',
|
|
228
|
+
(v) => send(acct.feed(v === 'hide'), v === 'hide' ? 'paused adding to your feed' : 'added to your home feed'));
|
|
141
229
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
off.className = 'inline danger';
|
|
149
|
-
off.textContent = 'Disconnect';
|
|
150
|
-
off.title = ` Stop reading ${acct.handle} and remove its stored token`;
|
|
151
|
-
off.addEventListener('click', async () => {
|
|
152
|
-
if (await write('/fediacct/disconnect', { id: acct.id }, `${acct.handle} disconnected`)) await load();
|
|
230
|
+
// Storage where the browser offers the choice; the Node agent's Bluesky shows
|
|
231
|
+
// its cross-post toggle in the same slot instead.
|
|
232
|
+
if (acct.storage) {
|
|
233
|
+
pick([['browser', 'Store key in Browser'], ['pod', 'Store key on Pod']], acct.storage, (v) => {
|
|
234
|
+
if (v === 'pod' && !confirm('Store this account on your pod?\n\nIt will follow you to any browser you sign in from — but a full-access token to that account then lives on your pod.')) { load(); return; }
|
|
235
|
+
send(acct.store(v), v === 'pod' ? 'stored on your pod' : 'stored in this browser');
|
|
153
236
|
});
|
|
154
|
-
|
|
237
|
+
} else if (acct.crossPost !== null) {
|
|
238
|
+
pick([['on', 'Cross-post On'], ['off', 'Cross-post Off']], acct.crossPost ? 'on' : 'off',
|
|
239
|
+
(v) => send(acct.cross(v === 'on'), ''));
|
|
155
240
|
}
|
|
156
|
-
|
|
241
|
+
// Connection: staying connected, or disconnecting.
|
|
242
|
+
pick([['connected', 'Connected'], ['disconnect', 'Disconnect']], 'connected',
|
|
243
|
+
(v) => { if (v === 'disconnect') send(acct.off(), 'disconnected'); else load(); });
|
|
244
|
+
|
|
245
|
+
// The storage consequence, at the bottom of the box under every dropdown.
|
|
246
|
+
if (acct.storage) {
|
|
247
|
+
const hint = document.createElement('p'); hint.className = 'opt-hint';
|
|
248
|
+
hint.textContent = acct.storage === 'pod'
|
|
249
|
+
? 'Follows you to every browser you sign in from; a full-access token lives on your pod.'
|
|
250
|
+
: 'Stays in this browser; the token never leaves this device.';
|
|
251
|
+
body.appendChild(hint);
|
|
252
|
+
}
|
|
253
|
+
return d;
|
|
157
254
|
}
|
|
158
255
|
|
|
159
256
|
function render() {
|
|
@@ -180,9 +277,10 @@ function render() {
|
|
|
180
277
|
['Other identities', 'ctl'],
|
|
181
278
|
['local store', config.home],
|
|
182
279
|
// The address you actually open, not the bare number — the named origin when
|
|
183
|
-
// there is one, since that is what the client and the OAuth redirect use.
|
|
184
|
-
|
|
185
|
-
|
|
280
|
+
// there is one, since that is what the client and the OAuth redirect use. An
|
|
281
|
+
// agent with no local host of its own (embedded in a pod server, or in a
|
|
282
|
+
// browser) sends none, and then there is no row rather than a bare number.
|
|
283
|
+
['local host', (origins.named || origins.loopback || '').replace(/\/$/, '') || null],
|
|
186
284
|
];
|
|
187
285
|
if (config.version || config.update || config.pendingUpgrade?.length) rows.push(['software', 'ctl']);
|
|
188
286
|
rows.push(['gateway', 'ctl']);
|
|
@@ -502,13 +600,8 @@ $('fediacct-form').addEventListener('submit', async (ev) => {
|
|
|
502
600
|
} finally { fediBusy = false; }
|
|
503
601
|
});
|
|
504
602
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
bskyBusy = true;
|
|
508
|
-
try {
|
|
509
|
-
if (await write('/atproto/disconnect', {}, 'bluesky account disconnected')) await load();
|
|
510
|
-
} finally { bskyBusy = false; }
|
|
511
|
-
});
|
|
603
|
+
// Disconnecting Bluesky, pausing its feed and choosing its storage all live in
|
|
604
|
+
// the account's Options menu now (see optionsMenu). Connecting is still here.
|
|
512
605
|
// Adding resolves the old account on the agent side, so what lands in
|
|
513
606
|
// alsoKnownAs is its canonical id, not the string typed here.
|
|
514
607
|
// The accounts elsewhere this one may receive a Move from — the chips in the
|
|
@@ -553,15 +646,6 @@ $('alias-add').addEventListener('click', async () => {
|
|
|
553
646
|
} finally { aliasBusy = false; }
|
|
554
647
|
});
|
|
555
648
|
|
|
556
|
-
// Silent on success — the select already shows the new state.
|
|
557
|
-
const setCrossPost = async (on) => {
|
|
558
|
-
if (bskyBusy || on === !!config.atproto?.crossPost) return;
|
|
559
|
-
bskyBusy = true;
|
|
560
|
-
try {
|
|
561
|
-
if (await write('/atproto', { crossPost: on }, '')) config.atproto.crossPost = on;
|
|
562
|
-
BSKY_CROSSPOST.value = config.atproto?.crossPost ? 'on' : 'off';
|
|
563
|
-
} finally { bskyBusy = false; }
|
|
564
|
-
};
|
|
565
649
|
|
|
566
650
|
// ---- group ----
|
|
567
651
|
|
|
@@ -655,8 +739,6 @@ function onPick(el, apply, repaint = renderGroupToggles) {
|
|
|
655
739
|
onPick($('joins-mod'), (v) => setJoins(v === MOD.on));
|
|
656
740
|
onPick($('review-mod'), (v) => setReview(v === MOD.on));
|
|
657
741
|
onPick(STATUS_PICK, (v) => setStatus(v === 'parked'), renderStatus);
|
|
658
|
-
onPick(BSKY_CROSSPOST, (v) => setCrossPost(v === 'on'),
|
|
659
|
-
() => { BSKY_CROSSPOST.value = config?.atproto?.crossPost ? 'on' : 'off'; });
|
|
660
742
|
|
|
661
743
|
// A person's follower gate. Automatic is what a migration wave needs: every
|
|
662
744
|
// follower's server re-follows at once, and the waiting queue caps at 500.
|
|
@@ -897,21 +979,7 @@ const LIFECYCLE = {
|
|
|
897
979
|
retire: { path: '/retire', title: 'Retire this identity', go: 'Retire it', danger: true, done: (r) => `retired ${r.deletedAt}: Delete delivered to ${r.inboxes} inbox(es)` },
|
|
898
980
|
move: { path: '/move', title: 'Transfer this account away', go: 'Transfer it', focus: 'move-target',
|
|
899
981
|
done: (r) => `transferred to ${r.target}: Move delivered to ${r.inboxes} inbox(es), unfollowed ${r.unfollowed}/${r.following}` },
|
|
900
|
-
'move-state': { path: '/state-move', title: 'Move private data', go: 'Move it', focus: 'state-path',
|
|
901
|
-
done: (r) => (r.unchanged ? 'already there — nothing moved'
|
|
902
|
-
: `moved ${r.docs} document(s) and ${r.notes} post(s) — private data is now ${r.now}`) },
|
|
903
982
|
};
|
|
904
|
-
|
|
905
|
-
// The destination choice reveals the field it needs: a directory for this
|
|
906
|
-
// device, an address for another one, nothing for the pod.
|
|
907
|
-
function stateRows() {
|
|
908
|
-
const w = picked('stateWhere');
|
|
909
|
-
$('state-row-path').hidden = w !== 'path';
|
|
910
|
-
$('state-row-url').hidden = w !== 'url';
|
|
911
|
-
}
|
|
912
|
-
for (const el of document.querySelectorAll('input[name=stateWhere]')) {
|
|
913
|
-
el.addEventListener('change', stateRows);
|
|
914
|
-
}
|
|
915
983
|
let pending = null;
|
|
916
984
|
|
|
917
985
|
const closeConfirm = () => closePanels();
|
|
@@ -954,16 +1022,15 @@ $('confirm-form').addEventListener('submit', async (ev) => {
|
|
|
954
1022
|
const what = pending;
|
|
955
1023
|
const body = what === 'retire' ? { confirm: $('confirm-handle').value.trim() }
|
|
956
1024
|
: what === 'move' ? { target: $('move-target').value.trim(), confirm: $('confirm-handle-move').value.trim() }
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1025
|
+
// Only where the field is shown (the browser build — see index.html).
|
|
1026
|
+
// On the Node agent the row stays hidden and nothing is sent, which is
|
|
1027
|
+
// what that agent expects.
|
|
1028
|
+
: what === 'rotate-key' && !$('rotate-pw-row').hidden
|
|
1029
|
+
? { password: $('rotate-password').value }
|
|
961
1030
|
: {};
|
|
962
1031
|
if (what === 'move' && !body.target) { say('name the account to transfer to', 'err'); return; }
|
|
963
|
-
if (what === 'move-state' && !body.to) { say('name the destination', 'err'); return; }
|
|
964
1032
|
$('confirm-go').disabled = true;
|
|
965
|
-
say(what
|
|
966
|
-
: `${what} — this talks to the pod and to other servers, so it takes a moment`);
|
|
1033
|
+
say(`${what} — this talks to the pod and to other servers, so it takes a moment`);
|
|
967
1034
|
const r = await write(LIFECYCLE[what].path, body, what);
|
|
968
1035
|
$('confirm-go').disabled = false;
|
|
969
1036
|
if (!r) return; // write() already said why
|