fedipod-server 0.29.0 → 0.30.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 +11 -6
- package/dist/door.d.ts +4 -0
- package/dist/door.js +31 -0
- package/dist/fedipod.d.ts +16 -0
- package/dist/fedipod.js +13 -0
- package/dist/front.d.ts +3 -0
- package/dist/front.js +94 -0
- package/dist/handler.d.ts +36 -101
- package/dist/handler.js +85 -645
- package/dist/handler.jsonld +2 -30
- package/dist/identities.d.ts +62 -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/package.json +10 -18
- 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,58 +0,0 @@
|
|
|
1
|
-
// safefetch.mjs — browser stand-in for lib/safefetch.mjs.
|
|
2
|
-
//
|
|
3
|
-
// The Node version pins connections to a validated IP to stop DNS-rebinding and
|
|
4
|
-
// refuses private targets, using undici and node:dns. A browser cannot open raw
|
|
5
|
-
// sockets and is bound by the same-origin policy and CORS, so the rebinding
|
|
6
|
-
// attack this defends against is not reachable from here; the browser agent's
|
|
7
|
-
// only cross-origin writes go through the relay, which runs the Node checks.
|
|
8
|
-
// This keeps the same surface with plain fetch and no top-level await.
|
|
9
|
-
export const HTTP_TIMEOUT_MS = 20_000;
|
|
10
|
-
const MAX_BYTES = 5 * 1024 * 1024;
|
|
11
|
-
|
|
12
|
-
export function retryAfterMs(res, max = 30 * 60_000) {
|
|
13
|
-
const h = res?.headers?.get?.('retry-after');
|
|
14
|
-
if (!h) return null;
|
|
15
|
-
const secs = /^\d+$/.test(h.trim()) ? Number(h) * 1000 : (Date.parse(h) - Date.now());
|
|
16
|
-
return Number.isFinite(secs) ? Math.max(0, Math.min(secs, max)) : null;
|
|
17
|
-
}
|
|
18
|
-
export function isPrivateAddress() { return false; }
|
|
19
|
-
export function isLoopbackHost(host) { return /^(localhost|127\.|\[?::1)/.test(String(host)); }
|
|
20
|
-
export function insecureUrlReason(url, what = 'address') {
|
|
21
|
-
try { const u = new URL(url); if (u.protocol === 'https:' || isLoopbackHost(u.hostname)) return null;
|
|
22
|
-
if (u.protocol !== 'http:') return `the ${what} must be http(s)`; return null; } catch { return `"${url}" is not a ${what}`; }
|
|
23
|
-
}
|
|
24
|
-
export async function assertPublicUrl() { return null; } // no socket pinning in a browser
|
|
25
|
-
export async function pinnedFor() { return undefined; }
|
|
26
|
-
// The one thing in this file that is NOT made unnecessary by being in a
|
|
27
|
-
// browser. Pinning and the private-address checks answer DNS rebinding, which
|
|
28
|
-
// a browser closes on its own; a byte budget answers a server that simply
|
|
29
|
-
// keeps sending, which it does not. This used to check `content-length` and
|
|
30
|
-
// then hand back `res.text()` — and a chunked response carries no
|
|
31
|
-
// `content-length` at all, so every caller below dereferences a URL a stranger
|
|
32
|
-
// chose (the note, the actor, the receipt, the Tombstone check, WebFinger)
|
|
33
|
-
// with no bound on what comes back. Stream it and stop at the budget, the way
|
|
34
|
-
// the Node version does. TextDecoder rather than Buffer, and `stream: true` so
|
|
35
|
-
// a UTF-8 sequence split across two chunks still decodes.
|
|
36
|
-
export async function readCapped(res, max = MAX_BYTES) {
|
|
37
|
-
const len = Number(res.headers?.get?.('content-length') || 0);
|
|
38
|
-
if (len > max) throw new Error(`response too large (${len} bytes)`);
|
|
39
|
-
if (!res.body) return res.text();
|
|
40
|
-
const reader = res.body.getReader();
|
|
41
|
-
const decoder = new TextDecoder('utf-8');
|
|
42
|
-
let out = '';
|
|
43
|
-
let total = 0;
|
|
44
|
-
for (;;) {
|
|
45
|
-
const { done, value } = await reader.read();
|
|
46
|
-
if (done) break;
|
|
47
|
-
total += value.length;
|
|
48
|
-
if (total > max) {
|
|
49
|
-
await reader.cancel();
|
|
50
|
-
throw new Error(`response exceeded ${max} bytes`);
|
|
51
|
-
}
|
|
52
|
-
out += decoder.decode(value, { stream: true });
|
|
53
|
-
}
|
|
54
|
-
return out + decoder.decode();
|
|
55
|
-
}
|
|
56
|
-
export async function safeFetch(url, init = {}, fetchImpl = fetch) {
|
|
57
|
-
return fetchImpl(url, { ...init, signal: init.signal || AbortSignal.timeout(HTTP_TIMEOUT_MS) });
|
|
58
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// shapes-text.mjs — the shapes document in the browser agent.
|
|
2
|
-
//
|
|
3
|
-
// Same activitystreams.ttl the Node agent reads; esbuild's text loader turns
|
|
4
|
-
// it into a string at build time, so there is no filesystem here and no second
|
|
5
|
-
// copy of the shapes.
|
|
6
|
-
import ttl from '../../../lib/core/shapes/activitystreams.ttl';
|
|
7
|
-
|
|
8
|
-
export const SHAPES_TTL = ttl;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
// web-push.mjs — browser stand-in. The facade needs a VAPID public key to put
|
|
2
|
-
// in its instance document; server-sent push while the tab is closed is not
|
|
3
|
-
// part of the browser build, so sendNotification is a no-op. The keypair is a
|
|
4
|
-
// well-formed placeholder (a real one needs a matching private key, which the
|
|
5
|
-
// browser push model does not use this way).
|
|
6
|
-
const b64u = (u) => btoa(String.fromCharCode(...u)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
7
|
-
export function generateVAPIDKeys() {
|
|
8
|
-
const pub = new Uint8Array(65); pub[0] = 0x04; crypto.getRandomValues(pub.subarray(1));
|
|
9
|
-
const priv = new Uint8Array(32); crypto.getRandomValues(priv);
|
|
10
|
-
return { publicKey: b64u(pub), privateKey: b64u(priv) };
|
|
11
|
-
}
|
|
12
|
-
export function setVapidDetails() { /* no-op */ }
|
|
13
|
-
export async function sendNotification() { return { statusCode: 201, body: '', headers: {} }; }
|
|
14
|
-
export default { generateVAPIDKeys, setVapidDetails, sendNotification };
|
package/web/app/signup.mjs
DELETED
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
// signup.mjs — the setup flow of `fedipod setup`, run in the browser, on the
|
|
2
|
-
// pod session the person already holds. The pod exists before this runs: a
|
|
3
|
-
// new one is made on the provider's own sign-up page, an existing one is
|
|
4
|
-
// signed in to as it is. So there is no account API here, no credential and
|
|
5
|
-
// no password — the session that signed in at the pod is the session that
|
|
6
|
-
// writes to it. Publishing the actor is the agent's first-run job, exactly as
|
|
7
|
-
// `connect()` publishes after `setup` mints — so this module needs nothing
|
|
8
|
-
// from lib beyond the pod helpers.
|
|
9
|
-
//
|
|
10
|
-
// What it writes to the pod: an owner-only state container holding the
|
|
11
|
-
// signing key and the account config; and, with a gateway, the attach.
|
|
12
|
-
|
|
13
|
-
import { generateKeys, isKeyEnvelope } from './keystore.mjs';
|
|
14
|
-
import { BrowserRemotePod } from './pod-remote.mjs';
|
|
15
|
-
import * as podState from '../../lib/pod/state.mjs';
|
|
16
|
-
import { resourceExists } from '../../lib/pod/root.mjs';
|
|
17
|
-
import { podBaseOfWebId } from '../../lib/pod/urls.mjs';
|
|
18
|
-
import { cacheOpenedKeys } from './keys-browser.mjs';
|
|
19
|
-
import { chosenRoot, findAccount, recordPlace, hasPublicIndex } from '../../lib/core/place.mjs';
|
|
20
|
-
|
|
21
|
-
// Everything the agent publishes hangs under a container named `fedipod`, in
|
|
22
|
-
// the container its owner chose (lib/core/place.mjs). The root is stored on the
|
|
23
|
-
// config, so the agent reads it rather than guessing, and the place is recorded
|
|
24
|
-
// in the owner's public type index, which is how the account is found again.
|
|
25
|
-
const actorUrlFor = (pod, root) => `${pod}${root}ap/actor`;
|
|
26
|
-
const keysDocFor = (pod, root) => `${pod}${root}ap-state/keys.json`;
|
|
27
|
-
const stateOf = (pod, root) => `${pod}${root}ap-state/`;
|
|
28
|
-
|
|
29
|
-
/** The account's config as it sits under `root`, read as its owner. */
|
|
30
|
-
const readConfigAt = (remote, pod) => (root) => podState.readConfig(remote, { state: stateOf(pod, root) });
|
|
31
|
-
|
|
32
|
-
const HANDLE_RE = /^[a-z0-9-]{2,30}$/;
|
|
33
|
-
|
|
34
|
-
export function handleProblem(handle) {
|
|
35
|
-
if (!handle) return 'a handle is required';
|
|
36
|
-
if (!HANDLE_RE.test(handle)) return 'letters, digits and hyphens only, 2–30 characters';
|
|
37
|
-
if (handle.startsWith('-') || handle.endsWith('-')) return 'cannot start or end with a hyphen';
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Resume across a failed attempt. A run that throws part-way keeps what it
|
|
42
|
-
// already achieved in this page session — the gateway it attached, the key it
|
|
43
|
-
// wrote — so a second attempt (the failure screen's Try again) continues from
|
|
44
|
-
// the first unfinished step instead of restarting. The record lives only in
|
|
45
|
-
// memory here and is dropped the moment the run completes. Keyed by the
|
|
46
|
-
// identity being built, so changing the handle on the form starts a clean
|
|
47
|
-
// attempt.
|
|
48
|
-
const PROGRESS = new Map();
|
|
49
|
-
const progressKey = (webId, a) => [webId, a.handle, a.shape || 'pod', a.container || ''].join('|');
|
|
50
|
-
|
|
51
|
-
/** A fronted name is one per gateway; a taken one is refused before anything is made. */
|
|
52
|
-
async function assertFrontNameFree(frontOrigin, handle) {
|
|
53
|
-
const res = await fetch(`${frontOrigin.replace(/\/$/, '')}/api/handle?handle=${encodeURIComponent(handle)}`,
|
|
54
|
-
{ headers: { accept: 'application/json' } }).catch(() => null);
|
|
55
|
-
const d = res ? await res.json().catch(() => ({})) : null;
|
|
56
|
-
if (!d) throw new Error(`${new URL(frontOrigin).host} did not answer whether @${handle} is free`);
|
|
57
|
-
if (!d.available) throw new Error(d.reason || `the name @${handle}@${new URL(frontOrigin).host} is taken — choose another handle`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Run sign-up on a pod session. Steps are reported through onStep(key, state,
|
|
62
|
-
* note) so a page can draw the same tick list the CLI setup shows. Resumable:
|
|
63
|
-
* see PROGRESS above.
|
|
64
|
-
*
|
|
65
|
-
* answers: { handle, shape?: 'pod'|'front', gateway?, container?, createIndex? }
|
|
66
|
-
* container the path of the container that holds `fedipod/` (the pod root
|
|
67
|
-
* when empty)
|
|
68
|
-
* createIndex the person's yes to making a public type index when their
|
|
69
|
-
* profile names none; without it, sign-up stops before writing
|
|
70
|
-
* anything and says so (error code 'needs-index')
|
|
71
|
-
* session: { webId, issuer, fetch } — the Solid-OIDC session from the pod's
|
|
72
|
-
* own login (oidc-session.mjs). The pod is where the WebID lives.
|
|
73
|
-
*
|
|
74
|
-
* `shape` is where the address lives. On the pod, `@handle@yourpod`, with the
|
|
75
|
-
* gateway as a mail door only — the default. At the gateway, `@handle@front`,
|
|
76
|
-
* a fronted identity whose documents still live on the pod. A pod on a path of
|
|
77
|
-
* a suffix-based host cannot answer WebFinger, so it is fronted whatever was asked.
|
|
78
|
-
*
|
|
79
|
-
* returns: { config, actorUrl, address, pod, keys, keysPublic }
|
|
80
|
-
*/
|
|
81
|
-
export async function signUp(answers, { session, onStep = () => {}, frontOrigin = null } = {}) {
|
|
82
|
-
const { handle } = answers;
|
|
83
|
-
const bad = handleProblem(handle);
|
|
84
|
-
if (bad) throw new Error(bad);
|
|
85
|
-
if (!session?.webId || typeof session.fetch !== 'function') throw new Error('sign in at your pod first');
|
|
86
|
-
const wantsFront = answers.shape === 'front';
|
|
87
|
-
if (wantsFront && !frontOrigin) throw new Error('an address at the gateway needs a gateway, and this page has none');
|
|
88
|
-
// A name at the gateway is one per gateway: settle it before making anything.
|
|
89
|
-
if (wantsFront) await assertFrontNameFree(frontOrigin, handle);
|
|
90
|
-
|
|
91
|
-
const webId = session.webId;
|
|
92
|
-
const pod = podBaseOfWebId(webId); // its own host, or a path on a suffix-based one
|
|
93
|
-
const key = progressKey(webId, answers);
|
|
94
|
-
const prog = PROGRESS.get(key) || {};
|
|
95
|
-
PROGRESS.set(key, prog); // resume record for this identity
|
|
96
|
-
|
|
97
|
-
const step = (key) => ({
|
|
98
|
-
running: (note) => onStep(key, 'running', note),
|
|
99
|
-
ok: (note) => onStep(key, 'ok', note),
|
|
100
|
-
skip: (note) => onStep(key, 'skipped', note),
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const place = chosenRoot(pod, answers.container);
|
|
104
|
-
if (place.problem) throw new Error(`Where to store it: ${place.problem}.`);
|
|
105
|
-
const root = place.root;
|
|
106
|
-
const remote = new BrowserRemotePod(session, { webId, role: 'signup', log: () => {} });
|
|
107
|
-
|
|
108
|
-
// --- the pod --- (the session came from it; what is checked is that it has no account yet)
|
|
109
|
-
const podStep = step('pod');
|
|
110
|
-
if (!prog.pod) {
|
|
111
|
-
podStep.running('checking your pod');
|
|
112
|
-
if (await findAccount(remote, pod, readConfigAt(remote, pod))
|
|
113
|
-
|| await resourceExists(session.fetch, actorUrlFor(pod, root))) {
|
|
114
|
-
throw new Error('The pod already hosts a FediPod account. If you want a second account, put it on a different pod.');
|
|
115
|
-
}
|
|
116
|
-
if (await resourceExists(session.fetch, `${pod}${root}`)) {
|
|
117
|
-
throw new Error(`${new URL(pod + root).pathname} is already there and is not a FediPod account — choose another container.`);
|
|
118
|
-
}
|
|
119
|
-
// Where the account lives is recorded in the public type index. With none,
|
|
120
|
-
// the person is asked first; nothing is written before they have said yes.
|
|
121
|
-
if (!answers.createIndex && !await hasPublicIndex(remote, pod)) {
|
|
122
|
-
throw Object.assign(new Error('Your profile names no public type index.'), { code: 'needs-index' });
|
|
123
|
-
}
|
|
124
|
-
prog.pod = pod;
|
|
125
|
-
podStep.ok(pod);
|
|
126
|
-
} else {
|
|
127
|
-
podStep.ok(prog.pod); // resumed
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// WebFinger is answered only at a host root. A pod on a path of a shared
|
|
131
|
-
// host therefore takes its address at the gateway, whatever was asked; a
|
|
132
|
-
// pod at its own root keeps the choice made on the form.
|
|
133
|
-
const pathPod = new URL(pod).pathname !== '/';
|
|
134
|
-
const fronted = pathPod || wantsFront;
|
|
135
|
-
if (fronted && !frontOrigin) {
|
|
136
|
-
throw new Error(`${pod} is a suffixed pod, so its address must live at a gateway, and this page has none.`);
|
|
137
|
-
}
|
|
138
|
-
if (pathPod && !wantsFront) await assertFrontNameFree(frontOrigin, handle);
|
|
139
|
-
|
|
140
|
-
const actorUrl = actorUrlFor(pod, root); // where the documents live, always
|
|
141
|
-
const frontActor = fronted ? `${frontOrigin.replace(/\/$/, '')}/u/${handle}/ap/actor` : null;
|
|
142
|
-
|
|
143
|
-
// Connect the gateway (fedipod.net) before the key is made, because a
|
|
144
|
-
// fronted identity's key is stamped with the gateway actor. Inbox-only: the
|
|
145
|
-
// actor advertises the door as its inbox and keeps its own ids. Fronted: the
|
|
146
|
-
// gateway answers WebFinger for @handle@front and serves the actor at its own
|
|
147
|
-
// address, rewriting reads onto the pod. Either way the door verifies each
|
|
148
|
-
// delivery and forwards the clean mail to the pod; the agent trusts the
|
|
149
|
-
// door's receipt. The pod session itself proves the pod to the gateway.
|
|
150
|
-
let gateway = answers.gateway || prog.gateway || null;
|
|
151
|
-
if (frontOrigin && !gateway) {
|
|
152
|
-
const gw = step('gateway');
|
|
153
|
-
gw.running(fronted ? `taking your address at ${new URL(frontOrigin).host}` : `connecting your mail door on ${new URL(frontOrigin).host}`);
|
|
154
|
-
const res = await session.fetch(`${frontOrigin.replace(/\/$/, '')}/api/attach`, {
|
|
155
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
156
|
-
// podHome is the AP CONTAINER, not the pod root: the front builds the
|
|
157
|
-
// delivery target as `podHome + 'ap/inbox/'` (lib/front-core.mjs), so a
|
|
158
|
-
// bare pod root sends this identity's mail to <pod>/ap/inbox/ — outside
|
|
159
|
-
// the container the agent drains, where nothing would ever read it. The
|
|
160
|
-
// manage surface has always sent `urls.home`; this is the same value.
|
|
161
|
-
body: JSON.stringify({ handle, podHome: `${pod}${root}`, actorUrl, kind: 'person', fronted }),
|
|
162
|
-
});
|
|
163
|
-
const d = await res.json().catch(() => ({}));
|
|
164
|
-
if (res.status !== 201 || !d.hmacSecret) {
|
|
165
|
-
throw new Error(`could not connect the gateway (HTTP ${res.status}): ${d.error || ''}`);
|
|
166
|
-
}
|
|
167
|
-
gateway = fronted
|
|
168
|
-
? { url: `${frontOrigin.replace(/\/$/, '')}/u/${handle}/ap/inbox/`, frontActor: String(d.frontActor || frontActor), hmacSecret: d.hmacSecret, mode: 'trust' }
|
|
169
|
-
: { url: d.doorInbox, hmacSecret: d.hmacSecret, mode: 'trust' };
|
|
170
|
-
prog.gateway = gateway;
|
|
171
|
-
gw.ok();
|
|
172
|
-
} else if (frontOrigin && gateway) {
|
|
173
|
-
step('gateway').ok(); // resumed
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// --- keys, in an owner-only container on the pod ---
|
|
177
|
-
//
|
|
178
|
-
// The durable copy is on the pod, because a browser has no disk you can carry
|
|
179
|
-
// to the next machine. It is stored as it is, behind the container's access
|
|
180
|
-
// rule: the same rule every private document on the pod lives under, so it
|
|
181
|
-
// is reachable through the pod's own login and by nobody else.
|
|
182
|
-
//
|
|
183
|
-
// Two things in that order, deliberately: the ACL BEFORE the key. Writing the
|
|
184
|
-
// key first leaves a window where a pod whose root is world-readable serves it
|
|
185
|
-
// to anyone who asks, and a brought pod is exactly the case where that root
|
|
186
|
-
// may be public.
|
|
187
|
-
//
|
|
188
|
-
// The opened copy is kept in this browser (IndexedDB) so the worker can boot
|
|
189
|
-
// itself after an idle kill with no read. A browser that has no copy reads
|
|
190
|
-
// the pod's with its session — see keys-browser.mjs.
|
|
191
|
-
const keysStep = step('keys');
|
|
192
|
-
let keys;
|
|
193
|
-
if (!prog.keysStored) {
|
|
194
|
-
keysStep.running('making your signing key and storing it on your pod');
|
|
195
|
-
keys = await generateKeys();
|
|
196
|
-
// One key, one actor (lib/keys.mjs): the actor the world knows, which for
|
|
197
|
-
// a fronted identity is the gateway's address for it.
|
|
198
|
-
keys.mintedFor = gateway?.frontActor || actorUrl;
|
|
199
|
-
// Owner-only, and THEN the key — one operation, so the order cannot be got
|
|
200
|
-
// wrong here or anywhere else. A pod that refuses the ACL write is not a
|
|
201
|
-
// pod this key may sit on.
|
|
202
|
-
try {
|
|
203
|
-
await podState.provisionKey(remote, {
|
|
204
|
-
stateUrl: stateOf(pod, root),
|
|
205
|
-
keysUrl: keysDocFor(pod, root),
|
|
206
|
-
keys,
|
|
207
|
-
});
|
|
208
|
-
} catch (e) {
|
|
209
|
-
throw new Error(`could not store the signing key on the pod (${e.message}). `
|
|
210
|
-
+ `You are signed in as ${webId} — that WebID must own ${pod} and ${root} in it must be writable by it.`);
|
|
211
|
-
}
|
|
212
|
-
// This browser's own opened copy, so the boot that follows needs no read.
|
|
213
|
-
// Best effort: a browser that refuses IndexedDB (private mode) reads the
|
|
214
|
-
// pod's copy on the way back in.
|
|
215
|
-
await cacheOpenedKeys(actorUrl, keys);
|
|
216
|
-
prog.keys = keys; prog.keysStored = true;
|
|
217
|
-
keysStep.ok();
|
|
218
|
-
} else {
|
|
219
|
-
keys = prog.keys;
|
|
220
|
-
keysStep.ok();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const config = {
|
|
224
|
-
remotePod: pod, root, handle, name: handle, issuer: String(session.issuer || '').replace(/\/+$/, ''),
|
|
225
|
-
createdAt: new Date().toISOString(),
|
|
226
|
-
...(gateway ? { gateway } : {}),
|
|
227
|
-
};
|
|
228
|
-
// Write the config to the pod (owner-only, beside the key) so a returning
|
|
229
|
-
// sign-in — which arrives with only an OIDC session — can read the account's
|
|
230
|
-
// config and key from the pod and boot.
|
|
231
|
-
try {
|
|
232
|
-
await podState.writeConfig(remote, { state: stateOf(pod, root) }, config);
|
|
233
|
-
} catch (e) {
|
|
234
|
-
throw new Error(`could not store the config on the pod (${e.message})`);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// --- where it lives, in the public type index ---
|
|
238
|
-
if (!prog.placed) {
|
|
239
|
-
const placeStep = step('place');
|
|
240
|
-
placeStep.running('recording where your account lives');
|
|
241
|
-
try {
|
|
242
|
-
await recordPlace(remote, pod, actorUrl, { create: !!answers.createIndex });
|
|
243
|
-
} catch (e) {
|
|
244
|
-
throw new Error(`could not record where your account lives in your type index (${e.message})`);
|
|
245
|
-
}
|
|
246
|
-
prog.placed = true;
|
|
247
|
-
placeStep.ok();
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
PROGRESS.delete(key); // finished — nothing left to resume
|
|
251
|
-
|
|
252
|
-
const host = gateway?.frontActor ? new URL(gateway.frontActor).host : new URL(pod).host;
|
|
253
|
-
return {
|
|
254
|
-
config, actorUrl, pod,
|
|
255
|
-
address: `@${handle}@${host}`,
|
|
256
|
-
// The opened keys, for booting the agent in THIS browser session right away.
|
|
257
|
-
keys,
|
|
258
|
-
keysPublic: { rsa: keys.rsa.publicPem, ed25519: keys.ed25519?.publicPem || null },
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// ---- an account already on the pod ----
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* What the pod already holds: null when there is no FediPod account on it,
|
|
266
|
-
* else its config, the pod, and where its address lives — `frontHost` is
|
|
267
|
-
* the gateway's host for an address at a gateway, null for one on the pod.
|
|
268
|
-
*/
|
|
269
|
-
export async function readAccount(session) {
|
|
270
|
-
const pod = podBaseOfWebId(session.webId);
|
|
271
|
-
const remote = new BrowserRemotePod(session, { webId: session.webId, role: 'signup', log: () => {} });
|
|
272
|
-
// A person account: the browser build is persons only.
|
|
273
|
-
const found = await findAccount(remote, pod, readConfigAt(remote, pod), (c) => (c.kind || 'person') === 'person');
|
|
274
|
-
if (!found) return null;
|
|
275
|
-
const { root, config } = found;
|
|
276
|
-
const frontActor = config.gateway?.frontActor || null;
|
|
277
|
-
let frontHost = null;
|
|
278
|
-
try { frontHost = frontActor ? new URL(frontActor).host : null; } catch { frontHost = null; }
|
|
279
|
-
let podHost = '';
|
|
280
|
-
try { podHost = new URL(config.remotePod || pod).host; } catch { podHost = ''; }
|
|
281
|
-
return { pod, root, config, frontActor, frontHost, address: `@${config.handle}@${frontHost || podHost}` };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Move an account whose address lives at another gateway to this one. The
|
|
286
|
-
* pod, its posts, its followers and its key all stay where they are; what
|
|
287
|
-
* changes is the gateway that answers for it. This half does the pod's
|
|
288
|
-
* part: attach here, rewrite the config to name this gateway, restamp the
|
|
289
|
-
* key. The agent's first boot from that config publishes under the new
|
|
290
|
-
* ids, and then tells the old gateway and the followers (gateway-move.mjs).
|
|
291
|
-
*
|
|
292
|
-
* answers: { handle } — the name here; the old one is the default.
|
|
293
|
-
* returns: { config, address, movedFrom }
|
|
294
|
-
*/
|
|
295
|
-
export async function moveIn(answers, { session, onStep = () => {}, frontOrigin = null } = {}) {
|
|
296
|
-
const { handle } = answers;
|
|
297
|
-
const bad = handleProblem(handle);
|
|
298
|
-
if (bad) throw new Error(bad);
|
|
299
|
-
if (!session?.webId || typeof session.fetch !== 'function') throw new Error('sign in at your pod first');
|
|
300
|
-
if (!frontOrigin) throw new Error('a move needs a gateway to move to, and this page has none');
|
|
301
|
-
const origin = frontOrigin.replace(/\/$/, '');
|
|
302
|
-
const step = (key) => ({ running: (note) => onStep(key, 'running', note), ok: (note) => onStep(key, 'ok', note) });
|
|
303
|
-
|
|
304
|
-
// --- the pod: the account that is there, and its key ---
|
|
305
|
-
const podStep = step('pod');
|
|
306
|
-
podStep.running('reading your account on your pod');
|
|
307
|
-
const here = await readAccount(session);
|
|
308
|
-
if (!here?.frontActor) throw new Error('this pod holds no account at another gateway to move here');
|
|
309
|
-
if (here.frontHost === new URL(origin).host) throw new Error('this account already lives at this gateway');
|
|
310
|
-
const { pod, root, config: old } = here;
|
|
311
|
-
const remote = new BrowserRemotePod(session, { webId: session.webId, role: 'signup', log: () => {} });
|
|
312
|
-
const urls = { state: stateOf(pod, root) };
|
|
313
|
-
const keys = await podState.readKeys(remote, urls);
|
|
314
|
-
if (!keys?.rsa) throw new Error('could not read this account\'s signing key on the pod');
|
|
315
|
-
if (isKeyEnvelope(keys)) throw new Error(`this account's key is still under a password from an earlier version — sign in once at ${here.frontHost} first, then come back`);
|
|
316
|
-
podStep.ok(here.address);
|
|
317
|
-
|
|
318
|
-
// --- this gateway: the name, then the attach, fronted ---
|
|
319
|
-
const gw = step('gateway');
|
|
320
|
-
gw.running(`taking your address at ${new URL(origin).host}`);
|
|
321
|
-
await assertFrontNameFree(origin, handle);
|
|
322
|
-
const res = await session.fetch(`${origin}/api/attach`, {
|
|
323
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
324
|
-
body: JSON.stringify({ handle, podHome: `${pod}${root}`, actorUrl: actorUrlFor(pod, root), kind: old.kind || 'person', fronted: true }),
|
|
325
|
-
});
|
|
326
|
-
const d = await res.json().catch(() => ({}));
|
|
327
|
-
if (res.status !== 201 || !d.hmacSecret) throw new Error(`could not attach to this gateway (HTTP ${res.status}): ${d.error || ''}`);
|
|
328
|
-
const gateway = {
|
|
329
|
-
url: `${origin}/u/${handle}/ap/inbox/`, frontActor: String(d.frontActor || `${origin}/u/${handle}/ap/actor`),
|
|
330
|
-
hmacSecret: d.hmacSecret, mode: 'trust',
|
|
331
|
-
};
|
|
332
|
-
gw.ok();
|
|
333
|
-
|
|
334
|
-
// --- the pod again: the config names this gateway, the key is restamped ---
|
|
335
|
-
//
|
|
336
|
-
// The old actor becomes an alias, which is what a server checks on the new
|
|
337
|
-
// actor before it honours a Move. `movedFrom` is the pending half: the
|
|
338
|
-
// agent completes it after its first boot under the new ids.
|
|
339
|
-
const keysStep = step('keys');
|
|
340
|
-
keysStep.running('moving your key and account record to the new address');
|
|
341
|
-
const aliases = [...new Set([...(old.aliases || []), here.frontActor])];
|
|
342
|
-
const config = {
|
|
343
|
-
...old, handle, gateway, aliases,
|
|
344
|
-
movedFrom: { actor: here.frontActor, gateway: new URL(here.frontActor).origin, handle: old.handle, at: new Date().toISOString() },
|
|
345
|
-
};
|
|
346
|
-
keys.mintedFor = gateway.frontActor; // one key, one actor (lib/keys.mjs)
|
|
347
|
-
try {
|
|
348
|
-
await podState.writeKeys(remote, urls, keys);
|
|
349
|
-
await podState.writeConfig(remote, urls, config);
|
|
350
|
-
} catch (e) {
|
|
351
|
-
throw new Error(`could not update your account on the pod (${e.message})`);
|
|
352
|
-
}
|
|
353
|
-
await cacheOpenedKeys(actorUrlFor(pod, root), keys);
|
|
354
|
-
keysStep.ok();
|
|
355
|
-
|
|
356
|
-
return { config, address: `@${handle}@${new URL(origin).host}`, movedFrom: config.movedFrom };
|
|
357
|
-
}
|
package/web/app/sw-src.mjs
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
// sw-src.mjs — the service worker that hosts the in-browser agent.
|
|
2
|
-
//
|
|
3
|
-
// Phanpy (served static on this origin) calls the Mastodon API on its own
|
|
4
|
-
// origin. This worker answers those calls from the agent running inside it, so
|
|
5
|
-
// there is no server behind the client. The page boots the agent by posting it
|
|
6
|
-
// the sign-up material (credential, config, plaintext keys); the worker holds
|
|
7
|
-
// the agent for the life of the worker and intercepts the facade paths. Static
|
|
8
|
-
// files fall through to the network.
|
|
9
|
-
import { BrowserAgent } from './agent.mjs';
|
|
10
|
-
import { getSession } from './oidc-session.mjs';
|
|
11
|
-
import { ADMIN_PATHS } from './admin-facade.mjs';
|
|
12
|
-
|
|
13
|
-
let agent = null;
|
|
14
|
-
let booting = null;
|
|
15
|
-
|
|
16
|
-
const FACADE = /^\/(api\/v[12]\/|oauth\/|nodeinfo\/)/; // Mastodon paths only — never the front's own /api/handle, /api/attach, /api/relay
|
|
17
|
-
const FACADE_EXACT = new Set(['/.well-known/nodeinfo', '/.well-known/oauth-authorization-server', '/nodeinfo/2.0']);
|
|
18
|
-
const isFacade = (p) => FACADE.test(p) || FACADE_EXACT.has(p);
|
|
19
|
-
// The owner's record/manage data endpoints (see admin-facade.mjs). The web/admin
|
|
20
|
-
// PAGES are static files Netlify serves; only these data calls are ours.
|
|
21
|
-
const isAdmin = (p) => ADMIN_PATHS.has(p);
|
|
22
|
-
|
|
23
|
-
// ---- who may drive these routes ----
|
|
24
|
-
//
|
|
25
|
-
// Same-origin is the WHOLE trust boundary for this build. The worker answers
|
|
26
|
-
// the owner's admin routes with no password and no token — the only interlock
|
|
27
|
-
// on the destructive ones is `confirm === handle`, and the handle is public —
|
|
28
|
-
// so without this gate any page the owner merely visited could move every
|
|
29
|
-
// follower to an attacker's account, retire the identity, rotate the key,
|
|
30
|
-
// point the inbox at an attacker's gateway, or set a UI password the owner
|
|
31
|
-
// does not know. The Node agent is covered by lib/guard.mjs; nothing from
|
|
32
|
-
// that file is in this bundle, and it could not do the job here anyway —
|
|
33
|
-
// `Sec-Fetch-*` headers are NOT visible inside a service worker's fetch
|
|
34
|
-
// event, and neither is `Origin`. So the gate is built from what a worker
|
|
35
|
-
// CAN see: the request's mode, its referrer, and its headers.
|
|
36
|
-
//
|
|
37
|
-
// The header is what does the work, and it does not need to be a secret to do
|
|
38
|
-
// it. A cross-site FORM cannot set a header at all. A cross-origin fetch()
|
|
39
|
-
// that sets one is a preflighted request, and nothing here ever answers a
|
|
40
|
-
// preflight with CORS headers, so the browser never sends the real one. That
|
|
41
|
-
// leaves a top-level navigation, which also carries no custom headers.
|
|
42
|
-
//
|
|
43
|
-
// (The review suggested making it a per-session secret handed to the page by
|
|
44
|
-
// postMessage. Deliberately not carried: the header already refuses every
|
|
45
|
-
// cross-ORIGIN caller, and a secret would only add something against an
|
|
46
|
-
// attacker already running script on this origin — who could read it straight
|
|
47
|
-
// out of the page. The defence at that layer is the CSP, in the site's
|
|
48
|
-
// _headers.)
|
|
49
|
-
const PAGE_HEADER = 'x-fedipod-page';
|
|
50
|
-
|
|
51
|
-
// The one route that MUST survive a cross-site navigation: the other server's
|
|
52
|
-
// OAuth redirect lands the browser here, from their origin, by design. Its
|
|
53
|
-
// `state` parameter is what stands in for the header.
|
|
54
|
-
const NAV_ALLOWED = new Set(['/fediacct/callback']);
|
|
55
|
-
|
|
56
|
-
const sameOriginReferrer = (request) => {
|
|
57
|
-
const r = request.referrer;
|
|
58
|
-
// 'about:client' is the browser saying "the context that asked", which for a
|
|
59
|
-
// request this worker intercepts is a page on this origin.
|
|
60
|
-
if (!r || r === 'about:client') return r === 'about:client';
|
|
61
|
-
try { return new URL(r).origin === self.location.origin; } catch { return false; }
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// Returns null when the request may proceed, else the reason to refuse with.
|
|
65
|
-
function notAllowed(request, url) {
|
|
66
|
-
const p = url.pathname;
|
|
67
|
-
|
|
68
|
-
// The Mastodon client API is deliberately open, exactly as it is on the Node
|
|
69
|
-
// agent and on any real instance: a bearer is its credential, and a client
|
|
70
|
-
// has to be able to call it. It mints nothing without one.
|
|
71
|
-
if (!isAdmin(p) && !p.startsWith('/oauth/')) return null;
|
|
72
|
-
|
|
73
|
-
if (request.mode === 'navigate') {
|
|
74
|
-
if (NAV_ALLOWED.has(p)) return null;
|
|
75
|
-
// The bundled client signs in by navigating its frame to /oauth/authorize
|
|
76
|
-
// from a page on this origin. A navigation from anywhere else — or from
|
|
77
|
-
// nowhere, which is what a page that suppressed its referrer looks like —
|
|
78
|
-
// is not that, and /oauth/authorize is where a 90-day bearer is minted.
|
|
79
|
-
if (p.startsWith('/oauth/') && sameOriginReferrer(request)) return null;
|
|
80
|
-
return 'a navigation may not drive this route';
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// /oauth/* is not open the way /api/* is: the only client here is the one
|
|
84
|
-
// served from this origin, so a call from another origin is never legitimate.
|
|
85
|
-
if (p.startsWith('/oauth/')) {
|
|
86
|
-
return sameOriginReferrer(request) ? null : 'this route answers this origin only';
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (request.headers.get(PAGE_HEADER) !== '1') return `missing ${PAGE_HEADER}`;
|
|
90
|
-
// Belt and braces on top of the header: the facade parses a body as JSON
|
|
91
|
-
// whatever its content type, and `text/plain` is the one a cross-site form
|
|
92
|
-
// can send. Nothing that got past the header sends anything else anyway.
|
|
93
|
-
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
94
|
-
const ct = (request.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
|
|
95
|
-
if (ct && ct !== 'application/json') return `unexpected content type "${ct}"`;
|
|
96
|
-
}
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
self.addEventListener('install', () => self.skipWaiting());
|
|
101
|
-
self.addEventListener('activate', (e) => e.waitUntil(self.clients.claim()));
|
|
102
|
-
|
|
103
|
-
// Boot the agent from the stored Solid-OIDC session. The sign-in page posts a
|
|
104
|
-
// 'boot' to prime it, but the worker ALSO boots itself on demand (see serve):
|
|
105
|
-
// browsers kill an idle service worker and restart it fresh, and the client at
|
|
106
|
-
// /app/ never posts 'boot' — so without on-demand boot every request after a
|
|
107
|
-
// restart 503s. On a boot failure `booting` is cleared so the next request retries.
|
|
108
|
-
async function bootFromSession(frontOrigin) {
|
|
109
|
-
const oidc = await getSession();
|
|
110
|
-
if (!oidc) throw new Error('no session — sign in first');
|
|
111
|
-
const a = new BrowserAgent({ log: (m) => console.log('[sw-agent]', m) });
|
|
112
|
-
await a.boot({ oidc, frontOrigin: frontOrigin || self.location.origin });
|
|
113
|
-
agent = a;
|
|
114
|
-
return a;
|
|
115
|
-
}
|
|
116
|
-
function ensureBooting(frontOrigin) {
|
|
117
|
-
if (agent) return Promise.resolve(agent);
|
|
118
|
-
if (!booting) booting = bootFromSession(frontOrigin).catch((err) => { booting = null; throw err; });
|
|
119
|
-
return booting;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
self.addEventListener('message', (e) => {
|
|
123
|
-
// Drop the identity this worker is holding. A worker outlives a page, so
|
|
124
|
-
// signing in as somebody else left the FIRST account's agent live and serving
|
|
125
|
-
// — the new session sat in IndexedDB while every request was still answered
|
|
126
|
-
// by the old actor, with the old key. The page posts this before it boots the
|
|
127
|
-
// new one; the next request boots from whatever session is stored now.
|
|
128
|
-
if (e.data?.type === 'reset') {
|
|
129
|
-
const had = !!agent;
|
|
130
|
-
agent = null; booting = null;
|
|
131
|
-
e.source?.postMessage({ type: 'reset-done', had });
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (e.data?.type !== 'boot') return;
|
|
135
|
-
ensureBooting(e.data.frontOrigin)
|
|
136
|
-
.then(() => e.source?.postMessage({ type: 'booted' }))
|
|
137
|
-
// `code` matters: a boot that failed only because this browser has never
|
|
138
|
-
// opened the signing key is not a failure the page should show as one — it
|
|
139
|
-
// is a question the page can answer (boot.mjs). Everything else is shown.
|
|
140
|
-
.catch((err) => e.source?.postMessage({ type: 'boot-error', error: err.message,
|
|
141
|
-
code: err.code || null, stack: String(err.stack || '') }));
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
self.addEventListener('fetch', (e) => {
|
|
145
|
-
const url = new URL(e.request.url);
|
|
146
|
-
if (url.origin !== self.location.origin || !(isFacade(url.pathname) || isAdmin(url.pathname))) return; // static → network
|
|
147
|
-
e.respondWith(serve(e.request, url));
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
async function serve(request, url) {
|
|
151
|
-
// Before anything is booted or read: is this caller allowed to be here at all?
|
|
152
|
-
const refuse = notAllowed(request, url);
|
|
153
|
-
if (refuse) {
|
|
154
|
-
console.warn(`[sw-agent] refused ${request.method} ${url.pathname}: ${refuse}`);
|
|
155
|
-
return json(403, { error: `refused: ${refuse}` });
|
|
156
|
-
}
|
|
157
|
-
// Boot on demand from the stored session, so the client works even when the
|
|
158
|
-
// worker was restarted (idle-killed) and nobody posted 'boot' this time.
|
|
159
|
-
if (!agent) { try { await ensureBooting(); } catch { /* no session → 503 below */ } }
|
|
160
|
-
if (!agent) return json(503, { error: 'not signed in on this browser — sign in at the front page' });
|
|
161
|
-
// Bytes, not text. A multipart upload is binary — read as text it comes back
|
|
162
|
-
// through a UTF-8 round trip that replaces every byte that is not valid UTF-8,
|
|
163
|
-
// which is most of a JPEG, so the boundary search found nothing and every
|
|
164
|
-
// media and avatar upload answered "422 file required". readBody() does
|
|
165
|
-
// `data += chunk`, which decodes a Buffer the same way it always did, so the
|
|
166
|
-
// JSON and form paths are unchanged.
|
|
167
|
-
const bodyBytes = (request.method === 'GET' || request.method === 'HEAD')
|
|
168
|
-
? null : Buffer.from(new Uint8Array(await request.arrayBuffer()));
|
|
169
|
-
// The admin facade is JSON-only (the gate above enforces the content type),
|
|
170
|
-
// and takes its body already decoded.
|
|
171
|
-
const bodyText = bodyBytes ? new TextDecoder().decode(bodyBytes) : '';
|
|
172
|
-
const reqHeaders = {}; for (const [k, v] of request.headers) reqHeaders[k.toLowerCase()] = v;
|
|
173
|
-
// `host` is a forbidden header name, so a fetch Request never carries one and
|
|
174
|
-
// the loop above cannot produce it — but it is a header every real request
|
|
175
|
-
// arrives with, and the agent reads it to say where it lives. Without it the
|
|
176
|
-
// notifications `Link` header named `https://undefined/`, so a client that
|
|
177
|
-
// paged by following it (which is how a client is meant to page) walked off
|
|
178
|
-
// the origin and saw nothing past the first screen.
|
|
179
|
-
reqHeaders.host = url.host;
|
|
180
|
-
const listeners = {};
|
|
181
|
-
// The body's events fire on the next microtask. A route that reads the body
|
|
182
|
-
// may only register for them after an await or two — the facade dispatches
|
|
183
|
-
// through its area modules first — so a listener that arrives after the
|
|
184
|
-
// events have fired is given them at once, in the order it asks.
|
|
185
|
-
let fired = false;
|
|
186
|
-
const req = { method: request.method, url: url.pathname + url.search, headers: reqHeaders,
|
|
187
|
-
// notAllowed() above let this through, so it came from this origin. Said
|
|
188
|
-
// out loud rather than left implicit: MastoApi asks (through the
|
|
189
|
-
// authorities object in agent.mjs) whether a request is the owner's own,
|
|
190
|
-
// and in a browser that question means exactly this.
|
|
191
|
-
sameOrigin: true,
|
|
192
|
-
socket: { encrypted: url.protocol === 'https:' },
|
|
193
|
-
on(ev, cb) {
|
|
194
|
-
(listeners[ev] ||= []).push(cb);
|
|
195
|
-
if (fired) { if (ev === 'data' && bodyBytes?.length) cb(bodyBytes); else if (ev === 'end') cb(); }
|
|
196
|
-
return req;
|
|
197
|
-
},
|
|
198
|
-
destroy() {} };
|
|
199
|
-
queueMicrotask(() => {
|
|
200
|
-
fired = true;
|
|
201
|
-
if (bodyBytes?.length) (listeners.data || []).forEach((cb) => cb(bodyBytes));
|
|
202
|
-
(listeners.end || []).forEach((cb) => cb());
|
|
203
|
-
});
|
|
204
|
-
let status = 200; const outHeaders = {}; const chunks = [];
|
|
205
|
-
const res = {
|
|
206
|
-
writeHead(s, h) { status = s; if (h) Object.assign(outHeaders, h); return res; },
|
|
207
|
-
setHeader(k, v) { outHeaders[k] = v; }, getHeader(k) { return outHeaders[k]; },
|
|
208
|
-
write(c) { chunks.push(c); }, end(c) { if (c) chunks.push(c); },
|
|
209
|
-
};
|
|
210
|
-
try {
|
|
211
|
-
const handled = isAdmin(url.pathname)
|
|
212
|
-
? await agent.admin.handle(req, res, url.pathname, url, bodyText)
|
|
213
|
-
: await agent.masto.handle(req, res, url.pathname, url);
|
|
214
|
-
if (!handled) return fetch(request);
|
|
215
|
-
return new Response(chunks.join(''), { status, headers: { 'content-type': 'application/json', ...outHeaders } });
|
|
216
|
-
} catch (err) {
|
|
217
|
-
return json(500, { error: err.message });
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
const json = (status, obj) => new Response(JSON.stringify(obj), { status, headers: { 'content-type': 'application/json' } });
|