fedipod 0.18.0 → 0.19.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 +20 -0
- package/bin/fedipod.mjs +3 -5
- package/cli.md +13 -0
- package/groups.md +11 -0
- package/gui.md +15 -1
- package/lib/admin.mjs +3 -3
- package/lib/deliver.mjs +6 -0
- package/lib/export-collections.mjs +1 -1
- package/lib/front-core.mjs +272 -49
- package/lib/gateway-core.mjs +3 -7
- package/lib/httpsig.mjs +33 -2
- package/lib/import.mjs +1 -1
- package/lib/intake.mjs +335 -118
- package/lib/links.mjs +6 -32
- package/lib/mastoapi.mjs +372 -35
- package/lib/migrate.mjs +4 -18
- package/lib/pod/README.md +61 -0
- package/lib/pod/actor.mjs +96 -0
- package/lib/pod/collection.mjs +77 -0
- package/lib/pod/containers.mjs +136 -0
- package/lib/pod/discovery.mjs +46 -0
- package/lib/pod/featured.mjs +18 -0
- package/lib/pod/followers.mjs +24 -0
- package/lib/pod/following.mjs +10 -0
- package/lib/pod/http.mjs +74 -0
- package/lib/pod/inbox.mjs +144 -0
- package/lib/pod/links.mjs +35 -0
- package/lib/pod/media.mjs +27 -0
- package/lib/pod/notes.mjs +80 -0
- package/lib/pod/notifications.mjs +89 -0
- package/lib/pod/outbox.mjs +18 -0
- package/lib/pod/package.json +19 -0
- package/lib/pod/policy.mjs +58 -0
- package/lib/pod/private.mjs +19 -0
- package/lib/pod/root.mjs +63 -0
- package/lib/pod/state.mjs +39 -0
- package/lib/pod/transport.mjs +477 -0
- package/lib/pod/urls.mjs +71 -0
- package/lib/publisher.mjs +112 -183
- package/lib/remote.mjs +30 -395
- package/lib/social.mjs +18 -9
- package/lib/storage.mjs +2 -2
- package/lib/store.mjs +8 -1
- package/lib/tagfeed.mjs +19 -2
- package/lib/wire.mjs +48 -67
- package/package.json +9 -2
- package/run-agent.mjs +14 -39
- package/scripts/build-app.mjs +64 -0
- package/scripts/check-pod-calls.mjs +177 -0
- package/scripts/stage-site.mjs +144 -0
- package/web/admin/admin.js +141 -74
- package/web/admin/bar.css +9 -13
- package/web/admin/bar.js +2 -2
- package/web/admin/client/client.js +1 -1
- package/web/admin/client/index.html +5 -6
- package/web/admin/index.html +53 -65
- package/web/admin/setup/index.html +2 -1
- package/web/admin/setup/setup.js +1 -1
- package/web/admin/tokens.css +56 -0
- package/web/app/README.md +77 -0
- package/web/app/admin-facade.mjs +549 -0
- package/web/app/agent.mjs +490 -0
- package/web/app/atproto-browser.mjs +86 -0
- package/web/app/boot.mjs +340 -0
- package/web/app/deliver-relay.mjs +72 -0
- package/web/app/dist/boot.js +34067 -0
- package/web/app/dist/boot.js.map +7 -0
- package/web/app/dist/sw.js +53745 -0
- package/web/app/dist/sw.js.map +7 -0
- package/web/app/fediacct-browser.mjs +146 -0
- package/web/app/idb-kv.mjs +35 -0
- package/web/app/index.html +203 -0
- package/web/app/index.html~ +152 -0
- package/web/app/keys-browser.mjs +46 -0
- package/web/app/keystore.mjs +99 -0
- package/web/app/oidc-session.mjs +189 -0
- package/web/app/pod-auth.mjs +248 -0
- package/web/app/pod-remote.mjs +84 -0
- package/web/app/shims/_globals.mjs +2 -0
- package/web/app/shims/fedify-sig.mjs +77 -0
- package/web/app/shims/node-crypto.mjs +113 -0
- package/web/app/shims/node-fs.mjs +17 -0
- package/web/app/shims/node-path.mjs +19 -0
- package/web/app/shims/node-url.mjs +7 -0
- package/web/app/shims/prelude.js +58 -0
- package/web/app/shims/safefetch.mjs +58 -0
- package/web/app/shims/web-push.mjs +14 -0
- package/web/app/signup.mjs +248 -0
- package/web/app/site/_headers +9 -0
- package/web/app/site/_redirects +21 -0
- package/web/app/site/admin/admin.js +1181 -0
- package/web/app/site/admin/bar.css +60 -0
- package/web/app/site/admin/bar.js +61 -0
- package/web/app/site/admin/client/client.js +66 -0
- package/web/app/site/admin/client/index.html +43 -0
- package/web/app/site/admin/index.html +556 -0
- package/web/app/site/admin/setup/index.html +191 -0
- package/web/app/site/admin/setup/setup.js +308 -0
- package/web/app/site/admin/tokens.css +56 -0
- package/web/app/site/admin/window.css +55 -0
- package/web/app/site/admin/window.js +136 -0
- package/web/app/site/app/404.html +40 -0
- package/web/app/site/app/_headers +2 -0
- package/web/app/site/app/apple-touch-icon.png +0 -0
- package/web/app/site/app/assets/ICONS-DIPJeU0C.js +2 -0
- package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +1 -0
- package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +183 -0
- package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +1 -0
- package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
- package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +2 -0
- package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +1 -0
- package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
- package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
- package/web/app/site/app/assets/chunk-aKtaBQYM.js +1 -0
- package/web/app/site/app/assets/compose-CFEx7wb4.js +2 -0
- package/web/app/site/app/assets/compose-CFEx7wb4.js.map +1 -0
- package/web/app/site/app/assets/compose-ZJN5xSb4.js +35 -0
- package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +1 -0
- package/web/app/site/app/assets/debug.module-YFQgwnIr.js +52 -0
- package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +1 -0
- package/web/app/site/app/assets/dom-p1YcJ5Rw.js +2 -0
- package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +1 -0
- package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
- package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
- package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
- package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
- package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +1 -0
- package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +1 -0
- package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +1 -0
- package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +1 -0
- package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +1 -0
- package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +1 -0
- package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +1 -0
- package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +1 -0
- package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +1 -0
- package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +1 -0
- package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +1 -0
- package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +1 -0
- package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +1 -0
- package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +1 -0
- package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +1 -0
- package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +1 -0
- package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +1 -0
- package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +1 -0
- package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +1 -0
- package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +1 -0
- package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +1 -0
- package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +1 -0
- package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +1 -0
- package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +1 -0
- package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +1 -0
- package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +1 -0
- package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +1 -0
- package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +1 -0
- package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +1 -0
- package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +1 -0
- package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +1 -0
- package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +1 -0
- package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +1 -0
- package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +1 -0
- package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +1 -0
- package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +1 -0
- package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +1 -0
- package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +1 -0
- package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +1 -0
- package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +1 -0
- package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +1 -0
- package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +1 -0
- package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +1 -0
- package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +1 -0
- package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +1 -0
- package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +1 -0
- package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +1 -0
- package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +1 -0
- package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +1 -0
- package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +1 -0
- package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +1 -0
- package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +1 -0
- package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +1 -0
- package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +1 -0
- package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +1 -0
- package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +1 -0
- package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +1 -0
- package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +1 -0
- package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +1 -0
- package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +1 -0
- package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +1 -0
- package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +1 -0
- package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +1 -0
- package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +1 -0
- package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +1 -0
- package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +1 -0
- package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +1 -0
- package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +1 -0
- package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +1 -0
- package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +1 -0
- package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +1 -0
- package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +1 -0
- package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +1 -0
- package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +1 -0
- package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +1 -0
- package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +1 -0
- package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +1 -0
- package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +1 -0
- package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +1 -0
- package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +1 -0
- package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +1 -0
- package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +1 -0
- package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +1 -0
- package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +1 -0
- package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +1 -0
- package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +1 -0
- package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +1 -0
- package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +1 -0
- package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +1 -0
- package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +1 -0
- package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +1 -0
- package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +1 -0
- package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +1 -0
- package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +1 -0
- package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +1 -0
- package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +1 -0
- package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +1 -0
- package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +1 -0
- package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +1 -0
- package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +1 -0
- package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +1 -0
- package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +1 -0
- package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +1 -0
- package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +1 -0
- package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +1 -0
- package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +1 -0
- package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +1 -0
- package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +1 -0
- package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +1 -0
- package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +1 -0
- package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +1 -0
- package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +1 -0
- package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +1 -0
- package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +1 -0
- package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +1 -0
- package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +1 -0
- package/web/app/site/app/assets/instances-BNPptnn-.json +333 -0
- package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +1 -0
- package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +1 -0
- package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +1 -0
- package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +1 -0
- package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +1 -0
- package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +1 -0
- package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +1 -0
- package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +1 -0
- package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +1 -0
- package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +1 -0
- package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +1 -0
- package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +1 -0
- package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +1 -0
- package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +1 -0
- package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +1 -0
- package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +1 -0
- package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +1 -0
- package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +1 -0
- package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +1 -0
- package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +1 -0
- package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +1 -0
- package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/tok-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +1 -0
- package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +1 -0
- package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +1 -0
- package/web/app/site/app/assets/main-BdqNbG-a.js +26 -0
- package/web/app/site/app/assets/main-BdqNbG-a.js.map +1 -0
- package/web/app/site/app/assets/mock-home-DGhR__He.js +2 -0
- package/web/app/site/app/assets/mock-home-DGhR__He.js.map +1 -0
- package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
- package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
- package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
- package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +2 -0
- package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +1 -0
- package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +20 -0
- package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +1 -0
- package/web/app/site/app/assets/style-B1iLPTPR.css +2 -0
- package/web/app/site/app/assets/temml-DuyIQMei.js +97 -0
- package/web/app/site/app/assets/temml-DuyIQMei.js.map +1 -0
- package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +2 -0
- package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +1 -0
- package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
- package/web/app/site/app/assets/year-in-posts-DJv_star.js +3 -0
- package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +1 -0
- package/web/app/site/app/compose/index.html +20 -0
- package/web/app/site/app/favicon.ico +0 -0
- package/web/app/site/app/index.html +73 -0
- package/web/app/site/app/logo-192.png +0 -0
- package/web/app/site/app/logo-512.png +0 -0
- package/web/app/site/app/logo-badge-72.png +0 -0
- package/web/app/site/app/logo-maskable-512.png +0 -0
- package/web/app/site/app/logo-monochrome-512.png +0 -0
- package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
- package/web/app/site/app/manifest.webmanifest +1 -0
- package/web/app/site/app/og-image-2.jpg +0 -0
- package/web/app/site/app/og-image.png +0 -0
- package/web/app/site/app/robots.txt +2 -0
- package/web/app/site/app/version.json +1 -0
- package/web/app/site/boot.js +34067 -0
- package/web/app/site/index.html +203 -0
- package/web/app/site/sw.js +53745 -0
- package/web/app/sw-src.mjs +205 -0
- package/web/front/#new-account.html# +43 -0
- package/web/front/admin.html +1 -81
- package/web/front/admin.js +85 -0
- package/web/front/new-account.html +1 -9
- package/web/front/new-account.js +13 -0
- package/web/front/run.html +1 -92
- package/web/front/run.js +107 -0
- package/lib/podrdf.mjs +0 -132
package/lib/wire.mjs
CHANGED
|
@@ -6,75 +6,40 @@ export const AS_CTX = 'https://www.w3.org/ns/activitystreams';
|
|
|
6
6
|
export const SEC_CTX = 'https://w3id.org/security/v1';
|
|
7
7
|
export const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// the
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
9
|
+
// The pod LAYOUT — which container each document lives in — moved into the pod
|
|
10
|
+
// library (lib/pod/urls.mjs), which knows nothing about FediPod and requires
|
|
11
|
+
// the container root to be stated rather than defaulted. What stays here is
|
|
12
|
+
// FediPod's own answer to "which root", and the handle rule, both of which read
|
|
13
|
+
// this application's config shape.
|
|
14
|
+
import { apUrls as podUrls } from './pod/urls.mjs';
|
|
15
|
+
|
|
16
|
+
export { webfingerHost } from './pod/urls.mjs';
|
|
17
|
+
|
|
18
|
+
// The container the Node agent has always used. Older than the rename to
|
|
19
|
+
// FediPod and kept because it is what every existing Node-side pod actually
|
|
20
|
+
// contains; the browser build states `fedipod/` on its own configs. A config
|
|
21
|
+
// that names a root is always believed — this is only the answer for one that
|
|
22
|
+
// does not.
|
|
23
|
+
export const DEFAULT_ROOT = 'activitypods-js/';
|
|
24
|
+
|
|
24
25
|
// The handle the fediverse sees: a fronted identity's name is the front's.
|
|
25
26
|
export function publicHandle(config) {
|
|
26
27
|
return config?.gateway?.frontActor?.match(/\/u\/([^/]+)\/ap\/actor\/?$/)?.[1]
|
|
27
28
|
|| config?.handle || null;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
following: face + 'ap/following',
|
|
43
|
-
notes: face + 'ap/notes/',
|
|
44
|
-
privateNotes: face + 'ap/private/',
|
|
45
|
-
featured: face + 'ap/featured',
|
|
46
|
-
// FEP-1b12: the moderator roster a recipient validates announced
|
|
47
|
-
// moderation against. Published only when moderators are configured.
|
|
48
|
-
moderators: face + 'ap/moderators',
|
|
49
|
-
// FEP-4ccd and FEP-c648: follows in limbo and the block list, as
|
|
50
|
-
// collections. They live in the private container so the owner-only ACL
|
|
51
|
-
// is inherited, not re-stated per document.
|
|
52
|
-
pendingFollowers: face + 'ap/private/pending-followers',
|
|
53
|
-
pendingFollowing: face + 'ap/private/pending-following',
|
|
54
|
-
blocked: face + 'ap/private/blocked',
|
|
55
|
-
profileHtml: face + 'ap/profile.html',
|
|
56
|
-
// Media stays on the pod even when fronted: attachment urls are not
|
|
57
|
-
// identity-checked by remotes, and proxying blobs would be pure cost.
|
|
58
|
-
media: home + 'ap/media/',
|
|
59
|
-
fediverse: home + 'fediverse/',
|
|
60
|
-
state: home + 'ap-state/',
|
|
61
|
-
};
|
|
62
|
-
if (publicBase) {
|
|
63
|
-
urls.podHome = home;
|
|
64
|
-
urls.publicHome = face;
|
|
65
|
-
urls.toPod = (u) => (typeof u === 'string' && u.startsWith(face) ? home + u.slice(face.length) : u);
|
|
66
|
-
urls.toPublic = (u) => (typeof u === 'string' && u.startsWith(home) ? face + u.slice(home.length) : u);
|
|
67
|
-
}
|
|
68
|
-
return urls;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// The host a handle would resolve through, or null when no handle can point
|
|
72
|
-
// here. @name@host is looked up at https://host/.well-known/webfinger, so only
|
|
73
|
-
// a pod that owns the root of its host can answer for one; a pod living at
|
|
74
|
-
// https://server/name/ may publish the document but nothing will ever ask.
|
|
75
|
-
export function webfingerHost(podUrl) {
|
|
76
|
-
const u = new URL(podUrl);
|
|
77
|
-
return u.pathname === '/' ? u.host : null;
|
|
31
|
+
/**
|
|
32
|
+
* The AP URLs for an identity, with FediPod's default root applied.
|
|
33
|
+
*
|
|
34
|
+
* The library underneath refuses to guess a root. Every caller here goes
|
|
35
|
+
* through this wrapper so there is exactly ONE answer to "which container",
|
|
36
|
+
* which is the thing that was wrong: the browser agent decided `fedipod/` for
|
|
37
|
+
* its own urls while the publisher, handed a config with no `root`, decided
|
|
38
|
+
* `activitypods-js/` — putting an identity's state in one container and its
|
|
39
|
+
* published documents in another.
|
|
40
|
+
*/
|
|
41
|
+
export function apUrls(remotePod, root, opts = {}) {
|
|
42
|
+
return podUrls(remotePod, root || DEFAULT_ROOT, opts);
|
|
78
43
|
}
|
|
79
44
|
|
|
80
45
|
// RFC 6415 host-meta: several fediverse implementations discover WebFinger
|
|
@@ -585,15 +550,31 @@ export function titledContent(note) {
|
|
|
585
550
|
}
|
|
586
551
|
|
|
587
552
|
// Normalize a wire Note's attachment list to { url, mediaType, description }.
|
|
553
|
+
// Capped: this list comes from somebody else's document and lands in a
|
|
554
|
+
// statuses.json row, which is rewritten whole on every change. Twenty is more
|
|
555
|
+
// than any client shows and more than any real post carries.
|
|
556
|
+
const MAX_ATTACHMENTS = 20;
|
|
557
|
+
const MAX_ATTACHMENT_URL = 2048;
|
|
558
|
+
// http(s) only: an attachment URL becomes a client's <img src> or <video src>,
|
|
559
|
+
// and the store has guarded avatars this way all along (safeUrl).
|
|
560
|
+
const attachmentUrl = (u) => {
|
|
561
|
+
if (!u) return null;
|
|
562
|
+
const s = String(u).slice(0, MAX_ATTACHMENT_URL);
|
|
563
|
+
try {
|
|
564
|
+
const p = new URL(s);
|
|
565
|
+
return (p.protocol === 'https:' || p.protocol === 'http:') ? s : null;
|
|
566
|
+
} catch { return null; }
|
|
567
|
+
};
|
|
568
|
+
|
|
588
569
|
export function attachmentsOf(note) {
|
|
589
570
|
const list = Array.isArray(note?.attachment) ? note.attachment : note?.attachment ? [note.attachment] : [];
|
|
590
571
|
// A bare Link carries `href` and no `url` — Lemmy's link posts arrive that
|
|
591
572
|
// way, and reading only `url` dropped the link the post was about.
|
|
592
|
-
return list.map(a => ({
|
|
573
|
+
return list.slice(0, MAX_ATTACHMENTS).map(a => ({
|
|
593
574
|
url: typeof a?.url === 'string' ? a.url : a?.url?.href || (typeof a?.href === 'string' ? a.href : undefined),
|
|
594
|
-
mediaType: a?.mediaType || '',
|
|
595
|
-
...(a?.name ? { description: a.name } : {}),
|
|
596
|
-
})).filter(a => a.url);
|
|
575
|
+
mediaType: String(a?.mediaType || '').slice(0, 128),
|
|
576
|
+
...(a?.name ? { description: String(a.name).slice(0, 1500) } : {}),
|
|
577
|
+
})).map(a => ({ ...a, url: attachmentUrl(a.url) })).filter(a => a.url);
|
|
597
578
|
}
|
|
598
579
|
|
|
599
580
|
const HTML_ESCAPES = { '&': '&', '<': '<', '>': '>' };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fedipod",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Standalone single-actor ActivityPub agent whose wire face, RDF truth and state all live on a Solid pod (CSS). Bundles a Phanpy UI and a Mastodon client-API facade.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,8 +33,12 @@
|
|
|
33
33
|
"install-service": "node bin/fedipod.mjs install-service",
|
|
34
34
|
"uninstall-service": "node bin/fedipod.mjs uninstall-service",
|
|
35
35
|
"prepublishOnly": "npm test",
|
|
36
|
-
"test": "node claude/smoke-tests/agent-smoke.mjs && node claude/smoke-tests/front-hard-test.mjs && node scripts/verify-ui.mjs",
|
|
36
|
+
"test": "node claude/smoke-tests/agent-smoke.mjs && node claude/smoke-tests/front-hard-test.mjs && node scripts/check-pod-calls.mjs && node scripts/verify-ui.mjs",
|
|
37
37
|
"verify-ui": "node scripts/verify-ui.mjs",
|
|
38
|
+
"build:sw": "node scripts/build-app.mjs --entry web/app/sw-src.mjs --out web/app/dist/sw.js",
|
|
39
|
+
"build:boot": "node scripts/build-app.mjs --entry web/app/boot.mjs --out web/app/dist/boot.js",
|
|
40
|
+
"build:app": "npm run build:sw && npm run build:boot",
|
|
41
|
+
"stage": "node scripts/stage-site.mjs",
|
|
38
42
|
"audit": "npm audit --omit=dev",
|
|
39
43
|
"agent": "node run-agent.mjs"
|
|
40
44
|
},
|
|
@@ -57,5 +61,8 @@
|
|
|
57
61
|
"@opentelemetry/core": "^2.8.0",
|
|
58
62
|
"package.json": "npm:empty-npm-package@1.0.0",
|
|
59
63
|
"package-lock.json": "npm:empty-npm-package@1.0.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"esbuild": "^0.28.2"
|
|
60
67
|
}
|
|
61
68
|
}
|
package/run-agent.mjs
CHANGED
|
@@ -34,7 +34,6 @@ import { ensureTrustedTls } from './lib/certs.mjs';
|
|
|
34
34
|
import { storageFor } from './lib/storage.mjs';
|
|
35
35
|
import { resolveKeys } from './lib/keys.mjs';
|
|
36
36
|
import { RemotePod } from './lib/remote.mjs';
|
|
37
|
-
import { PodRdf } from './lib/podrdf.mjs';
|
|
38
37
|
import { Deliverer } from './lib/deliver.mjs';
|
|
39
38
|
import { Publisher } from './lib/publisher.mjs';
|
|
40
39
|
import { Intake } from './lib/intake.mjs';
|
|
@@ -69,15 +68,14 @@ export class Agent {
|
|
|
69
68
|
|
|
70
69
|
// Where the private half lives. `privateRoot` in the credential file names a
|
|
71
70
|
// container — by default a plain directory beside the credential, and a pod
|
|
72
|
-
// on this machine if you move it there
|
|
73
|
-
// laid out as on the pod. Absent means on the pod, exactly as before, so an
|
|
71
|
+
// on this machine if you move it there. Absent means on the pod, so an
|
|
74
72
|
// existing install is untouched.
|
|
75
73
|
privateUrls(cred, urls = this.urls) {
|
|
76
74
|
if (!cred.privateRoot) {
|
|
77
|
-
return { state: urls.state,
|
|
75
|
+
return { state: urls.state, archive: urls.home + 'inbox-archive/', elsewhere: false };
|
|
78
76
|
}
|
|
79
77
|
const base = cred.privateRoot.endsWith('/') ? cred.privateRoot : cred.privateRoot + '/';
|
|
80
|
-
return { state: base + 'ap-state/',
|
|
78
|
+
return { state: base + 'ap-state/', archive: base + 'inbox-archive/', elsewhere: true };
|
|
81
79
|
}
|
|
82
80
|
|
|
83
81
|
// A container to keep the private half in. On the pod it is reached with the
|
|
@@ -146,14 +144,9 @@ export class Agent {
|
|
|
146
144
|
await this.remote.setAcl(this.urls.home, []);
|
|
147
145
|
await this.remote.setAcl(this.urls.state, []);
|
|
148
146
|
if (priv.elsewhere) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (!r.ok) throw new Error(`private store ${s.base}.keep → ${r.why}`);
|
|
152
|
-
}
|
|
147
|
+
const r = await stateStore.write('.keep', '{"keep":true}\n', 'application/json');
|
|
148
|
+
if (!r.ok) throw new Error(`private store ${stateStore.base}.keep → ${r.why}`);
|
|
153
149
|
this.log(`private state lives at ${cred.privateRoot} (${stateStore.kind})`);
|
|
154
|
-
} else {
|
|
155
|
-
await this.remote.putJson(this.urls.fediverse + '.keep', { keep: true }, 'application/json');
|
|
156
|
-
await this.remote.setAcl(this.urls.fediverse, []);
|
|
157
150
|
}
|
|
158
151
|
this.store.attach(stateStore);
|
|
159
152
|
// Re-running setup must not destroy state set afterwards (the UI
|
|
@@ -314,7 +307,6 @@ export class Agent {
|
|
|
314
307
|
const { rotateKeyOnce, ...rest } = cred;
|
|
315
308
|
writeJsonAtomic(path.join(this.home, 'credential.json'), rest);
|
|
316
309
|
}
|
|
317
|
-
this.local = new PodRdf({ storage: this.privateStorage(cred, 'fediverse') });
|
|
318
310
|
// connect() can run more than once — connectWithRetry retries after a throw,
|
|
319
311
|
// and the CLI connects for real after its confirmation. Deliverer arms a
|
|
320
312
|
// 60s queue timer in its constructor and Intake owns a poll timer and a
|
|
@@ -332,13 +324,10 @@ export class Agent {
|
|
|
332
324
|
log: this.log, passive: this.viewer,
|
|
333
325
|
});
|
|
334
326
|
this.publisher = new Publisher({
|
|
335
|
-
config, remote: this.remote,
|
|
327
|
+
config, remote: this.remote, store: this.store,
|
|
336
328
|
deliverer: this.deliverer, publicKeyPem: keys.rsaPublicPem,
|
|
337
329
|
assertionKey: keys.edPublicMultibase, log: this.log,
|
|
338
330
|
resolveMention: (h) => resolveHandle(this, h),
|
|
339
|
-
// Whether the fediverse tree is on the pod at all, so the ACL check does
|
|
340
|
-
// not probe for something the default layout keeps on local disk.
|
|
341
|
-
privateOnPod: !cred.privateRoot,
|
|
342
331
|
// Inside a pod server the client surface answers on the pod's own
|
|
343
332
|
// origin, so it can be advertised. Standalone it is on loopback, and
|
|
344
333
|
// naming it in a world-readable actor would send clients nowhere.
|
|
@@ -347,7 +336,7 @@ export class Agent {
|
|
|
347
336
|
// Intake is constructed even for viewers — its signed fetchAP powers
|
|
348
337
|
// search/deref; start() (draining) is active-only.
|
|
349
338
|
this.intake = new Intake({
|
|
350
|
-
config, urls: this.urls, remote: this.remote,
|
|
339
|
+
config, urls: this.urls, remote: this.remote, store: this.store,
|
|
351
340
|
deliverer: this.deliverer, publisher: this.publisher, log: this.log, lease: this.lease,
|
|
352
341
|
archive: this.privateStorage(cred, 'archive'),
|
|
353
342
|
push: !this.embedded, pollSeconds: this.pollSeconds || null,
|
|
@@ -581,7 +570,7 @@ export class Agent {
|
|
|
581
570
|
this.ensureActorPublished()
|
|
582
571
|
.catch(e => this.log(`actor check failed: ${e.message}`));
|
|
583
572
|
}
|
|
584
|
-
this.
|
|
573
|
+
this.seedFollowNotifications().catch(e => this.log(`notification seeding failed: ${e.message}`));
|
|
585
574
|
}
|
|
586
575
|
|
|
587
576
|
// Re-read state before acting on what we hold. A viewer's cache is kept
|
|
@@ -695,27 +684,13 @@ export class Agent {
|
|
|
695
684
|
return true;
|
|
696
685
|
}
|
|
697
686
|
|
|
698
|
-
//
|
|
699
|
-
//
|
|
700
|
-
async
|
|
701
|
-
if (
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
if (this.store.has('statuses.json')) return;
|
|
707
|
-
const entries = [];
|
|
708
|
-
for (const [container, kind] of [['timeline', 'timeline'], ['posts', 'post']]) {
|
|
709
|
-
for (const url of await this.local.listNotes(container)) {
|
|
710
|
-
try {
|
|
711
|
-
const n = await this.local.readNote(url);
|
|
712
|
-
if (n.noteId) entries.push({ ...n, kind, slug: url.split('/').pop() });
|
|
713
|
-
} catch (e) { this.log(`backfill: skipped ${url}: ${e.message}`); }
|
|
714
|
-
}
|
|
687
|
+
// A first run inherits followers without ever having seen them arrive, so
|
|
688
|
+
// the notifications list starts empty where it should start full.
|
|
689
|
+
async seedFollowNotifications() {
|
|
690
|
+
if (this.store.has('notifications.json')) return;
|
|
691
|
+
for (const f of this.store.getContacts().followers) {
|
|
692
|
+
this.store.addNotification({ type: 'follow', actor: f.actor });
|
|
715
693
|
}
|
|
716
|
-
entries.sort((a, b) => String(b.published || '').localeCompare(String(a.published || '')));
|
|
717
|
-
this.store.write('statuses.json', entries.slice(0, 1000));
|
|
718
|
-
this.log(`backfilled ${entries.length} statuses from pod RDF`);
|
|
719
694
|
}
|
|
720
695
|
}
|
|
721
696
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// build-app.mjs — bundle the in-browser agent (and its pieces) with esbuild.
|
|
2
|
+
//
|
|
3
|
+
// The agent's brain is lib/, written for Node. This bundles it for a browser by
|
|
4
|
+
// swapping the Node-only edges for shims (web/app/shims/) and stubbing the Node
|
|
5
|
+
// modules the browser agent never uses (a local HTTP server, the filesystem,
|
|
6
|
+
// DNS pinning). A stubbed module that is actually called throws a named error,
|
|
7
|
+
// so a missing port shows up loudly rather than as silent wrong behaviour.
|
|
8
|
+
//
|
|
9
|
+
// node scripts/build-app.mjs [--entry <file>] [--out <file>]
|
|
10
|
+
import esbuild from 'esbuild';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
const shim = (p) => path.join(root, 'web/app/shims', p);
|
|
17
|
+
|
|
18
|
+
// Node modules the browser agent does not use. Stubbed to an empty module so an
|
|
19
|
+
// unrelated transitive import does not fail the build; anything that truly needs
|
|
20
|
+
// one will fail at runtime with a clear message, which is the signal to port it.
|
|
21
|
+
const STUBBED = ['node:fs/promises', 'node:os', 'node:net', 'node:http',
|
|
22
|
+
'node:https', 'node:dns', 'node:dns/promises', 'node:child_process', 'node:tls',
|
|
23
|
+
'undici', 'node-forge'];
|
|
24
|
+
|
|
25
|
+
const aliasPlugin = {
|
|
26
|
+
name: 'fedipod-agent-aliases',
|
|
27
|
+
setup(build) {
|
|
28
|
+
build.onResolve({ filter: /^@fedify\/fedify\/sig$/ }, () => ({ path: shim('fedify-sig.mjs') }));
|
|
29
|
+
build.onResolve({ filter: /^node:crypto$/ }, () => ({ path: shim('node-crypto.mjs') }));
|
|
30
|
+
build.onResolve({ filter: /^node:path$/ }, () => ({ path: shim('node-path.mjs') }));
|
|
31
|
+
build.onResolve({ filter: /^node:url$/ }, () => ({ path: shim('node-url.mjs') }));
|
|
32
|
+
build.onResolve({ filter: /^node:fs$/ }, () => ({ path: shim('node-fs.mjs') }));
|
|
33
|
+
build.onResolve({ filter: /^web-push$/ }, () => ({ path: shim('web-push.mjs') }));
|
|
34
|
+
build.onResolve({ filter: /\/safefetch\.mjs$/ }, () => ({ path: shim('safefetch.mjs') }));
|
|
35
|
+
const stub = new RegExp(`^(${STUBBED.map((s) => s.replace('/', '\\/')).join('|')})$`);
|
|
36
|
+
build.onResolve({ filter: stub }, (args) => ({ path: args.path, namespace: 'stub' }));
|
|
37
|
+
build.onLoad({ filter: /.*/, namespace: 'stub' }, (args) => ({
|
|
38
|
+
contents: `const fail = () => { throw new Error(${JSON.stringify(args.path)} + ' is not available in the browser agent'); };`
|
|
39
|
+
+ 'export default new Proxy({}, { get: () => fail });',
|
|
40
|
+
loader: 'js',
|
|
41
|
+
}));
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export async function buildApp({ entry, out, minify = false, format = 'esm' } = {}) {
|
|
46
|
+
const result = await esbuild.build({
|
|
47
|
+
entryPoints: [entry],
|
|
48
|
+
bundle: true, format, platform: 'browser', target: 'es2022',
|
|
49
|
+
outfile: out, sourcemap: true, minify, logLevel: 'silent', metafile: true,
|
|
50
|
+
plugins: [aliasPlugin],
|
|
51
|
+
define: { 'process.env.NODE_ENV': '"production"' },
|
|
52
|
+
banner: { js: fs.readFileSync(shim('prelude.js'), 'utf8') },
|
|
53
|
+
});
|
|
54
|
+
return { out, bytes: Object.values(result.metafile.outputs).reduce((n, o) => n + o.bytes, 0) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
58
|
+
const args = process.argv.slice(2);
|
|
59
|
+
const entry = args[args.indexOf('--entry') + 1] || path.join(root, 'web/app/agent.mjs');
|
|
60
|
+
const out = args[args.indexOf('--out') + 1] || path.join(root, 'web/app/dist/agent.js');
|
|
61
|
+
const format = args.includes('--format') ? args[args.indexOf('--format') + 1] : 'esm';
|
|
62
|
+
const { bytes } = await buildApp({ entry, out, format });
|
|
63
|
+
console.log(`built ${path.relative(root, out)} — ${Math.round(bytes / 1024)} KB`);
|
|
64
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// check-pod-calls.mjs — keeps the pod library a library, and keeps pod I/O
|
|
2
|
+
// inside it.
|
|
3
|
+
//
|
|
4
|
+
// Two jobs. The first is that the layer does not erode: once every pod request
|
|
5
|
+
// goes through a named operation, one `remote.putJson(...)` added back at a
|
|
6
|
+
// call site is invisible in review and undoes the property quietly. The second
|
|
7
|
+
// is that lib/pod/ stays EXTRACTABLE — no imports above itself, no Node
|
|
8
|
+
// built-ins, its one dependency confined — because "we could lift this out"
|
|
9
|
+
// stops being true the first time something reaches upward, and nobody notices
|
|
10
|
+
// until they try.
|
|
11
|
+
//
|
|
12
|
+
// Comments are stripped before every check. The rules are about code; the
|
|
13
|
+
// files explain themselves in prose, and prose that mentions node:dns or
|
|
14
|
+
// names the application it grew up in is not a violation.
|
|
15
|
+
//
|
|
16
|
+
// node scripts/check-pod-calls.mjs
|
|
17
|
+
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const POD = path.join(root, 'lib/pod');
|
|
24
|
+
|
|
25
|
+
let failures = 0;
|
|
26
|
+
const fail = (msg) => { console.log(`FAIL ${msg}`); failures++; };
|
|
27
|
+
const pass = (msg) => console.log(`PASS ${msg}`);
|
|
28
|
+
|
|
29
|
+
const read = (p) => fs.readFileSync(p, 'utf8');
|
|
30
|
+
// Prose is stripped LINE BY LINE, never with a regex spanning lines. A
|
|
31
|
+
// `/*...*/` matcher looks obvious and is not: a `*/` inside a regex literal or
|
|
32
|
+
// a string ends the match early, and everything up to the next one disappears —
|
|
33
|
+
// which silently deleted the very code these rules exist to look at, and left
|
|
34
|
+
// the check passing. Every comment in this project's style occupies whole
|
|
35
|
+
// lines, so dropping whole comment lines is both sufficient and safe.
|
|
36
|
+
const code = (src) => {
|
|
37
|
+
const out = [];
|
|
38
|
+
let inBlock = false;
|
|
39
|
+
for (const line of src.split('\n')) {
|
|
40
|
+
const t = line.trim();
|
|
41
|
+
if (inBlock) { if (t.endsWith('*/') || t === '*/') inBlock = false; continue; }
|
|
42
|
+
if (t.startsWith('/*')) { if (!t.includes('*/')) inBlock = true; continue; }
|
|
43
|
+
if (t.startsWith('//') || t.startsWith('*')) continue;
|
|
44
|
+
out.push(line);
|
|
45
|
+
}
|
|
46
|
+
return out.join('\n');
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const podFiles = fs.readdirSync(POD).filter((f) => f.endsWith('.mjs'));
|
|
50
|
+
|
|
51
|
+
// ---- 1. the layer: pod I/O lives behind a named operation ----
|
|
52
|
+
|
|
53
|
+
// The files that run the browser agent and the gateway. Node-only code is
|
|
54
|
+
// deliberately out of scope for this pass and must not fail the check.
|
|
55
|
+
const IN_SCOPE = [
|
|
56
|
+
'lib/publisher.mjs', 'lib/intake.mjs', 'lib/social.mjs', 'lib/mastoapi.mjs',
|
|
57
|
+
'lib/gateway-core.mjs', 'lib/front-core.mjs',
|
|
58
|
+
'web/app/agent.mjs', 'web/app/signup.mjs', 'web/app/boot.mjs',
|
|
59
|
+
'web/app/admin-facade.mjs', 'web/app/keys-browser.mjs',
|
|
60
|
+
];
|
|
61
|
+
const VERBS = /\b(?:remote|pod)\??\.(put|putJson|getJson|delete|setAcl|listContainer|aclUrlFor|aclWritable|patchDocument|linkAccountInProfile|aclDoc)\(/;
|
|
62
|
+
|
|
63
|
+
for (const rel of IN_SCOPE) {
|
|
64
|
+
const hits = code(read(path.join(root, rel))).split('\n')
|
|
65
|
+
.map((l, i) => [i + 1, l]).filter(([, l]) => VERBS.test(l));
|
|
66
|
+
if (hits.length) {
|
|
67
|
+
fail(`${rel} calls a transport verb directly — that belongs in a lib/pod/ operation`);
|
|
68
|
+
for (const [n, l] of hits.slice(0, 5)) console.log(` ${rel}:${n} ${l.trim()}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!failures) pass('every in-scope caller reaches the pod through a named operation');
|
|
72
|
+
|
|
73
|
+
// ---- 2. the deny-list cannot be routed around ----
|
|
74
|
+
{
|
|
75
|
+
const offenders = [];
|
|
76
|
+
const walk = (dir) => {
|
|
77
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
78
|
+
const p = path.join(dir, e.name);
|
|
79
|
+
if (e.isDirectory()) {
|
|
80
|
+
if (['node_modules', 'dist', 'site', '.git', 'claude', 'phanpy', 'ui', 'packages'].includes(e.name)) continue;
|
|
81
|
+
walk(p);
|
|
82
|
+
} else if (e.name.endsWith('.mjs') && p !== path.join(POD, 'transport.mjs')) {
|
|
83
|
+
// A DELETE is only a problem when it is sent on a RAW fetch. One sent
|
|
84
|
+
// through an injected fetcher is going through the transport — that is
|
|
85
|
+
// what `storage.mjs` does, and is the point of it being injected.
|
|
86
|
+
const src = code(read(p));
|
|
87
|
+
for (const m of src.matchAll(/method:\s*['"]DELETE['"]/g)) {
|
|
88
|
+
const before = src.slice(Math.max(0, m.index - 240), m.index);
|
|
89
|
+
const call = before.lastIndexOf('(');
|
|
90
|
+
const receiver = before.slice(Math.max(0, call - 60), call + 1);
|
|
91
|
+
if (/(?:^|[^.\w])(?:window\.)?fetch\($/.test(receiver) || /session\.fetch\($/.test(receiver)) {
|
|
92
|
+
offenders.push(`${path.relative(root, p)} (raw fetch)`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
walk(path.join(root, 'lib'));
|
|
99
|
+
walk(path.join(root, 'web/app'));
|
|
100
|
+
if (offenders.length) fail(`a DELETE is issued outside the transport, skipping the deny-list: ${offenders.join(', ')}`);
|
|
101
|
+
else pass('every DELETE goes through the transport, where the deny-list is');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- 3. the signing key is never written without its ACL ----
|
|
105
|
+
{
|
|
106
|
+
const s = code(read(path.join(root, 'web/app/signup.mjs')));
|
|
107
|
+
if (/keys\.json[\s\S]{0,200}method:\s*['"]PUT['"]/.test(s) || /method:\s*['"]PUT['"][\s\S]{0,200}keys\.json/.test(s)) {
|
|
108
|
+
fail('signup writes keys.json directly — it must go through state.provisionKey, which sets the ACL first');
|
|
109
|
+
} else pass('the signing key is written only by the operation that locks the container first');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---- 4. lib/pod/ reaches nothing above itself ----
|
|
113
|
+
{
|
|
114
|
+
let bad = 0;
|
|
115
|
+
for (const f of podFiles) {
|
|
116
|
+
const src = code(read(path.join(POD, f)));
|
|
117
|
+
for (const m of src.matchAll(/from\s+['"]([^'"]+)['"]/g)) {
|
|
118
|
+
const spec = m[1];
|
|
119
|
+
if (spec.startsWith('../')) { fail(`lib/pod/${f} imports ${spec} — the library may not reach above itself`); bad++; }
|
|
120
|
+
if (spec.startsWith('node:')) { fail(`lib/pod/${f} imports ${spec} — the library must run in a service worker`); bad++; }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!bad) pass('lib/pod/ imports nothing above itself and no Node built-ins');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---- 5. its one dependency stays confined ----
|
|
127
|
+
{
|
|
128
|
+
const rdflibbers = podFiles.filter((f) => /from\s+['"]rdflib['"]/.test(code(read(path.join(POD, f)))));
|
|
129
|
+
const allowed = ['transport.mjs', 'notifications.mjs'];
|
|
130
|
+
const extra = rdflibbers.filter((f) => !allowed.includes(f));
|
|
131
|
+
if (extra.length) fail(`rdflib reached from ${extra.join(', ')} — a resource module must not pull a parser in behind it`);
|
|
132
|
+
else pass(`rdflib is confined to ${allowed.join(' and ')}`);
|
|
133
|
+
|
|
134
|
+
const importsTransport = podFiles
|
|
135
|
+
.filter((f) => f !== 'transport.mjs')
|
|
136
|
+
.filter((f) => /from\s+['"]\.\/transport\.mjs['"]/.test(code(read(path.join(POD, f)))));
|
|
137
|
+
if (importsTransport.length) fail(`${importsTransport.join(', ')} imports the transport — operations receive one, they do not reach for it`);
|
|
138
|
+
else pass('no resource module imports or constructs a transport');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---- 6. it does not know what application it grew up in ----
|
|
142
|
+
{
|
|
143
|
+
const named = podFiles.filter((f) => /fedipod/i.test(code(read(path.join(POD, f)))));
|
|
144
|
+
if (named.length) fail(`lib/pod/${named.join(', ')} names FediPod in code — the library is meant to outlive it`);
|
|
145
|
+
else pass('no FediPod vocabulary in library code');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---- 7. and it actually runs on its own ----
|
|
149
|
+
{
|
|
150
|
+
const { apUrls } = await import(path.join(POD, 'urls.mjs'));
|
|
151
|
+
const { PodTransport, protectedFromDeletion } = await import(path.join(POD, 'transport.mjs'));
|
|
152
|
+
const urls = apUrls('https://p.example/', 'anything/');
|
|
153
|
+
let threw = false;
|
|
154
|
+
try { apUrls('https://p.example/'); } catch { threw = true; }
|
|
155
|
+
if (!threw) fail('apUrls guessed a container root instead of requiring one');
|
|
156
|
+
|
|
157
|
+
const seen = [];
|
|
158
|
+
const pod = new PodTransport(
|
|
159
|
+
{ fetch: async (u, i) => { seen.push(`${i?.method || 'GET'} ${u}`); return { status: 200, headers: { get: () => null } }; } },
|
|
160
|
+
{ webId: 'https://p.example/profile/card#me', role: 'agent', runtime: 'node' },
|
|
161
|
+
);
|
|
162
|
+
const inbox = await import(path.join(POD, 'inbox.mjs'));
|
|
163
|
+
await inbox.writeKeep(pod, urls);
|
|
164
|
+
await inbox.setPosture(pod, urls, 'open');
|
|
165
|
+
let refused = false;
|
|
166
|
+
try { await pod.delete('https://p.example/profile/card'); } catch { refused = true; }
|
|
167
|
+
if (!refused) fail('the deny-list did not refuse a DELETE of the WebID document');
|
|
168
|
+
if (!seen.some((r) => r.startsWith('PUT https://p.example/anything/ap/inbox/.keep'))) {
|
|
169
|
+
fail(`the inbox operations did not reach the pod as expected: ${seen.join(', ')}`);
|
|
170
|
+
}
|
|
171
|
+
if (pod.label !== 'agent/node') fail(`the transport did not label itself: ${pod.label}`);
|
|
172
|
+
if (typeof protectedFromDeletion !== 'function') fail('protectedFromDeletion is not exported');
|
|
173
|
+
pass('the library imports and runs against a stub with nothing but rdflib on the path');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(failures ? `\n${failures} failure(s)` : '\npod layer intact');
|
|
177
|
+
process.exit(failures ? 1 : 0);
|