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,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// store-pod.ts — the agent's transport, over CSS's ResourceStore instead of
|
|
3
|
+
// the network. RemotePod funnels every pod operation through one fetch(), so
|
|
4
|
+
// swapping that one function moves the whole agent inside the server: no
|
|
5
|
+
// credential, no token, no socket to the pod it is already running in.
|
|
6
|
+
//
|
|
7
|
+
// The shape is WHATWG fetch because that is what RemotePod, Lease and Storage
|
|
8
|
+
// already speak — ETags and If-Match included, which is what keeps the lease's
|
|
9
|
+
// conditional-PUT protocol working unchanged.
|
|
10
|
+
//
|
|
11
|
+
// Writing here bypasses WAC by design, exactly as store-css.ts does: the agent
|
|
12
|
+
// acts as the pod's owner, and the server operator put it there. `podBase`
|
|
13
|
+
// confines the transport to that pod's own subtree — the store serves every
|
|
14
|
+
// pod on the server, and an identity's reach must not.
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.makeStoreFetch = makeStoreFetch;
|
|
17
|
+
exports.makeStoreSession = makeStoreSession;
|
|
18
|
+
const community_server_1 = require("@solid/community-server");
|
|
19
|
+
/** Header lookup that does not care about case, over either shape fetch accepts. */
|
|
20
|
+
function headerReader(init) {
|
|
21
|
+
const raw = init?.headers;
|
|
22
|
+
if (!raw)
|
|
23
|
+
return () => null;
|
|
24
|
+
if (typeof raw.get === 'function')
|
|
25
|
+
return (n) => raw.get(n);
|
|
26
|
+
const lower = {};
|
|
27
|
+
for (const [k, v] of Object.entries(raw))
|
|
28
|
+
lower[k.toLowerCase()] = String(v);
|
|
29
|
+
return (n) => lower[n.toLowerCase()] ?? null;
|
|
30
|
+
}
|
|
31
|
+
// Only Turtle needs asking for: a container's listing is quads until something
|
|
32
|
+
// requests a syntax, and listContainer parses Turtle. Everything else is read
|
|
33
|
+
// back exactly as it was written.
|
|
34
|
+
function preferencesFor(accept) {
|
|
35
|
+
return accept?.includes('text/turtle') ? { type: { 'text/turtle': 1 } } : {};
|
|
36
|
+
}
|
|
37
|
+
function isNotFound(e) {
|
|
38
|
+
return community_server_1.NotFoundHttpError.isInstance?.(e) === true || e?.statusCode === 404;
|
|
39
|
+
}
|
|
40
|
+
/** A store path is the URL itself; a doubled slash would name a different resource. */
|
|
41
|
+
function pathOf(url) {
|
|
42
|
+
const u = new URL(url);
|
|
43
|
+
u.pathname = u.pathname.replace(/\/{2,}/gu, '/');
|
|
44
|
+
u.hash = '';
|
|
45
|
+
return u.href;
|
|
46
|
+
}
|
|
47
|
+
async function discard(rep) {
|
|
48
|
+
rep.data.destroy();
|
|
49
|
+
}
|
|
50
|
+
function makeStoreFetch(resourceStore, podBase) {
|
|
51
|
+
const stats = { reads: 0, writes: 0, deletes: 0 };
|
|
52
|
+
const etags = new community_server_1.BasicETagHandler();
|
|
53
|
+
const base = podBase ? new URL(podBase).href : null;
|
|
54
|
+
const respond = (status, body, etag, contentType) => {
|
|
55
|
+
const headers = {};
|
|
56
|
+
if (etag)
|
|
57
|
+
headers.etag = etag;
|
|
58
|
+
if (contentType)
|
|
59
|
+
headers['content-type'] = contentType;
|
|
60
|
+
return new Response(status === 204 || status === 205 || status === 304 ? null : body, { status, headers });
|
|
61
|
+
};
|
|
62
|
+
const fetchImpl = async (url, init = {}) => {
|
|
63
|
+
const method = (init.method ?? 'GET').toUpperCase();
|
|
64
|
+
const header = headerReader(init);
|
|
65
|
+
const identifier = { path: pathOf(url) };
|
|
66
|
+
// Confinement, not access control: every legitimate target is under the
|
|
67
|
+
// pod's own base, so anything else is a bug or an escape — refused here
|
|
68
|
+
// rather than answered with another pod's resource.
|
|
69
|
+
if (base && !identifier.path.startsWith(base))
|
|
70
|
+
return respond(403, 'outside this pod');
|
|
71
|
+
if (method === 'GET' || method === 'HEAD') {
|
|
72
|
+
stats.reads++;
|
|
73
|
+
let rep;
|
|
74
|
+
try {
|
|
75
|
+
rep = await resourceStore.getRepresentation(identifier, preferencesFor(header('accept')));
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
if (isNotFound(e))
|
|
79
|
+
return respond(404, 'Not Found');
|
|
80
|
+
throw e;
|
|
81
|
+
}
|
|
82
|
+
const etag = etags.getETag(rep.metadata);
|
|
83
|
+
const noneMatch = header('if-none-match');
|
|
84
|
+
// Weak comparison, per RFC 7232 — the drain re-reads the inbox listing
|
|
85
|
+
// every couple of minutes and it is usually the same listing.
|
|
86
|
+
if (etag && noneMatch && noneMatch.split(',').some((t) => etags.matchesETag(rep.metadata, t.trim(), false))) {
|
|
87
|
+
await discard(rep);
|
|
88
|
+
return respond(304, null, etag);
|
|
89
|
+
}
|
|
90
|
+
const contentType = rep.metadata.contentType;
|
|
91
|
+
if (method === 'HEAD') {
|
|
92
|
+
await discard(rep);
|
|
93
|
+
return respond(200, null, etag, contentType);
|
|
94
|
+
}
|
|
95
|
+
return respond(200, await (0, community_server_1.readableToString)(rep.data), etag, contentType);
|
|
96
|
+
}
|
|
97
|
+
if (method === 'PUT') {
|
|
98
|
+
stats.writes++;
|
|
99
|
+
const ifMatch = header('if-match');
|
|
100
|
+
const conditions = ifMatch
|
|
101
|
+
? new community_server_1.BasicConditions(etags, { matchesETag: ifMatch.split(',').map((t) => t.trim()) })
|
|
102
|
+
: undefined;
|
|
103
|
+
const contentType = header('content-type') ?? 'application/octet-stream';
|
|
104
|
+
const rep = new community_server_1.BasicRepresentation(String(init.body ?? ''), identifier, contentType);
|
|
105
|
+
let changes;
|
|
106
|
+
try {
|
|
107
|
+
changes = await resourceStore.setRepresentation(identifier, rep, conditions);
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
if (community_server_1.PreconditionFailedHttpError.isInstance?.(e))
|
|
111
|
+
return respond(412, 'Precondition Failed');
|
|
112
|
+
throw e;
|
|
113
|
+
}
|
|
114
|
+
// The lease renews with a conditional PUT, so hand back the new ETag when
|
|
115
|
+
// the store reported one; without it the lease re-reads first, which is
|
|
116
|
+
// slower but just as correct.
|
|
117
|
+
const written = changes?.get?.(identifier);
|
|
118
|
+
return respond(205, null, written && etags.getETag(written));
|
|
119
|
+
}
|
|
120
|
+
if (method === 'DELETE') {
|
|
121
|
+
stats.deletes++;
|
|
122
|
+
try {
|
|
123
|
+
await resourceStore.deleteResource(identifier);
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
if (isNotFound(e))
|
|
127
|
+
return respond(404, 'Not Found');
|
|
128
|
+
throw e;
|
|
129
|
+
}
|
|
130
|
+
return respond(205, null);
|
|
131
|
+
}
|
|
132
|
+
throw new community_server_1.NotImplementedHttpError(`${method} is not something the pod store can answer`);
|
|
133
|
+
};
|
|
134
|
+
fetchImpl.stats = () => ({ ...stats });
|
|
135
|
+
return fetchImpl;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The session shape RemotePod consumes — the same three members its
|
|
139
|
+
* credential-backed session has, so the class itself needs no other change.
|
|
140
|
+
*/
|
|
141
|
+
function makeStoreSession(resourceStore, podBase) {
|
|
142
|
+
const storeFetch = makeStoreFetch(resourceStore, podBase);
|
|
143
|
+
return {
|
|
144
|
+
fetch: storeFetch,
|
|
145
|
+
warmup: async () => undefined, // nothing to mint
|
|
146
|
+
stats: () => storeFetch.stats(),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { WebSocketHandler } from '@solid/community-server';
|
|
2
|
+
import type { WebSocketHandlerInput } from '@solid/community-server';
|
|
3
|
+
import type { FediPodServerHandler } from './handler';
|
|
4
|
+
export interface FediPodStreamingArgs {
|
|
5
|
+
/** The server handler, which owns the running identities. */
|
|
6
|
+
server: FediPodServerHandler;
|
|
7
|
+
}
|
|
8
|
+
export declare class FediPodStreamingHandler extends WebSocketHandler {
|
|
9
|
+
private readonly server;
|
|
10
|
+
private readonly logger;
|
|
11
|
+
constructor(args: FediPodStreamingArgs);
|
|
12
|
+
canHandle({ upgradeRequest }: WebSocketHandlerInput): Promise<void>;
|
|
13
|
+
handle({ webSocket, upgradeRequest }: WebSocketHandlerInput): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// streaming-handler.ts — the Mastodon live feed, on the pod's own origin.
|
|
3
|
+
//
|
|
4
|
+
// A second class rather than more of the server handler: CSS routes websocket
|
|
5
|
+
// upgrades through a waterfall of its own, whose handlers take a socket rather
|
|
6
|
+
// than a request and response, and one class cannot be both. It shares the
|
|
7
|
+
// server handler’s state by holding the same instance, so a socket is handed
|
|
8
|
+
// to the identity that owns the host it arrived on.
|
|
9
|
+
//
|
|
10
|
+
// CSS completes the handshake before any of this runs, so a client that may not
|
|
11
|
+
// listen is closed rather than refused — there is no status code left to send.
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.FediPodStreamingHandler = void 0;
|
|
14
|
+
const community_server_1 = require("@solid/community-server");
|
|
15
|
+
const STREAMING_PATH = '/api/v1/streaming';
|
|
16
|
+
class FediPodStreamingHandler extends community_server_1.WebSocketHandler {
|
|
17
|
+
server;
|
|
18
|
+
logger = (0, community_server_1.getLoggerFor)(this);
|
|
19
|
+
constructor(args) {
|
|
20
|
+
super();
|
|
21
|
+
this.server = args.server;
|
|
22
|
+
}
|
|
23
|
+
async canHandle({ upgradeRequest }) {
|
|
24
|
+
const host = upgradeRequest.headers.host;
|
|
25
|
+
const { pathname } = new URL(upgradeRequest.url ?? '/', `http://${host}`);
|
|
26
|
+
if (!pathname.startsWith(STREAMING_PATH) || !this.server.surfaceFor(host)) {
|
|
27
|
+
throw new Error('not a FediPod streaming socket');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async handle({ webSocket, upgradeRequest }) {
|
|
31
|
+
const identity = this.server.surfaceFor(upgradeRequest.headers.host);
|
|
32
|
+
const streaming = identity?.surface?.streaming;
|
|
33
|
+
if (!streaming) {
|
|
34
|
+
webSocket.close(1011, 'identity not running');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const url = new URL(upgradeRequest.url ?? '/', `http://${upgradeRequest.headers.host}`);
|
|
38
|
+
const refusal = streaming.authorizeUpgrade(upgradeRequest, url);
|
|
39
|
+
if (refusal) {
|
|
40
|
+
this.logger.info(refusal);
|
|
41
|
+
webSocket.close(1008, refusal);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
streaming.adopt(webSocket);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.FediPodStreamingHandler = FediPodStreamingHandler;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@context": [
|
|
3
|
+
"https://linkedsoftwaredependencies.org/bundles/npm/fedipod-server/^0.0.0/components/context.jsonld",
|
|
4
|
+
"https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld"
|
|
5
|
+
],
|
|
6
|
+
"@id": "npmd:fedipod-server",
|
|
7
|
+
"components": [
|
|
8
|
+
{
|
|
9
|
+
"@id": "fps:dist/streaming-handler.jsonld#FediPodStreamingHandler",
|
|
10
|
+
"@type": "Class",
|
|
11
|
+
"requireElement": "FediPodStreamingHandler",
|
|
12
|
+
"extends": [
|
|
13
|
+
"css:dist/server/WebSocketHandler.jsonld#WebSocketHandler"
|
|
14
|
+
],
|
|
15
|
+
"parameters": [
|
|
16
|
+
{
|
|
17
|
+
"@id": "fps:dist/streaming-handler.jsonld#FediPodStreamingHandler_args_server",
|
|
18
|
+
"range": "fps:dist/handler.jsonld#FediPodServerHandler",
|
|
19
|
+
"comment": "The server handler, which owns the running identities."
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"memberFields": [
|
|
23
|
+
{
|
|
24
|
+
"@id": "fps:dist/streaming-handler.jsonld#FediPodStreamingHandler__member_server",
|
|
25
|
+
"memberFieldName": "server"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"@id": "fps:dist/streaming-handler.jsonld#FediPodStreamingHandler__member_logger",
|
|
29
|
+
"memberFieldName": "logger"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"@id": "fps:dist/streaming-handler.jsonld#FediPodStreamingHandler__member_constructor",
|
|
33
|
+
"memberFieldName": "constructor"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"@id": "fps:dist/streaming-handler.jsonld#FediPodStreamingHandler__member_canHandle",
|
|
37
|
+
"memberFieldName": "canHandle"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"@id": "fps:dist/streaming-handler.jsonld#FediPodStreamingHandler__member_handle",
|
|
41
|
+
"memberFieldName": "handle"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"constructorArguments": [
|
|
45
|
+
{
|
|
46
|
+
"@id": "fps:dist/streaming-handler.jsonld#FediPodStreamingHandler_args__constructorArgument",
|
|
47
|
+
"fields": [
|
|
48
|
+
{
|
|
49
|
+
"keyRaw": "server",
|
|
50
|
+
"value": {
|
|
51
|
+
"@id": "fps:dist/streaming-handler.jsonld#FediPodStreamingHandler_args_server"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"@id": "fps:dist/streaming-handler.jsonld#FediPodStreamingArgs",
|
|
60
|
+
"@type": "AbstractClass",
|
|
61
|
+
"requireElement": "FediPodStreamingArgs",
|
|
62
|
+
"parameters": [],
|
|
63
|
+
"memberFields": [
|
|
64
|
+
{
|
|
65
|
+
"@id": "fps:dist/streaming-handler.jsonld#FediPodStreamingArgs__member_server",
|
|
66
|
+
"memberFieldName": "server"
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"constructorArguments": []
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1620 1140" width="1620" height="1140" font-family="system-ui, sans-serif" role="img" aria-labelledby="ttl dsc">
|
|
2
|
+
<title id="ttl">FediPod Server, a Community Solid Server component</title>
|
|
3
|
+
<desc id="dsc">One Community Solid Server hosting many pods, with the FediPod component installed, and down the right-hand side everyone who talks to it. The open social web delivers mail and looks things up: some of that reaches the component, and the rest reaches pod storage directly, because a pod's public record and its inbox are ordinary pod resources the server already serves. Mei's phone and Tamara's laptop sign in, read and post; any authorized Solid app reads and writes the pods directly, except the paths the component claims. Inside, the component is the same FediPod Agent running in the server's own process, in two parts. The endpoints it serves are, on each pod's own address, the client API an app signs in to, the owner's pages behind that owner's own secret, and the live feed; and on the shared address, the front, and the door that verifies deliveries for the handles it fronts. The work it does for each pod is accepting a follow, delivering a post, emptying the inbox, and holding the lease so only one agent at a time acts. Below, server storage holds, outside every pod, each identity's signing key and door secret, plus a handle directory and the opt-in list when the server serves other people; an identity's log goes to the server's own log. Pod storage is one pod per identity, each holding that identity's public face, its inbox and its private state, written directly with no credential and no second connection, and a pod owner opts in from there.</desc>
|
|
4
|
+
<style>
|
|
5
|
+
.t { font-size: 18px; font-weight: 600; fill: #1a1a19; }
|
|
6
|
+
.d { font-size: 16px; fill: #333; }
|
|
7
|
+
.sub { font-size: 15px; fill: #666; }
|
|
8
|
+
.lbl { font-size: 15px; fill: #555; }
|
|
9
|
+
.cc { font-size: 11px; font-weight: 400; font-style: italic; fill: #555; }
|
|
10
|
+
.arrow { stroke: #555; stroke-width: 2; fill: none; marker-end: url(#arr); }
|
|
11
|
+
.arrow2 { stroke: #555; stroke-width: 2; fill: none; marker-end: url(#arr); marker-start: url(#arr); }
|
|
12
|
+
</style>
|
|
13
|
+
<defs>
|
|
14
|
+
<marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
15
|
+
<path d="M0,0 L10,5 L0,10 z" fill="#555"/>
|
|
16
|
+
</marker>
|
|
17
|
+
</defs>
|
|
18
|
+
|
|
19
|
+
<rect width="1620" height="1140" fill="#ffffff"/>
|
|
20
|
+
|
|
21
|
+
<!-- ===== the server ===== -->
|
|
22
|
+
<rect x="40" y="40" width="1060" height="1050" rx="12" fill="#f7f6f2" stroke="#9a988f"/>
|
|
23
|
+
<text class="t" x="570" y="80" text-anchor="middle">One Community Solid Server, hosting many pods, with the FediPod component installed</text>
|
|
24
|
+
<text class="sub" x="570" y="104" text-anchor="middle">the pods it already served, and a fediverse identity for each user who opts in</text>
|
|
25
|
+
|
|
26
|
+
<!-- ===== processing ===== -->
|
|
27
|
+
<rect x="64" y="128" width="1012" height="518" rx="12" fill="#fdeeee" stroke="#b87a7a"/>
|
|
28
|
+
<text class="t" x="570" y="164" text-anchor="middle">Processing — the same FediPod Agent, running inside the server</text>
|
|
29
|
+
|
|
30
|
+
<text class="sub" x="88" y="196">the endpoints it serves, on each pod's own address</text>
|
|
31
|
+
<rect x="88" y="206" width="468" height="68" rx="10" fill="#ffffff" stroke="#c9c7bf"/>
|
|
32
|
+
<text class="d" x="106" y="234">The client API</text>
|
|
33
|
+
<text class="sub" x="106" y="256">what a Mastodon app signs in to</text>
|
|
34
|
+
<rect x="88" y="286" width="468" height="68" rx="10" fill="#ffffff" stroke="#c9c7bf"/>
|
|
35
|
+
<text class="d" x="106" y="314">The owner's pages</text>
|
|
36
|
+
<text class="sub" x="106" y="336">the record and a web client, behind their own secret</text>
|
|
37
|
+
<rect x="88" y="366" width="468" height="68" rx="10" fill="#ffffff" stroke="#c9c7bf"/>
|
|
38
|
+
<text class="d" x="106" y="394">The live feed</text>
|
|
39
|
+
<text class="sub" x="106" y="416">a socket a client holds open</text>
|
|
40
|
+
|
|
41
|
+
<text class="sub" x="88" y="464">and on the shared address</text>
|
|
42
|
+
<rect x="88" y="474" width="468" height="68" rx="10" fill="#ffffff" stroke="#c9c7bf"/>
|
|
43
|
+
<text class="d" x="106" y="502">The front</text>
|
|
44
|
+
<text class="sub" x="106" y="524">signup, name lookups and attach</text>
|
|
45
|
+
<rect x="88" y="554" width="468" height="68" rx="10" fill="#ffffff" stroke="#c9c7bf"/>
|
|
46
|
+
<text class="d" x="106" y="582">The door</text>
|
|
47
|
+
<text class="sub" x="106" y="604">verifies deliveries for the handles it fronts</text>
|
|
48
|
+
|
|
49
|
+
<text class="sub" x="584" y="196">the work it does, for each pod it runs</text>
|
|
50
|
+
<rect x="584" y="206" width="468" height="68" rx="10" fill="#ffffff" stroke="#c9c7bf"/>
|
|
51
|
+
<text class="d" x="602" y="234">Accepts a follow</text>
|
|
52
|
+
<text class="sub" x="602" y="256">answers as that pod's owner</text>
|
|
53
|
+
<rect x="584" y="286" width="468" height="68" rx="10" fill="#ffffff" stroke="#c9c7bf"/>
|
|
54
|
+
<text class="d" x="602" y="314">Delivers a post</text>
|
|
55
|
+
<text class="sub" x="602" y="336">signs it and sends it on</text>
|
|
56
|
+
<rect x="584" y="366" width="468" height="68" rx="10" fill="#ffffff" stroke="#c9c7bf"/>
|
|
57
|
+
<text class="d" x="602" y="394">Empties the inbox</text>
|
|
58
|
+
<text class="sub" x="602" y="416">reads what arrived, then clears it</text>
|
|
59
|
+
<rect x="584" y="446" width="468" height="68" rx="10" fill="#ffffff" stroke="#c9c7bf"/>
|
|
60
|
+
<text class="d" x="602" y="474">Holds the lease</text>
|
|
61
|
+
<text class="sub" x="602" y="496">so only one agent at a time acts on a pod</text>
|
|
62
|
+
|
|
63
|
+
<!-- ===== processing <-> storage ===== -->
|
|
64
|
+
<path class="arrow" d="M 298 648 L 298 742"/>
|
|
65
|
+
<text class="lbl" x="286" y="678" text-anchor="end">keeps each identity's</text>
|
|
66
|
+
<text class="lbl" x="286" y="700" text-anchor="end">signing key and door secret</text>
|
|
67
|
+
|
|
68
|
+
<path class="arrow" d="M 798 648 L 798 742"/>
|
|
69
|
+
<text class="lbl" x="786" y="678" text-anchor="end">writes directly to each user's pod</text>
|
|
70
|
+
<text class="lbl" x="786" y="700" text-anchor="end">no credential, no second connection</text>
|
|
71
|
+
|
|
72
|
+
<path class="arrow" d="M 838 742 L 838 648"/>
|
|
73
|
+
<text class="lbl" x="850" y="678">user opts in to have a</text>
|
|
74
|
+
<text class="lbl" x="850" y="700">fediverse account on their pod</text>
|
|
75
|
+
|
|
76
|
+
<!-- ===== server storage ===== -->
|
|
77
|
+
<rect x="64" y="746" width="468" height="320" rx="12" fill="#f2ece0" stroke="#b09a5e"/>
|
|
78
|
+
<text class="t" x="298" y="782" text-anchor="middle">Server storage</text>
|
|
79
|
+
<text class="sub" x="298" y="806" text-anchor="middle">on the server, outside every pod</text>
|
|
80
|
+
<text class="sub" x="298" y="826" text-anchor="middle">an identity's log goes to the server's own log</text>
|
|
81
|
+
<rect x="88" y="852" width="420" height="68" rx="10" fill="#ffffff" stroke="#c9c7bf"/>
|
|
82
|
+
<text class="d" x="106" y="880">A signing key per identity</text>
|
|
83
|
+
<text class="sub" x="106" y="902">what it means to be able to post as them</text>
|
|
84
|
+
<rect x="88" y="932" width="420" height="68" rx="10" fill="#ffffff" stroke="#c9c7bf"/>
|
|
85
|
+
<text class="d" x="106" y="960">A door secret per identity</text>
|
|
86
|
+
<text class="sub" x="106" y="982">opens that identity's pages, nobody else's</text>
|
|
87
|
+
<rect x="88" y="1012" width="420" height="46" rx="10" fill="#ffffff" stroke="#c9c7bf" stroke-dasharray="4 4"/>
|
|
88
|
+
<text class="sub" x="106" y="1040">a handle directory and the opt-in list, if it serves others</text>
|
|
89
|
+
|
|
90
|
+
<!-- ===== pod storage ===== -->
|
|
91
|
+
<rect x="584" y="746" width="468" height="320" rx="12" fill="#e7f2e7" stroke="#6a9a6a"/>
|
|
92
|
+
<text class="t" x="818" y="782" text-anchor="middle">Pod storage — one pod per identity</text>
|
|
93
|
+
<text class="sub" x="818" y="806" text-anchor="middle">addressable by Solid and by ActivityPub</text>
|
|
94
|
+
<rect x="608" y="832" width="420" height="68" rx="10" fill="#ffffff" stroke="#c9c7bf"/>
|
|
95
|
+
<text class="d" x="626" y="860">mei.example.org</text>
|
|
96
|
+
<text class="sub" x="626" y="882">public face, inbox, and her agent's private state</text>
|
|
97
|
+
<rect x="608" y="912" width="420" height="68" rx="10" fill="#ffffff" stroke="#c9c7bf"/>
|
|
98
|
+
<text class="d" x="626" y="940">tamara.example.org</text>
|
|
99
|
+
<text class="sub" x="626" y="962">the same again, hers alone</text>
|
|
100
|
+
<rect x="608" y="992" width="420" height="46" rx="10" fill="#ffffff" stroke="#c9c7bf" stroke-dasharray="4 4"/>
|
|
101
|
+
<text class="sub" x="626" y="1020">and every other pod this server hosts, untouched</text>
|
|
102
|
+
|
|
103
|
+
<!-- ===== everyone who talks to it ===== -->
|
|
104
|
+
<rect x="1160" y="170" width="340" height="104" rx="12" fill="#f0e9f5" stroke="#9a7ab0"/>
|
|
105
|
+
<text class="t" x="1330" y="206" text-anchor="middle">Mei's phone</text>
|
|
106
|
+
<text class="d" x="1330" y="234" text-anchor="middle">any mobile Mastodon client</text>
|
|
107
|
+
<text class="d" x="1330" y="256" text-anchor="middle">signs in, reads, posts</text>
|
|
108
|
+
<path class="arrow2" d="M 1158 222 L 1080 222"/>
|
|
109
|
+
|
|
110
|
+
<rect x="1160" y="310" width="340" height="104" rx="12" fill="#f0e9f5" stroke="#9a7ab0"/>
|
|
111
|
+
<text class="t" x="1330" y="346" text-anchor="middle">Tamara's laptop</text>
|
|
112
|
+
<text class="d" x="1330" y="374" text-anchor="middle">browser or native app</text>
|
|
113
|
+
<text class="d" x="1330" y="396" text-anchor="middle">signs in, reads, posts</text>
|
|
114
|
+
<path class="arrow2" d="M 1158 362 L 1080 362"/>
|
|
115
|
+
|
|
116
|
+
<rect x="1160" y="450" width="340" height="126" rx="12" fill="#eef1f8" stroke="#7a88b8"/>
|
|
117
|
+
<text class="t" x="1330" y="486" text-anchor="middle">The open social web</text>
|
|
118
|
+
<text class="d" x="1330" y="514" text-anchor="middle">Mastodon, Lemmy, PeerTube,</text>
|
|
119
|
+
<text class="d" x="1330" y="536" text-anchor="middle">anyone else</text>
|
|
120
|
+
<text class="d" x="1330" y="558" text-anchor="middle">delivers mail, and looks things up</text>
|
|
121
|
+
<path class="arrow2" d="M 1158 513 L 1080 513"/>
|
|
122
|
+
<path class="arrow2" d="M 1330 578 L 1330 906 L 1058 906"/>
|
|
123
|
+
<text class="lbl" x="1348" y="734">to the pod — mail into its inbox</text>
|
|
124
|
+
<text class="lbl" x="1348" y="756">from the pod — its public record</text>
|
|
125
|
+
|
|
126
|
+
<rect x="1160" y="940" width="340" height="126" rx="12" fill="#f1efe8" stroke="#9a988f"/>
|
|
127
|
+
<text class="t" x="1330" y="976" text-anchor="middle">Any authorized Solid app</text>
|
|
128
|
+
<text class="d" x="1330" y="1004" text-anchor="middle">reads and writes the pod,</text>
|
|
129
|
+
<text class="d" x="1330" y="1026" text-anchor="middle">except the paths the</text>
|
|
130
|
+
<text class="d" x="1330" y="1048" text-anchor="middle">component claims</text>
|
|
131
|
+
<path class="arrow2" d="M 1158 1003 L 1056 1003"/>
|
|
132
|
+
|
|
133
|
+
<text class="cc" x="1580" y="1120" text-anchor="end">(cc) 4.0 By, Jeff Zucker, 2026</text>
|
|
134
|
+
</svg>
|
package/lib/account.mjs
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// account.mjs — create a brand-new account + pod on a CSS server (v7 account
|
|
2
|
+
// API), the `setup --new-account` path. Public-server sibling of
|
|
3
|
+
// data-kitchen's seed-account.cjs, but simpler: the pod.create control makes
|
|
4
|
+
// the pod AND its WebID in one step — no ownership challenge, because the
|
|
5
|
+
// server authors the WebID itself.
|
|
6
|
+
//
|
|
7
|
+
// Flow: POST .account/account/ (or login when the email already exists) →
|
|
8
|
+
// password.create → account.pod {name} → { pod, webId }.
|
|
9
|
+
|
|
10
|
+
async function jfetch(url, { method = 'GET', body, cookie } = {}) {
|
|
11
|
+
const headers = { accept: 'application/json' };
|
|
12
|
+
if (body !== undefined) headers['content-type'] = 'application/json';
|
|
13
|
+
if (cookie) headers.cookie = cookie;
|
|
14
|
+
const res = await fetch(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
15
|
+
let json = null;
|
|
16
|
+
try { json = await res.json(); } catch { /* non-JSON */ }
|
|
17
|
+
return { status: res.status, json };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function createAccountWithPod({ issuer, email, password, podName }) {
|
|
21
|
+
const origin = issuer.replace(/\/+$/, '');
|
|
22
|
+
const accountRoot = `${origin}/.account/`;
|
|
23
|
+
|
|
24
|
+
// Authenticate: log in when the email is already registered (idempotent
|
|
25
|
+
// re-runs), otherwise create the account and set its password.
|
|
26
|
+
let cookie;
|
|
27
|
+
const login = await jfetch(`${accountRoot}login/password/`, { method: 'POST', body: { email, password } });
|
|
28
|
+
if (login.status < 400 && login.json?.authorization) {
|
|
29
|
+
cookie = `css-account=${login.json.authorization}`;
|
|
30
|
+
} else {
|
|
31
|
+
const create = await jfetch(`${accountRoot}account/`, { method: 'POST' });
|
|
32
|
+
if (!create.json?.authorization) {
|
|
33
|
+
throw new Error(`account create failed (HTTP ${create.status}): ${create.json?.message || 'is this a CSS server with signup enabled?'}`);
|
|
34
|
+
}
|
|
35
|
+
cookie = `css-account=${create.json.authorization}`;
|
|
36
|
+
const pwCreate = (await jfetch(accountRoot, { cookie })).json?.controls?.password?.create;
|
|
37
|
+
if (!pwCreate) throw new Error('password.create control missing — not a CSS v7 account API');
|
|
38
|
+
const pw = await jfetch(pwCreate, { method: 'POST', cookie, body: { email, password } });
|
|
39
|
+
if (pw.status >= 400) throw new Error(`password create failed (HTTP ${pw.status}): ${pw.json?.message || ''}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Create the pod (server picks root-path or subdomain layout). When the
|
|
43
|
+
// name is already taken BY THIS ACCOUNT — a setup re-run, or a pod made
|
|
44
|
+
// earlier by hand — reuse it; taken by someone else stays a hard error.
|
|
45
|
+
const controls = (await jfetch(accountRoot, { cookie })).json?.controls;
|
|
46
|
+
const podCreate = controls?.account?.pod;
|
|
47
|
+
if (!podCreate) throw new Error('pod create control missing — not a CSS v7 account API');
|
|
48
|
+
// Scheme-agnostic on purpose: matching `https://name.` missed every pod on an
|
|
49
|
+
// http server, so a re-run after a crashed setup always tried to create again
|
|
50
|
+
// and died on "already registered to this account".
|
|
51
|
+
const findOwn = async () => {
|
|
52
|
+
const pods = (await jfetch(podCreate, { cookie })).json?.pods || {};
|
|
53
|
+
return Object.entries(pods).find(([url]) => {
|
|
54
|
+
try {
|
|
55
|
+
const u = new URL(url);
|
|
56
|
+
return u.hostname === podName || u.hostname.startsWith(`${podName}.`)
|
|
57
|
+
|| u.pathname.split('/').filter(Boolean).includes(podName);
|
|
58
|
+
} catch { return false; }
|
|
59
|
+
}) || null;
|
|
60
|
+
};
|
|
61
|
+
// One pod per account, refused BEFORE anything is created. A second pod gives
|
|
62
|
+
// the account a second WebID, and a credential can only be bound to one of
|
|
63
|
+
// them — so the second actor authenticates and then 403s every write to its
|
|
64
|
+
// own pod. Reusing a pod of the same name is still fine: that is a re-run of
|
|
65
|
+
// a setup, not a second identity.
|
|
66
|
+
const owned = Object.keys((await jfetch(podCreate, { cookie })).json?.pods || {});
|
|
67
|
+
if (owned.length && !await findOwn()) {
|
|
68
|
+
throw new Error(`${email} already has a pod on ${new URL(origin).host}: ${owned[0]} — `
|
|
69
|
+
+ 'one account, one pod. A second actor needs its own account and email. '
|
|
70
|
+
+ 'Nothing was created.');
|
|
71
|
+
}
|
|
72
|
+
const made = await jfetch(podCreate, { method: 'POST', cookie, body: { name: podName } });
|
|
73
|
+
let pod = made.json?.pod || made.json?.podBaseUrl || null;
|
|
74
|
+
let webId = made.json?.webId || null;
|
|
75
|
+
if (made.status >= 400 || !pod) {
|
|
76
|
+
const own = await findOwn();
|
|
77
|
+
if (own) {
|
|
78
|
+
pod = own[0];
|
|
79
|
+
webId = webId || own[1]?.webId || null;
|
|
80
|
+
} else if (made.status >= 400) {
|
|
81
|
+
throw new Error(`pod create failed (HTTP ${made.status}): ${made.json?.message || `is the name "${podName}" taken by another account?`}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!pod) throw new Error('pod created but its URL was not reported — check the account dashboard');
|
|
85
|
+
pod = pod.endsWith('/') ? pod : pod + '/';
|
|
86
|
+
return { pod, webId: webId || new URL('profile/card#me', pod).href };
|
|
87
|
+
}
|