fedipod-server 0.28.1 → 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 +38 -34
- package/config/server.json +22 -0
- package/dist/claims.js +3 -2
- package/dist/directory.d.ts +2 -0
- 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 +39 -101
- package/dist/handler.js +86 -617
- package/dist/handler.jsonld +6 -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/dist/store-pod.js +37 -0
- package/package.json +10 -18
- package/lib/client/c2s.mjs +0 -582
- package/lib/client/localapi.mjs +0 -87
- package/lib/client/masto/accounts.mjs +0 -309
- 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 -371
- package/lib/core/graphview.mjs +0 -327
- package/lib/core/intake/activities.mjs +0 -633
- package/lib/core/intake/activity.mjs +0 -318
- package/lib/core/intake/channel.mjs +0 -144
- package/lib/core/intake/group.mjs +0 -256
- package/lib/core/intake/index.mjs +0 -701
- package/lib/core/intake/notes.mjs +0 -360
- 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/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 -237
- package/lib/core/publisher/index.mjs +0 -500
- package/lib/core/publisher/notes.mjs +0 -453
- package/lib/core/publisher/questions.mjs +0 -239
- 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 -531
- package/lib/core/storage.mjs +0 -176
- package/lib/core/store.mjs +0 -568
- package/lib/core/wire-quotes.mjs +0 -71
- package/lib/core/wire.mjs +0 -889
- 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 -412
- 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 -447
- 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 -459
- 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 -970
- package/lib/gateway/gateway-core.mjs +0 -165
- 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/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/media.mjs +0 -27
- package/lib/pod/notes.mjs +0 -80
- 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 -525
- package/lib/pod/urls.mjs +0 -85
- package/lib/server/embed.mjs +0 -423
- package/lib/session/README.md +0 -137
- package/lib/session/demo.html +0 -94
- package/lib/session/fedi-account.mjs +0 -408
- 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 -897
- 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 -145
- 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 -603
- package/web/admin/notices-bar.js +0 -90
- package/web/admin/oauth-signin.mjs +0 -37
- package/web/admin/record.js +0 -390
- package/web/admin/setup/index.html +0 -241
- package/web/admin/setup/setup.js +0 -326
- 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 -663
- 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 -576
- package/web/app/deliver-relay.mjs +0 -129
- package/web/app/dist/boot.js +0 -34218
- package/web/app/dist/boot.js.map +0 -7
- package/web/app/dist/sw.js +0 -72810
- 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 -242
- 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 -318
- 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 -58
- package/web/front/run.js +0 -195
- 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,165 +0,0 @@
|
|
|
1
|
-
// gateway-core.mjs — the runtime-agnostic logic of an inbox gateway: verify a
|
|
2
|
-
// delivery at the door (where the HTTP-Signature headers still exist), drop
|
|
3
|
-
// spam and forgeries at the edge, and forward what remains — with a signed
|
|
4
|
-
// verification receipt — into the pod inbox for the ordinary drain.
|
|
5
|
-
//
|
|
6
|
-
// This holds NO signing key. It verifies with remotes' PUBLIC keys, writes to
|
|
7
|
-
// the pod inbox with an Append-only credential the adapter supplies, and stamps
|
|
8
|
-
// the receipt with a shared HMAC secret. A compromise here can inject inbox
|
|
9
|
-
// items (which still meet verify-by-dereference in the drain) but cannot
|
|
10
|
-
// impersonate the actor, read the private tree, or post as it.
|
|
11
|
-
//
|
|
12
|
-
// `netlify/functions/inbox.mjs` is a thin adapter over this; an agent-side
|
|
13
|
-
// endpoint could be another. The core is what the smoke suite exercises.
|
|
14
|
-
|
|
15
|
-
import crypto from 'node:crypto';
|
|
16
|
-
import { verifyHttpSignature, makeSafeLoader, makeReceipt, signReceipt } from './httpsig.mjs';
|
|
17
|
-
import * as inbox from '../pod/inbox.mjs';
|
|
18
|
-
import { senderKeys } from './caches.mjs';
|
|
19
|
-
|
|
20
|
-
const DEFAULT_MAX_BYTES = 512 * 1024; // mirror intake.mjs MAX_ITEM_BYTES
|
|
21
|
-
|
|
22
|
-
// Control activities are the message itself and must always pass — you want
|
|
23
|
-
// Follows from strangers. Only CONTENT is subject to the concerns-us drop,
|
|
24
|
-
// and to a pause: an account nobody is reading still takes its follows.
|
|
25
|
-
export const CONTROL = new Set(['Follow', 'Undo', 'Accept', 'Reject', 'Delete', 'Move',
|
|
26
|
-
'Add', 'Remove', 'Block']);
|
|
27
|
-
export const isControl = (type) => CONTROL.has(type);
|
|
28
|
-
|
|
29
|
-
const idOf = (v) => (typeof v === 'string' ? v : v?.id) || null;
|
|
30
|
-
const sha256hex = (s) => crypto.createHash('sha256').update(s).digest('hex');
|
|
31
|
-
|
|
32
|
-
function httpUrl(u) {
|
|
33
|
-
try { const p = new URL(u).protocol; return p === 'http:' || p === 'https:'; } catch { return false; }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Mirror of intake.isBlocked over the policy doc's public blocklist mirror.
|
|
37
|
-
function isBlocked(actor, blocklist = {}) {
|
|
38
|
-
if (!actor) return true;
|
|
39
|
-
if ((blocklist.actors || []).includes(actor)) return true;
|
|
40
|
-
// hostname, not host: the agent's own isBlocked strips the port, and a door
|
|
41
|
-
// that keeps it lets a blocked domain back in on a non-default port.
|
|
42
|
-
let host; try { host = new URL(actor).hostname; } catch { return false; }
|
|
43
|
-
return (blocklist.domains || []).some(d => host === d || host.endsWith('.' + d));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// The edge form of intake.concernsUs — pure addressing, no I/O. Conservative:
|
|
47
|
-
// only DROPS content it is confident does not concern the identity. A GROUP
|
|
48
|
-
// owns conversations under anything it has carried, which the keyless edge
|
|
49
|
-
// cannot see, so a group defers the whole check to the drain (returns true).
|
|
50
|
-
function concernsUsAtEdge(activity, ident) {
|
|
51
|
-
if (ident.kind === 'group') return true;
|
|
52
|
-
const actor = idOf(activity.actor);
|
|
53
|
-
if ((ident.following || []).includes(actor)) return true;
|
|
54
|
-
const obj = typeof activity.object === 'object' ? activity.object : null;
|
|
55
|
-
const audience = []
|
|
56
|
-
.concat(activity.to || [], activity.cc || [], activity.bto || [], activity.bcc || [], activity.audience || [],
|
|
57
|
-
obj?.to || [], obj?.cc || [], obj?.audience || [])
|
|
58
|
-
.map(idOf).filter(Boolean);
|
|
59
|
-
if (audience.includes(ident.actorUrl) || audience.includes(ident.followersUrl)) return true;
|
|
60
|
-
const tag = [].concat(obj?.tag || activity.tag || []);
|
|
61
|
-
if (tag.some(t => t?.type === 'Mention' && (t.href === ident.actorUrl || t.name?.includes(ident.actorUrl)))) return true;
|
|
62
|
-
const inReplyTo = idOf(obj?.inReplyTo ?? activity.inReplyTo);
|
|
63
|
-
return !!inReplyTo && ident.notesPrefix && String(inReplyTo).startsWith(ident.notesPrefix);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Handle one inbound delivery. `request` is a WHATWG Request. `ident` is the
|
|
67
|
-
// resolved identity policy: { inboxUrl, actorUrl, followersUrl, notesPrefix,
|
|
68
|
-
// following, blocklist, kind, gatewayWebId, hmacSecret }. `podPut(url, body,
|
|
69
|
-
// contentType) → boolean` appends to the pod with the gateway's credential.
|
|
70
|
-
// `paused` says nobody is reading this inbox: content is accepted and
|
|
71
|
-
// discarded (the same quiet 202 a blocked sender gets, so nothing retries
|
|
72
|
-
// and nothing counts a failure against this host), control still lands.
|
|
73
|
-
// Returns { status, reason } — the adapter turns it into an HTTP response —
|
|
74
|
-
// and, for a delivery that reached the pod, `content` (whether it was
|
|
75
|
-
// content rather than control) and `bytes`, so the caller can keep count.
|
|
76
|
-
export async function handleDelivery(request, ident, { podPut, fetchImpl = fetch, maxBytes = DEFAULT_MAX_BYTES, paused = false } = {}) {
|
|
77
|
-
// Read the body once from a clone; the original, unconsumed, goes to the
|
|
78
|
-
// verifier (which needs the body for the Digest check).
|
|
79
|
-
let raw;
|
|
80
|
-
try { raw = await request.clone().text(); } catch { return { status: 400, reason: 'unreadable body' }; }
|
|
81
|
-
if (Buffer.byteLength(raw) > maxBytes) return { status: 413, reason: 'too large' };
|
|
82
|
-
|
|
83
|
-
let activity;
|
|
84
|
-
try { activity = JSON.parse(raw); } catch { return { status: 400, reason: 'unparsable JSON' }; }
|
|
85
|
-
const actor = idOf(activity.actor);
|
|
86
|
-
if (!httpUrl(actor)) return { status: 400, reason: 'actor is not an http(s) URL' };
|
|
87
|
-
|
|
88
|
-
// Edge drops — silent 202 so a rejected sender does not retry a delivery we
|
|
89
|
-
// will never accept. None of these becomes a pod write.
|
|
90
|
-
if (isBlocked(actor, ident.blocklist)) return { status: 202, reason: 'blocked' };
|
|
91
|
-
const content = !CONTROL.has(activity.type);
|
|
92
|
-
if (content && paused) return { status: 202, reason: 'paused' };
|
|
93
|
-
if (content && !concernsUsAtEdge(activity, ident)) {
|
|
94
|
-
return { status: 202, reason: 'does not concern us' };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// The sender's key, kept between deliveries: a server pushing a hundred
|
|
98
|
-
// items is asked for its key once (caches.mjs).
|
|
99
|
-
const v = await verifyHttpSignature(request, {
|
|
100
|
-
documentLoader: makeSafeLoader({ fetchImpl }), keyCache: senderKeys,
|
|
101
|
-
});
|
|
102
|
-
// A present-but-invalid signature is a forgery — dropped here, so it never
|
|
103
|
-
// reaches the pod (today it would, drain, and die unapplied). An absent or
|
|
104
|
-
// unfetchable-key signature is NOT dropped: it forwards unverified and the
|
|
105
|
-
// drain's verify-by-dereference still stands behind it.
|
|
106
|
-
if (v.verified === false && v.reason === 'bad-signature') {
|
|
107
|
-
return { status: 202, reason: 'forged signature' };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// No per-user secret on record = no receipt: the delivery still filters and
|
|
111
|
-
// forwards, it just reads as unverified at the drain — never a failure.
|
|
112
|
-
const receipt = ident.hmacSecret
|
|
113
|
-
? signReceipt(makeReceipt(v, { gateway: ident.gatewayWebId }), ident.hmacSecret)
|
|
114
|
-
: null;
|
|
115
|
-
const hash = sha256hex(raw);
|
|
116
|
-
const okA = await inbox.appendVerifiedDelivery(podPut, ident.inboxUrl, hash, raw);
|
|
117
|
-
// A pod-write failure returns 5xx so the ORIGIN retries over its own ladder —
|
|
118
|
-
// that is what preserves the pod's buffer property without the gateway
|
|
119
|
-
// holding any state.
|
|
120
|
-
if (!okA) return { status: 502, reason: 'pod inbox write failed' };
|
|
121
|
-
if (receipt) await inbox.writeReceiptBeside(podPut, ident.inboxUrl, hash, receipt);
|
|
122
|
-
return { status: 202, reason: v.verified ? 'verified' : 'buffered-unverified', content, bytes: Buffer.byteLength(raw) };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// The outbox door: the owner's own post, taken on their behalf.
|
|
126
|
-
//
|
|
127
|
-
// A client such as dokieli POSTs an activity — or a bare object — to the
|
|
128
|
-
// outbox address the actor advertises. The door holds no key and mints nothing:
|
|
129
|
-
// it checks that the token proves the account's owner (the caller has already
|
|
130
|
-
// done that and hands in `owner`), writes the bytes into the pod inbox exactly
|
|
131
|
-
// as verified mail is written, and stamps them with a receipt whose method is
|
|
132
|
-
// `c2s` and whose actor is this account. The drain hands such an item to the
|
|
133
|
-
// client-to-server dispatcher, which publishes and delivers it — so the post
|
|
134
|
-
// goes out when the agent next runs, the same way inbound mail is read.
|
|
135
|
-
//
|
|
136
|
-
// `slug` is the name the client asked for its new document. It rides in the
|
|
137
|
-
// receipt so the dispatcher can use it, and it is what lets the door answer a
|
|
138
|
-
// Location before anything exists: the object will live at notesPrefix+slug
|
|
139
|
-
// unless that name is taken, in which case the agent mints another.
|
|
140
|
-
export const SLUG_OK = /^[A-Za-z0-9._-]{1,64}$/u;
|
|
141
|
-
export const safeSlug = (s) => (typeof s === 'string' && SLUG_OK.test(s) && !/^\.+$/u.test(s) ? s : null);
|
|
142
|
-
|
|
143
|
-
export async function handleOwnerPost(request, ident, { podPut, ownerWebId, maxBytes = DEFAULT_MAX_BYTES } = {}) {
|
|
144
|
-
if (!ident.hmacSecret) return { status: 409, reason: 'this account has no door secret — attach it again' };
|
|
145
|
-
let raw;
|
|
146
|
-
try { raw = await request.text(); } catch { return { status: 400, reason: 'unreadable body' }; }
|
|
147
|
-
if (Buffer.byteLength(raw) > maxBytes) return { status: 413, reason: 'too large' };
|
|
148
|
-
let doc;
|
|
149
|
-
try { doc = JSON.parse(raw); } catch { return { status: 400, reason: 'unparsable JSON' }; }
|
|
150
|
-
if (!doc || typeof doc !== 'object' || Array.isArray(doc) || !doc.type) {
|
|
151
|
-
return { status: 400, reason: 'a typed ActivityStreams object is required' };
|
|
152
|
-
}
|
|
153
|
-
const slug = safeSlug(request.headers.get('slug'));
|
|
154
|
-
const receipt = signReceipt({
|
|
155
|
-
v: 1, verified: true, method: 'c2s', keyId: ownerWebId || null, actor: ident.actorUrl,
|
|
156
|
-
checks: ['owner-token'], reason: 'owner', gateway: ident.gatewayWebId, ...(slug ? { slug } : {}),
|
|
157
|
-
}, ident.hmacSecret);
|
|
158
|
-
const hash = sha256hex(raw);
|
|
159
|
-
const okA = await inbox.appendVerifiedDelivery(podPut, ident.inboxUrl, hash, raw);
|
|
160
|
-
if (!okA) return { status: 502, reason: 'pod inbox write failed' };
|
|
161
|
-
await inbox.writeReceiptBeside(podPut, ident.inboxUrl, hash, receipt);
|
|
162
|
-
return { status: 201, reason: 'accepted', location: slug && ident.notesPrefix ? ident.notesPrefix + slug : null };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export const _internal = { isBlocked, concernsUsAtEdge, httpUrl, sha256hex };
|
package/lib/gateway/headers.mjs
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
// headers.mjs — the hardening every front response carries, in one place.
|
|
2
|
-
// Split out of front-core.mjs, which is at its size gate; nothing here knows
|
|
3
|
-
// about routes.
|
|
4
|
-
|
|
5
|
-
// Every response this file makes, hardened in one place rather than in each of
|
|
6
|
-
// the dozen shapes below.
|
|
7
|
-
//
|
|
8
|
-
// `nosniff` matters most: the front serves user-supplied JSON straight from
|
|
9
|
-
// somebody's pod (the proxied actor and object documents), and without it a
|
|
10
|
-
// browser is free to decide for itself that a document is HTML and run what is
|
|
11
|
-
// inside it. The rest is the same posture the app already has — nothing may be
|
|
12
|
-
// framed, no base tag may be rewritten, no plugin content.
|
|
13
|
-
//
|
|
14
|
-
// A content-security-policy goes on the HTML only: it would mean nothing on a
|
|
15
|
-
// JSON document, and `frame-ancestors` has to be a header rather than a meta
|
|
16
|
-
// tag anyway.
|
|
17
|
-
//
|
|
18
|
-
// `script-src 'self'` is the one that matters, and it is only possible because
|
|
19
|
-
// none of these pages carries inline script any more — each has its own file and
|
|
20
|
-
// its own route above. A policy cannot tell an inline block the author wrote
|
|
21
|
-
// from one an attacker injected, so as long as any inline script has to run,
|
|
22
|
-
// every inline script may.
|
|
23
|
-
//
|
|
24
|
-
// `connect-src` allows https: because the pages sign in against the user's own
|
|
25
|
-
// pod, which is a different origin by definition and not one we can name here.
|
|
26
|
-
export function withSecurityHeaders(headers = {}, body = null) {
|
|
27
|
-
const ct = String(headers['content-type'] || '');
|
|
28
|
-
const isHtml = ct.startsWith('text/html');
|
|
29
|
-
return {
|
|
30
|
-
...headers,
|
|
31
|
-
'x-content-type-options': 'nosniff',
|
|
32
|
-
'referrer-policy': 'same-origin',
|
|
33
|
-
'x-frame-options': 'SAMEORIGIN',
|
|
34
|
-
...(isHtml && body ? {
|
|
35
|
-
'content-security-policy': [
|
|
36
|
-
"default-src 'self'",
|
|
37
|
-
"script-src 'self'", // no inline script: see above
|
|
38
|
-
"style-src 'self' 'unsafe-inline'",
|
|
39
|
-
"img-src 'self' https: data:",
|
|
40
|
-
"connect-src 'self' https:", // sign-in goes to the user's own pod
|
|
41
|
-
"object-src 'none'", // no plugin content, ever
|
|
42
|
-
"base-uri 'none'", // no rewriting where relative URLs resolve
|
|
43
|
-
"frame-ancestors 'self'", // nobody else may frame these pages
|
|
44
|
-
"form-action 'self'", // a form here submits here
|
|
45
|
-
].join('; '),
|
|
46
|
-
} : {}),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
package/lib/gateway/httpsig.mjs
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
// httpsig.mjs — inbound HTTP-Signature verification, and the verification
|
|
2
|
-
// receipt that carries the outcome from wherever the verify happened (an
|
|
3
|
-
// always-on gateway, or the agent's own endpoint when it is publicly reachable)
|
|
4
|
-
// into the drain, which cannot see the delivery's headers.
|
|
5
|
-
//
|
|
6
|
-
// We SIGN outbound with Fedify draft-cavage (lib/deliver.mjs); we VERIFY with
|
|
7
|
-
// the same library, so the signing-string reconstruction can never drift out
|
|
8
|
-
// of step with what we produce. The one thing Fedify must be prevented from
|
|
9
|
-
// doing on its own is fetching an attacker-named key over an unguarded socket
|
|
10
|
-
// — so the key deref is injected as a loader that goes through safefetch.
|
|
11
|
-
|
|
12
|
-
import crypto from 'node:crypto';
|
|
13
|
-
import { verifyRequestDetailed } from '@fedify/fedify/sig';
|
|
14
|
-
import { assertPublicUrl, safeFetch, readCapped } from '../shared/safefetch.mjs';
|
|
15
|
-
|
|
16
|
-
// An SSRF-safe JSON-LD document loader for Fedify's key fetch. The keyId in a
|
|
17
|
-
// delivery is chosen by the sender, so every deref it triggers is untrusted
|
|
18
|
-
// input and must pass the same address policy as the rest of our outbound
|
|
19
|
-
// path. `skipActor` forces a network read past any cache, for the one refetch
|
|
20
|
-
// a rotated key needs.
|
|
21
|
-
export function makeSafeLoader({ getActors = null, fetchImpl = fetch } = {}) {
|
|
22
|
-
return async (url) => {
|
|
23
|
-
// A cached actor document short-circuits the network entirely — and it was
|
|
24
|
-
// only cached after its own origin vouched for its id (intake.fetchAP).
|
|
25
|
-
if (getActors) {
|
|
26
|
-
const hit = getActors()[url];
|
|
27
|
-
if (hit) return { document: hit, documentUrl: url, contextUrl: null };
|
|
28
|
-
}
|
|
29
|
-
await assertPublicUrl(url);
|
|
30
|
-
const res = await safeFetch(url, {
|
|
31
|
-
headers: { accept: 'application/activity+json, application/ld+json' },
|
|
32
|
-
}, fetchImpl);
|
|
33
|
-
if (res.status >= 400) throw new Error(`key fetch ${url} → ${res.status}`);
|
|
34
|
-
const document = JSON.parse(await readCapped(res));
|
|
35
|
-
return { document, documentUrl: url, contextUrl: null };
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Verify a delivery's signature. `request` is a WHATWG Request (the gateway
|
|
40
|
-
// has one natively; an agent-side endpoint builds one from its req). Returns a
|
|
41
|
-
// normalized result both the gateway and the drain understand — never throws
|
|
42
|
-
// on a bad signature, because "unverified" is a routine outcome, not an error.
|
|
43
|
-
//
|
|
44
|
-
// Three outcomes, kept apart because the door acts on them differently. Only a
|
|
45
|
-
// signature we could CHECK and found wrong is a forgery. A key we could not
|
|
46
|
-
// fetch proves nothing: a sender in secure mode — Threads, Mastodon with
|
|
47
|
-
// AUTHORIZED_FETCH — answers this keyless loader 404 or 401, so its deliveries
|
|
48
|
-
// have to degrade to unverified rather than be read as forged.
|
|
49
|
-
export async function verifyHttpSignature(request, { documentLoader, keyCache, timeWindow } = {}) {
|
|
50
|
-
const hadSig = request.headers.get('signature') != null;
|
|
51
|
-
let result = null;
|
|
52
|
-
try {
|
|
53
|
-
result = await verifyRequestDetailed(request, {
|
|
54
|
-
documentLoader,
|
|
55
|
-
...(keyCache ? { keyCache } : {}),
|
|
56
|
-
timeWindow: timeWindow ?? { hours: 1 },
|
|
57
|
-
});
|
|
58
|
-
} catch {
|
|
59
|
-
result = null; // the library itself failed — no more informative than a bad signature
|
|
60
|
-
}
|
|
61
|
-
// Which signature the sender used: RFC 9421 carries a Signature-Input
|
|
62
|
-
// header beside Signature; the cavage draft carries Signature alone. The
|
|
63
|
-
// library picks the verifier by the same header.
|
|
64
|
-
const method = request.headers.has('signature-input') ? 'rfc9421' : 'draft-cavage';
|
|
65
|
-
if (result?.verified) {
|
|
66
|
-
const key = result.key;
|
|
67
|
-
return {
|
|
68
|
-
verified: true, method,
|
|
69
|
-
keyId: key.id?.href ?? null, actor: key.ownerId?.href ?? null, reason: null,
|
|
70
|
-
checks: { signature: true, digest: true, dateSkew: true, keyFetched: true },
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
const kind = result?.reason?.type;
|
|
74
|
-
if (!hadSig || kind === 'noSignature') {
|
|
75
|
-
return {
|
|
76
|
-
verified: false, method: 'none', keyId: null, actor: null,
|
|
77
|
-
reason: 'no-signature', checks: { signature: false },
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
if (kind === 'keyFetchError') {
|
|
81
|
-
return {
|
|
82
|
-
verified: false, method, keyId: null, actor: null,
|
|
83
|
-
reason: 'key-unfetchable', checks: { signature: false, keyFetched: false },
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
return {
|
|
87
|
-
verified: false, method, keyId: null, actor: null,
|
|
88
|
-
reason: 'bad-signature', checks: { signature: false },
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// The receipt: what the door concluded, HMAC-stamped so the drain can trust it
|
|
93
|
-
// even while the inbox is still public-Append (a forger without the secret
|
|
94
|
-
// cannot mint `verified: true`). The HMAC is over the receipt minus the hmac
|
|
95
|
-
// field itself, with sorted keys so gateway and drain canonicalize alike.
|
|
96
|
-
//
|
|
97
|
-
// JSON.stringify's replacer ARRAY applies at EVERY level, not just the top: the
|
|
98
|
-
// old canonical() passed the top-level key names, so `checks` — whose own keys
|
|
99
|
-
// are `signature`, `digest`, `dateSkew`, `keyFetched` — serialized as `{}` and
|
|
100
|
-
// was covered by no signature at all. Anything nested was equally unstamped.
|
|
101
|
-
// Sort recursively instead, and exclude `hmac` only where it actually lives.
|
|
102
|
-
function sortedKeys(v) {
|
|
103
|
-
if (Array.isArray(v)) return v.map(sortedKeys);
|
|
104
|
-
if (v && typeof v === 'object') {
|
|
105
|
-
const out = {};
|
|
106
|
-
for (const k of Object.keys(v).sort()) out[k] = sortedKeys(v[k]);
|
|
107
|
-
return out;
|
|
108
|
-
}
|
|
109
|
-
return v;
|
|
110
|
-
}
|
|
111
|
-
function canonical(obj) {
|
|
112
|
-
const { hmac, ...rest } = obj; // eslint-disable-line no-unused-vars
|
|
113
|
-
return JSON.stringify(sortedKeys(rest));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// What canonical() used to produce. Kept ONLY for verification, and only until
|
|
117
|
-
// every door has been redeployed: a gateway (fedipod.net) and an agent update
|
|
118
|
-
// independently, so for a while a receipt signed by an old door has to keep
|
|
119
|
-
// verifying against a new drain. Receipts are short-lived — one per inbox item,
|
|
120
|
-
// deleted with the item — so this can go once the doors have turned over.
|
|
121
|
-
// Nothing signs with it.
|
|
122
|
-
function canonicalLegacy(obj) {
|
|
123
|
-
return JSON.stringify(obj, Object.keys(obj).filter(k => k !== 'hmac').sort());
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function makeReceipt(result, { gateway = null } = {}) {
|
|
127
|
-
return {
|
|
128
|
-
v: 1,
|
|
129
|
-
verified: !!result.verified,
|
|
130
|
-
method: result.method,
|
|
131
|
-
keyId: result.keyId,
|
|
132
|
-
actor: result.actor,
|
|
133
|
-
checks: result.checks,
|
|
134
|
-
reason: result.reason,
|
|
135
|
-
gateway,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function signReceipt(receipt, secret) {
|
|
140
|
-
const hmac = crypto.createHmac('sha256', secret).update(canonical(receipt)).digest('base64');
|
|
141
|
-
return { ...receipt, hmac };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// True only when the receipt carries a valid HMAC for `secret`. A missing
|
|
145
|
-
// secret, a missing hmac, or a mismatch all read as "not ours" → the drain
|
|
146
|
-
// treats the receipt as absent (today's unverified behavior). timingSafeEqual
|
|
147
|
-
// over equal-length buffers; unequal lengths are a straight mismatch.
|
|
148
|
-
export function verifyReceipt(receipt, secret) {
|
|
149
|
-
if (!receipt || typeof receipt !== 'object' || !receipt.hmac || !secret) return false;
|
|
150
|
-
let given;
|
|
151
|
-
try { given = Buffer.from(receipt.hmac, 'base64'); } catch { return false; }
|
|
152
|
-
const matches = (text) => {
|
|
153
|
-
const expected = crypto.createHmac('sha256', secret).update(text).digest();
|
|
154
|
-
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
|
|
155
|
-
};
|
|
156
|
-
// The current stamp first; the pre-2026-09-09 one second, for receipts a door
|
|
157
|
-
// that has not been redeployed yet is still writing. See canonicalLegacy.
|
|
158
|
-
return matches(canonical(receipt)) || matches(canonicalLegacy(receipt));
|
|
159
|
-
}
|
package/lib/gateway/notices.mjs
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// notices.mjs — notices from whoever runs the front, to every account here.
|
|
2
|
-
//
|
|
3
|
-
// The operator writes them on the /notices page, proved as the admin the way
|
|
4
|
-
// the roster is; every page of ours asks GET /api/notices once per load and
|
|
5
|
-
// shows a bell when there are any. A notice is a title, a plain-text body and
|
|
6
|
-
// a time. The body is never markup: the pages render it as text.
|
|
7
|
-
//
|
|
8
|
-
// Kept beside the directory, in a store the adapter supplies:
|
|
9
|
-
// listNotices() -> { id: notice } putNotice(id, notice) deleteNotice(id)
|
|
10
|
-
// A front that supplies none has no notices to offer, and says so.
|
|
11
|
-
|
|
12
|
-
import crypto from 'node:crypto';
|
|
13
|
-
|
|
14
|
-
const TITLE_MAX = 120;
|
|
15
|
-
const BODY_MAX = 4000;
|
|
16
|
-
const NOTICES_EDGE_SECONDS = 60;
|
|
17
|
-
|
|
18
|
-
const clean = (v, max) => String(v ?? '').replace(/\r\n?/gu, '\n').trim().slice(0, max);
|
|
19
|
-
const newest = (a, b) => String(b.at || '').localeCompare(String(a.at || ''));
|
|
20
|
-
|
|
21
|
-
export async function listNotices(ctx) {
|
|
22
|
-
const all = ctx.listNotices ? await ctx.listNotices() : {};
|
|
23
|
-
return Object.values(all || {}).filter(Boolean).sort(newest);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// GET is public: what every account is shown. POST is the admin's alone. The
|
|
27
|
-
// /notices page is where the admin writes them; it signs in like the roster.
|
|
28
|
-
// Returns a response, or null when the path is neither.
|
|
29
|
-
export async function routeNoticesApi(request, pathname, ctx, { j, verifyPodToken, publicFor, notFound }) {
|
|
30
|
-
// The two admin pages: the roster (who has accounts here) and the notices.
|
|
31
|
-
// Each signs in and calls its API; a deploy with no admin supplies no page.
|
|
32
|
-
const page = pathname === '/notices' ? ctx.noticesPage : pathname === '/roster' ? ctx.adminPage : undefined;
|
|
33
|
-
if (page !== undefined) {
|
|
34
|
-
if (request.method !== 'GET' && request.method !== 'HEAD') return { status: 405, headers: {}, body: '' };
|
|
35
|
-
if (!page) return notFound();
|
|
36
|
-
return { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: page };
|
|
37
|
-
}
|
|
38
|
-
if (pathname !== '/api/notices') return null;
|
|
39
|
-
const open = { 'access-control-allow-origin': '*' };
|
|
40
|
-
if (request.method === 'OPTIONS') {
|
|
41
|
-
return { status: 204, headers: { ...open, 'access-control-allow-methods': 'GET, OPTIONS',
|
|
42
|
-
'access-control-allow-headers': 'Accept', allow: 'GET, POST, OPTIONS' }, body: null };
|
|
43
|
-
}
|
|
44
|
-
if (request.method === 'GET') {
|
|
45
|
-
if (!ctx.listNotices) return j(501, { error: 'this front keeps no notices' });
|
|
46
|
-
const out = j(200, { notices: await listNotices(ctx) });
|
|
47
|
-
Object.assign(out.headers, open, publicFor(NOTICES_EDGE_SECONDS));
|
|
48
|
-
return out;
|
|
49
|
-
}
|
|
50
|
-
if (request.method !== 'POST') return { status: 405, headers: {}, body: '' };
|
|
51
|
-
if (!ctx.putNotice || !ctx.deleteNotice || !ctx.adminWebId) return j(501, { error: 'this front keeps no notices' });
|
|
52
|
-
const webid = await verifyPodToken(request, pathname, ctx.verifier);
|
|
53
|
-
if (!webid) return j(401, { error: 'a Solid-OIDC token is required' });
|
|
54
|
-
if (webid !== ctx.adminWebId) return j(403, { error: 'that WebID is not the admin of this front' });
|
|
55
|
-
let body;
|
|
56
|
-
try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
|
|
57
|
-
const action = String(body.action || '');
|
|
58
|
-
const all = (ctx.listNotices ? await ctx.listNotices() : {}) || {};
|
|
59
|
-
if (action === 'delete') {
|
|
60
|
-
const id = String(body.id || '');
|
|
61
|
-
if (!all[id]) return j(404, { error: 'no such notice' });
|
|
62
|
-
await ctx.deleteNotice(id);
|
|
63
|
-
console.log(`front: notice ${id} deleted by the admin`);
|
|
64
|
-
return j(200, { ok: true, id, deleted: true });
|
|
65
|
-
}
|
|
66
|
-
if (action !== 'create' && action !== 'update') return j(400, { error: 'action must be create, update or delete' });
|
|
67
|
-
const title = clean(body.title, TITLE_MAX);
|
|
68
|
-
const text = clean(body.body, BODY_MAX);
|
|
69
|
-
if (!title) return j(400, { error: 'a title is required' });
|
|
70
|
-
if (!text) return j(400, { error: 'a body is required' });
|
|
71
|
-
const now = new Date().toISOString();
|
|
72
|
-
let notice;
|
|
73
|
-
if (action === 'create') {
|
|
74
|
-
const id = `${Date.now().toString(36)}-${crypto.randomBytes(4).toString('hex')}`;
|
|
75
|
-
notice = { id, title, body: text, at: now, updatedAt: now };
|
|
76
|
-
} else {
|
|
77
|
-
const prior = all[String(body.id || '')];
|
|
78
|
-
if (!prior) return j(404, { error: 'no such notice' });
|
|
79
|
-
notice = { ...prior, title, body: text, updatedAt: now };
|
|
80
|
-
}
|
|
81
|
-
await ctx.putNotice(notice.id, notice);
|
|
82
|
-
console.log(`front: notice ${notice.id} ${action}d by the admin`);
|
|
83
|
-
return j(action === 'create' ? 201 : 200, { ok: true, notice });
|
|
84
|
-
}
|
package/lib/gateway/quiet.mjs
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
// quiet.mjs — accounts that go quiet: what the front knows about an account
|
|
2
|
-
// nobody opens, and what its owner may say about it. The router in
|
|
3
|
-
// front-core.mjs asks closedState/accountState at the door and on every
|
|
4
|
-
// public read, stamps noteOpened from the relay, counts noteReceived after
|
|
5
|
-
// a forwarded delivery, and hands /api/open, /api/pause and /api/close here.
|
|
6
|
-
|
|
7
|
-
// ---- accounts that go quiet -------------------------------------------------
|
|
8
|
-
//
|
|
9
|
-
// The gateway holds no mail: every delivery it takes is two writes into the
|
|
10
|
-
// owner's pod inbox, read only when their browser is open. An account nobody
|
|
11
|
-
// opens grows on its pod without limit, and its owner comes back to a drain
|
|
12
|
-
// of everything at once. So the front keeps two facts about each browser
|
|
13
|
-
// account — when its owner was last here, and how much content has arrived
|
|
14
|
-
// since — and acts on them.
|
|
15
|
-
//
|
|
16
|
-
// `openedAt` is written when the owner signs in (POST /api/open, always) and
|
|
17
|
-
// while they act through the relay (at most hourly). The count of content
|
|
18
|
-
// deliveries since then lives in its own record, keyed by handle AND
|
|
19
|
-
// openedAt: a fresh stamp starts a fresh count with nothing to reset, and a
|
|
20
|
-
// delivery's write can never overwrite a sign-in's. Two deliveries landing
|
|
21
|
-
// together may each read the same total and one increment is lost — the cap
|
|
22
|
-
// comes out slightly soft, never too strict.
|
|
23
|
-
//
|
|
24
|
-
// PAUSED: the count has reached the cap, or the owner said so. Content is
|
|
25
|
-
// accepted and discarded, control (follows, unfollows, moves, deletions,
|
|
26
|
-
// blocks) still lands. The owner's next sign-in ends a cap pause by itself; a
|
|
27
|
-
// pause they set lasts until they lift it.
|
|
28
|
-
//
|
|
29
|
-
// CLOSED: the owner said so, or nothing has opened the account for the close
|
|
30
|
-
// window. Found closed by time, it is written down so it stays closed. From
|
|
31
|
-
// then on the door, the actor and the handle answer 410 — never a failure
|
|
32
|
-
// code, which senders retry and count against this whole host, and never a
|
|
33
|
-
// success, which would keep them sending. Nothing on the pod is touched. An
|
|
34
|
-
// address that moved (see /api/move) keeps answering as moved; closing does
|
|
35
|
-
// not take that away.
|
|
36
|
-
//
|
|
37
|
-
// Only an account whose owner has signed in from a browser carries
|
|
38
|
-
// `openedAt`. A DeviceAgent behind this door never does: it drains its own
|
|
39
|
-
// inbox as it runs, so it is never counted, never paused and never closed by
|
|
40
|
-
// time. Accounts from before this was built are counted from their next
|
|
41
|
-
// sign-in.
|
|
42
|
-
export const DEFAULT_PAUSE_ITEMS = 5000;
|
|
43
|
-
export const DEFAULT_CLOSE_DAYS = 183; // six months
|
|
44
|
-
const OPEN_STAMP_EVERY_MS = 60 * 60_000; // the relay's stamp, at most hourly
|
|
45
|
-
const pauseItemsOf = (ctx) => (Number(ctx.pauseItems) > 0 ? Number(ctx.pauseItems) : DEFAULT_PAUSE_ITEMS);
|
|
46
|
-
const closeDaysOf = (ctx) => (Number(ctx.closeDays) > 0 ? Number(ctx.closeDays) : DEFAULT_CLOSE_DAYS);
|
|
47
|
-
const receivedKey = (key, rec) => `${key}/${rec.openedAt}`;
|
|
48
|
-
|
|
49
|
-
export async function receivedSince(ctx, key, rec) {
|
|
50
|
-
if (!ctx.readReceived || !rec.openedAt) return { items: 0, bytes: 0 };
|
|
51
|
-
const got = await ctx.readReceived(receivedKey(key, rec)).catch(() => null);
|
|
52
|
-
return { items: Number(got?.items) || 0, bytes: Number(got?.bytes) || 0 };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// One more content delivery reached the pod. Nothing to count against until
|
|
56
|
-
// the owner has been here once.
|
|
57
|
-
export async function noteReceived(ctx, key, rec, bytes) {
|
|
58
|
-
if (!ctx.readReceived || !ctx.writeReceived || !rec.openedAt) return;
|
|
59
|
-
const so = await receivedSince(ctx, key, rec);
|
|
60
|
-
await ctx.writeReceived(receivedKey(key, rec), { items: so.items + 1, bytes: so.bytes + (Number(bytes) || 0) })
|
|
61
|
-
.catch((e) => console.log(`front @${key}: count not written: ${e?.message || e}`));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// The owner is here. `always` is a sign-in; the relay stamps at most hourly,
|
|
65
|
-
// so a busy hour of posting is one write. The count that went with the old
|
|
66
|
-
// stamp is dropped: it is over, and a store should not keep one per hour.
|
|
67
|
-
export async function noteOpened(ctx, key, rec, { always = false } = {}) {
|
|
68
|
-
if (!ctx.putDirectory) return rec;
|
|
69
|
-
if (!always && rec.openedAt && Date.now() - Date.parse(rec.openedAt) < OPEN_STAMP_EVERY_MS) return rec;
|
|
70
|
-
const next = { ...rec, openedAt: new Date().toISOString() };
|
|
71
|
-
await ctx.putDirectory(key, next);
|
|
72
|
-
if (rec.openedAt && ctx.dropReceived) await ctx.dropReceived(receivedKey(key, rec)).catch(() => {});
|
|
73
|
-
return next;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Closed, and why. Reads no count: this is asked on every public read.
|
|
77
|
-
export async function closedState(ctx, key, rec) {
|
|
78
|
-
if (rec.movedTo) return { closed: false };
|
|
79
|
-
if (rec.closedAt) return { closed: true, closedAt: rec.closedAt, closedBy: rec.closedBy || 'owner' };
|
|
80
|
-
if (rec.openedAt && Date.now() - Date.parse(rec.openedAt) > closeDaysOf(ctx) * 86400_000) {
|
|
81
|
-
const closedAt = new Date().toISOString();
|
|
82
|
-
if (ctx.putDirectory) await ctx.putDirectory(key, { ...rec, closedAt, closedBy: 'quiet' });
|
|
83
|
-
console.log(`front @${key}: closed — nothing opened it since ${rec.openedAt}`);
|
|
84
|
-
return { closed: true, closedAt, closedBy: 'quiet' };
|
|
85
|
-
}
|
|
86
|
-
return { closed: false };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// The whole standing of an account, for the door and for its owner's page.
|
|
90
|
-
export async function accountState(ctx, key, rec) {
|
|
91
|
-
const c = await closedState(ctx, key, rec);
|
|
92
|
-
const received = c.closed ? { items: 0, bytes: 0 } : await receivedSince(ctx, key, rec);
|
|
93
|
-
const cap = pauseItemsOf(ctx);
|
|
94
|
-
const pausedBy = c.closed ? null : rec.pausedAt ? 'owner' : received.items >= cap ? 'quiet' : null;
|
|
95
|
-
return {
|
|
96
|
-
closed: c.closed, closedAt: c.closedAt || null, closedBy: c.closedBy || null,
|
|
97
|
-
paused: !!pausedBy, pausedBy, pausedAt: rec.pausedAt || null,
|
|
98
|
-
openedAt: rec.openedAt || null, received, pauseItems: cap, closeDays: closeDaysOf(ctx),
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// A 410 with a reason, the shape every closed or moved id answers with.
|
|
103
|
-
export const closedAnswer = (headers = {}, why = 'this address is closed') => ({
|
|
104
|
-
status: 410, headers: { 'cache-control': 'no-store', ...headers, 'content-type': 'application/json' },
|
|
105
|
-
body: JSON.stringify({ error: why }) });
|
|
106
|
-
|
|
107
|
-
// The owner of a row, proved the way the relay proves them: a pod token whose
|
|
108
|
-
// WebID is the row's, or lives on the row's pod for a row from before WebIDs
|
|
109
|
-
// were recorded.
|
|
110
|
-
async function provedOwner(request, pathname, ctx, rec, { j, verifyPodToken, webidUnderPod }) {
|
|
111
|
-
const webid = await verifyPodToken(request, pathname, ctx.verifier);
|
|
112
|
-
if (!webid) return { error: j(401, { error: 'a Solid-OIDC token proving the pod is required' }) };
|
|
113
|
-
const owner = rec.webId ? webid === rec.webId : webidUnderPod(webid, rec.podHome);
|
|
114
|
-
if (!owner) return { error: j(403, { error: "the token proves a different pod than this account's" }) };
|
|
115
|
-
return { webid };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// The owner's three routes. `deps` are front-core's own JSON reply and
|
|
119
|
-
// token check, handed in so this module stays free of the router's privates.
|
|
120
|
-
// Returns a response, or null when the path is not one of these.
|
|
121
|
-
export async function routeQuietApi(request, pathname, ctx, deps) {
|
|
122
|
-
const { j } = deps;
|
|
123
|
-
if (!['/api/open', '/api/pause', '/api/close'].includes(pathname)) return null;
|
|
124
|
-
// A page at another origin asks first whether it may call, and is answered
|
|
125
|
-
// as the relay answers it. Unanswered, the browser never sends the call and
|
|
126
|
-
// some pages asked again every minute.
|
|
127
|
-
if (request.method === 'OPTIONS') return deps.apiPreflight();
|
|
128
|
-
if (request.method !== 'POST') return null;
|
|
129
|
-
// The owner is here: their browser says so as it opens the account. The
|
|
130
|
-
// answer is the account's standing at this gateway, which the manage page
|
|
131
|
-
// shows. A closed address is told so and nothing is written.
|
|
132
|
-
if (pathname === '/api/open' && request.method === 'POST') {
|
|
133
|
-
if (!ctx.putDirectory) return j(501, { error: 'this front keeps no directory' });
|
|
134
|
-
let body;
|
|
135
|
-
try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
|
|
136
|
-
const handle = String(body.handle || '').toLowerCase();
|
|
137
|
-
let rec = await ctx.lookup(handle);
|
|
138
|
-
if (!rec) return j(404, { error: 'no such account' });
|
|
139
|
-
const who = await provedOwner(request, pathname, ctx, rec, deps);
|
|
140
|
-
if (who.error) return who.error;
|
|
141
|
-
const before = await closedState(ctx, handle, rec);
|
|
142
|
-
if (before.closed) return j(410, { error: 'this address is closed', closedAt: before.closedAt, closedBy: before.closedBy });
|
|
143
|
-
rec = await noteOpened(ctx, handle, rec, { always: true });
|
|
144
|
-
return j(200, { ok: true, handle, ...await accountState(ctx, handle, rec) });
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// The owner pausing their own account, or lifting the pause they set.
|
|
148
|
-
if (pathname === '/api/pause' && request.method === 'POST') {
|
|
149
|
-
if (!ctx.putDirectory) return j(501, { error: 'this front keeps no directory' });
|
|
150
|
-
let body;
|
|
151
|
-
try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
|
|
152
|
-
const handle = String(body.handle || '').toLowerCase();
|
|
153
|
-
let rec = await ctx.lookup(handle);
|
|
154
|
-
if (!rec) return j(404, { error: 'no such account' });
|
|
155
|
-
const who = await provedOwner(request, pathname, ctx, rec, deps);
|
|
156
|
-
if (who.error) return who.error;
|
|
157
|
-
const before = await closedState(ctx, handle, rec);
|
|
158
|
-
if (before.closed) return j(410, { error: 'this address is closed', closedAt: before.closedAt });
|
|
159
|
-
if (typeof body.paused !== 'boolean') return j(400, { error: 'paused must be true or false' });
|
|
160
|
-
if (body.paused && !rec.pausedAt) rec = { ...rec, pausedAt: new Date().toISOString() };
|
|
161
|
-
if (!body.paused && rec.pausedAt) { rec = { ...rec }; delete rec.pausedAt; }
|
|
162
|
-
// Lifting a pause is a sign-in's worth of "I am here": the count starts over.
|
|
163
|
-
if (!body.paused) rec = { ...rec, openedAt: new Date().toISOString() };
|
|
164
|
-
await ctx.putDirectory(handle, rec);
|
|
165
|
-
console.log(`front @${handle}: ${body.paused ? 'paused' : 'resumed'} by its owner`);
|
|
166
|
-
return j(200, { ok: true, handle, ...await accountState(ctx, handle, rec) });
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// The owner closing their address for good. Repeating it changes nothing.
|
|
170
|
-
if (pathname === '/api/close' && request.method === 'POST') {
|
|
171
|
-
if (!ctx.putDirectory) return j(501, { error: 'this front keeps no directory' });
|
|
172
|
-
let body;
|
|
173
|
-
try { body = JSON.parse(await request.clone().text()); } catch { return j(400, { error: 'bad JSON' }); }
|
|
174
|
-
const handle = String(body.handle || '').toLowerCase();
|
|
175
|
-
let rec = await ctx.lookup(handle);
|
|
176
|
-
if (!rec) return j(404, { error: 'no such account' });
|
|
177
|
-
const who = await provedOwner(request, pathname, ctx, rec, deps);
|
|
178
|
-
if (who.error) return who.error;
|
|
179
|
-
if (body.confirm !== true) return j(400, { error: 'closing is for good — send confirm: true' });
|
|
180
|
-
if (!rec.closedAt) {
|
|
181
|
-
rec = { ...rec, closedAt: new Date().toISOString(), closedBy: 'owner' };
|
|
182
|
-
await ctx.putDirectory(handle, rec);
|
|
183
|
-
console.log(`front @${handle}: closed by its owner`);
|
|
184
|
-
}
|
|
185
|
-
return j(200, { ok: true, handle, ...await accountState(ctx, handle, rec) });
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return null;
|
|
189
|
-
}
|