fedipod-server 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -6
- package/dist/claims.d.ts +2 -0
- package/dist/claims.js +13 -4
- package/dist/door.d.ts +4 -0
- package/dist/door.js +31 -0
- package/dist/fedipod.d.ts +15 -0
- package/dist/fedipod.js +13 -0
- package/dist/front.d.ts +3 -0
- package/dist/front.js +92 -0
- package/dist/handler.d.ts +36 -101
- package/dist/handler.js +93 -644
- package/dist/handler.jsonld +2 -30
- package/dist/identities.d.ts +71 -0
- package/dist/identities.js +236 -0
- package/dist/mounts.d.ts +43 -0
- package/dist/mounts.js +123 -0
- package/dist/opt-in.d.ts +46 -0
- package/dist/opt-in.js +176 -0
- package/dist/private-read.d.ts +4 -0
- package/dist/private-read.js +34 -0
- package/package.json +12 -20
- package/lib/client/c2s.mjs +0 -636
- package/lib/client/localapi.mjs +0 -87
- package/lib/client/masto/accounts.mjs +0 -310
- package/lib/client/masto/body.mjs +0 -156
- package/lib/client/masto/index.mjs +0 -206
- package/lib/client/masto/instance.mjs +0 -112
- package/lib/client/masto/media.mjs +0 -79
- package/lib/client/masto/oauth.mjs +0 -656
- package/lib/client/masto/render.mjs +0 -518
- package/lib/client/masto/statuses.mjs +0 -372
- package/lib/client/masto/timelines.mjs +0 -438
- package/lib/client/oidc-auth.mjs +0 -62
- package/lib/client/streaming.mjs +0 -154
- package/lib/client/webpush.mjs +0 -98
- package/lib/connections/acctfeed.mjs +0 -257
- package/lib/connections/atproto.mjs +0 -309
- package/lib/connections/bskyfeed.mjs +0 -243
- package/lib/connections/bskygroup.mjs +0 -162
- package/lib/connections/fediacct.mjs +0 -253
- package/lib/connections/import.mjs +0 -357
- package/lib/connections/tagfeed.mjs +0 -212
- package/lib/connections/vault.mjs +0 -114
- package/lib/core/as2.mjs +0 -206
- package/lib/core/contexts/activitystreams.json +0 -379
- package/lib/core/contexts/anno.json +0 -126
- package/lib/core/contexts/did-v1.json +0 -57
- package/lib/core/contexts/fep-5711.json +0 -36
- package/lib/core/contexts/gotosocial.json +0 -86
- package/lib/core/contexts/identity-v1.json +0 -152
- package/lib/core/contexts/index.mjs +0 -63
- package/lib/core/contexts/join-lemmy.json +0 -33
- package/lib/core/contexts/joinmastodon.json +0 -28
- package/lib/core/contexts/litepub-0.1.json +0 -60
- package/lib/core/contexts/map.json +0 -19
- package/lib/core/contexts/miscellany.json +0 -19
- package/lib/core/contexts/quotes.json +0 -18
- package/lib/core/contexts/schemaorg.json +0 -8845
- package/lib/core/contexts/security-data-integrity-v1.json +0 -78
- package/lib/core/contexts/security-data-integrity-v2.json +0 -81
- package/lib/core/contexts/security-multikey-v1.json +0 -35
- package/lib/core/contexts/security-v1.json +0 -74
- package/lib/core/contexts/webfinger.json +0 -10
- package/lib/core/deliver.mjs +0 -387
- package/lib/core/graphview.mjs +0 -327
- package/lib/core/intake/activities.mjs +0 -649
- package/lib/core/intake/activity.mjs +0 -318
- package/lib/core/intake/channel.mjs +0 -144
- package/lib/core/intake/group.mjs +0 -258
- package/lib/core/intake/index.mjs +0 -732
- package/lib/core/intake/notes.mjs +0 -367
- package/lib/core/intake/verify.mjs +0 -142
- package/lib/core/keys.mjs +0 -153
- package/lib/core/lease.mjs +0 -206
- package/lib/core/place.mjs +0 -82
- package/lib/core/polls.mjs +0 -105
- package/lib/core/profile-page.mjs +0 -104
- package/lib/core/proof.mjs +0 -79
- package/lib/core/publisher/collections.mjs +0 -272
- package/lib/core/publisher/index.mjs +0 -522
- package/lib/core/publisher/notes.mjs +0 -519
- package/lib/core/publisher/own.mjs +0 -143
- package/lib/core/publisher/questions.mjs +0 -241
- package/lib/core/publisher/restore.mjs +0 -224
- package/lib/core/shapes/activitystreams.ttl +0 -157
- package/lib/core/shapes/index.mjs +0 -132
- package/lib/core/shapes/shapes-text.mjs +0 -13
- package/lib/core/social.mjs +0 -561
- package/lib/core/storage.mjs +0 -176
- package/lib/core/store.mjs +0 -578
- package/lib/core/wire-quotes.mjs +0 -71
- package/lib/core/wire.mjs +0 -925
- package/lib/device/account.mjs +0 -87
- package/lib/device/admin/index.mjs +0 -13
- package/lib/device/admin/origins.mjs +0 -35
- package/lib/device/admin/routes/connections.mjs +0 -144
- package/lib/device/admin/routes/gateway.mjs +0 -199
- package/lib/device/admin/routes/lifecycle.mjs +0 -200
- package/lib/device/admin/routes/owner.mjs +0 -322
- package/lib/device/admin/routes/setup.mjs +0 -428
- package/lib/device/admin/routes/social.mjs +0 -188
- package/lib/device/admin/server.mjs +0 -95
- package/lib/device/admin/static.mjs +0 -247
- package/lib/device/admin/surface.mjs +0 -315
- package/lib/device/certs.mjs +0 -281
- package/lib/device/cli/commands/account.mjs +0 -586
- package/lib/device/cli/commands/run.mjs +0 -278
- package/lib/device/cli/commands/service.mjs +0 -221
- package/lib/device/cli/commands/setup.mjs +0 -489
- package/lib/device/cli/commands/state.mjs +0 -561
- package/lib/device/cli/context.mjs +0 -288
- package/lib/device/export-collections.mjs +0 -161
- package/lib/device/gateway-move.mjs +0 -58
- package/lib/device/home.mjs +0 -161
- package/lib/device/migrate.mjs +0 -115
- package/lib/device/ports.mjs +0 -27
- package/lib/device/remote.mjs +0 -54
- package/lib/device/setup.mjs +0 -496
- package/lib/device/update.mjs +0 -96
- package/lib/gateway/caches.mjs +0 -36
- package/lib/gateway/directory.mjs +0 -90
- package/lib/gateway/front-core.mjs +0 -966
- package/lib/gateway/gateway-core.mjs +0 -244
- package/lib/gateway/headers.mjs +0 -49
- package/lib/gateway/httpsig.mjs +0 -159
- package/lib/gateway/notices.mjs +0 -84
- package/lib/gateway/quiet.mjs +0 -189
- package/lib/gateway/relay-extras.mjs +0 -89
- package/lib/gateway/token-claims.mjs +0 -16
- package/lib/pod/README.md +0 -61
- package/lib/pod/actor.mjs +0 -96
- package/lib/pod/collection.mjs +0 -77
- package/lib/pod/containers.mjs +0 -140
- package/lib/pod/discovery.mjs +0 -46
- package/lib/pod/featured.mjs +0 -18
- package/lib/pod/followers.mjs +0 -24
- package/lib/pod/following.mjs +0 -10
- package/lib/pod/http.mjs +0 -74
- package/lib/pod/inbox.mjs +0 -166
- package/lib/pod/links.mjs +0 -35
- package/lib/pod/location.mjs +0 -52
- package/lib/pod/media.mjs +0 -27
- package/lib/pod/notes.mjs +0 -78
- package/lib/pod/notifications.mjs +0 -89
- package/lib/pod/outbox.mjs +0 -18
- package/lib/pod/package.json +0 -19
- package/lib/pod/policy.mjs +0 -58
- package/lib/pod/private.mjs +0 -19
- package/lib/pod/root.mjs +0 -104
- package/lib/pod/state.mjs +0 -40
- package/lib/pod/transport.mjs +0 -674
- package/lib/pod/type-index.mjs +0 -101
- package/lib/pod/urls.mjs +0 -91
- package/lib/server/embed.mjs +0 -424
- package/lib/session/README.md +0 -137
- package/lib/session/demo.html +0 -94
- package/lib/session/fedi-account.mjs +0 -417
- package/lib/session/fedi-login.mjs +0 -123
- package/lib/session/oidc-session.mjs +0 -233
- package/lib/session/package.json +0 -49
- package/lib/shared/guard.mjs +0 -213
- package/lib/shared/links.mjs +0 -9
- package/lib/shared/safefetch.mjs +0 -235
- package/lib/shared/ua.mjs +0 -29
- package/phanpy/dist/404.html +0 -40
- package/phanpy/dist/_headers +0 -2
- package/phanpy/dist/apple-touch-icon.png +0 -0
- package/phanpy/dist/assets/ICONS-DIPJeU0C.js +0 -2
- package/phanpy/dist/assets/ICONS-DIPJeU0C.js.map +0 -1
- package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js +0 -183
- package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
- package/phanpy/dist/assets/Temml-CCjoacWt.woff2 +0 -0
- package/phanpy/dist/assets/__vite-browser-external-C7iut881.js +0 -2
- package/phanpy/dist/assets/__vite-browser-external-C7iut881.js.map +0 -1
- package/phanpy/dist/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
- package/phanpy/dist/assets/catch-up-O5q5C75Z.png +0 -0
- package/phanpy/dist/assets/chunk-aKtaBQYM.js +0 -1
- package/phanpy/dist/assets/compose-CFEx7wb4.js +0 -2
- package/phanpy/dist/assets/compose-CFEx7wb4.js.map +0 -1
- package/phanpy/dist/assets/compose-ZJN5xSb4.js +0 -35
- package/phanpy/dist/assets/compose-ZJN5xSb4.js.map +0 -1
- package/phanpy/dist/assets/debug.module-YFQgwnIr.js +0 -52
- package/phanpy/dist/assets/debug.module-YFQgwnIr.js.map +0 -1
- package/phanpy/dist/assets/dom-p1YcJ5Rw.js +0 -2
- package/phanpy/dist/assets/dom-p1YcJ5Rw.js.map +0 -1
- package/phanpy/dist/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
- package/phanpy/dist/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
- package/phanpy/dist/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
- package/phanpy/dist/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
- package/phanpy/dist/assets/icons/add-circle-line-WD0mwORD.js +0 -1
- package/phanpy/dist/assets/icons/alert-line-CpFUMf9s.js +0 -1
- package/phanpy/dist/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
- package/phanpy/dist/assets/icons/announcement-line-5nwaJFJm.js +0 -1
- package/phanpy/dist/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
- package/phanpy/dist/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
- package/phanpy/dist/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
- package/phanpy/dist/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
- package/phanpy/dist/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
- package/phanpy/dist/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
- package/phanpy/dist/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
- package/phanpy/dist/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
- package/phanpy/dist/assets/icons/at-line-CF66Bznk.js +0 -1
- package/phanpy/dist/assets/icons/attachment-line-BY8It-l9.js +0 -1
- package/phanpy/dist/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
- package/phanpy/dist/assets/icons/board-line-C0UWXyA6.js +0 -1
- package/phanpy/dist/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
- package/phanpy/dist/assets/icons/building-5-line-Dd59MzbO.js +0 -1
- package/phanpy/dist/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
- package/phanpy/dist/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
- package/phanpy/dist/assets/icons/calendar-month-line-vommts2C.js +0 -1
- package/phanpy/dist/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
- package/phanpy/dist/assets/icons/camera-line-Cv69MEei.js +0 -1
- package/phanpy/dist/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
- package/phanpy/dist/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
- package/phanpy/dist/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
- package/phanpy/dist/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
- package/phanpy/dist/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
- package/phanpy/dist/assets/icons/clipboard-line-BBp364gJ.js +0 -1
- package/phanpy/dist/assets/icons/close-circle-line-CICdiuGz.js +0 -1
- package/phanpy/dist/assets/icons/close-line-DYrFWcjv.js +0 -1
- package/phanpy/dist/assets/icons/cloud-line-tiQAbah8.js +0 -1
- package/phanpy/dist/assets/icons/code-line-fAl8iiV3.js +0 -1
- package/phanpy/dist/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
- package/phanpy/dist/assets/icons/copy-2-line-CLW69yF5.js +0 -1
- package/phanpy/dist/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
- package/phanpy/dist/assets/icons/document-3-line-DBGNhAg2.js +0 -1
- package/phanpy/dist/assets/icons/document-line-C7mb9sb7.js +0 -1
- package/phanpy/dist/assets/icons/down-line-BGqSSThd.js +0 -1
- package/phanpy/dist/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
- package/phanpy/dist/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
- package/phanpy/dist/assets/icons/exit-line-Dc07BS-x.js +0 -1
- package/phanpy/dist/assets/icons/external-link-line-BvpSkmjO.js +0 -1
- package/phanpy/dist/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
- package/phanpy/dist/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
- package/phanpy/dist/assets/icons/filter-2-line-B_mR2usy.js +0 -1
- package/phanpy/dist/assets/icons/filter-line-C_nm-3cU.js +0 -1
- package/phanpy/dist/assets/icons/flag-1-line-C2HB05I0.js +0 -1
- package/phanpy/dist/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
- package/phanpy/dist/assets/icons/formula-line-DLrt2d6j.js +0 -1
- package/phanpy/dist/assets/icons/grid-line-CgUmEikf.js +0 -1
- package/phanpy/dist/assets/icons/group-line-C5fnX0wU.js +0 -1
- package/phanpy/dist/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
- package/phanpy/dist/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
- package/phanpy/dist/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
- package/phanpy/dist/assets/icons/heart-line-CJ7K47J4.js +0 -1
- package/phanpy/dist/assets/icons/history-2-line-kWWvFzqv.js +0 -1
- package/phanpy/dist/assets/icons/history-line-bGQiwbTH.js +0 -1
- package/phanpy/dist/assets/icons/home-3-line-fRU1zeLG.js +0 -1
- package/phanpy/dist/assets/icons/information-line-iNk6-sOY.js +0 -1
- package/phanpy/dist/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
- package/phanpy/dist/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
- package/phanpy/dist/assets/icons/layout-5-line-B_znt5wO.js +0 -1
- package/phanpy/dist/assets/icons/left-line-eUw0tya4.js +0 -1
- package/phanpy/dist/assets/icons/lightning-line-DaKoDdix.js +0 -1
- package/phanpy/dist/assets/icons/link-2-line-DCqLmMA7.js +0 -1
- package/phanpy/dist/assets/icons/list-check-line-BAxosBh9.js +0 -1
- package/phanpy/dist/assets/icons/lock-line-CPoaXeUW.js +0 -1
- package/phanpy/dist/assets/icons/mail-line-B0P8aa1L.js +0 -1
- package/phanpy/dist/assets/icons/moon-line-CEqNAToP.js +0 -1
- package/phanpy/dist/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
- package/phanpy/dist/assets/icons/more-3-line-BIiVjYTz.js +0 -1
- package/phanpy/dist/assets/icons/notification-line-BZZq2Gtu.js +0 -1
- package/phanpy/dist/assets/icons/pencil-line-CKh8-A1v.js +0 -1
- package/phanpy/dist/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
- package/phanpy/dist/assets/icons/pin-line-DpTolyhs.js +0 -1
- package/phanpy/dist/assets/icons/play-fill-B0-G_8lX.js +0 -1
- package/phanpy/dist/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
- package/phanpy/dist/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
- package/phanpy/dist/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
- package/phanpy/dist/assets/icons/radar-line-CKyroTth.js +0 -1
- package/phanpy/dist/assets/icons/react-line-CoIKhhtK.js +0 -1
- package/phanpy/dist/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
- package/phanpy/dist/assets/icons/right-line-B3KWJaqc.js +0 -1
- package/phanpy/dist/assets/icons/rocket-line-4KZl537D.js +0 -1
- package/phanpy/dist/assets/icons/round-fill-C69NTrZ0.js +0 -1
- package/phanpy/dist/assets/icons/round-line-B9k_1uo-.js +0 -1
- package/phanpy/dist/assets/icons/route-line-B28ReHcE.js +0 -1
- package/phanpy/dist/assets/icons/rows-4-line-CrBniagM.js +0 -1
- package/phanpy/dist/assets/icons/scan-line-DSf74YdP.js +0 -1
- package/phanpy/dist/assets/icons/search-2-line-B6geIU1z.js +0 -1
- package/phanpy/dist/assets/icons/settings-3-line-azJwK47w.js +0 -1
- package/phanpy/dist/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
- package/phanpy/dist/assets/icons/share-2-line-D5MxWjrm.js +0 -1
- package/phanpy/dist/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
- package/phanpy/dist/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
- package/phanpy/dist/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
- package/phanpy/dist/assets/icons/time-line-BF3eet1v.js +0 -1
- package/phanpy/dist/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
- package/phanpy/dist/assets/icons/translate-line-BFWol2Ce.js +0 -1
- package/phanpy/dist/assets/icons/unlock-line-X_x5vwv3.js +0 -1
- package/phanpy/dist/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
- package/phanpy/dist/assets/icons/user-4-line-BVSynZCp.js +0 -1
- package/phanpy/dist/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
- package/phanpy/dist/assets/icons/user-add-line-BbXdHG4z.js +0 -1
- package/phanpy/dist/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
- package/phanpy/dist/assets/icons/user-follow-line-BXux0wSd.js +0 -1
- package/phanpy/dist/assets/icons/user-star-line-DdYlpHLU.js +0 -1
- package/phanpy/dist/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
- package/phanpy/dist/assets/icons/user-x-line-DIDgNlPH.js +0 -1
- package/phanpy/dist/assets/icons/volume-line-Bs390KQt.js +0 -1
- package/phanpy/dist/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
- package/phanpy/dist/assets/icons/walk-line-yNXfB-md.js +0 -1
- package/phanpy/dist/assets/icons/world-2-line-CenkGIwj.js +0 -1
- package/phanpy/dist/assets/icons/zoom-in-line-CaENUphf.js +0 -1
- package/phanpy/dist/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
- package/phanpy/dist/assets/instances-BNPptnn-.json +0 -333
- package/phanpy/dist/assets/locales/ar-SA-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/ca-ES-DHRW86Tz.js +0 -1
- package/phanpy/dist/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
- package/phanpy/dist/assets/locales/de-DE-ByRPMRFy.js +0 -1
- package/phanpy/dist/assets/locales/eo-UY-CPC7wLCS.js +0 -1
- package/phanpy/dist/assets/locales/es-ES-dJa0Fd9t.js +0 -1
- package/phanpy/dist/assets/locales/eu-ES-C3djlPR2.js +0 -1
- package/phanpy/dist/assets/locales/fa-IR-DTT490Ck.js +0 -1
- package/phanpy/dist/assets/locales/fi-FI-B23MCPuz.js +0 -1
- package/phanpy/dist/assets/locales/fr-FR-BcaQsO5C.js +0 -1
- package/phanpy/dist/assets/locales/gl-ES-BR-CVKBN.js +0 -1
- package/phanpy/dist/assets/locales/he-IL-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/hu-HU-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/it-IT-C1136UFV.js +0 -1
- package/phanpy/dist/assets/locales/ja-JP-CRZmRAhD.js +0 -1
- package/phanpy/dist/assets/locales/kab-Cz8xH_TT.js +0 -1
- package/phanpy/dist/assets/locales/ko-KR-CG9UiY4o.js +0 -1
- package/phanpy/dist/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
- package/phanpy/dist/assets/locales/nb-NO-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
- package/phanpy/dist/assets/locales/oc-FR-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/pl-PL-DkwGXf6P.js +0 -1
- package/phanpy/dist/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
- package/phanpy/dist/assets/locales/pt-BR-nGL0HdFS.js +0 -1
- package/phanpy/dist/assets/locales/pt-PT-Cp17aj2z.js +0 -1
- package/phanpy/dist/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
- package/phanpy/dist/assets/locales/th-TH-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/tok-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/tr-TR-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/uk-UA-C5E3dFBf.js +0 -1
- package/phanpy/dist/assets/locales/zh-CN-BHJi7_et.js +0 -1
- package/phanpy/dist/assets/locales/zh-TW-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/main-BdqNbG-a.js +0 -26
- package/phanpy/dist/assets/main-BdqNbG-a.js.map +0 -1
- package/phanpy/dist/assets/mock-home-DGhR__He.js +0 -2
- package/phanpy/dist/assets/mock-home-DGhR__He.js.map +0 -1
- package/phanpy/dist/assets/multi-column-ROck0NVt.jpg +0 -0
- package/phanpy/dist/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
- package/phanpy/dist/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
- package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js +0 -2
- package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js.map +0 -1
- package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js +0 -20
- package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js.map +0 -1
- package/phanpy/dist/assets/style-B1iLPTPR.css +0 -2
- package/phanpy/dist/assets/temml-DuyIQMei.js +0 -97
- package/phanpy/dist/assets/temml-DuyIQMei.js.map +0 -1
- package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js +0 -2
- package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
- package/phanpy/dist/assets/year-in-posts-B2lywqdx.png +0 -0
- package/phanpy/dist/assets/year-in-posts-DJv_star.js +0 -3
- package/phanpy/dist/assets/year-in-posts-DJv_star.js.map +0 -1
- package/phanpy/dist/compose/index.html +0 -20
- package/phanpy/dist/favicon.ico +0 -0
- package/phanpy/dist/index.html +0 -73
- package/phanpy/dist/logo-192.png +0 -0
- package/phanpy/dist/logo-512.png +0 -0
- package/phanpy/dist/logo-badge-72.png +0 -0
- package/phanpy/dist/logo-maskable-512.png +0 -0
- package/phanpy/dist/logo-monochrome-512.png +0 -0
- package/phanpy/dist/logo-monochrome-maskable-512.png +0 -0
- package/phanpy/dist/manifest.webmanifest +0 -1
- package/phanpy/dist/og-image-2.jpg +0 -0
- package/phanpy/dist/og-image.png +0 -0
- package/phanpy/dist/robots.txt +0 -2
- package/phanpy/dist/sw.js +0 -2
- package/phanpy/dist/sw.js.map +0 -1
- package/phanpy/dist/version.json +0 -1
- package/run-agent.mjs +0 -899
- package/vendor/gate.cjs +0 -138
- package/vendor/idp-grant.cjs +0 -394
- package/vendor/jfetch.cjs +0 -16
- package/web/admin/actors.js +0 -147
- package/web/admin/bar.css +0 -126
- package/web/admin/bar.js +0 -99
- package/web/admin/client/client.js +0 -66
- package/web/admin/client/index.html +0 -69
- package/web/admin/client-pick.js +0 -79
- package/web/admin/common.js +0 -23
- package/web/admin/connections.js +0 -112
- package/web/admin/gateway.js +0 -133
- package/web/admin/group.js +0 -258
- package/web/admin/index.html +0 -612
- package/web/admin/notices-bar.js +0 -90
- package/web/admin/oauth-signin.mjs +0 -37
- package/web/admin/record.js +0 -393
- package/web/admin/setup/index.html +0 -257
- package/web/admin/setup/setup.js +0 -339
- package/web/admin/tokens.css +0 -68
- package/web/admin/upkeep.js +0 -177
- package/web/admin/window.css +0 -55
- package/web/admin/window.js +0 -136
- package/web/app/README.md +0 -81
- package/web/app/admin-facade.mjs +0 -588
- package/web/app/agent.mjs +0 -672
- package/web/app/atproto-browser.mjs +0 -86
- package/web/app/authorize_interaction.html +0 -56
- package/web/app/authorize_interaction.js +0 -69
- package/web/app/boot.mjs +0 -616
- package/web/app/deliver-relay.mjs +0 -129
- package/web/app/dist/boot.js +0 -34562
- package/web/app/dist/boot.js.map +0 -7
- package/web/app/dist/sw.js +0 -73456
- package/web/app/dist/sw.js.map +0 -7
- package/web/app/fediacct-browser.mjs +0 -146
- package/web/app/gateway-move.mjs +0 -60
- package/web/app/idb-kv.mjs +0 -35
- package/web/app/index.html +0 -258
- package/web/app/keys-browser.mjs +0 -71
- package/web/app/keystore.mjs +0 -82
- package/web/app/oidc-session.mjs +0 -6
- package/web/app/pod-auth.mjs +0 -248
- package/web/app/pod-remote.mjs +0 -84
- package/web/app/shims/_globals.mjs +0 -2
- package/web/app/shims/fedify-sig.mjs +0 -77
- package/web/app/shims/node-crypto.mjs +0 -113
- package/web/app/shims/node-fs.mjs +0 -17
- package/web/app/shims/node-path.mjs +0 -19
- package/web/app/shims/node-url.mjs +0 -7
- package/web/app/shims/prelude.js +0 -58
- package/web/app/shims/safefetch.mjs +0 -58
- package/web/app/shims/shapes-text.mjs +0 -8
- package/web/app/shims/web-push.mjs +0 -14
- package/web/app/signup.mjs +0 -357
- package/web/app/sw-src.mjs +0 -220
- package/web/app/update.js +0 -70
- package/web/front/admin.html +0 -55
- package/web/front/admin.js +0 -89
- package/web/front/install.sh +0 -69
- package/web/front/new-account.html +0 -35
- package/web/front/new-account.js +0 -13
- package/web/front/notices.html +0 -69
- package/web/front/notices.js +0 -107
- package/web/front/run.html +0 -64
- package/web/front/run.js +0 -221
- package/web/front/solid-oidc-client.js +0 -6
- package/web/icons/forum.svg +0 -11
- package/web/icons/gateway.svg +0 -14
- package/web/icons/local.svg +0 -11
|
@@ -1,966 +0,0 @@
|
|
|
1
|
-
// front-core.mjs — the multi-user front: one always-on box, run by a host, that
|
|
2
|
-
// fronts many independent FediPod users under a shared domain (@name@fedipod.net).
|
|
3
|
-
// It answers name lookups, serves each user's public face, and is the verifying
|
|
4
|
-
// inbox for all of them — routing every verified delivery into that user's own
|
|
5
|
-
// pod inbox. It is KEYLESS and holds no user data: only a directory (handle →
|
|
6
|
-
// where that user's pod is + their public key) and the per-user secrets needed
|
|
7
|
-
// to append verified mail to their inbox.
|
|
8
|
-
//
|
|
9
|
-
// The identity trick is rewrite-on-read (jg10's pattern): a user's public IRI
|
|
10
|
-
// space at `<front>/u/<handle>/` is a 1:1 path map of their pod's own tree, so
|
|
11
|
-
// the actor id and every id under it live on the shared domain while the bytes
|
|
12
|
-
// still come from the user's pod. The inbox maps the same way, so delivery to
|
|
13
|
-
// the fronted inbox is delivery this box verifies, then forwards to the pod.
|
|
14
|
-
//
|
|
15
|
-
// Runtime-agnostic: `netlify/functions/front.mjs` is one adapter; any always-on
|
|
16
|
-
// HTTPS box is another. UN-DEPLOYED — nothing in FediPod runs it.
|
|
17
|
-
|
|
18
|
-
import crypto from 'node:crypto';
|
|
19
|
-
import { handleDelivery, handleOwnerPost } from './gateway-core.mjs';
|
|
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';
|
|
23
|
-
import { podBaseOfWebId } from '../pod/urls.mjs';
|
|
24
|
-
import { routeQuietApi, noteOpened, noteReceived, closedState, accountState, closedAnswer } from './quiet.mjs';
|
|
25
|
-
import { routeNoticesApi } from './notices.mjs';
|
|
26
|
-
import { withSecurityHeaders } from './headers.mjs';
|
|
27
|
-
import { podTokenVerifier } from './caches.mjs';
|
|
28
|
-
import { claimedWebId } from './token-claims.mjs';
|
|
29
|
-
import { withdrawn, overLimit, relayOne } from './relay-extras.mjs';
|
|
30
|
-
|
|
31
|
-
// The one WebFinger document, spelled out here rather than imported from
|
|
32
|
-
// wire.mjs: wire drags the agent's whole HTML pipeline (sanitize-html and
|
|
33
|
-
// friends), which a serverless front must never carry — it crashed the
|
|
34
|
-
// deployed function before it answered its first request.
|
|
35
|
-
const jrd = ({ handle, host, actor, aliases = [], page = null, subscribe = null }) => ({
|
|
36
|
-
subject: `acct:${handle}@${host}`,
|
|
37
|
-
...(aliases.length ? { aliases } : {}),
|
|
38
|
-
links: [
|
|
39
|
-
{ rel: 'self', type: 'application/activity+json', href: actor },
|
|
40
|
-
...(page ? [{ rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: page }] : []),
|
|
41
|
-
// Remote follow. Somebody reading this account on another server presses
|
|
42
|
-
// Follow, says they are here, and that server looks for this link to find
|
|
43
|
-
// out where to send them. `{uri}` is a literal the other server fills in
|
|
44
|
-
// with whatever they are following — it must not be escaped.
|
|
45
|
-
...(subscribe ? [{ rel: 'http://ostatus.org/schema/1.0/subscribe', template: subscribe }] : []),
|
|
46
|
-
],
|
|
47
|
-
});
|
|
48
|
-
// The door that link names, which the site staged beside this function serves.
|
|
49
|
-
const subscribeTemplate = (origin) => (origin
|
|
50
|
-
? `${String(origin).replace(/\/$/u, '')}/authorize_interaction?uri={uri}` : null);
|
|
51
|
-
// The public profile page of any account the front answers for lives on
|
|
52
|
-
// its pod, whether the address is the front's or the pod's own.
|
|
53
|
-
const profilePageOf = (rec) => (rec?.podHome ? rec.podHome + 'ap/profile.html' : null);
|
|
54
|
-
|
|
55
|
-
const AP_CT = 'application/activity+json';
|
|
56
|
-
|
|
57
|
-
// Verify a Solid-OIDC token (DPoP-bound) and return its WebID, or null. The
|
|
58
|
-
// verifier is injected so tests stub it; in production it is the same library
|
|
59
|
-
// the agent's own C2S auth uses.
|
|
60
|
-
async function verifyPodToken(request, pathname, verifier) {
|
|
61
|
-
const authz = request.headers.get('authorization');
|
|
62
|
-
if (!authz) return null;
|
|
63
|
-
try {
|
|
64
|
-
const dpop = request.headers.get('dpop');
|
|
65
|
-
// Solid-OIDC binds the token to a key the client proves on every request;
|
|
66
|
-
// a token shown without the proof is one anyone who saw it could show.
|
|
67
|
-
if (!dpop) { console.log(`front: pod token without a DPoP proof refused on ${pathname}`); return null; }
|
|
68
|
-
const v = verifier || await podTokenVerifier();
|
|
69
|
-
const url = request.url;
|
|
70
|
-
const { webid } = await v(authz, { header: dpop, method: request.method, url });
|
|
71
|
-
return webid || null;
|
|
72
|
-
} catch (e) {
|
|
73
|
-
// Said aloud: a token the front will not take is otherwise a bare 401 to
|
|
74
|
-
// the caller and nothing at all here.
|
|
75
|
-
console.log(`front: pod token refused on ${pathname}: ${e?.message || e}`);
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Is this WebID served by the claimed pod? A pod owner's WebID lives on the pod
|
|
81
|
-
// origin — that is the whole proof: a token for a WebID under podHome could
|
|
82
|
-
// only be minted by someone who controls that pod's identity provider.
|
|
83
|
-
// A pod that will not answer must not hold up the person opting in; without
|
|
84
|
-
// an answer the older check stands on its own.
|
|
85
|
-
const OWNER_LOOKUP_MS = 5_000;
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Who the pod server says owns the pod. The server that hosts it is the
|
|
89
|
-
* authority on that, so when it answers, its answer decides. A server that
|
|
90
|
-
* says nothing leaves where the WebID lives as the only evidence there is.
|
|
91
|
-
*/
|
|
92
|
-
const podOwners = (podBase, fetchImpl = fetch) =>
|
|
93
|
-
podRoot.readOwnerLinks(fetchImpl, podBase, { timeoutMs: OWNER_LOOKUP_MS });
|
|
94
|
-
|
|
95
|
-
// Whether a token's WebID vouches for this pod, on the evidence of where it
|
|
96
|
-
// lives: the same origin, and — for a suffixed pod — inside
|
|
97
|
-
// the WebID's own pod, wherever in it the account was put. Weak on purpose,
|
|
98
|
-
// and never used alone for an attach — see provesPod.
|
|
99
|
-
function webidUnderPod(webid, podHome) {
|
|
100
|
-
try {
|
|
101
|
-
return new URL(webid).origin === new URL(podHome).origin && String(podHome).startsWith(podBaseOfWebId(webid));
|
|
102
|
-
} catch { return false; }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Whether this WebID may speak for this pod.
|
|
107
|
-
*
|
|
108
|
-
* Sharing an origin was the whole test, which is right where an origin is one
|
|
109
|
-
* person (a subdomain pod) and wrong where it is not. On a path-based server
|
|
110
|
-
* every account is on the same origin, so any account holder could attach
|
|
111
|
-
* somebody else's pod under a name of their choosing — and, worse, re-attach a
|
|
112
|
-
* name that already existed and be handed its receipt secret (S7).
|
|
113
|
-
*
|
|
114
|
-
* The pod server is the authority on who owns the pod, and it will say so in a
|
|
115
|
-
* `solid:owner` link if asked. When it answers, its answer decides. Only when
|
|
116
|
-
* it says nothing does this fall back to where the WebID lives, which is the
|
|
117
|
-
* best a silent server leaves available. This is the rule /api/agent's opt-in
|
|
118
|
-
* has used all along; attach simply never asked.
|
|
119
|
-
*/
|
|
120
|
-
async function provesPod(webid, podHome, fetchImpl) {
|
|
121
|
-
const owners = await podOwners(podHome, fetchImpl || fetch);
|
|
122
|
-
if (owners.length) return owners.includes(webid);
|
|
123
|
-
return webidUnderPod(webid, podHome);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// A pod this front will read from at all.
|
|
127
|
-
//
|
|
128
|
-
// The front reads `rec.podHome + <the rest of the request path>` and, on the
|
|
129
|
-
// CSS server component, that read goes straight into the resource store with
|
|
130
|
-
// no access control of its own — so a row whose podHome names the FRONT is a
|
|
131
|
-
// row that can read the server's own internals, `/.internal/fedipod/directory/`
|
|
132
|
-
// included, where every user's receipt secret is kept (S2). The front is not a
|
|
133
|
-
// pod and never was one; saying so outright is the fix, and it closes the
|
|
134
|
-
// self-recursion of S8 with the same line.
|
|
135
|
-
function podHomeProblem(podHome, frontOrigin) {
|
|
136
|
-
let u;
|
|
137
|
-
try { u = new URL(podHome); } catch { return 'podHome is not a URL'; }
|
|
138
|
-
if (frontOrigin) {
|
|
139
|
-
try {
|
|
140
|
-
if (u.origin === new URL(frontOrigin).origin) {
|
|
141
|
-
return 'a pod cannot live on this gateway — name the pod that holds your data';
|
|
142
|
-
}
|
|
143
|
-
} catch { /* no usable frontOrigin: the other checks still apply */ }
|
|
144
|
-
}
|
|
145
|
-
// Nothing may reach a server's private trees, whatever origin they are on.
|
|
146
|
-
if (/(^|\/)\.internal(\/|$)/u.test(u.pathname) || /(^|\/)\.\.(\/|$)/u.test(u.pathname)) {
|
|
147
|
-
return 'that is not a pod address';
|
|
148
|
-
}
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const RELAY_MAX_REQUESTS = 20;
|
|
153
|
-
const SERVER_PROBE_MS = 6000;
|
|
154
|
-
const SERVER_PROBE_BYTES = 64 * 1024;
|
|
155
|
-
|
|
156
|
-
// The owner's APIs a browser at ANOTHER origin may call: an address moving
|
|
157
|
-
// to a new gateway is driven from the new gateway's page, and it has to tell
|
|
158
|
-
// this one it moved and send the Move through this one's relay. Both take a
|
|
159
|
-
// pod token as proof, the way the outbox door does, so any origin may ask.
|
|
160
|
-
const API_CORS = {
|
|
161
|
-
'access-control-allow-origin': '*',
|
|
162
|
-
'access-control-allow-methods': 'POST, OPTIONS',
|
|
163
|
-
'access-control-allow-headers': 'Authorization, DPoP, Content-Type',
|
|
164
|
-
'access-control-max-age': '86400',
|
|
165
|
-
};
|
|
166
|
-
const withApiCors = (out) => ({ ...out, headers: { ...(out.headers || {}), ...API_CORS } });
|
|
167
|
-
const apiPreflight = () => ({ status: 204, headers: { ...API_CORS, allow: 'POST, OPTIONS' }, body: null });
|
|
168
|
-
|
|
169
|
-
const j = (status, obj, ct = 'application/json') =>
|
|
170
|
-
({ status, headers: { 'content-type': ct, 'cache-control': 'no-store' }, body: JSON.stringify(obj) });
|
|
171
|
-
// Held at the edge like any other missing document: a bot's scan, a dead
|
|
172
|
-
// handle, a typo in a WebFinger query each cost one call, not one per asker.
|
|
173
|
-
const notFound = () => ({ status: 404, headers: { 'content-type': 'text/plain', ...publicFor(MISSING_EDGE_SECONDS) }, body: 'not found\n' });
|
|
174
|
-
|
|
175
|
-
// A user's public base on the front, a 1:1 mirror of their pod home.
|
|
176
|
-
// The directory key is the FULL fediverse address — handle@host — never the
|
|
177
|
-
// bare handle, which is not unique: two different pods can each have a "me".
|
|
178
|
-
// A fronted identity answers at this front's host; a mail-door identity at its
|
|
179
|
-
// own pod's host. The `/u/` path segment carries that same key.
|
|
180
|
-
export const addressKey = (handle, host) => `${handle}@${host}`;
|
|
181
|
-
export const userBase = (frontOrigin, key) => `${frontOrigin}/u/${encodeURIComponent(key)}/`;
|
|
182
|
-
|
|
183
|
-
// Rewrite every occurrence of the pod home to the front base (or back). The
|
|
184
|
-
// bodies are JSON, so a whole-string swap rewrites ids wherever they appear —
|
|
185
|
-
// actor id, collection ids, note ids — keeping the whole space self-consistent.
|
|
186
|
-
const swap = (text, from, to) => text.split(from).join(to);
|
|
187
|
-
|
|
188
|
-
// Which handle a /u/<handle>/... path names, and the pod path under it.
|
|
189
|
-
function parseUserPath(pathname) {
|
|
190
|
-
const m = /^\/u\/([^/]+)\/(.*)$/.exec(pathname);
|
|
191
|
-
return m ? { handle: decodeURIComponent(m[1]), rest: m[2] } : null;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// A public document is the same for everybody who asks, so the edge may hold
|
|
195
|
-
// it and answer the next reader itself. Without this every read of an actor,
|
|
196
|
-
// a topic, a post or a card was a function call: one visit to a forum is
|
|
197
|
-
// twenty of them, and every visitor and every refresh paid again.
|
|
198
|
-
//
|
|
199
|
-
// `netlify-cdn-cache-control` is the edge's own instruction; `cache-control`
|
|
200
|
-
// is what the reader's browser is told. The edge may serve a stale copy while
|
|
201
|
-
// it fetches a fresh one, so nobody waits on the pod for a document that is
|
|
202
|
-
// merely minutes old. The edge holds a document far longer than a browser
|
|
203
|
-
// is asked to: the edge is what turns a thousand readers into one function
|
|
204
|
-
// call, while a browser that held an actor for ten minutes would show one
|
|
205
|
-
// person a stale profile they had just edited.
|
|
206
|
-
const BROWSER_MAX_AGE = 60;
|
|
207
|
-
const publicFor = (seconds, stale = seconds * 4) => ({
|
|
208
|
-
'cache-control': `public, max-age=${Math.min(seconds, BROWSER_MAX_AGE)}`,
|
|
209
|
-
// `durable`: one copy for every region, and one that a deploy does not
|
|
210
|
-
// empty. Without it each region held its own and every deploy started cold.
|
|
211
|
-
'netlify-cdn-cache-control': `public, durable, s-maxage=${seconds}, stale-while-revalidate=${stale}`,
|
|
212
|
-
});
|
|
213
|
-
// How long the edge holds a public document, and a handle's WebFinger
|
|
214
|
-
// answer. Every server that has heard of an account asks for its actor and
|
|
215
|
-
// its handle again and again; a post or a collection changes when its owner
|
|
216
|
-
// acts, and ten minutes of edge is what the plan's budget of function calls
|
|
217
|
-
// can afford.
|
|
218
|
-
const PUBLIC_EDGE_SECONDS = 600;
|
|
219
|
-
const WEBFINGER_EDGE_SECONDS = 3600;
|
|
220
|
-
// A closed or moved address stays gone; a picture's address stays where it is.
|
|
221
|
-
const GONE_EDGE_SECONDS = 3600;
|
|
222
|
-
const MEDIA_EDGE_SECONDS = 86400;
|
|
223
|
-
// A public document the pod would not give (missing, or not public) is asked
|
|
224
|
-
// for again and again by every server that shows the account — a pinned-posts
|
|
225
|
-
// collection an old account never wrote, a forum count nobody published. Held
|
|
226
|
-
// briefly, so the edge absorbs the asking; briefly, so a document published a
|
|
227
|
-
// moment later is not "missing" for long.
|
|
228
|
-
const MISSING_EDGE_SECONDS = 120;
|
|
229
|
-
|
|
230
|
-
// Where a pod owner opts their identity in. An operator may name it, because
|
|
231
|
-
// the path it takes is one their pod server can no longer serve; the dot says
|
|
232
|
-
// it belongs to the server rather than to anybody's data, the way
|
|
233
|
-
// `.well-known` and `.account` do.
|
|
234
|
-
export const DEFAULT_RUN_PATH = '/.fediverse-account';
|
|
235
|
-
export const runPath = (ctx) => {
|
|
236
|
-
const said = String(ctx?.runPath || '').trim().replace(/\/+$/u, '');
|
|
237
|
-
if (!said) return DEFAULT_RUN_PATH;
|
|
238
|
-
return said.startsWith('/') ? said : `/${said}`;
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
// A request the browser itself says came from this very origin. `Sec-Fetch-Site`
|
|
242
|
-
// is the browser's word and a page cannot forge it; an `Origin` that matches is
|
|
243
|
-
// the older way of saying the same. This is what makes it safe to read a
|
|
244
|
-
// session cookie as proof: a page on another site gets neither.
|
|
245
|
-
const sameOriginRequest = (request, origin) => {
|
|
246
|
-
const site = request.headers.get('sec-fetch-site');
|
|
247
|
-
if (site) return site === 'same-origin';
|
|
248
|
-
const said = request.headers.get('origin');
|
|
249
|
-
if (!said || !origin) return false;
|
|
250
|
-
try { return new URL(said).origin === new URL(origin).origin; } catch { return false; }
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
// Top-level paths a handle may not take, so a name never shadows a route.
|
|
254
|
-
const RESERVED = new Set(['u', 'api', 'signup', 'new-account', 'run', 'admin', 'roster', 'gateway',
|
|
255
|
-
'gw', 'well-known', 'inbox', 'outbox', 'actor', 'install', 'notices']);
|
|
256
|
-
|
|
257
|
-
// Why a handle is unusable, or null when it is fine. Lowercase letters, digits
|
|
258
|
-
// and hyphens; 2–30 chars; not edge-hyphenated; not a reserved route.
|
|
259
|
-
function handleProblem(h) {
|
|
260
|
-
if (!h) return 'empty';
|
|
261
|
-
if (!/^[a-z0-9-]{2,30}$/.test(h)) return 'letters, digits and hyphens only, 2–30 characters';
|
|
262
|
-
if (h.startsWith('-') || h.endsWith('-')) return 'cannot start or end with a hyphen';
|
|
263
|
-
if (RESERVED.has(h)) return 'that name is reserved';
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// The person's own filtering policy, published on their pod and readable by
|
|
268
|
-
// anyone: their accepted following list and a mirror of their blocklist. A
|
|
269
|
-
// directory row carries neither — the row is written at attach time, before
|
|
270
|
-
// the agent has published anything — so without this the door would judge by
|
|
271
|
-
// addressing alone and forward the junk a blocklist exists to stop.
|
|
272
|
-
//
|
|
273
|
-
// Cached briefly: a delivery flood must not become a read per delivery on the
|
|
274
|
-
// person's pod, which is the load this whole door exists to spare them.
|
|
275
|
-
const POLICY_TTL_MS = 5 * 60_000;
|
|
276
|
-
const policyCache = new Map(); // podHome -> { at, policy }
|
|
277
|
-
|
|
278
|
-
// The cache is OURS, not the library's: on a warm serverless container one Map
|
|
279
|
-
// is shared across every user this front serves, so what it is keyed by is a
|
|
280
|
-
// correctness property. Keyed by podHome, it is one person's policy under one
|
|
281
|
-
// person's key.
|
|
282
|
-
const policyFor = (rec, fetchImpl = fetch) =>
|
|
283
|
-
podPolicy.read(fetchImpl, rec.podHome, { cache: policyCache, ttlMs: POLICY_TTL_MS });
|
|
284
|
-
|
|
285
|
-
// The verified-delivery identity for a directory record: what gateway-core
|
|
286
|
-
// needs to check what concerns the user and to append to their pod inbox.
|
|
287
|
-
// `policy` is the person's published filtering policy when there is one.
|
|
288
|
-
function identFor(rec, policy = null) {
|
|
289
|
-
return {
|
|
290
|
-
// A row may name the inbox its deliveries are written to: a forum's
|
|
291
|
-
// categories share one, on the forum's pod, above their own trees.
|
|
292
|
-
inboxUrl: rec.inboxUrl || rec.podHome + 'ap/inbox/',
|
|
293
|
-
actorUrl: rec.actorUrl, // the FRONT actor id (what mail is addressed to)
|
|
294
|
-
followersUrl: rec.actorUrl.replace(/actor$/, 'followers'),
|
|
295
|
-
notesPrefix: rec.actorUrl.replace(/ap\/actor$/, 'ap/notes/'),
|
|
296
|
-
following: policy?.following || rec.following || [],
|
|
297
|
-
blocklist: policy?.blocklist || rec.blocklist || { domains: [], actors: [] },
|
|
298
|
-
kind: policy?.kind || rec.kind || 'person',
|
|
299
|
-
gatewayWebId: rec.gatewayWebId,
|
|
300
|
-
hmacSecret: rec.hmacSecret,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// The single entry point. `ctx`:
|
|
305
|
-
// host the front's own host, e.g. "fedipod.net" (for WebFinger subjects)
|
|
306
|
-
// frontOrigin "https://fedipod.net"
|
|
307
|
-
// lookup(handle) -> record | null the directory
|
|
308
|
-
// listDirectory() -> { handle: record } every row, for the admin roster
|
|
309
|
-
// removeDirectory(handle) -> boolean drop a row; false when a seeded row remains
|
|
310
|
-
// podPut(url, body, ct) -> boolean append to a user's pod (per-user cred inside)
|
|
311
|
-
// readReceived / writeReceived / dropReceived, pauseItems, closeDays — see quiet.mjs
|
|
312
|
-
// podGet(url, { podHome }) -> Response read a user's pod (public reads; plain fetch
|
|
313
|
-
// is fine). `podHome` is the row's own pod, for
|
|
314
|
-
// an adapter that reads a store directly and so
|
|
315
|
-
// must confine the read itself.
|
|
316
|
-
export async function routeFront(request, ctx) {
|
|
317
|
-
const out = await route(request, ctx);
|
|
318
|
-
return { ...out, headers: withSecurityHeaders(out.headers, out.body) };
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
async function route(request, ctx) {
|
|
322
|
-
const url = new URL(request.url);
|
|
323
|
-
const { pathname } = url;
|
|
324
|
-
|
|
325
|
-
// The new-account page: the host's front door, forking solo / group /
|
|
326
|
-
// manage. Served as a static page the adapter supplies; the page itself
|
|
327
|
-
// calls the availability API below. It only COLLECTS intent — creating an
|
|
328
|
-
// account is the Solid-OIDC attach step (proving the pod is the user's).
|
|
329
|
-
if (pathname === '/' || pathname === '/signup' || pathname === '/new-account') {
|
|
330
|
-
if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
|
|
331
|
-
if (!ctx.signupPage) return notFound();
|
|
332
|
-
return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: ctx.signupPage };
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// The run page: a pod owner opting their identity in or out of being run by
|
|
336
|
-
// this server. Meaningful only on a host that embeds the agent; a host that
|
|
337
|
-
// does not simply supplies no page.
|
|
338
|
-
if (pathname === runPath(ctx)) {
|
|
339
|
-
if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
|
|
340
|
-
if (!ctx.runPage) return notFound();
|
|
341
|
-
return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' }, body: ctx.runPage };
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// The roster page (/roster, off /admin, which is the owner's own record
|
|
345
|
-
// page on every agent) is served with the notices page, in notices.mjs.
|
|
346
|
-
|
|
347
|
-
// Notices from the operator to every account here, and the page they are
|
|
348
|
-
// written on (notices.mjs).
|
|
349
|
-
const notices = await routeNoticesApi(request, pathname, ctx, { j, verifyPodToken, publicFor, notFound });
|
|
350
|
-
if (notices) return notices;
|
|
351
|
-
|
|
352
|
-
// The roster: every directory row, secrets stripped, for the host's own
|
|
353
|
-
// eyes. The reader proves themself the way attach proves a pod — a
|
|
354
|
-
// Solid-OIDC token — and must be the WebID the deploy names as admin.
|
|
355
|
-
if (pathname === '/api/roster') {
|
|
356
|
-
if (!ctx.listDirectory || !ctx.adminWebId) return j(501, { error: 'this front has no roster to offer' });
|
|
357
|
-
const webid = await verifyPodToken(request, pathname, ctx.verifier);
|
|
358
|
-
if (!webid) return j(401, { error: 'a Solid-OIDC token is required' });
|
|
359
|
-
if (webid !== ctx.adminWebId) return j(403, { error: 'that WebID is not the admin of this front' });
|
|
360
|
-
const rows = await ctx.listDirectory();
|
|
361
|
-
const accounts = Object.values(rows)
|
|
362
|
-
.map((r) => ({
|
|
363
|
-
handle: r.handle, kind: r.kind || 'person', fronted: !r.inboxOnly,
|
|
364
|
-
podHome: r.podHome, webId: r.webId || null, actorUrl: r.actorUrl,
|
|
365
|
-
address: r.address || `@${r.handle}@${ctx.host}`,
|
|
366
|
-
openedAt: r.openedAt || null, pausedAt: r.pausedAt || null, closedAt: r.closedAt || null,
|
|
367
|
-
}))
|
|
368
|
-
.sort((a, b) => (a.address || a.handle).localeCompare(b.address || b.handle));
|
|
369
|
-
return j(200, { host: ctx.host, accounts });
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Revoke: the admin removes an account's row, so the server stops answering
|
|
373
|
-
// for the name. Nothing on the user's pod is touched. Attach-created rows go
|
|
374
|
-
// for good; a row seeded in the deploy's environment can only be removed there.
|
|
375
|
-
if (pathname === '/api/revoke' && request.method === 'POST') {
|
|
376
|
-
if (!ctx.removeDirectory || !ctx.adminWebId) return j(501, { error: 'this front cannot revoke accounts' });
|
|
377
|
-
const webid = await verifyPodToken(request, pathname, ctx.verifier);
|
|
378
|
-
if (!webid) return j(401, { error: 'a Solid-OIDC token is required' });
|
|
379
|
-
if (webid !== ctx.adminWebId) return j(403, { error: 'that WebID is not the admin of this front' });
|
|
380
|
-
let body;
|
|
381
|
-
try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
|
|
382
|
-
// Revoke by full address (what the roster lists); a bare handle still works
|
|
383
|
-
// for any legacy row keyed that way.
|
|
384
|
-
const key = String(body.address || body.handle || '').replace(/^@/, '').toLowerCase();
|
|
385
|
-
if (!key || !(await ctx.lookup(key))) return j(404, { error: 'no such account' });
|
|
386
|
-
const removed = (await ctx.removeDirectory(key)) === true;
|
|
387
|
-
return j(200, removed ? { account: key, removed }
|
|
388
|
-
: { account: key, removed: false,
|
|
389
|
-
reason: 'this row is seeded in the deploy environment (FEDIPOD_DIRECTORY_JSON) — remove it there and redeploy' });
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Live handle check for the page: valid shape AND not already in the
|
|
393
|
-
// directory. Also tells the page whether this host offers pods.
|
|
394
|
-
if (pathname === '/api/handle') {
|
|
395
|
-
const h = (url.searchParams.get('handle') || '').toLowerCase();
|
|
396
|
-
const problem = handleProblem(h);
|
|
397
|
-
if (problem) {
|
|
398
|
-
return j(200, { handle: h, available: false, reason: problem,
|
|
399
|
-
offersPods: !!ctx.offersPods, version: ctx.version || null });
|
|
400
|
-
}
|
|
401
|
-
const taken = !!(await ctx.lookup(h));
|
|
402
|
-
return j(200, { handle: h, available: !taken, reason: taken ? 'that name is taken' : null,
|
|
403
|
-
offersPods: !!ctx.offersPods, version: ctx.version || null });
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// What a reader's own server speaks, asked here rather than in their
|
|
407
|
-
// browser: a page cannot probe a stranger's server (no CORS), and the
|
|
408
|
-
// answer decides how that reader takes part in a forum.
|
|
409
|
-
if (pathname === '/api/server') {
|
|
410
|
-
const host = (url.searchParams.get('host') || '').trim().toLowerCase();
|
|
411
|
-
const open = { 'access-control-allow-origin': '*' };
|
|
412
|
-
const said = (kind, extra = {}) => ({ ...j(200, { host, kind, ...extra }), headers: { ...j(200, {}).headers, ...open } });
|
|
413
|
-
// An address at this front is a FediPod account: its client is the
|
|
414
|
-
// browser here, and no other server holds it. Asked before the shape of
|
|
415
|
-
// the host, since a front may answer at a name with no dot in it.
|
|
416
|
-
if (host && host === String(ctx.host || '').toLowerCase()) {
|
|
417
|
-
const handle = (url.searchParams.get('handle') || '').toLowerCase();
|
|
418
|
-
const rec = handle ? await ctx.lookup(handle) : null;
|
|
419
|
-
if (!rec) return said('fedipod');
|
|
420
|
-
// Where the owner of this pod signs in: the pod says so itself, at the
|
|
421
|
-
// well-known document every Solid server answers (or redirects to).
|
|
422
|
-
let issuer = null;
|
|
423
|
-
try {
|
|
424
|
-
const res = await safeFetch(new URL('/.well-known/openid-configuration', rec.podHome).href,
|
|
425
|
-
{ headers: { accept: 'application/json' }, redirect: 'follow', signal: AbortSignal.timeout(SERVER_PROBE_MS) },
|
|
426
|
-
ctx.fetchImpl || fetch);
|
|
427
|
-
if (res.ok) issuer = JSON.parse(await readCapped(res, SERVER_PROBE_BYTES)).issuer || null;
|
|
428
|
-
} catch { /* a pod that will not say leaves the reader to type it */ }
|
|
429
|
-
return said('fedipod', { podHome: rec.podHome, actor: rec.actorUrl, issuer, inbox: identFor(rec).inboxUrl });
|
|
430
|
-
}
|
|
431
|
-
if (!/^[a-z0-9.-]+\.[a-z0-9-]+(:\d+)?$/u.test(host)) return said('invalid');
|
|
432
|
-
const f = ctx.fetchImpl || fetch;
|
|
433
|
-
const ask = async (path) => {
|
|
434
|
-
try {
|
|
435
|
-
const res = await safeFetch(`https://${host}${path}`,
|
|
436
|
-
{ headers: { accept: 'application/json' }, signal: AbortSignal.timeout(SERVER_PROBE_MS) }, f);
|
|
437
|
-
if (!res.ok) return null;
|
|
438
|
-
return JSON.parse(await readCapped(res, SERVER_PROBE_BYTES));
|
|
439
|
-
} catch { return null; }
|
|
440
|
-
};
|
|
441
|
-
const masto = await ask('/api/v1/instance');
|
|
442
|
-
if (masto) return said('mastodon-api', { name: masto.title || masto.domain || host });
|
|
443
|
-
const lemmy = await ask('/api/v3/site');
|
|
444
|
-
if (lemmy) return said('lemmy', { name: lemmy.site_view?.site?.name || host });
|
|
445
|
-
return said('unknown');
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Attach: create the account. The user proves control of the pod they claim
|
|
449
|
-
// by presenting a Solid-OIDC token whose WebID lives on that pod — no
|
|
450
|
-
// password reaches the front. On success the directory gets a row and the
|
|
451
|
-
// agent gets a one-time HMAC secret to stamp the receipts its front writes.
|
|
452
|
-
if (pathname === '/api/attach' && request.method === 'POST') {
|
|
453
|
-
if (!ctx.putDirectory) return j(501, { error: 'this front does not accept signups' });
|
|
454
|
-
let body;
|
|
455
|
-
try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
|
|
456
|
-
const handle = String(body.handle || '').toLowerCase();
|
|
457
|
-
const podHome = String(body.podHome || '');
|
|
458
|
-
const problem = handleProblem(handle);
|
|
459
|
-
if (problem) return j(400, { error: problem });
|
|
460
|
-
const podLoopback = (() => { try { return isLoopbackHost(new URL(podHome).hostname); } catch { return false; } })();
|
|
461
|
-
if (!(/^https:\/\/\S+\/$/.test(podHome) || (/^http:\/\/\S+\/$/.test(podHome) && podLoopback))) {
|
|
462
|
-
return j(400, { error: 'podHome must be an https URL ending in /' });
|
|
463
|
-
}
|
|
464
|
-
const badPod = podHomeProblem(podHome, ctx.frontOrigin);
|
|
465
|
-
if (badPod) return j(400, { error: badPod });
|
|
466
|
-
const webid = await verifyPodToken(request, pathname, ctx.verifier);
|
|
467
|
-
if (!webid) return j(401, { error: 'a Solid-OIDC token proving the pod is required' });
|
|
468
|
-
if (!await provesPod(webid, podHome, ctx.fetchImpl)) {
|
|
469
|
-
return j(403, { error: 'the token proves a different pod than the one you listed' });
|
|
470
|
-
}
|
|
471
|
-
// The usual attachment keeps the user's identity on their pod and moves
|
|
472
|
-
// only the mail door here (@me@mypod). Fronted identity — actor ids on
|
|
473
|
-
// this domain — remains available to an explicit `fronted: true`.
|
|
474
|
-
const fronted = body.fronted === true;
|
|
475
|
-
// The full address is the key: a fronted name answers at this front's host,
|
|
476
|
-
// a mail-door name at its pod's host. The same handle on two pods is two
|
|
477
|
-
// distinct addresses, so it never collides.
|
|
478
|
-
let addressHost; try { addressHost = fronted ? ctx.host : new URL(podHome).host; } catch { return j(400, { error: 'bad podHome' }); }
|
|
479
|
-
// A fronted name answers at this front's host, where the handle alone is the
|
|
480
|
-
// whole address and is unique — so it keeps keying by bare handle (and its
|
|
481
|
-
// actor id stays /u/<handle>/, unchanged for existing accounts). A mail-door
|
|
482
|
-
// name answers at its own pod, where "me" is not unique, so it keys by the
|
|
483
|
-
// full address handle@podhost, which the /u/ segment then carries.
|
|
484
|
-
const key = fronted ? handle : addressKey(handle, addressHost);
|
|
485
|
-
const address = `@${handle}@${addressHost}`;
|
|
486
|
-
// "Taken" only when this exact address already belongs to a DIFFERENT pod.
|
|
487
|
-
// Re-attaching your own pod — a retry, a re-provision, a second browser — is
|
|
488
|
-
// idempotent: it updates the row in place and keeps the same door secret.
|
|
489
|
-
const prior = await ctx.lookup(key);
|
|
490
|
-
// The same account on the same pod may correct where its tree lives — a
|
|
491
|
-
// row made by an older sign-up named the pod root, and every delivery
|
|
492
|
-
// to it was written where nothing reads. A different pod is still taken.
|
|
493
|
-
const sameOwner = (a, b) => { try { return new URL(a).origin === new URL(b).origin; } catch { return false; } };
|
|
494
|
-
if (prior && prior.podHome !== podHome
|
|
495
|
-
&& !(prior.webId && prior.webId === webid && sameOwner(prior.podHome, podHome))) {
|
|
496
|
-
return j(409, { error: 'that name is taken' });
|
|
497
|
-
}
|
|
498
|
-
// Re-attaching is idempotent — a retry, a re-provision, a second browser —
|
|
499
|
-
// and it hands back the EXISTING receipt secret. So it has to be the same
|
|
500
|
-
// person: matching podHome was enough on a subdomain server and, on a
|
|
501
|
-
// path-based one, meant any co-tenant could re-attach your row, flip its
|
|
502
|
-
// kind, and be given your secret (S7). A row that predates `webId` has
|
|
503
|
-
// nothing to compare and falls back to the pod check above.
|
|
504
|
-
if (prior?.webId && prior.webId !== webid) {
|
|
505
|
-
return j(409, { error: 'that name belongs to a different account on this pod' });
|
|
506
|
-
}
|
|
507
|
-
// The pod's actor lives under its AP root container, which the pod owner
|
|
508
|
-
// knows and the front does not — so take the caller's actorUrl when it is on
|
|
509
|
-
// this pod, and fall back only when none is given.
|
|
510
|
-
const claimedActor = typeof body.actorUrl === 'string' && body.actorUrl.startsWith(podHome) ? body.actorUrl : null;
|
|
511
|
-
const actorUrl = fronted ? `${userBase(ctx.frontOrigin, key)}ap/actor` : (claimedActor || podHome + 'ap/actor');
|
|
512
|
-
const hmacSecret = prior?.hmacSecret || crypto.randomBytes(32).toString('base64');
|
|
513
|
-
// Where the door writes: the row's own tree unless the caller names an
|
|
514
|
-
// inbox on the same pod — a forum's categories name the forum's.
|
|
515
|
-
let inboxUrl = null;
|
|
516
|
-
if (typeof body.inboxUrl === 'string' && body.inboxUrl) {
|
|
517
|
-
let ok = false;
|
|
518
|
-
try { ok = new URL(body.inboxUrl).origin === new URL(podHome).origin && body.inboxUrl.endsWith('/'); } catch { ok = false; }
|
|
519
|
-
if (!ok) return j(400, { error: 'inboxUrl must be a container on the same pod' });
|
|
520
|
-
inboxUrl = body.inboxUrl;
|
|
521
|
-
}
|
|
522
|
-
const record = {
|
|
523
|
-
handle, address, podHome, webId: webid, actorUrl,
|
|
524
|
-
kind: body.kind === 'group' ? 'group' : body.kind === 'application' ? 'application' : 'person',
|
|
525
|
-
gatewayWebId: ctx.gatewayWebId || null, hmacSecret,
|
|
526
|
-
...(inboxUrl ? { inboxUrl } : {}),
|
|
527
|
-
...(fronted ? {} : { inboxOnly: true }),
|
|
528
|
-
};
|
|
529
|
-
await ctx.putDirectory(key, record);
|
|
530
|
-
// The secret is returned ONCE, with the one command the user runs to
|
|
531
|
-
// point their agent at this gateway.
|
|
532
|
-
const doorInbox = `${userBase(ctx.frontOrigin, key)}ap/inbox/`;
|
|
533
|
-
if (fronted) {
|
|
534
|
-
return j(201, { ok: true, handle, address, actorUrl, frontActor: actorUrl, hmacSecret,
|
|
535
|
-
command: `fedipod gateway ${actorUrl} --secret ${hmacSecret}` });
|
|
536
|
-
}
|
|
537
|
-
return j(201, { ok: true, handle, address, doorInbox, hmacSecret,
|
|
538
|
-
command: `fedipod gateway ${doorInbox} --secret ${hmacSecret} --inbox-only` });
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// An address at this gateway moving to another one. Its owner, proved the
|
|
542
|
-
// way attach proves a pod, says where it went. From then on this front
|
|
543
|
-
// answers the old actor as a moved stub — the same key under the old id,
|
|
544
|
-
// `movedTo` the new one, so the Move the owner sends from the old address
|
|
545
|
-
// verifies — every other old id redirects to its new address, and the
|
|
546
|
-
// mail door is shut: deliveries belong at the new one. Repeating it with
|
|
547
|
-
// the same target changes nothing.
|
|
548
|
-
if (pathname === '/api/move' && request.method === 'OPTIONS') return apiPreflight();
|
|
549
|
-
if (pathname === '/api/move' && request.method === 'POST') {
|
|
550
|
-
if (!ctx.putDirectory) return withApiCors(j(501, { error: 'this front keeps no directory' }));
|
|
551
|
-
let body;
|
|
552
|
-
try { body = JSON.parse(await request.clone().text()); } catch { return withApiCors(j(400, { error: 'bad JSON' })); }
|
|
553
|
-
const handle = String(body.handle || '').toLowerCase();
|
|
554
|
-
const rec = await ctx.lookup(handle);
|
|
555
|
-
if (!rec) return withApiCors(j(404, { error: 'no such account' }));
|
|
556
|
-
if (rec.inboxOnly) return withApiCors(j(400, { error: 'this address lives on its pod; only an address at this gateway can move' }));
|
|
557
|
-
const movedTo = String(body.movedTo || '');
|
|
558
|
-
let mu;
|
|
559
|
-
try { mu = new URL(movedTo); } catch { return withApiCors(j(400, { error: 'movedTo must be the new actor URL' })); }
|
|
560
|
-
if (!((mu.protocol === 'https:' || (mu.protocol === 'http:' && isLoopbackHost(mu.hostname))) && /\/ap\/actor$/u.test(mu.pathname))) {
|
|
561
|
-
return withApiCors(j(400, { error: 'movedTo must be an https …/ap/actor URL' }));
|
|
562
|
-
}
|
|
563
|
-
if (ctx.frontOrigin && mu.origin === new URL(ctx.frontOrigin).origin) return withApiCors(j(400, { error: 'that address is at this gateway' }));
|
|
564
|
-
const webid = await verifyPodToken(request, pathname, ctx.verifier);
|
|
565
|
-
if (!webid) return withApiCors(j(401, { error: 'a Solid-OIDC token proving the pod is required' }));
|
|
566
|
-
const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
|
|
567
|
-
if (!owner) return withApiCors(j(403, { error: "the token proves a different pod than this account's" }));
|
|
568
|
-
const movedAt = rec.movedTo === movedTo && rec.movedAt ? rec.movedAt : new Date().toISOString();
|
|
569
|
-
await ctx.putDirectory(handle, { ...rec, movedTo, movedAt });
|
|
570
|
-
console.log(`front @${handle}: moved to ${movedTo}`);
|
|
571
|
-
return withApiCors(j(200, { ok: true, handle, movedTo, movedAt }));
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// The owner's say over an account that goes quiet (quiet.mjs).
|
|
575
|
-
const quiet = await routeQuietApi(request, pathname, ctx, { j, verifyPodToken, webidUnderPod, apiPreflight });
|
|
576
|
-
if (quiet) return withApiCors(quiet);
|
|
577
|
-
|
|
578
|
-
// The relay: the front sends requests a browser has already signed. A page
|
|
579
|
-
// may not set the Date or Host header, and both are inside an HTTP
|
|
580
|
-
// signature, so a browser-run agent signs and hands the request here; the
|
|
581
|
-
// front sends it with exactly the headers that were signed. It can forward
|
|
582
|
-
// a signature and cannot make one. Only the account's own key may sign,
|
|
583
|
-
// only the account's own pod may call, and nothing unsigned-for reaches
|
|
584
|
-
// the remote server.
|
|
585
|
-
if (pathname === '/api/relay' && request.method === 'OPTIONS') return apiPreflight();
|
|
586
|
-
if (pathname === '/api/relay' && request.method === 'POST') {
|
|
587
|
-
let body;
|
|
588
|
-
try { body = JSON.parse(await request.clone().text()); } catch { return withApiCors(j(400, { error: 'bad JSON' })); }
|
|
589
|
-
const handle = String(body.handle || '').toLowerCase();
|
|
590
|
-
const rec = await ctx.lookup(handle);
|
|
591
|
-
if (!rec) { console.log(`relay: no account named ${handle}`); return withApiCors(j(404, { error: 'no such account' })); }
|
|
592
|
-
const webid = await verifyPodToken(request, pathname, ctx.verifier);
|
|
593
|
-
if (!webid) { console.log(`relay @${handle}: no usable pod token`); return withApiCors(j(401, { error: 'a Solid-OIDC token proving the pod is required' })); }
|
|
594
|
-
const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
|
|
595
|
-
if (!owner) { console.log(`relay @${handle}: token is for ${webid}, not this account's pod`); return withApiCors(j(403, { error: "the token proves a different pod than this account's" })); }
|
|
596
|
-
const items = Array.isArray(body.requests) ? body.requests : [];
|
|
597
|
-
if (!items.length) return withApiCors(j(400, { error: 'requests must be a non-empty list' }));
|
|
598
|
-
// Acting through the relay is being here. Hourly at most; see noteOpened.
|
|
599
|
-
await noteOpened(ctx, handle, rec).catch((e) => console.log(`relay @${handle}: stamp not written: ${e?.message || e}`));
|
|
600
|
-
if (items.length > RELAY_MAX_REQUESTS) return withApiCors(j(400, { error: `at most ${RELAY_MAX_REQUESTS} requests per call` }));
|
|
601
|
-
const slow = overLimit(handle, items.length) && j(429, { error: 'too many relayed requests — slow down' });
|
|
602
|
-
if (slow) return withApiCors({ ...slow, headers: { ...slow.headers, 'retry-after': '60' } });
|
|
603
|
-
const results = await Promise.all(items.map((it) => relayOne(it, rec, ctx.fetchImpl || fetch)));
|
|
604
|
-
// Something of the account's withdrawn: the edge's copies go.
|
|
605
|
-
if (withdrawn(items, rec.actorUrl)) await ctx.purge?.([`u-${handle}`]).catch((e) => console.log(`purge @${handle}: ${e?.message || e}`));
|
|
606
|
-
// One line per relayed request, so a lookup that fails on the far side is
|
|
607
|
-
// visible here and not only as an empty result in someone's browser.
|
|
608
|
-
for (const r of results) console.log(`relay @${handle}: ${r.method || ''} ${r.url} → ${r.status}${r.error ? ` (${r.error})` : ''}`);
|
|
609
|
-
return withApiCors(j(200, { results }));
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// Who is here, and which pod of theirs could opt in. The opt-in page asks
|
|
613
|
-
// this first: on a pod server that issued the reader's session, the answer
|
|
614
|
-
// names their pod and its address, and the page shows one button instead of
|
|
615
|
-
// a form. A Gateway holds no such session and says so; the page then asks
|
|
616
|
-
// for the pod by hand and proves it with a sign-in, as before.
|
|
617
|
-
if (pathname === '/api/agent' && request.method === 'GET') {
|
|
618
|
-
if (!ctx.agentControl) return j(501, { error: 'this server does not run identities' });
|
|
619
|
-
if (!ctx.webIdsFromSession) return j(200, { offered: true, session: 'unknown' });
|
|
620
|
-
if (!sameOriginRequest(request, ctx.frontOrigin)) return j(200, { offered: true, session: 'none' });
|
|
621
|
-
const webIds = await ctx.webIdsFromSession(request).catch(() => []);
|
|
622
|
-
if (!webIds.length) return j(200, { offered: true, session: 'none', loginUrl: '/.account/login/password/' });
|
|
623
|
-
const pods = [];
|
|
624
|
-
for (const webId of webIds) {
|
|
625
|
-
let podBase;
|
|
626
|
-
try { podBase = podBaseOfWebId(webId); } catch { continue; }
|
|
627
|
-
let about = null;
|
|
628
|
-
try { about = ctx.agentControl.describe ? await ctx.agentControl.describe({ podBase }) : null; } catch { about = null; }
|
|
629
|
-
pods.push({ webId, podBase, ...(about || {}) });
|
|
630
|
-
}
|
|
631
|
-
return j(200, { offered: true, session: 'ok', pods });
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// Runtime opt-in: a pod owner asks the server this front sits on to RUN
|
|
635
|
-
// their identity. Same proof as attach — a Solid-OIDC token — but the
|
|
636
|
-
// ownership check is stricter: the WebID must live UNDER the pod base, not
|
|
637
|
-
// merely on its origin, or on a path-pod server one user could opt in
|
|
638
|
-
// another's pod. Offered only where the deployment wired agentControl in
|
|
639
|
-
// (a pod server running the CSS component); everywhere else it is 501.
|
|
640
|
-
if (pathname === '/api/agent' && request.method === 'POST') {
|
|
641
|
-
if (!ctx.agentControl) return j(501, { error: 'this server does not run identities' });
|
|
642
|
-
let body;
|
|
643
|
-
try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
|
|
644
|
-
const action = String(body.action || '');
|
|
645
|
-
const podBase = String(body.podBase || '');
|
|
646
|
-
if (!/^https?:\/\/\S+\/$/.test(podBase)) {
|
|
647
|
-
return j(400, { error: 'podBase must be a URL ending in /' });
|
|
648
|
-
}
|
|
649
|
-
let podUrl;
|
|
650
|
-
try { podUrl = new URL(podBase); } catch { return j(400, { error: 'podBase is not a URL' }); }
|
|
651
|
-
// A pod may be host-root (its own origin) or on a PATH of a shared host — a
|
|
652
|
-
// suffix pod, which the Server runs at `https://server/aisha/`. What is
|
|
653
|
-
// refused is the gateway's own origin root itself: that is where the
|
|
654
|
-
// gateway lives, not a pod, and admitting it would (with the ownership
|
|
655
|
-
// fallback below) let a co-tenant claim the whole origin.
|
|
656
|
-
if (ctx.frontOrigin) {
|
|
657
|
-
try {
|
|
658
|
-
if (podUrl.origin === new URL(ctx.frontOrigin).origin && podUrl.pathname === '/') {
|
|
659
|
-
return j(403, { error: 'that is this gateway, not a pod — name the pod that holds your data' });
|
|
660
|
-
}
|
|
661
|
-
} catch { /* no usable frontOrigin: the checks below still apply */ }
|
|
662
|
-
}
|
|
663
|
-
if (/(^|\/)\.internal(\/|$)/u.test(podUrl.pathname) || /(^|\/)\.\.(\/|$)/u.test(podUrl.pathname)) {
|
|
664
|
-
return j(403, { error: 'that is not a pod address' });
|
|
665
|
-
}
|
|
666
|
-
// The pod's own server names its owner when it can, and where it does that
|
|
667
|
-
// is the proof. Where it does not, the WebID must live in EXACTLY this pod
|
|
668
|
-
// — its own pod base equal to podBase, not merely starting with it, which on
|
|
669
|
-
// a path server an ancestor of another's pod would.
|
|
670
|
-
const owners = await podOwners(podBase, ctx.fetchImpl || fetch);
|
|
671
|
-
const ownsIt = (who) => (owners.length ? owners.includes(who) : podBaseOfWebId(who) === podBase);
|
|
672
|
-
const token = await verifyPodToken(request, pathname, ctx.verifier);
|
|
673
|
-
// A server that issued the reader's own session can simply read it. On a
|
|
674
|
-
// pod server the page, the session and the pod are one origin, and a token
|
|
675
|
-
// proves nothing the session does not — so opting in cost a round trip to
|
|
676
|
-
// an identity provider that had just said who they were. A Gateway fronting
|
|
677
|
-
// somebody else's pod holds no such session and this does nothing there.
|
|
678
|
-
//
|
|
679
|
-
// One account can hold several WebIDs, so it is the one that owns this pod
|
|
680
|
-
// that is taken, not whichever came back first.
|
|
681
|
-
const session = !token && ctx.webIdsFromSession && sameOriginRequest(request, ctx.frontOrigin)
|
|
682
|
-
? (await ctx.webIdsFromSession(request).catch(() => [])).find(ownsIt) || null
|
|
683
|
-
: null;
|
|
684
|
-
const webid = token || session;
|
|
685
|
-
if (!webid) return j(401, { error: 'a Solid-OIDC token proving the pod is required' });
|
|
686
|
-
if (!ownsIt(webid)) {
|
|
687
|
-
return j(403, { error: 'the token proves a different pod than the one you listed' });
|
|
688
|
-
}
|
|
689
|
-
if (action === 'opt-in') {
|
|
690
|
-
const { httpStatus, ...reply } = await ctx.agentControl.optIn({ podBase, webId: webid,
|
|
691
|
-
container: String(body.container || ''), createIndex: body.createIndex === true });
|
|
692
|
-
if (reply.doorSecret) {
|
|
693
|
-
// The secret appears here and nowhere else; the command is the
|
|
694
|
-
// paste-and-run way to open the owner door once.
|
|
695
|
-
reply.command = `curl -H 'x-dk-token: ${reply.doorSecret}' ${podBase.replace(/\/$/, '')}${reply.doorPath}status`;
|
|
696
|
-
}
|
|
697
|
-
return j(httpStatus, reply);
|
|
698
|
-
}
|
|
699
|
-
if (action === 'opt-out') {
|
|
700
|
-
const { httpStatus, ...reply } = await ctx.agentControl.optOut({ podBase });
|
|
701
|
-
return j(httpStatus, reply);
|
|
702
|
-
}
|
|
703
|
-
return j(400, { error: 'action must be opt-in or opt-out' });
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// Each page's own script. They were inline until 2026-09-09; out here the
|
|
707
|
-
// pages can be served under `script-src 'self'`, which is the directive that
|
|
708
|
-
// tells running code from injected markup — and inline script defeats it by
|
|
709
|
-
// construction. Same mechanism the vendored library below has always used.
|
|
710
|
-
//
|
|
711
|
-
// The name is matched against a literal list, so nothing about the request
|
|
712
|
-
// chooses a file.
|
|
713
|
-
const mPageScript = /^\/(new-account|run|admin|notices)\.js$/u.exec(pathname);
|
|
714
|
-
if (mPageScript) {
|
|
715
|
-
const body = ctx.pageScripts?.[`${mPageScript[1]}.js`];
|
|
716
|
-
if (!body) return notFound();
|
|
717
|
-
// Revalidated on every load: a page served fresh with a script the
|
|
718
|
-
// browser kept from last time is the mismatch nobody can see.
|
|
719
|
-
return { status: 200, headers: { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-cache' }, body };
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// The vendored Solid-OIDC browser library the opt-in and admin pages load —
|
|
723
|
-
// served here because this function owns every path on the domain.
|
|
724
|
-
if (pathname === '/solid-oidc-client.js') {
|
|
725
|
-
if (!ctx.authBundle) return notFound();
|
|
726
|
-
return { status: 200, headers: { 'content-type': 'text/javascript' }, body: ctx.authBundle };
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// The installer: curl -fsSL https://<host>/install | sh
|
|
730
|
-
if (pathname === '/install') {
|
|
731
|
-
if (!ctx.installScript) return notFound();
|
|
732
|
-
return { status: 200, headers: { 'content-type': 'text/plain; charset=utf-8' }, body: ctx.installScript };
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// WebFinger: acct:<handle>@<host> → the fronted actor. This is what makes
|
|
736
|
-
// @name@fedipod.net a real handle the fediverse can look up.
|
|
737
|
-
if (pathname === '/.well-known/webfinger') {
|
|
738
|
-
const resource = url.searchParams.get('resource') || '';
|
|
739
|
-
const m = /^acct:([^@]+)@(.+)$/.exec(resource);
|
|
740
|
-
if (!m || m[2] !== ctx.host) return notFound();
|
|
741
|
-
const rec = await ctx.lookup(m[1]);
|
|
742
|
-
if (!rec) return notFound();
|
|
743
|
-
// A closed address is gone, and says so rather than pretending never to
|
|
744
|
-
// have existed: the name stays taken.
|
|
745
|
-
if ((await closedState(ctx, m[1], rec)).closed) return closedAnswer({ 'access-control-allow-origin': '*', ...publicFor(GONE_EDGE_SECONDS) });
|
|
746
|
-
// A fronted identity's documents live on its pod; the pod's own actor id
|
|
747
|
-
// is the alias, so a client signing in by the fronted address can find
|
|
748
|
-
// the pod (and its login) without a lookup only the host could answer.
|
|
749
|
-
const podActor = rec.inboxOnly ? [] : [rec.podHome + 'ap/actor'];
|
|
750
|
-
const out = j(200, jrd({ handle: m[1], host: ctx.host, actor: rec.actorUrl, aliases: podActor,
|
|
751
|
-
page: profilePageOf(rec), subscribe: subscribeTemplate(ctx.frontOrigin) }),
|
|
752
|
-
'application/jrd+json');
|
|
753
|
-
// RFC 7033: a JRD is read from any origin.
|
|
754
|
-
out.headers['access-control-allow-origin'] = '*';
|
|
755
|
-
// A handle answers the same for everybody, and changes only when a row
|
|
756
|
-
// does. Every server that has heard of this account asks for it; the edge
|
|
757
|
-
// can answer most of them.
|
|
758
|
-
Object.assign(out.headers, publicFor(WEBFINGER_EDGE_SECONDS));
|
|
759
|
-
return out;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// The short address a person types: https://<host>/@handle opens the
|
|
763
|
-
// profile page, the way most Fediverse servers spell it. /@handle@pod-host
|
|
764
|
-
// is the same for an account whose address is its pod's, which the front
|
|
765
|
-
// holds under that full address — the form other servers use for a
|
|
766
|
-
// profile that lives elsewhere.
|
|
767
|
-
const at = /^\/@([^/@]+)(?:@([^/@]+))?$/u.exec(pathname);
|
|
768
|
-
if (at) {
|
|
769
|
-
const handle = decodeURIComponent(at[1]).toLowerCase();
|
|
770
|
-
const host = at[2] ? decodeURIComponent(at[2]).toLowerCase() : null;
|
|
771
|
-
const key = host && host !== String(ctx.host).toLowerCase() ? `${handle}@${host}` : handle;
|
|
772
|
-
const rec = await ctx.lookup(key);
|
|
773
|
-
const page = rec && profilePageOf(rec);
|
|
774
|
-
if (!page) return notFound();
|
|
775
|
-
return { status: 302, headers: { location: page, 'cache-control': 'no-store' }, body: '' };
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
const up = parseUserPath(pathname);
|
|
779
|
-
if (!up) return notFound();
|
|
780
|
-
const rec = await ctx.lookup(up.handle);
|
|
781
|
-
if (!rec) return notFound();
|
|
782
|
-
const base = userBase(ctx.frontOrigin, up.handle);
|
|
783
|
-
// An account that moved to another gateway (see /api/move): where its ids
|
|
784
|
-
// live now. The actor below answers as a stub; everything else redirects.
|
|
785
|
-
const movedBase = rec.movedTo ? rec.movedTo.replace(/ap\/actor$/u, '') : null;
|
|
786
|
-
// A closed address (see "accounts that go quiet" above): every id under it
|
|
787
|
-
// is gone. Asked after moved, which a close does not undo.
|
|
788
|
-
const closed = movedBase ? false : (await closedState(ctx, up.handle, rec)).closed;
|
|
789
|
-
const gone = (headers = {}, why = `this account has moved to ${rec.movedTo}`) => closedAnswer(headers, why);
|
|
790
|
-
const CLOSED = 'this address is closed';
|
|
791
|
-
|
|
792
|
-
// Inbox: verify at the door, forward to the user's pod inbox. This is the
|
|
793
|
-
// gateway, per user.
|
|
794
|
-
if (up.rest === 'ap/inbox/' || up.rest === 'ap/inbox') {
|
|
795
|
-
if (request.method !== 'POST') return { status: 405, headers: {}, body: '' };
|
|
796
|
-
if (movedBase) { console.log(`door @${up.handle}: delivery → 410 (moved)`); return gone(); }
|
|
797
|
-
if (closed) { console.log(`door @${up.handle}: delivery → 410 (closed)`); return gone({}, CLOSED); }
|
|
798
|
-
const policy = await policyFor(rec, ctx.fetchImpl || fetch);
|
|
799
|
-
const standing = await accountState(ctx, up.handle, rec);
|
|
800
|
-
const { status, reason, content, bytes } = await handleDelivery(request, identFor(rec, policy),
|
|
801
|
-
{ podPut: (u, b, ct) => ctx.podPut(up.handle, u, b, ct), fetchImpl: ctx.fetchImpl, paused: standing.paused });
|
|
802
|
-
// One line per delivery, so "did it arrive at the door" has an answer.
|
|
803
|
-
console.log(`door @${up.handle}: delivery → ${status} (${reason})`);
|
|
804
|
-
if (status === 202 && content) await noteReceived(ctx, up.handle, rec, bytes);
|
|
805
|
-
return { status, headers: {}, body: '' };
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// Outbox: the owner's own post, from any client that speaks ActivityPub
|
|
809
|
-
// client-to-server (dokieli, say). Cross-origin by nature — such a client is
|
|
810
|
-
// a page on another site — so this route answers a preflight and carries
|
|
811
|
-
// CORS headers; the owner's pod token is the credential, so any origin may
|
|
812
|
-
// ask. GET stays a read: the pod's document for a fronted account, sent on
|
|
813
|
-
// to the pod for a door account whose documents live at their own address.
|
|
814
|
-
if (up.rest === 'ap/outbox') {
|
|
815
|
-
const cors = {
|
|
816
|
-
'access-control-allow-origin': '*',
|
|
817
|
-
'access-control-allow-methods': 'GET, POST, OPTIONS',
|
|
818
|
-
'access-control-allow-headers': 'Authorization, DPoP, Content-Type, Slug, Link',
|
|
819
|
-
'access-control-expose-headers': 'Location, Accept-Post',
|
|
820
|
-
'access-control-max-age': '86400',
|
|
821
|
-
};
|
|
822
|
-
if (request.method === 'OPTIONS') {
|
|
823
|
-
// Accept-Post names JSON only: a client that reads it (dokieli) sends
|
|
824
|
-
// JSON-LD when HTML is not offered.
|
|
825
|
-
// body null, not '': a Response refuses any body on a 204, and the
|
|
826
|
-
// function adapter hands `body` straight to one.
|
|
827
|
-
return { status: 204, headers: { ...cors, allow: 'GET, POST, OPTIONS',
|
|
828
|
-
'accept-post': 'application/ld+json, application/activity+json' }, body: null };
|
|
829
|
-
}
|
|
830
|
-
if (movedBase && request.method === 'POST') return gone(cors);
|
|
831
|
-
if (closed && request.method === 'POST') return gone(cors, CLOSED);
|
|
832
|
-
if (movedBase && (request.method === 'GET' || request.method === 'HEAD')) {
|
|
833
|
-
return { status: 301, headers: { ...cors, location: movedBase + up.rest, 'cache-control': 'no-store' }, body: '' };
|
|
834
|
-
}
|
|
835
|
-
if (request.method === 'POST') {
|
|
836
|
-
const json = (status, obj, extra = {}) => ({ status,
|
|
837
|
-
headers: { ...cors, 'content-type': 'application/json', 'cache-control': 'no-store', ...extra },
|
|
838
|
-
body: JSON.stringify(obj) });
|
|
839
|
-
const webid = await verifyPodToken(request, pathname, ctx.verifier);
|
|
840
|
-
if (!webid) return json(401, { error: 'a Solid-OIDC token proving this account\'s owner is required' });
|
|
841
|
-
const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
|
|
842
|
-
if (!owner) return json(403, { error: 'this outbox belongs to its owner alone' });
|
|
843
|
-
const onPod = (u) => u.replace(rec.actorUrl.replace(/ap\/actor$/u, ''), rec.podHome);
|
|
844
|
-
const { status, reason, location, object } = await handleOwnerPost(request, identFor(rec),
|
|
845
|
-
{ podPut: (u, b, ct) => ctx.podPut(up.handle, u, b, ct), ownerWebId: webid,
|
|
846
|
-
exists: async (u) => ((await (ctx.podGet || fetch)(onPod(u), { podHome: rec.podHome })).status !== 404) });
|
|
847
|
-
console.log(`door @${up.handle}: owner post → ${status} (${reason})`);
|
|
848
|
-
if (status !== 201) return json(status, { error: reason });
|
|
849
|
-
return json(201, { accepted: true, ...(location ? { id: location } : {}), ...(object ? { object } : {}),
|
|
850
|
-
note: 'it goes out when your FediPod agent next runs' }, location ? { location } : {});
|
|
851
|
-
}
|
|
852
|
-
// Its owner, signed in, reads every message the account produced (§5.1:
|
|
853
|
-
// the outbox is filtered by who asks); that view is on the pod, under the
|
|
854
|
-
// owner's own rule. Anyone else reads the public one.
|
|
855
|
-
// The token is only read here, not verified: the redirect grants nothing,
|
|
856
|
-
// the pod checks the credential when the owner arrives there, and a
|
|
857
|
-
// verification per read would let anyone make the front fetch keys from a
|
|
858
|
-
// server they name.
|
|
859
|
-
if ((request.method === 'GET' || request.method === 'HEAD') && request.headers.get('authorization')) {
|
|
860
|
-
const webid = claimedWebId(request);
|
|
861
|
-
if (webid && (rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome))) {
|
|
862
|
-
return { status: 303, headers: { ...cors, location: rec.podHome + 'ap/private/outbox', 'cache-control': 'no-store' }, body: '' };
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
if (rec.inboxOnly && (request.method === 'GET' || request.method === 'HEAD')) {
|
|
866
|
-
return { status: 303, headers: { ...cors, location: rec.podHome + 'ap/outbox', 'cache-control': 'no-store' }, body: '' };
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// Everything else is a public GET, served by reading the user's pod and
|
|
871
|
-
// rewriting pod ids to the front. The actor also gets its handle and inbox
|
|
872
|
-
// fixed to the front so a consumer cross-checks it consistently.
|
|
873
|
-
// Public documents are read from any origin — a page on another site, the
|
|
874
|
-
// forum's own host among them — the way a pod serves them, so the read
|
|
875
|
-
// carries the open origin and a preflight is answered.
|
|
876
|
-
const open = { 'access-control-allow-origin': '*' };
|
|
877
|
-
if (request.method === 'OPTIONS') {
|
|
878
|
-
return { status: 204, headers: { ...open, 'access-control-allow-methods': 'GET, HEAD, OPTIONS',
|
|
879
|
-
'access-control-allow-headers': 'Accept', 'access-control-max-age': '86400', allow: 'GET, HEAD, OPTIONS' }, body: null };
|
|
880
|
-
}
|
|
881
|
-
if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
|
|
882
|
-
// Moved: every old id but the actor's answers with where it is now, so a
|
|
883
|
-
// reply or a boost another server still holds under the old id resolves.
|
|
884
|
-
if (movedBase && up.rest !== 'ap/actor') {
|
|
885
|
-
return { status: 301, headers: { ...open, location: movedBase + up.rest, 'cache-control': 'no-store' }, body: '' };
|
|
886
|
-
}
|
|
887
|
-
if (closed) return gone({ ...open, ...publicFor(GONE_EDGE_SECONDS) }, CLOSED);
|
|
888
|
-
const podTarget = rec.podHome + up.rest;
|
|
889
|
-
// The owner's own documents — the full outbox, the liked list, the pending
|
|
890
|
-
// lists — are read at the pod, which decides who may; never held here,
|
|
891
|
-
// where one reader's answer would be served to the next.
|
|
892
|
-
if (up.rest.startsWith('ap/private/')) {
|
|
893
|
-
return { status: 303, headers: { ...open, location: podTarget, 'cache-control': 'no-store' }, body: '' };
|
|
894
|
-
}
|
|
895
|
-
// Media stays on the pod (lib/pod/urls.mjs keeps `media` off the front), but
|
|
896
|
-
// the id rewrite below turns media links onto the front like every other
|
|
897
|
-
// pod url in a document. Answer those by pointing at the pod: bytes are not
|
|
898
|
-
// a document to cap and relabel, and remotes follow a redirect for a picture.
|
|
899
|
-
if (up.rest.startsWith('ap/media/')) {
|
|
900
|
-
return { status: 302, headers: { location: podTarget, ...publicFor(MEDIA_EDGE_SECONDS) }, body: '' };
|
|
901
|
-
}
|
|
902
|
-
// The pod this read belongs to travels with it: an adapter reading a store
|
|
903
|
-
// directly (the CSS server component) has no access control of its own and
|
|
904
|
-
// needs to be told what it may reach. See podHomeProblem above for the other
|
|
905
|
-
// half — a row that names something which is not a pod.
|
|
906
|
-
// A container is Turtle, a profile page is HTML, everything else here is
|
|
907
|
-
// ActivityPub. Ask for what the thing can be, not for what most of them are.
|
|
908
|
-
const container = podTarget.endsWith('/');
|
|
909
|
-
const page = /\.html?$/u.test(podTarget);
|
|
910
|
-
const accept = container ? 'text/turtle, */*;q=0.8'
|
|
911
|
-
: page ? 'text/html, */*;q=0.8'
|
|
912
|
-
: AP_CT;
|
|
913
|
-
const got = await podRoot.readPublicDocument(
|
|
914
|
-
ctx.podGet || ((u) => fetch(u, { headers: { accept } })),
|
|
915
|
-
podTarget, { podHome: rec.podHome, accept });
|
|
916
|
-
if (got.text === null) {
|
|
917
|
-
const hold = [401, 403, 404, 410].includes(got.status) ? { ...publicFor(MISSING_EDGE_SECONDS), 'netlify-cache-tag': `u-${up.handle}` } : {};
|
|
918
|
-
// A stranger is not told a document exists that they may not read (§3.2).
|
|
919
|
-
return { status: got.status === 401 || got.status === 403 ? 404 : got.status, headers: { ...open, ...hold }, body: '' };
|
|
920
|
-
}
|
|
921
|
-
let text = got.text;
|
|
922
|
-
text = swap(text, rec.podHome, base);
|
|
923
|
-
// The moved stub: the pod's actor now carries the NEW gateway's ids, and a
|
|
924
|
-
// server verifying the Move fetches the OLD id and needs a document whose
|
|
925
|
-
// id is that old id — with the same key under it, and `movedTo` saying
|
|
926
|
-
// where the account went. Swapping the new base back to this one gives
|
|
927
|
-
// exactly that; alsoKnownAs and movedTo are set below.
|
|
928
|
-
if (movedBase) text = swap(text, movedBase, base);
|
|
929
|
-
if (up.rest === 'ap/actor') {
|
|
930
|
-
try {
|
|
931
|
-
const doc = JSON.parse(text);
|
|
932
|
-
doc.preferredUsername = up.handle; // so @handle@front cross-checks
|
|
933
|
-
doc.inbox = base + 'ap/inbox/'; // deliveries come to the front to be verified
|
|
934
|
-
doc.endpoints = { ...(doc.endpoints || {}), sharedInbox: base + 'ap/inbox/' };
|
|
935
|
-
// The owner's own documents are read at the pod, where their credential
|
|
936
|
-
// works; they keep the pod's address.
|
|
937
|
-
for (const k of ['liked', 'ownerOutbox']) if (typeof doc[k] === 'string') doc[k] = doc[k].replace(base, rec.podHome);
|
|
938
|
-
if (movedBase) {
|
|
939
|
-
doc.movedTo = rec.movedTo;
|
|
940
|
-
const aka = new Set([].concat(doc.alsoKnownAs || []).filter((a) => typeof a === 'string' && a !== doc.id));
|
|
941
|
-
aka.add(rec.movedTo);
|
|
942
|
-
doc.alsoKnownAs = [...aka];
|
|
943
|
-
const ctxs = [].concat(doc['@context'] || []);
|
|
944
|
-
const declared = (term) => ctxs.some((c) => c && typeof c === 'object' && term in c);
|
|
945
|
-
if (!declared('alsoKnownAs')) ctxs.push({ alsoKnownAs: { '@id': 'as:alsoKnownAs', '@type': '@id' } });
|
|
946
|
-
if (!declared('movedTo')) ctxs.push({ movedTo: { '@id': 'as:movedTo', '@type': '@id' } });
|
|
947
|
-
doc['@context'] = ctxs;
|
|
948
|
-
}
|
|
949
|
-
text = JSON.stringify(doc);
|
|
950
|
-
} catch { /* leave the rewritten text as-is if it will not parse */ }
|
|
951
|
-
}
|
|
952
|
-
// `readPublicDocument` asked for it with no credential, so a document that
|
|
953
|
-
// came back is one anybody can read — which is what makes it cacheable. A
|
|
954
|
-
// deleted post's Tombstone answers 410 Gone, and still says what it was.
|
|
955
|
-
let kind = null;
|
|
956
|
-
try { kind = JSON.parse(text)?.type; } catch { /* not JSON: a page or a container */ }
|
|
957
|
-
return { status: kind === 'Tombstone' ? 410 : 200,
|
|
958
|
-
headers: { ...open, 'content-type': got.type || (container ? 'text/turtle' : page ? 'text/html' : AP_CT), ...publicFor(PUBLIC_EDGE_SECONDS),
|
|
959
|
-
// The outbox answers its signed-in owner differently, so the edge keeps
|
|
960
|
-
// the public copy for those who show no credential.
|
|
961
|
-
...(up.rest === 'ap/outbox' ? { 'netlify-vary': 'header=Authorization', vary: 'Authorization' } : {}),
|
|
962
|
-
'netlify-cache-tag': `u-${up.handle}` },
|
|
963
|
-
body: text };
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
export const _internal = { parseUserPath, identFor, swap, handleProblem, policyFor, policyCache };
|