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,84 @@
|
|
|
1
|
+
// notices.mjs — notices from whoever runs the front, to every account here.
|
|
2
|
+
//
|
|
3
|
+
// The operator writes them on the /notices page, proved as the admin the way
|
|
4
|
+
// the roster is; every page of ours asks GET /api/notices once per load and
|
|
5
|
+
// shows a bell when there are any. A notice is a title, a plain-text body and
|
|
6
|
+
// a time. The body is never markup: the pages render it as text.
|
|
7
|
+
//
|
|
8
|
+
// Kept beside the directory, in a store the adapter supplies:
|
|
9
|
+
// listNotices() -> { id: notice } putNotice(id, notice) deleteNotice(id)
|
|
10
|
+
// A front that supplies none has no notices to offer, and says so.
|
|
11
|
+
|
|
12
|
+
import crypto from 'node:crypto';
|
|
13
|
+
|
|
14
|
+
const TITLE_MAX = 120;
|
|
15
|
+
const BODY_MAX = 4000;
|
|
16
|
+
const NOTICES_EDGE_SECONDS = 60;
|
|
17
|
+
|
|
18
|
+
const clean = (v, max) => String(v ?? '').replace(/\r\n?/gu, '\n').trim().slice(0, max);
|
|
19
|
+
const newest = (a, b) => String(b.at || '').localeCompare(String(a.at || ''));
|
|
20
|
+
|
|
21
|
+
export async function listNotices(ctx) {
|
|
22
|
+
const all = ctx.listNotices ? await ctx.listNotices() : {};
|
|
23
|
+
return Object.values(all || {}).filter(Boolean).sort(newest);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// GET is public: what every account is shown. POST is the admin's alone. The
|
|
27
|
+
// /notices page is where the admin writes them; it signs in like the roster.
|
|
28
|
+
// Returns a response, or null when the path is neither.
|
|
29
|
+
export async function routeNoticesApi(request, pathname, ctx, { j, verifyPodToken, publicFor, notFound }) {
|
|
30
|
+
// The two admin pages: the roster (who has accounts here) and the notices.
|
|
31
|
+
// Each signs in and calls its API; a deploy with no admin supplies no page.
|
|
32
|
+
const page = pathname === '/notices' ? ctx.noticesPage : pathname === '/roster' ? ctx.adminPage : undefined;
|
|
33
|
+
if (page !== undefined) {
|
|
34
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
|
|
35
|
+
if (!page) return notFound();
|
|
36
|
+
return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: page };
|
|
37
|
+
}
|
|
38
|
+
if (pathname !== '/api/notices') return null;
|
|
39
|
+
const open = { 'access-control-allow-origin': '*' };
|
|
40
|
+
if (request.method === 'OPTIONS') {
|
|
41
|
+
return { status: 204, headers: { ...open, 'access-control-allow-methods': 'GET, OPTIONS',
|
|
42
|
+
'access-control-allow-headers': 'Accept', allow: 'GET, POST, OPTIONS' }, body: null };
|
|
43
|
+
}
|
|
44
|
+
if (request.method === 'GET') {
|
|
45
|
+
if (!ctx.listNotices) return j(501, { error: 'this front keeps no notices' });
|
|
46
|
+
const out = j(200, { notices: await listNotices(ctx) });
|
|
47
|
+
Object.assign(out.headers, open, publicFor(NOTICES_EDGE_SECONDS));
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
if (request.method !== 'POST') return { status: 405, headers: {}, body: '' };
|
|
51
|
+
if (!ctx.putNotice || !ctx.deleteNotice || !ctx.adminWebId) return j(501, { error: 'this front keeps no notices' });
|
|
52
|
+
const webid = await verifyPodToken(request, pathname, ctx.verifier);
|
|
53
|
+
if (!webid) return j(401, { error: 'a Solid-OIDC token is required' });
|
|
54
|
+
if (webid !== ctx.adminWebId) return j(403, { error: 'that WebID is not the admin of this front' });
|
|
55
|
+
let body;
|
|
56
|
+
try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
|
|
57
|
+
const action = String(body.action || '');
|
|
58
|
+
const all = (ctx.listNotices ? await ctx.listNotices() : {}) || {};
|
|
59
|
+
if (action === 'delete') {
|
|
60
|
+
const id = String(body.id || '');
|
|
61
|
+
if (!all[id]) return j(404, { error: 'no such notice' });
|
|
62
|
+
await ctx.deleteNotice(id);
|
|
63
|
+
console.log(`front: notice ${id} deleted by the admin`);
|
|
64
|
+
return j(200, { ok: true, id, deleted: true });
|
|
65
|
+
}
|
|
66
|
+
if (action !== 'create' && action !== 'update') return j(400, { error: 'action must be create, update or delete' });
|
|
67
|
+
const title = clean(body.title, TITLE_MAX);
|
|
68
|
+
const text = clean(body.body, BODY_MAX);
|
|
69
|
+
if (!title) return j(400, { error: 'a title is required' });
|
|
70
|
+
if (!text) return j(400, { error: 'a body is required' });
|
|
71
|
+
const now = new Date().toISOString();
|
|
72
|
+
let notice;
|
|
73
|
+
if (action === 'create') {
|
|
74
|
+
const id = `${Date.now().toString(36)}-${crypto.randomBytes(4).toString('hex')}`;
|
|
75
|
+
notice = { id, title, body: text, at: now, updatedAt: now };
|
|
76
|
+
} else {
|
|
77
|
+
const prior = all[String(body.id || '')];
|
|
78
|
+
if (!prior) return j(404, { error: 'no such notice' });
|
|
79
|
+
notice = { ...prior, title, body: text, updatedAt: now };
|
|
80
|
+
}
|
|
81
|
+
await ctx.putNotice(notice.id, notice);
|
|
82
|
+
console.log(`front: notice ${notice.id} ${action}d by the admin`);
|
|
83
|
+
return j(action === 'create' ? 201 : 200, { ok: true, notice });
|
|
84
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// quiet.mjs — accounts that go quiet: what the front knows about an account
|
|
2
|
+
// nobody opens, and what its owner may say about it. The router in
|
|
3
|
+
// front-core.mjs asks closedState/accountState at the door and on every
|
|
4
|
+
// public read, stamps noteOpened from the relay, counts noteReceived after
|
|
5
|
+
// a forwarded delivery, and hands /api/open, /api/pause and /api/close here.
|
|
6
|
+
|
|
7
|
+
// ---- accounts that go quiet -------------------------------------------------
|
|
8
|
+
//
|
|
9
|
+
// The gateway holds no mail: every delivery it takes is two writes into the
|
|
10
|
+
// owner's pod inbox, read only when their browser is open. An account nobody
|
|
11
|
+
// opens grows on its pod without limit, and its owner comes back to a drain
|
|
12
|
+
// of everything at once. So the front keeps two facts about each browser
|
|
13
|
+
// account — when its owner was last here, and how much content has arrived
|
|
14
|
+
// since — and acts on them.
|
|
15
|
+
//
|
|
16
|
+
// `openedAt` is written when the owner signs in (POST /api/open, always) and
|
|
17
|
+
// while they act through the relay (at most hourly). The count of content
|
|
18
|
+
// deliveries since then lives in its own record, keyed by handle AND
|
|
19
|
+
// openedAt: a fresh stamp starts a fresh count with nothing to reset, and a
|
|
20
|
+
// delivery's write can never overwrite a sign-in's. Two deliveries landing
|
|
21
|
+
// together may each read the same total and one increment is lost — the cap
|
|
22
|
+
// comes out slightly soft, never too strict.
|
|
23
|
+
//
|
|
24
|
+
// PAUSED: the count has reached the cap, or the owner said so. Content is
|
|
25
|
+
// accepted and discarded, control (follows, unfollows, moves, deletions,
|
|
26
|
+
// blocks) still lands. The owner's next sign-in ends a cap pause by itself; a
|
|
27
|
+
// pause they set lasts until they lift it.
|
|
28
|
+
//
|
|
29
|
+
// CLOSED: the owner said so, or nothing has opened the account for the close
|
|
30
|
+
// window. Found closed by time, it is written down so it stays closed. From
|
|
31
|
+
// then on the door, the actor and the handle answer 410 — never a failure
|
|
32
|
+
// code, which senders retry and count against this whole host, and never a
|
|
33
|
+
// success, which would keep them sending. Nothing on the pod is touched. An
|
|
34
|
+
// address that moved (see /api/move) keeps answering as moved; closing does
|
|
35
|
+
// not take that away.
|
|
36
|
+
//
|
|
37
|
+
// Only an account whose owner has signed in from a browser carries
|
|
38
|
+
// `openedAt`. A DeviceAgent behind this door never does: it drains its own
|
|
39
|
+
// inbox as it runs, so it is never counted, never paused and never closed by
|
|
40
|
+
// time. Accounts from before this was built are counted from their next
|
|
41
|
+
// sign-in.
|
|
42
|
+
export const DEFAULT_PAUSE_ITEMS = 5000;
|
|
43
|
+
export const DEFAULT_CLOSE_DAYS = 183; // six months
|
|
44
|
+
const OPEN_STAMP_EVERY_MS = 60 * 60_000; // the relay's stamp, at most hourly
|
|
45
|
+
const pauseItemsOf = (ctx) => (Number(ctx.pauseItems) > 0 ? Number(ctx.pauseItems) : DEFAULT_PAUSE_ITEMS);
|
|
46
|
+
const closeDaysOf = (ctx) => (Number(ctx.closeDays) > 0 ? Number(ctx.closeDays) : DEFAULT_CLOSE_DAYS);
|
|
47
|
+
const receivedKey = (key, rec) => `${key}/${rec.openedAt}`;
|
|
48
|
+
|
|
49
|
+
export async function receivedSince(ctx, key, rec) {
|
|
50
|
+
if (!ctx.readReceived || !rec.openedAt) return { items: 0, bytes: 0 };
|
|
51
|
+
const got = await ctx.readReceived(receivedKey(key, rec)).catch(() => null);
|
|
52
|
+
return { items: Number(got?.items) || 0, bytes: Number(got?.bytes) || 0 };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// One more content delivery reached the pod. Nothing to count against until
|
|
56
|
+
// the owner has been here once.
|
|
57
|
+
export async function noteReceived(ctx, key, rec, bytes) {
|
|
58
|
+
if (!ctx.readReceived || !ctx.writeReceived || !rec.openedAt) return;
|
|
59
|
+
const so = await receivedSince(ctx, key, rec);
|
|
60
|
+
await ctx.writeReceived(receivedKey(key, rec), { items: so.items + 1, bytes: so.bytes + (Number(bytes) || 0) })
|
|
61
|
+
.catch((e) => console.log(`front @${key}: count not written: ${e?.message || e}`));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The owner is here. `always` is a sign-in; the relay stamps at most hourly,
|
|
65
|
+
// so a busy hour of posting is one write. The count that went with the old
|
|
66
|
+
// stamp is dropped: it is over, and a store should not keep one per hour.
|
|
67
|
+
export async function noteOpened(ctx, key, rec, { always = false } = {}) {
|
|
68
|
+
if (!ctx.putDirectory) return rec;
|
|
69
|
+
if (!always && rec.openedAt && Date.now() - Date.parse(rec.openedAt) < OPEN_STAMP_EVERY_MS) return rec;
|
|
70
|
+
const next = { ...rec, openedAt: new Date().toISOString() };
|
|
71
|
+
await ctx.putDirectory(key, next);
|
|
72
|
+
if (rec.openedAt && ctx.dropReceived) await ctx.dropReceived(receivedKey(key, rec)).catch(() => {});
|
|
73
|
+
return next;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Closed, and why. Reads no count: this is asked on every public read.
|
|
77
|
+
export async function closedState(ctx, key, rec) {
|
|
78
|
+
if (rec.movedTo) return { closed: false };
|
|
79
|
+
if (rec.closedAt) return { closed: true, closedAt: rec.closedAt, closedBy: rec.closedBy || 'owner' };
|
|
80
|
+
if (rec.openedAt && Date.now() - Date.parse(rec.openedAt) > closeDaysOf(ctx) * 86400_000) {
|
|
81
|
+
const closedAt = new Date().toISOString();
|
|
82
|
+
if (ctx.putDirectory) await ctx.putDirectory(key, { ...rec, closedAt, closedBy: 'quiet' });
|
|
83
|
+
console.log(`front @${key}: closed — nothing opened it since ${rec.openedAt}`);
|
|
84
|
+
return { closed: true, closedAt, closedBy: 'quiet' };
|
|
85
|
+
}
|
|
86
|
+
return { closed: false };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The whole standing of an account, for the door and for its owner's page.
|
|
90
|
+
export async function accountState(ctx, key, rec) {
|
|
91
|
+
const c = await closedState(ctx, key, rec);
|
|
92
|
+
const received = c.closed ? { items: 0, bytes: 0 } : await receivedSince(ctx, key, rec);
|
|
93
|
+
const cap = pauseItemsOf(ctx);
|
|
94
|
+
const pausedBy = c.closed ? null : rec.pausedAt ? 'owner' : received.items >= cap ? 'quiet' : null;
|
|
95
|
+
return {
|
|
96
|
+
closed: c.closed, closedAt: c.closedAt || null, closedBy: c.closedBy || null,
|
|
97
|
+
paused: !!pausedBy, pausedBy, pausedAt: rec.pausedAt || null,
|
|
98
|
+
openedAt: rec.openedAt || null, received, pauseItems: cap, closeDays: closeDaysOf(ctx),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// A 410 with a reason, the shape every closed or moved id answers with.
|
|
103
|
+
export const closedAnswer = (headers = {}, why = 'this address is closed') => ({
|
|
104
|
+
status: 410, headers: { ...headers, 'content-type': 'application/json', 'cache-control': 'no-store' },
|
|
105
|
+
body: JSON.stringify({ error: why }) });
|
|
106
|
+
|
|
107
|
+
// The owner of a row, proved the way the relay proves them: a pod token whose
|
|
108
|
+
// WebID is the row's, or lives on the row's pod for a row from before WebIDs
|
|
109
|
+
// were recorded.
|
|
110
|
+
async function provedOwner(request, pathname, ctx, rec, { j, verifyPodToken, webidUnderPod }) {
|
|
111
|
+
const webid = await verifyPodToken(request, pathname, ctx.verifier);
|
|
112
|
+
if (!webid) return { error: j(401, { error: 'a Solid-OIDC token proving the pod is required' }) };
|
|
113
|
+
const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
|
|
114
|
+
if (!owner) return { error: j(403, { error: "the token proves a different pod than this account's" }) };
|
|
115
|
+
return { webid };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The owner's three routes. `deps` are front-core's own JSON reply and
|
|
119
|
+
// token check, handed in so this module stays free of the router's privates.
|
|
120
|
+
// Returns a response, or null when the path is not one of these.
|
|
121
|
+
export async function routeQuietApi(request, pathname, ctx, deps) {
|
|
122
|
+
const { j } = deps;
|
|
123
|
+
if (request.method !== 'POST' || !['/api/open', '/api/pause', '/api/close'].includes(pathname)) return null;
|
|
124
|
+
// The owner is here: their browser says so as it opens the account. The
|
|
125
|
+
// answer is the account's standing at this gateway, which the manage page
|
|
126
|
+
// shows. A closed address is told so and nothing is written.
|
|
127
|
+
if (pathname === '/api/open' && request.method === 'POST') {
|
|
128
|
+
if (!ctx.putDirectory) return j(501, { error: 'this front keeps no directory' });
|
|
129
|
+
let body;
|
|
130
|
+
try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
|
|
131
|
+
const handle = String(body.handle || '').toLowerCase();
|
|
132
|
+
let rec = await ctx.lookup(handle);
|
|
133
|
+
if (!rec) return j(404, { error: 'no such account' });
|
|
134
|
+
const who = await provedOwner(request, pathname, ctx, rec, deps);
|
|
135
|
+
if (who.error) return who.error;
|
|
136
|
+
const before = await closedState(ctx, handle, rec);
|
|
137
|
+
if (before.closed) return j(410, { error: 'this address is closed', closedAt: before.closedAt, closedBy: before.closedBy });
|
|
138
|
+
rec = await noteOpened(ctx, handle, rec, { always: true });
|
|
139
|
+
return j(200, { ok: true, handle, ...await accountState(ctx, handle, rec) });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The owner pausing their own account, or lifting the pause they set.
|
|
143
|
+
if (pathname === '/api/pause' && request.method === 'POST') {
|
|
144
|
+
if (!ctx.putDirectory) return j(501, { error: 'this front keeps no directory' });
|
|
145
|
+
let body;
|
|
146
|
+
try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
|
|
147
|
+
const handle = String(body.handle || '').toLowerCase();
|
|
148
|
+
let rec = await ctx.lookup(handle);
|
|
149
|
+
if (!rec) return j(404, { error: 'no such account' });
|
|
150
|
+
const who = await provedOwner(request, pathname, ctx, rec, deps);
|
|
151
|
+
if (who.error) return who.error;
|
|
152
|
+
const before = await closedState(ctx, handle, rec);
|
|
153
|
+
if (before.closed) return j(410, { error: 'this address is closed', closedAt: before.closedAt });
|
|
154
|
+
if (typeof body.paused !== 'boolean') return j(400, { error: 'paused must be true or false' });
|
|
155
|
+
if (body.paused && !rec.pausedAt) rec = { ...rec, pausedAt: new Date().toISOString() };
|
|
156
|
+
if (!body.paused && rec.pausedAt) { rec = { ...rec }; delete rec.pausedAt; }
|
|
157
|
+
// Lifting a pause is a sign-in's worth of "I am here": the count starts over.
|
|
158
|
+
if (!body.paused) rec = { ...rec, openedAt: new Date().toISOString() };
|
|
159
|
+
await ctx.putDirectory(handle, rec);
|
|
160
|
+
console.log(`front @${handle}: ${body.paused ? 'paused' : 'resumed'} by its owner`);
|
|
161
|
+
return j(200, { ok: true, handle, ...await accountState(ctx, handle, rec) });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// The owner closing their address for good. Repeating it changes nothing.
|
|
165
|
+
if (pathname === '/api/close' && request.method === 'POST') {
|
|
166
|
+
if (!ctx.putDirectory) return j(501, { error: 'this front keeps no directory' });
|
|
167
|
+
let body;
|
|
168
|
+
try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
|
|
169
|
+
const handle = String(body.handle || '').toLowerCase();
|
|
170
|
+
let rec = await ctx.lookup(handle);
|
|
171
|
+
if (!rec) return j(404, { error: 'no such account' });
|
|
172
|
+
const who = await provedOwner(request, pathname, ctx, rec, deps);
|
|
173
|
+
if (who.error) return who.error;
|
|
174
|
+
if (body.confirm !== true) return j(400, { error: 'closing is for good — send confirm: true' });
|
|
175
|
+
if (!rec.closedAt) {
|
|
176
|
+
rec = { ...rec, closedAt: new Date().toISOString(), closedBy: 'owner' };
|
|
177
|
+
await ctx.putDirectory(handle, rec);
|
|
178
|
+
console.log(`front @${handle}: closed by its owner`);
|
|
179
|
+
}
|
|
180
|
+
return j(200, { ok: true, handle, ...await accountState(ctx, handle, rec) });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return null;
|
|
184
|
+
}
|
package/lib/pod/root.mjs
CHANGED
|
@@ -94,8 +94,11 @@ export async function probeAnswers(podUrl, fetchImpl = fetch) {
|
|
|
94
94
|
*
|
|
95
95
|
* ---- asked by: a front publishing a pod's public face under its own name ----
|
|
96
96
|
*/
|
|
97
|
-
export async function readPublicDocument(getter, podTarget, { podHome, maxBytes = PUBLIC_DOC_MAX_BYTES } = {}) {
|
|
98
|
-
const res = await getter(podTarget, { podHome });
|
|
97
|
+
export async function readPublicDocument(getter, podTarget, { podHome, accept = null, maxBytes = PUBLIC_DOC_MAX_BYTES } = {}) {
|
|
98
|
+
const res = await getter(podTarget, { podHome, accept });
|
|
99
99
|
if (!res || res.status >= 400) return { status: res?.status || 502, text: null };
|
|
100
|
-
|
|
100
|
+
// What the pod called it: a front that relabels an HTML page as JSON hands
|
|
101
|
+
// a browser something it will not render.
|
|
102
|
+
const type = res.headers?.get?.('content-type') || null;
|
|
103
|
+
return { status: 200, text: await readCapped(res, maxBytes), type };
|
|
101
104
|
}
|
package/lib/pod/state.mjs
CHANGED
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
|
|
12
12
|
// ---- the owner's agent ----
|
|
13
13
|
|
|
14
|
-
export const
|
|
14
|
+
export const readKeys = (pod, urls) => pod.getJson(urls.state + 'keys.json');
|
|
15
15
|
export const readConfig = (pod, urls) => pod.getJson(urls.state + 'config.json');
|
|
16
16
|
|
|
17
|
-
export const
|
|
18
|
-
pod.putJson(urls.state + 'keys.json',
|
|
17
|
+
export const writeKeys = (pod, urls, keys) =>
|
|
18
|
+
pod.putJson(urls.state + 'keys.json', keys, 'application/json');
|
|
19
19
|
export const writeConfig = (pod, urls, config) =>
|
|
20
20
|
pod.putJson(urls.state + 'config.json', config, 'application/json');
|
|
21
21
|
|
|
@@ -30,10 +30,11 @@ export const writeConfig = (pod, urls, config) =>
|
|
|
30
30
|
* public. As two statements this was two chances to get it wrong; as one
|
|
31
31
|
* operation there is nowhere to put them in the other order.
|
|
32
32
|
*
|
|
33
|
-
* The
|
|
34
|
-
*
|
|
33
|
+
* The keys record is stored as it is: the container's access rule is what
|
|
34
|
+
* keeps it to the owner, the same rule every private document on the pod
|
|
35
|
+
* lives under.
|
|
35
36
|
*/
|
|
36
|
-
export async function provisionKey(pod, { stateUrl, keysUrl,
|
|
37
|
+
export async function provisionKey(pod, { stateUrl, keysUrl, keys }) {
|
|
37
38
|
await pod.setAcl(stateUrl, []);
|
|
38
|
-
await pod.putJson(keysUrl,
|
|
39
|
+
await pod.putJson(keysUrl, keys, 'application/json');
|
|
39
40
|
}
|
package/lib/pod/transport.mjs
CHANGED
|
@@ -309,7 +309,7 @@ export class PodTransport {
|
|
|
309
309
|
// leaves the private trees world-readable. $rdf.sym() also throws on an
|
|
310
310
|
// illegal IRI, so a pod URL with something odd in it fails here rather than
|
|
311
311
|
// silently producing a document that means something else.
|
|
312
|
-
aclDoc(targetUrl, publicModes, { appendAgents = [], aclUrl = null } = {}) {
|
|
312
|
+
aclDoc(targetUrl, publicModes, { appendAgents = [], readAgents = [], aclUrl = null } = {}) {
|
|
313
313
|
const url = aclUrl || targetUrl + '.acl';
|
|
314
314
|
const doc = $rdf.sym(url);
|
|
315
315
|
const target = $rdf.sym(targetUrl);
|
|
@@ -328,6 +328,10 @@ export class PodTransport {
|
|
|
328
328
|
// locked to a gateway — public loses Append, the gateway's WebID keeps it.
|
|
329
329
|
appendAgents.forEach((webId, i) =>
|
|
330
330
|
authorize($rdf.sym(url + `#gw${i}`), ACL('agent'), $rdf.sym(webId), ['Append']));
|
|
331
|
+
// Named agents granted Read but not public: this is how a forum's
|
|
332
|
+
// moderators read a queue that names people, without it being public.
|
|
333
|
+
readAgents.forEach((webId, i) =>
|
|
334
|
+
authorize($rdf.sym(url + `#r${i}`), ACL('agent'), $rdf.sym(webId), ['Read']));
|
|
331
335
|
authorize($rdf.sym(url + '#owner'), ACL('agent'), $rdf.sym(this.webId),
|
|
332
336
|
['Read', 'Write', 'Control']);
|
|
333
337
|
return $rdf.serialize(doc, g, url, 'text/turtle');
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# fediverse-account
|
|
2
|
+
|
|
3
|
+
The library lets an app take a Fediverse handle or a WebID and act as that
|
|
4
|
+
account.
|
|
5
|
+
|
|
6
|
+
- **`login`** — sends the person to wherever their account signs in, their
|
|
7
|
+
Mastodon server or their pod, and brings them back to where they were.
|
|
8
|
+
- **`resume`** — on every page load, finishes a sign-in they are coming back
|
|
9
|
+
from. Otherwise does nothing.
|
|
10
|
+
- **`current`** — the signed-in account, or nothing.
|
|
11
|
+
- **`signOut`** — signs them out, at their server or pod as well as in the
|
|
12
|
+
browser.
|
|
13
|
+
- **`describe`** — says what an address is without signing in: a Mastodon
|
|
14
|
+
account or a pod account, and its handle.
|
|
15
|
+
|
|
16
|
+
Once signed in, the account gives you:
|
|
17
|
+
|
|
18
|
+
- **`post`** — publishes a post as them, public, unlisted or followers-only.
|
|
19
|
+
- **`reply`** — answers a post.
|
|
20
|
+
- **`timeline`** — their home timeline, newest first.
|
|
21
|
+
- **`follow`** — follows someone by handle.
|
|
22
|
+
- **`favourite`**, **`boost`** — as they say.
|
|
23
|
+
- **`profile`** — who they are as their server or pod shows them: name,
|
|
24
|
+
handle, picture, bio, counts, profile page, and the WebID for a pod
|
|
25
|
+
account.
|
|
26
|
+
- **`notice`** — a sentence to show them, or nothing. A browser-based
|
|
27
|
+
FediPod account gets: "Because you have a browser-based account, your
|
|
28
|
+
posts and interactions will only go out to the Fediverse when your browser
|
|
29
|
+
is opened to your account."
|
|
30
|
+
- **`handle`, `name`, `kind`** — who they are, and whether the account is on
|
|
31
|
+
Mastodon or a pod, for an app that wants to know.
|
|
32
|
+
- **`signOut`** — the same as above.
|
|
33
|
+
|
|
34
|
+
The app never asks which kind it has. A Mastodon account acts at once. A
|
|
35
|
+
pod account's actions are carried out by its own agent, at once for a
|
|
36
|
+
DeviceAgent or Server account, and when fedipod.net is next open for a
|
|
37
|
+
browser-based one.
|
|
38
|
+
|
|
39
|
+
Three files, no dependencies. It runs in a page and in a service worker.
|
|
40
|
+
|
|
41
|
+
[The demo](https://jeff-zucker.github.io/FediPod/) shows the sign-in and
|
|
42
|
+
the profile that comes back.
|
|
43
|
+
|
|
44
|
+
## Get it
|
|
45
|
+
|
|
46
|
+
From a CDN, with nothing to install:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
import { fediAccount } from 'https://cdn.jsdelivr.net/npm/fediverse-account@0.1.0/fedi-account.mjs';
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or from npm:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install fediverse-account
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import { fediAccount } from 'fediverse-account';
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Use
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import { fediAccount } from 'fediverse-account';
|
|
66
|
+
const accounts = fediAccount({ dbName: 'my-app', clientName: 'My app' });
|
|
67
|
+
|
|
68
|
+
await accounts.resume(); // on every page load
|
|
69
|
+
|
|
70
|
+
signInButton.onclick = () => accounts.login(addressField.value);
|
|
71
|
+
|
|
72
|
+
const me = await accounts.current(); // null when nobody is signed in
|
|
73
|
+
if (me?.notice) show(me.notice);
|
|
74
|
+
await me.post({ text: 'Hello from my app' });
|
|
75
|
+
for (const p of await me.timeline({ limit: 20 })) render(p);
|
|
76
|
+
await me.follow('@aisha@her.server');
|
|
77
|
+
await me.reply(p.url, 'Well said');
|
|
78
|
+
await me.signOut();
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## The details
|
|
82
|
+
|
|
83
|
+
- **`fediAccount({ dbName, clientName })`** — sets the library up for your
|
|
84
|
+
app, once. `dbName` keeps this app's sign-ins apart from another app's on
|
|
85
|
+
the same origin. `clientName` is what the person sees on the consent
|
|
86
|
+
screen.
|
|
87
|
+
- **`login(address, { returnTo, redirectUri })`** — `@kwame@mastodon.social`
|
|
88
|
+
goes to Mastodon; `@mei@fedipod.net`, or a WebID, goes to the pod. They
|
|
89
|
+
come back to this page unless `redirectUri` says otherwise, and are
|
|
90
|
+
returned to where they were unless `returnTo` says otherwise.
|
|
91
|
+
- **`post({ text, inReplyTo, visibility })`** — `visibility` is `public`,
|
|
92
|
+
`unlisted` or `followers`.
|
|
93
|
+
- **`reply(post, text)`** — the post is named by its address or its id.
|
|
94
|
+
- **`timeline({ limit })`** — each post as `{ id, url, author: { id, handle,
|
|
95
|
+
name }, html, published, inReplyTo }`.
|
|
96
|
+
- **`fetch`** — the raw authenticated fetch, for anything the above does not
|
|
97
|
+
cover.
|
|
98
|
+
- **`actor`** — the account's ActivityPub id.
|
|
99
|
+
|
|
100
|
+
Every action answers `{ sent: true }` when the server did it, or `{ queued:
|
|
101
|
+
true }` when the account's own agent will carry it out.
|
|
102
|
+
|
|
103
|
+
## What it cannot do
|
|
104
|
+
|
|
105
|
+
- **An address whose server is neither a Mastodon-family server nor a Solid
|
|
106
|
+
pod** is refused with a sentence naming the host.
|
|
107
|
+
- **A browser-based FediPod account acts when its browser is open.** Posts,
|
|
108
|
+
follows and replies wait at the account's outbox until fedipod.net is next
|
|
109
|
+
open in some browser, and its timeline is the one that browser last built.
|
|
110
|
+
The account's `notice` says so; show it.
|
|
111
|
+
- **A Mastodon account has no pod.** An app that keeps things on a pod has
|
|
112
|
+
nowhere to keep them for such a person.
|
|
113
|
+
|
|
114
|
+
## Where the sign-in goes, and what stays in the browser
|
|
115
|
+
|
|
116
|
+
A Mastodon account signs in at its own server, which asks the person to
|
|
117
|
+
approve your app once. The token it grants stays in their browser and is
|
|
118
|
+
handed back to the server at sign-out.
|
|
119
|
+
|
|
120
|
+
A pod account signs in at the pod's login provider. The key that proves the
|
|
121
|
+
sign-in is made in the browser and cannot be read out of it, even by a
|
|
122
|
+
script on the page. The person stays signed in across browser restarts
|
|
123
|
+
until they sign out, and sign-out is reported to the provider.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Act as a Fediverse account</title>
|
|
7
|
+
<link rel="stylesheet" href="../../web/admin/tokens.css">
|
|
8
|
+
<style>
|
|
9
|
+
body { font-family: var(--font, Arial, Helvetica, sans-serif); background: var(--bg); color: var(--fg); margin: 0; padding: .75rem 1rem 2rem; }
|
|
10
|
+
main { max-width: 44rem; margin: 0 auto; }
|
|
11
|
+
h1 { margin-top: .5rem; }
|
|
12
|
+
form { display: flex; gap: .75rem; flex-wrap: wrap; align-items: end; margin: 1.25rem 0; }
|
|
13
|
+
label { display: flex; flex-direction: column; gap: .35rem; flex: 1 1 18rem; }
|
|
14
|
+
input { font: inherit; padding: .6rem .8rem; background: var(--field-bg); color: var(--field-text); border: 1px solid var(--field-border); border-radius: .4rem; }
|
|
15
|
+
button { font: inherit; padding: .6rem 1.2rem; background: var(--btn); color: var(--btn-text); border: 1px solid var(--btn-edge); border-radius: .4rem; cursor: pointer; }
|
|
16
|
+
button:hover { background: var(--btn-hover); }
|
|
17
|
+
a { color: var(--link); }
|
|
18
|
+
.kinds { padding-left: 1.4rem; margin: .75rem 0 1.25rem; } .kinds li { margin: .4rem 0; }
|
|
19
|
+
:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
|
|
20
|
+
.skip { position: absolute; left: -999px; } .skip:focus { left: 1rem; top: 1rem; }
|
|
21
|
+
#status, #profile { padding: 1rem 1.2rem; border: 1px solid var(--line); border-radius: .4rem; background: var(--surface); margin: 1rem 0; }
|
|
22
|
+
#status { min-height: 1.5em; }
|
|
23
|
+
#status[data-kind="error"] { border-color: var(--err); }
|
|
24
|
+
#status[hidden] { display: none; }
|
|
25
|
+
#profile { display: grid; grid-template-columns: auto 1fr; gap: .5rem 1.2rem; align-items: start; }
|
|
26
|
+
#profile img { width: 5rem; height: 5rem; border-radius: .6rem; object-fit: cover; background: var(--raised); }
|
|
27
|
+
#profile dl { margin: 0; display: grid; grid-template-columns: max-content 1fr; gap: .3rem 1rem; }
|
|
28
|
+
#profile dt { font-weight: bold; } #profile dd { margin: 0; overflow-wrap: anywhere; }
|
|
29
|
+
section[hidden] { display: none; }
|
|
30
|
+
</style>
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<a href="#main" class="skip">Skip to content</a>
|
|
34
|
+
<main id="main">
|
|
35
|
+
<h1>Act as a Fediverse account</h1>
|
|
36
|
+
<p>Type an address and you are sent to wherever that account signs in, then come back here signed in as it. Any of these works:</p>
|
|
37
|
+
<ul class="kinds">
|
|
38
|
+
<li><strong>A FediPod handle</strong> — <code>@jeff@fedipod.net</code> — signs in at your Solid pod</li>
|
|
39
|
+
<li><strong>Other Fediverse handle</strong> — <code>@you@mastodon.social</code> — signs in at your server</li>
|
|
40
|
+
<li><strong>A WebID</strong> — <code>https://you.solidcommunity.net/profile/card#me</code> — signs in at your Solid pod</li>
|
|
41
|
+
</ul>
|
|
42
|
+
<p>This demo is created with the <a href="https://github.com/jeff-zucker/FediPod/tree/main/lib/session">fediverse-account</a> library. Once signed in, the demo shows you your profile. The library itself returns an account session with methods to post, reply, like, etc.</p>
|
|
43
|
+
<form id="form">
|
|
44
|
+
<label>Your address
|
|
45
|
+
<input id="address" name="address" type="text" autocomplete="username" placeholder="@you@your.server" required>
|
|
46
|
+
</label>
|
|
47
|
+
<button type="submit" id="go">Sign in</button>
|
|
48
|
+
<button type="button" id="out" hidden>Sign out</button>
|
|
49
|
+
</form>
|
|
50
|
+
<p id="status" role="status" aria-live="polite">Not signed in.</p>
|
|
51
|
+
<section id="acting" hidden aria-labelledby="profile-h">
|
|
52
|
+
<h2 id="profile-h">Profile</h2>
|
|
53
|
+
<div id="profile"></div>
|
|
54
|
+
</section>
|
|
55
|
+
</main>
|
|
56
|
+
<script type="module">
|
|
57
|
+
import { fediAccount } from './fedi-account.mjs';
|
|
58
|
+
const accounts = fediAccount({ dbName: 'fedi-account-demo', clientName: 'Fediverse account demo' });
|
|
59
|
+
const $ = (id) => document.getElementById(id);
|
|
60
|
+
const say = (text, kind = '') => { $('status').hidden = false; $('status').textContent = text; $('status').dataset.kind = kind; $('status').setAttribute('role', kind === 'error' ? 'alert' : 'status'); };
|
|
61
|
+
const esc = (s) => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
62
|
+
const link = (u) => (u ? `<a href="${esc(u)}">${esc(u)}</a>` : '—');
|
|
63
|
+
const showProfile = async (me) => {
|
|
64
|
+
try {
|
|
65
|
+
const p = await me.profile();
|
|
66
|
+
$('profile').innerHTML = `${p.avatar ? `<img src="${esc(p.avatar)}" alt="">` : '<span></span>'}
|
|
67
|
+
<dl>
|
|
68
|
+
<dt>Name</dt><dd>${esc(p.name || '—')}</dd>
|
|
69
|
+
<dt>Handle</dt><dd>${esc(p.handle || '—')}</dd>
|
|
70
|
+
<dt>Kind</dt><dd>${me.kind === 'pod' ? 'a FediPod account on a Solid pod' : 'a Mastodon account'}</dd>
|
|
71
|
+
<dt>Bio</dt><dd>${p.bio || '—'}</dd>
|
|
72
|
+
<dt>Counts</dt><dd>${p.posts ?? '?'} posts, ${p.followers ?? '?'} followers, ${p.following ?? '?'} following</dd>
|
|
73
|
+
<dt>Profile page</dt><dd>${link(p.url)}</dd>
|
|
74
|
+
<dt>Actor</dt><dd>${link(p.actor)}</dd>
|
|
75
|
+
${p.webId ? `<dt>WebID</dt><dd>${link(p.webId)}</dd>` : ''}
|
|
76
|
+
</dl>`;
|
|
77
|
+
} catch (e) { $('profile').textContent = `The profile could not be read: ${e.message}`; }
|
|
78
|
+
};
|
|
79
|
+
const show = async () => {
|
|
80
|
+
const me = await accounts.current();
|
|
81
|
+
$('out').hidden = !me; $('acting').hidden = !me; $('status').hidden = !!me;
|
|
82
|
+
if (!me) { say('Not signed in.'); $('profile').innerHTML = ''; return; }
|
|
83
|
+
await showProfile(me);
|
|
84
|
+
};
|
|
85
|
+
try { await accounts.resume(); } catch (e) { say(e.message, 'error'); }
|
|
86
|
+
await show();
|
|
87
|
+
$('form').addEventListener('submit', async (e) => {
|
|
88
|
+
e.preventDefault(); say('Finding that account…');
|
|
89
|
+
try { await accounts.login($('address').value); } catch (err) { say(err.message, 'error'); }
|
|
90
|
+
});
|
|
91
|
+
$('out').addEventListener('click', async () => { await accounts.signOut(); await show(); });
|
|
92
|
+
</script>
|
|
93
|
+
</body>
|
|
94
|
+
</html>
|