fedipod-server 0.10.0 → 0.12.1
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 +15 -6
- package/dist/claims.js +1 -1
- package/dist/handler.d.ts +7 -1
- package/dist/handler.js +58 -12
- package/dist/handler.jsonld +5 -1
- package/dist/store-pod.js +18 -4
- package/lib/{c2s.mjs → client/c2s.mjs} +8 -3
- package/lib/{localapi.mjs → client/localapi.mjs} +2 -2
- package/lib/client/masto/accounts.mjs +264 -0
- package/lib/client/masto/body.mjs +69 -0
- package/lib/client/masto/index.mjs +183 -0
- package/lib/client/masto/instance.mjs +104 -0
- package/lib/client/masto/media.mjs +133 -0
- package/lib/client/masto/oauth.mjs +599 -0
- package/lib/client/masto/render.mjs +459 -0
- package/lib/client/masto/statuses.mjs +331 -0
- package/lib/client/masto/timelines.mjs +316 -0
- package/lib/{streaming.mjs → client/streaming.mjs} +1 -1
- package/lib/{acctfeed.mjs → connections/acctfeed.mjs} +1 -1
- package/lib/{atproto.mjs → connections/atproto.mjs} +15 -16
- package/lib/{bskygroup.mjs → connections/bskygroup.mjs} +1 -1
- package/lib/{fediacct.mjs → connections/fediacct.mjs} +31 -35
- package/lib/{import.mjs → connections/import.mjs} +2 -2
- package/lib/{tagfeed.mjs → connections/tagfeed.mjs} +21 -4
- package/lib/connections/vault.mjs +114 -0
- package/lib/core/as2.mjs +124 -0
- package/lib/core/contexts/activitystreams.json +379 -0
- package/lib/core/contexts/did-v1.json +57 -0
- package/lib/core/contexts/fep-5711.json +36 -0
- package/lib/core/contexts/gotosocial.json +86 -0
- package/lib/core/contexts/identity-v1.json +152 -0
- package/lib/core/contexts/index.mjs +45 -0
- package/lib/core/contexts/join-lemmy.json +33 -0
- package/lib/core/contexts/joinmastodon.json +28 -0
- package/lib/core/contexts/map.json +16 -0
- package/lib/core/contexts/miscellany.json +19 -0
- package/lib/core/contexts/schemaorg.json +8845 -0
- package/lib/core/contexts/security-data-integrity-v1.json +78 -0
- package/lib/core/contexts/security-data-integrity-v2.json +81 -0
- package/lib/core/contexts/security-multikey-v1.json +35 -0
- package/lib/core/contexts/security-v1.json +74 -0
- package/lib/core/contexts/webfinger.json +10 -0
- package/lib/{deliver.mjs → core/deliver.mjs} +8 -2
- package/lib/core/intake/activities.mjs +437 -0
- package/lib/core/intake/activity.mjs +240 -0
- package/lib/core/intake/channel.mjs +144 -0
- package/lib/core/intake/group.mjs +222 -0
- package/lib/core/intake/index.mjs +629 -0
- package/lib/core/intake/notes.mjs +288 -0
- package/lib/core/intake/verify.mjs +141 -0
- package/lib/{keys.mjs → core/keys.mjs} +1 -1
- package/lib/core/publisher/collections.mjs +229 -0
- package/lib/core/publisher/index.mjs +421 -0
- package/lib/core/publisher/notes.mjs +188 -0
- package/lib/core/publisher/questions.mjs +233 -0
- package/lib/core/publisher/restore.mjs +196 -0
- package/lib/core/shapes/activitystreams.ttl +129 -0
- package/lib/core/shapes/index.mjs +107 -0
- package/lib/core/shapes/shapes-text.mjs +13 -0
- package/lib/{social.mjs → core/social.mjs} +19 -10
- package/lib/{storage.mjs → core/storage.mjs} +2 -2
- package/lib/{store.mjs → core/store.mjs} +12 -1
- package/lib/{wire.mjs → core/wire.mjs} +48 -67
- package/lib/device/admin/index.mjs +13 -0
- package/lib/device/admin/origins.mjs +35 -0
- package/lib/device/admin/routes/connections.mjs +144 -0
- package/lib/device/admin/routes/gateway.mjs +199 -0
- package/lib/device/admin/routes/lifecycle.mjs +191 -0
- package/lib/device/admin/routes/owner.mjs +322 -0
- package/lib/device/admin/routes/setup.mjs +393 -0
- package/lib/device/admin/routes/social.mjs +188 -0
- package/lib/device/admin/server.mjs +95 -0
- package/lib/device/admin/static.mjs +244 -0
- package/lib/device/admin/surface.mjs +274 -0
- package/lib/device/cli/commands/account.mjs +586 -0
- package/lib/device/cli/commands/run.mjs +278 -0
- package/lib/device/cli/commands/service.mjs +221 -0
- package/lib/device/cli/commands/setup.mjs +410 -0
- package/lib/device/cli/commands/state.mjs +559 -0
- package/lib/device/cli/context.mjs +288 -0
- package/lib/{export-collections.mjs → device/export-collections.mjs} +1 -1
- package/lib/{migrate.mjs → device/migrate.mjs} +5 -19
- package/lib/device/remote.mjs +54 -0
- package/lib/{setup.mjs → device/setup.mjs} +3 -3
- package/lib/{update.mjs → device/update.mjs} +1 -1
- package/lib/{directory.mjs → gateway/directory.mjs} +1 -1
- package/lib/{front-core.mjs → gateway/front-core.mjs} +272 -49
- package/lib/{gateway-core.mjs → gateway/gateway-core.mjs} +3 -7
- package/lib/{httpsig.mjs → gateway/httpsig.mjs} +34 -3
- 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/{links.mjs → pod/links.mjs} +1 -1
- 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/{remote.mjs → pod/transport.mjs} +217 -159
- package/lib/pod/urls.mjs +71 -0
- package/lib/{embed.mjs → server/embed.mjs} +149 -22
- package/lib/shared/links.mjs +9 -0
- package/lib/{ua.mjs → shared/ua.mjs} +1 -1
- package/package.json +1 -1
- package/run-agent.mjs +47 -64
- package/web/admin/actors.js +145 -0
- 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/common.js +23 -0
- package/web/admin/connections.js +112 -0
- package/web/admin/gateway.js +111 -0
- package/web/admin/group.js +258 -0
- package/web/admin/index.html +60 -66
- package/web/admin/record.js +378 -0
- package/web/admin/setup/index.html +3 -1
- package/web/admin/setup/setup.js +2 -13
- package/web/admin/tokens.css +56 -0
- package/web/admin/upkeep.js +170 -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 +70014 -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/shapes-text.mjs +8 -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/actors.js +145 -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/common.js +23 -0
- package/web/app/site/admin/connections.js +112 -0
- package/web/app/site/admin/gateway.js +111 -0
- package/web/app/site/admin/group.js +258 -0
- package/web/app/site/admin/index.html +562 -0
- package/web/app/site/admin/record.js +378 -0
- package/web/app/site/admin/setup/index.html +192 -0
- package/web/app/site/admin/setup/setup.js +297 -0
- package/web/app/site/admin/tokens.css +56 -0
- package/web/app/site/admin/upkeep.js +170 -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 +70014 -0
- package/web/app/sw-src.mjs +220 -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/admin.mjs +0 -1913
- package/lib/intake.mjs +0 -1764
- package/lib/mastoapi.mjs +0 -1947
- package/lib/podrdf.mjs +0 -132
- package/lib/publisher.mjs +0 -1263
- package/web/admin/admin.js +0 -1114
- /package/lib/{oidc-auth.mjs → client/oidc-auth.mjs} +0 -0
- /package/lib/{webpush.mjs → client/webpush.mjs} +0 -0
- /package/lib/{bskyfeed.mjs → connections/bskyfeed.mjs} +0 -0
- /package/lib/{lease.mjs → core/lease.mjs} +0 -0
- /package/lib/{polls.mjs → core/polls.mjs} +0 -0
- /package/lib/{proof.mjs → core/proof.mjs} +0 -0
- /package/lib/{account.mjs → device/account.mjs} +0 -0
- /package/lib/{certs.mjs → device/certs.mjs} +0 -0
- /package/lib/{home.mjs → device/home.mjs} +0 -0
- /package/lib/{ports.mjs → device/ports.mjs} +0 -0
- /package/lib/{guard.mjs → shared/guard.mjs} +0 -0
- /package/lib/{safefetch.mjs → shared/safefetch.mjs} +0 -0
package/README.md
CHANGED
|
@@ -78,15 +78,24 @@ and stop with the server, and on the websocket handler list for the live feed.
|
|
|
78
78
|
|
|
79
79
|
An identity is provisioned when its owner opts in: its name is the pod's
|
|
80
80
|
subdomain label, and it publishes an actor, a signing key and WebFinger on the
|
|
81
|
-
pod itself.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
pod itself. Everything it is made of lives on its pod — its state, its private
|
|
82
|
+
signing key, the secret guarding its own pages, and the credentials for any
|
|
83
|
+
accounts its owner connects on other servers. Each identity has its own
|
|
84
|
+
secret; one owner's opens nobody else's door. The opt-in reply is where the
|
|
85
|
+
owner is given it.
|
|
86
|
+
|
|
87
|
+
That is the difference from running FediPod on your own machine, where those
|
|
88
|
+
credentials stay on the machine and never reach the pod. Here the machine is
|
|
89
|
+
the pod's server, so a credential left on it is a connection its owner loses
|
|
90
|
+
the day they take their pod elsewhere.
|
|
91
|
+
|
|
92
|
+
`agentDataDir` holds what is left: one directory per identity, with the file
|
|
93
|
+
naming the pod the identity runs on. Nothing private is in it.
|
|
85
94
|
|
|
86
95
|
| Setting | What it is |
|
|
87
96
|
|---|---|
|
|
88
97
|
| `agentRuntimeOptIn` | Whether pod owners can sign up. With it off, nothing runs. |
|
|
89
|
-
| `agentDataDir` | Where each identity keeps
|
|
98
|
+
| `agentDataDir` | Where each identity keeps the file naming its pod; its log lines go to the server's own log. Required whenever sign-up is on. |
|
|
90
99
|
| `agentUiPath` | Where the owner's pages live on the pod's origin. `/fedipod/` by default; empty serves no pages. |
|
|
91
100
|
| `agentRegistryContainer` | The internal container holding the sign-up rows. |
|
|
92
101
|
| `runPage` | The HTML served at `/run`: the page where a pod owner opts in or out. The package's own `web/front/run.html` is served unless you set this. |
|
|
@@ -149,7 +158,7 @@ owner's door, with that identity's own door secret:
|
|
|
149
158
|
|
|
150
159
|
```
|
|
151
160
|
curl -X POST https://mei.example.org/fedipod/config \
|
|
152
|
-
-H 'x-dk-token:
|
|
161
|
+
-H 'x-dk-token: THE_DOOR_SECRET_FROM_THE_OPT_IN_REPLY' -H 'content-type: application/json' \
|
|
153
162
|
-d '{"password":"the one you will type into your phone"}'
|
|
154
163
|
```
|
|
155
164
|
|
package/dist/claims.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.claims = claims;
|
|
8
8
|
exports.agentClaims = agentClaims;
|
|
9
|
-
const FRONT_PATHS = new Set(['/', '/signup', '/new-account', '/run', '/
|
|
9
|
+
const FRONT_PATHS = new Set(['/', '/signup', '/new-account', '/run', '/roster',
|
|
10
10
|
'/.well-known/webfinger', '/api/handle', '/api/attach', '/api/agent',
|
|
11
11
|
'/api/roster', '/api/revoke',
|
|
12
12
|
// What the pages load: the sign-in library the /run and /admin pages use, and
|
package/dist/handler.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface FediPodServerArgs {
|
|
|
18
18
|
signupPage?: string;
|
|
19
19
|
/** The run-your-identity page HTML served at /run. */
|
|
20
20
|
runPage?: string;
|
|
21
|
-
/** The accounts-roster page HTML served at /
|
|
21
|
+
/** The accounts-roster page HTML served at /roster. */
|
|
22
22
|
adminPage?: string;
|
|
23
23
|
/** Directory holding each agent identity's signing key and log. Required when runtime opt-in is on. */
|
|
24
24
|
agentDataDir?: string;
|
|
@@ -91,6 +91,12 @@ export declare class FediPodServerHandler extends HttpHandler implements Initial
|
|
|
91
91
|
initialize(): Promise<void>;
|
|
92
92
|
/** Stop every identity: timers cleared, state written, lease let go. */
|
|
93
93
|
finalize(): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* One identity's door secret, in its own pod's state. `agentDataDir` is
|
|
96
|
+
* handed in as the place an identity set up before this kept it, so its
|
|
97
|
+
* owner's existing door link survives the move.
|
|
98
|
+
*/
|
|
99
|
+
private doorSecretFor;
|
|
94
100
|
private startIdentity;
|
|
95
101
|
/**
|
|
96
102
|
* When this server is also the door, give a running identity a
|
package/dist/handler.js
CHANGED
|
@@ -25,9 +25,9 @@ const directory_1 = require("./directory");
|
|
|
25
25
|
// Two layouts carry that tree: the published package ships its own copy of
|
|
26
26
|
// lib/ beside dist/ (prepack puts it there), and a repo checkout reaches the
|
|
27
27
|
// repo's lib/ three levels up. Prefer the package's own copy when it exists.
|
|
28
|
-
const LIB_ROOT = (0, node_fs_1.existsSync)((0, node_path_1.join)(__dirname, '../lib/embed.mjs')) ? '../lib' : '../../../lib';
|
|
29
|
-
const FRONT_CORE = `${LIB_ROOT}/front-core.mjs`;
|
|
30
|
-
const EMBED = `${LIB_ROOT}/embed.mjs`;
|
|
28
|
+
const LIB_ROOT = (0, node_fs_1.existsSync)((0, node_path_1.join)(__dirname, '../lib/server/embed.mjs')) ? '../lib' : '../../../lib';
|
|
29
|
+
const FRONT_CORE = `${LIB_ROOT}/gateway/front-core.mjs`;
|
|
30
|
+
const EMBED = `${LIB_ROOT}/server/embed.mjs`;
|
|
31
31
|
const esmImport = new Function('s', 'return import(s)');
|
|
32
32
|
// The front's pages and the files they load, carried in the same two layouts
|
|
33
33
|
// as lib/. A missing file is not fatal: the route it feeds answers 404.
|
|
@@ -215,6 +215,20 @@ class FediPodServerHandler extends community_server_1.HttpHandler {
|
|
|
215
215
|
this.logger.info(`FediPod agent @${identity.handle} stopped`);
|
|
216
216
|
}));
|
|
217
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* One identity's door secret, in its own pod's state. `agentDataDir` is
|
|
220
|
+
* handed in as the place an identity set up before this kept it, so its
|
|
221
|
+
* owner's existing door link survives the move.
|
|
222
|
+
*/
|
|
223
|
+
async doorSecretFor(podBase, session, opts = {}) {
|
|
224
|
+
const { ensureDoorSecret } = await esmImport(EMBED);
|
|
225
|
+
return ensureDoorSecret(session ?? (0, store_pod_1.makeStoreSession)(this.args.resourceStore, podBase), podBase, {
|
|
226
|
+
...opts,
|
|
227
|
+
dataDir: this.args.agentDataDir,
|
|
228
|
+
handle: deriveHandle(podBase),
|
|
229
|
+
log: (message) => { this.logger.info(`@${deriveHandle(podBase)}: ${message}`); },
|
|
230
|
+
});
|
|
231
|
+
}
|
|
218
232
|
async startIdentity(podBase) {
|
|
219
233
|
if (this.starting.has(podBase) || this.identities.has(podBase))
|
|
220
234
|
return;
|
|
@@ -224,13 +238,13 @@ class FediPodServerHandler extends community_server_1.HttpHandler {
|
|
|
224
238
|
try {
|
|
225
239
|
for (let attempt = 0; !this.stopping && !this.startCancelled.has(podBase); attempt++) {
|
|
226
240
|
try {
|
|
227
|
-
const { startEmbeddedAgent
|
|
241
|
+
const { startEmbeddedAgent } = await esmImport(EMBED);
|
|
228
242
|
// The secret is in the map BEFORE the surface can exist, so the
|
|
229
243
|
// gate's resolver never comes up empty — empty would mean gate-off.
|
|
230
244
|
if (!this.doorSecrets.has(podBase)) {
|
|
231
|
-
const door =
|
|
245
|
+
const door = await this.doorSecretFor(podBase, session);
|
|
232
246
|
this.doorSecrets.set(podBase, door.secret);
|
|
233
|
-
this.logger.info(`door secret for @${deriveHandle(podBase)} is at ${door.
|
|
247
|
+
this.logger.info(`door secret for @${deriveHandle(podBase)} is in its pod at ${door.url}`);
|
|
234
248
|
}
|
|
235
249
|
const identity = await startEmbeddedAgent({
|
|
236
250
|
podBase,
|
|
@@ -344,11 +358,10 @@ class FediPodServerHandler extends community_server_1.HttpHandler {
|
|
|
344
358
|
}
|
|
345
359
|
const base = podBase.endsWith('/') ? podBase : `${podBase}/`;
|
|
346
360
|
const handle = deriveHandle(base);
|
|
347
|
-
const { ensureDoorSecret } = await esmImport(EMBED);
|
|
348
361
|
// Already running here from an earlier opt-in: proving pod
|
|
349
362
|
// control again buys a fresh secret, nothing else.
|
|
350
363
|
if (this.agentHandles.get(handle) === base) {
|
|
351
|
-
const door =
|
|
364
|
+
const door = await this.doorSecretFor(base, undefined, { rotate: true });
|
|
352
365
|
this.doorSecrets.set(base, door.secret);
|
|
353
366
|
return { httpStatus: 201, ok: true, handle, host: new URL(base).host.toLowerCase(),
|
|
354
367
|
doorSecret: door.secret, doorPath: this.uiPath, status: 'rotated' };
|
|
@@ -371,10 +384,10 @@ class FediPodServerHandler extends community_server_1.HttpHandler {
|
|
|
371
384
|
// until the agent registers its surface.
|
|
372
385
|
this.agentHosts.add(host);
|
|
373
386
|
this.agentHandles.set(handle, base);
|
|
374
|
-
const door =
|
|
387
|
+
const door = await this.doorSecretFor(base, undefined, { rotate: true });
|
|
375
388
|
this.doorSecrets.set(base, door.secret);
|
|
376
389
|
void this.startIdentity(base);
|
|
377
|
-
this.logger.info(`runtime opt-in: @${handle} on ${base} (door secret at ${door.
|
|
390
|
+
this.logger.info(`runtime opt-in: @${handle} on ${base} (door secret in its pod at ${door.url})`);
|
|
378
391
|
return { httpStatus: 201, ok: true, handle, host,
|
|
379
392
|
doorSecret: door.secret, doorPath: this.uiPath, status: 'starting' };
|
|
380
393
|
}
|
|
@@ -441,15 +454,48 @@ class FediPodServerHandler extends community_server_1.HttpHandler {
|
|
|
441
454
|
signupPage: this.args.signupPage || webFile('new-account.html'),
|
|
442
455
|
runPage: this.args.runPage || webFile('run.html'),
|
|
443
456
|
adminPage: this.args.adminPage || webFile('admin.html'),
|
|
444
|
-
// The /run and /
|
|
457
|
+
// The /run and /roster pages load the sign-in library, and the signup page
|
|
445
458
|
// hands out the installer command; without these the pages render but
|
|
446
459
|
// cannot be used.
|
|
447
460
|
authBundle: webFile('solid-oidc-client.js'),
|
|
461
|
+
// Each page's own script — inline until 2026-09-09, so that the pages can
|
|
462
|
+
// be served under `script-src 'self'` (see lib/front-core.mjs).
|
|
463
|
+
pageScripts: {
|
|
464
|
+
'new-account.js': webFile('new-account.js'),
|
|
465
|
+
'run.js': webFile('run.js'),
|
|
466
|
+
'admin.js': webFile('admin.js'),
|
|
467
|
+
},
|
|
448
468
|
installScript: webFile('install.sh'),
|
|
449
469
|
lookup: (h) => this.dir.lookup(h),
|
|
450
470
|
putDirectory: (h, rec) => this.dir.putDirectory(h, rec),
|
|
451
471
|
podPut: (_handle, url, body, ct) => this.podPut(url, body, ct),
|
|
452
|
-
|
|
472
|
+
// Reads for the public proxy. `this.io.read` goes STRAIGHT into the
|
|
473
|
+
// resource store, which applies no access control of its own — so this is
|
|
474
|
+
// the one place that has to say what may be read, and it says: only
|
|
475
|
+
// inside the pod of the handle being served, and never a server-internal
|
|
476
|
+
// tree. Without it a row could name any location and this would fetch it
|
|
477
|
+
// (the directory, with every user's receipt secret, included).
|
|
478
|
+
//
|
|
479
|
+
// The front now refuses to attach such a row at all (podHomeProblem in
|
|
480
|
+
// lib/front-core.mjs); this is the same refusal at the other end, because
|
|
481
|
+
// a row can also arrive from a seed or an older deploy.
|
|
482
|
+
podGet: async (url, opts) => {
|
|
483
|
+
const denied = { status: 403, text: async () => '', headers: { get: () => null } };
|
|
484
|
+
let target;
|
|
485
|
+
try {
|
|
486
|
+
target = new URL(url);
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return denied;
|
|
490
|
+
}
|
|
491
|
+
if (/(^|\/)\.internal(\/|$)/u.test(target.pathname))
|
|
492
|
+
return denied;
|
|
493
|
+
// Which pod asked: routeFront reads `rec.podHome + rest` and passes
|
|
494
|
+
// that podHome here, so the confinement is a value on the call rather
|
|
495
|
+
// than state on the handler. No podHome, no read.
|
|
496
|
+
const home = opts?.podHome;
|
|
497
|
+
if (!home || !url.startsWith(home))
|
|
498
|
+
return denied;
|
|
453
499
|
const raw = await this.io.read(url);
|
|
454
500
|
return raw == null
|
|
455
501
|
? { status: 404, text: async () => '', headers: { get: () => null } }
|
package/dist/handler.jsonld
CHANGED
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
}
|
|
107
107
|
]
|
|
108
108
|
},
|
|
109
|
-
"comment": "The accounts-roster page HTML served at /
|
|
109
|
+
"comment": "The accounts-roster page HTML served at /roster."
|
|
110
110
|
},
|
|
111
111
|
{
|
|
112
112
|
"@id": "fps:dist/handler.jsonld#FediPodServerHandler_args_agentDataDir",
|
|
@@ -312,6 +312,10 @@
|
|
|
312
312
|
"@id": "fps:dist/handler.jsonld#FediPodServerHandler__member_finalize",
|
|
313
313
|
"memberFieldName": "finalize"
|
|
314
314
|
},
|
|
315
|
+
{
|
|
316
|
+
"@id": "fps:dist/handler.jsonld#FediPodServerHandler__member_doorSecretFor",
|
|
317
|
+
"memberFieldName": "doorSecretFor"
|
|
318
|
+
},
|
|
315
319
|
{
|
|
316
320
|
"@id": "fps:dist/handler.jsonld#FediPodServerHandler__member_startIdentity",
|
|
317
321
|
"memberFieldName": "startIdentity"
|
package/dist/store-pod.js
CHANGED
|
@@ -28,11 +28,25 @@ function headerReader(init) {
|
|
|
28
28
|
lower[k.toLowerCase()] = String(v);
|
|
29
29
|
return (n) => lower[n.toLowerCase()] ?? null;
|
|
30
30
|
}
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
31
|
+
// A container's listing is quads until something requests a syntax, and
|
|
32
|
+
// listContainer parses Turtle, so Turtle has to be askable for. It must not be
|
|
33
|
+
// the ONLY thing asked for: the state tree reads its own documents with
|
|
34
|
+
// `text/turtle, application/json;q=0.9, */*;q=0.8`, and a JSON document
|
|
35
|
+
// demanded as Turtle is refused outright — which is every state document an
|
|
36
|
+
// identity has, the moment it starts on a pod that already holds some.
|
|
37
|
+
// With no Turtle in the accept, no preference: read back as written.
|
|
34
38
|
function preferencesFor(accept) {
|
|
35
|
-
|
|
39
|
+
if (!accept?.includes('text/turtle'))
|
|
40
|
+
return {};
|
|
41
|
+
const type = {};
|
|
42
|
+
for (const part of accept.split(',')) {
|
|
43
|
+
const [media, ...params] = part.trim().split(';');
|
|
44
|
+
if (!media)
|
|
45
|
+
continue;
|
|
46
|
+
const q = params.map((p) => (/^\s*q=([\d.]+)\s*$/u).exec(p)).find(Boolean);
|
|
47
|
+
type[media.trim()] = q ? Number(q[1]) : 1;
|
|
48
|
+
}
|
|
49
|
+
return { type };
|
|
36
50
|
}
|
|
37
51
|
function isNotFound(e) {
|
|
38
52
|
return community_server_1.NotFoundHttpError.isInstance?.(e) === true || e?.statusCode === 404;
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
// read their own inbox" is served from there, by this agent, to the owner
|
|
16
16
|
// alone.
|
|
17
17
|
|
|
18
|
-
import * as social from '
|
|
19
|
-
import * as wire from '
|
|
18
|
+
import * as social from '../core/social.mjs';
|
|
19
|
+
import * as wire from '../core/wire.mjs';
|
|
20
|
+
import { readLenient } from '../core/as2.mjs';
|
|
20
21
|
|
|
21
22
|
const MAX_BODY = 512 * 1024; // same ceiling the inbox drain enforces
|
|
22
23
|
|
|
@@ -190,9 +191,13 @@ export class C2S {
|
|
|
190
191
|
if (!took) return this.send(res, 503, { error: 'another agent is active for this pod — takeover failed, try again' });
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
// Read as JSON-LD, so a client may send its activity with whatever context
|
|
195
|
+
// it likes and still be understood. One we cannot read that way is read as
|
|
196
|
+
// plain JSON rather than refused, which is what a client sending ordinary
|
|
197
|
+
// ActivityStreams has always got.
|
|
193
198
|
let activity;
|
|
194
199
|
try {
|
|
195
|
-
activity =
|
|
200
|
+
activity = (await readLenient(await readBody(req))).doc;
|
|
196
201
|
} catch (e) {
|
|
197
202
|
return this.send(res, 400, { error: `unreadable body: ${e.message}` });
|
|
198
203
|
}
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
import https from 'node:https';
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
|
-
import { rootOf, apRoot } from '
|
|
16
|
-
import { certPaths } from '
|
|
15
|
+
import { rootOf, apRoot } from '../device/home.mjs';
|
|
16
|
+
import { certPaths } from '../device/certs.mjs';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* The authorities worth offering for a loopback call: this install's, and the
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// accounts.mjs — the account endpoints: the owner's credentials and profile
|
|
2
|
+
// editor, follow requests, markers, relationships, search and lookup, block
|
|
3
|
+
// and mute, follow and unfollow, an account and its lists and statuses, and
|
|
4
|
+
// the web-push subscription a client login holds.
|
|
5
|
+
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import * as podMedia from '../../pod/media.mjs';
|
|
8
|
+
import * as social from '../../core/social.mjs';
|
|
9
|
+
import { readBody } from './body.mjs';
|
|
10
|
+
import { readMultipart } from './media.mjs';
|
|
11
|
+
|
|
12
|
+
export async function handle(api, ctx) {
|
|
13
|
+
const { req, res, pathname, url, send } = ctx; // eslint-disable-line no-unused-vars
|
|
14
|
+
|
|
15
|
+
if (pathname === '/api/v1/accounts/verify_credentials') {
|
|
16
|
+
const cfg0 = api.store.getConfig() || {};
|
|
17
|
+
return send(200, {
|
|
18
|
+
...api.selfAccount(),
|
|
19
|
+
// `source` is what the editor fills its inputs from: the raw text it
|
|
20
|
+
// will send back, not the HTML the profile renders.
|
|
21
|
+
source: {
|
|
22
|
+
privacy: 'public', sensitive: false, language: 'en',
|
|
23
|
+
note: cfg0.summary || '',
|
|
24
|
+
fields: (cfg0.fields || []).map(f => ({ name: f.name, value: f.value })),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The profile editor. Everything it can send is carried: the name and bio,
|
|
30
|
+
// both pictures, and the extra fields. An avatar or header arrives as file
|
|
31
|
+
// bytes, so it goes to the pod's media container first and the actor gets
|
|
32
|
+
// the URL — the same path a posted attachment takes.
|
|
33
|
+
if (pathname === '/api/v1/accounts/update_credentials') {
|
|
34
|
+
if (req.method !== 'PATCH' && req.method !== 'POST') return send(405, { error: 'PATCH expected' });
|
|
35
|
+
const ct = String(req.headers['content-type'] || '');
|
|
36
|
+
// readBody already covers JSON and urlencoded; only the file case differs.
|
|
37
|
+
let form = {}, files = {};
|
|
38
|
+
if (ct.includes('multipart/form-data')) ({ fields: form, files } = await readMultipart(req));
|
|
39
|
+
else form = await readBody(req);
|
|
40
|
+
|
|
41
|
+
const cfg = { ...api.store.getConfig() };
|
|
42
|
+
const putImage = async (f) => {
|
|
43
|
+
const ext = (f.filename || '').includes('.')
|
|
44
|
+
? f.filename.split('.').pop().replace(/[^\w]/g, '') : 'bin';
|
|
45
|
+
const slug = new Date().toISOString().slice(0, 10) + '-' + crypto.randomBytes(4).toString('hex') + '.' + ext;
|
|
46
|
+
const url = api.urls.media + slug;
|
|
47
|
+
await api.agent.publisher.ensureMediaContainer();
|
|
48
|
+
await podMedia.write(api.agent.remote, url, f.data, f.contentType);
|
|
49
|
+
return url;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if ('display_name' in form) cfg.name = String(form.display_name).trim() || cfg.handle;
|
|
53
|
+
if ('note' in form) cfg.summary = String(form.note) || undefined;
|
|
54
|
+
if ('locked' in form) cfg.approveJoins = form.locked === 'true' || form.locked === true;
|
|
55
|
+
if (files.avatar?.data?.length) cfg.icon = await putImage(files.avatar);
|
|
56
|
+
if (files.header?.data?.length) cfg.image = await putImage(files.header);
|
|
57
|
+
|
|
58
|
+
// fields_attributes arrives as fields_attributes[0][name] etc. A row with
|
|
59
|
+
// no name is how the editor says "delete this one", so it is dropped.
|
|
60
|
+
const rows = [];
|
|
61
|
+
for (const [k, v] of Object.entries(form)) {
|
|
62
|
+
const m = /^fields_attributes\[(\d+)\]\[(name|value)\]$/.exec(k);
|
|
63
|
+
if (!m) continue;
|
|
64
|
+
(rows[Number(m[1])] ||= {})[m[2]] = String(v);
|
|
65
|
+
}
|
|
66
|
+
if (rows.length) cfg.fields = rows.filter(r => r && r.name?.trim())
|
|
67
|
+
.map(r => ({ name: r.name.trim(), value: (r.value || '').trim() }));
|
|
68
|
+
|
|
69
|
+
api.store.setConfig(cfg);
|
|
70
|
+
Object.assign(api.agent.publisher.config, {
|
|
71
|
+
name: cfg.name, summary: cfg.summary, icon: cfg.icon, image: cfg.image,
|
|
72
|
+
fields: cfg.fields, approveJoins: !!cfg.approveJoins,
|
|
73
|
+
});
|
|
74
|
+
await api.store.flush();
|
|
75
|
+
// publishProfile says whether the world can actually read the actor it
|
|
76
|
+
// just wrote. Discarding that reported success for a save that left the
|
|
77
|
+
// account undiscoverable — the one outcome the caller needed to hear.
|
|
78
|
+
const published = await api.agent.publisher.publishProfile();
|
|
79
|
+
const unreachable = published?.unreachable;
|
|
80
|
+
if (unreachable?.length) {
|
|
81
|
+
api.log(`profile saved but NOT publicly readable: ${unreachable.join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
api.log(`profile updated from a client: ${Object.keys(form).join(', ') || '(files only)'}`);
|
|
84
|
+
return send(200, api.selfAccount());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Follow requests. The queue, and the two answers to it, have existed since
|
|
88
|
+
// groups did — `agent.store.getRequests()`, `admitRequest`, `refuseRequest`,
|
|
89
|
+
// all driven from the record page — but the facade stubbed the list to `[]`
|
|
90
|
+
// and offered no authorize/reject. So a locked account could see and answer
|
|
91
|
+
// its requests in FediPod's own page and in NO Mastodon client: Phanpy,
|
|
92
|
+
// Tuba and Whalebird all showed nothing waiting.
|
|
93
|
+
if (pathname === '/api/v1/follow_requests' && req.method === 'GET') {
|
|
94
|
+
const limit = Math.min(Number(url.searchParams.get('limit')) || 40, 80);
|
|
95
|
+
return send(200, api.store.getRequests().slice(0, limit)
|
|
96
|
+
.map((r) => api.account(r.actor)));
|
|
97
|
+
}
|
|
98
|
+
const mReq = /^\/api\/v1\/follow_requests\/([a-f0-9]+)\/(authorize|reject)$/.exec(pathname);
|
|
99
|
+
if (mReq && req.method === 'POST') {
|
|
100
|
+
// The client addresses an account by the id it was given for it, which is
|
|
101
|
+
// this store's own hash of the actor URL — the same one every other
|
|
102
|
+
// account route here uses.
|
|
103
|
+
const actorUrl = api.store.urlFor(mReq[1]);
|
|
104
|
+
if (!actorUrl) return send(404, { error: 'Record not found' });
|
|
105
|
+
if (!api.store.getRequests().some((r) => r.actor === actorUrl)) {
|
|
106
|
+
return send(404, { error: 'Record not found' });
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
if (mReq[2] === 'authorize') await social.admitRequest(api.agent, actorUrl);
|
|
110
|
+
else await social.refuseRequest(api.agent, actorUrl);
|
|
111
|
+
} catch (e) { return send(422, { error: e.message }); }
|
|
112
|
+
await api.store.flush();
|
|
113
|
+
return send(200, api.relationship(actorUrl));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (pathname === '/api/v1/markers') {
|
|
117
|
+
if (req.method === 'POST') {
|
|
118
|
+
const body = await readBody(req);
|
|
119
|
+
const markers = api.store.read('masto-markers.json', {});
|
|
120
|
+
for (const [k, v] of Object.entries(body)) {
|
|
121
|
+
const lastId = v?.last_read_id || v;
|
|
122
|
+
if (typeof lastId === 'string') {
|
|
123
|
+
markers[k] = { last_read_id: lastId, version: (markers[k]?.version || 0) + 1, updated_at: new Date().toISOString() };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
api.store.write('masto-markers.json', markers);
|
|
127
|
+
return send(200, markers);
|
|
128
|
+
}
|
|
129
|
+
return send(200, api.store.read('masto-markers.json', {}));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (pathname === '/api/v1/accounts/relationships') {
|
|
133
|
+
const ids = [...url.searchParams.getAll('id[]'), ...url.searchParams.getAll('id')];
|
|
134
|
+
const rels = ids.map(id => api.store.urlFor(id)).filter(Boolean).map(u => api.relationship(u));
|
|
135
|
+
return send(200, rels);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (pathname === '/api/v1/accounts/search') {
|
|
139
|
+
return send(200, await api.accountSearch(url.searchParams.get('q')));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (pathname === '/api/v1/accounts/lookup') {
|
|
143
|
+
const acct = String(url.searchParams.get('acct') || '').replace(/^@/, '');
|
|
144
|
+
const cfg = api.store.getConfig();
|
|
145
|
+
if (acct === cfg?.handle || acct === `${cfg?.handle}@${api.host}`) {
|
|
146
|
+
return send(200, api.selfAccount());
|
|
147
|
+
}
|
|
148
|
+
const hit = Object.entries(api.store.getActors()).find(([u, a]) => {
|
|
149
|
+
try { return `${a.preferredUsername}@${new URL(u).host}` === acct; } catch { return false; }
|
|
150
|
+
});
|
|
151
|
+
return hit ? send(200, api.account(hit[0])) : send(404, { error: 'Record not found' });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Block and mute, from where the trouble is seen. A block also unfollows —
|
|
155
|
+
// intake refuses a blocked author already — and a mute is view-only: their
|
|
156
|
+
// posts stay out of the timelines, nothing federates.
|
|
157
|
+
const mRel = /^\/api\/v1\/accounts\/([a-f0-9]+)\/(block|unblock|mute|unmute)$/.exec(pathname);
|
|
158
|
+
if (mRel && req.method === 'POST') {
|
|
159
|
+
const actorUrl = api.store.urlFor(mRel[1]);
|
|
160
|
+
if (!actorUrl) return send(404, { error: 'Record not found' });
|
|
161
|
+
if (mRel[2] === 'block' || mRel[2] === 'unblock') {
|
|
162
|
+
if (mRel[2] === 'block') await social.blockActor(api.agent, actorUrl);
|
|
163
|
+
else await social.unblockActor(api.agent, actorUrl);
|
|
164
|
+
} else {
|
|
165
|
+
const m = api.store.getMuted();
|
|
166
|
+
if (mRel[2] === 'mute' && !m.actors.includes(actorUrl)) m.actors.push(actorUrl);
|
|
167
|
+
if (mRel[2] === 'unmute') m.actors = m.actors.filter(a => a !== actorUrl);
|
|
168
|
+
api.store.setMuted(m);
|
|
169
|
+
}
|
|
170
|
+
return send(200, api.relationship(actorUrl));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const mFollow = /^\/api\/v1\/accounts\/([a-f0-9]+)\/(follow|unfollow)$/.exec(pathname);
|
|
174
|
+
if (mFollow && req.method === 'POST') {
|
|
175
|
+
const actorUrl = api.store.urlFor(mFollow[1]);
|
|
176
|
+
if (!actorUrl) return send(404, { error: 'Record not found' });
|
|
177
|
+
if (mFollow[2] === 'follow') await social.followActor(api.agent, actorUrl);
|
|
178
|
+
else await social.unfollowActor(api.agent, actorUrl).catch(() => {}); // already-gone is fine
|
|
179
|
+
return send(200, api.relationship(actorUrl));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const mAccount = /^\/api\/v1\/accounts\/([a-f0-9]+)$/.exec(pathname);
|
|
183
|
+
if (mAccount && req.method === 'GET') {
|
|
184
|
+
const actorUrl = api.store.urlFor(mAccount[1]);
|
|
185
|
+
return actorUrl ? send(200, api.account(actorUrl)) : send(404, { error: 'Record not found' });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Web push: one subscription per client login. The agent pushes payloads
|
|
189
|
+
// to the browser's push service itself, so a closed client still hears.
|
|
190
|
+
if (pathname === '/api/v1/push/subscription') {
|
|
191
|
+
const token = (/^Bearer (.+)$/.exec(req.headers.authorization || '') || [])[1];
|
|
192
|
+
if (!token) return send(401, { error: 'The access token is invalid' });
|
|
193
|
+
// A client that ignores the missing `vapid` and subscribes anyway must
|
|
194
|
+
// not be told it worked — a stored subscription nothing ever pushes to is
|
|
195
|
+
// the same silent nothing the toggle was.
|
|
196
|
+
if (!api.webPush) return send(422, { error: 'this instance does not send web push' });
|
|
197
|
+
if (req.method === 'GET') {
|
|
198
|
+
const sub = api.push.get(token);
|
|
199
|
+
return sub ? send(200, api.push.json(token, sub)) : send(404, { error: 'Record not found' });
|
|
200
|
+
}
|
|
201
|
+
if (req.method === 'POST') {
|
|
202
|
+
const body = await readBody(req);
|
|
203
|
+
const sub = api.push.set(token, {
|
|
204
|
+
endpoint: body.subscription?.endpoint,
|
|
205
|
+
keys: body.subscription?.keys,
|
|
206
|
+
alerts: body.data?.alerts,
|
|
207
|
+
});
|
|
208
|
+
if (!sub) return send(422, { error: 'a https endpoint and p256dh/auth keys are required' });
|
|
209
|
+
return send(200, api.push.json(token, sub));
|
|
210
|
+
}
|
|
211
|
+
if (req.method === 'PUT') {
|
|
212
|
+
const body = await readBody(req);
|
|
213
|
+
const sub = api.push.setAlerts(token, body.data?.alerts);
|
|
214
|
+
return sub ? send(200, api.push.json(token, sub)) : send(404, { error: 'Record not found' });
|
|
215
|
+
}
|
|
216
|
+
if (req.method === 'DELETE') {
|
|
217
|
+
api.push.drop(token);
|
|
218
|
+
return send(200, {});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (/^\/api\/v1\/accounts\/[a-f0-9]+\/featured_tags$/.test(pathname)) return send(200, []);
|
|
223
|
+
|
|
224
|
+
// The counts these back are rendered from the same two arrays (see
|
|
225
|
+
// `account`), so a client that shows a number here can always open it.
|
|
226
|
+
const mAccList = /^\/api\/v1\/accounts\/([a-f0-9]+)\/(following|followers)$/.exec(pathname);
|
|
227
|
+
if (mAccList && req.method === 'GET') {
|
|
228
|
+
const actorUrl = api.store.urlFor(mAccList[1]);
|
|
229
|
+
if (!actorUrl) return send(404, { error: 'Record not found' });
|
|
230
|
+
// Only our own lists are known. A remote actor's collections live on its
|
|
231
|
+
// own server, and opening a profile is not worth a fetch of a stranger's
|
|
232
|
+
// pod — an empty list, not an error, is what the API can honestly say.
|
|
233
|
+
const mine = actorUrl === api.urls?.actor;
|
|
234
|
+
const c = api.store.getContacts();
|
|
235
|
+
// Reversed: the contact arrays append, and page()'s cursors read
|
|
236
|
+
// newest-first — fed as stored, since_id answered with its complement.
|
|
237
|
+
const recs = (!mine ? []
|
|
238
|
+
: mAccList[2] === 'followers' ? c.followers
|
|
239
|
+
: c.following.filter(f => f.accepted)) // pending is not following
|
|
240
|
+
.slice().reverse();
|
|
241
|
+
const { items, headers } = api.page(recs, url,
|
|
242
|
+
{ limit: 40, max: 80, idOf: (r) => api.store.idFor(r.actor) });
|
|
243
|
+
return send(200, items.map(r => api.account(r.actor)), headers);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Who among the people I follow also follows THEM — a graph we do not hold,
|
|
247
|
+
// and the shape is an entry per requested account, not a bare list.
|
|
248
|
+
if (pathname === '/api/v1/accounts/familiar_followers') {
|
|
249
|
+
const ids = [...url.searchParams.getAll('id[]'), ...url.searchParams.getAll('id')];
|
|
250
|
+
return send(200, ids.map(id => ({ id, accounts: [] })));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const mAccStatuses = /^\/api\/v1\/accounts\/([a-f0-9]+)\/statuses$/.exec(pathname);
|
|
254
|
+
if (mAccStatuses) {
|
|
255
|
+
const actorUrl = api.store.urlFor(mAccStatuses[1]);
|
|
256
|
+
const all = api.store.getStatuses();
|
|
257
|
+
const pinnedOnly = url.searchParams.get('pinned') === 'true';
|
|
258
|
+
const { items, headers } = api.page(
|
|
259
|
+
all.filter(s => s.actor === actorUrl && (!pinnedOnly || s.pinned)), url);
|
|
260
|
+
return send(200, items.map(s => api.statusOrBoost(s, { all })), headers);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// body.mjs — reading what a client sent: JSON or form-encoded bodies, the
|
|
2
|
+
// poll a compose request carries, and the text of a post back out of its
|
|
3
|
+
// HTML.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A poll out of a compose request, or null when there is none. A JSON client
|
|
9
|
+
* sends a `poll` object; a form-encoded one spells the same thing out in
|
|
10
|
+
* Rails's bracket notation, which is the shape the option list arrives in.
|
|
11
|
+
*/
|
|
12
|
+
export function pollParams(body) {
|
|
13
|
+
const nested = body?.poll && typeof body.poll === 'object' ? body.poll : null;
|
|
14
|
+
const options = [].concat(nested?.options ?? body?.['poll[options][]'] ?? [])
|
|
15
|
+
.map(o => String(o ?? '').trim()).filter(Boolean);
|
|
16
|
+
const rawExpiry = nested?.expires_in ?? body?.['poll[expires_in]'];
|
|
17
|
+
const rawMultiple = nested?.multiple ?? body?.['poll[multiple]'];
|
|
18
|
+
if (!options.length && rawExpiry === undefined) return null;
|
|
19
|
+
return {
|
|
20
|
+
options,
|
|
21
|
+
expiresIn: rawExpiry === undefined || rawExpiry === null || rawExpiry === ''
|
|
22
|
+
? null : Number(rawExpiry),
|
|
23
|
+
multiple: rawMultiple === true || rawMultiple === 'true' || rawMultiple === '1',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Accepts JSON or form-encoded bodies (OAuth posts are often form-encoded).
|
|
28
|
+
// The source of a post that predates raw-text storage: its HTML back to
|
|
29
|
+
// typed text, near enough to edit.
|
|
30
|
+
export function htmlToText(html) {
|
|
31
|
+
return String(html)
|
|
32
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
33
|
+
.replace(/<\/p>\s*<p[^>]*>/gi, '\n\n')
|
|
34
|
+
.replace(/<[^>]+>/g, '')
|
|
35
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
36
|
+
.replace(/"/g, '"').replace(/'/g, "'")
|
|
37
|
+
.trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function readBody(req) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
let data = '';
|
|
43
|
+
req.on('data', c => {
|
|
44
|
+
data += c;
|
|
45
|
+
// Destroying the socket without settling left the awaiting handler
|
|
46
|
+
// pending for the life of the process and the client waiting on a
|
|
47
|
+
// response that would never come.
|
|
48
|
+
if (data.length > 1e6) { req.destroy(); reject(new Error('request body too large')); }
|
|
49
|
+
});
|
|
50
|
+
req.on('end', () => {
|
|
51
|
+
const ct = String(req.headers['content-type'] || '');
|
|
52
|
+
try {
|
|
53
|
+
if (ct.includes('application/json')) return resolve(data ? JSON.parse(data) : {});
|
|
54
|
+
// A form-encoded list is the same key repeated, spelled with a
|
|
55
|
+
// trailing `[]`. Reading it as a plain object kept only the last one,
|
|
56
|
+
// so a client sending its poll or its media that way lost all but the
|
|
57
|
+
// final value. Only the `[]` keys become lists: everything else keeps
|
|
58
|
+
// the single value the rest of this file reads.
|
|
59
|
+
const form = new URLSearchParams(data);
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const key of new Set(form.keys())) {
|
|
62
|
+
out[key] = key.endsWith('[]') ? form.getAll(key) : form.get(key);
|
|
63
|
+
}
|
|
64
|
+
resolve(out);
|
|
65
|
+
} catch (e) { reject(e); }
|
|
66
|
+
});
|
|
67
|
+
req.on('error', reject);
|
|
68
|
+
});
|
|
69
|
+
}
|