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
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
import crypto from 'node:crypto';
|
|
19
19
|
import { handleDelivery } from './gateway-core.mjs';
|
|
20
|
-
import { readCapped } from '
|
|
21
|
-
import
|
|
20
|
+
import { readCapped, safeFetch, isLoopbackHost } from '../shared/safefetch.mjs';
|
|
21
|
+
import * as podRoot from '../pod/root.mjs';
|
|
22
|
+
import * as podPolicy from '../pod/policy.mjs';
|
|
22
23
|
|
|
23
24
|
// The one WebFinger document, spelled out here rather than imported from
|
|
24
25
|
// wire.mjs: wire drags the agent's whole HTML pipeline (sanitize-html and
|
|
@@ -58,25 +59,122 @@ const OWNER_LOOKUP_MS = 5_000;
|
|
|
58
59
|
* authority on that, so when it answers, its answer decides. A server that
|
|
59
60
|
* says nothing leaves where the WebID lives as the only evidence there is.
|
|
60
61
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const res = await fetchImpl(podBase, {
|
|
64
|
-
method: 'HEAD', signal: AbortSignal.timeout(OWNER_LOOKUP_MS),
|
|
65
|
-
});
|
|
66
|
-
return linkTargets(res?.headers?.get?.('link'), REL.owner, podBase);
|
|
67
|
-
} catch { return []; }
|
|
68
|
-
}
|
|
62
|
+
const podOwners = (podBase, fetchImpl = fetch) =>
|
|
63
|
+
podRoot.readOwnerLinks(fetchImpl, podBase, { timeoutMs: OWNER_LOOKUP_MS });
|
|
69
64
|
|
|
65
|
+
// Whether a token's WebID vouches for this pod, on the evidence of where it
|
|
66
|
+
// lives. Weak on purpose, and never used alone any more — see provesPod.
|
|
70
67
|
function webidUnderPod(webid, podHome) {
|
|
71
68
|
try { return new URL(webid).origin === new URL(podHome).origin; } catch { return false; }
|
|
72
69
|
}
|
|
73
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Whether this WebID may speak for this pod.
|
|
73
|
+
*
|
|
74
|
+
* Sharing an origin was the whole test, which is right where an origin is one
|
|
75
|
+
* person (a subdomain pod) and wrong where it is not. On a path-based server
|
|
76
|
+
* every account is on the same origin, so any account holder could attach
|
|
77
|
+
* somebody else's pod under a name of their choosing — and, worse, re-attach a
|
|
78
|
+
* name that already existed and be handed its receipt secret (S7).
|
|
79
|
+
*
|
|
80
|
+
* The pod server is the authority on who owns the pod, and it will say so in a
|
|
81
|
+
* `solid:owner` link if asked. When it answers, its answer decides. Only when
|
|
82
|
+
* it says nothing does this fall back to where the WebID lives, which is the
|
|
83
|
+
* best a silent server leaves available. This is the rule /api/agent's opt-in
|
|
84
|
+
* has used all along; attach simply never asked.
|
|
85
|
+
*/
|
|
86
|
+
async function provesPod(webid, podHome, fetchImpl) {
|
|
87
|
+
const owners = await podOwners(podHome, fetchImpl || fetch);
|
|
88
|
+
if (owners.length) return owners.includes(webid);
|
|
89
|
+
return webidUnderPod(webid, podHome);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// A pod this front will read from at all.
|
|
93
|
+
//
|
|
94
|
+
// The front reads `rec.podHome + <the rest of the request path>` and, on the
|
|
95
|
+
// CSS server component, that read goes straight into the resource store with
|
|
96
|
+
// no access control of its own — so a row whose podHome names the FRONT is a
|
|
97
|
+
// row that can read the server's own internals, `/.internal/fedipod/directory/`
|
|
98
|
+
// included, where every user's receipt secret is kept (S2). The front is not a
|
|
99
|
+
// pod and never was one; saying so outright is the fix, and it closes the
|
|
100
|
+
// self-recursion of S8 with the same line.
|
|
101
|
+
function podHomeProblem(podHome, frontOrigin) {
|
|
102
|
+
let u;
|
|
103
|
+
try { u = new URL(podHome); } catch { return 'podHome is not a URL'; }
|
|
104
|
+
if (frontOrigin) {
|
|
105
|
+
try {
|
|
106
|
+
if (u.origin === new URL(frontOrigin).origin) {
|
|
107
|
+
return 'a pod cannot live on this gateway — name the pod that holds your data';
|
|
108
|
+
}
|
|
109
|
+
} catch { /* no usable frontOrigin: the other checks still apply */ }
|
|
110
|
+
}
|
|
111
|
+
// Nothing may reach a server's private trees, whatever origin they are on.
|
|
112
|
+
if (/(^|\/)\.internal(\/|$)/u.test(u.pathname) || /(^|\/)\.\.(\/|$)/u.test(u.pathname)) {
|
|
113
|
+
return 'that is not a pod address';
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const RELAY_MAX_REQUESTS = 20;
|
|
119
|
+
const RELAY_MAX_BODY = 1024 * 1024;
|
|
120
|
+
const RELAY_TIMEOUT_MS = 8_000;
|
|
121
|
+
// The only headers a relayed request may carry to the remote server. Host
|
|
122
|
+
// comes from the URL; the user agent from safeFetch.
|
|
123
|
+
const RELAY_HEADERS = new Set(['date', 'digest', 'signature', 'content-type', 'accept']);
|
|
124
|
+
|
|
125
|
+
const keyIdOf = (signature) => (/keyId="([^"]+)"/.exec(signature || '') || [])[1] || null;
|
|
126
|
+
|
|
127
|
+
async function relayOne(item, rec, fetchImpl) {
|
|
128
|
+
const url = String(item?.url || '');
|
|
129
|
+
const method = String(item?.method || 'POST').toUpperCase();
|
|
130
|
+
if (method !== 'GET' && method !== 'POST') return { url, status: 0, error: 'method must be GET or POST' };
|
|
131
|
+
let u;
|
|
132
|
+
try { u = new URL(url); } catch { return { url, status: 0, error: 'not a URL' }; }
|
|
133
|
+
if (u.protocol !== 'https:' && !(u.protocol === 'http:' && process.env.AP_ALLOW_PRIVATE_TARGETS === '1')) {
|
|
134
|
+
return { url, status: 0, error: 'https only' };
|
|
135
|
+
}
|
|
136
|
+
const headers = {};
|
|
137
|
+
for (const [k, v] of Object.entries(item?.headers || {})) {
|
|
138
|
+
const name = k.toLowerCase();
|
|
139
|
+
if (RELAY_HEADERS.has(name) && typeof v === 'string') headers[name] = v;
|
|
140
|
+
}
|
|
141
|
+
const body = method === 'POST' ? String(item?.body ?? '') : undefined;
|
|
142
|
+
if (body !== undefined && Buffer.byteLength(body) > RELAY_MAX_BODY) return { url, status: 0, error: 'body too large' };
|
|
143
|
+
const keyId = keyIdOf(headers.signature);
|
|
144
|
+
if (method === 'POST' && !keyId) return { url, status: 0, error: 'a delivery must be signed' };
|
|
145
|
+
if (keyId && !keyId.startsWith(rec.actorUrl + '#')) return { url, status: 0, error: "signed with a key that is not this account's" };
|
|
146
|
+
if (headers.digest) {
|
|
147
|
+
const want = 'SHA-256=' + crypto.createHash('sha256').update(body || '').digest('base64');
|
|
148
|
+
if (headers.digest !== want) return { url, status: 0, error: 'digest does not match the body' };
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const res = await safeFetch(url, { method, headers, body, signal: AbortSignal.timeout(RELAY_TIMEOUT_MS) }, fetchImpl);
|
|
152
|
+
const out = { url, status: res.status };
|
|
153
|
+
// The far server asking to be left alone has to reach the agent that will
|
|
154
|
+
// do the asking again. Without this the browser build could not honour a
|
|
155
|
+
// Retry-After at all — every delivery it makes goes through here — and fell
|
|
156
|
+
// back to its own ladder against a server that had already said how long.
|
|
157
|
+
const retryAfter = res.headers.get('retry-after');
|
|
158
|
+
if (retryAfter) out.retryAfter = retryAfter;
|
|
159
|
+
if (method === 'GET') {
|
|
160
|
+
out.contentType = res.headers.get('content-type') || null;
|
|
161
|
+
out.body = await readCapped(res, RELAY_MAX_BODY);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
} catch (e) { return { url, status: 0, error: e.message }; }
|
|
165
|
+
}
|
|
166
|
+
|
|
74
167
|
const j = (status, obj, ct = 'application/json') =>
|
|
75
168
|
({ status, headers: { 'content-type': ct, 'cache-control': 'no-store' }, body: JSON.stringify(obj) });
|
|
76
169
|
const notFound = () => ({ status: 404, headers: { 'content-type': 'text/plain' }, body: 'not found\n' });
|
|
77
170
|
|
|
78
171
|
// A user's public base on the front, a 1:1 mirror of their pod home.
|
|
79
|
-
|
|
172
|
+
// The directory key is the FULL fediverse address — handle@host — never the
|
|
173
|
+
// bare handle, which is not unique: two different pods can each have a "me".
|
|
174
|
+
// A fronted identity answers at this front's host; a mail-door identity at its
|
|
175
|
+
// own pod's host. The `/u/` path segment carries that same key.
|
|
176
|
+
export const addressKey = (handle, host) => `${handle}@${host}`;
|
|
177
|
+
export const userBase = (frontOrigin, key) => `${frontOrigin}/u/${encodeURIComponent(key)}/`;
|
|
80
178
|
|
|
81
179
|
// Rewrite every occurrence of the pod home to the front base (or back). The
|
|
82
180
|
// bodies are JSON, so a whole-string swap rewrites ids wherever they appear —
|
|
@@ -90,7 +188,7 @@ function parseUserPath(pathname) {
|
|
|
90
188
|
}
|
|
91
189
|
|
|
92
190
|
// Top-level paths a handle may not take, so a name never shadows a route.
|
|
93
|
-
const RESERVED = new Set(['u', 'api', 'signup', 'new-account', 'run', 'admin', 'gateway',
|
|
191
|
+
const RESERVED = new Set(['u', 'api', 'signup', 'new-account', 'run', 'admin', 'roster', 'gateway',
|
|
94
192
|
'gw', 'well-known', 'inbox', 'outbox', 'actor', 'install']);
|
|
95
193
|
|
|
96
194
|
// Why a handle is unusable, or null when it is fine. Lowercase letters, digits
|
|
@@ -114,19 +212,12 @@ function handleProblem(h) {
|
|
|
114
212
|
const POLICY_TTL_MS = 5 * 60_000;
|
|
115
213
|
const policyCache = new Map(); // podHome -> { at, policy }
|
|
116
214
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const res = await fetchImpl(rec.podHome + 'ap/gateway-policy.json',
|
|
124
|
-
{ headers: { accept: 'application/json' } });
|
|
125
|
-
if (res && res.status < 400) policy = JSON.parse(await readCapped(res, 256 * 1024));
|
|
126
|
-
} catch { /* unpublished or unreachable: the row's own fields stand */ }
|
|
127
|
-
policyCache.set(key, { at: Date.now(), policy });
|
|
128
|
-
return policy;
|
|
129
|
-
}
|
|
215
|
+
// The cache is OURS, not the library's: on a warm serverless container one Map
|
|
216
|
+
// is shared across every user this front serves, so what it is keyed by is a
|
|
217
|
+
// correctness property. Keyed by podHome, it is one person's policy under one
|
|
218
|
+
// person's key.
|
|
219
|
+
const policyFor = (rec, fetchImpl = fetch) =>
|
|
220
|
+
podPolicy.read(fetchImpl, rec.podHome, { cache: policyCache, ttlMs: POLICY_TTL_MS });
|
|
130
221
|
|
|
131
222
|
// The verified-delivery identity for a directory record: what gateway-core
|
|
132
223
|
// needs to check what concerns the user and to append to their pod inbox.
|
|
@@ -152,8 +243,61 @@ function identFor(rec, policy = null) {
|
|
|
152
243
|
// listDirectory() -> { handle: record } every row, for the admin roster
|
|
153
244
|
// removeDirectory(handle) -> boolean drop a row; false when a seeded row remains
|
|
154
245
|
// podPut(url, body, ct) -> boolean append to a user's pod (per-user cred inside)
|
|
155
|
-
// podGet(url) -> Response
|
|
246
|
+
// podGet(url, { podHome }) -> Response read a user's pod (public reads; plain fetch
|
|
247
|
+
// is fine). `podHome` is the row's own pod, for
|
|
248
|
+
// an adapter that reads a store directly and so
|
|
249
|
+
// must confine the read itself.
|
|
156
250
|
export async function routeFront(request, ctx) {
|
|
251
|
+
const out = await route(request, ctx);
|
|
252
|
+
return { ...out, headers: withSecurityHeaders(out.headers, out.body) };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Every response this file makes, hardened in one place rather than in each of
|
|
256
|
+
// the dozen shapes below.
|
|
257
|
+
//
|
|
258
|
+
// `nosniff` matters most: the front serves user-supplied JSON straight from
|
|
259
|
+
// somebody's pod (the proxied actor and object documents), and without it a
|
|
260
|
+
// browser is free to decide for itself that a document is HTML and run what is
|
|
261
|
+
// inside it. The rest is the same posture the app already has — nothing may be
|
|
262
|
+
// framed, no base tag may be rewritten, no plugin content.
|
|
263
|
+
//
|
|
264
|
+
// A content-security-policy goes on the HTML only: it would mean nothing on a
|
|
265
|
+
// JSON document, and `frame-ancestors` has to be a header rather than a meta
|
|
266
|
+
// tag anyway.
|
|
267
|
+
//
|
|
268
|
+
// `script-src 'self'` is the one that matters, and it is only possible because
|
|
269
|
+
// none of these pages carries inline script any more — each has its own file and
|
|
270
|
+
// its own route above. A policy cannot tell an inline block the author wrote
|
|
271
|
+
// from one an attacker injected, so as long as any inline script has to run,
|
|
272
|
+
// every inline script may.
|
|
273
|
+
//
|
|
274
|
+
// `connect-src` allows https: because the pages sign in against the user's own
|
|
275
|
+
// pod, which is a different origin by definition and not one we can name here.
|
|
276
|
+
function withSecurityHeaders(headers = {}, body = null) {
|
|
277
|
+
const ct = String(headers['content-type'] || '');
|
|
278
|
+
const isHtml = ct.startsWith('text/html');
|
|
279
|
+
return {
|
|
280
|
+
...headers,
|
|
281
|
+
'x-content-type-options': 'nosniff',
|
|
282
|
+
'referrer-policy': 'same-origin',
|
|
283
|
+
'x-frame-options': 'SAMEORIGIN',
|
|
284
|
+
...(isHtml && body ? {
|
|
285
|
+
'content-security-policy': [
|
|
286
|
+
"default-src 'self'",
|
|
287
|
+
"script-src 'self'", // no inline script: see above
|
|
288
|
+
"style-src 'self' 'unsafe-inline'",
|
|
289
|
+
"img-src 'self' https: data:",
|
|
290
|
+
"connect-src 'self' https:", // sign-in goes to the user's own pod
|
|
291
|
+
"object-src 'none'", // no plugin content, ever
|
|
292
|
+
"base-uri 'none'", // no rewriting where relative URLs resolve
|
|
293
|
+
"frame-ancestors 'self'", // nobody else may frame these pages
|
|
294
|
+
"form-action 'self'", // a form here submits here
|
|
295
|
+
].join('; '),
|
|
296
|
+
} : {}),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function route(request, ctx) {
|
|
157
301
|
const url = new URL(request.url);
|
|
158
302
|
const { pathname } = url;
|
|
159
303
|
|
|
@@ -176,9 +320,10 @@ export async function routeFront(request, ctx) {
|
|
|
176
320
|
return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: ctx.runPage };
|
|
177
321
|
}
|
|
178
322
|
|
|
179
|
-
// The
|
|
323
|
+
// The roster page: the host reading who has accounts here. The page signs in
|
|
180
324
|
// and calls the roster API below; a deploy with no admin supplies no page.
|
|
181
|
-
|
|
325
|
+
// Off /admin, which is the owner's own record page on every agent.
|
|
326
|
+
if (pathname === '/roster') {
|
|
182
327
|
if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
|
|
183
328
|
if (!ctx.adminPage) return notFound();
|
|
184
329
|
return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: ctx.adminPage };
|
|
@@ -197,9 +342,9 @@ export async function routeFront(request, ctx) {
|
|
|
197
342
|
.map((r) => ({
|
|
198
343
|
handle: r.handle, kind: r.kind || 'person', fronted: !r.inboxOnly,
|
|
199
344
|
podHome: r.podHome, webId: r.webId || null, actorUrl: r.actorUrl,
|
|
200
|
-
address: `@${r.handle}@${ctx.host}`,
|
|
345
|
+
address: r.address || `@${r.handle}@${ctx.host}`,
|
|
201
346
|
}))
|
|
202
|
-
.sort((a, b) => a.handle.localeCompare(b.handle));
|
|
347
|
+
.sort((a, b) => (a.address || a.handle).localeCompare(b.address || b.handle));
|
|
203
348
|
return j(200, { host: ctx.host, accounts });
|
|
204
349
|
}
|
|
205
350
|
|
|
@@ -213,11 +358,13 @@ export async function routeFront(request, ctx) {
|
|
|
213
358
|
if (webid !== ctx.adminWebId) return j(403, { error: 'that WebID is not the admin of this front' });
|
|
214
359
|
let body;
|
|
215
360
|
try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
return j(
|
|
220
|
-
|
|
361
|
+
// Revoke by full address (what the roster lists); a bare handle still works
|
|
362
|
+
// for any legacy row keyed that way.
|
|
363
|
+
const key = String(body.address || body.handle || '').replace(/^@/, '').toLowerCase();
|
|
364
|
+
if (!key || !(await ctx.lookup(key))) return j(404, { error: 'no such account' });
|
|
365
|
+
const removed = (await ctx.removeDirectory(key)) === true;
|
|
366
|
+
return j(200, removed ? { account: key, removed }
|
|
367
|
+
: { account: key, removed: false,
|
|
221
368
|
reason: 'this row is seeded in the deploy environment (FEDIPOD_DIRECTORY_JSON) — remove it there and redeploy' });
|
|
222
369
|
}
|
|
223
370
|
|
|
@@ -247,38 +394,94 @@ export async function routeFront(request, ctx) {
|
|
|
247
394
|
const podHome = String(body.podHome || '');
|
|
248
395
|
const problem = handleProblem(handle);
|
|
249
396
|
if (problem) return j(400, { error: problem });
|
|
250
|
-
|
|
251
|
-
if (
|
|
397
|
+
const podLoopback = (() => { try { return isLoopbackHost(new URL(podHome).hostname); } catch { return false; } })();
|
|
398
|
+
if (!(/^https:\/\/\S+\/$/.test(podHome) || (/^http:\/\/\S+\/$/.test(podHome) && podLoopback))) {
|
|
399
|
+
return j(400, { error: 'podHome must be an https URL ending in /' });
|
|
400
|
+
}
|
|
401
|
+
const badPod = podHomeProblem(podHome, ctx.frontOrigin);
|
|
402
|
+
if (badPod) return j(400, { error: badPod });
|
|
252
403
|
const webid = await verifyPodToken(request, pathname, ctx.verifier);
|
|
253
404
|
if (!webid) return j(401, { error: 'a Solid-OIDC token proving the pod is required' });
|
|
254
|
-
if (!
|
|
405
|
+
if (!await provesPod(webid, podHome, ctx.fetchImpl)) {
|
|
255
406
|
return j(403, { error: 'the token proves a different pod than the one you listed' });
|
|
256
407
|
}
|
|
257
408
|
// The usual attachment keeps the user's identity on their pod and moves
|
|
258
409
|
// only the mail door here (@me@mypod). Fronted identity — actor ids on
|
|
259
410
|
// this domain — remains available to an explicit `fronted: true`.
|
|
260
411
|
const fronted = body.fronted === true;
|
|
261
|
-
|
|
262
|
-
|
|
412
|
+
// The full address is the key: a fronted name answers at this front's host,
|
|
413
|
+
// a mail-door name at its pod's host. The same handle on two pods is two
|
|
414
|
+
// distinct addresses, so it never collides.
|
|
415
|
+
let addressHost; try { addressHost = fronted ? ctx.host : new URL(podHome).host; } catch { return j(400, { error: 'bad podHome' }); }
|
|
416
|
+
// A fronted name answers at this front's host, where the handle alone is the
|
|
417
|
+
// whole address and is unique — so it keeps keying by bare handle (and its
|
|
418
|
+
// actor id stays /u/<handle>/, unchanged for existing accounts). A mail-door
|
|
419
|
+
// name answers at its own pod, where "me" is not unique, so it keys by the
|
|
420
|
+
// full address handle@podhost, which the /u/ segment then carries.
|
|
421
|
+
const key = fronted ? handle : addressKey(handle, addressHost);
|
|
422
|
+
const address = `@${handle}@${addressHost}`;
|
|
423
|
+
// "Taken" only when this exact address already belongs to a DIFFERENT pod.
|
|
424
|
+
// Re-attaching your own pod — a retry, a re-provision, a second browser — is
|
|
425
|
+
// idempotent: it updates the row in place and keeps the same door secret.
|
|
426
|
+
const prior = await ctx.lookup(key);
|
|
427
|
+
if (prior && prior.podHome !== podHome) return j(409, { error: 'that name is taken' });
|
|
428
|
+
// Re-attaching is idempotent — a retry, a re-provision, a second browser —
|
|
429
|
+
// and it hands back the EXISTING receipt secret. So it has to be the same
|
|
430
|
+
// person: matching podHome was enough on a subdomain server and, on a
|
|
431
|
+
// path-based one, meant any co-tenant could re-attach your row, flip its
|
|
432
|
+
// kind, and be given your secret (S7). A row that predates `webId` has
|
|
433
|
+
// nothing to compare and falls back to the pod check above.
|
|
434
|
+
if (prior?.webId && prior.webId !== webid) {
|
|
435
|
+
return j(409, { error: 'that name belongs to a different account on this pod' });
|
|
436
|
+
}
|
|
437
|
+
// The pod's actor lives under its AP root container, which the pod owner
|
|
438
|
+
// knows and the front does not — so take the caller's actorUrl when it is on
|
|
439
|
+
// this pod, and fall back only when none is given.
|
|
440
|
+
const claimedActor = typeof body.actorUrl === 'string' && body.actorUrl.startsWith(podHome) ? body.actorUrl : null;
|
|
441
|
+
const actorUrl = fronted ? `${userBase(ctx.frontOrigin, key)}ap/actor` : (claimedActor || podHome + 'ap/actor');
|
|
442
|
+
const hmacSecret = prior?.hmacSecret || crypto.randomBytes(32).toString('base64');
|
|
263
443
|
const record = {
|
|
264
|
-
handle, podHome, webId: webid, actorUrl,
|
|
444
|
+
handle, address, podHome, webId: webid, actorUrl,
|
|
265
445
|
kind: body.kind === 'group' ? 'group' : 'person',
|
|
266
446
|
gatewayWebId: ctx.gatewayWebId || null, hmacSecret,
|
|
267
447
|
...(fronted ? {} : { inboxOnly: true }),
|
|
268
448
|
};
|
|
269
|
-
await ctx.putDirectory(
|
|
449
|
+
await ctx.putDirectory(key, record);
|
|
270
450
|
// The secret is returned ONCE, with the one command the user runs to
|
|
271
451
|
// point their agent at this gateway.
|
|
272
|
-
const doorInbox = `${ctx.frontOrigin}
|
|
452
|
+
const doorInbox = `${userBase(ctx.frontOrigin, key)}ap/inbox/`;
|
|
273
453
|
if (fronted) {
|
|
274
|
-
return j(201, { ok: true, handle, actorUrl, frontActor: actorUrl, hmacSecret,
|
|
275
|
-
address: `@${handle}@${ctx.host}`,
|
|
454
|
+
return j(201, { ok: true, handle, address, actorUrl, frontActor: actorUrl, hmacSecret,
|
|
276
455
|
command: `fedipod gateway ${actorUrl} --secret ${hmacSecret}` });
|
|
277
456
|
}
|
|
278
|
-
return j(201, { ok: true, handle, doorInbox, hmacSecret,
|
|
457
|
+
return j(201, { ok: true, handle, address, doorInbox, hmacSecret,
|
|
279
458
|
command: `fedipod gateway ${doorInbox} --secret ${hmacSecret} --inbox-only` });
|
|
280
459
|
}
|
|
281
460
|
|
|
461
|
+
// The relay: the front sends requests a browser has already signed. A page
|
|
462
|
+
// may not set the Date or Host header, and both are inside an HTTP
|
|
463
|
+
// signature, so a browser-run agent signs and hands the request here; the
|
|
464
|
+
// front sends it with exactly the headers that were signed. It can forward
|
|
465
|
+
// a signature and cannot make one. Only the account's own key may sign,
|
|
466
|
+
// only the account's own pod may call, and nothing unsigned-for reaches
|
|
467
|
+
// the remote server.
|
|
468
|
+
if (pathname === '/api/relay' && request.method === 'POST') {
|
|
469
|
+
let body;
|
|
470
|
+
try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
|
|
471
|
+
const handle = String(body.handle || '').toLowerCase();
|
|
472
|
+
const rec = await ctx.lookup(handle);
|
|
473
|
+
if (!rec) return j(404, { error: 'no such account' });
|
|
474
|
+
const webid = await verifyPodToken(request, pathname, ctx.verifier);
|
|
475
|
+
if (!webid) return j(401, { error: 'a Solid-OIDC token proving the pod is required' });
|
|
476
|
+
const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
|
|
477
|
+
if (!owner) return j(403, { error: "the token proves a different pod than this account's" });
|
|
478
|
+
const items = Array.isArray(body.requests) ? body.requests : [];
|
|
479
|
+
if (!items.length) return j(400, { error: 'requests must be a non-empty list' });
|
|
480
|
+
if (items.length > RELAY_MAX_REQUESTS) return j(400, { error: `at most ${RELAY_MAX_REQUESTS} requests per call` });
|
|
481
|
+
const results = await Promise.all(items.map((it) => relayOne(it, rec, ctx.fetchImpl || fetch)));
|
|
482
|
+
return j(200, { results });
|
|
483
|
+
}
|
|
484
|
+
|
|
282
485
|
// Runtime opt-in: a pod owner asks the server this front sits on to RUN
|
|
283
486
|
// their identity. Same proof as attach — a Solid-OIDC token — but the
|
|
284
487
|
// ownership check is stricter: the WebID must live UNDER the pod base, not
|
|
@@ -327,6 +530,20 @@ export async function routeFront(request, ctx) {
|
|
|
327
530
|
return j(400, { error: 'action must be opt-in or opt-out' });
|
|
328
531
|
}
|
|
329
532
|
|
|
533
|
+
// Each page's own script. They were inline until 2026-09-09; out here the
|
|
534
|
+
// pages can be served under `script-src 'self'`, which is the directive that
|
|
535
|
+
// tells running code from injected markup — and inline script defeats it by
|
|
536
|
+
// construction. Same mechanism the vendored library below has always used.
|
|
537
|
+
//
|
|
538
|
+
// The name is matched against a literal list, so nothing about the request
|
|
539
|
+
// chooses a file.
|
|
540
|
+
const mPageScript = /^\/(new-account|run|admin)\.js$/u.exec(pathname);
|
|
541
|
+
if (mPageScript) {
|
|
542
|
+
const body = ctx.pageScripts?.[`${mPageScript[1]}.js`];
|
|
543
|
+
if (!body) return notFound();
|
|
544
|
+
return { status: 200, headers: { 'content-type': 'text/javascript; charset=utf-8' }, body };
|
|
545
|
+
}
|
|
546
|
+
|
|
330
547
|
// The vendored Solid-OIDC browser library the /run and /admin pages load —
|
|
331
548
|
// served here because this function owns every path on the domain.
|
|
332
549
|
if (pathname === '/solid-oidc-client.js') {
|
|
@@ -373,9 +590,15 @@ export async function routeFront(request, ctx) {
|
|
|
373
590
|
// fixed to the front so a consumer cross-checks it consistently.
|
|
374
591
|
if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
|
|
375
592
|
const podTarget = rec.podHome + up.rest;
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
593
|
+
// The pod this read belongs to travels with it: an adapter reading a store
|
|
594
|
+
// directly (the CSS server component) has no access control of its own and
|
|
595
|
+
// needs to be told what it may reach. See podHomeProblem above for the other
|
|
596
|
+
// half — a row that names something which is not a pod.
|
|
597
|
+
const got = await podRoot.readPublicDocument(
|
|
598
|
+
ctx.podGet || ((u) => fetch(u, { headers: { accept: AP_CT } })),
|
|
599
|
+
podTarget, { podHome: rec.podHome });
|
|
600
|
+
if (got.text === null) return { status: got.status, headers: {}, body: '' };
|
|
601
|
+
let text = got.text;
|
|
379
602
|
text = swap(text, rec.podHome, base);
|
|
380
603
|
if (up.rest === 'ap/actor') {
|
|
381
604
|
try {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import crypto from 'node:crypto';
|
|
16
16
|
import { verifyHttpSignature, makeSafeLoader, makeReceipt, signReceipt } from './httpsig.mjs';
|
|
17
|
+
import * as inbox from '../pod/inbox.mjs';
|
|
17
18
|
|
|
18
19
|
const DEFAULT_MAX_BYTES = 512 * 1024; // mirror intake.mjs MAX_ITEM_BYTES
|
|
19
20
|
|
|
@@ -100,17 +101,12 @@ export async function handleDelivery(request, ident, { podPut, fetchImpl = fetch
|
|
|
100
101
|
? signReceipt(makeReceipt(v, { gateway: ident.gatewayWebId }), ident.hmacSecret)
|
|
101
102
|
: null;
|
|
102
103
|
const hash = sha256hex(raw);
|
|
103
|
-
const okA = await podPut
|
|
104
|
+
const okA = await inbox.appendVerifiedDelivery(podPut, ident.inboxUrl, hash, raw);
|
|
104
105
|
// A pod-write failure returns 5xx so the ORIGIN retries over its own ladder —
|
|
105
106
|
// that is what preserves the pod's buffer property without the gateway
|
|
106
107
|
// holding any state.
|
|
107
108
|
if (!okA) return { status: 502, reason: 'pod inbox write failed' };
|
|
108
|
-
|
|
109
|
-
// receipt did not is simply unverified, never lost.
|
|
110
|
-
if (receipt) {
|
|
111
|
-
await podPut(ident.inboxUrl + hash + '.receipt.json', JSON.stringify(receipt), 'application/json')
|
|
112
|
-
.catch(() => {});
|
|
113
|
-
}
|
|
109
|
+
if (receipt) await inbox.writeReceiptBeside(podPut, ident.inboxUrl, hash, receipt);
|
|
114
110
|
return { status: 202, reason: v.verified ? 'verified' : 'buffered-unverified' };
|
|
115
111
|
}
|
|
116
112
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import crypto from 'node:crypto';
|
|
13
13
|
import { verifyRequestDetailed } from '@fedify/fedify/sig';
|
|
14
|
-
import { assertPublicUrl, safeFetch, readCapped } from '
|
|
14
|
+
import { assertPublicUrl, safeFetch, readCapped } from '../shared/safefetch.mjs';
|
|
15
15
|
|
|
16
16
|
// An SSRF-safe JSON-LD document loader for Fedify's key fetch. The keyId in a
|
|
17
17
|
// delivery is chosen by the sender, so every deref it triggers is untrusted
|
|
@@ -89,7 +89,33 @@ export async function verifyHttpSignature(request, { documentLoader, keyCache, t
|
|
|
89
89
|
// even while the inbox is still public-Append (a forger without the secret
|
|
90
90
|
// cannot mint `verified: true`). The HMAC is over the receipt minus the hmac
|
|
91
91
|
// field itself, with sorted keys so gateway and drain canonicalize alike.
|
|
92
|
+
//
|
|
93
|
+
// JSON.stringify's replacer ARRAY applies at EVERY level, not just the top: the
|
|
94
|
+
// old canonical() passed the top-level key names, so `checks` — whose own keys
|
|
95
|
+
// are `signature`, `digest`, `dateSkew`, `keyFetched` — serialized as `{}` and
|
|
96
|
+
// was covered by no signature at all. Anything nested was equally unstamped.
|
|
97
|
+
// Sort recursively instead, and exclude `hmac` only where it actually lives.
|
|
98
|
+
function sortedKeys(v) {
|
|
99
|
+
if (Array.isArray(v)) return v.map(sortedKeys);
|
|
100
|
+
if (v && typeof v === 'object') {
|
|
101
|
+
const out = {};
|
|
102
|
+
for (const k of Object.keys(v).sort()) out[k] = sortedKeys(v[k]);
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
return v;
|
|
106
|
+
}
|
|
92
107
|
function canonical(obj) {
|
|
108
|
+
const { hmac, ...rest } = obj; // eslint-disable-line no-unused-vars
|
|
109
|
+
return JSON.stringify(sortedKeys(rest));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// What canonical() used to produce. Kept ONLY for verification, and only until
|
|
113
|
+
// every door has been redeployed: a gateway (fedipod.net) and an agent update
|
|
114
|
+
// independently, so for a while a receipt signed by an old door has to keep
|
|
115
|
+
// verifying against a new drain. Receipts are short-lived — one per inbox item,
|
|
116
|
+
// deleted with the item — so this can go once the doors have turned over.
|
|
117
|
+
// Nothing signs with it.
|
|
118
|
+
function canonicalLegacy(obj) {
|
|
93
119
|
return JSON.stringify(obj, Object.keys(obj).filter(k => k !== 'hmac').sort());
|
|
94
120
|
}
|
|
95
121
|
|
|
@@ -117,8 +143,13 @@ export function signReceipt(receipt, secret) {
|
|
|
117
143
|
// over equal-length buffers; unequal lengths are a straight mismatch.
|
|
118
144
|
export function verifyReceipt(receipt, secret) {
|
|
119
145
|
if (!receipt || typeof receipt !== 'object' || !receipt.hmac || !secret) return false;
|
|
120
|
-
const expected = crypto.createHmac('sha256', secret).update(canonical(receipt)).digest();
|
|
121
146
|
let given;
|
|
122
147
|
try { given = Buffer.from(receipt.hmac, 'base64'); } catch { return false; }
|
|
123
|
-
|
|
148
|
+
const matches = (text) => {
|
|
149
|
+
const expected = crypto.createHmac('sha256', secret).update(text).digest();
|
|
150
|
+
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
|
|
151
|
+
};
|
|
152
|
+
// The current stamp first; the pre-2026-09-09 one second, for receipts a door
|
|
153
|
+
// that has not been redeployed yet is still writing. See canonicalLegacy.
|
|
154
|
+
return matches(canonical(receipt)) || matches(canonicalLegacy(receipt));
|
|
124
155
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# solid-ap-pod
|
|
2
|
+
|
|
3
|
+
Reading and writing an ActivityPub actor's documents on a Solid pod.
|
|
4
|
+
|
|
5
|
+
It knows **where each document lives and how it lands safely** — the container
|
|
6
|
+
layout, the transport, the WAC access-control documents, the deletion deny-list,
|
|
7
|
+
the byte budgets. It does not know **what is in them**: every AS2 document is
|
|
8
|
+
built by the caller and passed in. That line is why this can be lifted out of
|
|
9
|
+
the application it grew up in.
|
|
10
|
+
|
|
11
|
+
It currently lives inside FediPod at `lib/pod/`. Extracting it is a directory
|
|
12
|
+
move plus this `package.json`; `scripts/check-pod-calls.mjs` in the parent repo
|
|
13
|
+
exists to keep that true.
|
|
14
|
+
|
|
15
|
+
## Rules it holds itself to
|
|
16
|
+
|
|
17
|
+
1. **No upward imports.** Nothing here reaches outside this directory.
|
|
18
|
+
2. **No Node built-ins.** It runs unmodified in a service worker, which is why
|
|
19
|
+
it carries its own `readCapped`/`retryAfterMs` in `http.mjs` rather than
|
|
20
|
+
using a host application's SSRF-guarding fetch.
|
|
21
|
+
3. **One runtime dependency, rdflib**, reached only by `transport.mjs` (listings,
|
|
22
|
+
ACL documents, ACP detection, the WebID patch). Resource modules must stay
|
|
23
|
+
free of it so a size-sensitive host — a serverless function answering every
|
|
24
|
+
request — can import one without paying for a parser.
|
|
25
|
+
4. **Everything injected.** It never builds a session, mints a credential, or
|
|
26
|
+
reads config or environment.
|
|
27
|
+
5. **No opinions with defaults.** `apUrls` requires the container root rather
|
|
28
|
+
than guessing one, because a guessed root put an identity's state in one
|
|
29
|
+
container and its published documents in another.
|
|
30
|
+
|
|
31
|
+
## Layout
|
|
32
|
+
|
|
33
|
+
| file | what |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `urls.mjs` | the container layout; `apUrls(remotePod, root, { publicBase })` |
|
|
36
|
+
| `transport.mjs` | `PodTransport` — verbs, ACLs, listings, cooldown, deny-list |
|
|
37
|
+
| `http.mjs` | `readCapped`, `retryAfterMs` |
|
|
38
|
+
| `links.mjs` | RFC 8288 `Link` header parsing |
|
|
39
|
+
|
|
40
|
+
## The transport
|
|
41
|
+
|
|
42
|
+
`PodTransport` takes a session — `{ fetch(url, init) }` — and never makes one.
|
|
43
|
+
A Node agent hands it a DPoP client-credentials grant, a browser hands it a
|
|
44
|
+
Solid-OIDC session, a pod server hands it a shim straight onto its own store.
|
|
45
|
+
|
|
46
|
+
Three manners hold for every request, and they are in `fetch()` so nothing can
|
|
47
|
+
route around them:
|
|
48
|
+
|
|
49
|
+
- **The cooldown.** A 429 or 503 arms a pod-wide pause. `cooldownMode: 'refuse'`
|
|
50
|
+
fails fast for the window (right for a daemon with timers behind it);
|
|
51
|
+
`'wait'` sleeps it out (right for a tab with a person in front of it).
|
|
52
|
+
- **The url map.** A fronted identity's advertised ids are mapped to their pod
|
|
53
|
+
locations at this one choke point, so callers pass advertised ids throughout.
|
|
54
|
+
- **The deletion deny-list.** A pod's profile, settings, discovery documents,
|
|
55
|
+
access-control documents and lease are never deletable. A pod was crippled
|
|
56
|
+
exactly this way; the list is a DENY-list rather than an allow-prefix because
|
|
57
|
+
the next caller will have a different prefix and the same things it must
|
|
58
|
+
never touch.
|
|
59
|
+
|
|
60
|
+
Subclasses override **`_send`**, not `fetch` — an override of `_send` cannot
|
|
61
|
+
skip any of the three.
|