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
package/lib/core/store.mjs
DELETED
|
@@ -1,578 +0,0 @@
|
|
|
1
|
-
// store.mjs — PodStore: the agent's operational state as JSON documents in a
|
|
2
|
-
// container. The container is a Storage (lib/storage.mjs) — a pod over HTTP or
|
|
3
|
-
// a directory — and this file does not care which. The sync read/write surface
|
|
4
|
-
// of dk's fs Store is preserved so every consumer (publisher, intake, facade,
|
|
5
|
-
// …) is unchanged: reads come from an in-memory cache loaded once at boot,
|
|
6
|
-
// writes update the cache and are flushed through a serialized, debounced,
|
|
7
|
-
// retrying queue. Everything here is rebuildable from the /fediverse/ RDF, so
|
|
8
|
-
// a lost write degrades, it doesn't destroy. Without a storage the store is
|
|
9
|
-
// pure memory (tests, unconfigured boot).
|
|
10
|
-
|
|
11
|
-
import crypto from 'node:crypto';
|
|
12
|
-
import { sanitizeHtml } from './wire.mjs';
|
|
13
|
-
|
|
14
|
-
// A display name is text, not markup: strip tags rather than allow a subset,
|
|
15
|
-
// because there is no tag that belongs in one.
|
|
16
|
-
const plainText = (s) => sanitizeHtml(String(s || '')).replace(/<[^>]*>/g, '').trim();
|
|
17
|
-
|
|
18
|
-
// An avatar or header URL is written straight into an <img src> by the client.
|
|
19
|
-
// http(s) only — `javascript:` and `data:` have no business being an avatar.
|
|
20
|
-
const safeUrl = (u) => {
|
|
21
|
-
if (!u) return u;
|
|
22
|
-
try {
|
|
23
|
-
const parsed = new URL(String(u));
|
|
24
|
-
return (parsed.protocol === 'https:' || parsed.protocol === 'http:') ? String(u) : null;
|
|
25
|
-
} catch { return null; }
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
// Remote text arrives with no length of its own. `readCapped` bounds a document
|
|
29
|
-
// at 5 MB, which is a sane ceiling for a fetch and an absurd one for a display
|
|
30
|
-
// name — and whatever survives is then written into a document we serialize
|
|
31
|
-
// WHOLE on every change. One planted actor with a megabyte of bio permanently
|
|
32
|
-
// inflated actors.json, and a Follow flood re-PUT it once per commit batch.
|
|
33
|
-
// Mastodon's own limits are the shape of the real world; a little over them is
|
|
34
|
-
// generous rather than lossy.
|
|
35
|
-
const MAX_NAME = 500;
|
|
36
|
-
const MAX_SUMMARY = 5_000;
|
|
37
|
-
const MAX_CONTENT = 100_000;
|
|
38
|
-
const clamp = (s, n) => (typeof s === 'string' && s.length > n ? s.slice(0, n) : s);
|
|
39
|
-
|
|
40
|
-
const PUT_DEBOUNCE_MS = 300;
|
|
41
|
-
const PUT_RETRIES = 5;
|
|
42
|
-
// This is a CACHE of other people's actor documents — every search, every
|
|
43
|
-
// timeline post from a stranger, every hashtag sweep adds one, and nothing ever
|
|
44
|
-
// took one out. Bounded by dropping the least recently fetched, except for
|
|
45
|
-
// anyone we actually have a relationship with: losing a follower's display name
|
|
46
|
-
// would be re-fetching it on the next render for nothing.
|
|
47
|
-
const ACTOR_CACHE_MAX = 2000;
|
|
48
|
-
const NOTIFICATIONS_MAX = 500;
|
|
49
|
-
|
|
50
|
-
function prune(actors, max, contacts) {
|
|
51
|
-
const urls = Object.keys(actors);
|
|
52
|
-
if (urls.length <= max) return actors;
|
|
53
|
-
const keep = new Set([
|
|
54
|
-
...contacts.followers.map(f => f.actor),
|
|
55
|
-
...contacts.following.map(f => f.actor),
|
|
56
|
-
]);
|
|
57
|
-
const droppable = urls.filter(u => !keep.has(u))
|
|
58
|
-
.sort((a, b) => String(actors[a].fetchedAt || '').localeCompare(String(actors[b].fetchedAt || '')));
|
|
59
|
-
for (const u of droppable.slice(0, urls.length - max)) delete actors[u];
|
|
60
|
-
return actors;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Removing a follower is a DECISION — they unfollowed, or were ejected, or the
|
|
64
|
-
// account is gone. Reconciling the local list against the pod's published one
|
|
65
|
-
// would otherwise bring every one of them back, so each removal leaves a mark.
|
|
66
|
-
// Bounded: the same actor leaving twice is still one entry.
|
|
67
|
-
export function dropFollower(contacts, actor, why) {
|
|
68
|
-
contacts.followers = contacts.followers.filter(f => f.actor !== actor);
|
|
69
|
-
const gone = (contacts.removedFollowers || []).filter(r => r.actor !== actor);
|
|
70
|
-
gone.push({ actor, why, at: new Date().toISOString() });
|
|
71
|
-
contacts.removedFollowers = gone.slice(-500);
|
|
72
|
-
return contacts;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// What a state document looks like on the pod; write() and _put() agree on it.
|
|
76
|
-
const serialise = (obj) => JSON.stringify(obj, null, 2) + '\n';
|
|
77
|
-
// The same record apart from when it was fetched.
|
|
78
|
-
const sameButWhen = (a, b) => JSON.stringify({ ...a, fetchedAt: null }) === JSON.stringify({ ...b, fetchedAt: null });
|
|
79
|
-
|
|
80
|
-
export class PodStore {
|
|
81
|
-
constructor({ storage = null, log = console.log } = {}) {
|
|
82
|
-
this.lastSkipped = []; // what the last load could not read, as `name (HTTP n)`
|
|
83
|
-
this.storage = storage;
|
|
84
|
-
this.log = log;
|
|
85
|
-
this.cache = new Map(); // name → parsed value
|
|
86
|
-
this.etags = new Map(); // name (or '') → last ETag, for revalidation
|
|
87
|
-
this.lastText = new Map(); // name → the bytes last loaded from or sent to the pod
|
|
88
|
-
this.timers = new Map(); // name → debounce timer
|
|
89
|
-
this.dirty = new Set(); // written while held; flushed by commit/release
|
|
90
|
-
this._held = 0;
|
|
91
|
-
this.chain = Promise.resolve(); // serialized writes
|
|
92
|
-
this.verdicts = new Map(); // name → did its last write land? (read by commit)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
get base() { return this.storage?.base || null; }
|
|
96
|
-
|
|
97
|
-
attach(storage) {
|
|
98
|
-
// A different tree is different state: carrying the old cache and its
|
|
99
|
-
// ETags across would serve one container's documents as another's.
|
|
100
|
-
if (this.storage && this.storage.base !== storage.base) { this.cache.clear(); this.etags.clear(); this.lastText.clear(); }
|
|
101
|
-
this.storage = storage;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Load every state doc in the container into the cache. Missing container
|
|
105
|
-
// (first run) is fine — the cache just starts empty.
|
|
106
|
-
//
|
|
107
|
-
// `force` skips the container short-circuit below. A container's ETag says
|
|
108
|
-
// its CHILDREN have not changed; it does not vouch for their contents, and a
|
|
109
|
-
// peer agent rewriting a document it already had changes no containment
|
|
110
|
-
// triple. That is a fine trade for a viewer refreshing a display, and a bad
|
|
111
|
-
// one at the moment a viewer is promoted and starts acting on what it holds.
|
|
112
|
-
// Throws when the container cannot be read. An empty cache MUST mean "no
|
|
113
|
-
// state yet", never "the pod was unreachable" — the caller treats the
|
|
114
|
-
// former as a fresh install, and silently conflating them would look like
|
|
115
|
-
// an un-set-up agent every time the pod hiccups.
|
|
116
|
-
async load({ force = false } = {}) {
|
|
117
|
-
if (!this.storage) return;
|
|
118
|
-
// Revalidate rather than re-download: a viewer reloads this every few
|
|
119
|
-
// minutes and it is almost always unchanged. (Only a pod answers 304; a
|
|
120
|
-
// directory has nothing to revalidate against, so it always relists —
|
|
121
|
-
// which is a readdir, not a request.)
|
|
122
|
-
const listing = await this.storage.list('', { etag: force ? null : this.etags.get('') });
|
|
123
|
-
if (listing.notModified) return; // cannot happen when forced: no etag was sent
|
|
124
|
-
// A 404'd container over HTTP is "could not read", not "no state yet":
|
|
125
|
-
// setup creates this container, so once a credential exists it should
|
|
126
|
-
// answer — and a proxy fronting a dead pod 404s in exactly the same shape.
|
|
127
|
-
// A directory has no proxy to lie for it, so its absence stays an answer.
|
|
128
|
-
if (listing.missing && this.storage.kind === 'pod') {
|
|
129
|
-
throw new Error(`state container missing (HTTP 404) at ${this.base} — pod unreachable or state gone`);
|
|
130
|
-
}
|
|
131
|
-
this.etags.set('', listing.etag);
|
|
132
|
-
const names = listing.names.filter(n => n.endsWith('.json'));
|
|
133
|
-
let fetched = 0;
|
|
134
|
-
const skipped = [];
|
|
135
|
-
for (const name of names) {
|
|
136
|
-
const etag = force ? null : this.etags.get(name);
|
|
137
|
-
const r = await this.storage.read(name, { etag: etag && this.cache.has(name) ? etag : null });
|
|
138
|
-
if (r.notModified) continue; // ours is current
|
|
139
|
-
if (!r.ok) {
|
|
140
|
-
// One unreadable document must not restart the whole sweep — skip it,
|
|
141
|
-
// keep whatever we already hold, and try again next load (no etag is
|
|
142
|
-
// recorded, so the retry is unconditional). config.json is the
|
|
143
|
-
// exception: without it the caller cannot tell "never set up" from
|
|
144
|
-
// "could not read", and would tell the user to run setup.
|
|
145
|
-
if (name === 'config.json' && !this.cache.has(name)) {
|
|
146
|
-
throw new Error(`state doc ${name} unreadable (HTTP ${r.status})`);
|
|
147
|
-
}
|
|
148
|
-
skipped.push(`${name} (HTTP ${r.status})`);
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
fetched++;
|
|
152
|
-
this.etags.set(name, r.etag);
|
|
153
|
-
try { this.cache.set(name, JSON.parse(r.body)); this.lastText.set(name, r.body); }
|
|
154
|
-
catch (e) { this.log(`state load ${name}: unparsable (${e.message})`); }
|
|
155
|
-
}
|
|
156
|
-
// Kept for /status: a document skipped here is a timeline or a contact
|
|
157
|
-
// list quietly missing, and the owner should be able to see that.
|
|
158
|
-
this.lastSkipped = skipped;
|
|
159
|
-
if (skipped.length) this.log(`state load skipped ${skipped.length}: ${skipped.join(', ')}`);
|
|
160
|
-
this.log(`state loaded: ${this.cache.size} doc(s) from ${this.base} (${fetched} re-fetched)`);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
has(name) { return this.cache.has(name); }
|
|
164
|
-
|
|
165
|
-
// The documents held here. Callers that keep one document per thing — a
|
|
166
|
-
// connected account, say — list them with this rather than a fixed name.
|
|
167
|
-
names() { return [...this.cache.keys()]; }
|
|
168
|
-
|
|
169
|
-
read(name, fallback) {
|
|
170
|
-
return this.cache.has(name) ? structuredClone(this.cache.get(name)) : fallback;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
write(name, obj) {
|
|
174
|
-
this.cache.set(name, structuredClone(obj));
|
|
175
|
-
if (!this.storage) return;
|
|
176
|
-
// The same bytes the pod already holds are not sent again. A like, a
|
|
177
|
-
// bookmark, a sweep that found nothing new each used to upload the whole
|
|
178
|
-
// timeline index and the whole people cache, unchanged — the largest
|
|
179
|
-
// write on the pod, on every small action. A write that had been armed
|
|
180
|
-
// and is now undone is unarmed with it.
|
|
181
|
-
if (this.lastText.get(name) === serialise(obj)) {
|
|
182
|
-
clearTimeout(this.timers.get(name)); this.timers.delete(name); this.dirty.delete(name);
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
// Held: record it and leave the writing to the commit boundary the caller
|
|
186
|
-
// already has. See hold().
|
|
187
|
-
if (this._held) { this.dirty.add(name); return; }
|
|
188
|
-
this._arm(name);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
_arm(name) {
|
|
192
|
-
clearTimeout(this.timers.get(name));
|
|
193
|
-
this.timers.set(name, setTimeout(() => { this.timers.delete(name); this._put(name); }, PUT_DEBOUNCE_MS));
|
|
194
|
-
this.timers.get(name).unref?.();
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Suspend the debounce for the length of a sweep.
|
|
198
|
-
//
|
|
199
|
-
// 300ms coalesces writes that arrive together, and the inbox drain's never
|
|
200
|
-
// do: every handler awaits a signed fetch to somebody else's server first, so
|
|
201
|
-
// each item's timer fires before the next item is even read. statuses.json,
|
|
202
|
-
// actors.json and notifications.json were therefore serialized and written
|
|
203
|
-
// WHOLE once per item — fifty times in a fifty-item sweep, where the drain
|
|
204
|
-
// already commits every ten.
|
|
205
|
-
//
|
|
206
|
-
// Nested, because a drain can run a handler that starts another. release()
|
|
207
|
-
// re-arms anything still dirty rather than dropping it, so a write made by
|
|
208
|
-
// something else while the drain held the store is never stranded.
|
|
209
|
-
hold() { this._held = (this._held || 0) + 1; }
|
|
210
|
-
|
|
211
|
-
release() {
|
|
212
|
-
if (this._held) this._held -= 1;
|
|
213
|
-
if (this._held) return;
|
|
214
|
-
for (const name of [...this.dirty]) { this.dirty.delete(name); this._arm(name); }
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Resolves true when the document is on the pod, false when it is not — a
|
|
218
|
-
// caller that is about to destroy the only other copy of something needs to
|
|
219
|
-
// be able to tell. `chain` stays the bare serializer; the boolean rides on
|
|
220
|
-
// the returned promise so one failure cannot poison the queue.
|
|
221
|
-
_put(name) {
|
|
222
|
-
const done = this.chain.then(async () => {
|
|
223
|
-
const body = serialise(this.cache.get(name));
|
|
224
|
-
for (let attempt = 1; attempt <= PUT_RETRIES; attempt++) {
|
|
225
|
-
// A storage that throws is a bug, not a hiccup — but it must not
|
|
226
|
-
// escape into the write queue, where it would look like success.
|
|
227
|
-
const r = await this.storage.write(name, body, 'application/json')
|
|
228
|
-
.catch(e => ({ ok: false, retry: false, why: e.message }));
|
|
229
|
-
if (r.ok) { this.lastText.set(name, body); return true; }
|
|
230
|
-
// The storage says whether trying again could possibly help: a pod's
|
|
231
|
-
// 4xx is an answer rather than a hiccup, and a directory's EACCES will
|
|
232
|
-
// still be an EACCES in two seconds.
|
|
233
|
-
if (!r.retry) { this.log(`state write ${name} refused (${r.why}) — not retrying`); return false; }
|
|
234
|
-
if (attempt === PUT_RETRIES) { this.log(`state write ${name} gave up: ${r.why}`); return false; }
|
|
235
|
-
const ladder = Math.min(attempt * 2000, 30_000);
|
|
236
|
-
await new Promise(res => setTimeout(res, r.retryAfterMs || Math.round(ladder * (0.8 + Math.random() * 0.4))));
|
|
237
|
-
}
|
|
238
|
-
return false;
|
|
239
|
-
});
|
|
240
|
-
// Record the outcome. A write whose debounce fires on its own is not in
|
|
241
|
-
// any caller's hands: `chain` waits for it but throws its result away, so
|
|
242
|
-
// commit() reported a clean sweep for a document that had been refused —
|
|
243
|
-
// and commit()'s one caller is the inbox drain, about to delete the pod's
|
|
244
|
-
// only copy of what that document describes.
|
|
245
|
-
const tracked = done.then((ok) => { this.verdicts.set(name, ok); return ok; });
|
|
246
|
-
this.chain = tracked.then(() => {}, () => {});
|
|
247
|
-
return tracked;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Remove a state doc from the cache AND the pod (used when key material
|
|
251
|
-
// migrates to the local machine — leaving the copy behind would defeat it).
|
|
252
|
-
async remove(name) {
|
|
253
|
-
this.cache.delete(name);
|
|
254
|
-
this.lastText.delete(name);
|
|
255
|
-
clearTimeout(this.timers.get(name));
|
|
256
|
-
this.timers.delete(name);
|
|
257
|
-
if (!this.storage) return true;
|
|
258
|
-
return this.storage.remove(name);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Force every pending write out NOW and say whether they all landed.
|
|
262
|
-
// The caller that needs this is the inbox drain: taking an item out of the
|
|
263
|
-
// pod's inbox is a destructive read, so it must not happen until the result
|
|
264
|
-
// of handling it is written down. With no storage the store is pure memory
|
|
265
|
-
// and there is nothing to land, so that counts as written.
|
|
266
|
-
async commit() {
|
|
267
|
-
const pending = [];
|
|
268
|
-
// Held names too, or a sweep that suspended the debounce would delete the
|
|
269
|
-
// pod's copy of an item whose result had not been written down — the one
|
|
270
|
-
// thing commit() exists to prevent.
|
|
271
|
-
for (const name of new Set([...this.timers.keys(), ...this.dirty])) {
|
|
272
|
-
const t = this.timers.get(name);
|
|
273
|
-
if (t) { clearTimeout(t); this.timers.delete(name); }
|
|
274
|
-
this.dirty.delete(name);
|
|
275
|
-
pending.push(this._put(name));
|
|
276
|
-
}
|
|
277
|
-
await this.chain; // includes anything already in flight
|
|
278
|
-
await Promise.all(pending);
|
|
279
|
-
// Every verdict since the last commit, forced and self-fired alike. Cleared
|
|
280
|
-
// once read: a refusal is reported to the caller that can act on it, and
|
|
281
|
-
// does not then condemn every later commit.
|
|
282
|
-
const landed = [...this.verdicts.values()].every(Boolean);
|
|
283
|
-
this.verdicts.clear();
|
|
284
|
-
return landed;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Flush pending debounced writes (shutdown path). Same work, result ignored.
|
|
288
|
-
async flush() { await this.commit(); }
|
|
289
|
-
|
|
290
|
-
// ---- the domain helpers, unchanged from dk's Store ----
|
|
291
|
-
|
|
292
|
-
// config: { remotePod, handle, name, issuer } (credential lives ONLY in
|
|
293
|
-
// the local credential file, never in pod state)
|
|
294
|
-
getConfig() { return this.read('config.json', null); }
|
|
295
|
-
setConfig(cfg) { this.write('config.json', cfg); }
|
|
296
|
-
|
|
297
|
-
// queue: [{ inbox, activity, attempts, nextAt }]
|
|
298
|
-
getQueue() { return this.read('queue.json', []); }
|
|
299
|
-
setQueue(q) { this.write('queue.json', q); }
|
|
300
|
-
|
|
301
|
-
// blocklist: { domains: ["spam.example", ...], actors: ["https://host/actor", ...] }
|
|
302
|
-
// Two granularities because a whole instance is usually the wrong unit: one
|
|
303
|
-
// bad neighbour should not cost you everyone else on their server.
|
|
304
|
-
getBlocklist() {
|
|
305
|
-
const b = this.read('blocklist.json', {});
|
|
306
|
-
return { domains: b.domains || [], actors: b.actors || [] };
|
|
307
|
-
}
|
|
308
|
-
// Blocking someone ends their following as well: a blocked actor, or anyone
|
|
309
|
-
// on a blocked domain, is no longer delivered to. The mark dropFollower leaves
|
|
310
|
-
// keeps a reconcile from bringing them back.
|
|
311
|
-
setBlocklist(b) {
|
|
312
|
-
this.write('blocklist.json', b);
|
|
313
|
-
const c = this.getContacts();
|
|
314
|
-
const gone = c.followers.filter(f => f.actor && this.isBlocked(f.actor)).map(f => f.actor);
|
|
315
|
-
if (!gone.length) return;
|
|
316
|
-
for (const actor of gone) dropFollower(c, actor, 'blocked');
|
|
317
|
-
this.setContacts(c);
|
|
318
|
-
}
|
|
319
|
-
// Takes an actor URL or an object URL: the actor list only ever matches the
|
|
320
|
-
// former, the domain list matches either.
|
|
321
|
-
isBlocked(url) {
|
|
322
|
-
let host;
|
|
323
|
-
try { host = new URL(url).hostname; } catch { return true; } // unparsable → treat as hostile
|
|
324
|
-
const { domains, actors } = this.getBlocklist();
|
|
325
|
-
if (actors.includes(url)) return true;
|
|
326
|
-
return domains.some(d => host === d || host.endsWith('.' + d));
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// contacts: { followers: [{actor, inbox, sharedInbox}], following: [{actor, inbox, accepted}] }
|
|
330
|
-
getContacts() { return this.read('contacts.json', { followers: [], following: [] }); }
|
|
331
|
-
setContacts(c) { this.write('contacts.json', c); }
|
|
332
|
-
|
|
333
|
-
// muted: { actors: [...] } — members whose posts a group declines to carry.
|
|
334
|
-
// A group cannot force an unfollow, so declining to amplify is the only
|
|
335
|
-
// lever it actually holds.
|
|
336
|
-
getMuted() { return this.read('muted.json', { actors: [] }); }
|
|
337
|
-
setMuted(m) { this.write('muted.json', m); }
|
|
338
|
-
|
|
339
|
-
// The client's own reading arrangements — lists, keyword filters, posts
|
|
340
|
-
// waiting to be published. None of it federates.
|
|
341
|
-
getLists() { return this.read('lists.json', []); }
|
|
342
|
-
setLists(l) { this.write('lists.json', l); }
|
|
343
|
-
getFilters() { return this.read('filters.json', []); }
|
|
344
|
-
setFilters(f) { this.write('filters.json', f); }
|
|
345
|
-
getScheduled() { return this.read('scheduled.json', []); }
|
|
346
|
-
setScheduled(s) { this.write('scheduled.json', s); }
|
|
347
|
-
|
|
348
|
-
// pending: [{ noteId, actor, at }] — posts a reviewed group has ingested but
|
|
349
|
-
// not carried, awaiting the operator.
|
|
350
|
-
getPending() { return this.read('pending.json', []); }
|
|
351
|
-
setPending(p) { this.write('pending.json', p); }
|
|
352
|
-
|
|
353
|
-
// requests: [{ actor, inbox, sharedInbox, activity, at }] — Follows a group
|
|
354
|
-
// with approveJoins has neither accepted nor rejected. The whole Follow is
|
|
355
|
-
// kept because the Accept or Reject has to name it.
|
|
356
|
-
getRequests() { return this.read('requests.json', []); }
|
|
357
|
-
setRequests(r) { this.write('requests.json', r); }
|
|
358
|
-
|
|
359
|
-
// dead letters: inbox items that failed verification or exhausted retries —
|
|
360
|
-
// kept for inspection (GET /deadletter) instead of being destroyed.
|
|
361
|
-
getDeadLetters() { return this.read('deadletter.json', []); }
|
|
362
|
-
addDeadLetter(entry) {
|
|
363
|
-
const dl = this.getDeadLetters();
|
|
364
|
-
dl.unshift({ at: new Date().toISOString(), ...entry });
|
|
365
|
-
this.write('deadletter.json', dl.slice(0, 200));
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// statuses index: operational mirror of what lives in the pod as RDF, in
|
|
369
|
-
// arrival order — the Mastodon-API facade serves timelines from this.
|
|
370
|
-
// [{ noteId, actor, content, published, inReplyTo, kind: 'timeline'|'post'|'tag' }]
|
|
371
|
-
getStatuses() { return this.read('statuses.json', []); }
|
|
372
|
-
// A count must not pay for a clone: nodeinfo and the instance document are
|
|
373
|
-
// public and answer strangers, so they read the cache directly.
|
|
374
|
-
countStatuses(kind = null) {
|
|
375
|
-
const all = this.cache.get('statuses.json') || [];
|
|
376
|
-
return kind === null ? all.length : all.reduce((n, s) => n + (s.kind === kind ? 1 : 0), 0);
|
|
377
|
-
}
|
|
378
|
-
addStatus(s) {
|
|
379
|
-
const all = this.getStatuses();
|
|
380
|
-
const at = all.findIndex(x => x.noteId === s.noteId);
|
|
381
|
-
if (at >= 0) return this._mergeStatus(all, at, s);
|
|
382
|
-
// Same reason as cacheActor: statuses.json is serialized whole on every
|
|
383
|
-
// change, and remote content is only bounded by the 5 MB fetch ceiling.
|
|
384
|
-
if (typeof s.content === 'string' && s.content.length > MAX_CONTENT) {
|
|
385
|
-
s = { ...s, content: clamp(s.content, MAX_CONTENT), truncated: true };
|
|
386
|
-
}
|
|
387
|
-
all.unshift(s);
|
|
388
|
-
this.write('statuses.json', all.slice(0, 1000));
|
|
389
|
-
this.onEvent?.('status', s); // streaming subscribers
|
|
390
|
-
return { added: true, merged: false, status: s };
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// The same post arrives twice when two of the owner's accounts follow its
|
|
394
|
-
// author, and the second arrival is the only record that the other one saw
|
|
395
|
-
// it. Merged in place — the per-kind prune tails take the tail to be the
|
|
396
|
-
// oldest — and with no stream event, which would show the post twice.
|
|
397
|
-
_mergeStatus(all, at, s) {
|
|
398
|
-
const row = all[at];
|
|
399
|
-
const known = new Set((row.sourceAccts || []).map(v => v.acct));
|
|
400
|
-
const fresh = (s.sourceAccts || []).filter(v => v && !known.has(v.acct));
|
|
401
|
-
// Only our own verified intake may raise a row's kind. A general ladder
|
|
402
|
-
// would let a source that merely SAW a post promote a stranger's mention
|
|
403
|
-
// into the home timeline, which is the route tagfeed had to close.
|
|
404
|
-
const first = (k) => k === 'post' || k === 'timeline';
|
|
405
|
-
const raise = first(s.kind) && !first(row.kind);
|
|
406
|
-
if (!fresh.length && !raise) return { added: false, merged: false, status: row };
|
|
407
|
-
all[at] = {
|
|
408
|
-
...row,
|
|
409
|
-
...(fresh.length ? { sourceAccts: [...(row.sourceAccts || []), ...fresh] } : {}),
|
|
410
|
-
...(raise ? { kind: s.kind, ...(s.slug ? { slug: s.slug } : {}) } : {}),
|
|
411
|
-
};
|
|
412
|
-
this.write('statuses.json', all);
|
|
413
|
-
return { added: false, merged: true, status: all[at] };
|
|
414
|
-
}
|
|
415
|
-
updateStatus(noteId, patch) {
|
|
416
|
-
const all = this.getStatuses();
|
|
417
|
-
const i = all.findIndex(x => x.noteId === noteId);
|
|
418
|
-
if (i < 0) return null;
|
|
419
|
-
all[i] = { ...all[i], ...patch };
|
|
420
|
-
this.write('statuses.json', all);
|
|
421
|
-
return all[i];
|
|
422
|
-
}
|
|
423
|
-
removeStatus(noteId) {
|
|
424
|
-
this.write('statuses.json', this.getStatuses().filter(x => x.noteId !== noteId));
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// notifications: what other actors did to us — the facade serves
|
|
428
|
-
// /api/v1/notifications from this. [{ id, type, actor, noteId?, at }]
|
|
429
|
-
// The id is a content hash, so a re-delivered activity dedupes.
|
|
430
|
-
getNotifications() { return this.read('notifications.json', []); }
|
|
431
|
-
|
|
432
|
-
// `unverified` marks one whose actor nothing vouched for — a Like or Announce
|
|
433
|
-
// arrives with no signature and, unlike a Create, has no object at the
|
|
434
|
-
// sender's origin to re-read, so the actor is whatever the body claimed.
|
|
435
|
-
//
|
|
436
|
-
// It decides what the cap evicts. The id is a content hash, so changing one
|
|
437
|
-
// character of the actor gives a fresh entry: ~500 appends into the pod's
|
|
438
|
-
// public-append inbox used to push out every real favourite, boost, mention
|
|
439
|
-
// and follow request, and notifications are the one thing nothing can rebuild
|
|
440
|
-
// — not from the pod, not from the RDF. Unverified entries are dropped first
|
|
441
|
-
// now, so a flood can only ever evict itself, and a stranger's genuine
|
|
442
|
-
// favourite still shows up, which is what recording them at all is for.
|
|
443
|
-
// What an Undo unsays: the favourite or boost it names, matched by a
|
|
444
|
-
// predicate on what was recorded. Returns how many went.
|
|
445
|
-
removeNotifications(pred) {
|
|
446
|
-
const all = this.getNotifications();
|
|
447
|
-
const kept = all.filter(n => !pred(n));
|
|
448
|
-
if (kept.length !== all.length) this.write('notifications.json', kept);
|
|
449
|
-
return all.length - kept.length;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
addNotification(n) {
|
|
453
|
-
const all = this.getNotifications();
|
|
454
|
-
const id = crypto.createHash('sha256').update(JSON.stringify(n)).digest('hex').slice(0, 16);
|
|
455
|
-
if (all.some(x => x.id === id)) return;
|
|
456
|
-
const entry = { id, at: new Date().toISOString(), ...n };
|
|
457
|
-
all.unshift(entry);
|
|
458
|
-
let kept = all;
|
|
459
|
-
if (kept.length > NOTIFICATIONS_MAX) {
|
|
460
|
-
const solid = kept.filter(x => !x.unverified);
|
|
461
|
-
const loose = kept.filter(x => x.unverified);
|
|
462
|
-
kept = solid.length >= NOTIFICATIONS_MAX
|
|
463
|
-
? solid.slice(0, NOTIFICATIONS_MAX)
|
|
464
|
-
: [...solid, ...loose.slice(0, NOTIFICATIONS_MAX - solid.length)]
|
|
465
|
-
.sort((a, b) => String(b.at).localeCompare(String(a.at)));
|
|
466
|
-
}
|
|
467
|
-
this.write('notifications.json', kept);
|
|
468
|
-
this.onEvent?.('notification', entry); // streaming subscribers
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// uploaded media registry: opaque id → { url, mediaType, description }
|
|
472
|
-
getMedia() { return this.read('media.json', {}); }
|
|
473
|
-
setMedia(id, entry) {
|
|
474
|
-
const m = this.getMedia();
|
|
475
|
-
m[id] = entry;
|
|
476
|
-
this.write('media.json', m);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// actor-doc cache for account rendering (display name, avatar).
|
|
480
|
-
//
|
|
481
|
-
// Everything here comes from a document at someone else's origin, and the
|
|
482
|
-
// facade serves `summary` back to the client as a status account's `note`,
|
|
483
|
-
// which every Mastodon client renders as HTML. Note CONTENT was sanitized at
|
|
484
|
-
// all four of its entry points and this, the other thing a remote party
|
|
485
|
-
// writes, was not — so looking anyone up handed their markup to the client.
|
|
486
|
-
// Sanitized at the boundary where it enters our store, so nothing downstream
|
|
487
|
-
// has to remember. Names are plain text and are stripped outright.
|
|
488
|
-
getActors() { return this.read('actors.json', {}); }
|
|
489
|
-
cacheActor(url, doc) {
|
|
490
|
-
const a = this.getActors();
|
|
491
|
-
const known = a[url];
|
|
492
|
-
a[url] = {
|
|
493
|
-
name: clamp(plainText(doc.name || doc.preferredUsername || ''), MAX_NAME),
|
|
494
|
-
preferredUsername: clamp(plainText(doc.preferredUsername || ''), MAX_NAME),
|
|
495
|
-
icon: safeUrl(typeof doc.icon === 'object' ? doc.icon?.url : doc.icon),
|
|
496
|
-
image: safeUrl(typeof doc.image === 'object' ? doc.image?.url : doc.image),
|
|
497
|
-
summary: clamp(sanitizeHtml(doc.summary || ''), MAX_SUMMARY),
|
|
498
|
-
// A Group is an actor too, and a client that cannot tell shows it as a
|
|
499
|
-
// person. The counts are NOT in this document — they are the collections'
|
|
500
|
-
// totalItems, filled in only when someone asks about this actor by name.
|
|
501
|
-
type: doc.type || 'Person',
|
|
502
|
-
followers: doc.followers || null,
|
|
503
|
-
following: doc.following || null,
|
|
504
|
-
...(known?.counts ? { counts: known.counts } : {}),
|
|
505
|
-
fetchedAt: new Date().toISOString(),
|
|
506
|
-
};
|
|
507
|
-
// Fetched again and found the same: nothing to write. Only the time of
|
|
508
|
-
// asking differs, and that is not worth the whole cache going to the pod.
|
|
509
|
-
if (known && sameButWhen(known, a[url])) { a[url] = known; return; }
|
|
510
|
-
this.write('actors.json', prune(a, ACTOR_CACHE_MAX, this.getContacts()));
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// @user@host for an actor we have cached. Null when we have not, rather than
|
|
514
|
-
// a last-segment guess off the URL: that guess is what rendered a group as
|
|
515
|
-
// @actor@host, because our own actors all end in /actor.
|
|
516
|
-
handleOf(actorUrl) {
|
|
517
|
-
const user = this.getActors()[actorUrl]?.preferredUsername;
|
|
518
|
-
if (!user) return null;
|
|
519
|
-
try { return `@${user}@${new URL(actorUrl).host}`; } catch { return null; }
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// Mastodon-API opaque ids ↔ URLs (snac-style: hashes are fine for clients).
|
|
523
|
-
getIds() { return this.read('ids.json', {}); }
|
|
524
|
-
// The id is a hash OF the url, so the mapping is computable — the scan was
|
|
525
|
-
// only ever finding what the hash already tells us. It ran on the status
|
|
526
|
-
// render path, once per rendered status, over a map that never shrinks.
|
|
527
|
-
//
|
|
528
|
-
// The map is still kept, because urlFor has to answer for ids a client is
|
|
529
|
-
// still holding, and it is still consulted first so an entry written under
|
|
530
|
-
// some older scheme keeps resolving.
|
|
531
|
-
idFor(url) {
|
|
532
|
-
const id = crypto.createHash('sha256').update(url).digest('hex').slice(0, 16);
|
|
533
|
-
const ids = this.getIds();
|
|
534
|
-
if (ids[id] === url) return id; // already known, nothing to write
|
|
535
|
-
const legacy = Object.entries(ids).find(([, u]) => u === url);
|
|
536
|
-
if (legacy) return legacy[0]; // pre-hash entry a client may hold
|
|
537
|
-
// In memory only. This runs on the RENDER path — once per status, per
|
|
538
|
-
// account, per attachment, every time a timeline is drawn — and `write()`
|
|
539
|
-
// is write-through, so on the browser build scrolling a timeline was a pod
|
|
540
|
-
// PUT of this whole document. Nothing durable is lost: the id IS the hash
|
|
541
|
-
// of the url, so idFor recomputes it identically on any later run, and the
|
|
542
|
-
// map exists only so urlFor can still answer for ids minted under an older
|
|
543
|
-
// scheme (which are loaded from the pod and never written here).
|
|
544
|
-
ids[id] = url;
|
|
545
|
-
this.cache.set('ids.json', ids);
|
|
546
|
-
return id;
|
|
547
|
-
}
|
|
548
|
-
// The map first, for ids minted under an older scheme. Then, because the
|
|
549
|
-
// id IS the hash of the url, whatever this store knows is scanned for the
|
|
550
|
-
// url that hashes to it — actors, posts, contacts, requests, media. The
|
|
551
|
-
// browser build's worker is stopped whenever it idles, and the in-memory
|
|
552
|
-
// map went with it: a client clicking an account it had just been shown
|
|
553
|
-
// reached a fresh worker that held the actor and could not name it.
|
|
554
|
-
urlFor(id) {
|
|
555
|
-
const ids = this.getIds();
|
|
556
|
-
if (ids[id]) return ids[id];
|
|
557
|
-
if (!/^[a-f0-9]{16}$/u.test(String(id))) return null;
|
|
558
|
-
const hash = (u) => crypto.createHash('sha256').update(u).digest('hex').slice(0, 16);
|
|
559
|
-
const seen = new Set();
|
|
560
|
-
const candidates = function* (store) {
|
|
561
|
-
for (const u of Object.keys(store.getActors())) yield u;
|
|
562
|
-
for (const st of store.getStatuses()) {
|
|
563
|
-
yield st.noteId; yield st.actor;
|
|
564
|
-
for (const a of st.attachments || []) if (a?.url) yield a.url;
|
|
565
|
-
}
|
|
566
|
-
const c = store.getContacts();
|
|
567
|
-
for (const f of [...c.followers, ...c.following]) if (f?.actor) yield f.actor;
|
|
568
|
-
for (const r of store.getRequests()) if (r?.actor) yield r.actor;
|
|
569
|
-
for (const u of Object.keys(store.getMedia())) yield u;
|
|
570
|
-
};
|
|
571
|
-
for (const u of candidates(this)) {
|
|
572
|
-
if (typeof u !== 'string' || seen.has(u)) continue;
|
|
573
|
-
seen.add(u);
|
|
574
|
-
if (hash(u) === id) { ids[id] = u; this.cache.set('ids.json', ids); return u; }
|
|
575
|
-
}
|
|
576
|
-
return null;
|
|
577
|
-
}
|
|
578
|
-
}
|
package/lib/core/wire-quotes.mjs
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// wire-quotes.mjs — the documents and activities a quote is made of on the
|
|
2
|
-
// wire (FEP-044f): the request to the quoted author, the authorization they
|
|
3
|
-
// publish, and the Accept or Reject that answers. The quote terms a note
|
|
4
|
-
// itself carries live in wire.mjs with the note.
|
|
5
|
-
|
|
6
|
-
import crypto from 'node:crypto';
|
|
7
|
-
import { AS_CTX, QUOTE_CTX, POLICY_CTX } from './wire.mjs';
|
|
8
|
-
|
|
9
|
-
// The activity and object types, and the two terms an authorization names
|
|
10
|
-
// its parties by — GoToSocial's, declared as Mastodon declares them.
|
|
11
|
-
export const QUOTE_TYPES_CTX = {
|
|
12
|
-
QuoteRequest: 'https://w3id.org/fep/044f#QuoteRequest',
|
|
13
|
-
QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization',
|
|
14
|
-
gts: 'https://gotosocial.org/ns#',
|
|
15
|
-
interactingObject: { '@id': 'gts:interactingObject', '@type': '@id' },
|
|
16
|
-
interactionTarget: { '@id': 'gts:interactionTarget', '@type': '@id' },
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
// FEP-044f: asking the quoted post's author to allow the quote. The whole
|
|
20
|
-
// quoting note rides along as the instrument, so their server can see what
|
|
21
|
-
// is being asked without fetching it; `object` is their post.
|
|
22
|
-
export function quoteRequestActivity({ urls, note, quoted, quotedActor, serial }) {
|
|
23
|
-
const { '@context': ctx, ...instrument } = note; // eslint-disable-line no-unused-vars
|
|
24
|
-
return {
|
|
25
|
-
'@context': [AS_CTX, { ...QUOTE_CTX, ...POLICY_CTX, QuoteRequest: QUOTE_TYPES_CTX.QuoteRequest }],
|
|
26
|
-
id: urls.actor + '#quote-request-' + serial,
|
|
27
|
-
type: 'QuoteRequest',
|
|
28
|
-
actor: urls.actor,
|
|
29
|
-
to: [quotedActor],
|
|
30
|
-
object: quoted,
|
|
31
|
-
instrument,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// The authorization a quoted author publishes (FEP-044f): a document of its
|
|
36
|
-
// own, at the quoted post's origin, naming the quoting post and the quoted
|
|
37
|
-
// one. A receiver checks the quote by fetching this and comparing both.
|
|
38
|
-
export function quoteAuthorizationId(noteId, instrumentId) {
|
|
39
|
-
return noteId + '-quote-' + crypto.createHash('sha256').update(String(instrumentId)).digest('hex').slice(0, 16);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function quoteAuthorizationDoc({ urls, id, instrument, target }) {
|
|
43
|
-
return {
|
|
44
|
-
'@context': [AS_CTX, QUOTE_TYPES_CTX],
|
|
45
|
-
id,
|
|
46
|
-
type: 'QuoteAuthorization',
|
|
47
|
-
attributedTo: urls.actor,
|
|
48
|
-
interactingObject: instrument,
|
|
49
|
-
interactionTarget: target,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// The answer to a QuoteRequest: Accept, carrying the authorization as its
|
|
54
|
-
// result, or Reject. The request is echoed by its id, type, actor, object
|
|
55
|
-
// and the instrument's id — enough for the asker to match it, and nothing a
|
|
56
|
-
// stranger wrote is repeated whole.
|
|
57
|
-
export function quoteAnswerActivity({ urls, type, request, result = null, to, serial }) {
|
|
58
|
-
const idOf = (v) => (typeof v === 'string' ? v : v?.id);
|
|
59
|
-
return {
|
|
60
|
-
'@context': [AS_CTX, { QuoteRequest: QUOTE_TYPES_CTX.QuoteRequest }],
|
|
61
|
-
id: urls.actor + '#' + type.toLowerCase() + '-' + serial,
|
|
62
|
-
type,
|
|
63
|
-
actor: urls.actor,
|
|
64
|
-
to: [to],
|
|
65
|
-
object: {
|
|
66
|
-
id: idOf(request), type: 'QuoteRequest', actor: idOf(request?.actor),
|
|
67
|
-
object: idOf(request?.object), instrument: idOf(request?.instrument),
|
|
68
|
-
},
|
|
69
|
-
...(result ? { result } : {}),
|
|
70
|
-
};
|
|
71
|
-
}
|