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/pod/transport.mjs
DELETED
|
@@ -1,674 +0,0 @@
|
|
|
1
|
-
// transport.mjs — every authenticated request this library makes to a pod, and
|
|
2
|
-
// the manners that must hold for all of them.
|
|
3
|
-
//
|
|
4
|
-
// The session is injected and never built here: a Node agent hands in a DPoP
|
|
5
|
-
// client-credentials grant, a browser hands in a Solid-OIDC session, a pod
|
|
6
|
-
// server hands in a shim straight onto its own store. That is what lets one
|
|
7
|
-
// implementation serve all three, and it is why this file has no `import` of
|
|
8
|
-
// anything with a runtime.
|
|
9
|
-
//
|
|
10
|
-
// Two copies of this used to exist — lib/remote.mjs and web/app/pod-remote.mjs
|
|
11
|
-
// — hand-kept in sync, with the browser copy quietly behind on five separate
|
|
12
|
-
// hardenings. The subclasses below now add only what genuinely differs: how the
|
|
13
|
-
// session is obtained, and whether a throttled pod is waited out or refused.
|
|
14
|
-
|
|
15
|
-
import * as $rdf from 'rdflib';
|
|
16
|
-
import { readCapped, retryAfterMs } from './http.mjs';
|
|
17
|
-
import { linkTargets, REL } from './links.mjs';
|
|
18
|
-
import { podBaseOfWebId } from './urls.mjs';
|
|
19
|
-
|
|
20
|
-
const LDP = $rdf.Namespace('http://www.w3.org/ns/ldp#');
|
|
21
|
-
const DC = $rdf.Namespace('http://purl.org/dc/terms/');
|
|
22
|
-
const POSIX = $rdf.Namespace('http://www.w3.org/ns/posix/stat#');
|
|
23
|
-
const RDF = $rdf.Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#');
|
|
24
|
-
const ACL = $rdf.Namespace('http://www.w3.org/ns/auth/acl#');
|
|
25
|
-
const FOAF = $rdf.Namespace('http://xmlns.com/foaf/0.1/');
|
|
26
|
-
const AS = $rdf.Namespace('https://www.w3.org/ns/activitystreams#');
|
|
27
|
-
const RDFS = $rdf.Namespace('http://www.w3.org/2000/01/rdf-schema#');
|
|
28
|
-
|
|
29
|
-
// An inserted term must be something a reader can take for what it says it
|
|
30
|
-
// is: an absolute http(s) IRI, or a literal.
|
|
31
|
-
function termProblem(t) {
|
|
32
|
-
if (t?.termType === 'Literal') return null;
|
|
33
|
-
if (t?.termType !== 'NamedNode') return `${t?.value ?? t} is not an IRI or a literal`;
|
|
34
|
-
try {
|
|
35
|
-
return /^https?:$/.test(new URL(t.value).protocol) ? null : `${t.value} is not an http(s) IRI`;
|
|
36
|
-
} catch { return `${t.value} is not an absolute IRI`; }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// A pod whose access rules are ACP policies, not WAC authorizations. This
|
|
40
|
-
// library writes WAC; over an ACP resource that would be noise where the pod's
|
|
41
|
-
// real rules used to be, so it stops instead.
|
|
42
|
-
const ACP_NS = 'http://www.w3.org/ns/solid/acp#';
|
|
43
|
-
|
|
44
|
-
// The inbox is public-Append, so the listing's size is in other people's
|
|
45
|
-
// hands; reading it whole must still have a ceiling.
|
|
46
|
-
const LISTING_MAX_BYTES = 10 * 1024 * 1024;
|
|
47
|
-
|
|
48
|
-
// No Retry-After header still means "not now". Sixty seconds is the answer
|
|
49
|
-
// when the server declined to give one.
|
|
50
|
-
const COOLDOWN_DEFAULT_MS = 60_000;
|
|
51
|
-
|
|
52
|
-
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
53
|
-
|
|
54
|
-
// A pod's ability to BE a pod. Not data, and never ours to remove: delete one
|
|
55
|
-
// of these and the pod does not degrade, it stops — and it cannot be repaired
|
|
56
|
-
// by the tool that broke it, because that tool can no longer authenticate. A
|
|
57
|
-
// pod was crippled exactly this way.
|
|
58
|
-
//
|
|
59
|
-
// Here rather than in any one caller, because every DELETE this library sends
|
|
60
|
-
// goes through PodTransport.fetch — so this is the one place that cannot be
|
|
61
|
-
// routed around. A DENY-list, not an allow-prefix: the next script will have a
|
|
62
|
-
// different prefix and the same list of things it must never touch.
|
|
63
|
-
const PROTECTED = [
|
|
64
|
-
[/\/profile(\/|$)/, 'the WebID document — nothing could authenticate as this pod again'],
|
|
65
|
-
[/\/settings(\/|$)/, "the pod's own settings"],
|
|
66
|
-
[/\/\.well-known(\/|$)/, 'discovery — the handle would stop resolving'],
|
|
67
|
-
[/\.acl$/, 'an access-control document — what it governs becomes unreachable'],
|
|
68
|
-
[/\.meta$/, 'a resource description the server itself reads'],
|
|
69
|
-
// The single-active-agent lock. It is on the pod precisely because the
|
|
70
|
-
// private half need not be — a lease only one machine can reach coordinates
|
|
71
|
-
// nothing — so it survives moving the state off, and nothing ever deletes it:
|
|
72
|
-
// release() writes an expiry, it does not remove the document. Two agents
|
|
73
|
-
// both believing they hold it is silent, destructive, duplicated inbox
|
|
74
|
-
// draining.
|
|
75
|
-
[/\/lease\.json$/, 'the lease — two agents would drain the same inbox'],
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
export function protectedFromDeletion(url) {
|
|
79
|
-
let p;
|
|
80
|
-
try { p = new URL(url).pathname; } catch { throw new Error(`refusing to DELETE an unparsable URL: ${url}`); }
|
|
81
|
-
if (p === '/' || p === '') throw new Error(`refusing to DELETE the pod root: ${url}`);
|
|
82
|
-
for (const [re, why] of PROTECTED) {
|
|
83
|
-
if (re.test(p)) {
|
|
84
|
-
throw new Error(`refusing to DELETE ${url} — ${why}. Deny-list: lib/pod/transport.mjs.`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export class PodTransport {
|
|
90
|
-
/**
|
|
91
|
-
* @param {object} session `{ fetch(url, init) }` — injected, never built here
|
|
92
|
-
* @param {object} opts
|
|
93
|
-
* @param {string} opts.webId who this transport acts as; the ACL owner
|
|
94
|
-
* @param {string} [opts.role] 'agent' | 'gateway' | 'signup' — what is talking
|
|
95
|
-
* @param {string} [opts.runtime] 'node' | 'browser' — where it is talking from
|
|
96
|
-
* @param {string} [opts.cooldownMode] 'refuse' fails fast for the window;
|
|
97
|
-
* 'wait' sleeps it out and continues
|
|
98
|
-
* @param {number} [opts.maxCooldownMs] ceiling on a Retry-After we will honour
|
|
99
|
-
*/
|
|
100
|
-
constructor(session, {
|
|
101
|
-
webId, log = () => {}, role = 'agent', runtime = 'node',
|
|
102
|
-
cooldownMode = 'refuse', maxCooldownMs = 30 * 60_000,
|
|
103
|
-
} = {}) {
|
|
104
|
-
this.session = session;
|
|
105
|
-
this.webId = webId;
|
|
106
|
-
this.log = log;
|
|
107
|
-
this.role = role;
|
|
108
|
-
this.runtime = runtime;
|
|
109
|
-
this.cooldownMode = cooldownMode;
|
|
110
|
-
this.maxCooldownMs = maxCooldownMs;
|
|
111
|
-
this.pausedUntil = 0;
|
|
112
|
-
this.probeCount = 0;
|
|
113
|
-
// Where each resource's access control lives, as the pod itself said. WAC
|
|
114
|
-
// forbids working it out from the resource's own URL, so it is asked for
|
|
115
|
-
// and remembered rather than assembled.
|
|
116
|
-
this.aclUrls = new Map();
|
|
117
|
-
this.aclFlavour = null; // null until the first write asks what this pod speaks
|
|
118
|
-
this._listCache = new Map();
|
|
119
|
-
// A fronted identity advertises ids on a shared domain but writes to the
|
|
120
|
-
// pod. The caller installs the advertised→pod mapping here, so every
|
|
121
|
-
// request built from an advertised id lands on the pod — one choke point.
|
|
122
|
-
// Unset → identity, byte-for-byte the unfronted behaviour.
|
|
123
|
-
this.toPod = null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Who is talking, and from where — the prefix on every log line and error. */
|
|
127
|
-
get label() { return `${this.role}/${this.runtime}`; }
|
|
128
|
-
|
|
129
|
-
/** One line for a boot log: what this transport is and what it points at. */
|
|
130
|
-
describe() {
|
|
131
|
-
return `${this.role} over a ${this.runtime} session → ${this.webId?.split('#')[0] || 'unknown'}`;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Install the advertised→pod url map for a fronted identity (urls.toPod). */
|
|
135
|
-
setUrlMap(fn) { this.toPod = typeof fn === 'function' ? fn : null; }
|
|
136
|
-
|
|
137
|
-
async warmup() { return this.session.warmup?.(); }
|
|
138
|
-
|
|
139
|
-
/** What we have asked of this pod, for a status page or an operator. */
|
|
140
|
-
stats() {
|
|
141
|
-
return {
|
|
142
|
-
role: this.role,
|
|
143
|
-
runtime: this.runtime,
|
|
144
|
-
probes: this.probeCount,
|
|
145
|
-
pausedFor: Math.max(0, Math.round((this.pausedUntil - Date.now()) / 1000)),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Observe a throttling answer and arm the pod-wide cooldown.
|
|
151
|
-
*
|
|
152
|
-
* One pause for the whole pod, not one per request: without it every
|
|
153
|
-
* in-flight call rides its own ladder into a server that has already said it
|
|
154
|
-
* is overloaded, which is how a throttle becomes a stampede. A 429 or 503
|
|
155
|
-
* with NO Retry-After still arms the window — the browser copy used to arm
|
|
156
|
-
* nothing at all in that case, which is most cases.
|
|
157
|
-
*/
|
|
158
|
-
_observe(res) {
|
|
159
|
-
if (res.status !== 429 && res.status !== 503) return res;
|
|
160
|
-
// What the server ASKED for is always honoured. What to do when it asked
|
|
161
|
-
// for nothing depends on what the cooldown costs here: a caller that
|
|
162
|
-
// REFUSES for the window loses nothing by assuming a minute, while one that
|
|
163
|
-
// WAITS it out would freeze a person's tab for a minute over a throttle
|
|
164
|
-
// that clears in seconds — so there, absence arms nothing and the caller's
|
|
165
|
-
// own retry ladder paces it instead.
|
|
166
|
-
const asked = retryAfterMs(res, this.maxCooldownMs);
|
|
167
|
-
const ms = asked ?? (this.cooldownMode === 'refuse' ? COOLDOWN_DEFAULT_MS : 0);
|
|
168
|
-
if (!ms) return res;
|
|
169
|
-
this.pausedUntil = Date.now() + ms;
|
|
170
|
-
this.log(`[${this.label}] pod returned ${res.status}${asked ? ', Retry-After' : ''} — `
|
|
171
|
-
+ `holding all requests for ${Math.round(ms / 1000)}s`);
|
|
172
|
-
return res;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Honour an armed cooldown: fail fast, or wait it out, as configured. */
|
|
176
|
-
async _cooldownGate() {
|
|
177
|
-
const left = this.pausedUntil - Date.now();
|
|
178
|
-
if (left <= 0) return;
|
|
179
|
-
if (this.cooldownMode === 'wait') {
|
|
180
|
-
await sleep(Math.min(left, this.maxCooldownMs));
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
throw new Error(`[${this.label}] pod asked us to back off — ${Math.ceil(left / 1000)}s left of its Retry-After`);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* The seam a subclass overrides to add transport behaviour — the browser's
|
|
188
|
-
* retry ladder lives here. Overriding THIS rather than `fetch` is deliberate:
|
|
189
|
-
* an override cannot skip the url map, the cooldown accounting or the
|
|
190
|
-
* deletion deny-list, because those are in `fetch` above it.
|
|
191
|
-
*/
|
|
192
|
-
async _send(url, init) { return this.session.fetch(url, init); }
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* A deliberately CREDENTIAL-FREE request: it asks what a stranger would see,
|
|
196
|
-
* so it cannot go through the session without answering a different question.
|
|
197
|
-
* It still belongs to this pod, though — it opens a socket to it and takes
|
|
198
|
-
* one of its workers — so it observes the same cooldown and is counted.
|
|
199
|
-
*
|
|
200
|
-
* No url map: a probe asks about the ADVERTISED face, which is the whole
|
|
201
|
-
* point of asking.
|
|
202
|
-
*/
|
|
203
|
-
async probe(url, init = {}) {
|
|
204
|
-
await this._cooldownGate();
|
|
205
|
-
this.probeCount++;
|
|
206
|
-
return this._observe(await fetch(url, init));
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Every authenticated request. Nothing below this reaches the session
|
|
211
|
-
* directly: this is the only place the cooldown is both OBSERVED and ARMED,
|
|
212
|
-
* the url map applied, and the deny-list enforced.
|
|
213
|
-
*/
|
|
214
|
-
async fetch(url, init) {
|
|
215
|
-
await this._cooldownGate();
|
|
216
|
-
if (this.toPod) url = this.toPod(url);
|
|
217
|
-
if (String(init?.method || '').toUpperCase() === 'DELETE') this._guardDelete(url);
|
|
218
|
-
return this._observe(await this._send(url, init));
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** Both refusals a DELETE must pass, wherever it was issued from. */
|
|
222
|
-
_guardDelete(url) {
|
|
223
|
-
protectedFromDeletion(url);
|
|
224
|
-
// The pattern list knows the usual name for an access-control document.
|
|
225
|
-
// One the pod named itself is just as fatal to remove. Tolerating an
|
|
226
|
-
// absent map matters: this now runs for EVERY delete, including one issued
|
|
227
|
-
// through `fetch` by a caller holding a minimally-built transport.
|
|
228
|
-
for (const acl of this.aclUrls?.values() ?? []) {
|
|
229
|
-
if (acl === url) throw new Error(`refusing to DELETE an access-control document: ${url}`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async put(url, body, contentType) {
|
|
234
|
-
const res = await this.fetch(url, {
|
|
235
|
-
method: 'PUT', headers: { 'content-type': contentType }, body,
|
|
236
|
-
});
|
|
237
|
-
if (res.status >= 400) throw new Error(`[${this.label}] PUT ${url} → ${res.status}`);
|
|
238
|
-
// Writing a document is usually the step before setting its access, and
|
|
239
|
-
// the answer to the write already says where that lives. Taking it here
|
|
240
|
-
// spares the extra request the ACL write would otherwise make.
|
|
241
|
-
this.noteAclLink(url, res);
|
|
242
|
-
return res;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async putJson(url, obj, contentType = 'application/activity+json') {
|
|
246
|
-
return this.put(url, JSON.stringify(obj), contentType);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// A read that FAILED is not a document that is ABSENT. Returning null for
|
|
250
|
-
// both let a 429 read as "no replies yet", and the caller then rewrote the
|
|
251
|
-
// collection from empty — erasing every reply already recorded. Only a real
|
|
252
|
-
// 404/410 is absence; anything else throws and the caller retries later.
|
|
253
|
-
async getJson(url) {
|
|
254
|
-
const res = await this.fetch(url, { headers: { accept: '*/*' } });
|
|
255
|
-
if (res.status === 404 || res.status === 410) return null;
|
|
256
|
-
if (res.status >= 400) throw new Error(`[${this.label}] GET ${url} → ${res.status}`);
|
|
257
|
-
return res.json().catch(() => null);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async delete(url) {
|
|
261
|
-
const res = await this.fetch(url, { method: 'DELETE' });
|
|
262
|
-
return res.status < 400 || res.status === 404;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/** Remember an access-control location the pod volunteered on a response. */
|
|
266
|
-
noteAclLink(url, res) {
|
|
267
|
-
if (this.aclUrls.has(url)) return;
|
|
268
|
-
const [acl] = linkTargets(res?.headers?.get?.('link'), REL.acl, url);
|
|
269
|
-
if (acl) this.aclUrls.set(url, acl);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Where this resource's access control lives. The pod says so on any
|
|
274
|
-
* response about the resource; a pod that says nothing is taken to keep it
|
|
275
|
-
* at the usual suffix, which is what every server this runs against does.
|
|
276
|
-
*/
|
|
277
|
-
async aclUrlFor(targetUrl) {
|
|
278
|
-
const known = this.aclUrls.get(targetUrl);
|
|
279
|
-
if (known) return known;
|
|
280
|
-
try {
|
|
281
|
-
const res = await this.fetch(targetUrl, { method: 'HEAD' });
|
|
282
|
-
this.noteAclLink(targetUrl, res);
|
|
283
|
-
} catch { /* unreachable or no such resource yet: the suffix below */ }
|
|
284
|
-
const resolved = this.aclUrls.get(targetUrl) || targetUrl + '.acl';
|
|
285
|
-
this.aclUrls.set(targetUrl, resolved);
|
|
286
|
-
return resolved;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Whether writing a WAC document here is meaningful. Asked once per pod, on
|
|
291
|
-
* the first access-control write. A pod that answers with ACP policies is
|
|
292
|
-
* left alone: replacing them with authorizations it does not read would take
|
|
293
|
-
* away the rules actually protecting it.
|
|
294
|
-
*/
|
|
295
|
-
async aclWritable(aclUrl) {
|
|
296
|
-
if (this.aclFlavour !== null) return this.aclFlavour;
|
|
297
|
-
this.aclFlavour = true;
|
|
298
|
-
try {
|
|
299
|
-
const res = await this.fetch(aclUrl, { headers: { accept: 'text/turtle' } });
|
|
300
|
-
if (res.status < 300) {
|
|
301
|
-
const g = $rdf.graph();
|
|
302
|
-
$rdf.parse(await res.text(), g, aclUrl, 'text/turtle');
|
|
303
|
-
const acp = g.statements.some(st => st.predicate.value.startsWith(ACP_NS)
|
|
304
|
-
|| st.object.value.startsWith(ACP_NS));
|
|
305
|
-
if (acp) {
|
|
306
|
-
this.aclFlavour = false;
|
|
307
|
-
this.log(`[${this.label}] this pod states access as ACP policies, which this library does not write — `
|
|
308
|
-
+ 'its access rules are left exactly as they are, and nothing here is published private');
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
} catch { /* absent, unreadable or unparsable: WAC is what we write */ }
|
|
312
|
-
return this.aclFlavour;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// WAC doc granting the public `publicModes` on target, owner full control.
|
|
316
|
-
// An empty publicModes list yields an owner-only document.
|
|
317
|
-
//
|
|
318
|
-
// Built and serialised by rdflib, like every other document written here.
|
|
319
|
-
// This is the highest-consequence RDF in the project: an ACL that comes out
|
|
320
|
-
// malformed, or naming the wrong subject, either locks the owner out or
|
|
321
|
-
// leaves the private trees world-readable. $rdf.sym() also throws on an
|
|
322
|
-
// illegal IRI, so a pod URL with something odd in it fails here rather than
|
|
323
|
-
// silently producing a document that means something else.
|
|
324
|
-
aclDoc(targetUrl, publicModes, { appendAgents = [], readAgents = [], aclUrl = null } = {}) {
|
|
325
|
-
const url = aclUrl || targetUrl + '.acl';
|
|
326
|
-
const doc = $rdf.sym(url);
|
|
327
|
-
const target = $rdf.sym(targetUrl);
|
|
328
|
-
const g = $rdf.graph();
|
|
329
|
-
const authorize = (subject, agentPred, agent, modes) => {
|
|
330
|
-
g.add(subject, RDF('type'), ACL('Authorization'), doc);
|
|
331
|
-
g.add(subject, agentPred, agent, doc);
|
|
332
|
-
g.add(subject, ACL('accessTo'), target, doc);
|
|
333
|
-
g.add(subject, ACL('default'), target, doc);
|
|
334
|
-
for (const m of modes) g.add(subject, ACL('mode'), ACL(m), doc);
|
|
335
|
-
};
|
|
336
|
-
if (publicModes.length) {
|
|
337
|
-
authorize($rdf.sym(url + '#public'), ACL('agentClass'), FOAF('Agent'), publicModes);
|
|
338
|
-
}
|
|
339
|
-
// Named agents granted Append but not public: this is how the inbox is
|
|
340
|
-
// locked to a gateway — public loses Append, the gateway's WebID keeps it.
|
|
341
|
-
appendAgents.forEach((webId, i) =>
|
|
342
|
-
authorize($rdf.sym(url + `#gw${i}`), ACL('agent'), $rdf.sym(webId), ['Append']));
|
|
343
|
-
// Named agents granted Read but not public: this is how a forum's
|
|
344
|
-
// moderators read a queue that names people, without it being public.
|
|
345
|
-
readAgents.forEach((webId, i) =>
|
|
346
|
-
authorize($rdf.sym(url + `#r${i}`), ACL('agent'), $rdf.sym(webId), ['Read']));
|
|
347
|
-
authorize($rdf.sym(url + '#owner'), ACL('agent'), $rdf.sym(this.webId),
|
|
348
|
-
['Read', 'Write', 'Control']);
|
|
349
|
-
return $rdf.serialize(doc, g, url, 'text/turtle');
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
async setAcl(targetUrl, publicModes, opts = {}) {
|
|
353
|
-
// The rule names the resource on the POD. A fronted identity hands in
|
|
354
|
-
// advertised urls; `fetch` maps the request, but a rule whose accessTo
|
|
355
|
-
// named the advertised url would guard a resource the pod does not have,
|
|
356
|
-
// and lock the real one to nobody — the owner included.
|
|
357
|
-
const podTarget = this.toPod ? this.toPod(targetUrl) : targetUrl;
|
|
358
|
-
const url = await this.aclUrlFor(podTarget);
|
|
359
|
-
if (!await this.aclWritable(url)) return null;
|
|
360
|
-
const doc = this.aclDoc(podTarget, publicModes, { ...opts, aclUrl: url });
|
|
361
|
-
// `ifChanged`: read the rule the pod holds and write only when it differs.
|
|
362
|
-
// For a rule restated at every start — the inbox door — a read is the
|
|
363
|
-
// whole cost, where a write took the pod's lock to say the same thing.
|
|
364
|
-
if (opts.ifChanged && await this.aclSame(url, doc)) return { status: 304, unchanged: true };
|
|
365
|
-
return this.put(url, doc, 'text/turtle');
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Whether the pod's rule at `aclUrl` states exactly what `doc` states.
|
|
369
|
-
// Compared as graphs, not bytes: the pod serialises what it holds its own
|
|
370
|
-
// way. Every rule this file writes names its subjects, so triple sets are
|
|
371
|
-
// enough; anything unreadable or with blank nodes reads as different.
|
|
372
|
-
async aclSame(aclUrl, doc) {
|
|
373
|
-
try {
|
|
374
|
-
const res = await this.fetch(aclUrl, { headers: { accept: 'text/turtle' } });
|
|
375
|
-
if (res.status !== 200) return false;
|
|
376
|
-
const triples = (text) => {
|
|
377
|
-
const g = $rdf.graph();
|
|
378
|
-
$rdf.parse(text, g, aclUrl, 'text/turtle');
|
|
379
|
-
if (g.statements.some((st) => st.subject.termType === 'BlankNode' || st.object.termType === 'BlankNode')) return null;
|
|
380
|
-
return g.statements.map((st) => `${st.subject.value} ${st.predicate.value} ${st.object.value}`).sort().join('\n');
|
|
381
|
-
};
|
|
382
|
-
const theirs = triples(await res.text());
|
|
383
|
-
return theirs !== null && theirs === triples(doc);
|
|
384
|
-
} catch { return false; }
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Child documents of an LDP container (URLs under it, excluding aux docs).
|
|
388
|
-
// Revalidated: the inbox is polled every couple of minutes and is usually
|
|
389
|
-
// unchanged, so ask conditionally and let the server answer 304.
|
|
390
|
-
async listContainer(url) {
|
|
391
|
-
// Lazily, not only in the constructor: a caller may hold a transport built
|
|
392
|
-
// with Object.create for a test, setting just the few fields it drives.
|
|
393
|
-
this._listCache ||= new Map();
|
|
394
|
-
const known = this._listCache.get(url);
|
|
395
|
-
const res = await this.fetch(url, {
|
|
396
|
-
headers: { accept: 'text/turtle', ...(known?.etag ? { 'if-none-match': known.etag } : {}) },
|
|
397
|
-
});
|
|
398
|
-
if (res.status === 304 && known) return known.children;
|
|
399
|
-
if (res.status >= 400) return [];
|
|
400
|
-
// rdflib, not a regex: a container listing is RDF, and the gap between
|
|
401
|
-
// "any angle-bracketed thing in the document" and "what this container
|
|
402
|
-
// actually contains" is where quiet bugs live.
|
|
403
|
-
//
|
|
404
|
-
// CSS puts dc:modified and posix:size on every child of the same listing,
|
|
405
|
-
// so age and weight cost nothing extra — which is what lets a drain work
|
|
406
|
-
// oldest-first and lets a backlog be measured without reading any of it.
|
|
407
|
-
let body;
|
|
408
|
-
try {
|
|
409
|
-
body = await readCapped(res, LISTING_MAX_BYTES);
|
|
410
|
-
} catch (e) {
|
|
411
|
-
this.log(`[${this.label}] listing at ${url}: ${e.message} — using the last known listing`);
|
|
412
|
-
return known?.children ?? [];
|
|
413
|
-
}
|
|
414
|
-
// A parse failure is a real fault, not a cache hit: it propagates. The
|
|
415
|
-
// browser copy used to swallow it and hand back a stale listing, which
|
|
416
|
-
// turns a corrupt container into silently missing work.
|
|
417
|
-
const g = $rdf.graph();
|
|
418
|
-
$rdf.parse(body, g, url, 'text/turtle');
|
|
419
|
-
const here = $rdf.sym(url);
|
|
420
|
-
const seen = new Set();
|
|
421
|
-
const receipts = new Set();
|
|
422
|
-
const list = [];
|
|
423
|
-
for (const child of g.each(here, LDP('contains'), null, here)) {
|
|
424
|
-
const u = child.value;
|
|
425
|
-
// `.receipt.json` is a verification receipt a gateway wrote beside an
|
|
426
|
-
// inbox item — read with the item, never enumerated as an item itself.
|
|
427
|
-
if (u.endsWith('.receipt.json')) { receipts.add(u); continue; }
|
|
428
|
-
if (!u.startsWith(url) || u === url || /\.(acl|meta)$/.test(u) || seen.has(u)) continue;
|
|
429
|
-
seen.add(u);
|
|
430
|
-
list.push({
|
|
431
|
-
url: u,
|
|
432
|
-
size: Number(g.any(child, POSIX('size'), null, here)?.value || 0),
|
|
433
|
-
modified: g.any(child, DC('modified'), null, here)?.value || null,
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
// Whether a receipt sits beside each item, so the drain asks for one only
|
|
437
|
-
// where there is one; and the receipts nothing sits beside any more.
|
|
438
|
-
for (const item of list) item.receipt = receipts.has(item.url + '.receipt.json');
|
|
439
|
-
const orphans = [...receipts].filter((r) => !seen.has(r.slice(0, -'.receipt.json'.length)));
|
|
440
|
-
// Oldest first. An LDP listing is a set, so without this a drain works in
|
|
441
|
-
// whatever order the graph happened to parse — a mention from last week
|
|
442
|
-
// after one from today, and no way to make progress predictable.
|
|
443
|
-
list.sort((a, b) => String(a.modified || '').localeCompare(String(b.modified || '')));
|
|
444
|
-
this._listCache.set(url, { etag: res.headers.get('etag'), children: list, orphans });
|
|
445
|
-
return list;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/** Receipts in the last listing of `url` whose item is gone. */
|
|
449
|
-
orphanReceipts(url) { return this._listCache?.get(url)?.orphans ?? []; }
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* The WebID profile advertises the actor as an account:
|
|
453
|
-
* <webId> foaf:account <actor> .
|
|
454
|
-
* <actor> a foaf:OnlineAccount, as:Person|as:Group ; foaf:accountName "@handle@host" .
|
|
455
|
-
* Read–check–write through rdflib. Returns false when the profile already
|
|
456
|
-
* says all of it. The parsed graph must mention the WebID before anything is
|
|
457
|
-
* written back — an empty or foreign body must never become the new profile.
|
|
458
|
-
*/
|
|
459
|
-
async linkAccountInProfile({ actorUrl, accountName, kind = 'person', outbox = null }) {
|
|
460
|
-
const me = $rdf.sym(this.webId);
|
|
461
|
-
const podBase = podBaseOfWebId(this.webId);
|
|
462
|
-
// The profile and what its seeAlso names: what either says is said.
|
|
463
|
-
const docs = await this.profileDocs(podBase);
|
|
464
|
-
const says = (s, p, o) => docs.some(({ g }) => g?.holds(s, p, o));
|
|
465
|
-
const actor = $rdf.sym(actorUrl);
|
|
466
|
-
const wanted = [
|
|
467
|
-
[me, FOAF('account'), actor],
|
|
468
|
-
[actor, RDF('type'), FOAF('OnlineAccount')],
|
|
469
|
-
[actor, RDF('type'), kind === 'group' ? AS('Group') : AS('Person')],
|
|
470
|
-
[actor, FOAF('accountName'), $rdf.literal(accountName)],
|
|
471
|
-
// Where a Solid client posts on this person's behalf (`as:outbox` on the
|
|
472
|
-
// WebID is what dokieli reads); only where a door exists to take it.
|
|
473
|
-
...(outbox ? [[me, AS('outbox'), $rdf.sym(outbox)]] : []),
|
|
474
|
-
];
|
|
475
|
-
const missing = wanted.filter(([s, p, o]) => !says(s, p, o));
|
|
476
|
-
// A handle change leaves the old accountName behind; ours is replaced.
|
|
477
|
-
// Likewise an outbox that moved.
|
|
478
|
-
const stale = [];
|
|
479
|
-
for (const { g } of docs) {
|
|
480
|
-
for (const st of g?.statementsMatching(actor, FOAF('accountName'), null) || []) {
|
|
481
|
-
if (st.object.value !== accountName) stale.push([st.subject, st.predicate, st.object]);
|
|
482
|
-
}
|
|
483
|
-
if (outbox) {
|
|
484
|
-
for (const st of g?.statementsMatching(me, AS('outbox'), null) || []) {
|
|
485
|
-
if (st.object.value !== outbox) stale.push([st.subject, st.predicate, st.object]);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
if (!missing.length && !stale.length) return false;
|
|
490
|
-
// Checked before it is written, to the profile or else to a document its
|
|
491
|
-
// seeAlso names (writeAboutWebId); a patch carries only these statements.
|
|
492
|
-
await this.writeAboutWebId(podBase, { inserts: missing, deletes: stale });
|
|
493
|
-
return true;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* An N3 Patch of exactly these statements, or false when the pod will not
|
|
498
|
-
* take one and the caller should write the document instead.
|
|
499
|
-
*
|
|
500
|
-
* The statements are serialised by rdflib; only the wrapper naming what is
|
|
501
|
-
* being patched is assembled here, because N3's braces have no rdflib form.
|
|
502
|
-
*/
|
|
503
|
-
n3Patch(docUrl, inserts, deletes) {
|
|
504
|
-
const block = (triples) => {
|
|
505
|
-
const g = $rdf.graph();
|
|
506
|
-
for (const [s, p, o] of triples) g.add(s, p, o);
|
|
507
|
-
return $rdf.serialize(null, g, docUrl, 'application/n-triples').trim();
|
|
508
|
-
};
|
|
509
|
-
const clauses = [];
|
|
510
|
-
if (deletes.length) clauses.push(` solid:deletes { ${block(deletes)} }`);
|
|
511
|
-
if (inserts.length) clauses.push(` solid:inserts { ${block(inserts)} }`);
|
|
512
|
-
return `@prefix solid: <http://www.w3.org/ns/solid/terms#>.\n`
|
|
513
|
-
+ `<> a solid:InsertDeletePatch;\n${clauses.join(';\n')}.\n`;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
async patchDocument(docUrl, inserts, deletes) {
|
|
517
|
-
let res;
|
|
518
|
-
try {
|
|
519
|
-
res = await this.fetch(docUrl, {
|
|
520
|
-
method: 'PATCH',
|
|
521
|
-
headers: { 'content-type': 'text/n3' },
|
|
522
|
-
body: this.n3Patch(docUrl, inserts, deletes),
|
|
523
|
-
});
|
|
524
|
-
} catch {
|
|
525
|
-
return false; // no PATCH on this transport at all
|
|
526
|
-
}
|
|
527
|
-
if (res.status < 300) return true;
|
|
528
|
-
// The pod cannot patch. Anything else — a 409 saying what we meant to
|
|
529
|
-
// remove is not there any more — is a real answer, and rewriting the whole
|
|
530
|
-
// document over the top of it would destroy whatever changed it.
|
|
531
|
-
if (res.status === 405 || res.status === 415 || res.status === 501) return false;
|
|
532
|
-
throw new Error(`[${this.label}] PATCH ${docUrl} → ${res.status}`);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// ---- writing a profile or a type index: valid RDF, or nothing ----
|
|
536
|
-
//
|
|
537
|
-
// Two rules. A change is written only if the document it leaves behind is
|
|
538
|
-
// valid RDF: the graph as it would be afterwards is serialised and parsed
|
|
539
|
-
// back, and the subject that must stay described still is. And a profile
|
|
540
|
-
// that will not take a write is not the end: the statements go to the first
|
|
541
|
-
// document its rdfs:seeAlso names, inside the same pod, that takes them —
|
|
542
|
-
// under the same check. A profile is what every Solid app signs in through;
|
|
543
|
-
// one left broken breaks them all. Reading follows the same links.
|
|
544
|
-
|
|
545
|
-
/** An IRI or a literal as a term, for the operations that pass plain values. */
|
|
546
|
-
sym(iri) {
|
|
547
|
-
try { return $rdf.sym(iri); } catch { throw new Error(`not written: ${iri} is not an absolute IRI`); }
|
|
548
|
-
}
|
|
549
|
-
literal(value) { return $rdf.literal(value); }
|
|
550
|
-
|
|
551
|
-
/** A document as a graph, or null when it is not there. */
|
|
552
|
-
async readRdf(docUrl) {
|
|
553
|
-
const res = await this.fetch(docUrl, { headers: { accept: 'text/turtle' } });
|
|
554
|
-
if (res.status === 404 || res.status === 410) return null;
|
|
555
|
-
if (res.status >= 400) throw new Error(`[${this.label}] GET ${docUrl} → ${res.status}`);
|
|
556
|
-
const g = $rdf.graph();
|
|
557
|
-
$rdf.parse(await res.text(), g, docUrl, 'text/turtle');
|
|
558
|
-
return g;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* The document as it would be after the change, checked. Returns the Turtle
|
|
563
|
-
* to write, or throws saying why nothing may be written.
|
|
564
|
-
*/
|
|
565
|
-
checkedRdf(g, docUrl, { inserts = [], deletes = [], mustDescribe = null }) {
|
|
566
|
-
const doc = $rdf.sym(docUrl);
|
|
567
|
-
for (const [s, p, o] of inserts) {
|
|
568
|
-
const bad = termProblem(s) || termProblem(p) || termProblem(o)
|
|
569
|
-
|| (s?.termType === 'Literal' || p?.termType !== 'NamedNode' ? 'a literal subject or predicate' : null);
|
|
570
|
-
if (bad) throw new Error(`not written: ${bad}`);
|
|
571
|
-
}
|
|
572
|
-
const after = $rdf.graph();
|
|
573
|
-
for (const st of g.statementsMatching(null, null, null, doc)) after.add(st.subject, st.predicate, st.object, doc);
|
|
574
|
-
for (const [s, p, o] of deletes) for (const st of after.statementsMatching(s, p, o, doc)) after.remove(st);
|
|
575
|
-
for (const [s, p, o] of inserts) if (!after.holds(s, p, o, doc)) after.add(s, p, o, doc);
|
|
576
|
-
let text;
|
|
577
|
-
try { text = $rdf.serialize(doc, after, docUrl, 'text/turtle'); } catch (e) {
|
|
578
|
-
throw new Error(`not written: ${docUrl} would not serialise (${e.message})`);
|
|
579
|
-
}
|
|
580
|
-
const back = $rdf.graph();
|
|
581
|
-
try { $rdf.parse(text, back, docUrl, 'text/turtle'); } catch (e) {
|
|
582
|
-
throw new Error(`not written: ${docUrl} would not parse back (${e.message})`);
|
|
583
|
-
}
|
|
584
|
-
if (back.statementsMatching(null, null, null, doc).length !== after.statementsMatching(null, null, null, doc).length) {
|
|
585
|
-
throw new Error(`not written: ${docUrl} would not read back as written`);
|
|
586
|
-
}
|
|
587
|
-
if (mustDescribe && !back.statementsMatching($rdf.sym(mustDescribe), null, null, doc).length) {
|
|
588
|
-
throw new Error(`not written: ${docUrl} would no longer describe ${mustDescribe}`);
|
|
589
|
-
}
|
|
590
|
-
return text;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
* Change one document, or refuse. `g` is the document as read (null for a
|
|
595
|
-
* new one). A PATCH carries only these statements; where the pod cannot
|
|
596
|
-
* patch, the whole checked document is written. Returns the write's status.
|
|
597
|
-
*/
|
|
598
|
-
async writeRdfChecked(docUrl, g, { inserts = [], deletes = [], mustDescribe = null }) {
|
|
599
|
-
const current = g || $rdf.graph();
|
|
600
|
-
const doc = $rdf.sym(docUrl);
|
|
601
|
-
const todo = inserts.filter(([s, p, o]) => !current.holds(s, p, o, doc));
|
|
602
|
-
const gone = deletes.filter(([s, p, o]) => current.holds(s, p, o, doc));
|
|
603
|
-
if (!todo.length && !gone.length) return 200;
|
|
604
|
-
const text = this.checkedRdf(current, docUrl, { inserts: todo, deletes: gone, mustDescribe });
|
|
605
|
-
if (g) {
|
|
606
|
-
let res = null;
|
|
607
|
-
try {
|
|
608
|
-
res = await this.fetch(docUrl, { method: 'PATCH', headers: { 'content-type': 'text/n3' },
|
|
609
|
-
body: this.n3Patch(docUrl, todo, gone) });
|
|
610
|
-
} catch { res = null; }
|
|
611
|
-
if (res && res.status < 300) return res.status;
|
|
612
|
-
// A pod that cannot patch gets the whole checked document; any other
|
|
613
|
-
// answer — a refusal, a conflict — is the answer.
|
|
614
|
-
if (res && ![405, 415, 501].includes(res.status)) return res.status;
|
|
615
|
-
}
|
|
616
|
-
const put = await this.fetch(docUrl, { method: 'PUT', headers: { 'content-type': 'text/turtle' }, body: text });
|
|
617
|
-
return put.status;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* The profile and the documents its rdfs:seeAlso names inside this pod,
|
|
622
|
-
* each with its graph, the profile first. What any of them says about the
|
|
623
|
-
* WebID is what the profile says.
|
|
624
|
-
*/
|
|
625
|
-
async profileDocs(podBase) {
|
|
626
|
-
const profileUrl = this.webId.split('#')[0];
|
|
627
|
-
const g = await this.readRdf(profileUrl);
|
|
628
|
-
if (!g) throw new Error(`no profile at ${profileUrl}`);
|
|
629
|
-
const out = [{ url: profileUrl, g, profile: true }];
|
|
630
|
-
const seen = new Set([profileUrl]);
|
|
631
|
-
for (const st of g.statementsMatching(null, RDFS('seeAlso'), null)) {
|
|
632
|
-
const url = st.object.value.split('#')[0];
|
|
633
|
-
if (seen.has(url) || !url.startsWith(podBase)) continue; // inside this pod only
|
|
634
|
-
seen.add(url);
|
|
635
|
-
out.push({ url, g: await this.readRdf(url).catch(() => null), profile: false });
|
|
636
|
-
}
|
|
637
|
-
return out;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
/** Every value the WebID has for `predicate`, across the profile and its seeAlso documents. */
|
|
641
|
-
webIdValues(docs, predicate) {
|
|
642
|
-
const me = $rdf.sym(this.webId);
|
|
643
|
-
const p = $rdf.sym(predicate);
|
|
644
|
-
const out = [];
|
|
645
|
-
for (const { g } of docs) for (const st of g?.statementsMatching(me, p, null) || []) out.push(st.object.value);
|
|
646
|
-
return [...new Set(out)];
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
/**
|
|
650
|
-
* Write statements about the WebID: to the profile, or — if the profile will
|
|
651
|
-
* not take them — to the first of its seeAlso documents that will. Every
|
|
652
|
-
* write checked. Returns the document written to, or null when nothing
|
|
653
|
-
* needed writing; throws when none would take it.
|
|
654
|
-
*/
|
|
655
|
-
async writeAboutWebId(podBase, { inserts = [], deletes = [] }) {
|
|
656
|
-
const docs = await this.profileDocs(podBase);
|
|
657
|
-
const holds = ([s, p, o]) => docs.some(({ g }) => g?.holds(s, p, o));
|
|
658
|
-
const todo = inserts.filter(t => !holds(t));
|
|
659
|
-
const gone = deletes.filter(holds);
|
|
660
|
-
if (!todo.length && !gone.length) return null;
|
|
661
|
-
const refusals = [];
|
|
662
|
-
for (const d of docs) {
|
|
663
|
-
const here = gone.filter(([s, p, o]) => d.g?.holds(s, p, o));
|
|
664
|
-
const status = await this.writeRdfChecked(d.url, d.g, {
|
|
665
|
-
inserts: todo, deletes: here, mustDescribe: d.profile ? this.webId : null,
|
|
666
|
-
});
|
|
667
|
-
if (status < 300) return d.url;
|
|
668
|
-
refusals.push(`${d.url} → ${status}`);
|
|
669
|
-
if (status !== 401 && status !== 403) break; // a real answer, not "not yours to write"
|
|
670
|
-
}
|
|
671
|
-
throw new Error(`not written: neither the profile nor a document it names would take it (${refusals.join('; ')})`);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
}
|