fedipod 0.8.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/LICENSE +21 -0
- package/README.md +123 -0
- package/architecture.svg +89 -0
- package/bin/fedipod.mjs +2303 -0
- package/cli.md +243 -0
- package/groups.md +85 -0
- package/gui.md +99 -0
- package/lib/account.mjs +87 -0
- package/lib/admin.mjs +1606 -0
- package/lib/atproto.mjs +310 -0
- package/lib/bskyfeed.mjs +166 -0
- package/lib/bskygroup.mjs +162 -0
- package/lib/c2s.mjs +305 -0
- package/lib/certs.mjs +265 -0
- package/lib/deliver.mjs +249 -0
- package/lib/directory.mjs +90 -0
- package/lib/embed.mjs +214 -0
- package/lib/export-collections.mjs +161 -0
- package/lib/front-core.mjs +322 -0
- package/lib/gateway-core.mjs +115 -0
- package/lib/guard.mjs +213 -0
- package/lib/home.mjs +161 -0
- package/lib/httpsig.mjs +109 -0
- package/lib/import.mjs +357 -0
- package/lib/intake.mjs +1710 -0
- package/lib/keys.mjs +139 -0
- package/lib/lease.mjs +190 -0
- package/lib/localapi.mjs +87 -0
- package/lib/mastoapi.mjs +1588 -0
- package/lib/migrate.mjs +127 -0
- package/lib/oidc-auth.mjs +53 -0
- package/lib/podrdf.mjs +132 -0
- package/lib/ports.mjs +27 -0
- package/lib/publisher.mjs +1003 -0
- package/lib/remote.mjs +303 -0
- package/lib/safefetch.mjs +235 -0
- package/lib/setup.mjs +262 -0
- package/lib/social.mjs +505 -0
- package/lib/storage.mjs +170 -0
- package/lib/store.mjs +470 -0
- package/lib/streaming.mjs +154 -0
- package/lib/tagfeed.mjs +158 -0
- package/lib/ua.mjs +29 -0
- package/lib/update.mjs +91 -0
- package/lib/webpush.mjs +98 -0
- package/lib/wire.mjs +762 -0
- package/package.json +60 -0
- package/phanpy/dist/404.html +40 -0
- package/phanpy/dist/_headers +2 -0
- package/phanpy/dist/apple-touch-icon.png +0 -0
- package/phanpy/dist/assets/ICONS-DIPJeU0C.js +2 -0
- package/phanpy/dist/assets/ICONS-DIPJeU0C.js.map +1 -0
- package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js +183 -0
- package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js.map +1 -0
- package/phanpy/dist/assets/Temml-CCjoacWt.woff2 +0 -0
- package/phanpy/dist/assets/__vite-browser-external-C7iut881.js +2 -0
- package/phanpy/dist/assets/__vite-browser-external-C7iut881.js.map +1 -0
- package/phanpy/dist/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
- package/phanpy/dist/assets/catch-up-O5q5C75Z.png +0 -0
- package/phanpy/dist/assets/chunk-aKtaBQYM.js +1 -0
- package/phanpy/dist/assets/compose-CFEx7wb4.js +2 -0
- package/phanpy/dist/assets/compose-CFEx7wb4.js.map +1 -0
- package/phanpy/dist/assets/compose-ZJN5xSb4.js +35 -0
- package/phanpy/dist/assets/compose-ZJN5xSb4.js.map +1 -0
- package/phanpy/dist/assets/debug.module-YFQgwnIr.js +52 -0
- package/phanpy/dist/assets/debug.module-YFQgwnIr.js.map +1 -0
- package/phanpy/dist/assets/dom-p1YcJ5Rw.js +2 -0
- package/phanpy/dist/assets/dom-p1YcJ5Rw.js.map +1 -0
- package/phanpy/dist/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
- package/phanpy/dist/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
- package/phanpy/dist/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
- package/phanpy/dist/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
- package/phanpy/dist/assets/icons/add-circle-line-WD0mwORD.js +1 -0
- package/phanpy/dist/assets/icons/alert-line-CpFUMf9s.js +1 -0
- package/phanpy/dist/assets/icons/android-2-line-Bzxnrd3S.js +1 -0
- package/phanpy/dist/assets/icons/announcement-line-5nwaJFJm.js +1 -0
- package/phanpy/dist/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +1 -0
- package/phanpy/dist/assets/icons/arrow-down-line-B8uzXzq6.js +1 -0
- package/phanpy/dist/assets/icons/arrow-left-line-CGrGT9C-.js +1 -0
- package/phanpy/dist/assets/icons/arrow-right-line-CE-HERfX.js +1 -0
- package/phanpy/dist/assets/icons/arrow-to-up-line-CvdQjmcy.js +1 -0
- package/phanpy/dist/assets/icons/arrow-up-circle-line-DEparSNF.js +1 -0
- package/phanpy/dist/assets/icons/arrow-up-line-C7yBeugc.js +1 -0
- package/phanpy/dist/assets/icons/arrows-right-line-BtbXmJGP.js +1 -0
- package/phanpy/dist/assets/icons/at-line-CF66Bznk.js +1 -0
- package/phanpy/dist/assets/icons/attachment-line-BY8It-l9.js +1 -0
- package/phanpy/dist/assets/icons/blockquote-line-BS6pU_hm.js +1 -0
- package/phanpy/dist/assets/icons/board-line-C0UWXyA6.js +1 -0
- package/phanpy/dist/assets/icons/bookmark-line-1CtX5JLF.js +1 -0
- package/phanpy/dist/assets/icons/building-5-line-Dd59MzbO.js +1 -0
- package/phanpy/dist/assets/icons/bus-2-line-6_dvA0GG.js +1 -0
- package/phanpy/dist/assets/icons/calendar-day-line-Crpl_fmu.js +1 -0
- package/phanpy/dist/assets/icons/calendar-month-line-vommts2C.js +1 -0
- package/phanpy/dist/assets/icons/calendar-time-add-line-BU_DTlQk.js +1 -0
- package/phanpy/dist/assets/icons/camera-line-Cv69MEei.js +1 -0
- package/phanpy/dist/assets/icons/celebrate-line-B8DvIoi0.js +1 -0
- package/phanpy/dist/assets/icons/chart-bar-line-FK-QOmtF.js +1 -0
- package/phanpy/dist/assets/icons/chart-line-line-BGcJAFWw.js +1 -0
- package/phanpy/dist/assets/icons/chat-3-line-YFK2-hwz.js +1 -0
- package/phanpy/dist/assets/icons/check-circle-line-DsAxFr5-.js +1 -0
- package/phanpy/dist/assets/icons/clipboard-line-BBp364gJ.js +1 -0
- package/phanpy/dist/assets/icons/close-circle-line-CICdiuGz.js +1 -0
- package/phanpy/dist/assets/icons/close-line-DYrFWcjv.js +1 -0
- package/phanpy/dist/assets/icons/cloud-line-tiQAbah8.js +1 -0
- package/phanpy/dist/assets/icons/code-line-fAl8iiV3.js +1 -0
- package/phanpy/dist/assets/icons/comment-2-line-CR3g7WeB.js +1 -0
- package/phanpy/dist/assets/icons/copy-2-line-CLW69yF5.js +1 -0
- package/phanpy/dist/assets/icons/delete-2-line-B8xmbr4n.js +1 -0
- package/phanpy/dist/assets/icons/document-3-line-DBGNhAg2.js +1 -0
- package/phanpy/dist/assets/icons/document-line-C7mb9sb7.js +1 -0
- package/phanpy/dist/assets/icons/down-line-BGqSSThd.js +1 -0
- package/phanpy/dist/assets/icons/edit-4-line-DcgGaEhx.js +1 -0
- package/phanpy/dist/assets/icons/emoji-2-line-D8YgnM-y.js +1 -0
- package/phanpy/dist/assets/icons/exit-line-Dc07BS-x.js +1 -0
- package/phanpy/dist/assets/icons/external-link-line-BvpSkmjO.js +1 -0
- package/phanpy/dist/assets/icons/eye-2-line-CiWXx-f_.js +1 -0
- package/phanpy/dist/assets/icons/eye-close-line-BgTaSJ4P.js +1 -0
- package/phanpy/dist/assets/icons/filter-2-line-B_mR2usy.js +1 -0
- package/phanpy/dist/assets/icons/filter-line-C_nm-3cU.js +1 -0
- package/phanpy/dist/assets/icons/flag-1-line-C2HB05I0.js +1 -0
- package/phanpy/dist/assets/icons/forbid-circle-line-D3LTT9yd.js +1 -0
- package/phanpy/dist/assets/icons/formula-line-DLrt2d6j.js +1 -0
- package/phanpy/dist/assets/icons/grid-line-CgUmEikf.js +1 -0
- package/phanpy/dist/assets/icons/group-line-C5fnX0wU.js +1 -0
- package/phanpy/dist/assets/icons/hand-finger-2-line-BGgxbuL9.js +1 -0
- package/phanpy/dist/assets/icons/hashtag-line-CyTEEXBb.js +1 -0
- package/phanpy/dist/assets/icons/heart-crack-line-BZddbO2C.js +1 -0
- package/phanpy/dist/assets/icons/heart-line-CJ7K47J4.js +1 -0
- package/phanpy/dist/assets/icons/history-2-line-kWWvFzqv.js +1 -0
- package/phanpy/dist/assets/icons/history-line-bGQiwbTH.js +1 -0
- package/phanpy/dist/assets/icons/home-3-line-fRU1zeLG.js +1 -0
- package/phanpy/dist/assets/icons/information-line-iNk6-sOY.js +1 -0
- package/phanpy/dist/assets/icons/keyboard-line-DsCyuQNz.js +1 -0
- package/phanpy/dist/assets/icons/layout-4-line-CZkOX52Z.js +1 -0
- package/phanpy/dist/assets/icons/layout-5-line-B_znt5wO.js +1 -0
- package/phanpy/dist/assets/icons/left-line-eUw0tya4.js +1 -0
- package/phanpy/dist/assets/icons/lightning-line-DaKoDdix.js +1 -0
- package/phanpy/dist/assets/icons/link-2-line-DCqLmMA7.js +1 -0
- package/phanpy/dist/assets/icons/list-check-line-BAxosBh9.js +1 -0
- package/phanpy/dist/assets/icons/lock-line-CPoaXeUW.js +1 -0
- package/phanpy/dist/assets/icons/mail-line-B0P8aa1L.js +1 -0
- package/phanpy/dist/assets/icons/moon-line-CEqNAToP.js +1 -0
- package/phanpy/dist/assets/icons/more-1-fill-Dw2e5xab.js +1 -0
- package/phanpy/dist/assets/icons/more-3-line-BIiVjYTz.js +1 -0
- package/phanpy/dist/assets/icons/notification-line-BZZq2Gtu.js +1 -0
- package/phanpy/dist/assets/icons/pencil-line-CKh8-A1v.js +1 -0
- package/phanpy/dist/assets/icons/photo-album-line-Bnfg2u40.js +1 -0
- package/phanpy/dist/assets/icons/pin-line-DpTolyhs.js +1 -0
- package/phanpy/dist/assets/icons/play-fill-B0-G_8lX.js +1 -0
- package/phanpy/dist/assets/icons/qrcode-2-line-B6SPCaHn.js +1 -0
- package/phanpy/dist/assets/icons/quill-pen-line-B6LUrnCP.js +1 -0
- package/phanpy/dist/assets/icons/quote-left-fill-hQZ1cj_6.js +1 -0
- package/phanpy/dist/assets/icons/radar-line-CKyroTth.js +1 -0
- package/phanpy/dist/assets/icons/react-line-CoIKhhtK.js +1 -0
- package/phanpy/dist/assets/icons/refresh-2-line-D0mTF97S.js +1 -0
- package/phanpy/dist/assets/icons/right-line-B3KWJaqc.js +1 -0
- package/phanpy/dist/assets/icons/rocket-line-4KZl537D.js +1 -0
- package/phanpy/dist/assets/icons/round-fill-C69NTrZ0.js +1 -0
- package/phanpy/dist/assets/icons/round-line-B9k_1uo-.js +1 -0
- package/phanpy/dist/assets/icons/route-line-B28ReHcE.js +1 -0
- package/phanpy/dist/assets/icons/rows-4-line-CrBniagM.js +1 -0
- package/phanpy/dist/assets/icons/scan-line-DSf74YdP.js +1 -0
- package/phanpy/dist/assets/icons/search-2-line-B6geIU1z.js +1 -0
- package/phanpy/dist/assets/icons/settings-3-line-azJwK47w.js +1 -0
- package/phanpy/dist/assets/icons/settings-6-line-C_5zc5YR.js +1 -0
- package/phanpy/dist/assets/icons/share-2-line-D5MxWjrm.js +1 -0
- package/phanpy/dist/assets/icons/share-forward-line-Cp61fZzZ.js +1 -0
- package/phanpy/dist/assets/icons/sparkles-2-line-DYYyXTaW.js +1 -0
- package/phanpy/dist/assets/icons/sparkles-line-C2hY1s6I.js +1 -0
- package/phanpy/dist/assets/icons/time-line-BF3eet1v.js +1 -0
- package/phanpy/dist/assets/icons/transfer-4-line-oisfvTBw.js +1 -0
- package/phanpy/dist/assets/icons/translate-line-BFWol2Ce.js +1 -0
- package/phanpy/dist/assets/icons/unlock-line-X_x5vwv3.js +1 -0
- package/phanpy/dist/assets/icons/upload-3-line-DgzwUJeW.js +1 -0
- package/phanpy/dist/assets/icons/user-4-line-BVSynZCp.js +1 -0
- package/phanpy/dist/assets/icons/user-add-2-line-BlGS5Q8q.js +1 -0
- package/phanpy/dist/assets/icons/user-add-line-BbXdHG4z.js +1 -0
- package/phanpy/dist/assets/icons/user-edit-line-BFAHOV0X.js +1 -0
- package/phanpy/dist/assets/icons/user-follow-line-BXux0wSd.js +1 -0
- package/phanpy/dist/assets/icons/user-star-line-DdYlpHLU.js +1 -0
- package/phanpy/dist/assets/icons/user-warning-line-Cj43j0_E.js +1 -0
- package/phanpy/dist/assets/icons/user-x-line-DIDgNlPH.js +1 -0
- package/phanpy/dist/assets/icons/volume-line-Bs390KQt.js +1 -0
- package/phanpy/dist/assets/icons/volume-mute-line-DzlzIqFZ.js +1 -0
- package/phanpy/dist/assets/icons/walk-line-yNXfB-md.js +1 -0
- package/phanpy/dist/assets/icons/world-2-line-CenkGIwj.js +1 -0
- package/phanpy/dist/assets/icons/zoom-in-line-CaENUphf.js +1 -0
- package/phanpy/dist/assets/icons/zoom-out-line-BDNk0EWo.js +1 -0
- package/phanpy/dist/assets/instances-BNPptnn-.json +333 -0
- package/phanpy/dist/assets/locales/ar-SA-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/locales/ca-ES-DHRW86Tz.js +1 -0
- package/phanpy/dist/assets/locales/cs-CZ-CKoqEuA1.js +1 -0
- package/phanpy/dist/assets/locales/de-DE-ByRPMRFy.js +1 -0
- package/phanpy/dist/assets/locales/eo-UY-CPC7wLCS.js +1 -0
- package/phanpy/dist/assets/locales/es-ES-dJa0Fd9t.js +1 -0
- package/phanpy/dist/assets/locales/eu-ES-C3djlPR2.js +1 -0
- package/phanpy/dist/assets/locales/fa-IR-DTT490Ck.js +1 -0
- package/phanpy/dist/assets/locales/fi-FI-B23MCPuz.js +1 -0
- package/phanpy/dist/assets/locales/fr-FR-BcaQsO5C.js +1 -0
- package/phanpy/dist/assets/locales/gl-ES-BR-CVKBN.js +1 -0
- package/phanpy/dist/assets/locales/he-IL-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/locales/hu-HU-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/locales/it-IT-C1136UFV.js +1 -0
- package/phanpy/dist/assets/locales/ja-JP-CRZmRAhD.js +1 -0
- package/phanpy/dist/assets/locales/kab-Cz8xH_TT.js +1 -0
- package/phanpy/dist/assets/locales/ko-KR-CG9UiY4o.js +1 -0
- package/phanpy/dist/assets/locales/lt-LT-CJcI6sHZ.js +1 -0
- package/phanpy/dist/assets/locales/nb-NO-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/locales/nl-NL-DxN4Yc1t.js +1 -0
- package/phanpy/dist/assets/locales/oc-FR-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/locales/pl-PL-DkwGXf6P.js +1 -0
- package/phanpy/dist/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +1 -0
- package/phanpy/dist/assets/locales/pt-BR-nGL0HdFS.js +1 -0
- package/phanpy/dist/assets/locales/pt-PT-Cp17aj2z.js +1 -0
- package/phanpy/dist/assets/locales/ru-RU-0Y8cIoaK.js +1 -0
- package/phanpy/dist/assets/locales/th-TH-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/locales/tok-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/locales/tr-TR-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/locales/uk-UA-C5E3dFBf.js +1 -0
- package/phanpy/dist/assets/locales/zh-CN-BHJi7_et.js +1 -0
- package/phanpy/dist/assets/locales/zh-TW-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/main-BdqNbG-a.js +26 -0
- package/phanpy/dist/assets/main-BdqNbG-a.js.map +1 -0
- package/phanpy/dist/assets/mock-home-DGhR__He.js +2 -0
- package/phanpy/dist/assets/mock-home-DGhR__He.js.map +1 -0
- package/phanpy/dist/assets/multi-column-ROck0NVt.jpg +0 -0
- package/phanpy/dist/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
- package/phanpy/dist/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
- package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js +2 -0
- package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js.map +1 -0
- package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js +20 -0
- package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js.map +1 -0
- package/phanpy/dist/assets/style-B1iLPTPR.css +2 -0
- package/phanpy/dist/assets/temml-DuyIQMei.js +97 -0
- package/phanpy/dist/assets/temml-DuyIQMei.js.map +1 -0
- package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js +2 -0
- package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js.map +1 -0
- package/phanpy/dist/assets/year-in-posts-B2lywqdx.png +0 -0
- package/phanpy/dist/assets/year-in-posts-DJv_star.js +3 -0
- package/phanpy/dist/assets/year-in-posts-DJv_star.js.map +1 -0
- package/phanpy/dist/compose/index.html +20 -0
- package/phanpy/dist/favicon.ico +0 -0
- package/phanpy/dist/index.html +73 -0
- package/phanpy/dist/logo-192.png +0 -0
- package/phanpy/dist/logo-512.png +0 -0
- package/phanpy/dist/logo-badge-72.png +0 -0
- package/phanpy/dist/logo-maskable-512.png +0 -0
- package/phanpy/dist/logo-monochrome-512.png +0 -0
- package/phanpy/dist/logo-monochrome-maskable-512.png +0 -0
- package/phanpy/dist/manifest.webmanifest +1 -0
- package/phanpy/dist/og-image-2.jpg +0 -0
- package/phanpy/dist/og-image.png +0 -0
- package/phanpy/dist/robots.txt +2 -0
- package/phanpy/dist/sw.js +25 -0
- package/phanpy/dist/version.json +1 -0
- package/run-agent.mjs +805 -0
- package/scripts/build-dist.mjs +37 -0
- package/scripts/verify-ui.mjs +60 -0
- package/ui/README.md +23 -0
- package/vendor/gate.cjs +132 -0
- package/vendor/idp-grant.cjs +394 -0
- package/vendor/jfetch.cjs +16 -0
- package/web/admin/admin.js +958 -0
- package/web/admin/bar.css +64 -0
- package/web/admin/bar.js +61 -0
- package/web/admin/client/client.js +66 -0
- package/web/admin/client/index.html +42 -0
- package/web/admin/index.html +493 -0
- package/web/admin/setup/index.html +172 -0
- package/web/admin/setup/setup.js +268 -0
- package/web/admin/window.css +55 -0
- package/web/admin/window.js +136 -0
- package/web/front/install.sh +69 -0
- package/web/front/new-account.html +424 -0
- package/web/front/run.html +138 -0
- package/web/front/solid-client-authn.bundle.js +2 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# FediPod installer. Usage: curl -fsSL https://fedipod.net/install | sh
|
|
3
|
+
# Installs into ~/FediPod (or $FEDIPOD_DIR); re-running updates the install.
|
|
4
|
+
#
|
|
5
|
+
# A signup page may append parameters (sh -s -- --gateway ... --secret ...);
|
|
6
|
+
# they are saved for the first start, whose setup opens pre-filled and
|
|
7
|
+
# attaches to the gateway when it completes.
|
|
8
|
+
set -eu
|
|
9
|
+
|
|
10
|
+
REPO="https://github.com/jeff-zucker/FediPod"
|
|
11
|
+
DIR="${FEDIPOD_DIR:-$HOME/FediPod}"
|
|
12
|
+
|
|
13
|
+
say() { printf '%s\n' "$*"; }
|
|
14
|
+
fail() { printf 'fedipod install: %s\n' "$*" >&2; exit 1; }
|
|
15
|
+
|
|
16
|
+
GATEWAY= SECRET= POD= ISSUER= HANDLE= KIND=person FRONTED=false
|
|
17
|
+
while [ $# -gt 0 ]; do
|
|
18
|
+
case "$1" in
|
|
19
|
+
--gateway) GATEWAY=$2; shift 2;;
|
|
20
|
+
--secret) SECRET=$2; shift 2;;
|
|
21
|
+
--pod) POD=$2; shift 2;;
|
|
22
|
+
--issuer) ISSUER=$2; shift 2;;
|
|
23
|
+
--handle) HANDLE=$2; shift 2;;
|
|
24
|
+
--kind) KIND=$2; shift 2;;
|
|
25
|
+
--fronted) FRONTED=true; shift;;
|
|
26
|
+
*) fail "unknown option $1";;
|
|
27
|
+
esac
|
|
28
|
+
done
|
|
29
|
+
|
|
30
|
+
command -v git >/dev/null 2>&1 || fail "git is required — install it from https://git-scm.com/ and re-run"
|
|
31
|
+
command -v node >/dev/null 2>&1 || fail "node 20 or newer is required — install it from https://nodejs.org/ and re-run"
|
|
32
|
+
command -v npm >/dev/null 2>&1 || fail "npm is required (it ships with node) — install node from https://nodejs.org/ and re-run"
|
|
33
|
+
|
|
34
|
+
major=$(node -v | sed 's/^v//' | cut -d. -f1)
|
|
35
|
+
[ "$major" -ge 20 ] 2>/dev/null || fail "node 20 or newer is required (found $(node -v))"
|
|
36
|
+
|
|
37
|
+
if [ -d "$DIR/.git" ]; then
|
|
38
|
+
say "updating the existing install in $DIR"
|
|
39
|
+
git -C "$DIR" pull --ff-only || fail "could not update $DIR — it has local changes; update it by hand"
|
|
40
|
+
elif [ -e "$DIR" ]; then
|
|
41
|
+
fail "$DIR exists and is not a FediPod checkout — set FEDIPOD_DIR to another location and re-run"
|
|
42
|
+
else
|
|
43
|
+
say "installing into $DIR"
|
|
44
|
+
git clone --depth 1 "$REPO" "$DIR"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
cd "$DIR"
|
|
48
|
+
npm install --omit=dev --no-audit --no-fund
|
|
49
|
+
|
|
50
|
+
# Put `fedipod` on PATH, the same command an `npm install -g fedipod` gives.
|
|
51
|
+
# Everything printed from here on — the docs, the agent's own advice, the
|
|
52
|
+
# command a signup page hands you — names it that way, and a checkout that
|
|
53
|
+
# skipped this step could run none of them.
|
|
54
|
+
if npm link >/dev/null 2>&1; then
|
|
55
|
+
say "linked: the fedipod command is on your PATH"
|
|
56
|
+
else
|
|
57
|
+
say "note: could not link the fedipod command — run it as: node $DIR/bin/fedipod.mjs"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
if [ -n "$HANDLE" ]; then
|
|
61
|
+
printf '{"handle":"%s","kind":"%s","pod":"%s","issuer":"%s","gateway":"%s","secret":"%s","fronted":%s}\n' \
|
|
62
|
+
"$HANDLE" "$KIND" "$POD" "$ISSUER" "$GATEWAY" "$SECRET" "$FRONTED" > "$DIR/first-run.json"
|
|
63
|
+
chmod 600 "$DIR/first-run.json"
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
say ""
|
|
67
|
+
say "Installed. To start (first run opens setup in your browser):"
|
|
68
|
+
say ""
|
|
69
|
+
say " fedipod start"
|
|
@@ -0,0 +1,424 @@
|
|
|
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>FediPod — keep your name, lose the noise</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: light dark; --line:#d9d4c7; --dim:#45443d; --accent:#6a8a5a; }
|
|
9
|
+
@media (prefers-color-scheme: dark) { :root { --dim:#c2c0b4; } }
|
|
10
|
+
body { font: 19px/1.6 system-ui, sans-serif; max-width: 44rem; margin: 2rem auto; padding: 0 1.1rem; }
|
|
11
|
+
h1 { font-size: 1.7rem; margin: 0 0 .2rem; }
|
|
12
|
+
h2 { font-size: 1.2rem; margin: 0 0 .4rem; }
|
|
13
|
+
p.lede { color: var(--dim); margin-top: 0; }
|
|
14
|
+
fieldset { border: 1px solid var(--line); border-radius: 12px; padding: 1rem 1.1rem; margin: 1rem 0; }
|
|
15
|
+
legend { font-weight: 600; padding: 0 .4rem; }
|
|
16
|
+
.choices { display: flex; flex-direction: column; gap: .5rem; }
|
|
17
|
+
.choices button { padding: .7rem .8rem; border: 1px solid var(--line); border-radius: 10px;
|
|
18
|
+
background: transparent; font: inherit; cursor: pointer; text-align: left; }
|
|
19
|
+
.choices button[aria-pressed="true"] { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
|
|
20
|
+
.choices button b { display: block; }
|
|
21
|
+
.choices button span { color: var(--dim); font-size: .95rem; }
|
|
22
|
+
label { display: block; font-weight: 600; margin: .8rem 0 .2rem; }
|
|
23
|
+
input[type=text], input[type=url] { width: 100%; padding: .5rem .6rem; font: inherit;
|
|
24
|
+
border: 1px solid var(--line); border-radius: 8px; box-sizing: border-box; }
|
|
25
|
+
.msg { min-height: 1.3em; font-size: .95rem; margin-top: .3rem; }
|
|
26
|
+
.msg.ok { color: var(--accent); } .msg.no { color: #a4433a; }
|
|
27
|
+
.hint { color: var(--dim); font-size: .95rem; }
|
|
28
|
+
.primary { background: var(--accent); color: #fff; border: none; border-radius: 9px;
|
|
29
|
+
padding: .6rem 1.1rem; font: inherit; cursor: pointer; }
|
|
30
|
+
.primary[disabled] { opacity: .5; cursor: not-allowed; }
|
|
31
|
+
[hidden] { display: none !important; }
|
|
32
|
+
code { background: rgba(120,120,120,.12); padding: .05em .35em; border-radius: 4px; }
|
|
33
|
+
pre { background: rgba(120,120,120,.12); padding: .7rem .9rem; border-radius: 8px; overflow-x: auto; }
|
|
34
|
+
ol.steps li { margin: .5rem 0; }
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<h1>FediPod - join the fediverse from a Solid pod.</h1>
|
|
39
|
+
<p class="lede">Welcome to the <strong>FediPod.net gateway</strong>. Here, you can get a free pod and fediverse account or link to your existing pod/account. This gateway is a mail filter for accounts that stay yours. Your name, key and data live on your own pod; this box only receives your fediverse mail, verifies each delivery, drops spam and passes the rest to your pod.</p>
|
|
40
|
+
|
|
41
|
+
<p>
|
|
42
|
+
FediPod is experimental and free to use. See the <a href="https://github.com/jeff-zucker/FediPod">FediPod repository</a> for details.
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<p class="hint" id="current-version" hidden>Current FediPod: <strong id="current-version-num"></strong>
|
|
46
|
+
— update an existing install with <code id="current-version-cmd"></code></p>
|
|
47
|
+
|
|
48
|
+
<fieldset>
|
|
49
|
+
<legend>What do you want to do?</legend>
|
|
50
|
+
<div class="choices" role="group" aria-label="Path">
|
|
51
|
+
<button type="button" id="pick-solo" aria-pressed="false"><b>Create a new solo account</b><span>a fediverse account for one person, on a pod you own</span></button>
|
|
52
|
+
<button type="button" id="pick-group" aria-pressed="false"><b>Create a new group account</b><span>an account people join — it carries members' posts to the whole group</span></button>
|
|
53
|
+
<button type="button" id="pick-manage" aria-pressed="false"><b>Manage an existing account</b><span>attach it to this gateway, detach it, or transfer in</span></button>
|
|
54
|
+
</div>
|
|
55
|
+
</fieldset>
|
|
56
|
+
|
|
57
|
+
<!-- create: the signup itself — prove the pod, take the name, then install -->
|
|
58
|
+
<form id="create-form" hidden>
|
|
59
|
+
<h2 id="create-title">Create a new solo account</h2>
|
|
60
|
+
<p class="hint" id="create-group-hint" hidden>A group is an account people join: members post to it, and it
|
|
61
|
+
carries each member's post to everyone in the group. Mastodon users can join with no pod of their own.
|
|
62
|
+
A group needs a pod of its own.</p>
|
|
63
|
+
<p class="hint">Your account lives on a pod you own; this gateway filters its mail.</p>
|
|
64
|
+
|
|
65
|
+
<label for="create-pod">Your pod address</label>
|
|
66
|
+
<input type="url" id="create-pod" autocomplete="off" placeholder="https://you.solidcommunity.net/">
|
|
67
|
+
<p class="hint">No pod yet? Any Solid pod host that offers <em>https subdomains</em> works, or a Community
|
|
68
|
+
Solid Server you run yourself. Get the pod, then come back here.</p>
|
|
69
|
+
<p id="create-pods" class="hint" hidden>This host also offers pods — you can take one instead of bringing
|
|
70
|
+
your own; it stays exportable, so you can leave with everything.</p>
|
|
71
|
+
|
|
72
|
+
<label for="create-issuer">Where you log in (your identity provider)</label>
|
|
73
|
+
<input type="url" id="create-issuer" autocomplete="off" placeholder="https://solidcommunity.net">
|
|
74
|
+
|
|
75
|
+
<label for="create-handle">Your handle (the name in your address; permanent)</label>
|
|
76
|
+
<input type="text" id="create-handle" autocomplete="off" autocapitalize="off" spellcheck="false"
|
|
77
|
+
inputmode="latin" placeholder="you" aria-describedby="create-handle-msg">
|
|
78
|
+
<p class="msg" id="create-handle-msg" aria-live="polite"></p>
|
|
79
|
+
|
|
80
|
+
<label>Your fediverse address</label>
|
|
81
|
+
<div class="choices" role="group" aria-label="Address">
|
|
82
|
+
<button type="button" id="addr-pod" aria-pressed="false"><b id="addr-pod-name">@you@your.pod</b><span>your handle is tied to your pod — you can drop the gateway and keep everything</span></button>
|
|
83
|
+
<button type="button" id="addr-front" aria-pressed="false"><b id="addr-front-name">@you@this.host</b><span>your handle is tied to the gateway — you can move pods and keep your handle</span></button>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<p style="margin-top:1.1rem">
|
|
87
|
+
<button type="button" id="create-continue" class="primary" disabled>Sign in with your pod & create</button>
|
|
88
|
+
</p>
|
|
89
|
+
<p class="hint">Signing in proves the pod is yours — no password ever reaches this gateway.</p>
|
|
90
|
+
<p class="hint" id="create-note" hidden></p>
|
|
91
|
+
</form>
|
|
92
|
+
|
|
93
|
+
<!-- what a finished signup hands over: the one install command -->
|
|
94
|
+
<section id="create-done" hidden></section>
|
|
95
|
+
|
|
96
|
+
<!-- manage: the existing-account actions, one panel each -->
|
|
97
|
+
<section id="manage-form" hidden>
|
|
98
|
+
<div class="choices" role="group" aria-label="Manage">
|
|
99
|
+
<button type="button" id="pick-attach" aria-pressed="false"><b>Attach to this gateway</b><span>filter your mail through this door — your address stays yours</span></button>
|
|
100
|
+
<button type="button" id="pick-detach" aria-pressed="false"><b>Detach from this gateway</b><span>go back to unfiltered delivery</span></button>
|
|
101
|
+
<button type="button" id="pick-transfer" aria-pressed="false"><b>Transfer a fediverse account to FediPod</b><span>move here from Mastodon and friends</span></button>
|
|
102
|
+
</div>
|
|
103
|
+
</section>
|
|
104
|
+
|
|
105
|
+
<!-- attach: an existing FediPod points its inbox here; identity stays put -->
|
|
106
|
+
<form id="attach-form" hidden>
|
|
107
|
+
<h2>Attach an existing FediPod account</h2>
|
|
108
|
+
<p class="hint">Your account stays <code>@you@your.pod</code> — other servers keep finding you at your own
|
|
109
|
+
address. Attaching only changes where your <em>mail</em> arrives: your actor advertises this gateway's door
|
|
110
|
+
as its inbox, and everything that passes verification lands in your pod as before.</p>
|
|
111
|
+
|
|
112
|
+
<label for="pod-url">Your pod address</label>
|
|
113
|
+
<input type="url" id="pod-url" autocomplete="off" placeholder="https://you.solidcommunity.net/">
|
|
114
|
+
<p class="hint">If your FediPod uses a custom root, give the full home URL (ending in <code>/</code>);
|
|
115
|
+
a bare pod address assumes the default <code>activitypods-js/</code>.</p>
|
|
116
|
+
|
|
117
|
+
<label for="issuer">Where you log in (your identity provider)</label>
|
|
118
|
+
<input type="url" id="issuer" autocomplete="off" placeholder="https://solidcommunity.net">
|
|
119
|
+
|
|
120
|
+
<label for="slug">Your door's name here</label>
|
|
121
|
+
<input type="text" id="slug" autocomplete="off" autocapitalize="off" spellcheck="false"
|
|
122
|
+
inputmode="latin" placeholder="you" aria-describedby="slug-msg">
|
|
123
|
+
<p class="msg" id="slug-msg" aria-live="polite"></p>
|
|
124
|
+
<p class="hint">Only the door is named here — it is a routing address for servers, not your handle.</p>
|
|
125
|
+
|
|
126
|
+
<p style="margin-top:1.1rem">
|
|
127
|
+
<button type="button" id="attach-continue" class="primary" disabled>Sign in with your pod & attach</button>
|
|
128
|
+
</p>
|
|
129
|
+
<p class="hint">Signing in proves the pod is yours — no password ever reaches this gateway.</p>
|
|
130
|
+
<p class="hint" id="attach-note" hidden></p>
|
|
131
|
+
</form>
|
|
132
|
+
|
|
133
|
+
<!-- detach: done on the user's own agent; the gateway cannot reach in -->
|
|
134
|
+
<section id="detach-form" hidden>
|
|
135
|
+
<h2>Detach an existing FediPod account</h2>
|
|
136
|
+
<p class="hint">Detaching is yours to do, not this gateway's — your agent republishes your actor with your
|
|
137
|
+
pod's own inbox, and deliveries go straight there again. On your machine:</p>
|
|
138
|
+
<pre>node bin/fedipod.mjs gateway --detach</pre>
|
|
139
|
+
<p class="hint">Your name and data never moved, so nothing else changes. The door you had here goes quiet
|
|
140
|
+
and its name can be reused.</p>
|
|
141
|
+
</section>
|
|
142
|
+
|
|
143
|
+
<!-- transfer: the standard fediverse move, aimed at a FediPod -->
|
|
144
|
+
<section id="transfer-form" hidden>
|
|
145
|
+
<h2>Transfer a fediverse account to FediPod</h2>
|
|
146
|
+
<p class="hint">The fediverse's standard migration works toward a FediPod: your followers move over
|
|
147
|
+
automatically, and your old profile redirects. Posts stay on the old server — no fediverse migration
|
|
148
|
+
carries them.</p>
|
|
149
|
+
<ol class="steps">
|
|
150
|
+
<li><strong>Have a FediPod</strong> — create one first (the first option above).</li>
|
|
151
|
+
<li>On your agent's admin page, set <strong>new followers</strong> to <em>accepted automatically</em>,
|
|
152
|
+
so the wave of arriving followers doesn't queue.</li>
|
|
153
|
+
<li>Tell your FediPod about the old account:
|
|
154
|
+
<pre>node bin/fedipod.mjs alias --add @you@old.server</pre></li>
|
|
155
|
+
<li>On the old server: Preferences → Account → <em>Move to a different account</em>, naming your
|
|
156
|
+
FediPod address.</li>
|
|
157
|
+
<li>Bring the rest over from the old server's CSV export:
|
|
158
|
+
<pre>node bin/fedipod.mjs import following_accounts.csv blocked_accounts.csv muted_accounts.csv lists.csv</pre></li>
|
|
159
|
+
</ol>
|
|
160
|
+
</section>
|
|
161
|
+
|
|
162
|
+
<script src="/solid-client-authn.bundle.js"></script>
|
|
163
|
+
<script>
|
|
164
|
+
const $ = (id) => document.getElementById(id);
|
|
165
|
+
let offersPods = false, slugOk = false;
|
|
166
|
+
|
|
167
|
+
fetch('/api/handle?handle=__probe__').then(r => r.json()).then(d => {
|
|
168
|
+
offersPods = !!d.offersPods;
|
|
169
|
+
$('create-pods').hidden = !offersPods;
|
|
170
|
+
if (d.version) {
|
|
171
|
+
$('current-version-num').textContent = d.version;
|
|
172
|
+
$('current-version-cmd').textContent = `curl -fsSL ${location.origin}/install | sh`;
|
|
173
|
+
$('current-version').hidden = false;
|
|
174
|
+
}
|
|
175
|
+
}).catch(() => {});
|
|
176
|
+
|
|
177
|
+
const picks = ['pick-solo', 'pick-group', 'pick-manage'];
|
|
178
|
+
const panels = { 'pick-solo': 'create-form', 'pick-group': 'create-form',
|
|
179
|
+
'pick-manage': 'manage-form' };
|
|
180
|
+
const subPicks = ['pick-attach', 'pick-detach', 'pick-transfer'];
|
|
181
|
+
const subPanels = { 'pick-attach': 'attach-form', 'pick-detach': 'detach-form',
|
|
182
|
+
'pick-transfer': 'transfer-form' };
|
|
183
|
+
let createKind = 'person';
|
|
184
|
+
function subPick(which) {
|
|
185
|
+
for (const id of subPicks) {
|
|
186
|
+
$(id).setAttribute('aria-pressed', String(id === which));
|
|
187
|
+
$(subPanels[id]).hidden = id !== which;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function pick(which) {
|
|
191
|
+
for (const id of picks) $(id).setAttribute('aria-pressed', String(id === which));
|
|
192
|
+
for (const panel of new Set(Object.values(panels))) $(panel).hidden = panels[which] !== panel;
|
|
193
|
+
$('create-done').hidden = true;
|
|
194
|
+
if (which !== 'pick-manage') subPick(null);
|
|
195
|
+
if (panels[which] === 'create-form') {
|
|
196
|
+
createKind = which === 'pick-group' ? 'group' : 'person';
|
|
197
|
+
$('create-title').textContent = createKind === 'group'
|
|
198
|
+
? 'Create a new group account' : 'Create a new solo account';
|
|
199
|
+
$('create-group-hint').hidden = createKind !== 'group';
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const id of picks) $(id).onclick = () => pick(id);
|
|
203
|
+
for (const id of subPicks) $(id).onclick = () => subPick(id);
|
|
204
|
+
|
|
205
|
+
// Suggest the door name from the pod's own first label, and the identity
|
|
206
|
+
// provider from the pod's apex domain — both still editable.
|
|
207
|
+
$('pod-url').addEventListener('input', () => {
|
|
208
|
+
try {
|
|
209
|
+
const u = new URL($('pod-url').value.trim());
|
|
210
|
+
const label = u.hostname.split('.')[0];
|
|
211
|
+
if (!$('slug').value && /^[a-z0-9-]{1,63}$/.test(label)) {
|
|
212
|
+
$('slug').value = label;
|
|
213
|
+
$('slug').dispatchEvent(new Event('input'));
|
|
214
|
+
}
|
|
215
|
+
if (!$('issuer').value) {
|
|
216
|
+
const parts = u.hostname.split('.');
|
|
217
|
+
$('issuer').value = 'https://' + (parts.length > 2 ? parts.slice(1).join('.') : u.hostname);
|
|
218
|
+
}
|
|
219
|
+
} catch { /* not a URL yet */ }
|
|
220
|
+
refresh();
|
|
221
|
+
});
|
|
222
|
+
$('issuer').addEventListener('input', refresh);
|
|
223
|
+
|
|
224
|
+
let t = null;
|
|
225
|
+
$('slug').addEventListener('input', () => {
|
|
226
|
+
slugOk = false; $('slug-msg').textContent = ''; $('slug-msg').className = 'msg';
|
|
227
|
+
clearTimeout(t);
|
|
228
|
+
const h = $('slug').value.trim().toLowerCase();
|
|
229
|
+
if (!h) { refresh(); return; }
|
|
230
|
+
t = setTimeout(async () => {
|
|
231
|
+
try {
|
|
232
|
+
const d = await (await fetch('/api/handle?handle=' + encodeURIComponent(h))).json();
|
|
233
|
+
slugOk = d.available;
|
|
234
|
+
$('slug-msg').textContent = d.available ? 'this door is free' : (d.reason || 'taken');
|
|
235
|
+
$('slug-msg').className = 'msg ' + (d.available ? 'ok' : 'no');
|
|
236
|
+
} catch { $('slug-msg').textContent = 'could not check'; $('slug-msg').className = 'msg no'; }
|
|
237
|
+
refresh();
|
|
238
|
+
}, 250);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
function refresh() {
|
|
242
|
+
const podOk = /^https:\/\/\S+/.test($('pod-url').value.trim());
|
|
243
|
+
const issuerOk = /^https:\/\/\S+/.test($('issuer').value.trim());
|
|
244
|
+
$('attach-continue').disabled = !(podOk && slugOk && issuerOk);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// The user's pod home: their agent tree on the pod. A bare pod root gets
|
|
248
|
+
// the default root; anything with a path is taken as given.
|
|
249
|
+
function podHomeOf(raw) {
|
|
250
|
+
const u = new URL(raw.trim());
|
|
251
|
+
if (u.pathname === '/') u.pathname = '/activitypods-js/';
|
|
252
|
+
else if (!u.pathname.endsWith('/')) u.pathname += '/';
|
|
253
|
+
u.search = ''; u.hash = '';
|
|
254
|
+
return u.href;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
$('attach-continue').onclick = () => {
|
|
258
|
+
const auth = window.solidClientAuthentication;
|
|
259
|
+
const n = $('attach-note');
|
|
260
|
+
if (!auth) { n.hidden = false; n.textContent = 'the sign-in library did not load — reload and try again'; return; }
|
|
261
|
+
sessionStorage.setItem('fp-attach', JSON.stringify({
|
|
262
|
+
slug: $('slug').value.trim().toLowerCase(),
|
|
263
|
+
podHome: podHomeOf($('pod-url').value),
|
|
264
|
+
kind: 'person',
|
|
265
|
+
}));
|
|
266
|
+
auth.login({
|
|
267
|
+
oidcIssuer: $('issuer').value.trim(),
|
|
268
|
+
redirectUrl: location.origin + '/',
|
|
269
|
+
clientName: 'FediPod gateway',
|
|
270
|
+
}).catch((e) => { n.hidden = false; n.textContent = 'sign-in failed to start: ' + e.message; });
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// ---- the create (signup) form ----
|
|
274
|
+
let createSlugOk = false, addrShape = null;
|
|
275
|
+
const esc = (s) => String(s).replace(/[&<>"']/g, (c) =>
|
|
276
|
+
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
277
|
+
|
|
278
|
+
// The two address previews follow the handle and pod as they are typed.
|
|
279
|
+
function addrPreview() {
|
|
280
|
+
const h = $('create-handle').value.trim().toLowerCase() || 'you';
|
|
281
|
+
let podHost = 'your.pod';
|
|
282
|
+
try { podHost = new URL($('create-pod').value.trim()).hostname; } catch { /* not a URL yet */ }
|
|
283
|
+
$('addr-pod-name').textContent = `@${h}@${podHost}`;
|
|
284
|
+
$('addr-front-name').textContent = `@${h}@${location.host}`;
|
|
285
|
+
}
|
|
286
|
+
function createRefresh() {
|
|
287
|
+
const podOk = /^https:\/\/\S+/.test($('create-pod').value.trim());
|
|
288
|
+
const issuerOk = /^https:\/\/\S+/.test($('create-issuer').value.trim());
|
|
289
|
+
$('create-continue').disabled = !(podOk && issuerOk && createSlugOk && addrShape);
|
|
290
|
+
}
|
|
291
|
+
$('addr-pod').onclick = () => { addrShape = 'pod';
|
|
292
|
+
$('addr-pod').setAttribute('aria-pressed', 'true');
|
|
293
|
+
$('addr-front').setAttribute('aria-pressed', 'false'); createRefresh(); };
|
|
294
|
+
$('addr-front').onclick = () => { addrShape = 'front';
|
|
295
|
+
$('addr-front').setAttribute('aria-pressed', 'true');
|
|
296
|
+
$('addr-pod').setAttribute('aria-pressed', 'false'); createRefresh(); };
|
|
297
|
+
|
|
298
|
+
$('create-pod').addEventListener('input', () => {
|
|
299
|
+
try {
|
|
300
|
+
const u = new URL($('create-pod').value.trim());
|
|
301
|
+
const label = u.hostname.split('.')[0];
|
|
302
|
+
if (!$('create-handle').value && /^[a-z0-9-]{1,63}$/.test(label)) {
|
|
303
|
+
$('create-handle').value = label;
|
|
304
|
+
$('create-handle').dispatchEvent(new Event('input'));
|
|
305
|
+
}
|
|
306
|
+
if (!$('create-issuer').value) {
|
|
307
|
+
const parts = u.hostname.split('.');
|
|
308
|
+
$('create-issuer').value = 'https://' + (parts.length > 2 ? parts.slice(1).join('.') : u.hostname);
|
|
309
|
+
}
|
|
310
|
+
} catch { /* not a URL yet */ }
|
|
311
|
+
addrPreview(); createRefresh();
|
|
312
|
+
});
|
|
313
|
+
$('create-issuer').addEventListener('input', createRefresh);
|
|
314
|
+
|
|
315
|
+
let ct = null;
|
|
316
|
+
$('create-handle').addEventListener('input', () => {
|
|
317
|
+
createSlugOk = false; $('create-handle-msg').textContent = ''; $('create-handle-msg').className = 'msg';
|
|
318
|
+
addrPreview();
|
|
319
|
+
clearTimeout(ct);
|
|
320
|
+
const h = $('create-handle').value.trim().toLowerCase();
|
|
321
|
+
if (!h) { createRefresh(); return; }
|
|
322
|
+
ct = setTimeout(async () => {
|
|
323
|
+
try {
|
|
324
|
+
const d = await (await fetch('/api/handle?handle=' + encodeURIComponent(h))).json();
|
|
325
|
+
createSlugOk = d.available;
|
|
326
|
+
$('create-handle-msg').textContent = d.available ? 'this name is free' : (d.reason || 'taken');
|
|
327
|
+
$('create-handle-msg').className = 'msg ' + (d.available ? 'ok' : 'no');
|
|
328
|
+
} catch { $('create-handle-msg').textContent = 'could not check'; $('create-handle-msg').className = 'msg no'; }
|
|
329
|
+
createRefresh();
|
|
330
|
+
}, 250);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
$('create-continue').onclick = () => {
|
|
334
|
+
const auth = window.solidClientAuthentication;
|
|
335
|
+
const n = $('create-note');
|
|
336
|
+
if (!auth) { n.hidden = false; n.textContent = 'the sign-in library did not load — reload and try again'; return; }
|
|
337
|
+
sessionStorage.setItem('fp-create', JSON.stringify({
|
|
338
|
+
slug: $('create-handle').value.trim().toLowerCase(),
|
|
339
|
+
podHome: podHomeOf($('create-pod').value),
|
|
340
|
+
issuer: $('create-issuer').value.trim(),
|
|
341
|
+
kind: createKind,
|
|
342
|
+
fronted: addrShape === 'front',
|
|
343
|
+
}));
|
|
344
|
+
auth.login({
|
|
345
|
+
oidcIssuer: $('create-issuer').value.trim(),
|
|
346
|
+
redirectUrl: location.origin + '/',
|
|
347
|
+
clientName: 'FediPod gateway',
|
|
348
|
+
}).catch((e) => { n.hidden = false; n.textContent = 'sign-in failed to start: ' + e.message; });
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// Back from the identity provider: finish the signup with the proven login.
|
|
352
|
+
(async () => {
|
|
353
|
+
const auth = window.solidClientAuthentication;
|
|
354
|
+
if (!auth) return;
|
|
355
|
+
const info = await auth.handleIncomingRedirect().catch(() => null);
|
|
356
|
+
const pending = sessionStorage.getItem('fp-create');
|
|
357
|
+
if (!info?.isLoggedIn || !pending) return;
|
|
358
|
+
sessionStorage.removeItem('fp-create');
|
|
359
|
+
const p = JSON.parse(pending);
|
|
360
|
+
pick(p.kind === 'group' ? 'pick-group' : 'pick-solo');
|
|
361
|
+
const n = $('create-note');
|
|
362
|
+
n.hidden = false;
|
|
363
|
+
n.textContent = 'signed in as ' + info.webId + ' — creating…';
|
|
364
|
+
const res = await auth.fetch('/api/attach', {
|
|
365
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
366
|
+
body: JSON.stringify({ handle: p.slug, podHome: p.podHome, kind: p.kind, fronted: p.fronted }),
|
|
367
|
+
}).catch(() => null);
|
|
368
|
+
const d = res ? await res.json().catch(() => ({})) : {};
|
|
369
|
+
if (res && res.status === 201) {
|
|
370
|
+
const address = p.fronted ? d.address : `@${p.slug}@${new URL(p.podHome).hostname}`;
|
|
371
|
+
const flags = [
|
|
372
|
+
`--gateway ${d.frontActor || d.doorInbox}`,
|
|
373
|
+
`--secret ${d.hmacSecret}`,
|
|
374
|
+
`--pod ${p.podHome}`,
|
|
375
|
+
`--issuer ${p.issuer}`,
|
|
376
|
+
`--handle ${p.slug}`,
|
|
377
|
+
`--kind ${p.kind}`,
|
|
378
|
+
...(p.fronted ? ['--fronted'] : []),
|
|
379
|
+
];
|
|
380
|
+
const cmd = `curl -fsSL ${location.origin}/install | sh -s -- \\\n ` + flags.join(' \\\n ');
|
|
381
|
+
$('create-form').hidden = true;
|
|
382
|
+
const done = $('create-done');
|
|
383
|
+
done.hidden = false;
|
|
384
|
+
done.innerHTML = '<h2>' + esc(address) + '</h2>'
|
|
385
|
+
+ '<p>Created. Now install your agent on the machine it will live on — one command, then start it:</p>'
|
|
386
|
+
+ '<pre>' + esc(cmd) + '\n\nfedipod start</pre>'
|
|
387
|
+
+ '<p>Setup opens in your browser with everything already filled in. When it finishes, your '
|
|
388
|
+
+ 'account is live and your mail arrives through this gateway, filtered.</p>';
|
|
389
|
+
} else {
|
|
390
|
+
n.textContent = 'signup failed: ' + (d.error || (res ? 'HTTP ' + res.status : 'no response'));
|
|
391
|
+
}
|
|
392
|
+
})();
|
|
393
|
+
|
|
394
|
+
// Back from the identity provider: finish the attach with the proven login.
|
|
395
|
+
(async () => {
|
|
396
|
+
const auth = window.solidClientAuthentication;
|
|
397
|
+
if (!auth) return;
|
|
398
|
+
const info = await auth.handleIncomingRedirect().catch(() => null);
|
|
399
|
+
const pending = sessionStorage.getItem('fp-attach');
|
|
400
|
+
if (!info?.isLoggedIn || !pending) return;
|
|
401
|
+
sessionStorage.removeItem('fp-attach');
|
|
402
|
+
const p = JSON.parse(pending);
|
|
403
|
+
pick('pick-manage');
|
|
404
|
+
subPick('pick-attach');
|
|
405
|
+
const n = $('attach-note');
|
|
406
|
+
n.hidden = false;
|
|
407
|
+
n.textContent = 'signed in as ' + info.webId + ' — attaching…';
|
|
408
|
+
const res = await auth.fetch('/api/attach', {
|
|
409
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
410
|
+
body: JSON.stringify({ handle: p.slug, podHome: p.podHome, kind: p.kind }),
|
|
411
|
+
}).catch(() => null);
|
|
412
|
+
const d = res ? await res.json().catch(() => ({})) : {};
|
|
413
|
+
if (res && res.status === 201) {
|
|
414
|
+
n.innerHTML = 'Attached. Run this on the machine your agent lives on, then restart the agent:'
|
|
415
|
+
+ '<pre>' + d.command + '</pre>'
|
|
416
|
+
+ 'Your mail then arrives through <code>' + d.doorInbox + '</code>, filtered. Your name has not moved.';
|
|
417
|
+
} else {
|
|
418
|
+
n.textContent = 'attach failed: ' + (d.error || (res ? 'HTTP ' + res.status : 'no response'));
|
|
419
|
+
}
|
|
420
|
+
})();
|
|
421
|
+
</script>
|
|
422
|
+
<i>(cc) 4.0 By, Jeff Zucker, 2026</i>
|
|
423
|
+
</body>
|
|
424
|
+
</html>
|
|
@@ -0,0 +1,138 @@
|
|
|
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>FediPod — run your identity on this server</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: light dark; --line:#d9d4c7; --dim:#45443d; --accent:#6a8a5a; }
|
|
9
|
+
@media (prefers-color-scheme: dark) { :root { --dim:#c2c0b4; } }
|
|
10
|
+
body { font: 19px/1.6 system-ui, sans-serif; max-width: 44rem; margin: 2rem auto; padding: 0 1.1rem; }
|
|
11
|
+
h1 { font-size: 1.7rem; margin: 0 0 .2rem; }
|
|
12
|
+
p.lede { color: var(--dim); margin-top: 0; }
|
|
13
|
+
label { display: block; font-weight: 600; margin: .8rem 0 .2rem; }
|
|
14
|
+
input[type=text], input[type=url] { width: 100%; padding: .5rem .6rem; font: inherit;
|
|
15
|
+
border: 1px solid var(--line); border-radius: 8px; box-sizing: border-box; }
|
|
16
|
+
.hint { color: var(--dim); font-size: .95rem; }
|
|
17
|
+
.primary { background: var(--accent); color: #fff; border: none; border-radius: 9px;
|
|
18
|
+
padding: .6rem 1.1rem; font: inherit; cursor: pointer; }
|
|
19
|
+
button { font: inherit; }
|
|
20
|
+
.primary[disabled], button[disabled] { opacity: .5; cursor: not-allowed; }
|
|
21
|
+
[hidden] { display: none !important; }
|
|
22
|
+
code { background: rgba(120,120,120,.12); padding: .05em .35em; border-radius: 4px; }
|
|
23
|
+
pre { background: rgba(120,120,120,.12); padding: .7rem .9rem; border-radius: 8px; overflow-x: auto; }
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
<h1>Run your identity on this server</h1>
|
|
28
|
+
<p class="lede">If your pod is hosted by this server, the server can run your fediverse identity
|
|
29
|
+
for you: it accepts follows, delivers your posts and serves your Mastodon app, with nothing for
|
|
30
|
+
you to keep running. Sign in with your pod to prove it is yours.</p>
|
|
31
|
+
|
|
32
|
+
<form id="run-form">
|
|
33
|
+
<p class="field">
|
|
34
|
+
<label for="run-pod-url">Your pod's address on this server</label>
|
|
35
|
+
<input type="url" id="run-pod-url" autocomplete="off" placeholder="https://you.this.server/">
|
|
36
|
+
</p>
|
|
37
|
+
<p class="field">
|
|
38
|
+
<label for="run-issuer">Your identity provider</label>
|
|
39
|
+
<input type="url" id="run-issuer" autocomplete="off" placeholder="https://this.server">
|
|
40
|
+
</p>
|
|
41
|
+
<p class="actions">
|
|
42
|
+
<button type="button" id="run-continue" class="primary" disabled>Sign in with your pod & opt in</button>
|
|
43
|
+
<button type="button" id="run-leave">Stop running it here</button>
|
|
44
|
+
</p>
|
|
45
|
+
<p class="hint" id="run-note" hidden></p>
|
|
46
|
+
</form>
|
|
47
|
+
|
|
48
|
+
<script src="/solid-client-authn.bundle.js"></script>
|
|
49
|
+
<script>
|
|
50
|
+
const $ = (id) => document.getElementById(id);
|
|
51
|
+
|
|
52
|
+
// Offered only where this server actually runs identities. An
|
|
53
|
+
// unauthenticated probe tells the two apart: 501 = not offered here,
|
|
54
|
+
// anything else = offered (a real opt-in still needs the signed-in proof).
|
|
55
|
+
let offered = true;
|
|
56
|
+
(async () => {
|
|
57
|
+
const res = await fetch('/api/agent', { method: 'POST',
|
|
58
|
+
headers: { 'content-type': 'application/json' }, body: '{}' }).catch(() => null);
|
|
59
|
+
if (!res || res.status === 501) {
|
|
60
|
+
offered = false;
|
|
61
|
+
const n = $('run-note');
|
|
62
|
+
n.hidden = false;
|
|
63
|
+
n.textContent = 'This server does not run identities. Your agent stays where it is.';
|
|
64
|
+
runFormCheck();
|
|
65
|
+
}
|
|
66
|
+
})();
|
|
67
|
+
|
|
68
|
+
function runFormCheck() {
|
|
69
|
+
let ok = false;
|
|
70
|
+
try { ok = /^https?:$/.test(new URL($('run-pod-url').value.trim()).protocol); } catch { /* not yet */ }
|
|
71
|
+
const issuerOk = /^https?:\/\/\S+/.test($('run-issuer').value.trim());
|
|
72
|
+
$('run-continue').disabled = !(offered && ok && issuerOk);
|
|
73
|
+
$('run-leave').disabled = !(offered && ok && issuerOk);
|
|
74
|
+
}
|
|
75
|
+
$('run-pod-url').addEventListener('input', () => {
|
|
76
|
+
runFormCheck();
|
|
77
|
+
// The identity provider is usually the server itself — still editable.
|
|
78
|
+
try {
|
|
79
|
+
const u = new URL($('run-pod-url').value.trim());
|
|
80
|
+
const apex = u.hostname.split('.').slice(1).join('.');
|
|
81
|
+
if (!$('run-issuer').value && apex) $('run-issuer').value = `${u.protocol}//${apex}${u.port ? ':' + u.port : ''}`;
|
|
82
|
+
} catch { /* not a URL yet */ }
|
|
83
|
+
runFormCheck();
|
|
84
|
+
});
|
|
85
|
+
$('run-issuer').addEventListener('input', runFormCheck);
|
|
86
|
+
|
|
87
|
+
const runStart = (action) => {
|
|
88
|
+
const auth = window.solidClientAuthentication;
|
|
89
|
+
const n = $('run-note');
|
|
90
|
+
if (!auth) { n.hidden = false; n.textContent = 'the sign-in library did not load — reload and try again'; return; }
|
|
91
|
+
const u = new URL($('run-pod-url').value.trim());
|
|
92
|
+
if (u.pathname === '') u.pathname = '/';
|
|
93
|
+
if (!u.pathname.endsWith('/')) u.pathname += '/';
|
|
94
|
+
u.search = ''; u.hash = '';
|
|
95
|
+
sessionStorage.setItem('fp-run', JSON.stringify({ podBase: u.href, action }));
|
|
96
|
+
auth.login({
|
|
97
|
+
oidcIssuer: $('run-issuer').value.trim(),
|
|
98
|
+
redirectUrl: location.origin + '/run',
|
|
99
|
+
clientName: 'FediPod gateway',
|
|
100
|
+
}).catch((e) => { n.hidden = false; n.textContent = 'sign-in failed to start: ' + e.message; });
|
|
101
|
+
};
|
|
102
|
+
$('run-continue').onclick = () => runStart('opt-in');
|
|
103
|
+
$('run-leave').onclick = () => runStart('opt-out');
|
|
104
|
+
|
|
105
|
+
// Back from the identity provider: finish the opt-in with the proven login.
|
|
106
|
+
(async () => {
|
|
107
|
+
const auth = window.solidClientAuthentication;
|
|
108
|
+
if (!auth) return;
|
|
109
|
+
const info = await auth.handleIncomingRedirect().catch(() => null);
|
|
110
|
+
const pending = sessionStorage.getItem('fp-run');
|
|
111
|
+
if (!info?.isLoggedIn || !pending) return;
|
|
112
|
+
sessionStorage.removeItem('fp-run');
|
|
113
|
+
const p = JSON.parse(pending);
|
|
114
|
+
const n = $('run-note');
|
|
115
|
+
n.hidden = false;
|
|
116
|
+
n.textContent = 'signed in as ' + info.webId + ' — asking the server…';
|
|
117
|
+
const res = await auth.fetch('/api/agent', {
|
|
118
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
119
|
+
body: JSON.stringify({ action: p.action, podBase: p.podBase }),
|
|
120
|
+
}).catch(() => null);
|
|
121
|
+
const d = res ? await res.json().catch(() => ({})) : {};
|
|
122
|
+
if (res && res.status === 201 && d.doorSecret) {
|
|
123
|
+
n.innerHTML = 'Your identity runs here now. This is your door secret — <b>save it now</b>, '
|
|
124
|
+
+ 'it is shown only this once. It opens your admin pages at <code>' + p.podBase.replace(/\/$/, '') + d.doorPath + '</code>:'
|
|
125
|
+
+ '<pre>' + d.doorSecret + '</pre>'
|
|
126
|
+
+ 'To check it works:<pre>' + d.command + '</pre>'
|
|
127
|
+
+ 'Lost it? Opt in again — that mints a fresh one and retires this one.';
|
|
128
|
+
} else if (res && res.status === 200) {
|
|
129
|
+
n.textContent = 'Done — this server no longer runs that identity. Your pod and its data are untouched.';
|
|
130
|
+
} else {
|
|
131
|
+
n.textContent = (p.action === 'opt-in' ? 'opt-in' : 'opt-out') + ' failed: '
|
|
132
|
+
+ (d.error || (res ? 'HTTP ' + res.status : 'no response'));
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
135
|
+
</script>
|
|
136
|
+
<i>(cc) 4.0 By, Jeff Zucker, 2026</i>
|
|
137
|
+
</body>
|
|
138
|
+
</html>
|