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
package/lib/migrate.mjs
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Keeping every identity on this machine at the current shape of an install.
|
|
2
|
+
//
|
|
3
|
+
// bbba587 fixed the DEFAULT: a new install keeps its private half — timeline,
|
|
4
|
+
// contacts, blocklist, notifications, masto-tokens.json — on this machine.
|
|
5
|
+
// It did nothing for the installs that already existed, and `privateRoot`
|
|
6
|
+
// absent still means "on the pod" (run-agent.mjs privateUrls). So an identity
|
|
7
|
+
// set up before that commit still writes every state change to a pod, and
|
|
8
|
+
// still keeps its private data on a server.
|
|
9
|
+
//
|
|
10
|
+
// A new default only ever fixes the installs that do not exist yet. That is the
|
|
11
|
+
// reason this file exists rather than a one-off script: a change to install
|
|
12
|
+
// shape ships as a numbered step HERE, and `fedipod upgrade` runs the
|
|
13
|
+
// pending ones over every identity under the root, not just the one you
|
|
14
|
+
// happened to be in.
|
|
15
|
+
|
|
16
|
+
// Bump when a step is added, and add the step to pendingSteps below.
|
|
17
|
+
export const CURRENT_LAYOUT = 1;
|
|
18
|
+
|
|
19
|
+
// 0 is "never stamped", which is every install made before this file existed.
|
|
20
|
+
// It is not the same as "wrong": an old install may already be in the right
|
|
21
|
+
// shape, and pendingSteps is what decides, per step.
|
|
22
|
+
export const layoutOf = (cred) => Number(cred?.layout) || 0;
|
|
23
|
+
|
|
24
|
+
// Layout 1 — the private half lives on this machine.
|
|
25
|
+
//
|
|
26
|
+
// Only when `privateRoot` is ABSENT. An operator who pointed it somewhere
|
|
27
|
+
// deliberately, with `--private-root`, has said where they want it; a migration
|
|
28
|
+
// that overrode that would be moving data on its own initiative.
|
|
29
|
+
export function needsStateMove(cred) {
|
|
30
|
+
return !cred?.privateRoot;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function pendingSteps(cred) {
|
|
34
|
+
const steps = [];
|
|
35
|
+
if (needsStateMove(cred)) {
|
|
36
|
+
steps.push({
|
|
37
|
+
id: 'state-off-pod',
|
|
38
|
+
what: 'move the private half off the pod, onto this machine',
|
|
39
|
+
why: 'receiving a post costs pod writes, and the timeline lives on a server',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return steps;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// True when there is nothing left to do and the stamp can be written.
|
|
46
|
+
export const isCurrent = (cred) => pendingSteps(cred).length === 0;
|
|
47
|
+
|
|
48
|
+
// The documents on the pod that the state tree consists of — everything the
|
|
49
|
+
// move copied. Deliberately NOT "everything in the container":
|
|
50
|
+
//
|
|
51
|
+
// lease.json stays. It is the single-active-agent lock, and it is on the pod
|
|
52
|
+
// precisely BECAUSE the private half is not: a lease only one machine can
|
|
53
|
+
// reach coordinates nothing. run-agent builds it from urls.state, never from
|
|
54
|
+
// privateRoot, so it goes on being used after the move.
|
|
55
|
+
//
|
|
56
|
+
// .keep and .acl belong to the container, which also stays — the container is
|
|
57
|
+
// what carries the owner-only ACL that lease.json sits behind.
|
|
58
|
+
//
|
|
59
|
+
// RemotePod.delete's deny-list is the backstop under all of this; this list is
|
|
60
|
+
// what we affirmatively claim, and the two disagreeing is a bug worth hearing
|
|
61
|
+
// about rather than a file quietly surviving.
|
|
62
|
+
export const MOVED_STATE_DOCS = new Set([
|
|
63
|
+
'config.json', 'queue.json', 'blocklist.json', 'contacts.json', 'muted.json',
|
|
64
|
+
'pending.json', 'requests.json', 'deadletter.json', 'statuses.json',
|
|
65
|
+
'notifications.json', 'media.json', 'actors.json', 'ids.json', 'published.json',
|
|
66
|
+
'outbox.json', 'outbox-removed.json', 'parked.json', 'tagfeed.json',
|
|
67
|
+
'intake-attempts.json', 'inbox-channel.json', 'masto-tokens.json', 'masto-markers.json',
|
|
68
|
+
'lists.json', 'filters.json', 'scheduled.json', 'webpush.json',
|
|
69
|
+
'import-state.json',
|
|
70
|
+
// Present only in pod-keys mode; a state move must carry it or the moved
|
|
71
|
+
// store cannot sign.
|
|
72
|
+
'keys.json',
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
export const KEPT_ON_POD = new Set(['lease.json', '.keep']);
|
|
76
|
+
|
|
77
|
+
// What `state --drop-remote` would remove, given a container listing. Split out
|
|
78
|
+
// so the decision can be tested without a pod, and so the caller can show it
|
|
79
|
+
// before doing anything.
|
|
80
|
+
export function classifyRemoteState(urls, base) {
|
|
81
|
+
const drop = [];
|
|
82
|
+
const keep = [];
|
|
83
|
+
for (const u of urls) {
|
|
84
|
+
const name = String(u).slice(String(base).length).replace(/^\/+/, '');
|
|
85
|
+
if (!name || name.includes('/')) { keep.push({ url: u, name, why: 'not a document of this container' }); continue; }
|
|
86
|
+
if (KEPT_ON_POD.has(name)) {
|
|
87
|
+
keep.push({ url: u, name, why: name === 'lease.json' ? 'the lease stays on the pod by design' : 'the container needs it' });
|
|
88
|
+
} else if (MOVED_STATE_DOCS.has(name)) {
|
|
89
|
+
drop.push({ url: u, name });
|
|
90
|
+
} else {
|
|
91
|
+
keep.push({ url: u, name, why: 'not one of ours' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { drop, keep };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Copy the private half — every state document, then the RDF tree — from one
|
|
98
|
+
// pair of storages to the other. Copy and verify only: nothing is repointed and
|
|
99
|
+
// nothing is deleted, so a failure part-way leaves the source authoritative.
|
|
100
|
+
// Used by both `state --to` and the record page's Move private data.
|
|
101
|
+
export async function copyPrivateHalf({ from, to, log = () => {} }) {
|
|
102
|
+
const { PodStore } = await import('./store.mjs');
|
|
103
|
+
const { PodRdf } = await import('./podrdf.mjs');
|
|
104
|
+
const src = new PodStore({ log: () => {} });
|
|
105
|
+
src.attach(from.state);
|
|
106
|
+
await src.load();
|
|
107
|
+
const dst = new PodStore({ log });
|
|
108
|
+
dst.attach(to.state);
|
|
109
|
+
for (const [name, value] of src.cache) dst.write(name, value);
|
|
110
|
+
if (!await dst.commit()) {
|
|
111
|
+
throw new Error('some state documents did not land — nothing was repointed, nothing was deleted');
|
|
112
|
+
}
|
|
113
|
+
const rdfFrom = new PodRdf({ storage: from.fediverse });
|
|
114
|
+
const rdfTo = new PodRdf({ storage: to.fediverse });
|
|
115
|
+
let notes = 0;
|
|
116
|
+
for (const doc of ['settings', 'contacts']) {
|
|
117
|
+
try { await rdfTo.put(rdfTo.fedi + doc, await rdfFrom.get(rdfFrom.fedi + doc)); }
|
|
118
|
+
catch (e) { if (!/ 404$/.test(e.message)) throw e; }
|
|
119
|
+
}
|
|
120
|
+
for (const kind of ['posts', 'timeline']) {
|
|
121
|
+
for (const url of await rdfFrom.listNotes(kind)) {
|
|
122
|
+
await rdfTo.put(`${rdfTo.fedi}${kind}/${url.split('/').pop()}`, await rdfFrom.get(url));
|
|
123
|
+
notes++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { docs: src.cache.size, notes };
|
|
127
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// oidc-auth.mjs — who may drive the C2S surface. Two proofs are accepted:
|
|
2
|
+
// the Mastodon facade's bearer (minted on this machine, so its holder is the
|
|
3
|
+
// operator by construction), or a Solid-OIDC token — DPoP-bound when the
|
|
4
|
+
// client sends the proof header — verified by the same library the wider
|
|
5
|
+
// Solid world uses. The token alone is not enough: the WebID it names must
|
|
6
|
+
// be THIS identity's owner, an authorization step the token does not carry.
|
|
7
|
+
//
|
|
8
|
+
// The verifier is injected so offline tests stub it, and wrapped so the
|
|
9
|
+
// library (CJS, older jose) can be replaced without touching any caller.
|
|
10
|
+
|
|
11
|
+
export function makeC2sAuth({ agent, masto = null, verifier = null, log = () => {}, scheme = null }) {
|
|
12
|
+
let verify = verifier;
|
|
13
|
+
const loadVerifier = async () => {
|
|
14
|
+
if (!verify) {
|
|
15
|
+
const { createSolidTokenVerifier } = await import('@solid/access-token-verifier');
|
|
16
|
+
verify = createSolidTokenVerifier();
|
|
17
|
+
}
|
|
18
|
+
return verify;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return async function authenticate(req, pathname) {
|
|
22
|
+
if (masto?.authed(req)) {
|
|
23
|
+
return { ok: true, webid: agent.remote?.webId || null, via: 'bearer' };
|
|
24
|
+
}
|
|
25
|
+
if (!req.headers.authorization) {
|
|
26
|
+
return { ok: false, status: 401, error: 'authentication required: a Solid-OIDC token (DPoP) or this agent\'s own bearer' };
|
|
27
|
+
}
|
|
28
|
+
let webid;
|
|
29
|
+
try {
|
|
30
|
+
const v = await loadVerifier();
|
|
31
|
+
// The URL the client signed its proof over. The Host header already
|
|
32
|
+
// passed the Authorities firewall, so whichever alias the client used
|
|
33
|
+
// (localhost, 127.0.0.1, the named origin) is one this agent answers on;
|
|
34
|
+
// the scheme is whichever listener the request arrived on.
|
|
35
|
+
const htu = `${scheme ? scheme.replace(/:$/u, '') : req.socket?.encrypted ? 'https' : 'http'
|
|
36
|
+
}://${req.headers.host}${pathname}`;
|
|
37
|
+
({ webid } = await v(
|
|
38
|
+
req.headers.authorization,
|
|
39
|
+
req.headers.dpop ? { header: req.headers.dpop, method: req.method, url: htu } : undefined,
|
|
40
|
+
));
|
|
41
|
+
} catch (e) {
|
|
42
|
+
log(`c2s auth: token rejected — ${e.message}`);
|
|
43
|
+
return { ok: false, status: 401, error: `token rejected: ${e.message}` };
|
|
44
|
+
}
|
|
45
|
+
const owner = agent.remote?.webId;
|
|
46
|
+
if (!owner || webid !== owner) {
|
|
47
|
+
// Verified is not authorized: a valid token from ANY WebID must not
|
|
48
|
+
// post as this actor. (The gap jg10's outbox leaves open.)
|
|
49
|
+
return { ok: false, status: 403, error: 'authenticated, but this outbox belongs to its owner alone' };
|
|
50
|
+
}
|
|
51
|
+
return { ok: true, webid, via: 'oidc' };
|
|
52
|
+
};
|
|
53
|
+
}
|
package/lib/podrdf.mjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// podrdf.mjs — the RDF source of truth under /activitypods-js/fediverse/:
|
|
2
|
+
// settings, contacts, and one document per note. Same documents and AS2
|
|
3
|
+
// vocabulary as dk's local.mjs (plan: ap-pod-mapping.md).
|
|
4
|
+
//
|
|
5
|
+
// Parsed and serialised by rdflib, never by hand. Escaping, encoding and shape
|
|
6
|
+
// are exactly what a regex gets wrong quietly — a tab inside a literal, a '%'
|
|
7
|
+
// in a slug, a triple laid out in a form the pattern did not anticipate — and
|
|
8
|
+
// a hand-built serialiser writes the documents the hand-built parser reads
|
|
9
|
+
// back, so the two bugs cover for each other. See claude/plans/no-regex-rdf.md.
|
|
10
|
+
//
|
|
11
|
+
// The container it lives in is a Storage (lib/storage.mjs): a pod over HTTP or
|
|
12
|
+
// a directory. Callers still speak in absolute URLs, which is what the rest of
|
|
13
|
+
// the agent has.
|
|
14
|
+
|
|
15
|
+
import * as $rdf from 'rdflib';
|
|
16
|
+
|
|
17
|
+
const AS = $rdf.Namespace('https://www.w3.org/ns/activitystreams#');
|
|
18
|
+
const RDF = $rdf.Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#');
|
|
19
|
+
const XSD = $rdf.Namespace('http://www.w3.org/2001/XMLSchema#');
|
|
20
|
+
const TURTLE = 'text/turtle';
|
|
21
|
+
|
|
22
|
+
export class PodRdf {
|
|
23
|
+
constructor({ storage }) {
|
|
24
|
+
this.storage = storage;
|
|
25
|
+
this.base = storage.base;
|
|
26
|
+
this.fedi = this.base;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Callers hold absolute URLs; the storage works in paths under its base.
|
|
30
|
+
_rel(url) {
|
|
31
|
+
if (!url.startsWith(this.fedi)) throw new Error(`${url} is not under ${this.fedi}`);
|
|
32
|
+
return url.slice(this.fedi.length);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async get(url) {
|
|
36
|
+
const r = await this.storage.read(this._rel(url));
|
|
37
|
+
if (!r.ok) throw new Error(`pod GET ${url} → ${r.status}`);
|
|
38
|
+
return r.body;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async put(url, body, contentType = TURTLE) {
|
|
42
|
+
const r = await this.storage.write(this._rel(url), body, contentType);
|
|
43
|
+
if (!r.ok) throw new Error(`pod PUT ${url} → ${r.why}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async delete(url) {
|
|
47
|
+
if (!await this.storage.remove(this._rel(url))) throw new Error(`pod DELETE ${url} failed`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Child resource URLs of {fedi}{kind}/ (empty when the container is absent).
|
|
51
|
+
async listNotes(kind) {
|
|
52
|
+
const { names } = await this.storage.list(`${kind}/`);
|
|
53
|
+
return names
|
|
54
|
+
// `.<pid>.tmp` is what FileStorage.write leaves if it is interrupted
|
|
55
|
+
// between the write and the rename. It is not a note, and handing one to
|
|
56
|
+
// the parser produces a failure that reads like a corrupt document.
|
|
57
|
+
.filter(n => !n.endsWith('/') && !/\.(acl|meta)$/.test(n) && !/\.\d+\.tmp$/.test(n))
|
|
58
|
+
.map(n => `${this.fedi}${kind}/${n}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_graph(url, ttl) {
|
|
62
|
+
const g = $rdf.graph();
|
|
63
|
+
$rdf.parse(ttl, g, url, TURTLE);
|
|
64
|
+
return g;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Inverse of writeNote for one resource.
|
|
68
|
+
async readNote(url) {
|
|
69
|
+
const g = this._graph(url, await this.get(url));
|
|
70
|
+
const doc = $rdf.sym(url);
|
|
71
|
+
const iri = (p) => g.any(doc, AS(p), null, doc)?.value;
|
|
72
|
+
const str = (p) => g.any(doc, AS(p), null, doc)?.value;
|
|
73
|
+
const attachments = g.each(doc, AS('attachment'), null, doc).map((a) => {
|
|
74
|
+
const mediaType = g.any(a, AS('mediaType'), null, doc)?.value;
|
|
75
|
+
const description = g.any(a, AS('name'), null, doc)?.value;
|
|
76
|
+
return { url: a.value, mediaType: mediaType || '', ...(description ? { description } : {}) };
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
noteId: iri('url'), actor: iri('attributedTo'),
|
|
80
|
+
published: str('published'), inReplyTo: iri('inReplyTo'), content: str('content'),
|
|
81
|
+
...(attachments.length ? { attachments } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Incoming or own post → one RDF resource. kind: 'timeline' | 'posts'
|
|
86
|
+
async writeNote(kind, slug, { noteId, actor, published, content, inReplyTo, attachments }) {
|
|
87
|
+
const url = `${this.fedi}${kind}/${slug}`;
|
|
88
|
+
const doc = $rdf.sym(url);
|
|
89
|
+
const g = $rdf.graph();
|
|
90
|
+
g.add(doc, RDF('type'), AS('Note'), doc);
|
|
91
|
+
g.add(doc, AS('url'), $rdf.sym(noteId), doc);
|
|
92
|
+
g.add(doc, AS('attributedTo'), $rdf.sym(actor), doc);
|
|
93
|
+
// Two-arg with a NamedNode is how rdflib takes a datatype; passing it
|
|
94
|
+
// third silently yields an xsd:string, which would drop ^^xsd:dateTime
|
|
95
|
+
// from every note published from here on.
|
|
96
|
+
if (published) g.add(doc, AS('published'), $rdf.literal(published, XSD('dateTime')), doc);
|
|
97
|
+
if (inReplyTo) g.add(doc, AS('inReplyTo'), $rdf.sym(inReplyTo), doc);
|
|
98
|
+
g.add(doc, AS('content'), $rdf.literal(content || ''), doc);
|
|
99
|
+
for (const a of attachments || []) {
|
|
100
|
+
const at = $rdf.sym(a.url);
|
|
101
|
+
g.add(doc, AS('attachment'), at, doc);
|
|
102
|
+
g.add(at, RDF('type'), AS('Document'), doc);
|
|
103
|
+
if (a.mediaType) g.add(at, AS('mediaType'), $rdf.literal(a.mediaType), doc);
|
|
104
|
+
if (a.description) g.add(at, AS('name'), $rdf.literal(a.description), doc);
|
|
105
|
+
}
|
|
106
|
+
await this.put(url, $rdf.serialize(doc, g, url, TURTLE));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Contacts doc — the followers/following truth, rebuilt whole each change.
|
|
110
|
+
async writeContacts({ followers, following }) {
|
|
111
|
+
const url = this.fedi + 'contacts';
|
|
112
|
+
const doc = $rdf.sym(url);
|
|
113
|
+
const me = $rdf.sym(url + '#me');
|
|
114
|
+
const g = $rdf.graph();
|
|
115
|
+
g.add(me, RDF('type'), AS('Person'), doc);
|
|
116
|
+
for (const f of followers) g.add(me, AS('followers'), $rdf.sym(f.actor), doc);
|
|
117
|
+
for (const f of following) g.add(me, AS('following'), $rdf.sym(f.actor), doc);
|
|
118
|
+
await this.put(url, $rdf.serialize(doc, g, url, TURTLE));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Settings doc — handle + pointer to the public face.
|
|
122
|
+
async writeSettings({ handle, actorUrl }) {
|
|
123
|
+
const url = this.fedi + 'settings';
|
|
124
|
+
const doc = $rdf.sym(url);
|
|
125
|
+
const me = $rdf.sym(url + '#me');
|
|
126
|
+
const g = $rdf.graph();
|
|
127
|
+
g.add(me, RDF('type'), AS('Person'), doc);
|
|
128
|
+
g.add(me, AS('preferredUsername'), $rdf.literal(handle), doc);
|
|
129
|
+
g.add(me, AS('url'), $rdf.sym(actorUrl), doc);
|
|
130
|
+
await this.put(url, $rdf.serialize(doc, g, url, TURTLE));
|
|
131
|
+
}
|
|
132
|
+
}
|
package/lib/ports.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// ports.mjs — finding a port nothing else is on.
|
|
2
|
+
//
|
|
3
|
+
// This lived twice, in bin/fedipod.mjs and lib/admin.mjs, and the two
|
|
4
|
+
// copies had already drifted: one returned null when it ran out of ports and
|
|
5
|
+
// the other threw. Both spawn agents, so which one you got decided whether a
|
|
6
|
+
// full range produced an error message or a silent undefined.
|
|
7
|
+
|
|
8
|
+
import net from 'node:net';
|
|
9
|
+
|
|
10
|
+
// Bind to find out. Something holding a port without answering HTTP reads as
|
|
11
|
+
// free to a probe that only asks for a response, and the new agent then dies on
|
|
12
|
+
// EADDRINUSE with nobody watching.
|
|
13
|
+
export function portFree(port) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const probe = net.createServer();
|
|
16
|
+
probe.once('error', () => resolve(false));
|
|
17
|
+
probe.once('listening', () => probe.close(() => resolve(true)));
|
|
18
|
+
probe.listen(port, '127.0.0.1');
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// null when the whole span is taken. Callers that want an error say so in their
|
|
23
|
+
// own words — the CLI has a person reading it, the admin API has a status code.
|
|
24
|
+
export async function freePortFrom(first, span = 50) {
|
|
25
|
+
for (let p = first; p < first + span; p++) if (await portFree(p)) return p;
|
|
26
|
+
return null;
|
|
27
|
+
}
|