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
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// oidc-session.mjs — Solid-OIDC login for a browser page, with a secure,
|
|
2
|
+
// durable, worker-visible session.
|
|
3
|
+
//
|
|
4
|
+
// The redirect handshake (authorization-code + PKCE) runs in the page. The
|
|
5
|
+
// session it yields — a DPoP keypair whose private half is NON-EXTRACTABLE, plus
|
|
6
|
+
// the refresh token, the token endpoint and the WebID — is kept in IndexedDB.
|
|
7
|
+
// IndexedDB survives a browser restart and is readable by a service worker,
|
|
8
|
+
// and a non-extractable key can sign but never be read out, even by injected
|
|
9
|
+
// script.
|
|
10
|
+
//
|
|
11
|
+
// import { solidOidcSession } from 'solid-oidc-session/oidc-session.mjs';
|
|
12
|
+
// const oidc = solidOidcSession({ dbName: 'my-app-oidc', clientName: 'My app' });
|
|
13
|
+
//
|
|
14
|
+
// Page: const { authorizationUrl } = await oidc.beginLogin({ issuer, redirectUri });
|
|
15
|
+
// location.href = authorizationUrl; // ... redirect ...
|
|
16
|
+
// await oidc.completeLogin({ currentUrl: location.href }); // on return
|
|
17
|
+
// Anywhere (page or worker, SAME dbName):
|
|
18
|
+
// const session = await oidc.getSession(); // { webId, issuer, fetch, refresh, signOut } | null
|
|
19
|
+
//
|
|
20
|
+
// No imports, no Node built-ins, nothing named after the application that
|
|
21
|
+
// uses it: IndexedDB, WebCrypto and fetch are all it needs.
|
|
22
|
+
|
|
23
|
+
const STORE = 'session';
|
|
24
|
+
const b64u = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
25
|
+
const enc = (o) => b64u(new TextEncoder().encode(JSON.stringify(o)));
|
|
26
|
+
const sha256 = (s) => crypto.subtle.digest('SHA-256', new TextEncoder().encode(s));
|
|
27
|
+
const rand = (n = 32) => b64u(crypto.getRandomValues(new Uint8Array(n)));
|
|
28
|
+
|
|
29
|
+
async function dpopKey() { return crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']); }
|
|
30
|
+
async function publicJwk(pair) { const j = await crypto.subtle.exportKey('jwk', pair.publicKey); return { kty: j.kty, crv: j.crv, x: j.x, y: j.y }; }
|
|
31
|
+
async function dpopProof(pair, jwk, htm, htu, ath) {
|
|
32
|
+
const header = { typ: 'dpop+jwt', alg: 'ES256', jwk };
|
|
33
|
+
const payload = { htm, htu: htu.split('#')[0], jti: crypto.randomUUID(), iat: Math.floor(Date.now() / 1000), ...(ath ? { ath } : {}) };
|
|
34
|
+
const data = `${enc(header)}.${enc(payload)}`;
|
|
35
|
+
const sig = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, pair.privateKey, new TextEncoder().encode(data));
|
|
36
|
+
return `${data}.${b64u(sig)}`;
|
|
37
|
+
}
|
|
38
|
+
const jwtPayload = (jwt) => { try { return JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(jwt.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0)))); } catch { return {}; } };
|
|
39
|
+
|
|
40
|
+
async function discover(issuer) {
|
|
41
|
+
const res = await fetch(`${issuer.replace(/\/+$/, '')}/.well-known/openid-configuration`, { headers: { accept: 'application/json' } });
|
|
42
|
+
if (!res.ok) throw new Error(`OIDC discovery failed at ${issuer} (HTTP ${res.status})`);
|
|
43
|
+
return res.json();
|
|
44
|
+
}
|
|
45
|
+
async function registerClient(cfg, redirectUri, clientName) {
|
|
46
|
+
const res = await fetch(cfg.registration_endpoint, {
|
|
47
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
48
|
+
body: JSON.stringify({ client_name: clientName, redirect_uris: [redirectUri], token_endpoint_auth_method: 'none',
|
|
49
|
+
grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], application_type: 'web', scope: 'openid webid offline_access' }),
|
|
50
|
+
});
|
|
51
|
+
if (!res.ok) throw new Error(`client registration failed (HTTP ${res.status})`);
|
|
52
|
+
return (await res.json()).client_id;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* One session store. `dbName` is the IndexedDB database the session lives in:
|
|
57
|
+
* a page and its service worker must pass the same name to see the same
|
|
58
|
+
* sign-in, and two apps on one origin pass different names to keep theirs
|
|
59
|
+
* apart. `clientName` is what the pod's login screen shows the person.
|
|
60
|
+
*/
|
|
61
|
+
export function solidOidcSession({ dbName = 'solid-oidc-session', clientName = 'Solid app' } = {}) {
|
|
62
|
+
function idb() {
|
|
63
|
+
return new Promise((res, rej) => {
|
|
64
|
+
const r = indexedDB.open(dbName, 1);
|
|
65
|
+
r.onupgradeneeded = () => r.result.createObjectStore(STORE);
|
|
66
|
+
r.onsuccess = () => res(r.result);
|
|
67
|
+
r.onerror = () => rej(r.error);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async function idbGet(key) {
|
|
71
|
+
const db = await idb();
|
|
72
|
+
return new Promise((res, rej) => { const t = db.transaction(STORE).objectStore(STORE).get(key); t.onsuccess = () => res(t.result); t.onerror = () => rej(t.error); });
|
|
73
|
+
}
|
|
74
|
+
async function idbPut(key, val) {
|
|
75
|
+
const db = await idb();
|
|
76
|
+
return new Promise((res, rej) => { const t = db.transaction(STORE, 'readwrite').objectStore(STORE).put(val, key); t.onsuccess = () => res(); t.onerror = () => rej(t.error); });
|
|
77
|
+
}
|
|
78
|
+
async function idbDel(key) {
|
|
79
|
+
const db = await idb();
|
|
80
|
+
return new Promise((res, rej) => { const t = db.transaction(STORE, 'readwrite').objectStore(STORE).delete(key); t.onsuccess = () => res(); t.onerror = () => rej(t.error); });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Begin login: prepare PKCE + a non-extractable DPoP key, stash the pending
|
|
84
|
+
* state, and return the authorization URL for the caller to navigate to. */
|
|
85
|
+
async function beginLogin({ issuer, redirectUri, clientName: name = clientName, returnTo = null }) {
|
|
86
|
+
const cfg = await discover(issuer);
|
|
87
|
+
const client_id = await registerClient(cfg, redirectUri, name);
|
|
88
|
+
const pair = await dpopKey();
|
|
89
|
+
const verifier = rand(48);
|
|
90
|
+
const challenge = b64u(await sha256(verifier));
|
|
91
|
+
const state = rand(16);
|
|
92
|
+
await idbPut('pending', { issuer, client_id, redirectUri, verifier, state, pair, returnTo,
|
|
93
|
+
tokenEndpoint: cfg.token_endpoint, authorizationEndpoint: cfg.authorization_endpoint,
|
|
94
|
+
revocationEndpoint: cfg.revocation_endpoint || null });
|
|
95
|
+
const url = new URL(cfg.authorization_endpoint);
|
|
96
|
+
for (const [k, v] of Object.entries({ client_id, redirect_uri: redirectUri, response_type: 'code',
|
|
97
|
+
scope: 'openid webid offline_access', code_challenge: challenge, code_challenge_method: 'S256', state, prompt: 'consent' })) url.searchParams.set(k, v);
|
|
98
|
+
return { authorizationUrl: url.href };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Complete login from the redirect back. Exchanges the code (DPoP + PKCE),
|
|
102
|
+
* stores the durable session, and returns it. Returns null if there is no code. */
|
|
103
|
+
async function completeLogin({ currentUrl }) {
|
|
104
|
+
const u = new URL(currentUrl);
|
|
105
|
+
const code = u.searchParams.get('code');
|
|
106
|
+
const state = u.searchParams.get('state');
|
|
107
|
+
if (!code) return null;
|
|
108
|
+
const p = await idbGet('pending');
|
|
109
|
+
if (!p || p.state !== state) throw new Error('login state mismatch');
|
|
110
|
+
const jwk = await publicJwk(p.pair);
|
|
111
|
+
const res = await fetch(p.tokenEndpoint, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded', dpop: await dpopProof(p.pair, jwk, 'POST', p.tokenEndpoint) },
|
|
114
|
+
body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: p.redirectUri, client_id: p.client_id, code_verifier: p.verifier }),
|
|
115
|
+
});
|
|
116
|
+
const tok = await res.json().catch(() => ({}));
|
|
117
|
+
if (!res.ok || !tok.access_token) throw new Error(`token exchange failed (HTTP ${res.status}): ${tok.error || ''}`);
|
|
118
|
+
const webId = jwtPayload(tok.access_token).webid || jwtPayload(tok.id_token || '').webid || null;
|
|
119
|
+
const session = { issuer: p.issuer, client_id: p.client_id, tokenEndpoint: p.tokenEndpoint, pair: p.pair,
|
|
120
|
+
// Where to hand the refresh token back at sign-out. Kept on the session
|
|
121
|
+
// because discovery is a network round trip and sign-out should not need
|
|
122
|
+
// one — see signOut().
|
|
123
|
+
revocationEndpoint: p.revocationEndpoint || null,
|
|
124
|
+
refreshToken: tok.refresh_token || null, accessToken: tok.access_token,
|
|
125
|
+
expiresAt: Date.now() + Math.max(30, (tok.expires_in || 300)) * 1000, webId };
|
|
126
|
+
await idbPut('session', session);
|
|
127
|
+
await idbDel('pending');
|
|
128
|
+
// `returnTo` rides only on the handle this call returns: where the person
|
|
129
|
+
// was when they left for the login page, for the caller to send them back.
|
|
130
|
+
return Object.assign(sessionHandle(session), p.returnTo ? { returnTo: p.returnTo } : {});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Restore the session from IndexedDB — works in the page and the worker. */
|
|
134
|
+
async function getSession() {
|
|
135
|
+
const s = await idbGet('session');
|
|
136
|
+
return s ? sessionHandle(s) : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Sign out, and tell the issuer.
|
|
141
|
+
*
|
|
142
|
+
* Dropping the row locally leaves the REFRESH TOKEN alive at the pod for its
|
|
143
|
+
* full life — signing out of a shared or borrowed browser would clear the
|
|
144
|
+
* screen and nothing else, and anyone who had copied the token could go on
|
|
145
|
+
* minting access tokens with it. RFC 7009: hand it back.
|
|
146
|
+
*
|
|
147
|
+
* Best effort, and the local teardown happens either way: an issuer that is
|
|
148
|
+
* unreachable, or that publishes no revocation endpoint, must not be able to
|
|
149
|
+
* keep somebody signed in.
|
|
150
|
+
*/
|
|
151
|
+
async function signOut() {
|
|
152
|
+
const s = await idbGet('session').catch(() => null);
|
|
153
|
+
if (s?.revocationEndpoint && s.refreshToken) {
|
|
154
|
+
try {
|
|
155
|
+
await fetch(s.revocationEndpoint, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
158
|
+
body: new URLSearchParams({
|
|
159
|
+
token: s.refreshToken, token_type_hint: 'refresh_token', client_id: s.client_id,
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
} catch { /* told if we could; leaving is not conditional on being heard */ }
|
|
163
|
+
}
|
|
164
|
+
await idbDel('session');
|
|
165
|
+
await idbDel('pending');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function sessionHandle(s) {
|
|
169
|
+
let { accessToken, expiresAt, refreshToken } = s;
|
|
170
|
+
const jwkP = publicJwk(s.pair);
|
|
171
|
+
const refresh = async () => {
|
|
172
|
+
if (!refreshToken) throw new Error('session expired and there is no refresh token — sign in again');
|
|
173
|
+
const jwk = await jwkP;
|
|
174
|
+
let res;
|
|
175
|
+
try {
|
|
176
|
+
res = await fetch(s.tokenEndpoint, {
|
|
177
|
+
method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', dpop: await dpopProof(s.pair, jwk, 'POST', s.tokenEndpoint) },
|
|
178
|
+
body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: s.client_id, scope: 'openid webid offline_access' }),
|
|
179
|
+
});
|
|
180
|
+
} catch (e) {
|
|
181
|
+
// A renewal that never reached the server is not a refused one: the
|
|
182
|
+
// session is still good, and saying "your pod refused the write" about
|
|
183
|
+
// it sends whoever reads it looking in the wrong place. Under load a
|
|
184
|
+
// server refuses the token endpoint first, and a refusal with no CORS
|
|
185
|
+
// headers arrives here as the browser's bare "Failed to fetch".
|
|
186
|
+
throw new Error(`${e.message} — renewing your sign-in at ${new URL(s.tokenEndpoint).host}`);
|
|
187
|
+
}
|
|
188
|
+
const tok = await res.json().catch(() => ({}));
|
|
189
|
+
if (res.status === 429) {
|
|
190
|
+
throw new Error(`${new URL(s.tokenEndpoint).host} is asking us to slow down — your sign-in could not be renewed just now`);
|
|
191
|
+
}
|
|
192
|
+
if (!res.ok || !tok.access_token) { await idbDel('session'); throw new Error('refresh failed — sign in again'); }
|
|
193
|
+
accessToken = tok.access_token;
|
|
194
|
+
expiresAt = Date.now() + Math.max(30, (tok.expires_in || 300)) * 1000;
|
|
195
|
+
if (tok.refresh_token) refreshToken = tok.refresh_token;
|
|
196
|
+
// Re-read before writing back. There is ONE 'session' row, and this handle
|
|
197
|
+
// closed over the session it was made from — so a refresh that was already
|
|
198
|
+
// in flight when the user signed in as somebody else would put the OLD
|
|
199
|
+
// identity back, silently, over the new one. Only write if the row is still
|
|
200
|
+
// the session this handle belongs to.
|
|
201
|
+
const now = await idbGet('session');
|
|
202
|
+
if (now && now.webId !== s.webId) {
|
|
203
|
+
throw new Error('signed in as somebody else while this session was refreshing');
|
|
204
|
+
}
|
|
205
|
+
await idbPut('session', { ...s, accessToken, expiresAt, refreshToken });
|
|
206
|
+
};
|
|
207
|
+
const send = async (url, init) => {
|
|
208
|
+
const jwk = await jwkP;
|
|
209
|
+
const ath = b64u(await sha256(accessToken));
|
|
210
|
+
const headers = { ...(init.headers || {}), authorization: `DPoP ${accessToken}`, dpop: await dpopProof(s.pair, jwk, (init.method || 'GET'), url, ath) };
|
|
211
|
+
return fetch(url, { ...init, headers });
|
|
212
|
+
};
|
|
213
|
+
const authFetch = async (url, init = {}) => {
|
|
214
|
+
if (Date.now() > expiresAt - 30_000) await refresh();
|
|
215
|
+
const res = await send(url, init);
|
|
216
|
+
// A token the pod REFUSES is not only ever an expired one: a pod that
|
|
217
|
+
// restarted, a clock that drifted, a token the server stopped recognising
|
|
218
|
+
// all answer 401 while the clock here still says the token is good — so the
|
|
219
|
+
// renewal above never fires and every call fails from then on. Renew once
|
|
220
|
+
// and ask again.
|
|
221
|
+
if (res.status === 401 && refreshToken) {
|
|
222
|
+
try { await refresh(); } catch { return res; }
|
|
223
|
+
return send(url, init);
|
|
224
|
+
}
|
|
225
|
+
return res;
|
|
226
|
+
};
|
|
227
|
+
// `issuer` so a page that cannot renew can send the person back to the right
|
|
228
|
+
// login without asking them for their address again.
|
|
229
|
+
return { webId: s.webId, issuer: s.issuer, fetch: authFetch, refresh, signOut };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { beginLogin, completeLogin, getSession, signOut };
|
|
233
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fediverse-account",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Type a Fediverse handle or a WebID and get back an account you can act with: post, read its timeline, follow, reply, favourite, boost. Works out whether the account lives on a Mastodon-family server or on a Solid pod through FediPod, sends the person to sign in there, brings them back to where they were, and speaks to whichever it is.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Jeff Zucker",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/jeff-zucker/FediPod.git",
|
|
11
|
+
"directory": "lib/session"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/jeff-zucker/FediPod/tree/main/lib/session#readme",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"fediverse",
|
|
16
|
+
"activitypub",
|
|
17
|
+
"mastodon",
|
|
18
|
+
"solid",
|
|
19
|
+
"solid-oidc",
|
|
20
|
+
"webid",
|
|
21
|
+
"login",
|
|
22
|
+
"oauth",
|
|
23
|
+
"fedipod"
|
|
24
|
+
],
|
|
25
|
+
"files": [
|
|
26
|
+
"fedi-account.mjs",
|
|
27
|
+
"fedi-login.mjs",
|
|
28
|
+
"oidc-session.mjs",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"exports": {
|
|
32
|
+
".": "./fedi-account.mjs",
|
|
33
|
+
"./fedi-account.mjs": "./fedi-account.mjs",
|
|
34
|
+
"./fedi-login.mjs": "./fedi-login.mjs",
|
|
35
|
+
"./oidc-session.mjs": "./oidc-session.mjs"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"prepublishOnly": "node ../../scripts/check-pod-calls.mjs"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {}
|
|
41
|
+
}
|
package/package.json
CHANGED
package/run-agent.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import { apRoot, rootOf, writeJsonAtomic } from './lib/device/home.mjs';
|
|
|
33
33
|
import { ensureTrustedTls } from './lib/device/certs.mjs';
|
|
34
34
|
import { storageFor } from './lib/core/storage.mjs';
|
|
35
35
|
import { resolveKeys } from './lib/core/keys.mjs';
|
|
36
|
+
import { completeGatewayMove } from './lib/device/gateway-move.mjs';
|
|
36
37
|
import { RemotePod } from './lib/device/remote.mjs';
|
|
37
38
|
import { Deliverer } from './lib/core/deliver.mjs';
|
|
38
39
|
import { Publisher } from './lib/core/publisher/index.mjs';
|
|
@@ -497,6 +498,18 @@ export class Agent {
|
|
|
497
498
|
return true;
|
|
498
499
|
}
|
|
499
500
|
|
|
501
|
+
// The featured collection was once written only on the first pin, so an
|
|
502
|
+
// account from before then has none, and every server that shows the
|
|
503
|
+
// profile is refused and asks again. One authenticated GET; the empty
|
|
504
|
+
// collection goes up when the pod lacks it. True when it had to.
|
|
505
|
+
async ensureFeaturedPublished() {
|
|
506
|
+
const doc = await this.remote.getJson(this.urls.featured);
|
|
507
|
+
if (doc?.id) return false;
|
|
508
|
+
this.log('featured collection missing from the pod — publishing it');
|
|
509
|
+
await this.publisher.publishFeatured();
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
|
|
500
513
|
// Read-only mode: refresh the state cache periodically, and take over the
|
|
501
514
|
// moment the active agent's lease frees.
|
|
502
515
|
startViewer() {
|
|
@@ -535,6 +548,10 @@ export class Agent {
|
|
|
535
548
|
return; // renewal is already running, above
|
|
536
549
|
}
|
|
537
550
|
await this.intake.start();
|
|
551
|
+
// An address that moved here from another gateway, whose move was left
|
|
552
|
+
// pending (the old gateway could not be told): try again now that this
|
|
553
|
+
// agent acts and the actor is published.
|
|
554
|
+
completeGatewayMove(this).catch(e => this.log(`gateway move: ${e.message}`));
|
|
538
555
|
// Reused, not replaced: demote() stops this instance but does not clear it,
|
|
539
556
|
// so constructing a new one over the top orphaned the old chain beyond the
|
|
540
557
|
// reach of any later stop().
|
|
@@ -588,6 +605,8 @@ export class Agent {
|
|
|
588
605
|
if (repair) {
|
|
589
606
|
this.ensureActorPublished()
|
|
590
607
|
.catch(e => this.log(`actor check failed: ${e.message}`));
|
|
608
|
+
this.ensureFeaturedPublished()
|
|
609
|
+
.catch(e => this.log(`featured check failed: ${e.message}`));
|
|
591
610
|
// The human page, rewritten only when what it shows has changed: one
|
|
592
611
|
// local digest compare, and a write the first time after an upgrade.
|
|
593
612
|
this.publisher.publishProfilePage()
|
|
@@ -730,6 +749,15 @@ export async function startAgent({
|
|
|
730
749
|
handle = null,
|
|
731
750
|
} = {}) {
|
|
732
751
|
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
752
|
+
// A home can hold a FORUM rather than a person or a group: same profiles
|
|
753
|
+
// directory, same unit, same startup — a different thing to run. Its
|
|
754
|
+
// credential says so, and running the agent over it would publish a person
|
|
755
|
+
// where a forum lives.
|
|
756
|
+
const { isForumHome, runForum } = await import('./packages/fedipod-bb/src/run.mjs');
|
|
757
|
+
if (isForumHome(home)) {
|
|
758
|
+
console.log(`[bb:${port}] this home holds a forum — running it`);
|
|
759
|
+
return runForum({ home, port, log: (...a) => console.log(`[bb:${port}]`, ...a) });
|
|
760
|
+
}
|
|
733
761
|
// connect() sets this from pod state a moment later, but the browser may
|
|
734
762
|
// already be opening — seed it from what setup recorded so the named origin
|
|
735
763
|
// works from the first request.
|
package/vendor/gate.cjs
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
// authorized when it carries the secret:
|
|
8
8
|
// 1. header `x-dk-token` — the Electron shell injects this on app traffic, or
|
|
9
9
|
// 2. cookie `dk-token` — a browser "blessed" via the token URL, or
|
|
10
|
-
// 3. query `?dk-token=<secret>` — answered with a SameSite=
|
|
11
|
-
// a redirect to the same URL minus the param (the blessing flow),
|
|
10
|
+
// 3. query `?dk-token=<secret>` — answered with a SameSite=Lax cookie and
|
|
11
|
+
// a redirect to the same URL minus the param (the blessing flow). Lax,
|
|
12
|
+
// not Strict: a link from another site — the pod server's own opt-in
|
|
13
|
+
// page among them — must arrive with the cookie it was just handed, and
|
|
14
|
+
// Lax still withholds it from cross-site POSTs and subresources; or
|
|
12
15
|
// 4. (opt-in, proxy only) an Origin/Referer on the allowOrigins list — app
|
|
13
16
|
// pages in a blessed browser call the proxy with plain fetch(), which
|
|
14
17
|
// doesn't attach cookies cross-port; a hostile page can't forge Origin.
|
|
@@ -105,7 +108,7 @@ function makeGate(token, { allowOrigins = [], publicEndpoints = false, secureCoo
|
|
|
105
108
|
// Path scopes the cookie to this identity's own door: on a shared
|
|
106
109
|
// origin (suffix pods) two co-tenants must not clobber each other's,
|
|
107
110
|
// and a whole-origin cookie would. Defaults to '/'.
|
|
108
|
-
'set-cookie': `${COOKIE}=${t}; Path=${cookiePath}; HttpOnly; SameSite=
|
|
111
|
+
'set-cookie': `${COOKIE}=${t}; Path=${cookiePath}; HttpOnly; SameSite=Lax; Max-Age=31536000`
|
|
109
112
|
+ (secureCookie ? '; Secure' : ''),
|
|
110
113
|
'location': url.pathname + url.search,
|
|
111
114
|
});
|
package/web/admin/bar.css
CHANGED
|
@@ -58,3 +58,69 @@ body { background: var(--surface); }
|
|
|
58
58
|
underlined rather than removed. */
|
|
59
59
|
#bar button[aria-current="page"], #bar a[aria-current="page"] {
|
|
60
60
|
text-decoration: underline; text-underline-offset: .3rem; }
|
|
61
|
+
/* The client control: which fediverse client this browser opens. The label is
|
|
62
|
+
quieter than the links; the one in use is marked `aria-current="true"` — the
|
|
63
|
+
current item of a set, which on the record page is not the current PAGE — so
|
|
64
|
+
it needs its own mark rather than borrowing the rule above. */
|
|
65
|
+
#bar #client-pick, #bar #account-pick { display: inline-flex; gap: .6rem; align-items: baseline;
|
|
66
|
+
white-space: nowrap; margin-left: 1rem; }
|
|
67
|
+
#bar #client-now, #bar #account-now { opacity: .75; }
|
|
68
|
+
/* The notices bell: the one filled control in the bar, because it is the one
|
|
69
|
+
thing there that is not a place to go but something to be told. The count is
|
|
70
|
+
how many notices this browser has not opened yet; with none it is quiet. */
|
|
71
|
+
#bar #bar-notices { display: inline-flex; align-items: center; gap: .35rem; margin-left: 1rem;
|
|
72
|
+
padding: .3rem .7rem; border-radius: 999px; font-size: 1.05rem; line-height: 1;
|
|
73
|
+
background: var(--btn); color: var(--btn-text); border: 1px solid var(--btn); }
|
|
74
|
+
/* `display` on an id rule outranks the browser's own [hidden] rule, and the
|
|
75
|
+
shell has no !important of its own, so the bell hid on the record page and
|
|
76
|
+
showed on the client with nothing to show. Said here, once, for every page. */
|
|
77
|
+
#bar #bar-notices[hidden] { display: none; }
|
|
78
|
+
#bar #bar-notices:hover { background: var(--btn-hover); border-color: var(--btn-hover); }
|
|
79
|
+
#bar #bar-notices:focus-visible { outline: 3px solid var(--ring); outline-offset: 2px; }
|
|
80
|
+
#bar #bar-notices-count { font-size: .9rem; font-weight: 700; min-width: 1.3em; text-align: center;
|
|
81
|
+
padding: .05rem .4rem; border-radius: 999px; background: var(--btn-text); color: var(--btn); }
|
|
82
|
+
#bar #bar-notices-count:empty { display: none; }
|
|
83
|
+
#bar #bar-notices[data-new="0"] { background: var(--field-bg); color: var(--ink); border-color: var(--field-edge); }
|
|
84
|
+
#bar a[aria-current="true"] {
|
|
85
|
+
font-weight: 700; text-decoration: underline; text-underline-offset: .3rem; }
|
|
86
|
+
|
|
87
|
+
/* The two-clients notice: said once, to somebody who knew this page when it
|
|
88
|
+
had one client. Sized and placed rather than left to the browser's default,
|
|
89
|
+
which stretched it edge to edge and put it over the very links it names. */
|
|
90
|
+
#client-news, #notices-list, #notice-view {
|
|
91
|
+
/* Low rather than centred: it points at the links in the bar, so the bar has
|
|
92
|
+
to stay in view while it is read. */
|
|
93
|
+
max-width: 34rem; margin: auto auto 2.5rem; padding: 1.5rem 1.7rem;
|
|
94
|
+
border: 1px solid var(--dialog-edge); border-radius: .6rem;
|
|
95
|
+
background: var(--raised); color: var(--fg);
|
|
96
|
+
font: 1rem/1.55 var(--font);
|
|
97
|
+
box-shadow: 0 .6rem 2rem rgba(0, 0, 0, .25);
|
|
98
|
+
}
|
|
99
|
+
#client-news::backdrop, #notices-list::backdrop, #notice-view::backdrop { background: rgba(0, 0, 0, .45); }
|
|
100
|
+
#client-news h2, #notices-list h2, #notice-view h2 { font-size: 1.25rem; margin: 0 0 .9rem; color: var(--heading); }
|
|
101
|
+
#client-news p, #notices-list p, #notice-view p { margin: 0 0 1.2rem; }
|
|
102
|
+
#client-news p:last-child, #notices-list p:last-child, #notice-view p:last-child { margin: 0; }
|
|
103
|
+
#client-news button, #notices-list button, #notice-view button {
|
|
104
|
+
font: inherit; padding: .55rem 1.3rem; border-radius: .35rem; cursor: pointer;
|
|
105
|
+
background: var(--btn); color: var(--btn-text); border: 1px solid var(--btn);
|
|
106
|
+
}
|
|
107
|
+
#client-news button:hover, #notices-list button:hover, #notice-view button:hover { background: var(--btn-hover); border-color: var(--btn-hover); }
|
|
108
|
+
#client-news button:focus-visible, #notices-list button:focus-visible, #notice-view button:focus-visible { outline: 3px solid var(--ring); outline-offset: 2px; }
|
|
109
|
+
|
|
110
|
+
/* The notices dialogs are centred and never taller than the window: the
|
|
111
|
+
client-news rule above sits low, and a short window pushed a list of
|
|
112
|
+
notices up past the top edge where nothing could reach it. */
|
|
113
|
+
#notices-list, #notice-view { margin: auto; max-height: calc(100dvh - 3rem); overflow: auto; box-sizing: border-box; }
|
|
114
|
+
/* The list of notices: each title a button that opens that notice, the date
|
|
115
|
+
beside it, the ones not yet opened here in bold. */
|
|
116
|
+
#notices-items { list-style: none; margin: 0 0 1.2rem; padding: 0; }
|
|
117
|
+
#notices-items li { display: flex; gap: .8rem; align-items: baseline; padding: .45rem 0;
|
|
118
|
+
border-bottom: 1px solid var(--line); }
|
|
119
|
+
#notices-items li:last-child { border-bottom: 0; }
|
|
120
|
+
#notices-items button.notice-open { flex: 1 1 auto; text-align: left; padding: .1rem 0;
|
|
121
|
+
background: none; border: 0; color: var(--link); text-decoration: underline; text-underline-offset: .2rem; }
|
|
122
|
+
#notices-items li.new button.notice-open { font-weight: 700; }
|
|
123
|
+
#notices-items .when, #notice-view .when { color: var(--sub); font-size: .9rem; white-space: nowrap; }
|
|
124
|
+
#notices-items p.none { color: var(--sub); }
|
|
125
|
+
#notice-view-body p { margin: 0 0 .9rem; }
|
|
126
|
+
#notice-view-body a { color: var(--link); word-break: break-all; }
|
package/web/admin/bar.js
CHANGED
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
if (el && s?.handle && s?.actor) {
|
|
25
25
|
try { el.textContent = `@${s.handle}@${new URL(s.actor).host}`; } catch { /* odd actor url */ }
|
|
26
26
|
}
|
|
27
|
+
// A viewer does nothing: it publishes no profile, carries nothing and
|
|
28
|
+
// leaves the account looking broken from outside. Say so where the
|
|
29
|
+
// account is, and let the owner take it back from here.
|
|
30
|
+
if (s?.mode === 'viewer') viewerBanner();
|
|
27
31
|
}).catch(() => { /* not up yet; the bar still works */ });
|
|
28
32
|
|
|
29
33
|
// The actors dropdown, on a page whose bar carries one. The record page
|
|
@@ -59,3 +63,37 @@
|
|
|
59
63
|
}).catch(() => { pick.hidden = true; });
|
|
60
64
|
}
|
|
61
65
|
})();
|
|
66
|
+
|
|
67
|
+
// "Another device is active": what it means for this account, and the way out.
|
|
68
|
+
// The lease is a document on the pod with a five-minute life; a browser that
|
|
69
|
+
// was closed leaves one behind that nothing renews, and every reload asks
|
|
70
|
+
// under a new name, so waiting can look like waiting forever.
|
|
71
|
+
function viewerBanner() {
|
|
72
|
+
if (document.getElementById('viewer-banner')) return;
|
|
73
|
+
const base = location.pathname.replace(/\/admin\/.*$/u, '');
|
|
74
|
+
const bar = document.createElement('div');
|
|
75
|
+
bar.id = 'viewer-banner';
|
|
76
|
+
bar.setAttribute('role', 'status');
|
|
77
|
+
bar.style.cssText = 'padding:.7rem 1rem;background:var(--field-bg,#ececec);color:var(--fg,#1a1a1a);'
|
|
78
|
+
+ 'border-bottom:1px solid var(--line,#b0b0b0);display:flex;gap:1rem;align-items:center;flex-wrap:wrap;font:inherit';
|
|
79
|
+
const said = document.createElement('span');
|
|
80
|
+
said.textContent = 'Another device holds this account, so nothing is being published or delivered from here.';
|
|
81
|
+
const go = document.createElement('button');
|
|
82
|
+
go.type = 'button';
|
|
83
|
+
go.textContent = 'Take it over';
|
|
84
|
+
go.style.cssText = 'font:inherit;font-weight:600;padding:.4rem 1rem;border-radius:.4rem;border:1px solid var(--btn,#3a5f43);'
|
|
85
|
+
+ 'background:var(--btn,#3a5f43);color:var(--btn-text,#fff);cursor:pointer';
|
|
86
|
+
go.addEventListener('click', async () => {
|
|
87
|
+
go.disabled = true;
|
|
88
|
+
said.textContent = 'Taking it over…';
|
|
89
|
+
try {
|
|
90
|
+
const r = await fetch(base + '/takeover', { method: 'POST', headers: { 'x-fedipod-page': '1' } });
|
|
91
|
+
const j = await r.json().catch(() => null);
|
|
92
|
+
if (r.ok && j?.mode === 'active') { location.reload(); return; }
|
|
93
|
+
said.textContent = j?.error || 'The lease could not be taken.';
|
|
94
|
+
} catch (e) { said.textContent = e.message; }
|
|
95
|
+
go.disabled = false;
|
|
96
|
+
});
|
|
97
|
+
bar.append(said, go);
|
|
98
|
+
document.body.prepend(bar);
|
|
99
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>FediPod — client</title>
|
|
7
|
+
<link rel="icon" href="/icons/local.svg" type="image/svg+xml">
|
|
7
8
|
<link rel="stylesheet" href="../tokens.css">
|
|
8
9
|
<style>
|
|
9
10
|
/* Colours are the shared tokens (tokens.css); only layout is here. The bar is
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
does. */
|
|
12
13
|
:root { color-scheme: light dark; font-size: 125%; }
|
|
13
14
|
html { height: 100%; }
|
|
14
|
-
body { font: 1rem/1.5
|
|
15
|
+
body { font: 1rem/1.5 Arial, Helvetica, sans-serif; margin: 0; height: 100dvh;
|
|
15
16
|
display: flex; flex-direction: column; overflow: hidden; }
|
|
16
17
|
main { flex: 1 1 auto; min-height: 0; display: flex; }
|
|
17
18
|
#client { flex: 1 1 auto; min-height: 0; width: 100%; border: 0; }
|
|
@@ -27,8 +28,16 @@ main { flex: 1 1 auto; min-height: 0; display: flex; }
|
|
|
27
28
|
<h1 class="name">FediPod</h1>
|
|
28
29
|
<select id="actor-pick" aria-label="Local actors"
|
|
29
30
|
title=" Every actor on this machine; choosing one opens its client"></select>
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
<!-- The account group, styled like the client group beside it: one label,
|
|
32
|
+
then the places to go. The browser build adds "sign out" to it. -->
|
|
33
|
+
<span id="account-pick"><span id="account-now">account:</span>
|
|
34
|
+
<a id="bar-fediverse" href="./" title=" Open the client and read your timeline" aria-current="page">visit</a>
|
|
35
|
+
<a id="bar-manage" href="../" title=" The record: what this actor is, and what you can do to it">manage</a>
|
|
36
|
+
</span>
|
|
37
|
+
<!-- Notices from whoever runs this site. Shown only when the site has any
|
|
38
|
+
(notices-bar.js asks /api/notices); the count is how many are new here. -->
|
|
39
|
+
<button type="button" id="bar-notices" hidden aria-haspopup="dialog" aria-controls="notices-list"
|
|
40
|
+
title=" Notices from the operator of this site">🔔<span id="bar-notices-count" aria-live="polite"></span></button>
|
|
32
41
|
</nav>
|
|
33
42
|
|
|
34
43
|
<main>
|
|
@@ -37,5 +46,24 @@ main { flex: 1 1 auto; min-height: 0; display: flex; }
|
|
|
37
46
|
|
|
38
47
|
<script src="../bar.js"></script>
|
|
39
48
|
<script src="client.js"></script>
|
|
49
|
+
|
|
50
|
+
<!-- The notices: the list, and one notice open. Declared here and filled by
|
|
51
|
+
notices-bar.js from /api/notices; nothing in them is markup from a notice. -->
|
|
52
|
+
<dialog id="notices-list" aria-labelledby="notices-title">
|
|
53
|
+
<section>
|
|
54
|
+
<h2 id="notices-title">Notices</h2>
|
|
55
|
+
<ul id="notices-items"></ul>
|
|
56
|
+
<p class="row"><button type="button" id="notices-close" class="primary" title=" Close the list of notices">Close</button></p>
|
|
57
|
+
</section>
|
|
58
|
+
</dialog>
|
|
59
|
+
<dialog id="notice-view" aria-labelledby="notice-view-title">
|
|
60
|
+
<section>
|
|
61
|
+
<h2 id="notice-view-title"></h2>
|
|
62
|
+
<p class="when" id="notice-view-when"></p>
|
|
63
|
+
<div id="notice-view-body"></div>
|
|
64
|
+
<p class="row"><button type="button" id="notice-view-close" class="primary" title=" Back to the list of notices">Close</button></p>
|
|
65
|
+
</section>
|
|
66
|
+
</dialog>
|
|
67
|
+
<script src="../notices-bar.js"></script>
|
|
40
68
|
</body>
|
|
41
69
|
</html>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// client-pick.js — which fediverse client this browser opens.
|
|
2
|
+
//
|
|
3
|
+
// The site carries more than one client. Each one has its own shell page whose
|
|
4
|
+
// markup declares the app it frames, and every switch is an ordinary link from
|
|
5
|
+
// one shell to another. This file adds one thing only: it REMEMBERS which shell
|
|
6
|
+
// you last chose, so /admin/client/ — where signing in lands, and where the
|
|
7
|
+
// record's "visit account" goes — hands you back the same client next time.
|
|
8
|
+
//
|
|
9
|
+
// The choice is this browser's, not the account's. It decides which page you
|
|
10
|
+
// are given and nothing else: both clients talk to the same agent, with the
|
|
11
|
+
// same bearer, on this same origin. Nothing about the identity moves with it,
|
|
12
|
+
// which is why it lives in localStorage and is never written to the pod.
|
|
13
|
+
//
|
|
14
|
+
// Staged into the site by scripts/stage-site.mjs, which also generates the
|
|
15
|
+
// shells and the links in the bar. The device agent carries one client and
|
|
16
|
+
// never loads this.
|
|
17
|
+
|
|
18
|
+
const KEY = 'fedipod-client';
|
|
19
|
+
// The default client's shell. It is also the page that redirects, so a browser
|
|
20
|
+
// that has chosen something else does not have to pass through the default.
|
|
21
|
+
const DEFAULT_SHELL = '/admin/client/';
|
|
22
|
+
// A stored value is a path this site generated, and nothing else is followed.
|
|
23
|
+
// Without this, anything that can write localStorage — including the framed
|
|
24
|
+
// client itself — could send the next visit off this origin.
|
|
25
|
+
const SHELL = /^\/admin\/client(-[a-z0-9-]+)?\/$/;
|
|
26
|
+
|
|
27
|
+
const read = () => {
|
|
28
|
+
// A private window, or a browser told to keep no site data, throws on the
|
|
29
|
+
// read rather than answering empty. There is no choice to honour then, which
|
|
30
|
+
// is the same as not having made one.
|
|
31
|
+
try { return localStorage.getItem(KEY) || ''; } catch { return ''; }
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Before anything renders: if this browser last chose another client, that is
|
|
35
|
+
// the page it asked for. `replace` and not `href`, so Back does not bounce
|
|
36
|
+
// between the two shells.
|
|
37
|
+
if (location.pathname === DEFAULT_SHELL) {
|
|
38
|
+
const chosen = read();
|
|
39
|
+
if (chosen && chosen !== DEFAULT_SHELL && SHELL.test(chosen)) location.replace(chosen);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Somebody who knew this page when it had one client should be told, once,
|
|
43
|
+
// that it now has two and where to change it. Marked as said as soon as it is
|
|
44
|
+
// shown rather than when it is dismissed: a reader who closes the tab instead
|
|
45
|
+
// of pressing the button has still read it, and being told twice is worse than
|
|
46
|
+
// not being told a second time.
|
|
47
|
+
const SAID = 'fedipod-client-news';
|
|
48
|
+
function sayOnce() {
|
|
49
|
+
const dlg = document.getElementById('client-news');
|
|
50
|
+
if (!dlg) return;
|
|
51
|
+
let told = true;
|
|
52
|
+
try { told = !!localStorage.getItem(SAID); } catch { /* keeps no site data */ }
|
|
53
|
+
if (told) return;
|
|
54
|
+
try { localStorage.setItem(SAID, '1'); } catch { /* keeps no site data */ }
|
|
55
|
+
dlg.showModal();
|
|
56
|
+
document.getElementById('client-news-ok')?.addEventListener('click', () => dlg.close());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
60
|
+
sayOnce();
|
|
61
|
+
// Which one is current. On a shell it is the page you are on — you may have
|
|
62
|
+
// typed its address, and what is in front of you is the true answer whatever
|
|
63
|
+
// storage says. Anywhere else (the record page) it is the remembered choice.
|
|
64
|
+
const here = SHELL.test(location.pathname) ? location.pathname : (read() || DEFAULT_SHELL);
|
|
65
|
+
|
|
66
|
+
for (const a of document.querySelectorAll('a.client-switch')) {
|
|
67
|
+
// `aria-current` and not a class alone: this is one of a set of links, one
|
|
68
|
+
// of which is the one in use — which is the state the attribute exists to
|
|
69
|
+
// carry, and the only way a screen reader is told about it at all.
|
|
70
|
+
if (a.getAttribute('href') === here) a.setAttribute('aria-current', 'true');
|
|
71
|
+
// Each link records the choice it stands for and then follows its own href,
|
|
72
|
+
// the destination written in the markup. Recorded on the way OUT rather
|
|
73
|
+
// than on arrival, so a shell opened deliberately by address — or the one
|
|
74
|
+
// the redirect above lands on — does not silently become the default.
|
|
75
|
+
a.addEventListener('click', () => {
|
|
76
|
+
try { localStorage.setItem(KEY, a.getAttribute('href')); } catch { /* keeps no site data */ }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|