fedipod-server 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -6
- package/dist/claims.d.ts +2 -0
- package/dist/claims.js +13 -4
- package/dist/door.d.ts +4 -0
- package/dist/door.js +31 -0
- package/dist/fedipod.d.ts +15 -0
- package/dist/fedipod.js +13 -0
- package/dist/front.d.ts +3 -0
- package/dist/front.js +92 -0
- package/dist/handler.d.ts +36 -101
- package/dist/handler.js +93 -644
- package/dist/handler.jsonld +2 -30
- package/dist/identities.d.ts +71 -0
- package/dist/identities.js +236 -0
- package/dist/mounts.d.ts +43 -0
- package/dist/mounts.js +123 -0
- package/dist/opt-in.d.ts +46 -0
- package/dist/opt-in.js +176 -0
- package/dist/private-read.d.ts +4 -0
- package/dist/private-read.js +34 -0
- package/package.json +12 -20
- package/lib/client/c2s.mjs +0 -636
- package/lib/client/localapi.mjs +0 -87
- package/lib/client/masto/accounts.mjs +0 -310
- package/lib/client/masto/body.mjs +0 -156
- package/lib/client/masto/index.mjs +0 -206
- package/lib/client/masto/instance.mjs +0 -112
- package/lib/client/masto/media.mjs +0 -79
- package/lib/client/masto/oauth.mjs +0 -656
- package/lib/client/masto/render.mjs +0 -518
- package/lib/client/masto/statuses.mjs +0 -372
- package/lib/client/masto/timelines.mjs +0 -438
- package/lib/client/oidc-auth.mjs +0 -62
- package/lib/client/streaming.mjs +0 -154
- package/lib/client/webpush.mjs +0 -98
- package/lib/connections/acctfeed.mjs +0 -257
- package/lib/connections/atproto.mjs +0 -309
- package/lib/connections/bskyfeed.mjs +0 -243
- package/lib/connections/bskygroup.mjs +0 -162
- package/lib/connections/fediacct.mjs +0 -253
- package/lib/connections/import.mjs +0 -357
- package/lib/connections/tagfeed.mjs +0 -212
- package/lib/connections/vault.mjs +0 -114
- package/lib/core/as2.mjs +0 -206
- package/lib/core/contexts/activitystreams.json +0 -379
- package/lib/core/contexts/anno.json +0 -126
- package/lib/core/contexts/did-v1.json +0 -57
- package/lib/core/contexts/fep-5711.json +0 -36
- package/lib/core/contexts/gotosocial.json +0 -86
- package/lib/core/contexts/identity-v1.json +0 -152
- package/lib/core/contexts/index.mjs +0 -63
- package/lib/core/contexts/join-lemmy.json +0 -33
- package/lib/core/contexts/joinmastodon.json +0 -28
- package/lib/core/contexts/litepub-0.1.json +0 -60
- package/lib/core/contexts/map.json +0 -19
- package/lib/core/contexts/miscellany.json +0 -19
- package/lib/core/contexts/quotes.json +0 -18
- package/lib/core/contexts/schemaorg.json +0 -8845
- package/lib/core/contexts/security-data-integrity-v1.json +0 -78
- package/lib/core/contexts/security-data-integrity-v2.json +0 -81
- package/lib/core/contexts/security-multikey-v1.json +0 -35
- package/lib/core/contexts/security-v1.json +0 -74
- package/lib/core/contexts/webfinger.json +0 -10
- package/lib/core/deliver.mjs +0 -387
- package/lib/core/graphview.mjs +0 -327
- package/lib/core/intake/activities.mjs +0 -649
- package/lib/core/intake/activity.mjs +0 -318
- package/lib/core/intake/channel.mjs +0 -144
- package/lib/core/intake/group.mjs +0 -258
- package/lib/core/intake/index.mjs +0 -732
- package/lib/core/intake/notes.mjs +0 -367
- package/lib/core/intake/verify.mjs +0 -142
- package/lib/core/keys.mjs +0 -153
- package/lib/core/lease.mjs +0 -206
- package/lib/core/place.mjs +0 -82
- package/lib/core/polls.mjs +0 -105
- package/lib/core/profile-page.mjs +0 -104
- package/lib/core/proof.mjs +0 -79
- package/lib/core/publisher/collections.mjs +0 -272
- package/lib/core/publisher/index.mjs +0 -522
- package/lib/core/publisher/notes.mjs +0 -519
- package/lib/core/publisher/own.mjs +0 -143
- package/lib/core/publisher/questions.mjs +0 -241
- package/lib/core/publisher/restore.mjs +0 -224
- package/lib/core/shapes/activitystreams.ttl +0 -157
- package/lib/core/shapes/index.mjs +0 -132
- package/lib/core/shapes/shapes-text.mjs +0 -13
- package/lib/core/social.mjs +0 -561
- package/lib/core/storage.mjs +0 -176
- package/lib/core/store.mjs +0 -578
- package/lib/core/wire-quotes.mjs +0 -71
- package/lib/core/wire.mjs +0 -925
- package/lib/device/account.mjs +0 -87
- package/lib/device/admin/index.mjs +0 -13
- package/lib/device/admin/origins.mjs +0 -35
- package/lib/device/admin/routes/connections.mjs +0 -144
- package/lib/device/admin/routes/gateway.mjs +0 -199
- package/lib/device/admin/routes/lifecycle.mjs +0 -200
- package/lib/device/admin/routes/owner.mjs +0 -322
- package/lib/device/admin/routes/setup.mjs +0 -428
- package/lib/device/admin/routes/social.mjs +0 -188
- package/lib/device/admin/server.mjs +0 -95
- package/lib/device/admin/static.mjs +0 -247
- package/lib/device/admin/surface.mjs +0 -315
- package/lib/device/certs.mjs +0 -281
- package/lib/device/cli/commands/account.mjs +0 -586
- package/lib/device/cli/commands/run.mjs +0 -278
- package/lib/device/cli/commands/service.mjs +0 -221
- package/lib/device/cli/commands/setup.mjs +0 -489
- package/lib/device/cli/commands/state.mjs +0 -561
- package/lib/device/cli/context.mjs +0 -288
- package/lib/device/export-collections.mjs +0 -161
- package/lib/device/gateway-move.mjs +0 -58
- package/lib/device/home.mjs +0 -161
- package/lib/device/migrate.mjs +0 -115
- package/lib/device/ports.mjs +0 -27
- package/lib/device/remote.mjs +0 -54
- package/lib/device/setup.mjs +0 -496
- package/lib/device/update.mjs +0 -96
- package/lib/gateway/caches.mjs +0 -36
- package/lib/gateway/directory.mjs +0 -90
- package/lib/gateway/front-core.mjs +0 -966
- package/lib/gateway/gateway-core.mjs +0 -244
- package/lib/gateway/headers.mjs +0 -49
- package/lib/gateway/httpsig.mjs +0 -159
- package/lib/gateway/notices.mjs +0 -84
- package/lib/gateway/quiet.mjs +0 -189
- package/lib/gateway/relay-extras.mjs +0 -89
- package/lib/gateway/token-claims.mjs +0 -16
- package/lib/pod/README.md +0 -61
- package/lib/pod/actor.mjs +0 -96
- package/lib/pod/collection.mjs +0 -77
- package/lib/pod/containers.mjs +0 -140
- package/lib/pod/discovery.mjs +0 -46
- package/lib/pod/featured.mjs +0 -18
- package/lib/pod/followers.mjs +0 -24
- package/lib/pod/following.mjs +0 -10
- package/lib/pod/http.mjs +0 -74
- package/lib/pod/inbox.mjs +0 -166
- package/lib/pod/links.mjs +0 -35
- package/lib/pod/location.mjs +0 -52
- package/lib/pod/media.mjs +0 -27
- package/lib/pod/notes.mjs +0 -78
- package/lib/pod/notifications.mjs +0 -89
- package/lib/pod/outbox.mjs +0 -18
- package/lib/pod/package.json +0 -19
- package/lib/pod/policy.mjs +0 -58
- package/lib/pod/private.mjs +0 -19
- package/lib/pod/root.mjs +0 -104
- package/lib/pod/state.mjs +0 -40
- package/lib/pod/transport.mjs +0 -674
- package/lib/pod/type-index.mjs +0 -101
- package/lib/pod/urls.mjs +0 -91
- package/lib/server/embed.mjs +0 -424
- package/lib/session/README.md +0 -137
- package/lib/session/demo.html +0 -94
- package/lib/session/fedi-account.mjs +0 -417
- package/lib/session/fedi-login.mjs +0 -123
- package/lib/session/oidc-session.mjs +0 -233
- package/lib/session/package.json +0 -49
- package/lib/shared/guard.mjs +0 -213
- package/lib/shared/links.mjs +0 -9
- package/lib/shared/safefetch.mjs +0 -235
- package/lib/shared/ua.mjs +0 -29
- package/phanpy/dist/404.html +0 -40
- package/phanpy/dist/_headers +0 -2
- package/phanpy/dist/apple-touch-icon.png +0 -0
- package/phanpy/dist/assets/ICONS-DIPJeU0C.js +0 -2
- package/phanpy/dist/assets/ICONS-DIPJeU0C.js.map +0 -1
- package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js +0 -183
- package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
- package/phanpy/dist/assets/Temml-CCjoacWt.woff2 +0 -0
- package/phanpy/dist/assets/__vite-browser-external-C7iut881.js +0 -2
- package/phanpy/dist/assets/__vite-browser-external-C7iut881.js.map +0 -1
- package/phanpy/dist/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
- package/phanpy/dist/assets/catch-up-O5q5C75Z.png +0 -0
- package/phanpy/dist/assets/chunk-aKtaBQYM.js +0 -1
- package/phanpy/dist/assets/compose-CFEx7wb4.js +0 -2
- package/phanpy/dist/assets/compose-CFEx7wb4.js.map +0 -1
- package/phanpy/dist/assets/compose-ZJN5xSb4.js +0 -35
- package/phanpy/dist/assets/compose-ZJN5xSb4.js.map +0 -1
- package/phanpy/dist/assets/debug.module-YFQgwnIr.js +0 -52
- package/phanpy/dist/assets/debug.module-YFQgwnIr.js.map +0 -1
- package/phanpy/dist/assets/dom-p1YcJ5Rw.js +0 -2
- package/phanpy/dist/assets/dom-p1YcJ5Rw.js.map +0 -1
- package/phanpy/dist/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
- package/phanpy/dist/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
- package/phanpy/dist/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
- package/phanpy/dist/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
- package/phanpy/dist/assets/icons/add-circle-line-WD0mwORD.js +0 -1
- package/phanpy/dist/assets/icons/alert-line-CpFUMf9s.js +0 -1
- package/phanpy/dist/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
- package/phanpy/dist/assets/icons/announcement-line-5nwaJFJm.js +0 -1
- package/phanpy/dist/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
- package/phanpy/dist/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
- package/phanpy/dist/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
- package/phanpy/dist/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
- package/phanpy/dist/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
- package/phanpy/dist/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
- package/phanpy/dist/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
- package/phanpy/dist/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
- package/phanpy/dist/assets/icons/at-line-CF66Bznk.js +0 -1
- package/phanpy/dist/assets/icons/attachment-line-BY8It-l9.js +0 -1
- package/phanpy/dist/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
- package/phanpy/dist/assets/icons/board-line-C0UWXyA6.js +0 -1
- package/phanpy/dist/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
- package/phanpy/dist/assets/icons/building-5-line-Dd59MzbO.js +0 -1
- package/phanpy/dist/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
- package/phanpy/dist/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
- package/phanpy/dist/assets/icons/calendar-month-line-vommts2C.js +0 -1
- package/phanpy/dist/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
- package/phanpy/dist/assets/icons/camera-line-Cv69MEei.js +0 -1
- package/phanpy/dist/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
- package/phanpy/dist/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
- package/phanpy/dist/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
- package/phanpy/dist/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
- package/phanpy/dist/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
- package/phanpy/dist/assets/icons/clipboard-line-BBp364gJ.js +0 -1
- package/phanpy/dist/assets/icons/close-circle-line-CICdiuGz.js +0 -1
- package/phanpy/dist/assets/icons/close-line-DYrFWcjv.js +0 -1
- package/phanpy/dist/assets/icons/cloud-line-tiQAbah8.js +0 -1
- package/phanpy/dist/assets/icons/code-line-fAl8iiV3.js +0 -1
- package/phanpy/dist/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
- package/phanpy/dist/assets/icons/copy-2-line-CLW69yF5.js +0 -1
- package/phanpy/dist/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
- package/phanpy/dist/assets/icons/document-3-line-DBGNhAg2.js +0 -1
- package/phanpy/dist/assets/icons/document-line-C7mb9sb7.js +0 -1
- package/phanpy/dist/assets/icons/down-line-BGqSSThd.js +0 -1
- package/phanpy/dist/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
- package/phanpy/dist/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
- package/phanpy/dist/assets/icons/exit-line-Dc07BS-x.js +0 -1
- package/phanpy/dist/assets/icons/external-link-line-BvpSkmjO.js +0 -1
- package/phanpy/dist/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
- package/phanpy/dist/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
- package/phanpy/dist/assets/icons/filter-2-line-B_mR2usy.js +0 -1
- package/phanpy/dist/assets/icons/filter-line-C_nm-3cU.js +0 -1
- package/phanpy/dist/assets/icons/flag-1-line-C2HB05I0.js +0 -1
- package/phanpy/dist/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
- package/phanpy/dist/assets/icons/formula-line-DLrt2d6j.js +0 -1
- package/phanpy/dist/assets/icons/grid-line-CgUmEikf.js +0 -1
- package/phanpy/dist/assets/icons/group-line-C5fnX0wU.js +0 -1
- package/phanpy/dist/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
- package/phanpy/dist/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
- package/phanpy/dist/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
- package/phanpy/dist/assets/icons/heart-line-CJ7K47J4.js +0 -1
- package/phanpy/dist/assets/icons/history-2-line-kWWvFzqv.js +0 -1
- package/phanpy/dist/assets/icons/history-line-bGQiwbTH.js +0 -1
- package/phanpy/dist/assets/icons/home-3-line-fRU1zeLG.js +0 -1
- package/phanpy/dist/assets/icons/information-line-iNk6-sOY.js +0 -1
- package/phanpy/dist/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
- package/phanpy/dist/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
- package/phanpy/dist/assets/icons/layout-5-line-B_znt5wO.js +0 -1
- package/phanpy/dist/assets/icons/left-line-eUw0tya4.js +0 -1
- package/phanpy/dist/assets/icons/lightning-line-DaKoDdix.js +0 -1
- package/phanpy/dist/assets/icons/link-2-line-DCqLmMA7.js +0 -1
- package/phanpy/dist/assets/icons/list-check-line-BAxosBh9.js +0 -1
- package/phanpy/dist/assets/icons/lock-line-CPoaXeUW.js +0 -1
- package/phanpy/dist/assets/icons/mail-line-B0P8aa1L.js +0 -1
- package/phanpy/dist/assets/icons/moon-line-CEqNAToP.js +0 -1
- package/phanpy/dist/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
- package/phanpy/dist/assets/icons/more-3-line-BIiVjYTz.js +0 -1
- package/phanpy/dist/assets/icons/notification-line-BZZq2Gtu.js +0 -1
- package/phanpy/dist/assets/icons/pencil-line-CKh8-A1v.js +0 -1
- package/phanpy/dist/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
- package/phanpy/dist/assets/icons/pin-line-DpTolyhs.js +0 -1
- package/phanpy/dist/assets/icons/play-fill-B0-G_8lX.js +0 -1
- package/phanpy/dist/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
- package/phanpy/dist/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
- package/phanpy/dist/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
- package/phanpy/dist/assets/icons/radar-line-CKyroTth.js +0 -1
- package/phanpy/dist/assets/icons/react-line-CoIKhhtK.js +0 -1
- package/phanpy/dist/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
- package/phanpy/dist/assets/icons/right-line-B3KWJaqc.js +0 -1
- package/phanpy/dist/assets/icons/rocket-line-4KZl537D.js +0 -1
- package/phanpy/dist/assets/icons/round-fill-C69NTrZ0.js +0 -1
- package/phanpy/dist/assets/icons/round-line-B9k_1uo-.js +0 -1
- package/phanpy/dist/assets/icons/route-line-B28ReHcE.js +0 -1
- package/phanpy/dist/assets/icons/rows-4-line-CrBniagM.js +0 -1
- package/phanpy/dist/assets/icons/scan-line-DSf74YdP.js +0 -1
- package/phanpy/dist/assets/icons/search-2-line-B6geIU1z.js +0 -1
- package/phanpy/dist/assets/icons/settings-3-line-azJwK47w.js +0 -1
- package/phanpy/dist/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
- package/phanpy/dist/assets/icons/share-2-line-D5MxWjrm.js +0 -1
- package/phanpy/dist/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
- package/phanpy/dist/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
- package/phanpy/dist/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
- package/phanpy/dist/assets/icons/time-line-BF3eet1v.js +0 -1
- package/phanpy/dist/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
- package/phanpy/dist/assets/icons/translate-line-BFWol2Ce.js +0 -1
- package/phanpy/dist/assets/icons/unlock-line-X_x5vwv3.js +0 -1
- package/phanpy/dist/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
- package/phanpy/dist/assets/icons/user-4-line-BVSynZCp.js +0 -1
- package/phanpy/dist/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
- package/phanpy/dist/assets/icons/user-add-line-BbXdHG4z.js +0 -1
- package/phanpy/dist/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
- package/phanpy/dist/assets/icons/user-follow-line-BXux0wSd.js +0 -1
- package/phanpy/dist/assets/icons/user-star-line-DdYlpHLU.js +0 -1
- package/phanpy/dist/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
- package/phanpy/dist/assets/icons/user-x-line-DIDgNlPH.js +0 -1
- package/phanpy/dist/assets/icons/volume-line-Bs390KQt.js +0 -1
- package/phanpy/dist/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
- package/phanpy/dist/assets/icons/walk-line-yNXfB-md.js +0 -1
- package/phanpy/dist/assets/icons/world-2-line-CenkGIwj.js +0 -1
- package/phanpy/dist/assets/icons/zoom-in-line-CaENUphf.js +0 -1
- package/phanpy/dist/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
- package/phanpy/dist/assets/instances-BNPptnn-.json +0 -333
- package/phanpy/dist/assets/locales/ar-SA-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/ca-ES-DHRW86Tz.js +0 -1
- package/phanpy/dist/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
- package/phanpy/dist/assets/locales/de-DE-ByRPMRFy.js +0 -1
- package/phanpy/dist/assets/locales/eo-UY-CPC7wLCS.js +0 -1
- package/phanpy/dist/assets/locales/es-ES-dJa0Fd9t.js +0 -1
- package/phanpy/dist/assets/locales/eu-ES-C3djlPR2.js +0 -1
- package/phanpy/dist/assets/locales/fa-IR-DTT490Ck.js +0 -1
- package/phanpy/dist/assets/locales/fi-FI-B23MCPuz.js +0 -1
- package/phanpy/dist/assets/locales/fr-FR-BcaQsO5C.js +0 -1
- package/phanpy/dist/assets/locales/gl-ES-BR-CVKBN.js +0 -1
- package/phanpy/dist/assets/locales/he-IL-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/hu-HU-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/it-IT-C1136UFV.js +0 -1
- package/phanpy/dist/assets/locales/ja-JP-CRZmRAhD.js +0 -1
- package/phanpy/dist/assets/locales/kab-Cz8xH_TT.js +0 -1
- package/phanpy/dist/assets/locales/ko-KR-CG9UiY4o.js +0 -1
- package/phanpy/dist/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
- package/phanpy/dist/assets/locales/nb-NO-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
- package/phanpy/dist/assets/locales/oc-FR-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/pl-PL-DkwGXf6P.js +0 -1
- package/phanpy/dist/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
- package/phanpy/dist/assets/locales/pt-BR-nGL0HdFS.js +0 -1
- package/phanpy/dist/assets/locales/pt-PT-Cp17aj2z.js +0 -1
- package/phanpy/dist/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
- package/phanpy/dist/assets/locales/th-TH-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/tok-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/tr-TR-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/locales/uk-UA-C5E3dFBf.js +0 -1
- package/phanpy/dist/assets/locales/zh-CN-BHJi7_et.js +0 -1
- package/phanpy/dist/assets/locales/zh-TW-CfheRbCu.js +0 -1
- package/phanpy/dist/assets/main-BdqNbG-a.js +0 -26
- package/phanpy/dist/assets/main-BdqNbG-a.js.map +0 -1
- package/phanpy/dist/assets/mock-home-DGhR__He.js +0 -2
- package/phanpy/dist/assets/mock-home-DGhR__He.js.map +0 -1
- package/phanpy/dist/assets/multi-column-ROck0NVt.jpg +0 -0
- package/phanpy/dist/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
- package/phanpy/dist/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
- package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js +0 -2
- package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js.map +0 -1
- package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js +0 -20
- package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js.map +0 -1
- package/phanpy/dist/assets/style-B1iLPTPR.css +0 -2
- package/phanpy/dist/assets/temml-DuyIQMei.js +0 -97
- package/phanpy/dist/assets/temml-DuyIQMei.js.map +0 -1
- package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js +0 -2
- package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
- package/phanpy/dist/assets/year-in-posts-B2lywqdx.png +0 -0
- package/phanpy/dist/assets/year-in-posts-DJv_star.js +0 -3
- package/phanpy/dist/assets/year-in-posts-DJv_star.js.map +0 -1
- package/phanpy/dist/compose/index.html +0 -20
- package/phanpy/dist/favicon.ico +0 -0
- package/phanpy/dist/index.html +0 -73
- package/phanpy/dist/logo-192.png +0 -0
- package/phanpy/dist/logo-512.png +0 -0
- package/phanpy/dist/logo-badge-72.png +0 -0
- package/phanpy/dist/logo-maskable-512.png +0 -0
- package/phanpy/dist/logo-monochrome-512.png +0 -0
- package/phanpy/dist/logo-monochrome-maskable-512.png +0 -0
- package/phanpy/dist/manifest.webmanifest +0 -1
- package/phanpy/dist/og-image-2.jpg +0 -0
- package/phanpy/dist/og-image.png +0 -0
- package/phanpy/dist/robots.txt +0 -2
- package/phanpy/dist/sw.js +0 -2
- package/phanpy/dist/sw.js.map +0 -1
- package/phanpy/dist/version.json +0 -1
- package/run-agent.mjs +0 -899
- package/vendor/gate.cjs +0 -138
- package/vendor/idp-grant.cjs +0 -394
- package/vendor/jfetch.cjs +0 -16
- package/web/admin/actors.js +0 -147
- package/web/admin/bar.css +0 -126
- package/web/admin/bar.js +0 -99
- package/web/admin/client/client.js +0 -66
- package/web/admin/client/index.html +0 -69
- package/web/admin/client-pick.js +0 -79
- package/web/admin/common.js +0 -23
- package/web/admin/connections.js +0 -112
- package/web/admin/gateway.js +0 -133
- package/web/admin/group.js +0 -258
- package/web/admin/index.html +0 -612
- package/web/admin/notices-bar.js +0 -90
- package/web/admin/oauth-signin.mjs +0 -37
- package/web/admin/record.js +0 -393
- package/web/admin/setup/index.html +0 -257
- package/web/admin/setup/setup.js +0 -339
- package/web/admin/tokens.css +0 -68
- package/web/admin/upkeep.js +0 -177
- package/web/admin/window.css +0 -55
- package/web/admin/window.js +0 -136
- package/web/app/README.md +0 -81
- package/web/app/admin-facade.mjs +0 -588
- package/web/app/agent.mjs +0 -672
- package/web/app/atproto-browser.mjs +0 -86
- package/web/app/authorize_interaction.html +0 -56
- package/web/app/authorize_interaction.js +0 -69
- package/web/app/boot.mjs +0 -616
- package/web/app/deliver-relay.mjs +0 -129
- package/web/app/dist/boot.js +0 -34562
- package/web/app/dist/boot.js.map +0 -7
- package/web/app/dist/sw.js +0 -73456
- package/web/app/dist/sw.js.map +0 -7
- package/web/app/fediacct-browser.mjs +0 -146
- package/web/app/gateway-move.mjs +0 -60
- package/web/app/idb-kv.mjs +0 -35
- package/web/app/index.html +0 -258
- package/web/app/keys-browser.mjs +0 -71
- package/web/app/keystore.mjs +0 -82
- package/web/app/oidc-session.mjs +0 -6
- package/web/app/pod-auth.mjs +0 -248
- package/web/app/pod-remote.mjs +0 -84
- package/web/app/shims/_globals.mjs +0 -2
- package/web/app/shims/fedify-sig.mjs +0 -77
- package/web/app/shims/node-crypto.mjs +0 -113
- package/web/app/shims/node-fs.mjs +0 -17
- package/web/app/shims/node-path.mjs +0 -19
- package/web/app/shims/node-url.mjs +0 -7
- package/web/app/shims/prelude.js +0 -58
- package/web/app/shims/safefetch.mjs +0 -58
- package/web/app/shims/shapes-text.mjs +0 -8
- package/web/app/shims/web-push.mjs +0 -14
- package/web/app/signup.mjs +0 -357
- package/web/app/sw-src.mjs +0 -220
- package/web/app/update.js +0 -70
- package/web/front/admin.html +0 -55
- package/web/front/admin.js +0 -89
- package/web/front/install.sh +0 -69
- package/web/front/new-account.html +0 -35
- package/web/front/new-account.js +0 -13
- package/web/front/notices.html +0 -69
- package/web/front/notices.js +0 -107
- package/web/front/run.html +0 -64
- package/web/front/run.js +0 -221
- package/web/front/solid-oidc-client.js +0 -6
- package/web/icons/forum.svg +0 -11
- package/web/icons/gateway.svg +0 -14
- package/web/icons/local.svg +0 -11
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
// notes.mjs — a note on its way in: whether it concerns us, fetching it at
|
|
2
|
-
// its origin, filing it, the replies collection on our own posts, and the
|
|
3
|
-
// two ways a held post leaves again (forget, retract). Also the §7.1.2
|
|
4
|
-
// forward of a reply into one of our threads to our followers.
|
|
5
|
-
|
|
6
|
-
import * as podNotes from '../../pod/notes.mjs';
|
|
7
|
-
import { PUBLIC } from '../wire.mjs';
|
|
8
|
-
import * as polls from '../polls.mjs';
|
|
9
|
-
import { httpOnly, emojisOf, pollOf, isContentType, authorOf, quoteOf, quotePolicyOf, MAX_MENTIONS, MAX_URL_CHARS, MAX_REPLIES_RECORDED, MAX_FORWARDED, FORWARDABLE, FORWARD_TYPES } from './activity.mjs';
|
|
10
|
-
|
|
11
|
-
// Does this activity/note concern us at all? Either it comes from someone
|
|
12
|
-
// we follow, or it names us (to/cc, mention tag) or replies to one of our
|
|
13
|
-
// notes. Anything else is a stranger blasting inboxes — refuse it before
|
|
14
|
-
// spending a dereference on it.
|
|
15
|
-
export function concernsUs(intake, doc, actor) {
|
|
16
|
-
if (intake.store.getContacts().following.some(f => f.actor === actor && f.accepted)) return true;
|
|
17
|
-
const audience = []
|
|
18
|
-
.concat(doc?.to || [], doc?.cc || [], doc?.bto || [], doc?.bcc || [], doc?.audience || [])
|
|
19
|
-
.map(v => (typeof v === 'string' ? v : v?.id)).filter(Boolean);
|
|
20
|
-
if (audience.includes(intake.urls.actor) || audience.includes(intake.urls.followers)) return true;
|
|
21
|
-
const tagged = [].concat(doc?.tag || [])
|
|
22
|
-
.some(t => t?.type === 'Mention' && (t.href === intake.urls.actor || t.name?.includes(intake.urls.actor)));
|
|
23
|
-
if (tagged) return true;
|
|
24
|
-
// A post that quotes one of ours is about us as much as a reply is.
|
|
25
|
-
const quoted = quoteOf(doc);
|
|
26
|
-
if (quoted && quoted.startsWith(intake.urls.notes)) return true;
|
|
27
|
-
const inReplyTo = typeof doc?.inReplyTo === 'string' ? doc.inReplyTo : doc?.inReplyTo?.id;
|
|
28
|
-
if (!inReplyTo) return false;
|
|
29
|
-
if (String(inReplyTo).startsWith(intake.urls.notes)) return true;
|
|
30
|
-
// A group also owns the conversation under anything it carried. Without
|
|
31
|
-
// this, a reply that lost the group's mention on its way round the
|
|
32
|
-
// fediverse is refused, and the thread breaks for everyone who was only
|
|
33
|
-
// ever following the group.
|
|
34
|
-
return intake.config.kind === 'group'
|
|
35
|
-
&& intake.store.getStatuses().some(s => s.noteId === String(inReplyTo));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// §7.1.2 Forwarding from the inbox. A reply into one of our threads reaches
|
|
39
|
-
// only the servers the replier's server chose to deliver to — never our
|
|
40
|
-
// followers on servers it has never heard of. As the actor those followers
|
|
41
|
-
// follow, WE close that gap: an activity addressed to our followers collection
|
|
42
|
-
// that names one of our objects is re-delivered to our followers' inboxes.
|
|
43
|
-
//
|
|
44
|
-
// Only what was addressed to the followers COLLECTION is carried — bto/bcc are
|
|
45
|
-
// never read here, so a direct message (addressed to a person) never qualifies
|
|
46
|
-
// and is never rebroadcast. And this runs only after handle() accepted the
|
|
47
|
-
// activity, so anything blocked or muted was already refused upstream and is
|
|
48
|
-
// never forwarded.
|
|
49
|
-
export async function maybeForward(intake, activity) {
|
|
50
|
-
if (!activity || typeof activity !== 'object') return;
|
|
51
|
-
if (!FORWARDABLE.has(activity.type)) return; // see FORWARDABLE
|
|
52
|
-
// Forwarding signs somebody else's activity with OUR key and pushes it to
|
|
53
|
-
// every follower we have. That is a lot to do on the word of an unsigned
|
|
54
|
-
// POST, so it is narrowed three ways:
|
|
55
|
-
//
|
|
56
|
-
// 1. Only the types whose object this drain actually DEREFERENCED at the
|
|
57
|
-
// author's own origin. A Create/Update/Delete went through onCreate /
|
|
58
|
-
// onUpdate / onDelete, which fetch and check the author; a Like, an
|
|
59
|
-
// Announce or an Undo is believed on the envelope alone, so relaying
|
|
60
|
-
// one made us a signed relay for anything a stranger cared to write.
|
|
61
|
-
// 2. The actor has to be someone we already know of. A complete stranger
|
|
62
|
-
// addressing our followers collection is not a conversation we are
|
|
63
|
-
// party to.
|
|
64
|
-
// 3. A budget per drain, so a flood cannot turn one sweep into thousands
|
|
65
|
-
// of outbound deliveries under our signature.
|
|
66
|
-
if (!FORWARD_TYPES.has(activity.type)) return;
|
|
67
|
-
try {
|
|
68
|
-
const audience = []
|
|
69
|
-
.concat(activity.to || [], activity.cc || [], activity.audience || [])
|
|
70
|
-
.map(v => (typeof v === 'string' ? v : v?.id)).filter(Boolean);
|
|
71
|
-
if (!audience.includes(intake.urls.followers)) return; // not for our followers
|
|
72
|
-
if (!intake._referencesOurObject(activity)) return; // not into a thread of ours
|
|
73
|
-
const actor = typeof activity.actor === 'string' ? activity.actor : activity.actor?.id;
|
|
74
|
-
if (actor === intake.urls.actor) return; // our own; nothing to forward
|
|
75
|
-
if (!intake.known(actor)) {
|
|
76
|
-
intake.log(`not forwarding ${activity.type} from ${actor}: nobody we know of`);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (intake._forwardBudget <= 0) {
|
|
80
|
-
intake.log(`not forwarding ${activity.type}: this drain's forwarding budget is spent`);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
const id = typeof activity.id === 'string' ? activity.id : null;
|
|
84
|
-
if (!id) return;
|
|
85
|
-
const forwarded = intake.store.read('forwarded.json', []);
|
|
86
|
-
if (forwarded.includes(id)) return; // already done
|
|
87
|
-
|
|
88
|
-
const inboxes = [...new Set(intake.store.getContacts().followers
|
|
89
|
-
.filter(f => !f.bsky) // Bluesky members are not AP inboxes
|
|
90
|
-
.map(f => f.sharedInbox || f.inbox)
|
|
91
|
-
.filter(Boolean))];
|
|
92
|
-
if (!inboxes.length) return;
|
|
93
|
-
intake._forwardBudget -= 1;
|
|
94
|
-
// Blind copies are for their sender's delivery alone and never travel on
|
|
95
|
-
// (§6, §7.1), from the activity or its object.
|
|
96
|
-
const strip = (o) => { if (!o || typeof o !== 'object') return o; const { bto, bcc, ...rest } = o; return rest; }; // eslint-disable-line no-unused-vars
|
|
97
|
-
const onward = { ...strip(activity), ...(activity.object && typeof activity.object === 'object' ? { object: strip(activity.object) } : {}) };
|
|
98
|
-
await intake.deliverer.deliverToAll(inboxes, onward);
|
|
99
|
-
intake.store.write('forwarded.json', [...forwarded, id].slice(-MAX_FORWARDED));
|
|
100
|
-
intake.log(`forwarded ${activity.type} ${id} to ${inboxes.length} follower inbox(es)`);
|
|
101
|
-
} catch (e) {
|
|
102
|
-
intake.log(`inbox forwarding: ${e.message}`); // never stalls the drain
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// The "objects owned by the server" half of §7.1.2: does the activity reply
|
|
107
|
-
// to, like, boost or otherwise name one of our own objects?
|
|
108
|
-
export function referencesOurObject(intake, activity) {
|
|
109
|
-
const refs = [];
|
|
110
|
-
const add = (v) => { const id = typeof v === 'string' ? v : v?.id; if (id) refs.push(String(id)); };
|
|
111
|
-
const obj = activity.object;
|
|
112
|
-
if (obj && typeof obj === 'object') add(obj.inReplyTo); // a reply's parent
|
|
113
|
-
add(activity.inReplyTo);
|
|
114
|
-
add(activity.object); // a Like/Announce of our note
|
|
115
|
-
add(activity.target);
|
|
116
|
-
if (refs.some(r => r.startsWith(intake.urls.notes))) return true;
|
|
117
|
-
return intake.config.kind === 'group'
|
|
118
|
-
&& refs.some(r => intake.store.getStatuses().some(s => s.noteId === r));
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Shared tail of Create/Announce: deref the note at its origin (never trust
|
|
122
|
-
// the delivered copy), mirror it into pod RDF + statuses, notify on replies
|
|
123
|
-
// to our own notes. Returns a rejection reason string, or undefined.
|
|
124
|
-
export async function ingestNote(intake, objectId, actor, { via, inline = null } = {}) {
|
|
125
|
-
// `inline` is the note as delivered, offered only for a delivery whose
|
|
126
|
-
// signature the door verified (onCreate); otherwise the origin's copy.
|
|
127
|
-
const note = inline || await intake.fetchAP(objectId);
|
|
128
|
-
if (!note) return `object fetch failed (${objectId})`;
|
|
129
|
-
if (note.id !== objectId || !isContentType(note.type)) return `object not verifiable content (${objectId}, ${note.type})`;
|
|
130
|
-
const { attachmentsOf, titledContent } = await import('../wire.mjs');
|
|
131
|
-
const attachments = attachmentsOf(note);
|
|
132
|
-
const content = titledContent(note); // hostile markup never reaches pod or client
|
|
133
|
-
// The delivering actor was checked on arrival; the author is only known once
|
|
134
|
-
// the note is dereferenced. authorOf refuses an author the note's own origin
|
|
135
|
-
// does not vouch for — see its comment; this is where a forged attribution
|
|
136
|
-
// would otherwise become a timeline entry, a pod document, and for a group a
|
|
137
|
-
// signed Announce to every member.
|
|
138
|
-
const author = authorOf(note, actor);
|
|
139
|
-
if (!author) return `object names an author its origin does not vouch for (${objectId})`;
|
|
140
|
-
// The verified document, kept for whoever acts on this note after the
|
|
141
|
-
// carry: a forum caches it for its readers. Only when someone asked.
|
|
142
|
-
intake.recentNotes?.set(note.id, note);
|
|
143
|
-
// This is the check that catches a blocked actor reaching us through
|
|
144
|
-
// somebody else's boost, or through a hashtag feed.
|
|
145
|
-
if (intake.store.isBlocked(author)) return `blocked author (${author})`;
|
|
146
|
-
|
|
147
|
-
// The ENVELOPE said this concerns us. The envelope is a document a stranger
|
|
148
|
-
// wrote: onCreate reads addressing off the delivered copy to decide whether
|
|
149
|
-
// fetching is worth it, and nothing re-asked the question of the copy that
|
|
150
|
-
// came back from the author's own server. So anyone could take any public
|
|
151
|
-
// post, address the delivery to us, and have it filed as "X mentioned you"
|
|
152
|
-
// — or, to a group, have a member's post carried to every follower when the
|
|
153
|
-
// member never sent it there.
|
|
154
|
-
//
|
|
155
|
-
// A boost is exempt (`via`): someone we follow deliberately putting a post
|
|
156
|
-
// in front of us is the whole point, and the note will not address us.
|
|
157
|
-
if (!via && !intake.concernsUs(note, author)) {
|
|
158
|
-
return `the note its own server serves does not address us (${objectId})`;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// An answer to one of our polls is a number on a document, not a post. It
|
|
162
|
-
// arrives as an ordinary reply naming an option and carrying nothing else,
|
|
163
|
-
// so filing it as one would put a blank entry in the thread and ring the
|
|
164
|
-
// owner once per voter. Counted or refused — a second answer, an option we
|
|
165
|
-
// do not offer, a poll already shut — it stops here either way.
|
|
166
|
-
const asked = note.inReplyTo && intake.store.getStatuses()
|
|
167
|
-
.find(x => x.noteId === String(note.inReplyTo) && x.kind === 'post' && x.poll);
|
|
168
|
-
if (asked && polls.isVoteShape(note)) {
|
|
169
|
-
const counted = await intake.publisher.recordVote(asked.noteId, author, note.name)
|
|
170
|
-
.catch(e => { intake.log(`vote on ${asked.noteId}: ${e.message}`); return false; });
|
|
171
|
-
intake.log(counted
|
|
172
|
-
? `vote counted (${note.name}): ${asked.noteId}`
|
|
173
|
-
: `vote not counted (${note.name}) from ${author}: ${asked.noteId}`);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Anyone can Append to a public inbox, so arriving is not the same as
|
|
178
|
-
// belonging in the home timeline. Follow Mastodon's split: people you
|
|
179
|
-
// follow (and their boosts) are HOME; anyone else is a MENTION — kept,
|
|
180
|
-
// notified, readable in the Mentions view, but out of the timeline, and
|
|
181
|
-
// mirror-only so unsolicited content never accumulates in the pod.
|
|
182
|
-
// A group's people are its FOLLOWERS — it follows nobody. Reading the
|
|
183
|
-
// following list for one filed every member's post as a stranger's mention,
|
|
184
|
-
// so nothing reached the pod RDF and each post raised a notification.
|
|
185
|
-
const contacts = intake.store.getContacts();
|
|
186
|
-
const known = intake.config.kind === 'group'
|
|
187
|
-
? contacts.followers.some(f => f.actor === author)
|
|
188
|
-
: contacts.following.some(f => f.actor === author && f.accepted);
|
|
189
|
-
// Someone in a group you are in is not a stranger: their reply belongs in
|
|
190
|
-
// the room, not in the drawer of unsolicited mail. Whose word this is on
|
|
191
|
-
// is the group's — its published membership — so the group's own door
|
|
192
|
-
// decides who gets in.
|
|
193
|
-
const followed = via || known || (!known && await intake.isCoMember(author));
|
|
194
|
-
const kind = followed ? 'timeline' : 'mention';
|
|
195
|
-
|
|
196
|
-
// Mastodon carries a thread's mentions into every reply, which is the only
|
|
197
|
-
// reason a reply ever reaches a group. Keep them so our composer can too.
|
|
198
|
-
const mentions = [].concat(note.tag || [])
|
|
199
|
-
.filter(t => t?.type === 'Mention' && t.href && t.name)
|
|
200
|
-
.slice(0, MAX_MENTIONS)
|
|
201
|
-
.map(t => ({ href: httpOnly(String(t.href).slice(0, MAX_URL_CHARS)), name: String(t.name).slice(0, 256) }))
|
|
202
|
-
.filter(m => m.href);
|
|
203
|
-
const hashtags = [].concat(note.tag || [])
|
|
204
|
-
.filter(t => t?.type === 'Hashtag' && t.name)
|
|
205
|
-
.slice(0, MAX_MENTIONS)
|
|
206
|
-
.map(t => ({ name: String(t.name).replace(/^#/, '').slice(0, 128), url: httpOnly(String(t.href || '').slice(0, MAX_URL_CHARS)) }))
|
|
207
|
-
.filter(t => t.name);
|
|
208
|
-
const emojis = emojisOf(note);
|
|
209
|
-
const poll = pollOf(note);
|
|
210
|
-
// What the note quotes, and what its author lets others quote. The quoted
|
|
211
|
-
// post is fetched and kept so the quote can be shown whole; whether the
|
|
212
|
-
// quote was allowed is checked, never assumed.
|
|
213
|
-
const quote = quoteOf(note);
|
|
214
|
-
const quotePolicy = quotePolicyOf(note, author);
|
|
215
|
-
const quoteState = quote ? await intake.quoteStateOf(note, quote) : null;
|
|
216
|
-
if (quote) await holdQuoted(intake, quote);
|
|
217
|
-
// Explicitly addressed, but to nobody public and to no followers
|
|
218
|
-
// collection: a direct message, which belongs to the conversations view
|
|
219
|
-
// rather than a timeline. A note with no addressing at all is NOT direct —
|
|
220
|
-
// some servers omit to/cc, and vanishing from home is the wrong reading.
|
|
221
|
-
const audience = [].concat(note.to || [], note.cc || []).map(String);
|
|
222
|
-
const direct = audience.length > 0
|
|
223
|
-
&& !audience.includes(PUBLIC) && !audience.some(a => a.endsWith('/followers'));
|
|
224
|
-
// Addressed to less than the world: whatever else happens to it, a group
|
|
225
|
-
// must never widen its audience by carrying it.
|
|
226
|
-
const nonPublic = audience.length > 0 && !audience.includes(PUBLIC);
|
|
227
|
-
intake.store.addStatus({
|
|
228
|
-
noteId: note.id, actor: author, content,
|
|
229
|
-
published: note.published, inReplyTo: note.inReplyTo, kind,
|
|
230
|
-
...(direct ? { direct: true } : {}),
|
|
231
|
-
...(nonPublic ? { nonPublic: true } : {}),
|
|
232
|
-
// The author's content warning, shown as one: plain text only.
|
|
233
|
-
...(note.summary ? { spoiler: String(note.summary).replace(/<[^>]*>/g, '') } : {}),
|
|
234
|
-
...(note.sensitive === true ? { sensitive: true } : {}),
|
|
235
|
-
...(hashtags.length ? { tags: hashtags } : {}),
|
|
236
|
-
...(poll ? { poll } : {}),
|
|
237
|
-
...(emojis.length ? { emojis } : {}),
|
|
238
|
-
...(mentions.length ? { mentions } : {}),
|
|
239
|
-
...(attachments.length ? { attachments } : {}),
|
|
240
|
-
...(quote ? { quote, quoteState } : {}),
|
|
241
|
-
...(quotePolicy ? { quotePolicy } : {}),
|
|
242
|
-
...(via ? { via } : {}),
|
|
243
|
-
});
|
|
244
|
-
// A notification for anything addressed to this person: a stranger's
|
|
245
|
-
// post, a reply to one of ours, a direct message, or a post that names
|
|
246
|
-
// us — whoever sent it. A direct message from someone followed used to
|
|
247
|
-
// raise nothing and sat unseen in the private mentions. A post quoting
|
|
248
|
-
// one of ours is its own kind of notification.
|
|
249
|
-
const namesUs = mentions.some((m) => m.href === intake.urls.actor);
|
|
250
|
-
const replyToOurs = !!note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes);
|
|
251
|
-
const quotesUs = !!quote && quote.startsWith(intake.urls.notes);
|
|
252
|
-
if (quotesUs) {
|
|
253
|
-
intake.store.addNotification({ type: 'quote', actor: author, noteId: note.id });
|
|
254
|
-
} else if (!followed || replyToOurs || direct || namesUs) {
|
|
255
|
-
intake.store.addNotification({ type: 'mention', actor: author, noteId: note.id });
|
|
256
|
-
}
|
|
257
|
-
if (note.inReplyTo && String(note.inReplyTo).startsWith(intake.urls.notes)) {
|
|
258
|
-
await intake.addReply(String(note.inReplyTo), note.id)
|
|
259
|
-
.catch(e => intake.log(`replies collection: ${e.message}`));
|
|
260
|
-
}
|
|
261
|
-
intake.log(`${kind}: ${note.id}${via ? ` (boosted by ${via})` : ''}`);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// The post a quote shows: fetched at its origin and kept out of every
|
|
265
|
-
// timeline (`remote`), the way a thread's ancestors are — it is there to be
|
|
266
|
-
// shown inside the quoting post, not on its own. Nothing is fetched twice,
|
|
267
|
-
// and nothing is followed further: a quote of a quote shows one level.
|
|
268
|
-
export async function holdQuoted(intake, quoteId) {
|
|
269
|
-
if (intake.store.getStatuses().some(s => s.noteId === quoteId)) return;
|
|
270
|
-
if (intake.store.isBlocked(quoteId)) return;
|
|
271
|
-
const q = await intake.fetchAP(quoteId).catch(() => null);
|
|
272
|
-
if (!q || q.id !== quoteId || !isContentType(q.type)) {
|
|
273
|
-
intake.log(`quoted post ${quoteId} could not be read — the quote shows without it`);
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
const by = authorOf(q, null);
|
|
277
|
-
if (!by || intake.store.isBlocked(by)) return;
|
|
278
|
-
const { attachmentsOf, titledContent } = await import('../wire.mjs');
|
|
279
|
-
const attachments = attachmentsOf(q);
|
|
280
|
-
const emojis = emojisOf(q);
|
|
281
|
-
const poll = pollOf(q);
|
|
282
|
-
const quotePolicy = quotePolicyOf(q, by);
|
|
283
|
-
const mentions = [].concat(q.tag || [])
|
|
284
|
-
.filter(t => t?.type === 'Mention' && t.href && t.name)
|
|
285
|
-
.slice(0, MAX_MENTIONS)
|
|
286
|
-
.map(t => ({ href: httpOnly(String(t.href).slice(0, MAX_URL_CHARS)), name: String(t.name).slice(0, 256) }))
|
|
287
|
-
.filter(m => m.href);
|
|
288
|
-
intake.store.addStatus({
|
|
289
|
-
noteId: q.id, actor: by, content: titledContent(q),
|
|
290
|
-
published: q.published, inReplyTo: q.inReplyTo, kind: 'remote',
|
|
291
|
-
...(q.summary ? { spoiler: String(q.summary).replace(/<[^>]*>/g, '') } : {}),
|
|
292
|
-
...(q.sensitive === true ? { sensitive: true } : {}),
|
|
293
|
-
...(poll ? { poll } : {}),
|
|
294
|
-
...(emojis.length ? { emojis } : {}),
|
|
295
|
-
...(mentions.length ? { mentions } : {}),
|
|
296
|
-
...(attachments.length ? { attachments } : {}),
|
|
297
|
-
...(quotePolicy ? { quotePolicy } : {}),
|
|
298
|
-
});
|
|
299
|
-
intake.log(`quoted post kept: ${q.id}`);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Drop a post we were holding. A group that carried it also unsays its own
|
|
303
|
-
// Announce — forwarding the author's Delete would be signed by us and not by
|
|
304
|
-
// them, which receivers are right to refuse.
|
|
305
|
-
// `collect` batches the outbox side: retract pushes the Announce id onto it
|
|
306
|
-
// instead of republishing, and the caller writes once for all of them.
|
|
307
|
-
export async function forget(intake, s, { collect = null } = {}) {
|
|
308
|
-
if (s.announceActivity) {
|
|
309
|
-
await intake.retract(s.noteId, { collect }).catch(e => intake.log(`retract: ${e.message}`));
|
|
310
|
-
}
|
|
311
|
-
intake.store.removeStatus(s.noteId);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Undo an Announce this group made. Shared with the operator's `retract`.
|
|
315
|
-
export async function retract(intake, noteId, { collect = null } = {}) {
|
|
316
|
-
const s = intake.store.getStatuses().find(x => x.noteId === noteId);
|
|
317
|
-
if (!s) throw new Error('no such post');
|
|
318
|
-
// A Bluesky carry is a repost, and unsaying it is deleting the repost.
|
|
319
|
-
if (s.repostUri) {
|
|
320
|
-
if (!intake.bskyGroup) throw new Error('no bluesky account connected');
|
|
321
|
-
return intake.bskyGroup.retract(s);
|
|
322
|
-
}
|
|
323
|
-
if (!s.announceActivity) throw new Error('that post was never carried');
|
|
324
|
-
const { undoActivity } = await import('../wire.mjs');
|
|
325
|
-
const inboxes = intake.announceTargets(s.actor);
|
|
326
|
-
const undo = undoActivity({ urls: intake.urls, activity: s.announceActivity, serial: intake.serial++ });
|
|
327
|
-
await intake.deliverer.deliverToAll(inboxes, undo);
|
|
328
|
-
// The Undo takes the carry's place in the outbox, so a reader catching up
|
|
329
|
-
// from it learns the carry was withdrawn.
|
|
330
|
-
if (collect) collect.push(undo);
|
|
331
|
-
else await intake.publisher.unrecordOutbox(i => i?.id === s.announceActivity.id,
|
|
332
|
-
{ record: () => (intake.config.private ? null : undo) });
|
|
333
|
-
intake.store.updateStatus(noteId, {
|
|
334
|
-
announcedAt: undefined, announceActivity: undefined, retractedAt: new Date().toISOString(),
|
|
335
|
-
});
|
|
336
|
-
return { ok: true, noteId, inboxes: inboxes.length };
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Read-modify-write, and the drain is serialized, so two replies in one sweep
|
|
340
|
-
// do not race. Nothing else writes this document.
|
|
341
|
-
// MAX_REPLIES_RECORDED caps the collection. It is a discovery aid — a client
|
|
342
|
-
// reading a thread — and the statuses index is what actually holds the
|
|
343
|
-
// replies, so dropping the oldest costs a hop, not a record.
|
|
344
|
-
export async function addReply(intake, parentId, replyId) {
|
|
345
|
-
// The parent has to be a post we actually made. The only check used to be
|
|
346
|
-
// that the id started with our notes prefix, and `inReplyTo` is read off a
|
|
347
|
-
// document at the sender's own origin — so a stranger could name a note we
|
|
348
|
-
// never wrote, and we would CREATE a document on the pod at a URL of their
|
|
349
|
-
// choosing and then grow it, one whole re-PUT per reply, with no cap. Four
|
|
350
|
-
// pod requests each and bytes quadratic in the number of replies.
|
|
351
|
-
if (!intake.store.getStatuses().some(s => s.noteId === parentId && s.kind === 'post')) {
|
|
352
|
-
intake.log(`reply names ${parentId}, which is not a post of ours — not recorded`);
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
const { repliesId, collection } = await import('../wire.mjs');
|
|
356
|
-
const url = repliesId(parentId);
|
|
357
|
-
// Deliberately NOT caught: a read we could not make is not an empty
|
|
358
|
-
// collection, and rewriting on top of one erases every reply already
|
|
359
|
-
// recorded. getJson returns null only for a genuine 404 — the document does
|
|
360
|
-
// not exist yet — and throws otherwise, which the caller logs and retries.
|
|
361
|
-
const cur = await podNotes.readReplies(intake.remote, url);
|
|
362
|
-
const items = Array.isArray(cur?.items) ? cur.items : [];
|
|
363
|
-
if (items.includes(replyId)) return;
|
|
364
|
-
items.push(replyId);
|
|
365
|
-
await podNotes.writeReplies(intake.remote, url, collection(url, items.slice(-MAX_REPLIES_RECORDED)));
|
|
366
|
-
intake.log(`reply recorded on ${parentId}`);
|
|
367
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
// verify.mjs — how the drain decides what to believe: a signed fetch of the
|
|
2
|
-
// claimed document at its own origin, what local state already knows of, and
|
|
3
|
-
// the receipt a gateway left beside a delivery.
|
|
4
|
-
|
|
5
|
-
import * as podInbox from '../../pod/inbox.mjs';
|
|
6
|
-
import { readCapped } from '../../shared/safefetch.mjs';
|
|
7
|
-
import { isActorType, sameOrigin, ACCEPT_AP } from './activity.mjs';
|
|
8
|
-
import { readLenient } from '../as2.mjs';
|
|
9
|
-
|
|
10
|
-
export async function fetchAP(intake, url) {
|
|
11
|
-
const res = await intake.deliverer.signedFetch(url, { headers: { accept: ACCEPT_AP } });
|
|
12
|
-
if (res.status >= 400) return null;
|
|
13
|
-
// Remote servers are untrusted: read with a byte budget rather than
|
|
14
|
-
// letting res.json() buffer whatever they choose to send.
|
|
15
|
-
const { readCapped } = await import('../../shared/safefetch.mjs');
|
|
16
|
-
// Plenty of servers answer 200 text/html however politely we ask for AS2 —
|
|
17
|
-
// people reply to ordinary web pages, and their id is that page. Say so,
|
|
18
|
-
// rather than handing the HTML to JSON.parse and logging the parser's
|
|
19
|
-
// complaint about an unexpected `<`.
|
|
20
|
-
// Not logged: people reply to ordinary web pages, so the reply's object id
|
|
21
|
-
// is that page and this is the expected answer, not a fault. Only a server
|
|
22
|
-
// that CLAIMS to be sending JSON and then does not is worth a line.
|
|
23
|
-
const ct = (res.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
|
|
24
|
-
if (ct && !ct.endsWith('json')) return null;
|
|
25
|
-
let doc = null;
|
|
26
|
-
try {
|
|
27
|
-
const read = await readLenient(await readCapped(res));
|
|
28
|
-
if (read.degraded) intake.log(`fetch ${url} grounded to read: ${read.degraded}`);
|
|
29
|
-
doc = read.view ?? read.doc;
|
|
30
|
-
} catch (e) { intake.log(`fetch ${url}: unreadable — ${e.message}`); return null; }
|
|
31
|
-
if (!doc) { intake.log(`fetch ${url}: unreadable as JSON`); return null; }
|
|
32
|
-
// A document is only evidence about its OWN origin.
|
|
33
|
-
//
|
|
34
|
-
// signedFetch follows redirects and hands back only the final response, and
|
|
35
|
-
// every caller checks `doc.id` against the URL it ASKED for — so an open
|
|
36
|
-
// redirector on a victim's origin let an attacker's server answer for a
|
|
37
|
-
// victim's URL and be believed by that check.
|
|
38
|
-
//
|
|
39
|
-
// Only a redirect that CROSSED AN ORIGIN is refused here. A same-origin
|
|
40
|
-
// redirect is ordinary (canonicalisation, trailing slashes), and a document
|
|
41
|
-
// fetched with no redirect at all is still returned whatever it claims —
|
|
42
|
-
// the callers reject it on id, and they say something more useful about it
|
|
43
|
-
// than this could ("actor id mismatch", "object not verifiable content").
|
|
44
|
-
const landed = res.finalUrl || url;
|
|
45
|
-
if (!sameOrigin(landed, url) && doc && doc.id && !sameOrigin(doc.id, landed)) {
|
|
46
|
-
intake.log(`fetch ${url}: redirected to ${landed}, which is not where ${doc.id} lives — refused`);
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
// Every actor type, not just Person. A Group was fetched, used and thrown
|
|
50
|
-
// away, so nothing knew its preferredUsername — and a client rendering it
|
|
51
|
-
// fell back to the last path segment of the actor URL, which is the literal
|
|
52
|
-
// word `actor`. That is where @actor@host came from.
|
|
53
|
-
// Under the id the document CLAIMS, but only when its own origin vouches
|
|
54
|
-
// for that id. A stranger's actor document naming someone else's id used to
|
|
55
|
-
// overwrite that actor's cached name, bio, avatar and Person/Group flag —
|
|
56
|
-
// one appended Follow was enough, and the id-mismatch checks in onFollow
|
|
57
|
-
// and ingestNote both run after this line and never undid it.
|
|
58
|
-
//
|
|
59
|
-
// Same origin rather than exact equality: signedFetch follows redirects
|
|
60
|
-
// without reporting where it landed, so a server that redirects its own
|
|
61
|
-
// canonical actor URL would otherwise stop being cached at all.
|
|
62
|
-
if (isActorType(doc?.type) && doc.id && sameOrigin(doc.id, url)) {
|
|
63
|
-
intake.store.cacheActor(doc.id, doc);
|
|
64
|
-
}
|
|
65
|
-
return doc;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Have we ever heard of this actor or object? Answered entirely from local
|
|
69
|
-
// state, so asking costs nothing. It is what stops a stranger's Delete or
|
|
70
|
-
// Update — of which Mastodon broadcasts a great many, and of which anyone at
|
|
71
|
-
// all can Append one — turning into a signed request to a host they chose.
|
|
72
|
-
export function known(intake, id) {
|
|
73
|
-
const c = intake.store.getContacts();
|
|
74
|
-
return c.followers.some(f => f.actor === id)
|
|
75
|
-
|| c.following.some(f => f.actor === id)
|
|
76
|
-
|| intake.store.getStatuses().some(s => s.noteId === id || s.actor === id)
|
|
77
|
-
|| !!intake.store.getActors()[id];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Returns a rejection reason string, or undefined when handled.
|
|
81
|
-
// The gateway's shared HMAC secret, or null when no gateway is configured.
|
|
82
|
-
// Its absence is what makes the whole receipt path dormant by default.
|
|
83
|
-
export function gatewaySecret(intake) {
|
|
84
|
-
return intake.store.getConfig()?.gateway?.hmacSecret || null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Shadow-mode measurement: how much real traffic actually verified. The one
|
|
88
|
-
// number an operator needs before trusting receipts. Only touched while a
|
|
89
|
-
// gateway is configured, so it costs a non-gateway install nothing.
|
|
90
|
-
export function bumpGatewayStat(intake, verified) {
|
|
91
|
-
const s = intake.store.read('gateway-stats.json', { verified: 0, unverified: 0 });
|
|
92
|
-
if (verified) s.verified++; else s.unverified++;
|
|
93
|
-
s.lastAt = new Date().toISOString();
|
|
94
|
-
intake.store.write('gateway-stats.json', s);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Read and authenticate the receipt a gateway wrote beside an inbox item.
|
|
98
|
-
// Returns the receipt object only when its HMAC verifies against our secret;
|
|
99
|
-
// null otherwise (no gateway, no receipt, a stranger's forged one, or a read
|
|
100
|
-
// failure) — and null means "unverified", the pre-gateway behavior.
|
|
101
|
-
export async function readReceipt(intake, itemUrl) {
|
|
102
|
-
const secret = intake.gatewaySecret();
|
|
103
|
-
if (!secret) return null;
|
|
104
|
-
try {
|
|
105
|
-
const { readCapped: cap } = await import('../../shared/safefetch.mjs');
|
|
106
|
-
const receipt = await podInbox.readDeliveryReceipt(intake.remote, itemUrl, { maxBytes: 64 * 1024, readCapped: cap });
|
|
107
|
-
if (!receipt) return null;
|
|
108
|
-
const { verifyReceipt } = await import('../../gateway/httpsig.mjs');
|
|
109
|
-
return verifyReceipt(receipt, secret) ? receipt : null;
|
|
110
|
-
} catch { return null; }
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Whether a receipt says anything about THIS actor. Verified-and-about-someone
|
|
114
|
-
// -else is worth exactly as much as unverified, and is treated the same way:
|
|
115
|
-
// the drain's verify-by-dereference still stands behind it.
|
|
116
|
-
export function receiptVouchesFor(intake, receipt, actor) {
|
|
117
|
-
if (!receipt?.verified) return false;
|
|
118
|
-
if (!receipt.actor || receipt.actor !== actor) return false;
|
|
119
|
-
// A missing keyId with verified:true cannot come from our own door
|
|
120
|
-
// (makeReceipt fills both from the same key), so refuse rather than waive.
|
|
121
|
-
if (!receipt.keyId || !sameOrigin(receipt.keyId, actor)) return false;
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Is this really gone at its origin? true / false / null when the origin
|
|
126
|
-
// could not be asked. Delivered bodies carry no signature, so this is how a
|
|
127
|
-
// Delete is verified — the same verify-by-dereference the rest of intake uses.
|
|
128
|
-
export async function isGone(intake, url) {
|
|
129
|
-
let res;
|
|
130
|
-
try { res = await intake.deliverer.signedFetch(url, { headers: { accept: ACCEPT_AP } }); }
|
|
131
|
-
catch { return null; }
|
|
132
|
-
if (res.status === 404 || res.status === 410) return true;
|
|
133
|
-
if (res.status < 400) {
|
|
134
|
-
// A Tombstone answers 200 and still means deleted.
|
|
135
|
-
try {
|
|
136
|
-
const { readCapped } = await import('../../shared/safefetch.mjs');
|
|
137
|
-
const read = await readLenient(await readCapped(res));
|
|
138
|
-
return (read.view ?? read.doc)?.type === 'Tombstone';
|
|
139
|
-
} catch { return false; }
|
|
140
|
-
}
|
|
141
|
-
return null; // 401/403/5xx — no answer, not a denial
|
|
142
|
-
}
|
package/lib/core/keys.mjs
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
// keys.mjs — actor signing keys. RSA (Mastodon's draft-cavage HTTP Signatures
|
|
2
|
-
// require RSA-SHA256) signs deliveries; Ed25519 signs the FEP-8b32 proof that
|
|
3
|
-
// travels with an activity. PEM at rest (0600), CryptoKey in memory for
|
|
4
|
-
// Fedify's signRequest and for the proof.
|
|
5
|
-
//
|
|
6
|
-
// The key lives on THIS MACHINE by default (AP_HOME/keys.json): the pod host
|
|
7
|
-
// then never holds it. `setup --keys pod` puts it in pod state instead,
|
|
8
|
-
// which is what lets several devices sign as the same actor without copying
|
|
9
|
-
// anything — see resolveKeys() for the guard that keeps the two modes from
|
|
10
|
-
// silently breaking each other.
|
|
11
|
-
|
|
12
|
-
import crypto from 'node:crypto';
|
|
13
|
-
import fs from 'node:fs';
|
|
14
|
-
import path from 'node:path';
|
|
15
|
-
import { writeJsonAtomic } from '../device/home.mjs';
|
|
16
|
-
|
|
17
|
-
const RSA_ALG = { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' };
|
|
18
|
-
|
|
19
|
-
const localPath = (dir) => path.join(dir, 'keys.json');
|
|
20
|
-
const readLocal = (dir) => {
|
|
21
|
-
try { return JSON.parse(fs.readFileSync(localPath(dir), 'utf8')); } catch { return null; }
|
|
22
|
-
};
|
|
23
|
-
// Atomic: this is the private key remote servers have cached, and a truncated
|
|
24
|
-
// one cannot be recovered from anywhere — the pod does not hold it.
|
|
25
|
-
const writeLocal = (dir, rec) => {
|
|
26
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
27
|
-
writeJsonAtomic(localPath(dir), rec);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
function generate() {
|
|
31
|
-
const rsa = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
|
|
32
|
-
const ed = crypto.generateKeyPairSync('ed25519');
|
|
33
|
-
return {
|
|
34
|
-
rsa: {
|
|
35
|
-
publicPem: rsa.publicKey.export({ type: 'spki', format: 'pem' }),
|
|
36
|
-
privatePem: rsa.privateKey.export({ type: 'pkcs8', format: 'pem' }),
|
|
37
|
-
},
|
|
38
|
-
ed25519: {
|
|
39
|
-
publicPem: ed.publicKey.export({ type: 'spki', format: 'pem' }),
|
|
40
|
-
privatePem: ed.privateKey.export({ type: 'pkcs8', format: 'pem' }),
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Find or create the key record.
|
|
47
|
-
*
|
|
48
|
-
* The dangerous case is minting a NEW key when the actor already advertises
|
|
49
|
-
* one: remote servers cache the public key, so a fresh keypair silently
|
|
50
|
-
* invalidates every signature until the actor document is republished — and
|
|
51
|
-
* then breaks whichever other device holds the old key. So when key material
|
|
52
|
-
* is missing but the actor doc already has a key, we refuse and explain,
|
|
53
|
-
* unless the caller passes rotate:true deliberately.
|
|
54
|
-
*
|
|
55
|
-
* @param {object} store pod-backed state store
|
|
56
|
-
* @param {object} o
|
|
57
|
-
* @param {string|null} o.localDir AP_HOME when keys are local (the default)
|
|
58
|
-
* @param {boolean} o.rotate mint a replacement on purpose
|
|
59
|
-
* @param {function} [o.actorHasKey] async () => boolean, consulted only when
|
|
60
|
-
* no key material exists anywhere
|
|
61
|
-
* @param {function} [o.log]
|
|
62
|
-
*/
|
|
63
|
-
export async function resolveKeys(store, { localDir = null, rotate = false, actorHasKey = null, actorId = null, log = () => {} } = {}) {
|
|
64
|
-
let rec = localDir ? readLocal(localDir) : null;
|
|
65
|
-
|
|
66
|
-
// A key belongs to ONE actor. Adopting whatever keys.json happens to be in
|
|
67
|
-
// the directory is right within an identity and wrong across two: it is how
|
|
68
|
-
// a second setup in the same AP_HOME ended up signing as a different actor
|
|
69
|
-
// with the first one's key, which links the two publicly for anyone who
|
|
70
|
-
// compares the actor documents. A stamped key that names another actor is
|
|
71
|
-
// treated as absent, so the mint path (and its actorHasKey guard) applies.
|
|
72
|
-
if (rec && actorId && rec.mintedFor && rec.mintedFor !== actorId) {
|
|
73
|
-
log(`local keys.json belongs to ${rec.mintedFor} — not reusing it for ${actorId}`);
|
|
74
|
-
rec = null;
|
|
75
|
-
} else if (rec && actorId && !rec.mintedFor) {
|
|
76
|
-
// Written before keys were stamped: keep working, but say so, because it
|
|
77
|
-
// may be another actor's key.
|
|
78
|
-
log('keys.json predates per-identity stamping — if this actor was set up in a '
|
|
79
|
-
+ 'home that already held a key, `setup --rotate-key` mints one of its own');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!rec) {
|
|
83
|
-
const podRec = store.read('keys.json', null);
|
|
84
|
-
if (podRec && localDir) {
|
|
85
|
-
// Migrating an existing actor to local keys: adopt the pod's key so
|
|
86
|
-
// the identity is preserved, then remove the pod's copy — leaving it
|
|
87
|
-
// would defeat the point of going local.
|
|
88
|
-
writeLocal(localDir, podRec);
|
|
89
|
-
rec = podRec;
|
|
90
|
-
const gone = await store.remove('keys.json');
|
|
91
|
-
log(gone
|
|
92
|
-
? `signing key moved to ${localPath(localDir)} and deleted from pod state`
|
|
93
|
-
: `signing key copied to ${localPath(localDir)} — could NOT delete the pod copy, remove it by hand`);
|
|
94
|
-
} else {
|
|
95
|
-
rec = podRec;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (!rec && !rotate && actorHasKey && await actorHasKey()) {
|
|
100
|
-
throw new Error(
|
|
101
|
-
'this actor already publishes a signing key, but no key material is present here.\n'
|
|
102
|
-
+ ` Copy keys.json from your other device into ${localDir || 'pod state'}, or\n`
|
|
103
|
-
+ ' re-run setup with --keys pod to share the key through the pod, or\n'
|
|
104
|
-
// --force, because rotate-key connects before it rotates and would meet
|
|
105
|
-
// this same refusal: advice has to name a command that can actually run.
|
|
106
|
-
+ ' run `fedipod rotate-key --force` to replace the published key (breaks other devices).');
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (!rec || rotate) {
|
|
110
|
-
rec = { ...generate(), ...(actorId ? { mintedFor: actorId } : {}) };
|
|
111
|
-
if (localDir) writeLocal(localDir, rec); else store.write('keys.json', rec);
|
|
112
|
-
log(rotate ? 'minted a REPLACEMENT signing key — republish the profile' : 'minted a signing key');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// A record written before proofs existed has no Ed25519 half. Minting one is
|
|
116
|
-
// safe in a way minting an RSA key is not: nothing has ever published or
|
|
117
|
-
// cached it, so no remote signature is invalidated.
|
|
118
|
-
if (!rec.ed25519) {
|
|
119
|
-
rec = { ...rec, ed25519: generate().ed25519 };
|
|
120
|
-
if (localDir) writeLocal(localDir, rec); else store.write('keys.json', rec);
|
|
121
|
-
log('minted the Ed25519 half of the signing key');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const der = crypto.createPrivateKey(rec.rsa.privatePem).export({ type: 'pkcs8', format: 'der' });
|
|
125
|
-
const rsaPrivate = await crypto.subtle.importKey('pkcs8', der, RSA_ALG, true, ['sign']);
|
|
126
|
-
const { edPrivateKey, multibaseEd25519 } = await import('./proof.mjs');
|
|
127
|
-
return {
|
|
128
|
-
rsaPrivate, rsaPublicPem: rec.rsa.publicPem,
|
|
129
|
-
edPrivate: await edPrivateKey(rec.ed25519.privatePem),
|
|
130
|
-
edPublicMultibase: multibaseEd25519(rec.ed25519.publicPem),
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Move a local key into pod state — the arrangement that lets several
|
|
135
|
-
// devices sign as one actor once the state store is a pod they all reach.
|
|
136
|
-
// The pod write is forced and checked BEFORE the local file goes: a key
|
|
137
|
-
// remote servers have cached cannot be recovered from anywhere else.
|
|
138
|
-
export async function moveKeysToPod(store, { localDir, actorId = null, log = () => {} }) {
|
|
139
|
-
const rec = readLocal(localDir);
|
|
140
|
-
if (!rec) throw new Error(`no local key at ${localPath(localDir)} — nothing to move`);
|
|
141
|
-
if (actorId && rec.mintedFor && rec.mintedFor !== actorId) {
|
|
142
|
-
throw new Error(`local keys.json belongs to ${rec.mintedFor}, not ${actorId} — refusing to move it`);
|
|
143
|
-
}
|
|
144
|
-
store.write('keys.json', rec);
|
|
145
|
-
const landed = store.commit ? await store.commit() : await store.flush?.();
|
|
146
|
-
if (landed === false) throw new Error('the pod write did not land — the local key is untouched');
|
|
147
|
-
fs.rmSync(localPath(localDir), { force: true });
|
|
148
|
-
log('signing key moved to pod state and removed from this machine');
|
|
149
|
-
return true;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Back-compat alias for callers that just want "the keys, wherever they are".
|
|
153
|
-
export const ensureKeys = (store, opts) => resolveKeys(store, opts);
|