fedipod-server 0.21.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/claims.d.ts +3 -1
- package/dist/claims.js +20 -7
- package/dist/components/context.jsonld +6 -0
- package/dist/handler.d.ts +29 -1
- package/dist/handler.js +89 -4
- package/dist/handler.jsonld +36 -1
- package/lib/client/c2s.mjs +100 -3
- package/lib/client/masto/accounts.mjs +44 -4
- package/lib/client/masto/body.mjs +90 -3
- package/lib/client/masto/index.mjs +5 -1
- package/lib/client/masto/instance.mjs +11 -3
- package/lib/client/masto/media.mjs +3 -57
- package/lib/client/masto/oauth.mjs +66 -9
- package/lib/client/masto/render.mjs +62 -5
- package/lib/client/masto/statuses.mjs +25 -1
- package/lib/client/masto/timelines.mjs +128 -6
- package/lib/connections/tagfeed.mjs +42 -5
- package/lib/core/as2.mjs +36 -5
- package/lib/core/contexts/index.mjs +14 -0
- package/lib/core/contexts/litepub-0.1.json +60 -0
- package/lib/core/contexts/map.json +3 -1
- package/lib/core/contexts/quotes.json +18 -0
- package/lib/core/graphview.mjs +41 -3
- package/lib/core/intake/activities.mjs +136 -2
- package/lib/core/intake/activity.mjs +79 -1
- package/lib/core/intake/group.mjs +37 -3
- package/lib/core/intake/index.mjs +32 -2
- package/lib/core/intake/notes.mjs +60 -3
- package/lib/core/lease.mjs +12 -2
- package/lib/core/profile-page.mjs +104 -0
- package/lib/core/publisher/collections.mjs +6 -1
- package/lib/core/publisher/index.mjs +35 -17
- package/lib/core/publisher/notes.mjs +104 -3
- package/lib/core/shapes/activitystreams.ttl +28 -0
- package/lib/core/shapes/index.mjs +26 -1
- package/lib/core/wire-quotes.mjs +71 -0
- package/lib/core/wire.mjs +59 -103
- package/lib/device/admin/routes/lifecycle.mjs +9 -0
- package/lib/device/admin/static.mjs +4 -1
- package/lib/device/admin/surface.mjs +26 -2
- package/lib/device/certs.mjs +18 -2
- package/lib/device/cli/commands/setup.mjs +6 -2
- package/lib/device/cli/commands/state.mjs +3 -1
- package/lib/device/gateway-move.mjs +58 -0
- package/lib/device/migrate.mjs +3 -1
- package/lib/device/setup.mjs +67 -7
- package/lib/gateway/front-core.mjs +307 -36
- package/lib/gateway/gateway-core.mjs +15 -6
- package/lib/gateway/notices.mjs +84 -0
- package/lib/gateway/quiet.mjs +184 -0
- package/lib/pod/root.mjs +6 -3
- package/lib/pod/state.mjs +8 -7
- package/lib/pod/transport.mjs +5 -1
- package/lib/session/README.md +123 -0
- package/lib/session/demo.html +94 -0
- package/lib/session/fedi-account.mjs +346 -0
- package/lib/session/fedi-login.mjs +123 -0
- package/lib/session/oidc-session.mjs +233 -0
- package/lib/session/package.json +41 -0
- package/package.json +1 -1
- package/run-agent.mjs +28 -0
- package/vendor/gate.cjs +6 -3
- package/web/admin/bar.css +66 -0
- package/web/admin/bar.js +38 -0
- package/web/admin/client/index.html +31 -3
- package/web/admin/client-pick.js +79 -0
- package/web/admin/gateway.js +22 -0
- package/web/admin/index.html +54 -13
- package/web/admin/notices-bar.js +90 -0
- package/web/admin/oauth-signin.mjs +37 -0
- package/web/admin/record.js +13 -1
- package/web/admin/setup/index.html +31 -3
- package/web/admin/tokens.css +15 -3
- package/web/admin/upkeep.js +4 -5
- package/web/app/README.md +14 -10
- package/web/app/admin-facade.mjs +51 -12
- package/web/app/agent.mjs +157 -19
- package/web/app/authorize_interaction.html +56 -0
- package/web/app/authorize_interaction.js +69 -0
- package/web/app/boot.mjs +233 -95
- package/web/app/dist/boot.js +477 -569
- package/web/app/dist/boot.js.map +4 -4
- package/web/app/dist/sw.js +1861 -532
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/gateway-move.mjs +60 -0
- package/web/app/index.html +40 -49
- package/web/app/keys-browser.mjs +12 -15
- package/web/app/keystore.mjs +19 -36
- package/web/app/oidc-session.mjs +5 -188
- package/web/app/signup.mjs +172 -132
- package/web/app/sw-src.mjs +1 -1
- package/web/app/update.js +66 -0
- package/web/front/admin.html +5 -3
- package/web/front/new-account.html +2 -2
- package/web/front/notices.html +69 -0
- package/web/front/notices.js +107 -0
- package/web/front/run.html +15 -8
- package/web/front/run.js +124 -36
- package/web/icons/forum.svg +11 -0
- package/web/icons/gateway.svg +14 -0
- package/web/icons/local.svg +11 -0
- package/web/app/index.html~ +0 -152
- package/web/app/site/_headers +0 -9
- package/web/app/site/_redirects +0 -22
- package/web/app/site/admin/actors.js +0 -145
- package/web/app/site/admin/bar.css +0 -60
- package/web/app/site/admin/bar.js +0 -61
- package/web/app/site/admin/client/client.js +0 -66
- package/web/app/site/admin/client/index.html +0 -43
- package/web/app/site/admin/common.js +0 -23
- package/web/app/site/admin/connections.js +0 -112
- package/web/app/site/admin/gateway.js +0 -111
- package/web/app/site/admin/group.js +0 -258
- package/web/app/site/admin/index.html +0 -564
- package/web/app/site/admin/record.js +0 -378
- package/web/app/site/admin/setup/index.html +0 -215
- package/web/app/site/admin/setup/setup.js +0 -326
- package/web/app/site/admin/tokens.css +0 -56
- package/web/app/site/admin/upkeep.js +0 -178
- package/web/app/site/admin/window.css +0 -55
- package/web/app/site/admin/window.js +0 -136
- package/web/app/site/app/404.html +0 -40
- package/web/app/site/app/_headers +0 -2
- package/web/app/site/app/apple-touch-icon.png +0 -0
- package/web/app/site/app/assets/ICONS-DIPJeU0C.js +0 -2
- package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +0 -1
- package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +0 -183
- package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
- package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
- package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +0 -2
- package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +0 -1
- package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
- package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
- package/web/app/site/app/assets/chunk-aKtaBQYM.js +0 -1
- package/web/app/site/app/assets/compose-CFEx7wb4.js +0 -2
- package/web/app/site/app/assets/compose-CFEx7wb4.js.map +0 -1
- package/web/app/site/app/assets/compose-ZJN5xSb4.js +0 -35
- package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +0 -1
- package/web/app/site/app/assets/debug.module-YFQgwnIr.js +0 -52
- package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +0 -1
- package/web/app/site/app/assets/dom-p1YcJ5Rw.js +0 -2
- package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +0 -1
- package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
- package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
- package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
- package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
- package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +0 -1
- package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +0 -1
- package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
- package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +0 -1
- package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
- package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
- package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
- package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
- package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
- package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
- package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
- package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
- package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +0 -1
- package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +0 -1
- package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
- package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +0 -1
- package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
- package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +0 -1
- package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
- package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
- package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +0 -1
- package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
- package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +0 -1
- package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
- package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
- package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
- package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
- package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
- package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +0 -1
- package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +0 -1
- package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +0 -1
- package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +0 -1
- package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +0 -1
- package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
- package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +0 -1
- package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
- package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +0 -1
- package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +0 -1
- package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +0 -1
- package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
- package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
- package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +0 -1
- package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +0 -1
- package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
- package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
- package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +0 -1
- package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +0 -1
- package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +0 -1
- package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
- package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +0 -1
- package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +0 -1
- package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +0 -1
- package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
- package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
- package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
- package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +0 -1
- package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +0 -1
- package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +0 -1
- package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +0 -1
- package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +0 -1
- package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
- package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
- package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +0 -1
- package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +0 -1
- package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +0 -1
- package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +0 -1
- package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +0 -1
- package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +0 -1
- package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +0 -1
- package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +0 -1
- package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
- package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +0 -1
- package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +0 -1
- package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +0 -1
- package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
- package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +0 -1
- package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +0 -1
- package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
- package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
- package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
- package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +0 -1
- package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +0 -1
- package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
- package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +0 -1
- package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +0 -1
- package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +0 -1
- package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +0 -1
- package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +0 -1
- package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +0 -1
- package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +0 -1
- package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +0 -1
- package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +0 -1
- package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
- package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +0 -1
- package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
- package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
- package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
- package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +0 -1
- package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
- package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +0 -1
- package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +0 -1
- package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
- package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +0 -1
- package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
- package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +0 -1
- package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
- package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +0 -1
- package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +0 -1
- package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
- package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +0 -1
- package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +0 -1
- package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
- package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +0 -1
- package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +0 -1
- package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +0 -1
- package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
- package/web/app/site/app/assets/instances-BNPptnn-.json +0 -333
- package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +0 -1
- package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
- package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +0 -1
- package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +0 -1
- package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +0 -1
- package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +0 -1
- package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +0 -1
- package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +0 -1
- package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +0 -1
- package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +0 -1
- package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +0 -1
- package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +0 -1
- package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +0 -1
- package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +0 -1
- package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
- package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
- package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +0 -1
- package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
- package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +0 -1
- package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +0 -1
- package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
- package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/tok-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +0 -1
- package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +0 -1
- package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/main-BdqNbG-a.js +0 -26
- package/web/app/site/app/assets/main-BdqNbG-a.js.map +0 -1
- package/web/app/site/app/assets/mock-home-DGhR__He.js +0 -2
- package/web/app/site/app/assets/mock-home-DGhR__He.js.map +0 -1
- package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
- package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
- package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
- package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +0 -2
- package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +0 -1
- package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +0 -20
- package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +0 -1
- package/web/app/site/app/assets/style-B1iLPTPR.css +0 -2
- package/web/app/site/app/assets/temml-DuyIQMei.js +0 -97
- package/web/app/site/app/assets/temml-DuyIQMei.js.map +0 -1
- package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +0 -2
- package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
- package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
- package/web/app/site/app/assets/year-in-posts-DJv_star.js +0 -3
- package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +0 -1
- package/web/app/site/app/compose/index.html +0 -20
- package/web/app/site/app/favicon.ico +0 -0
- package/web/app/site/app/index.html +0 -73
- package/web/app/site/app/logo-192.png +0 -0
- package/web/app/site/app/logo-512.png +0 -0
- package/web/app/site/app/logo-badge-72.png +0 -0
- package/web/app/site/app/logo-maskable-512.png +0 -0
- package/web/app/site/app/logo-monochrome-512.png +0 -0
- package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
- package/web/app/site/app/manifest.webmanifest +0 -1
- package/web/app/site/app/og-image-2.jpg +0 -0
- package/web/app/site/app/og-image.png +0 -0
- package/web/app/site/app/robots.txt +0 -2
- package/web/app/site/app/version.json +0 -1
- package/web/app/site/boot.js +0 -34277
- package/web/app/site/index.html +0 -251
- package/web/app/site/sw.js +0 -71331
package/lib/core/wire.mjs
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
// protocol documents, not our RDF.
|
|
4
4
|
|
|
5
5
|
export const AS_CTX = 'https://www.w3.org/ns/activitystreams';
|
|
6
|
+
// Lemmy's own context, held locally (lib/core/contexts/join-lemmy.json). It
|
|
7
|
+
// names the three terms a Lemmy reader looks for on a community and its
|
|
8
|
+
// posts: postingRestrictedToMods, stickied, commentsEnabled.
|
|
9
|
+
export const LEMMY_CTX = 'https://join-lemmy.org/context.json';
|
|
6
10
|
export const SEC_CTX = 'https://w3id.org/security/v1';
|
|
7
11
|
export const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
|
|
8
12
|
|
|
@@ -80,7 +84,8 @@ export const assertionKeyId = (urls) => urls.actor + '#ed25519-key';
|
|
|
80
84
|
export function actorDoc({ urls, handle, name, publicKeyPem, assertionKey = null, movedTo = null, kind = 'person',
|
|
81
85
|
approveJoins = false, summary = null, icon = null, image = null, fields = [],
|
|
82
86
|
webId = null, aliases = [], moderators = null, pendingFollowers = null, pendingFollowing = null,
|
|
83
|
-
blocked = null, inbox = null, outbox = null, oauthAuthorize = null, oauthToken = null
|
|
87
|
+
blocked = null, inbox = null, outbox = null, oauthAuthorize = null, oauthToken = null,
|
|
88
|
+
postingRestrictedToMods = null }) {
|
|
84
89
|
// manuallyApprovesFollowers is NOT in the base AS2 context, so it is declared
|
|
85
90
|
// inline exactly as Mastodon declares it — and only when we actually use it.
|
|
86
91
|
// It is what makes a client show "Request to follow" rather than "Follow" and
|
|
@@ -118,13 +123,18 @@ export function actorDoc({ urls, handle, name, publicKeyPem, assertionKey = null
|
|
|
118
123
|
// old accounts elsewhere whose servers check for their own URL here before
|
|
119
124
|
// they will send a Move. Declared inline exactly as Mastodon declares it.
|
|
120
125
|
if (webId || aliases.length) context.push({ alsoKnownAs: { '@id': 'as:alsoKnownAs', '@type': '@id' } });
|
|
126
|
+
// A community says whether only its moderators may open posts in it, in
|
|
127
|
+
// Lemmy's term, declared through Lemmy's context. Groups only.
|
|
128
|
+
if (kind === 'group' && postingRestrictedToMods !== null) context.push(LEMMY_CTX);
|
|
121
129
|
if (fields.length) {
|
|
122
130
|
context.push({ schema: 'http://schema.org#', PropertyValue: 'schema:PropertyValue', value: 'schema:value' });
|
|
123
131
|
}
|
|
124
132
|
return {
|
|
125
133
|
'@context': context,
|
|
126
134
|
id: urls.actor,
|
|
127
|
-
|
|
135
|
+
// A group is a community; an application is a service that speaks for a
|
|
136
|
+
// site rather than a person — the actor a forum publishes for itself.
|
|
137
|
+
type: kind === 'group' ? 'Group' : kind === 'application' ? 'Application' : 'Person',
|
|
128
138
|
...(approveJoins ? { manuallyApprovesFollowers: true } : {}),
|
|
129
139
|
...(assertionKey ? { assertionMethod: [{
|
|
130
140
|
id: assertionKeyId(urls), type: 'Multikey', controller: urls.actor,
|
|
@@ -132,6 +142,7 @@ export function actorDoc({ urls, handle, name, publicKeyPem, assertionKey = null
|
|
|
132
142
|
}] } : {}),
|
|
133
143
|
...(movedTo ? { movedTo } : {}),
|
|
134
144
|
...(webId || aliases.length ? { alsoKnownAs: [...(webId ? [webId] : []), ...aliases] } : {}),
|
|
145
|
+
...(kind === 'group' && postingRestrictedToMods !== null ? { postingRestrictedToMods: !!postingRestrictedToMods } : {}),
|
|
135
146
|
preferredUsername: handle,
|
|
136
147
|
name: name || handle,
|
|
137
148
|
// The bio, and the avatar. For a group, `summary` is where what the group
|
|
@@ -196,102 +207,7 @@ export function actorDoc({ urls, handle, name, publicKeyPem, assertionKey = null
|
|
|
196
207
|
// the actor document. It says who this is and how to follow — the handle to
|
|
197
208
|
// paste, and the remote-follow control that hands the visitor to their own
|
|
198
209
|
// server's follow screen.
|
|
199
|
-
|
|
200
|
-
// the pinned posts as { content (sanitized HTML), published, url }.
|
|
201
|
-
export function profilePageHtml({ name, address, summary = null, icon = null, image = null,
|
|
202
|
-
fields = [], joined = null, pinned = [], kind = 'person' }) {
|
|
203
|
-
const esc = (s) => String(s ?? '').replace(/[&<>"]/g, c => ({ ...HTML_ESCAPES, '"': '"' }[c]));
|
|
204
|
-
const what = kind === 'group' ? 'a group' : 'an account';
|
|
205
|
-
const month = (iso) => {
|
|
206
|
-
const d = new Date(iso);
|
|
207
|
-
return Number.isNaN(d.getTime()) ? null
|
|
208
|
-
: d.toLocaleDateString('en', { month: 'long', year: 'numeric', timeZone: 'UTC' });
|
|
209
|
-
};
|
|
210
|
-
const day = (iso) => {
|
|
211
|
-
const d = new Date(iso);
|
|
212
|
-
return Number.isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 10);
|
|
213
|
-
};
|
|
214
|
-
const fieldRows = fields.filter(f => f?.name && f?.value !== undefined && f?.value !== null && String(f.value) !== '');
|
|
215
|
-
const joinedText = joined ? month(joined) : null;
|
|
216
|
-
const pins = pinned.filter(x => x && typeof x.content === 'string');
|
|
217
|
-
return `<!doctype html>
|
|
218
|
-
<html lang="en">
|
|
219
|
-
<head>
|
|
220
|
-
<meta charset="utf-8">
|
|
221
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
222
|
-
<title>${esc(name)} (${esc(address)})</title>
|
|
223
|
-
<style>
|
|
224
|
-
:root { color-scheme: light dark; }
|
|
225
|
-
body { font: 112.5%/1.5 system-ui, sans-serif; max-width: 34rem; margin: 3rem auto; padding: 0 1rem; }
|
|
226
|
-
img.header { width: 100%; max-height: 12rem; object-fit: cover; border-radius: 1rem; }
|
|
227
|
-
img.avatar { width: 6rem; height: 6rem; border-radius: 1rem; object-fit: cover; margin-top: 1rem; }
|
|
228
|
-
h1 { margin: .5rem 0 0; }
|
|
229
|
-
.address { font-size: 1.1rem; user-select: all; }
|
|
230
|
-
dl.fields { display: grid; grid-template-columns: max-content 1fr; gap: .4rem 1rem; margin: 1.5rem 0; }
|
|
231
|
-
dl.fields dt { font-weight: 600; }
|
|
232
|
-
dl.fields dd { margin: 0; overflow-wrap: anywhere; }
|
|
233
|
-
.joined { margin: 1rem 0; }
|
|
234
|
-
section.pinned h2 { font-size: 1.1rem; margin: 2rem 0 .5rem; }
|
|
235
|
-
article.pin { border: 1px solid #8884; border-radius: 1rem; padding: 1rem 1.2rem; margin: 1rem 0; }
|
|
236
|
-
article.pin p:last-child { margin-bottom: 0; }
|
|
237
|
-
article.pin .when { font-size: 1rem; }
|
|
238
|
-
form { margin-top: 2rem; }
|
|
239
|
-
label { display: block; margin-bottom: .3rem; }
|
|
240
|
-
input { font: inherit; padding: .5rem; width: 14rem; max-width: 100%; }
|
|
241
|
-
button { font: inherit; padding: .5rem 1rem; }
|
|
242
|
-
.hint { color: #666; font-size: 1rem; }
|
|
243
|
-
.err { color: #b00020; }
|
|
244
|
-
.err:empty { display: none; }
|
|
245
|
-
@media (prefers-color-scheme: dark) {
|
|
246
|
-
.hint { color: #aaa; }
|
|
247
|
-
.err { color: #ff8a80; }
|
|
248
|
-
}
|
|
249
|
-
</style>
|
|
250
|
-
</head>
|
|
251
|
-
<body>
|
|
252
|
-
<main>
|
|
253
|
-
${image ? `<img class="header" src="${esc(image)}" alt="">` : ''}
|
|
254
|
-
${icon ? `<img class="avatar" src="${esc(icon)}" alt="">` : ''}
|
|
255
|
-
<h1>${esc(name)}</h1>
|
|
256
|
-
<p class="address">${esc(address)}</p>
|
|
257
|
-
${summary ? `<div>${summary}</div>` : ''}
|
|
258
|
-
${fieldRows.length ? `<dl class="fields">
|
|
259
|
-
${fieldRows.map(f => `<dt>${esc(f.name)}</dt><dd>${esc(f.value)}</dd>`).join('\n')}
|
|
260
|
-
</dl>` : ''}
|
|
261
|
-
${joinedText ? `<p class="joined">Joined ${esc(joinedText)}</p>` : ''}
|
|
262
|
-
${pins.length ? `<section class="pinned">
|
|
263
|
-
<h2>Pinned</h2>
|
|
264
|
-
${pins.map(x => `<article class="pin">
|
|
265
|
-
${x.content}
|
|
266
|
-
<p class="when">${x.url ? `<a href="${esc(x.url)}">${esc(day(x.published))}</a>` : esc(day(x.published))}</p>
|
|
267
|
-
</article>`).join('\n')}
|
|
268
|
-
</section>` : ''}
|
|
269
|
-
<p>This is ${what} on the Fediverse. To follow it, paste the address above
|
|
270
|
-
into the search box of Mastodon or any Fediverse app — or use the form.</p>
|
|
271
|
-
<form id="follow">
|
|
272
|
-
<label for="server">your server</label>
|
|
273
|
-
<input id="server" type="text" placeholder="mastodon.social" autocomplete="off"
|
|
274
|
-
required aria-describedby="follow-err">
|
|
275
|
-
<button type="submit">Follow</button>
|
|
276
|
-
<p class="err" id="follow-err" role="alert"></p>
|
|
277
|
-
</form>
|
|
278
|
-
<p class="hint">The form sends you to your own server's follow screen.</p>
|
|
279
|
-
</main>
|
|
280
|
-
<script>
|
|
281
|
-
document.getElementById('follow').addEventListener('submit', (ev) => {
|
|
282
|
-
ev.preventDefault();
|
|
283
|
-
const s = document.getElementById('server').value.trim().replace(/^https?:\\/\\//, '').replace(/\\/.*$/, '');
|
|
284
|
-
const err = document.getElementById('follow-err');
|
|
285
|
-
const addr = document.querySelector('.address').textContent.trim();
|
|
286
|
-
if (!s) { err.textContent = 'Enter your server, e.g. mastodon.social'; return; }
|
|
287
|
-
err.textContent = '';
|
|
288
|
-
location.href = 'https://' + s + '/authorize_interaction?uri=' + encodeURIComponent(addr);
|
|
289
|
-
});
|
|
290
|
-
</script>
|
|
291
|
-
</body>
|
|
292
|
-
</html>
|
|
293
|
-
`;
|
|
294
|
-
}
|
|
210
|
+
export { profilePageHtml } from './profile-page.mjs';
|
|
295
211
|
|
|
296
212
|
// Retirement. An abandoned pod keeps accepting deliveries into a container
|
|
297
213
|
// nobody will ever drain, and no remote server has any way to learn that. A
|
|
@@ -692,18 +608,47 @@ export function addressing(urls, visibility, who = []) {
|
|
|
692
608
|
}[visibility] || { to: [PUBLIC], cc: [urls.followers, ...who] };
|
|
693
609
|
}
|
|
694
610
|
|
|
611
|
+
// The quote terms, declared exactly as Mastodon declares them on a post:
|
|
612
|
+
// FEP-044f's `quote` and `quoteAuthorization`, and the two older spellings
|
|
613
|
+
// (Fedibird's quoteUri, Misskey's _misskey_quote) that older servers read.
|
|
614
|
+
export const QUOTE_CTX = {
|
|
615
|
+
quote: { '@id': 'https://w3id.org/fep/044f#quote', '@type': '@id' },
|
|
616
|
+
quoteUri: { '@id': 'http://fedibird.com/ns#quoteUri', '@type': '@id' },
|
|
617
|
+
_misskey_quote: { '@id': 'https://misskey-hub.net/ns#_misskey_quote', '@type': '@id' },
|
|
618
|
+
quoteAuthorization: { '@id': 'https://w3id.org/fep/044f#quoteAuthorization', '@type': '@id' },
|
|
619
|
+
};
|
|
620
|
+
// The interaction policy terms, GoToSocial's, declared as Mastodon declares them.
|
|
621
|
+
export const POLICY_CTX = {
|
|
622
|
+
gts: 'https://gotosocial.org/ns#',
|
|
623
|
+
interactionPolicy: { '@id': 'gts:interactionPolicy', '@type': '@id' },
|
|
624
|
+
canQuote: { '@id': 'gts:canQuote', '@type': '@id' },
|
|
625
|
+
automaticApproval: { '@id': 'gts:automaticApproval', '@type': '@id' },
|
|
626
|
+
manualApproval: { '@id': 'gts:manualApproval', '@type': '@id' },
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
// Who may quote a post: anyone, for a post the world can read; nobody, for a
|
|
630
|
+
// followers-only or direct one. Said on every note, because a server that
|
|
631
|
+
// finds no policy offers no quote button.
|
|
632
|
+
export function quotePolicy(visibility) {
|
|
633
|
+
const open = visibility === 'public' || visibility === 'unlisted';
|
|
634
|
+
return { canQuote: { automaticApproval: open ? [PUBLIC] : [], manualApproval: [] } };
|
|
635
|
+
}
|
|
636
|
+
|
|
695
637
|
// `also`: actors addressed without being mentioned — the parent's author on
|
|
696
638
|
// a reply. In cc but not in tag, so their server receives the post and does
|
|
697
639
|
// not notify them. A direct post names its audience in the text alone.
|
|
640
|
+
// `quote`: the post this one quotes, `{ id, authorization }` — the
|
|
641
|
+
// authorization is the quoted author's QuoteAuthorization, once granted.
|
|
698
642
|
export function noteDoc({ urls, slug, content, published, inReplyTo, attachments, mentions = [],
|
|
699
|
-
visibility = 'public', summary = null, sensitive = false, updated = null, container = null, also = []
|
|
643
|
+
visibility = 'public', summary = null, sensitive = false, updated = null, container = null, also = [],
|
|
644
|
+
quote = null }) {
|
|
700
645
|
const id = (container || urls.notes) + slug;
|
|
701
646
|
const who = mentions.map(m => m.actor);
|
|
702
647
|
const extra = visibility === 'direct' ? [] : also.filter(a => a && !who.includes(a));
|
|
703
648
|
const addressed = addressing(urls, visibility, [...who, ...extra]);
|
|
704
649
|
const hashtags = hashtagsIn(content).map(name => ({ name, href: tagHref(urls, name) }));
|
|
705
650
|
const note = {
|
|
706
|
-
'@context': AS_CTX,
|
|
651
|
+
'@context': [AS_CTX, { ...POLICY_CTX, ...(quote?.id ? QUOTE_CTX : {}) }],
|
|
707
652
|
id, type: 'Note',
|
|
708
653
|
attributedTo: urls.actor,
|
|
709
654
|
content: contentHtml(content, mentions, hashtags),
|
|
@@ -711,7 +656,15 @@ export function noteDoc({ urls, slug, content, published, inReplyTo, attachments
|
|
|
711
656
|
to: addressed.to,
|
|
712
657
|
cc: addressed.cc,
|
|
713
658
|
replies: repliesId(id),
|
|
659
|
+
interactionPolicy: quotePolicy(visibility),
|
|
714
660
|
};
|
|
661
|
+
if (quote?.id) {
|
|
662
|
+
// All three spellings name the same post, as Mastodon writes them.
|
|
663
|
+
note.quote = quote.id;
|
|
664
|
+
note.quoteUri = quote.id;
|
|
665
|
+
note._misskey_quote = quote.id;
|
|
666
|
+
if (quote.authorization) note.quoteAuthorization = quote.authorization;
|
|
667
|
+
}
|
|
715
668
|
if (summary) note.summary = summary; // the content warning
|
|
716
669
|
// A content warning is what a receiver hides behind; media marked
|
|
717
670
|
// sensitive without one is hidden the same way.
|
|
@@ -759,7 +712,7 @@ export function questionDoc({ options = [], multiple = false, endTime = null,
|
|
|
759
712
|
if (endTime) question.endTime = endTime;
|
|
760
713
|
if (closed) question.closed = closed;
|
|
761
714
|
if (votersCount !== null && votersCount !== undefined) {
|
|
762
|
-
question['@context'] = [
|
|
715
|
+
question['@context'] = [...[].concat(note['@context']), {
|
|
763
716
|
toot: 'http://joinmastodon.org/ns#',
|
|
764
717
|
votersCount: {
|
|
765
718
|
'@id': 'toot:votersCount',
|
|
@@ -875,10 +828,13 @@ export function updateActorActivity({ urls, actor, serial, published = new Date(
|
|
|
875
828
|
// An edit: same Note id, `updated` stamped, so receivers replace their copy
|
|
876
829
|
// and show it as edited. The activity id carries the stamp — each edit is a
|
|
877
830
|
// new activity, and a replay of an old one is already seen.
|
|
878
|
-
|
|
831
|
+
// `serial` names the activity when the note is not being edited — a quote
|
|
832
|
+
// authorization arriving on it changes what the note says about its quote,
|
|
833
|
+
// not what it says.
|
|
834
|
+
export function updateActivity(note, urls, { serial = null } = {}) {
|
|
879
835
|
return {
|
|
880
836
|
'@context': AS_CTX,
|
|
881
|
-
id: note.id + '#update-' +
|
|
837
|
+
id: note.id + '#update-' + (note.updated ? String(note.updated).replace(/[^0-9TZ]/g, '') : 'q' + (serial ?? Date.now())),
|
|
882
838
|
type: 'Update',
|
|
883
839
|
actor: urls.actor,
|
|
884
840
|
to: note.to, cc: note.cc,
|
|
@@ -14,6 +14,15 @@ export async function post(p, body, ctx, req, res) { // eslint-disable-line no
|
|
|
14
14
|
// lease exists to keep to a single agent. A viewer must claim it first
|
|
15
15
|
// and give up if it cannot — the facade has refused viewer writes since
|
|
16
16
|
// multi-device landed, and these two were simply missed.
|
|
17
|
+
// The account back in this agent's hands. A viewer publishes nothing and
|
|
18
|
+
// drains nothing, so a lease left behind by something that is gone leaves
|
|
19
|
+
// the account silent with nothing in the interface to say so.
|
|
20
|
+
case '/takeover': {
|
|
21
|
+
if (!await agent.requestTakeover?.()) {
|
|
22
|
+
return json(res, 503, { error: 'the lease could not be taken — the pod refused the write' });
|
|
23
|
+
}
|
|
24
|
+
return json(res, 200, { ok: true, mode: agent.status().mode });
|
|
25
|
+
}
|
|
17
26
|
case '/drain': {
|
|
18
27
|
if (!await agent.requestTakeover?.()) {
|
|
19
28
|
return json(res, 503, { error: 'another agent is active for this pod — it is doing the draining' });
|
|
@@ -52,7 +52,10 @@ const UI_DIR = path.join(projectRoot, 'ui'); // extra client dists: ui/<na
|
|
|
52
52
|
// One surface, /admin/, with setup as its first section: /admin/setup/ is the
|
|
53
53
|
// first run, /admin/ is the record, and there is room for the rest.
|
|
54
54
|
const WEB_DIR = path.join(projectRoot, 'web');
|
|
55
|
-
|
|
55
|
+
// `icons` alongside `admin`: the favicons the agent's own pages name. One
|
|
56
|
+
// file each, no authority behind them, and the same jail as everything else
|
|
57
|
+
// under web/.
|
|
58
|
+
const WEB_MOUNTS = ['admin', 'icons'];
|
|
56
59
|
export const webMount = (pathname) => {
|
|
57
60
|
const seg = decodeURIComponent(pathname).replace(/^\/+/, '').split('/')[0];
|
|
58
61
|
return WEB_MOUNTS.includes(seg) ? seg : null;
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// served off disk, and the dispatch to the routes modules beside it. Each of
|
|
7
7
|
// those exports get() and post() over the same ctx.
|
|
8
8
|
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
9
11
|
import { MastoApi } from '../../client/masto/index.mjs';
|
|
10
12
|
import { C2S } from '../../client/c2s.mjs';
|
|
11
13
|
import { makeC2sAuth } from '../../client/oidc-auth.mjs';
|
|
@@ -75,15 +77,27 @@ const ROUTES = [owner, setup, lifecycle, gateway, social, connections];
|
|
|
75
77
|
// meaning inside a server that is not one — and moves the gate: a pod that is
|
|
76
78
|
// a fediverse instance must let strangers reach /api and /oauth, so the gate
|
|
77
79
|
// guards the operator's door (basePath) instead of the whole surface.
|
|
80
|
+
const SESSION_FILES = {
|
|
81
|
+
'/oauth/session/signin.mjs': 'web/admin/oauth-signin.mjs',
|
|
82
|
+
'/oauth/session/fedi-login.mjs': 'lib/session/fedi-login.mjs',
|
|
83
|
+
'/oauth/session/oidc-session.mjs': 'lib/session/oidc-session.mjs',
|
|
84
|
+
};
|
|
85
|
+
|
|
78
86
|
export function buildAdminSurface({ agent, gate, allowed, log = console.log,
|
|
79
87
|
port = null, handle = null, embedded = false, basePath = '/', mount = '',
|
|
80
88
|
publicOrigin = null, scheme = null,
|
|
81
89
|
versionOnDisk = () => localVersion(projectRoot) }) {
|
|
82
90
|
const json = (res, status, obj) => sendJson(res, status, obj, allowed);
|
|
83
|
-
const masto = new MastoApi({ agent, log, allowed, scheme, embedded, mount
|
|
91
|
+
const masto = new MastoApi({ agent, log, allowed, scheme, embedded, mount,
|
|
92
|
+
throughDoor: (req) => Boolean(gate?.upgradeOk?.(req)) });
|
|
93
|
+
// A client's sign-in can be proved with the owner's pod sign-in instead of a
|
|
94
|
+
// password: the same verification the post door uses, and the owner it names.
|
|
95
|
+
masto.podAuth = makeC2sAuth({ agent, log, scheme, mount });
|
|
96
|
+
masto.ownerWebId = () => agent.remote?.webId || null;
|
|
84
97
|
// The spec's own write API (§6), beside the facade. Its bearer fallback is
|
|
85
98
|
// the facade's token, so the two surfaces share one notion of the operator.
|
|
86
|
-
const c2s = new C2S({ agent, log,
|
|
99
|
+
const c2s = new C2S({ agent, log, scheme, mount,
|
|
100
|
+
auth: makeC2sAuth({ agent, masto, log, scheme, mount }) });
|
|
87
101
|
const streaming = new Streaming({ masto, log, allowed, gate, gateOptional: embedded });
|
|
88
102
|
// Asked per request, not once here: startAdmin runs before connect, so the
|
|
89
103
|
// kind is not known yet at mount time.
|
|
@@ -171,6 +185,16 @@ export function buildAdminSurface({ agent, gate, allowed, log = console.log,
|
|
|
171
185
|
if (p === '/ap/outbox' || p === '/ap/actor' || p === '/ap/inbox') {
|
|
172
186
|
if (await c2s.handle(req, res, p, url)) return;
|
|
173
187
|
}
|
|
188
|
+
// The sign-in page's script and the library it signs in with, outside
|
|
189
|
+
// the door like the sign-in itself. A fixed list; nothing in the request
|
|
190
|
+
// names a file.
|
|
191
|
+
const sessionFile = SESSION_FILES[p];
|
|
192
|
+
if (sessionFile) {
|
|
193
|
+
let body;
|
|
194
|
+
try { body = fs.readFileSync(path.join(projectRoot, sessionFile)); } catch { return json(res, 404, { error: 'not here' }); }
|
|
195
|
+
res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-cache' });
|
|
196
|
+
res.end(body); return;
|
|
197
|
+
}
|
|
174
198
|
// Where a client looks first to find out how to sign in (RFC 8414), and
|
|
175
199
|
// in front of the door for the same reason C2S is: a client that has to
|
|
176
200
|
// be handed a secret before it can ask how to sign in cannot set itself
|
package/lib/device/certs.mjs
CHANGED
|
@@ -71,7 +71,16 @@ function nameConstraintsExtension() {
|
|
|
71
71
|
return { id: '2.5.29.30', critical: true, value: der.getBytes() };
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
// The issuer's own key identifier, for the certificate it signs to name. A
|
|
75
|
+
// verifier that finds several CAs under one name — this machine's browser
|
|
76
|
+
// store has held dozens of "FediPod Local CA" from scratch agents — picks by
|
|
77
|
+
// this, and without it picks by name, tries the wrong key and fails.
|
|
78
|
+
const issuerKeyId = (issuerCertPem) => {
|
|
79
|
+
const ext = forge.pki.certificateFromPem(issuerCertPem).getExtension('subjectKeyIdentifier');
|
|
80
|
+
return ext?.subjectKeyIdentifier ? forge.util.hexToBytes(ext.subjectKeyIdentifier) : null;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function makeCert({ subjectCN, issuer = null, issuerKeyPem = null, issuerCertPem = null, isCA = false, days, extraDns = [] }) {
|
|
75
84
|
const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 });
|
|
76
85
|
const cert = forge.pki.createCertificate();
|
|
77
86
|
cert.publicKey = keys.publicKey;
|
|
@@ -89,6 +98,9 @@ function makeCert({ subjectCN, issuer = null, issuerKeyPem = null, isCA = false,
|
|
|
89
98
|
: [{ name: 'basicConstraints', cA: false },
|
|
90
99
|
{ name: 'keyUsage', digitalSignature: true, keyEncipherment: true },
|
|
91
100
|
{ name: 'extKeyUsage', serverAuth: true },
|
|
101
|
+
{ name: 'subjectKeyIdentifier' },
|
|
102
|
+
...(issuerCertPem && issuerKeyId(issuerCertPem)
|
|
103
|
+
? [{ name: 'authorityKeyIdentifier', keyIdentifier: issuerKeyId(issuerCertPem) }] : []),
|
|
92
104
|
{ name: 'subjectAltName',
|
|
93
105
|
altNames: [...SANS, ...extraDns.map(v => ({ type: 2, value: v }))] }]);
|
|
94
106
|
const signingKey = issuerKeyPem ? forge.pki.privateKeyFromPem(issuerKeyPem) : keys.privateKey;
|
|
@@ -131,6 +143,7 @@ export function ensureLocalTls(dir, { log = () => {}, names = [] } = {}) {
|
|
|
131
143
|
const trust = !!(caKey && caCert);
|
|
132
144
|
const isStale = (k, c) => !k || !c || daysLeft(c) < REMINT_DAYS || !coversLocalhost(c)
|
|
133
145
|
|| (trust && !issuedBy(c, caCert))
|
|
146
|
+
|| (trust && !namesIssuerKey(c))
|
|
134
147
|
|| names.some(n => !certDnsNames(c).includes(n));
|
|
135
148
|
let key = readPem(dir, FILES.key);
|
|
136
149
|
let cert = readPem(dir, FILES.cert);
|
|
@@ -165,7 +178,7 @@ export function ensureLocalTls(dir, { log = () => {}, names = [] } = {}) {
|
|
|
165
178
|
.filter(n => n !== 'localhost' && n !== '*.localhost');
|
|
166
179
|
const minted = makeCert({
|
|
167
180
|
subjectCN: 'localhost',
|
|
168
|
-
...(trust ? { issuer: caSubject(caCert), issuerKeyPem: caKey } : {}),
|
|
181
|
+
...(trust ? { issuer: caSubject(caCert), issuerKeyPem: caKey, issuerCertPem: caCert } : {}),
|
|
169
182
|
days: LEAF_DAYS,
|
|
170
183
|
extraDns: carried,
|
|
171
184
|
});
|
|
@@ -181,6 +194,9 @@ export function ensureLocalTls(dir, { log = () => {}, names = [] } = {}) {
|
|
|
181
194
|
return { key, cert, trust, dir, expiresInDays: Math.floor(daysLeft(cert)) };
|
|
182
195
|
}
|
|
183
196
|
|
|
197
|
+
const namesIssuerKey = (certPem) => {
|
|
198
|
+
try { return !!forge.pki.certificateFromPem(certPem).getExtension('authorityKeyIdentifier'); } catch { return false; }
|
|
199
|
+
};
|
|
184
200
|
const caSubject = (caCertPem) => forge.pki.certificateFromPem(caCertPem).subject.attributes
|
|
185
201
|
.map(a => ({ name: a.name, value: a.value }));
|
|
186
202
|
const issuedBy = (certPem, caCertPem) => {
|
|
@@ -52,8 +52,12 @@ if (!newAccount) {
|
|
|
52
52
|
const { resourceExists } = await import(new URL('../../../../lib/pod/root.mjs', import.meta.url));
|
|
53
53
|
const { apUrls, DEFAULT_ROOT: DR } = await import(new URL('../../../../lib/core/wire.mjs', import.meta.url));
|
|
54
54
|
if (await resourceExists(fetch, apUrls(pod, DR).actor)) {
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
if (String(flag('address') || 'pod').toLowerCase() !== 'front') {
|
|
56
|
+
console.error('The pod already hosts a FediPod account. If you want a second account, put it on a different pod.');
|
|
57
|
+
console.error('To move an account whose address is at another gateway to this one, add --address front.');
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
console.log('The pod already holds an account; if its address is at another gateway, setup moves it here.');
|
|
57
61
|
}
|
|
58
62
|
}
|
|
59
63
|
|
|
@@ -289,7 +289,9 @@ for (const { name, dir } of homes) {
|
|
|
289
289
|
// The kind lives in pod state, so it is only knowable from the live probe —
|
|
290
290
|
// a stopped identity honestly shows nothing rather than a guess.
|
|
291
291
|
rows.push({ name, pod: pod ? new URL(pod).host : '(no credential)', port: port || '—',
|
|
292
|
-
|
|
292
|
+
// A home can hold a forum, which says so itself; anything else that
|
|
293
|
+
// answers is a person unless it says group.
|
|
294
|
+
kind: live?.kind === 'group' ? 'group' : live?.kind === 'forum' ? 'forum' : live ? 'person' : '—',
|
|
293
295
|
state: live ? `${live.mode}${live.podRequests ? ` · ${live.podRequests.perMinuteNow}/min` : ''}` : 'not running' });
|
|
294
296
|
}
|
|
295
297
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// gateway-move.mjs — the second half of a DeviceAgent moving an address from
|
|
2
|
+
// one gateway to another (lib/device/setup.mjs does the first, when a pod
|
|
3
|
+
// that already holds an account at another gateway is set up with an address
|
|
4
|
+
// at this one). The browser build has the same two halves in
|
|
5
|
+
// web/app/gateway-move.mjs; this is the Node one, which signs and delivers
|
|
6
|
+
// itself instead of going through a relay.
|
|
7
|
+
//
|
|
8
|
+
// 1. The old gateway is told the address moved (POST /api/move, proved with
|
|
9
|
+
// the pod session as attach was). From then on it serves the old actor
|
|
10
|
+
// as a moved stub — same key, old id, `movedTo` the new one.
|
|
11
|
+
// 2. A Move goes to every follower, FROM the old actor, signed under the
|
|
12
|
+
// old key id: the only signature a follower's server can verify against
|
|
13
|
+
// the old actor. Same key material; only the key id differs.
|
|
14
|
+
//
|
|
15
|
+
// Runs when the agent starts acting, and is safe to run again: a move left
|
|
16
|
+
// pending (the old gateway unreachable) is tried at the next start.
|
|
17
|
+
import { Deliverer } from '../core/deliver.mjs';
|
|
18
|
+
import { moveActivity } from '../core/wire.mjs';
|
|
19
|
+
|
|
20
|
+
export async function completeGatewayMove(agent, { makeDeliverer = (o) => new Deliverer(o) } = {}) {
|
|
21
|
+
const cfg = agent.store.getConfig();
|
|
22
|
+
const mv = cfg?.movedFrom;
|
|
23
|
+
if (!mv || mv.completedAt) return null;
|
|
24
|
+
const log = agent.log || (() => {});
|
|
25
|
+
const newActor = agent.urls.actor;
|
|
26
|
+
|
|
27
|
+
// 1. the old gateway
|
|
28
|
+
let told;
|
|
29
|
+
try {
|
|
30
|
+
told = await agent.remote.session.fetch(`${mv.gateway}/api/move`, {
|
|
31
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
32
|
+
body: JSON.stringify({ handle: mv.handle, movedTo: newActor }),
|
|
33
|
+
});
|
|
34
|
+
} catch (e) { told = { status: 0, statusText: e.message }; }
|
|
35
|
+
if (told.status !== 200) {
|
|
36
|
+
log(`gateway move: ${mv.gateway} did not mark @${mv.handle} as moved (${told.status || told.statusText}) — will retry at the next start`);
|
|
37
|
+
return { told: false };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. the Move, from the old address
|
|
41
|
+
const contacts = agent.store.getContacts();
|
|
42
|
+
const inboxes = [...new Set(contacts.followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean))];
|
|
43
|
+
const activity = moveActivity({ actor: mv.actor }, newActor, Date.now());
|
|
44
|
+
const sender = makeDeliverer({
|
|
45
|
+
store: agent.store, rsaPrivate: agent.deliverer.rsaPrivate,
|
|
46
|
+
keyId: `${mv.actor}#main-key`, actorId: mv.actor, log, passive: true,
|
|
47
|
+
});
|
|
48
|
+
let sent = 0; let failed = 0;
|
|
49
|
+
for (const inbox of inboxes) {
|
|
50
|
+
try { await sender.deliverNow(inbox, activity); sent++; }
|
|
51
|
+
catch (e) { failed++; log(`gateway move: Move to ${inbox} failed: ${e.message}`); }
|
|
52
|
+
}
|
|
53
|
+
const done = { ...mv, completedAt: new Date().toISOString(), moveSent: sent, moveFailed: failed };
|
|
54
|
+
agent.store.setConfig({ ...agent.store.getConfig(), movedFrom: done });
|
|
55
|
+
await agent.store.flush?.();
|
|
56
|
+
log(`moved from ${mv.actor} to ${newActor}: Move sent to ${sent} inbox(es)${failed ? `, ${failed} failed` : ''}`);
|
|
57
|
+
return done;
|
|
58
|
+
}
|
package/lib/device/migrate.mjs
CHANGED
|
@@ -27,7 +27,9 @@ export const layoutOf = (cred) => Number(cred?.layout) || 0;
|
|
|
27
27
|
// deliberately, with `--private-root`, has said where they want it; a migration
|
|
28
28
|
// that overrode that would be moving data on its own initiative.
|
|
29
29
|
export function needsStateMove(cred) {
|
|
30
|
-
|
|
30
|
+
// `stateOnPod` is a move-in from a browser account (setup.mjs): the state
|
|
31
|
+
// is on the pod because that is where the account already lives.
|
|
32
|
+
return !cred?.privateRoot && !cred?.stateOnPod;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export function pendingSteps(cred) {
|
package/lib/device/setup.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { rootOf, recordLastUsed, writeJsonAtomic } from './home.mjs';
|
|
|
21
21
|
import { insecureUrlReason } from '../shared/safefetch.mjs';
|
|
22
22
|
import { resourceExists } from '../pod/root.mjs';
|
|
23
23
|
import { CURRENT_LAYOUT, isCurrent } from './migrate.mjs';
|
|
24
|
+
import { completeGatewayMove } from './gateway-move.mjs';
|
|
24
25
|
|
|
25
26
|
const SOLID = $rdf.Namespace('http://www.w3.org/ns/solid/terms#');
|
|
26
27
|
|
|
@@ -125,6 +126,18 @@ export function frontedAddress({ pod, shape }) {
|
|
|
125
126
|
return pathPod || shape === 'front';
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
// The account a pod already holds, read as its owner: the config and the key
|
|
130
|
+
// record in its state container. Both null when there is none, or when the
|
|
131
|
+
// credential is not the owner's. Injected for tests.
|
|
132
|
+
export async function readPodAccount({ home, credential, pod, root, log = () => {} }) {
|
|
133
|
+
const remote = new RemotePod(credential, { home, log });
|
|
134
|
+
await remote.warmup?.();
|
|
135
|
+
const urls = apUrls(pod, root || DEFAULT_ROOT);
|
|
136
|
+
const config = await remote.getJson(urls.state + 'config.json').catch(() => null);
|
|
137
|
+
const keys = await remote.getJson(urls.state + 'keys.json').catch(() => null);
|
|
138
|
+
return { config, keys };
|
|
139
|
+
}
|
|
140
|
+
|
|
128
141
|
// Take an address at a Gateway: attach the pod to it, fronted, and return the
|
|
129
142
|
// gateway config bootstrap writes and connect reads. The pod session proves
|
|
130
143
|
// the pod — no password reaches the Gateway. Injected for tests.
|
|
@@ -231,8 +244,13 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
|
|
|
231
244
|
// several writes, and that pod is a server somebody runs. `state --to` moves
|
|
232
245
|
// it afterwards; starting on the pod and moving later is strictly worse,
|
|
233
246
|
// because the copy left behind was on the pod the whole time.
|
|
234
|
-
|
|
247
|
+
let privateRoot = answers.privateRoot
|
|
235
248
|
|| pathToFileURL(path.join(home, 'private')).href + '/';
|
|
249
|
+
// A pod that already holds an account at ANOTHER gateway, set up with an
|
|
250
|
+
// address at this one: not a second account but the same one moving in.
|
|
251
|
+
// Its state and key are on the pod, and stay there.
|
|
252
|
+
let movingIn = false;
|
|
253
|
+
let movedFrom = null;
|
|
236
254
|
// Everything that leaves this function — notes, errors, the log — goes
|
|
237
255
|
// through here. Nothing today echoes a password back, but /setup/progress is
|
|
238
256
|
// polled repeatedly and would re-serve one forever if anything ever did.
|
|
@@ -266,10 +284,15 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
|
|
|
266
284
|
// reachable, and no silent 401 later on a pod whose profile is empty.
|
|
267
285
|
const usable = await checkPod(pod);
|
|
268
286
|
if (!usable.ok) throw new Error(usable.error);
|
|
269
|
-
|
|
270
|
-
|
|
287
|
+
const held = await (deps.resourceExists || resourceExists)(deps.fetch || fetch, apUrls(pod, root || DEFAULT_ROOT).actor);
|
|
288
|
+
if (held && !frontedAddress({ pod, shape })) {
|
|
289
|
+
throw new Error('The pod already hosts a FediPod account. If you want a second account, put it on a different pod. '
|
|
290
|
+
+ 'To move an account whose address is at another gateway to this one, ask for an address at this gateway.');
|
|
271
291
|
}
|
|
272
|
-
|
|
292
|
+
if (held && kind === 'group') throw new Error('The pod already hosts a FediPod account, and a group cannot move between gateways.');
|
|
293
|
+
movingIn = held;
|
|
294
|
+
if (movingIn) { privateRoot = null; }
|
|
295
|
+
skip('account', movingIn ? 'the pod already holds the account — its address moves here' : 'using the pod you already have');
|
|
273
296
|
}
|
|
274
297
|
|
|
275
298
|
// --- credential: the point of no return, and the durability boundary ---
|
|
@@ -295,12 +318,14 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
|
|
|
295
318
|
remotePod: pod.endsWith('/') ? pod : pod + '/',
|
|
296
319
|
createdAt: new Date().toISOString(),
|
|
297
320
|
...(root ? { root } : {}),
|
|
298
|
-
...(keys === 'pod' ? { keysMode: 'pod' } : {}),
|
|
299
|
-
// Where the private half lives. Per-machine, like keysMode.
|
|
321
|
+
...(keys === 'pod' || movingIn ? { keysMode: 'pod' } : {}),
|
|
322
|
+
// Where the private half lives. Per-machine, like keysMode. A move-in
|
|
323
|
+
// keeps it on the pod, where the account already is, and says so.
|
|
300
324
|
...(privateRoot ? { privateRoot } : {}),
|
|
325
|
+
...(movingIn ? { stateOnPod: true } : {}),
|
|
301
326
|
// What shape this install is, so `upgrade` can tell an old one from a
|
|
302
327
|
// new one without guessing from the fields.
|
|
303
|
-
...(isCurrent({ privateRoot }) ? { layout: CURRENT_LAYOUT } : {}),
|
|
328
|
+
...(isCurrent({ privateRoot, stateOnPod: movingIn }) ? { layout: CURRENT_LAYOUT } : {}),
|
|
304
329
|
};
|
|
305
330
|
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
306
331
|
writeJsonAtomic(credPath, rec);
|
|
@@ -316,6 +341,24 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
|
|
|
316
341
|
done('credential', credPath);
|
|
317
342
|
}
|
|
318
343
|
|
|
344
|
+
// --- a move-in: the account on the pod, read with the credential just
|
|
345
|
+
// minted, has to be one whose address lives at another gateway ---
|
|
346
|
+
if (movingIn) {
|
|
347
|
+
const cred = JSON.parse(fs.readFileSync(credPath, 'utf8'));
|
|
348
|
+
const account = await (deps.readPodAccount || readPodAccount)({ home, credential: cred, pod, root: root || DEFAULT_ROOT, log });
|
|
349
|
+
if (!account?.config) throw new Error('the pod holds an account, but its record could not be read with this credential — is this account the pod\'s owner?');
|
|
350
|
+
const frontActor = account.config.gateway?.frontActor || null;
|
|
351
|
+
const gwHost = new URL(gatewayOrigin).host;
|
|
352
|
+
if (!frontActor) throw new Error('The pod already hosts a FediPod account whose address lives on the pod: nothing to move. Sign in to it instead.');
|
|
353
|
+
if (new URL(frontActor).host === gwHost) throw new Error(`The pod already hosts a FediPod account at ${gwHost}: sign in to it instead of setting up again.`);
|
|
354
|
+
if ((account.config.kind || 'person') === 'group') throw new Error('a group cannot move between gateways');
|
|
355
|
+
if (!account.keys?.rsa || account.keys.ct) {
|
|
356
|
+
throw new Error(`this account's key is still under a password from an earlier version — sign in once at ${new URL(frontActor).host} first, then set up here`);
|
|
357
|
+
}
|
|
358
|
+
movedFrom = { actor: frontActor, gateway: new URL(frontActor).origin, handle: account.config.handle, at: new Date().toISOString() };
|
|
359
|
+
log(`moving @${account.config.handle}@${new URL(frontActor).host} here, as @${handle}@${gwHost}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
319
362
|
// --- take an address at the Gateway, if this pod needs one or asked for
|
|
320
363
|
// one --- before bootstrap, so the config it writes carries the Gateway
|
|
321
364
|
// ids and the key connect mints below is stamped to the Gateway actor from
|
|
@@ -346,6 +389,17 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
|
|
|
346
389
|
if (!ready.ok) throw new Error(ready.error);
|
|
347
390
|
}
|
|
348
391
|
await agent.bootstrap({ handle, name: name || handle, root, kind, approveJoins, summary, icon, gateway: gatewayCfg });
|
|
392
|
+
if (movingIn && movedFrom) {
|
|
393
|
+
// The old actor becomes an alias, which is what a server checks on the
|
|
394
|
+
// new actor before it honours a Move; the Move itself is pending until
|
|
395
|
+
// the new actor is published (completeGatewayMove, below and at every
|
|
396
|
+
// start). The key stays; its stamp follows the actor.
|
|
397
|
+
const cfg = agent.store.getConfig() || {};
|
|
398
|
+
agent.store.setConfig({ ...cfg, aliases: [...new Set([...(cfg.aliases || []), movedFrom.actor])], movedFrom });
|
|
399
|
+
const keyRec = agent.store.read?.('keys.json', null);
|
|
400
|
+
if (keyRec?.mintedFor && gatewayCfg?.frontActor) agent.store.write?.('keys.json', { ...keyRec, mintedFor: gatewayCfg.frontActor });
|
|
401
|
+
await agent.store.flush();
|
|
402
|
+
}
|
|
349
403
|
done('bootstrap');
|
|
350
404
|
|
|
351
405
|
begin('connect');
|
|
@@ -360,6 +414,12 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
|
|
|
360
414
|
}
|
|
361
415
|
const published = await agent.publisher.publishProfile();
|
|
362
416
|
await agent.store.flush();
|
|
417
|
+
if (movingIn) {
|
|
418
|
+
// Tell the old gateway and the followers. Not fatal: left pending, it
|
|
419
|
+
// is tried again when the agent next starts acting.
|
|
420
|
+
const moved = await (deps.completeGatewayMove || completeGatewayMove)(agent).catch((e) => { log(`gateway move: ${e.message}`); return null; });
|
|
421
|
+
if (moved?.completedAt) log(`Move sent from ${movedFrom.actor} to ${moved.moveSent} inbox(es)`);
|
|
422
|
+
}
|
|
363
423
|
done('publish');
|
|
364
424
|
|
|
365
425
|
// --- and say plainly whether the world can actually see it ---
|