fedipod-server 0.4.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 +198 -0
- package/config/server.json +66 -0
- package/dist/adapt.d.ts +18 -0
- package/dist/adapt.js +79 -0
- package/dist/claims.d.ts +12 -0
- package/dist/claims.js +43 -0
- package/dist/components/components.jsonld +12 -0
- package/dist/components/context.jsonld +136 -0
- package/dist/directory.d.ts +38 -0
- package/dist/directory.js +77 -0
- package/dist/handler.d.ts +121 -0
- package/dist/handler.js +432 -0
- package/dist/handler.jsonld +519 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -0
- package/dist/store-css.d.ts +3 -0
- package/dist/store-css.js +41 -0
- package/dist/store-pod.d.ts +20 -0
- package/dist/store-pod.js +148 -0
- package/dist/streaming-handler.d.ts +14 -0
- package/dist/streaming-handler.js +47 -0
- package/dist/streaming-handler.jsonld +72 -0
- package/fedipod-server.svg +134 -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 +56 -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/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,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// directory.ts — the handle→pod directory, one small JSON resource per handle,
|
|
3
|
+
// read and written through a minimal IO shape { read, write } so it tests
|
|
4
|
+
// without a running CSS. store-css.ts supplies the real IO over a ResourceStore.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.makeDirectory = makeDirectory;
|
|
7
|
+
exports.makeAgentRegistry = makeAgentRegistry;
|
|
8
|
+
exports.makeStorePodPut = makeStorePodPut;
|
|
9
|
+
// One JSON document per key in a container — the shape both registries share.
|
|
10
|
+
function jsonTable(io, containerUrl) {
|
|
11
|
+
const base = containerUrl.endsWith('/') ? containerUrl : containerUrl + '/';
|
|
12
|
+
const docFor = (key) => base + encodeURIComponent(key) + '.json';
|
|
13
|
+
return {
|
|
14
|
+
docFor,
|
|
15
|
+
async get(key) {
|
|
16
|
+
const raw = await io.read(docFor(key));
|
|
17
|
+
if (!raw)
|
|
18
|
+
return null;
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
async put(key, record) {
|
|
27
|
+
await io.write(docFor(key), JSON.stringify(record), 'application/json');
|
|
28
|
+
},
|
|
29
|
+
async remove(key) {
|
|
30
|
+
await io.remove?.(docFor(key));
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function makeDirectory(io, containerUrl) {
|
|
35
|
+
const table = jsonTable(io, containerUrl);
|
|
36
|
+
return {
|
|
37
|
+
lookup: (handle) => table.get(handle),
|
|
38
|
+
putDirectory: (handle, record) => table.put(handle, record),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// The registry of runtime-opted-in pods, keyed by host (the claim key). The
|
|
42
|
+
// IO layer cannot enumerate a container, so index.json carries the host list;
|
|
43
|
+
// the row is written FIRST, so a crash between the two writes still leaves a
|
|
44
|
+
// row the next boot claims once the index catches up on the next change.
|
|
45
|
+
function makeAgentRegistry(io, containerUrl) {
|
|
46
|
+
const table = jsonTable(io, containerUrl);
|
|
47
|
+
const index = jsonTable(io, containerUrl);
|
|
48
|
+
const INDEX = 'index';
|
|
49
|
+
const hosts = async () => (await index.get(INDEX))?.hosts ?? [];
|
|
50
|
+
return {
|
|
51
|
+
listHosts: hosts,
|
|
52
|
+
get: (host) => table.get(host),
|
|
53
|
+
async add(record) {
|
|
54
|
+
await table.put(record.host, record);
|
|
55
|
+
const list = await hosts();
|
|
56
|
+
if (!list.includes(record.host))
|
|
57
|
+
await index.put(INDEX, { hosts: [...list, record.host] });
|
|
58
|
+
},
|
|
59
|
+
async remove(host) {
|
|
60
|
+
const list = await hosts();
|
|
61
|
+
await index.put(INDEX, { hosts: list.filter((h) => h !== host) });
|
|
62
|
+
await table.remove(host);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// podPut for the gateway core: write an inbox item straight through the store.
|
|
67
|
+
function makeStorePodPut(io) {
|
|
68
|
+
return async (url, body, contentType) => {
|
|
69
|
+
try {
|
|
70
|
+
await io.write(url, body, contentType);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { HttpHandler } from '@solid/community-server';
|
|
2
|
+
import type { HttpHandlerInput, ResourceStore, Initializable, Finalizable, ClusterManager } from '@solid/community-server';
|
|
3
|
+
import type { Directory } from './directory';
|
|
4
|
+
export interface FediPodServerArgs {
|
|
5
|
+
/** The server's ResourceStore: the handler reads pods and writes inbox items and directory rows directly through it — no HTTP, no credential. */
|
|
6
|
+
resourceStore: ResourceStore;
|
|
7
|
+
/** The apex host the front answers on, e.g. fedipod.net. Defaults to the host of frontOrigin, so a server that sets nothing answers on its own address. Pod subdomains are never claimed. */
|
|
8
|
+
frontHost?: string;
|
|
9
|
+
/** The front's origin, e.g. https://fedipod.net. */
|
|
10
|
+
frontOrigin: string;
|
|
11
|
+
/** An internal container URL where the handle→pod directory rows live. */
|
|
12
|
+
directoryContainer: string;
|
|
13
|
+
/** The WebID stamped on verification receipts. */
|
|
14
|
+
gatewayWebId?: string;
|
|
15
|
+
/** Whether this host also offers pods (the signup page shows the take-one option). The front never hosts pods itself. */
|
|
16
|
+
offersPods?: boolean;
|
|
17
|
+
/** The new-account page HTML served at the root. */
|
|
18
|
+
signupPage?: string;
|
|
19
|
+
/** The run-your-identity page HTML served at /run. */
|
|
20
|
+
runPage?: string;
|
|
21
|
+
/** Directory holding each agent identity's signing key and log. Required when runtime opt-in is on. */
|
|
22
|
+
agentDataDir?: string;
|
|
23
|
+
/** Path from a pod's base to the owner's WebID. */
|
|
24
|
+
agentWebIdSuffix?: string;
|
|
25
|
+
/** Seconds between inbox sweeps. Deliveries also wake the sweep as they land. */
|
|
26
|
+
agentPollSeconds?: number;
|
|
27
|
+
/** Whether a newly provisioned identity accepts follows without review. */
|
|
28
|
+
agentAutoAcceptFollows?: boolean;
|
|
29
|
+
/** The server's cluster manager, so the agent can say when it is running blind to other workers' writes. */
|
|
30
|
+
clusterManager?: ClusterManager;
|
|
31
|
+
/** Path on a pod's origin where its owner's pages live. Empty serves no pages at all. */
|
|
32
|
+
agentUiPath?: string;
|
|
33
|
+
/** Whether a pod owner may opt in at runtime by proving control of their pod. Off unless the host chooses it. */
|
|
34
|
+
agentRuntimeOptIn?: boolean;
|
|
35
|
+
/** When this server also runs the door, give each identity a @handle@frontHost address as it starts — an inbox-only directory row, written once. The actor keeps its own ids on the pod. Off by default. */
|
|
36
|
+
agentAutoFront?: boolean;
|
|
37
|
+
/** An internal container URL where the runtime opt-in rows live. */
|
|
38
|
+
agentRegistryContainer?: string;
|
|
39
|
+
}
|
|
40
|
+
/** One running identity, and the call that stops it. */
|
|
41
|
+
interface EmbeddedIdentity {
|
|
42
|
+
handle: string;
|
|
43
|
+
host: string;
|
|
44
|
+
surface: {
|
|
45
|
+
handler: (req: unknown, res: unknown) => Promise<void>;
|
|
46
|
+
streaming?: unknown;
|
|
47
|
+
};
|
|
48
|
+
stop: () => Promise<void>;
|
|
49
|
+
agent?: {
|
|
50
|
+
store?: {
|
|
51
|
+
getConfig?: () => {
|
|
52
|
+
kind?: string;
|
|
53
|
+
} | null | undefined;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export declare class FediPodServerHandler extends HttpHandler implements Initializable, Finalizable {
|
|
58
|
+
private readonly args;
|
|
59
|
+
private readonly io;
|
|
60
|
+
readonly dir: Directory;
|
|
61
|
+
private readonly podPut;
|
|
62
|
+
private readonly logger;
|
|
63
|
+
private readonly agentHosts;
|
|
64
|
+
private readonly agentHandles;
|
|
65
|
+
private readonly frontHost;
|
|
66
|
+
private readonly uiPath;
|
|
67
|
+
private readonly identities;
|
|
68
|
+
private readonly surfaces;
|
|
69
|
+
private readonly registry;
|
|
70
|
+
private readonly doorSecrets;
|
|
71
|
+
private readonly starting;
|
|
72
|
+
private readonly startCancelled;
|
|
73
|
+
private stopping;
|
|
74
|
+
private onSignal;
|
|
75
|
+
constructor(args: FediPodServerArgs);
|
|
76
|
+
/**
|
|
77
|
+
* Whether this pod may become an identity here: a real URL, an origin of its
|
|
78
|
+
* own, not the front's host, and a handle no other identity already uses —
|
|
79
|
+
* two pods must never share <agentDataDir>/<handle>/.
|
|
80
|
+
*/
|
|
81
|
+
private validateAgentHost;
|
|
82
|
+
/**
|
|
83
|
+
* Start an agent for each opted-in pod. Runs before the server listens, so
|
|
84
|
+
* the identities come up in the background and boot is never held on a pod.
|
|
85
|
+
*/
|
|
86
|
+
initialize(): Promise<void>;
|
|
87
|
+
/** Stop every identity: timers cleared, state written, lease let go. */
|
|
88
|
+
finalize(): Promise<void>;
|
|
89
|
+
private startIdentity;
|
|
90
|
+
/**
|
|
91
|
+
* When this server is also the door, give a running identity a
|
|
92
|
+
* @handle@<frontHost> address: one inbox-only directory row so the
|
|
93
|
+
* shared-domain handle resolves and the door can take verified delivery for
|
|
94
|
+
* it. Written once — a manual attach or an earlier boot wins. The identity
|
|
95
|
+
* keeps its own actor ids on the pod; nothing is moved.
|
|
96
|
+
*/
|
|
97
|
+
private frontIdentity;
|
|
98
|
+
canHandle({ request }: HttpHandlerInput): Promise<void>;
|
|
99
|
+
/** The running identity answering on a host, if it has finished starting. */
|
|
100
|
+
surfaceFor(host?: string): EmbeddedIdentity | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* A pod owner, already proven to control podBase, asks this server to run
|
|
103
|
+
* their identity. Returns { httpStatus, ...body }; the secret appears in the
|
|
104
|
+
* reply and nowhere else. Re-opting-in rotates the secret — that is how a
|
|
105
|
+
* lost one is recovered.
|
|
106
|
+
*/
|
|
107
|
+
optInPod({ podBase, webId }: {
|
|
108
|
+
podBase: string;
|
|
109
|
+
webId: string;
|
|
110
|
+
}): Promise<Record<string, unknown> & {
|
|
111
|
+
httpStatus: number;
|
|
112
|
+
}>;
|
|
113
|
+
/** The reverse: stop the identity and let the pod be plain LDP again. */
|
|
114
|
+
optOutPod({ podBase }: {
|
|
115
|
+
podBase: string;
|
|
116
|
+
}): Promise<Record<string, unknown> & {
|
|
117
|
+
httpStatus: number;
|
|
118
|
+
}>;
|
|
119
|
+
handle({ request, response }: HttpHandlerInput): Promise<void>;
|
|
120
|
+
}
|
|
121
|
+
export {};
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// handler.ts — the CSS HttpHandler. Thin by design: claim only the front's
|
|
3
|
+
// routes (canHandle), adapt Node↔WHATWG, and hand off to the same FediPod core
|
|
4
|
+
// the standalone gateway runs (routeFront). All the logic lives elsewhere and
|
|
5
|
+
// tests without CSS; this file is the wiring CSS needs.
|
|
6
|
+
//
|
|
7
|
+
// componentsjs-generator reads FediPodServerArgs to emit one component
|
|
8
|
+
// parameter per field, so the config injects each by name.
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.FediPodServerHandler = void 0;
|
|
11
|
+
const node_fs_1 = require("node:fs");
|
|
12
|
+
const node_path_1 = require("node:path");
|
|
13
|
+
const node_crypto_1 = require("node:crypto");
|
|
14
|
+
const community_server_1 = require("@solid/community-server");
|
|
15
|
+
const claims_1 = require("./claims");
|
|
16
|
+
const adapt_1 = require("./adapt");
|
|
17
|
+
const store_css_1 = require("./store-css");
|
|
18
|
+
const store_pod_1 = require("./store-pod");
|
|
19
|
+
const directory_1 = require("./directory");
|
|
20
|
+
// The JS front-core is FediPod's own ESM tree, reached at runtime. A real
|
|
21
|
+
// dynamic import() built via Function keeps tsc from downleveling it to
|
|
22
|
+
// require() — which cannot load an ESM module with top-level await under a
|
|
23
|
+
// CommonJS build.
|
|
24
|
+
//
|
|
25
|
+
// Two layouts carry that tree: the published package ships its own copy of
|
|
26
|
+
// lib/ beside dist/ (prepack puts it there), and a repo checkout reaches the
|
|
27
|
+
// repo's lib/ three levels up. Prefer the package's own copy when it exists.
|
|
28
|
+
const LIB_ROOT = (0, node_fs_1.existsSync)((0, node_path_1.join)(__dirname, '../lib/embed.mjs')) ? '../lib' : '../../../lib';
|
|
29
|
+
const FRONT_CORE = `${LIB_ROOT}/front-core.mjs`;
|
|
30
|
+
const EMBED = `${LIB_ROOT}/embed.mjs`;
|
|
31
|
+
const esmImport = new Function('s', 'return import(s)');
|
|
32
|
+
// The front's pages and the files they load, carried in the same two layouts
|
|
33
|
+
// as lib/. A missing file is not fatal: the route it feeds answers 404.
|
|
34
|
+
const WEB_ROOT = (0, node_fs_1.existsSync)((0, node_path_1.join)(__dirname, '../web/front/run.html'))
|
|
35
|
+
? (0, node_path_1.join)(__dirname, '../web/front') : (0, node_path_1.join)(__dirname, '../../../web/front');
|
|
36
|
+
const webFile = (name) => {
|
|
37
|
+
try {
|
|
38
|
+
return (0, node_fs_1.readFileSync)((0, node_path_1.join)(WEB_ROOT, name), 'utf8');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
// A pod that will not come up yet is usually a pod still being created by the
|
|
45
|
+
// server that is booting. Keep asking, slower each time, up to a few minutes.
|
|
46
|
+
const START_RETRY_MS = [2_000, 5_000, 15_000, 60_000, 300_000];
|
|
47
|
+
/** The identity's name, from its pod URL. Mirrors handleFor in lib/embed.mjs. */
|
|
48
|
+
function deriveHandle(podBase) {
|
|
49
|
+
const u = new URL(podBase);
|
|
50
|
+
const segments = u.pathname.split('/').filter((seg) => seg.length > 0);
|
|
51
|
+
return segments.length > 0 ? segments[segments.length - 1] : u.hostname.split('.')[0];
|
|
52
|
+
}
|
|
53
|
+
/** A door path always has both slashes, so claiming and stripping agree. */
|
|
54
|
+
function normalizeUiPath(raw) {
|
|
55
|
+
if (raw === '')
|
|
56
|
+
return '';
|
|
57
|
+
const path = raw ?? '/fedipod/';
|
|
58
|
+
return `/${path.replace(/^\/+|\/+$/gu, '')}/`;
|
|
59
|
+
}
|
|
60
|
+
class FediPodServerHandler extends community_server_1.HttpHandler {
|
|
61
|
+
args;
|
|
62
|
+
io;
|
|
63
|
+
dir;
|
|
64
|
+
podPut;
|
|
65
|
+
logger = (0, community_server_1.getLoggerFor)(this);
|
|
66
|
+
agentHosts = new Set();
|
|
67
|
+
agentHandles = new Map(); // handle → pod base
|
|
68
|
+
frontHost;
|
|
69
|
+
uiPath;
|
|
70
|
+
identities = new Map();
|
|
71
|
+
surfaces = new Map();
|
|
72
|
+
registry;
|
|
73
|
+
doorSecrets = new Map(); // pod base → its door secret
|
|
74
|
+
starting = new Set();
|
|
75
|
+
startCancelled = new Set();
|
|
76
|
+
stopping = false;
|
|
77
|
+
onSignal = null;
|
|
78
|
+
constructor(args) {
|
|
79
|
+
super();
|
|
80
|
+
this.args = args;
|
|
81
|
+
this.io = (0, store_css_1.makeStoreIO)(args.resourceStore);
|
|
82
|
+
// The internal containers are configured as paths; the store speaks
|
|
83
|
+
// absolute identifiers, rooted at the server's own origin.
|
|
84
|
+
const absolute = (container) => (container.startsWith('/') ? new URL(container, args.frontOrigin).href : container);
|
|
85
|
+
this.dir = (0, directory_1.makeDirectory)(this.io, absolute(args.directoryContainer));
|
|
86
|
+
this.podPut = (0, directory_1.makeStorePodPut)(this.io);
|
|
87
|
+
// Fail at construction, not at first use: a server told to run agents
|
|
88
|
+
// and unable to should not boot into a state where it silently runs none.
|
|
89
|
+
if (args.agentRuntimeOptIn && !args.agentDataDir) {
|
|
90
|
+
throw new Error('runtime opt-in is enabled but agentDataDir is not set — identities have nowhere to keep their signing keys');
|
|
91
|
+
}
|
|
92
|
+
// Without this a shipped default apex would silently claim nothing on the
|
|
93
|
+
// operator's own host, and sign-up would never route.
|
|
94
|
+
// hostname, not host: the claim check compares bare hostnames, so a port
|
|
95
|
+
// carried here would stop the front matching its own requests.
|
|
96
|
+
this.frontHost = args.frontHost || new URL(args.frontOrigin).hostname;
|
|
97
|
+
this.uiPath = normalizeUiPath(args.agentUiPath);
|
|
98
|
+
this.registry = args.agentRuntimeOptIn
|
|
99
|
+
? (0, directory_1.makeAgentRegistry)(this.io, absolute(args.agentRegistryContainer ?? '/.internal/fedipod/agents/'))
|
|
100
|
+
: null;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Whether this pod may become an identity here: a real URL, an origin of its
|
|
104
|
+
* own, not the front's host, and a handle no other identity already uses —
|
|
105
|
+
* two pods must never share <agentDataDir>/<handle>/.
|
|
106
|
+
*/
|
|
107
|
+
validateAgentHost(podBase) {
|
|
108
|
+
let host;
|
|
109
|
+
try {
|
|
110
|
+
host = new URL(podBase).host.toLowerCase();
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
throw new Error(`not a pod URL: ${podBase}`);
|
|
114
|
+
}
|
|
115
|
+
// The Mastodon client API is rooted at an origin, so two identities
|
|
116
|
+
// cannot share one — and an identity cannot share the front's origin.
|
|
117
|
+
if (this.agentHosts.has(host)) {
|
|
118
|
+
throw new Error(`the host ${host} already carries an identity — an identity needs an origin of its own`);
|
|
119
|
+
}
|
|
120
|
+
if (host.split(':')[0] === String(this.frontHost).toLowerCase()) {
|
|
121
|
+
throw new Error(`${podBase} is on the front's own host — give the identity its own origin`);
|
|
122
|
+
}
|
|
123
|
+
const handle = deriveHandle(podBase);
|
|
124
|
+
const holder = this.agentHandles.get(handle);
|
|
125
|
+
if (holder && holder !== podBase) {
|
|
126
|
+
throw new Error(`the name ${handle} already belongs to ${holder} — two identities cannot share it`);
|
|
127
|
+
}
|
|
128
|
+
return host;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Start an agent for each opted-in pod. Runs before the server listens, so
|
|
132
|
+
* the identities come up in the background and boot is never held on a pod.
|
|
133
|
+
*/
|
|
134
|
+
async initialize() {
|
|
135
|
+
if (!this.registry)
|
|
136
|
+
return;
|
|
137
|
+
// The stock CSS CLI installs no signal handlers, so a SIGTERM (systemd
|
|
138
|
+
// stop, docker stop, Ctrl+C) killed the process with agent state
|
|
139
|
+
// unflushed and the lease held for its whole TTL. Flush first, bounded,
|
|
140
|
+
// then re-raise so the process still dies the way it was asked to.
|
|
141
|
+
if (!this.onSignal) {
|
|
142
|
+
this.onSignal = (signal) => {
|
|
143
|
+
const timeout = new Promise((resolve) => { setTimeout(resolve, 5_000).unref?.(); });
|
|
144
|
+
void Promise.race([this.finalize(), timeout]).then(() => {
|
|
145
|
+
process.kill(process.pid, signal);
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
process.once('SIGTERM', this.onSignal);
|
|
149
|
+
process.once('SIGINT', this.onSignal);
|
|
150
|
+
}
|
|
151
|
+
// The agent runs in the primary process, and a write made by a worker
|
|
152
|
+
// raises its change event there — so with workers the inbox is swept on the
|
|
153
|
+
// timer rather than the moment a delivery lands. Everything still works; it
|
|
154
|
+
// is just slower, and worth saying rather than leaving to be discovered.
|
|
155
|
+
if (this.args.clusterManager && !this.args.clusterManager.isSingleThreaded()) {
|
|
156
|
+
this.logger.warn('FediPod agent is running in a multi-worker server: deliveries are picked up by the '
|
|
157
|
+
+ 'inbox sweep instead of as they arrive. Run with --workers 1 for immediate delivery.');
|
|
158
|
+
}
|
|
159
|
+
// Awaited, and BEFORE the server listens: a pod whose owner opted in must
|
|
160
|
+
// have its routes claimed from the first request after a restart, never
|
|
161
|
+
// served briefly by LDP. A failed load must not fail the boot — the rows
|
|
162
|
+
// persist, and the next start recovers them.
|
|
163
|
+
const pods = [];
|
|
164
|
+
try {
|
|
165
|
+
const hosts = await this.registry.listHosts();
|
|
166
|
+
for (const host of hosts) {
|
|
167
|
+
const row = await this.registry.get(host);
|
|
168
|
+
if (!row)
|
|
169
|
+
continue;
|
|
170
|
+
if (this.agentHosts.has(row.host) || this.identities.has(row.podBase))
|
|
171
|
+
continue; // already claimed
|
|
172
|
+
try {
|
|
173
|
+
this.validateAgentHost(row.podBase);
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
this.logger.error(`opted-in pod ${row.podBase} no longer valid: ${e.message}`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
this.agentHosts.add(row.host);
|
|
180
|
+
this.agentHandles.set(row.handle, row.podBase);
|
|
181
|
+
pods.push(row.podBase);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
this.logger.error(`could not read the opt-in registry — opted-in identities are absent this boot: ${e.message}`);
|
|
186
|
+
}
|
|
187
|
+
if (pods.length > 0)
|
|
188
|
+
this.logger.info(`FediPod agent enabled for ${pods.length} opted-in pod(s)`);
|
|
189
|
+
for (const pod of pods)
|
|
190
|
+
void this.startIdentity(pod);
|
|
191
|
+
}
|
|
192
|
+
/** Stop every identity: timers cleared, state written, lease let go. */
|
|
193
|
+
async finalize() {
|
|
194
|
+
this.stopping = true;
|
|
195
|
+
if (this.onSignal) {
|
|
196
|
+
process.removeListener('SIGTERM', this.onSignal);
|
|
197
|
+
process.removeListener('SIGINT', this.onSignal);
|
|
198
|
+
this.onSignal = null;
|
|
199
|
+
}
|
|
200
|
+
const running = [...this.identities.values()];
|
|
201
|
+
this.identities.clear();
|
|
202
|
+
this.surfaces.clear();
|
|
203
|
+
this.doorSecrets.clear();
|
|
204
|
+
this.starting.clear();
|
|
205
|
+
await Promise.allSettled(running.map(async (identity) => {
|
|
206
|
+
await identity.stop();
|
|
207
|
+
this.logger.info(`FediPod agent @${identity.handle} stopped`);
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
async startIdentity(podBase) {
|
|
211
|
+
if (this.starting.has(podBase) || this.identities.has(podBase))
|
|
212
|
+
return;
|
|
213
|
+
this.starting.add(podBase);
|
|
214
|
+
this.startCancelled.delete(podBase);
|
|
215
|
+
const session = (0, store_pod_1.makeStoreSession)(this.args.resourceStore, podBase);
|
|
216
|
+
try {
|
|
217
|
+
for (let attempt = 0; !this.stopping && !this.startCancelled.has(podBase); attempt++) {
|
|
218
|
+
try {
|
|
219
|
+
const { startEmbeddedAgent, ensureDoorSecret } = await esmImport(EMBED);
|
|
220
|
+
// The secret is in the map BEFORE the surface can exist, so the
|
|
221
|
+
// gate's resolver never comes up empty — empty would mean gate-off.
|
|
222
|
+
if (!this.doorSecrets.has(podBase)) {
|
|
223
|
+
const door = ensureDoorSecret(this.args.agentDataDir, deriveHandle(podBase));
|
|
224
|
+
this.doorSecrets.set(podBase, door.secret);
|
|
225
|
+
this.logger.info(`door secret for @${deriveHandle(podBase)} is at ${door.path}`);
|
|
226
|
+
}
|
|
227
|
+
const identity = await startEmbeddedAgent({
|
|
228
|
+
podBase,
|
|
229
|
+
dataDir: this.args.agentDataDir,
|
|
230
|
+
session,
|
|
231
|
+
resourceStore: this.args.resourceStore,
|
|
232
|
+
webIdSuffix: this.args.agentWebIdSuffix ?? 'profile/card#me',
|
|
233
|
+
pollSeconds: this.args.agentPollSeconds ?? null,
|
|
234
|
+
autoAcceptFollows: this.args.agentAutoAcceptFollows !== false,
|
|
235
|
+
gateToken: () => this.doorSecrets.get(podBase),
|
|
236
|
+
uiPath: this.uiPath,
|
|
237
|
+
log: (message) => {
|
|
238
|
+
this.logger.info(message);
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
// Stopped, or opted out, while this one was still coming up.
|
|
242
|
+
if (this.stopping || this.startCancelled.has(podBase)) {
|
|
243
|
+
await identity.stop();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
this.identities.set(podBase, identity);
|
|
247
|
+
this.surfaces.set(identity.host, identity);
|
|
248
|
+
this.logger.info(`FediPod agent @${identity.handle} running on ${podBase}`);
|
|
249
|
+
if (this.args.agentAutoFront)
|
|
250
|
+
await this.frontIdentity(podBase, identity);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
const wait = START_RETRY_MS[Math.min(attempt, START_RETRY_MS.length - 1)];
|
|
255
|
+
this.logger.warn(`FediPod agent for ${podBase} did not start: ${e.message} — retrying in ${Math.round(wait / 1000)}s`);
|
|
256
|
+
await new Promise((resolve) => {
|
|
257
|
+
setTimeout(resolve, wait).unref?.();
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
this.starting.delete(podBase);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* When this server is also the door, give a running identity a
|
|
268
|
+
* @handle@<frontHost> address: one inbox-only directory row so the
|
|
269
|
+
* shared-domain handle resolves and the door can take verified delivery for
|
|
270
|
+
* it. Written once — a manual attach or an earlier boot wins. The identity
|
|
271
|
+
* keeps its own actor ids on the pod; nothing is moved.
|
|
272
|
+
*/
|
|
273
|
+
async frontIdentity(podBase, identity) {
|
|
274
|
+
const { handle } = identity;
|
|
275
|
+
try {
|
|
276
|
+
if (await this.dir.lookup(handle))
|
|
277
|
+
return;
|
|
278
|
+
const kind = identity.agent?.store?.getConfig?.()?.kind === 'group' ? 'group' : 'person';
|
|
279
|
+
await this.dir.putDirectory(handle, {
|
|
280
|
+
handle, podHome: podBase, actorUrl: `${podBase}ap/actor`, kind,
|
|
281
|
+
gatewayWebId: this.args.gatewayWebId ?? null,
|
|
282
|
+
hmacSecret: (0, node_crypto_1.randomBytes)(32).toString('base64'),
|
|
283
|
+
inboxOnly: true,
|
|
284
|
+
});
|
|
285
|
+
this.logger.info(`FediPod: @${handle}@${this.frontHost} now resolves to ${podBase}`);
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
this.logger.warn(`FediPod: could not front @${handle}: ${e.message}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async canHandle({ request }) {
|
|
292
|
+
const host = request.headers.host;
|
|
293
|
+
const pathname = new URL(request.url ?? '/', `https://${host}`).pathname;
|
|
294
|
+
// Claimed from the opt-in roster, never from what is running: a pod resource
|
|
295
|
+
// must not be served by CSS for the seconds before an identity finishes
|
|
296
|
+
// starting, and then stop being served once it has.
|
|
297
|
+
if ((0, claims_1.claims)({ host, pathname }, this.frontHost))
|
|
298
|
+
return;
|
|
299
|
+
if ((0, claims_1.agentClaims)({ host, pathname }, this.agentHosts, this.uiPath))
|
|
300
|
+
return;
|
|
301
|
+
throw new Error('not a gateway route'); // reject → CSS's LDP handler takes it
|
|
302
|
+
}
|
|
303
|
+
/** The running identity answering on a host, if it has finished starting. */
|
|
304
|
+
surfaceFor(host) {
|
|
305
|
+
return this.surfaces.get(String(host ?? '').toLowerCase());
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* A pod owner, already proven to control podBase, asks this server to run
|
|
309
|
+
* their identity. Returns { httpStatus, ...body }; the secret appears in the
|
|
310
|
+
* reply and nowhere else. Re-opting-in rotates the secret — that is how a
|
|
311
|
+
* lost one is recovered.
|
|
312
|
+
*/
|
|
313
|
+
async optInPod({ podBase, webId }) {
|
|
314
|
+
if (!this.registry)
|
|
315
|
+
return { httpStatus: 501, error: 'this server does not offer runtime opt-in' };
|
|
316
|
+
if (this.args.clusterManager && !this.args.clusterManager.isSingleThreaded()) {
|
|
317
|
+
return { httpStatus: 503, error: 'runtime opt-in needs a single-worker server (--workers 1)' };
|
|
318
|
+
}
|
|
319
|
+
const base = podBase.endsWith('/') ? podBase : `${podBase}/`;
|
|
320
|
+
const handle = deriveHandle(base);
|
|
321
|
+
const { ensureDoorSecret } = await esmImport(EMBED);
|
|
322
|
+
// Already running here from an earlier opt-in: proving pod
|
|
323
|
+
// control again buys a fresh secret, nothing else.
|
|
324
|
+
if (this.agentHandles.get(handle) === base) {
|
|
325
|
+
const door = ensureDoorSecret(this.args.agentDataDir, handle, { rotate: true });
|
|
326
|
+
this.doorSecrets.set(base, door.secret);
|
|
327
|
+
return { httpStatus: 201, ok: true, handle, host: new URL(base).host.toLowerCase(),
|
|
328
|
+
doorSecret: door.secret, doorPath: this.uiPath, status: 'rotated' };
|
|
329
|
+
}
|
|
330
|
+
let host;
|
|
331
|
+
try {
|
|
332
|
+
host = this.validateAgentHost(base);
|
|
333
|
+
}
|
|
334
|
+
catch (e) {
|
|
335
|
+
return { httpStatus: 409, error: e.message };
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
await this.registry.add({ podBase: base, handle, host, webId, optedInAt: new Date().toISOString() });
|
|
339
|
+
}
|
|
340
|
+
catch (e) {
|
|
341
|
+
this.logger.error(`opt-in row for ${base} could not be written: ${e.message}`);
|
|
342
|
+
return { httpStatus: 500, error: 'could not record the opt-in' };
|
|
343
|
+
}
|
|
344
|
+
// From this instant the pod's identity routes answer 503 instead of LDP,
|
|
345
|
+
// until the agent registers its surface.
|
|
346
|
+
this.agentHosts.add(host);
|
|
347
|
+
this.agentHandles.set(handle, base);
|
|
348
|
+
const door = ensureDoorSecret(this.args.agentDataDir, handle, { rotate: true });
|
|
349
|
+
this.doorSecrets.set(base, door.secret);
|
|
350
|
+
void this.startIdentity(base);
|
|
351
|
+
this.logger.info(`runtime opt-in: @${handle} on ${base} (door secret at ${door.path})`);
|
|
352
|
+
return { httpStatus: 201, ok: true, handle, host,
|
|
353
|
+
doorSecret: door.secret, doorPath: this.uiPath, status: 'starting' };
|
|
354
|
+
}
|
|
355
|
+
/** The reverse: stop the identity and let the pod be plain LDP again. */
|
|
356
|
+
async optOutPod({ podBase }) {
|
|
357
|
+
if (!this.registry)
|
|
358
|
+
return { httpStatus: 501, error: 'this server does not offer runtime opt-in' };
|
|
359
|
+
const base = podBase.endsWith('/') ? podBase : `${podBase}/`;
|
|
360
|
+
const host = new URL(base).host.toLowerCase();
|
|
361
|
+
const row = await this.registry.get(host);
|
|
362
|
+
if (!row || row.podBase !== base)
|
|
363
|
+
return { httpStatus: 404, error: 'this pod has not opted in' };
|
|
364
|
+
this.agentHosts.delete(host); // routes fall to LDP now
|
|
365
|
+
this.startCancelled.add(base); // a pending start stands down
|
|
366
|
+
const identity = this.identities.get(base);
|
|
367
|
+
this.identities.delete(base);
|
|
368
|
+
this.surfaces.delete(host);
|
|
369
|
+
this.agentHandles.delete(row.handle);
|
|
370
|
+
this.doorSecrets.delete(base);
|
|
371
|
+
if (identity)
|
|
372
|
+
await identity.stop();
|
|
373
|
+
await this.registry.remove(host);
|
|
374
|
+
this.logger.info(`runtime opt-out: @${row.handle} on ${base} — the pod serves plain LDP again`);
|
|
375
|
+
return { httpStatus: 200, ok: true, stopped: Boolean(identity) };
|
|
376
|
+
}
|
|
377
|
+
async handle({ request, response }) {
|
|
378
|
+
const host = String(request.headers.host ?? '').toLowerCase();
|
|
379
|
+
if (this.agentHosts.has(host)) {
|
|
380
|
+
const identity = this.surfaces.get(host);
|
|
381
|
+
if (!identity) {
|
|
382
|
+
// Claimed, but its identity is not up yet. Saying so is better than
|
|
383
|
+
// letting the pod answer for a route that is about to stop being pod.
|
|
384
|
+
response.writeHead(503, { 'content-type': 'application/json', 'retry-after': '5' });
|
|
385
|
+
response.end(JSON.stringify({ error: 'this identity is still starting' }));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
await identity.surface.handler(request, response);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const { routeFront } = await esmImport(FRONT_CORE);
|
|
392
|
+
let whatwg;
|
|
393
|
+
try {
|
|
394
|
+
whatwg = await (0, adapt_1.nodeToWhatwg)(request, this.args.frontOrigin);
|
|
395
|
+
}
|
|
396
|
+
catch (e) {
|
|
397
|
+
const status = e.statusCode === 413 ? 413 : 400;
|
|
398
|
+
response.writeHead(status, { 'content-type': 'application/json' });
|
|
399
|
+
response.end(JSON.stringify({ error: e.message }));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const out = await routeFront(whatwg, {
|
|
403
|
+
host: this.frontHost,
|
|
404
|
+
frontOrigin: this.args.frontOrigin,
|
|
405
|
+
gatewayWebId: this.args.gatewayWebId,
|
|
406
|
+
offersPods: !!this.args.offersPods,
|
|
407
|
+
signupPage: this.args.signupPage || webFile('new-account.html'),
|
|
408
|
+
runPage: this.args.runPage || webFile('run.html'),
|
|
409
|
+
// Both pages load the sign-in library, and the signup page hands out the
|
|
410
|
+
// installer command; without these the pages render but cannot be used.
|
|
411
|
+
authBundle: webFile('solid-client-authn.bundle.js'),
|
|
412
|
+
installScript: webFile('install.sh'),
|
|
413
|
+
lookup: (h) => this.dir.lookup(h),
|
|
414
|
+
putDirectory: (h, rec) => this.dir.putDirectory(h, rec),
|
|
415
|
+
podPut: (_handle, url, body, ct) => this.podPut(url, body, ct),
|
|
416
|
+
podGet: async (url) => {
|
|
417
|
+
const raw = await this.io.read(url);
|
|
418
|
+
return raw == null
|
|
419
|
+
? { status: 404, text: async () => '', headers: { get: () => null } }
|
|
420
|
+
: { status: 200, text: async () => raw, headers: { get: () => null } };
|
|
421
|
+
},
|
|
422
|
+
agentControl: this.registry
|
|
423
|
+
? {
|
|
424
|
+
optIn: (a) => this.optInPod(a),
|
|
425
|
+
optOut: (a) => this.optOutPod(a),
|
|
426
|
+
}
|
|
427
|
+
: undefined,
|
|
428
|
+
});
|
|
429
|
+
await (0, adapt_1.applyToNode)(response, out);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
exports.FediPodServerHandler = FediPodServerHandler;
|