fedipod-server 0.21.0 → 0.28.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 +5 -4
- package/dist/claims.d.ts +3 -1
- package/dist/claims.js +20 -7
- package/dist/components/context.jsonld +6 -0
- package/dist/handler.d.ts +29 -1
- package/dist/handler.js +89 -4
- package/dist/handler.jsonld +36 -1
- package/lib/client/c2s.mjs +100 -3
- package/lib/client/masto/accounts.mjs +44 -4
- package/lib/client/masto/body.mjs +90 -3
- package/lib/client/masto/index.mjs +5 -1
- package/lib/client/masto/instance.mjs +11 -3
- package/lib/client/masto/media.mjs +3 -57
- package/lib/client/masto/oauth.mjs +66 -9
- package/lib/client/masto/render.mjs +62 -5
- package/lib/client/masto/statuses.mjs +25 -1
- package/lib/client/masto/timelines.mjs +128 -6
- package/lib/connections/tagfeed.mjs +42 -5
- package/lib/core/as2.mjs +36 -5
- package/lib/core/contexts/index.mjs +14 -0
- package/lib/core/contexts/litepub-0.1.json +60 -0
- package/lib/core/contexts/map.json +3 -1
- package/lib/core/contexts/quotes.json +18 -0
- package/lib/core/graphview.mjs +41 -3
- package/lib/core/intake/activities.mjs +136 -2
- package/lib/core/intake/activity.mjs +79 -1
- package/lib/core/intake/group.mjs +37 -3
- package/lib/core/intake/index.mjs +32 -2
- package/lib/core/intake/notes.mjs +60 -3
- package/lib/core/lease.mjs +12 -2
- package/lib/core/profile-page.mjs +104 -0
- package/lib/core/publisher/collections.mjs +6 -1
- package/lib/core/publisher/index.mjs +35 -17
- package/lib/core/publisher/notes.mjs +104 -3
- package/lib/core/shapes/activitystreams.ttl +28 -0
- package/lib/core/shapes/index.mjs +26 -1
- package/lib/core/wire-quotes.mjs +71 -0
- package/lib/core/wire.mjs +59 -103
- package/lib/device/admin/routes/lifecycle.mjs +9 -0
- package/lib/device/admin/static.mjs +4 -1
- package/lib/device/admin/surface.mjs +26 -2
- package/lib/device/certs.mjs +18 -2
- package/lib/device/cli/commands/setup.mjs +6 -2
- package/lib/device/cli/commands/state.mjs +3 -1
- package/lib/device/gateway-move.mjs +58 -0
- package/lib/device/migrate.mjs +3 -1
- package/lib/device/setup.mjs +67 -7
- package/lib/gateway/front-core.mjs +307 -36
- package/lib/gateway/gateway-core.mjs +15 -6
- package/lib/gateway/notices.mjs +84 -0
- package/lib/gateway/quiet.mjs +184 -0
- package/lib/pod/root.mjs +6 -3
- package/lib/pod/state.mjs +8 -7
- package/lib/pod/transport.mjs +5 -1
- package/lib/session/README.md +123 -0
- package/lib/session/demo.html +94 -0
- package/lib/session/fedi-account.mjs +346 -0
- package/lib/session/fedi-login.mjs +123 -0
- package/lib/session/oidc-session.mjs +233 -0
- package/lib/session/package.json +41 -0
- package/package.json +1 -1
- package/run-agent.mjs +28 -0
- package/vendor/gate.cjs +6 -3
- package/web/admin/bar.css +66 -0
- package/web/admin/bar.js +38 -0
- package/web/admin/client/index.html +31 -3
- package/web/admin/client-pick.js +79 -0
- package/web/admin/gateway.js +22 -0
- package/web/admin/index.html +54 -13
- package/web/admin/notices-bar.js +90 -0
- package/web/admin/oauth-signin.mjs +37 -0
- package/web/admin/record.js +13 -1
- package/web/admin/setup/index.html +31 -3
- package/web/admin/tokens.css +15 -3
- package/web/admin/upkeep.js +4 -5
- package/web/app/README.md +14 -10
- package/web/app/admin-facade.mjs +51 -12
- package/web/app/agent.mjs +157 -19
- package/web/app/authorize_interaction.html +56 -0
- package/web/app/authorize_interaction.js +69 -0
- package/web/app/boot.mjs +233 -95
- package/web/app/dist/boot.js +477 -569
- package/web/app/dist/boot.js.map +4 -4
- package/web/app/dist/sw.js +1861 -532
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/gateway-move.mjs +60 -0
- package/web/app/index.html +40 -49
- package/web/app/keys-browser.mjs +12 -15
- package/web/app/keystore.mjs +19 -36
- package/web/app/oidc-session.mjs +5 -188
- package/web/app/signup.mjs +172 -132
- package/web/app/sw-src.mjs +1 -1
- package/web/app/update.js +66 -0
- package/web/front/admin.html +5 -3
- package/web/front/new-account.html +2 -2
- package/web/front/notices.html +69 -0
- package/web/front/notices.js +107 -0
- package/web/front/run.html +15 -8
- package/web/front/run.js +124 -36
- package/web/icons/forum.svg +11 -0
- package/web/icons/gateway.svg +14 -0
- package/web/icons/local.svg +11 -0
- package/web/app/index.html~ +0 -152
- package/web/app/site/_headers +0 -9
- package/web/app/site/_redirects +0 -22
- package/web/app/site/admin/actors.js +0 -145
- package/web/app/site/admin/bar.css +0 -60
- package/web/app/site/admin/bar.js +0 -61
- package/web/app/site/admin/client/client.js +0 -66
- package/web/app/site/admin/client/index.html +0 -43
- package/web/app/site/admin/common.js +0 -23
- package/web/app/site/admin/connections.js +0 -112
- package/web/app/site/admin/gateway.js +0 -111
- package/web/app/site/admin/group.js +0 -258
- package/web/app/site/admin/index.html +0 -564
- package/web/app/site/admin/record.js +0 -378
- package/web/app/site/admin/setup/index.html +0 -215
- package/web/app/site/admin/setup/setup.js +0 -326
- package/web/app/site/admin/tokens.css +0 -56
- package/web/app/site/admin/upkeep.js +0 -178
- package/web/app/site/admin/window.css +0 -55
- package/web/app/site/admin/window.js +0 -136
- package/web/app/site/app/404.html +0 -40
- package/web/app/site/app/_headers +0 -2
- package/web/app/site/app/apple-touch-icon.png +0 -0
- package/web/app/site/app/assets/ICONS-DIPJeU0C.js +0 -2
- package/web/app/site/app/assets/ICONS-DIPJeU0C.js.map +0 -1
- package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js +0 -183
- package/web/app/site/app/assets/ScheduledAtField-D-Abfo4J.js.map +0 -1
- package/web/app/site/app/assets/Temml-CCjoacWt.woff2 +0 -0
- package/web/app/site/app/assets/__vite-browser-external-C7iut881.js +0 -2
- package/web/app/site/app/assets/__vite-browser-external-C7iut881.js.map +0 -1
- package/web/app/site/app/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
- package/web/app/site/app/assets/catch-up-O5q5C75Z.png +0 -0
- package/web/app/site/app/assets/chunk-aKtaBQYM.js +0 -1
- package/web/app/site/app/assets/compose-CFEx7wb4.js +0 -2
- package/web/app/site/app/assets/compose-CFEx7wb4.js.map +0 -1
- package/web/app/site/app/assets/compose-ZJN5xSb4.js +0 -35
- package/web/app/site/app/assets/compose-ZJN5xSb4.js.map +0 -1
- package/web/app/site/app/assets/debug.module-YFQgwnIr.js +0 -52
- package/web/app/site/app/assets/debug.module-YFQgwnIr.js.map +0 -1
- package/web/app/site/app/assets/dom-p1YcJ5Rw.js +0 -2
- package/web/app/site/app/assets/dom-p1YcJ5Rw.js.map +0 -1
- package/web/app/site/app/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
- package/web/app/site/app/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
- package/web/app/site/app/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
- package/web/app/site/app/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
- package/web/app/site/app/assets/icons/add-circle-line-WD0mwORD.js +0 -1
- package/web/app/site/app/assets/icons/alert-line-CpFUMf9s.js +0 -1
- package/web/app/site/app/assets/icons/android-2-line-Bzxnrd3S.js +0 -1
- package/web/app/site/app/assets/icons/announcement-line-5nwaJFJm.js +0 -1
- package/web/app/site/app/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +0 -1
- package/web/app/site/app/assets/icons/arrow-down-line-B8uzXzq6.js +0 -1
- package/web/app/site/app/assets/icons/arrow-left-line-CGrGT9C-.js +0 -1
- package/web/app/site/app/assets/icons/arrow-right-line-CE-HERfX.js +0 -1
- package/web/app/site/app/assets/icons/arrow-to-up-line-CvdQjmcy.js +0 -1
- package/web/app/site/app/assets/icons/arrow-up-circle-line-DEparSNF.js +0 -1
- package/web/app/site/app/assets/icons/arrow-up-line-C7yBeugc.js +0 -1
- package/web/app/site/app/assets/icons/arrows-right-line-BtbXmJGP.js +0 -1
- package/web/app/site/app/assets/icons/at-line-CF66Bznk.js +0 -1
- package/web/app/site/app/assets/icons/attachment-line-BY8It-l9.js +0 -1
- package/web/app/site/app/assets/icons/blockquote-line-BS6pU_hm.js +0 -1
- package/web/app/site/app/assets/icons/board-line-C0UWXyA6.js +0 -1
- package/web/app/site/app/assets/icons/bookmark-line-1CtX5JLF.js +0 -1
- package/web/app/site/app/assets/icons/building-5-line-Dd59MzbO.js +0 -1
- package/web/app/site/app/assets/icons/bus-2-line-6_dvA0GG.js +0 -1
- package/web/app/site/app/assets/icons/calendar-day-line-Crpl_fmu.js +0 -1
- package/web/app/site/app/assets/icons/calendar-month-line-vommts2C.js +0 -1
- package/web/app/site/app/assets/icons/calendar-time-add-line-BU_DTlQk.js +0 -1
- package/web/app/site/app/assets/icons/camera-line-Cv69MEei.js +0 -1
- package/web/app/site/app/assets/icons/celebrate-line-B8DvIoi0.js +0 -1
- package/web/app/site/app/assets/icons/chart-bar-line-FK-QOmtF.js +0 -1
- package/web/app/site/app/assets/icons/chart-line-line-BGcJAFWw.js +0 -1
- package/web/app/site/app/assets/icons/chat-3-line-YFK2-hwz.js +0 -1
- package/web/app/site/app/assets/icons/check-circle-line-DsAxFr5-.js +0 -1
- package/web/app/site/app/assets/icons/clipboard-line-BBp364gJ.js +0 -1
- package/web/app/site/app/assets/icons/close-circle-line-CICdiuGz.js +0 -1
- package/web/app/site/app/assets/icons/close-line-DYrFWcjv.js +0 -1
- package/web/app/site/app/assets/icons/cloud-line-tiQAbah8.js +0 -1
- package/web/app/site/app/assets/icons/code-line-fAl8iiV3.js +0 -1
- package/web/app/site/app/assets/icons/comment-2-line-CR3g7WeB.js +0 -1
- package/web/app/site/app/assets/icons/copy-2-line-CLW69yF5.js +0 -1
- package/web/app/site/app/assets/icons/delete-2-line-B8xmbr4n.js +0 -1
- package/web/app/site/app/assets/icons/document-3-line-DBGNhAg2.js +0 -1
- package/web/app/site/app/assets/icons/document-line-C7mb9sb7.js +0 -1
- package/web/app/site/app/assets/icons/down-line-BGqSSThd.js +0 -1
- package/web/app/site/app/assets/icons/edit-4-line-DcgGaEhx.js +0 -1
- package/web/app/site/app/assets/icons/emoji-2-line-D8YgnM-y.js +0 -1
- package/web/app/site/app/assets/icons/exit-line-Dc07BS-x.js +0 -1
- package/web/app/site/app/assets/icons/external-link-line-BvpSkmjO.js +0 -1
- package/web/app/site/app/assets/icons/eye-2-line-CiWXx-f_.js +0 -1
- package/web/app/site/app/assets/icons/eye-close-line-BgTaSJ4P.js +0 -1
- package/web/app/site/app/assets/icons/filter-2-line-B_mR2usy.js +0 -1
- package/web/app/site/app/assets/icons/filter-line-C_nm-3cU.js +0 -1
- package/web/app/site/app/assets/icons/flag-1-line-C2HB05I0.js +0 -1
- package/web/app/site/app/assets/icons/forbid-circle-line-D3LTT9yd.js +0 -1
- package/web/app/site/app/assets/icons/formula-line-DLrt2d6j.js +0 -1
- package/web/app/site/app/assets/icons/grid-line-CgUmEikf.js +0 -1
- package/web/app/site/app/assets/icons/group-line-C5fnX0wU.js +0 -1
- package/web/app/site/app/assets/icons/hand-finger-2-line-BGgxbuL9.js +0 -1
- package/web/app/site/app/assets/icons/hashtag-line-CyTEEXBb.js +0 -1
- package/web/app/site/app/assets/icons/heart-crack-line-BZddbO2C.js +0 -1
- package/web/app/site/app/assets/icons/heart-line-CJ7K47J4.js +0 -1
- package/web/app/site/app/assets/icons/history-2-line-kWWvFzqv.js +0 -1
- package/web/app/site/app/assets/icons/history-line-bGQiwbTH.js +0 -1
- package/web/app/site/app/assets/icons/home-3-line-fRU1zeLG.js +0 -1
- package/web/app/site/app/assets/icons/information-line-iNk6-sOY.js +0 -1
- package/web/app/site/app/assets/icons/keyboard-line-DsCyuQNz.js +0 -1
- package/web/app/site/app/assets/icons/layout-4-line-CZkOX52Z.js +0 -1
- package/web/app/site/app/assets/icons/layout-5-line-B_znt5wO.js +0 -1
- package/web/app/site/app/assets/icons/left-line-eUw0tya4.js +0 -1
- package/web/app/site/app/assets/icons/lightning-line-DaKoDdix.js +0 -1
- package/web/app/site/app/assets/icons/link-2-line-DCqLmMA7.js +0 -1
- package/web/app/site/app/assets/icons/list-check-line-BAxosBh9.js +0 -1
- package/web/app/site/app/assets/icons/lock-line-CPoaXeUW.js +0 -1
- package/web/app/site/app/assets/icons/mail-line-B0P8aa1L.js +0 -1
- package/web/app/site/app/assets/icons/moon-line-CEqNAToP.js +0 -1
- package/web/app/site/app/assets/icons/more-1-fill-Dw2e5xab.js +0 -1
- package/web/app/site/app/assets/icons/more-3-line-BIiVjYTz.js +0 -1
- package/web/app/site/app/assets/icons/notification-line-BZZq2Gtu.js +0 -1
- package/web/app/site/app/assets/icons/pencil-line-CKh8-A1v.js +0 -1
- package/web/app/site/app/assets/icons/photo-album-line-Bnfg2u40.js +0 -1
- package/web/app/site/app/assets/icons/pin-line-DpTolyhs.js +0 -1
- package/web/app/site/app/assets/icons/play-fill-B0-G_8lX.js +0 -1
- package/web/app/site/app/assets/icons/qrcode-2-line-B6SPCaHn.js +0 -1
- package/web/app/site/app/assets/icons/quill-pen-line-B6LUrnCP.js +0 -1
- package/web/app/site/app/assets/icons/quote-left-fill-hQZ1cj_6.js +0 -1
- package/web/app/site/app/assets/icons/radar-line-CKyroTth.js +0 -1
- package/web/app/site/app/assets/icons/react-line-CoIKhhtK.js +0 -1
- package/web/app/site/app/assets/icons/refresh-2-line-D0mTF97S.js +0 -1
- package/web/app/site/app/assets/icons/right-line-B3KWJaqc.js +0 -1
- package/web/app/site/app/assets/icons/rocket-line-4KZl537D.js +0 -1
- package/web/app/site/app/assets/icons/round-fill-C69NTrZ0.js +0 -1
- package/web/app/site/app/assets/icons/round-line-B9k_1uo-.js +0 -1
- package/web/app/site/app/assets/icons/route-line-B28ReHcE.js +0 -1
- package/web/app/site/app/assets/icons/rows-4-line-CrBniagM.js +0 -1
- package/web/app/site/app/assets/icons/scan-line-DSf74YdP.js +0 -1
- package/web/app/site/app/assets/icons/search-2-line-B6geIU1z.js +0 -1
- package/web/app/site/app/assets/icons/settings-3-line-azJwK47w.js +0 -1
- package/web/app/site/app/assets/icons/settings-6-line-C_5zc5YR.js +0 -1
- package/web/app/site/app/assets/icons/share-2-line-D5MxWjrm.js +0 -1
- package/web/app/site/app/assets/icons/share-forward-line-Cp61fZzZ.js +0 -1
- package/web/app/site/app/assets/icons/sparkles-2-line-DYYyXTaW.js +0 -1
- package/web/app/site/app/assets/icons/sparkles-line-C2hY1s6I.js +0 -1
- package/web/app/site/app/assets/icons/time-line-BF3eet1v.js +0 -1
- package/web/app/site/app/assets/icons/transfer-4-line-oisfvTBw.js +0 -1
- package/web/app/site/app/assets/icons/translate-line-BFWol2Ce.js +0 -1
- package/web/app/site/app/assets/icons/unlock-line-X_x5vwv3.js +0 -1
- package/web/app/site/app/assets/icons/upload-3-line-DgzwUJeW.js +0 -1
- package/web/app/site/app/assets/icons/user-4-line-BVSynZCp.js +0 -1
- package/web/app/site/app/assets/icons/user-add-2-line-BlGS5Q8q.js +0 -1
- package/web/app/site/app/assets/icons/user-add-line-BbXdHG4z.js +0 -1
- package/web/app/site/app/assets/icons/user-edit-line-BFAHOV0X.js +0 -1
- package/web/app/site/app/assets/icons/user-follow-line-BXux0wSd.js +0 -1
- package/web/app/site/app/assets/icons/user-star-line-DdYlpHLU.js +0 -1
- package/web/app/site/app/assets/icons/user-warning-line-Cj43j0_E.js +0 -1
- package/web/app/site/app/assets/icons/user-x-line-DIDgNlPH.js +0 -1
- package/web/app/site/app/assets/icons/volume-line-Bs390KQt.js +0 -1
- package/web/app/site/app/assets/icons/volume-mute-line-DzlzIqFZ.js +0 -1
- package/web/app/site/app/assets/icons/walk-line-yNXfB-md.js +0 -1
- package/web/app/site/app/assets/icons/world-2-line-CenkGIwj.js +0 -1
- package/web/app/site/app/assets/icons/zoom-in-line-CaENUphf.js +0 -1
- package/web/app/site/app/assets/icons/zoom-out-line-BDNk0EWo.js +0 -1
- package/web/app/site/app/assets/instances-BNPptnn-.json +0 -333
- package/web/app/site/app/assets/locales/ar-SA-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/ca-ES-DHRW86Tz.js +0 -1
- package/web/app/site/app/assets/locales/cs-CZ-CKoqEuA1.js +0 -1
- package/web/app/site/app/assets/locales/de-DE-ByRPMRFy.js +0 -1
- package/web/app/site/app/assets/locales/eo-UY-CPC7wLCS.js +0 -1
- package/web/app/site/app/assets/locales/es-ES-dJa0Fd9t.js +0 -1
- package/web/app/site/app/assets/locales/eu-ES-C3djlPR2.js +0 -1
- package/web/app/site/app/assets/locales/fa-IR-DTT490Ck.js +0 -1
- package/web/app/site/app/assets/locales/fi-FI-B23MCPuz.js +0 -1
- package/web/app/site/app/assets/locales/fr-FR-BcaQsO5C.js +0 -1
- package/web/app/site/app/assets/locales/gl-ES-BR-CVKBN.js +0 -1
- package/web/app/site/app/assets/locales/he-IL-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/hu-HU-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/it-IT-C1136UFV.js +0 -1
- package/web/app/site/app/assets/locales/ja-JP-CRZmRAhD.js +0 -1
- package/web/app/site/app/assets/locales/kab-Cz8xH_TT.js +0 -1
- package/web/app/site/app/assets/locales/ko-KR-CG9UiY4o.js +0 -1
- package/web/app/site/app/assets/locales/lt-LT-CJcI6sHZ.js +0 -1
- package/web/app/site/app/assets/locales/nb-NO-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/nl-NL-DxN4Yc1t.js +0 -1
- package/web/app/site/app/assets/locales/oc-FR-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/pl-PL-DkwGXf6P.js +0 -1
- package/web/app/site/app/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +0 -1
- package/web/app/site/app/assets/locales/pt-BR-nGL0HdFS.js +0 -1
- package/web/app/site/app/assets/locales/pt-PT-Cp17aj2z.js +0 -1
- package/web/app/site/app/assets/locales/ru-RU-0Y8cIoaK.js +0 -1
- package/web/app/site/app/assets/locales/th-TH-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/tok-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/tr-TR-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/locales/uk-UA-C5E3dFBf.js +0 -1
- package/web/app/site/app/assets/locales/zh-CN-BHJi7_et.js +0 -1
- package/web/app/site/app/assets/locales/zh-TW-CfheRbCu.js +0 -1
- package/web/app/site/app/assets/main-BdqNbG-a.js +0 -26
- package/web/app/site/app/assets/main-BdqNbG-a.js.map +0 -1
- package/web/app/site/app/assets/mock-home-DGhR__He.js +0 -2
- package/web/app/site/app/assets/mock-home-DGhR__He.js.map +0 -1
- package/web/app/site/app/assets/multi-column-ROck0NVt.jpg +0 -0
- package/web/app/site/app/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
- package/web/app/site/app/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
- package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js +0 -2
- package/web/app/site/app/assets/polyfill-force-H8sUkLGp.js.map +0 -1
- package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js +0 -20
- package/web/app/site/app/assets/pwa-viewport-ogKbF51K.js.map +0 -1
- package/web/app/site/app/assets/style-B1iLPTPR.css +0 -2
- package/web/app/site/app/assets/temml-DuyIQMei.js +0 -97
- package/web/app/site/app/assets/temml-DuyIQMei.js.map +0 -1
- package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js +0 -2
- package/web/app/site/app/assets/tinyld.light.node-DtbBhLYC.js.map +0 -1
- package/web/app/site/app/assets/year-in-posts-B2lywqdx.png +0 -0
- package/web/app/site/app/assets/year-in-posts-DJv_star.js +0 -3
- package/web/app/site/app/assets/year-in-posts-DJv_star.js.map +0 -1
- package/web/app/site/app/compose/index.html +0 -20
- package/web/app/site/app/favicon.ico +0 -0
- package/web/app/site/app/index.html +0 -73
- package/web/app/site/app/logo-192.png +0 -0
- package/web/app/site/app/logo-512.png +0 -0
- package/web/app/site/app/logo-badge-72.png +0 -0
- package/web/app/site/app/logo-maskable-512.png +0 -0
- package/web/app/site/app/logo-monochrome-512.png +0 -0
- package/web/app/site/app/logo-monochrome-maskable-512.png +0 -0
- package/web/app/site/app/manifest.webmanifest +0 -1
- package/web/app/site/app/og-image-2.jpg +0 -0
- package/web/app/site/app/og-image.png +0 -0
- package/web/app/site/app/robots.txt +0 -2
- package/web/app/site/app/version.json +0 -1
- package/web/app/site/boot.js +0 -34277
- package/web/app/site/index.html +0 -251
- package/web/app/site/sw.js +0 -71331
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// gateway-move.mjs — the second half of moving an address from one gateway
|
|
2
|
+
// to another (signup.mjs moveIn is the first). It runs on the agent's first
|
|
3
|
+
// active boot under the new ids, once the new actor is published with the
|
|
4
|
+
// old one as an alias, and it is safe to run again: nothing here is done
|
|
5
|
+
// twice with the same result changed.
|
|
6
|
+
//
|
|
7
|
+
// 1. The old gateway is told the address moved (POST /api/move, proved
|
|
8
|
+
// with the pod session as attach was). It serves the old actor as a
|
|
9
|
+
// moved stub from then on — same key, old id, `movedTo` the new one.
|
|
10
|
+
// 2. A Move goes to every follower, FROM the old actor, signed under the
|
|
11
|
+
// old key id, through the old gateway's relay: that is the only
|
|
12
|
+
// signature a follower's server can verify against the old actor.
|
|
13
|
+
//
|
|
14
|
+
// The followers' servers fetch the old actor, find `movedTo`, fetch the new
|
|
15
|
+
// one, find the old among its `alsoKnownAs`, and move the follow.
|
|
16
|
+
import { RelayDeliverer } from './deliver-relay.mjs';
|
|
17
|
+
import { moveActivity } from '../../lib/core/wire.mjs';
|
|
18
|
+
|
|
19
|
+
export async function completeGatewayMove(agent) {
|
|
20
|
+
const cfg = agent.store.getConfig();
|
|
21
|
+
const mv = cfg?.movedFrom;
|
|
22
|
+
if (!mv || mv.completedAt) return null;
|
|
23
|
+
const log = agent.log;
|
|
24
|
+
const newActor = agent.urls.actor;
|
|
25
|
+
|
|
26
|
+
// 1. the old gateway
|
|
27
|
+
let told;
|
|
28
|
+
try {
|
|
29
|
+
told = await agent.sessionFetch(`${mv.gateway}/api/move`, {
|
|
30
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
31
|
+
body: JSON.stringify({ handle: mv.handle, movedTo: newActor }),
|
|
32
|
+
});
|
|
33
|
+
} catch (e) { told = { status: 0, statusText: e.message }; }
|
|
34
|
+
if (told.status !== 200) {
|
|
35
|
+
// Left pending: the next active boot tries again. Followers are not told
|
|
36
|
+
// until the old actor can vouch for the Move.
|
|
37
|
+
log(`gateway move: ${mv.gateway} did not mark @${mv.handle} as moved (${told.status || told.statusText}) — will retry`);
|
|
38
|
+
return { told: false };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. the Move, from the old address
|
|
42
|
+
const contacts = agent.store.getContacts();
|
|
43
|
+
const inboxes = [...new Set(contacts.followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean))];
|
|
44
|
+
const activity = moveActivity({ actor: mv.actor }, newActor, Date.now());
|
|
45
|
+
const sender = new RelayDeliverer({
|
|
46
|
+
passive: true, store: agent.store, rsaPrivate: agent.deliverer.rsaPrivate,
|
|
47
|
+
keyId: `${mv.actor}#main-key`, actorId: mv.actor, log,
|
|
48
|
+
relayUrl: `${mv.gateway}/api/relay`, handle: mv.handle, sessionFetch: agent.sessionFetch,
|
|
49
|
+
});
|
|
50
|
+
let sent = 0; let failed = 0;
|
|
51
|
+
for (const inbox of inboxes) {
|
|
52
|
+
try { await sender.deliverNow(inbox, activity); sent++; }
|
|
53
|
+
catch (e) { failed++; log(`gateway move: Move to ${inbox} failed: ${e.message}`); }
|
|
54
|
+
}
|
|
55
|
+
const done = { ...mv, completedAt: new Date().toISOString(), moveSent: sent, moveFailed: failed };
|
|
56
|
+
agent.store.setConfig({ ...agent.store.getConfig(), movedFrom: done });
|
|
57
|
+
await agent.store.flush?.();
|
|
58
|
+
log(`moved from ${mv.actor} to ${newActor}: Move sent to ${sent} inbox(es)${failed ? `, ${failed} failed` : ''}`);
|
|
59
|
+
return done;
|
|
60
|
+
}
|
package/web/app/index.html
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https:; form-action 'self'; base-uri 'none'">
|
|
7
7
|
<title>FediPod</title>
|
|
8
|
+
<link rel="icon" href="/icons/local.svg" type="image/svg+xml">
|
|
8
9
|
<style>
|
|
9
10
|
:root {
|
|
10
11
|
color-scheme: light dark;
|
|
11
12
|
font-size: 125%; /* 20px default (125% of 16) */
|
|
12
13
|
--bg: #ffffff; --fg: #1a1a1a;
|
|
13
|
-
--heading: #
|
|
14
|
+
--heading: #2c4a35; --sub: #565656; --line: #b0b0b0; --accent: #12467e;
|
|
14
15
|
--btn: #3a5f43; --btn-hover: #30503a; --btn-text: #ffffff;
|
|
15
16
|
--field-bg: #ececec; --field-text: #141414; --field-border: #6f6f6f;
|
|
16
17
|
--select-bg: #dce7f3; --select-border: #12467e;
|
|
@@ -19,14 +20,14 @@
|
|
|
19
20
|
@media (prefers-color-scheme: dark) {
|
|
20
21
|
:root {
|
|
21
22
|
--bg: #191919; --fg: #f3f3f3;
|
|
22
|
-
--heading: #
|
|
23
|
+
--heading: #a7d4b5; --sub: #d2d2d2; --line: #6d6d6d; --accent: #8fc2f2;
|
|
23
24
|
--field-bg: #474747; --field-text: #f5f5f5; --field-border: #9aa0a6;
|
|
24
25
|
--select-bg: #2f4a66; --select-border: #8fc2f2;
|
|
25
26
|
--btn: #4a7a54; --btn-hover: #3f6a49;
|
|
26
27
|
--err: #ff9b9b; --warn: #ffb366; --focus: #8fc2f2;
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
|
-
body { font: 1rem/1.55
|
|
30
|
+
body { font: 1rem/1.55 Arial, Helvetica, sans-serif; margin: 0; background: var(--bg); color: var(--fg); }
|
|
30
31
|
#page { max-width: 42rem; margin: 0 auto; padding: 1.75rem 1rem 4rem; box-sizing: border-box; }
|
|
31
32
|
h1, h2, h3 { color: var(--heading); }
|
|
32
33
|
h1 { font-size: 1.6rem; margin: 0 0 .5rem; }
|
|
@@ -103,36 +104,35 @@ a { color: var(--accent); }
|
|
|
103
104
|
<p><button type="button" class="primary" id="create">create an account</button></p>
|
|
104
105
|
<p>Fedipod.net is a FediPod Gateway operated by Jeff Zucker. You can detach from the gateway at any
|
|
105
106
|
time and transfer your account either to your own FediPod Gateway (easy to set up on a free tier Netlify)
|
|
106
|
-
or to any Fediverse server. All of your data lives on your pod and
|
|
107
|
-
|
|
107
|
+
or to any Fediverse server. All of your data lives on your pod and is yours regardless of which
|
|
108
|
+
gateway you are attached to.</p>
|
|
108
109
|
<p>For more information, see
|
|
109
110
|
<a href="https://github.com/jeff-zucker/FediPod">github.com/jeff-zucker/FediPod</a></p>
|
|
110
111
|
</section>
|
|
111
112
|
|
|
112
|
-
<!-- Shown only
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
key, locked under the password they use now. -->
|
|
113
|
+
<!-- Shown only for an account made before 2026-09-22, whose signing key on
|
|
114
|
+
the pod is still under the password given at sign-up. It is opened once
|
|
115
|
+
and stored as it is from then on. The second block is for someone who no
|
|
116
|
+
longer has that password: a new key. -->
|
|
117
117
|
<section id="unlock" hidden>
|
|
118
|
-
<h2>
|
|
119
|
-
<p>Your
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
<h2>Open your signing key</h2>
|
|
119
|
+
<p>Your account was made when this site kept your signing key under the
|
|
120
|
+
password you gave at sign-up. Enter that password once. Your key is then
|
|
121
|
+
stored on your pod as it is, behind your pod's own login, and no browser
|
|
122
|
+
asks again.</p>
|
|
122
123
|
<p class="row">
|
|
123
124
|
<input type="password" id="unlock-password" autocomplete="current-password"
|
|
124
|
-
placeholder="
|
|
125
|
-
<button type="button" class="primary" id="unlock-go">
|
|
125
|
+
placeholder="The password you gave at sign-up" aria-label="The password you gave at sign-up">
|
|
126
|
+
<button type="button" class="primary" id="unlock-go">open</button>
|
|
126
127
|
</p>
|
|
127
128
|
<p class="err" id="unlock-error" role="alert"></p>
|
|
128
129
|
<p class="hint">This browser only — it is not sent anywhere.</p>
|
|
129
|
-
<h2>
|
|
130
|
-
<p>Make a new signing key
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
<h2>Lost that password?</h2>
|
|
131
|
+
<p>Make a new signing key instead. Your account, address, posts and followers
|
|
132
|
+
stay as they are; other servers learn the new key the next time they hear
|
|
133
|
+
from you.</p>
|
|
133
134
|
<p><button type="button" id="unlock-newkey">make a new signing key</button></p>
|
|
134
|
-
<p id="unlock-newkey-confirm" hidden>This replaces the key on your pod.
|
|
135
|
-
password you use now in the field above, then
|
|
135
|
+
<p id="unlock-newkey-confirm" hidden>This replaces the key on your pod.
|
|
136
136
|
<button type="button" id="unlock-newkey-go">make the new key</button></p>
|
|
137
137
|
</section>
|
|
138
138
|
|
|
@@ -153,7 +153,7 @@ a { color: var(--accent); }
|
|
|
153
153
|
|
|
154
154
|
<label for="provider">Pod provider</label>
|
|
155
155
|
<!-- The providers listed at https://solidproject.org/get_a_pod that run Community
|
|
156
|
-
Solid Server, whose
|
|
156
|
+
Solid Server, whose sign-up page "Create your pod" links to. Checked 2026-09-14. -->
|
|
157
157
|
<select id="provider" name="provider">
|
|
158
158
|
<option value="https://solidcommunity.net">solidcommunity.net</option>
|
|
159
159
|
<option value="https://privatedatapod.com">privatedatapod.com</option>
|
|
@@ -168,44 +168,35 @@ a { color: var(--accent); }
|
|
|
168
168
|
<input type="url" id="providerOther" name="providerOther" autocomplete="url" placeholder="https://pods.example.org">
|
|
169
169
|
</div>
|
|
170
170
|
|
|
171
|
-
<div id="
|
|
172
|
-
<
|
|
173
|
-
<
|
|
174
|
-
<p class="hint" id="podname-hint">Depending on your provider's setup, your pod will be at
|
|
175
|
-
either https://USERNAME.podprovider or https://podprovider/USERNAME.</p>
|
|
171
|
+
<div id="newpod-field">
|
|
172
|
+
<p>Make your pod on your provider's own page, then come back here.</p>
|
|
173
|
+
<p><a id="register-link" href="#" target="_blank" rel="noopener">Create your pod at your provider</a></p>
|
|
176
174
|
</div>
|
|
177
175
|
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
<input type="url" id="pod" name="pod" autocomplete="url" aria-describedby="pod-hint"
|
|
181
|
-
placeholder="https://alice.solidcommunity.net/ or https://server.example/alice/">
|
|
182
|
-
<p class="hint" id="pod-hint">The address of your pod: its own host, or a path on a
|
|
183
|
-
suffix-based host.</p>
|
|
184
|
-
</div>
|
|
185
|
-
|
|
186
|
-
<label for="email">Email (used as pod id)</label>
|
|
187
|
-
<input type="email" id="email" name="email" autocomplete="email" placeholder="you@example.org">
|
|
188
|
-
|
|
189
|
-
<label for="password">Pod password</label>
|
|
190
|
-
<input type="password" id="password" name="password" autocomplete="new-password"
|
|
191
|
-
aria-describedby="password-hint">
|
|
192
|
-
<p class="hint" id="password-hint">Your password is sent once, to make your account and to
|
|
193
|
-
lock your signing key. It is never stored. The credential used to set things up is
|
|
194
|
-
deleted when setup finishes — from then on this browser signs in through your pod,
|
|
195
|
-
the way any other Solid app does.</p>
|
|
176
|
+
<p class="hint" id="signin-hint">Your password is typed at your pod only. This site
|
|
177
|
+
never sees it: you sign in at your pod, and it sends you back here.</p>
|
|
196
178
|
</fieldset>
|
|
197
179
|
<p class="err" id="err-1" role="alert"></p>
|
|
198
180
|
<p>
|
|
199
181
|
<button type="button" id="cancel">Cancel</button>
|
|
200
|
-
<button type="button" class="primary" id="to-2">
|
|
182
|
+
<button type="button" class="primary" id="to-2">Sign in at your pod</button>
|
|
201
183
|
</p>
|
|
202
184
|
</div>
|
|
203
185
|
|
|
204
|
-
<!-- Screen 2: Your Fediverse Identity -->
|
|
186
|
+
<!-- Screen 2: Your Fediverse Identity — shown on return from the pod's login -->
|
|
205
187
|
<div class="step" id="step-2" hidden>
|
|
206
188
|
<fieldset>
|
|
207
189
|
<legend>Your Fediverse Identity</legend>
|
|
208
190
|
|
|
191
|
+
<span id="signed-pod-label" class="grouplabel">Your pod</span>
|
|
192
|
+
<p id="signed-pod" role="status" aria-labelledby="signed-pod-label"></p>
|
|
193
|
+
|
|
194
|
+
<!-- Shown when the pod's account lives at another gateway: this is a
|
|
195
|
+
move, not a new account. -->
|
|
196
|
+
<p id="movein-note" hidden>Moving <strong id="movein-from"></strong> to this site.
|
|
197
|
+
Your posts, followers and key come with you, and your followers are told the
|
|
198
|
+
new address. Keep the same handle, or choose a new one here.</p>
|
|
199
|
+
|
|
209
200
|
<label for="handle">Fediverse handle</label>
|
|
210
201
|
<input type="text" id="handle" name="handle" autocomplete="off"
|
|
211
202
|
placeholder="the handle part of your address: @HANDLE@server" aria-describedby="handle-hint">
|
|
@@ -228,7 +219,7 @@ a { color: var(--accent); }
|
|
|
228
219
|
<p class="err" id="form-error" role="alert"></p>
|
|
229
220
|
<p>
|
|
230
221
|
<button type="button" id="back-1">Back</button>
|
|
231
|
-
<button type="submit" class="primary" id="submit">Create
|
|
222
|
+
<button type="submit" class="primary" id="submit">Create your Fediverse account</button>
|
|
232
223
|
</p>
|
|
233
224
|
</div>
|
|
234
225
|
|
package/web/app/keys-browser.mjs
CHANGED
|
@@ -29,7 +29,7 @@ const isOpenedKey = (c) => c?.rsaPrivate?.type === 'private' && typeof c.rsaPubl
|
|
|
29
29
|
/**
|
|
30
30
|
* Keep this browser's opened copy: import the record and store the CryptoKey.
|
|
31
31
|
* Best effort on the store — a browser that refuses IndexedDB (private mode)
|
|
32
|
-
*
|
|
32
|
+
* reads the pod's copy again on the way back in. Returns the imported keys.
|
|
33
33
|
*/
|
|
34
34
|
export async function cacheOpenedKeys(actorUrl, keysRecord) {
|
|
35
35
|
const keys = await importSigningKey(keysRecord);
|
|
@@ -38,24 +38,22 @@ export async function cacheOpenedKeys(actorUrl, keysRecord) {
|
|
|
38
38
|
return keys;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
// Where this browser keeps its own opened copy of the signing key
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// without a soul around to type anything.
|
|
41
|
+
// Where this browser keeps its own opened copy of the signing key: a
|
|
42
|
+
// non-extractable CryptoKey that never leaves the origin — the same protection
|
|
43
|
+
// the OIDC session in the next IDB row already relies on. It is what lets the
|
|
44
|
+
// worker boot itself after an idle kill without a network read.
|
|
46
45
|
export const keyCacheKey = (actorUrl) => `signing-keys:${actorUrl}`;
|
|
47
46
|
// The cache is keyed by the POD actor, whatever the identity advertises: a
|
|
48
47
|
// fronted identity's advertised actor is the gateway's, and keying by that
|
|
49
|
-
// would hide the opened copy sign-up stored and
|
|
48
|
+
// would hide the opened copy sign-up stored and read the pod again.
|
|
50
49
|
export const podActorOf = (urls) => (urls.toPod ? urls.toPod(urls.actor) : urls.actor);
|
|
51
50
|
|
|
52
51
|
// Read the signing key for this actor and import it for signing.
|
|
53
52
|
//
|
|
54
|
-
// Two places hold one: this browser (opened, in IndexedDB) and the pod
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
// there is from before wrapping and is used as it stands, while an envelope
|
|
53
|
+
// Two places hold one: this browser (opened, in IndexedDB) and the pod. The
|
|
54
|
+
// browser's is tried first — no network read. Falling through to the pod covers
|
|
55
|
+
// the browser that has just signed in and has no copy yet: the record is read
|
|
56
|
+
// with the pod session and used as it stands. An envelope from before 1.28.0
|
|
59
57
|
// needs the password and so has to go back to the page.
|
|
60
58
|
export async function loadKeysFromPod(remote, urls) {
|
|
61
59
|
const podActor = podActorOf(urls);
|
|
@@ -65,10 +63,9 @@ export async function loadKeysFromPod(remote, urls) {
|
|
|
65
63
|
// in its place, so the PEM is gone from storage after one boot.
|
|
66
64
|
if (cached?.rsa?.privatePem) return cacheOpenedKeys(podActor, cached);
|
|
67
65
|
|
|
68
|
-
const doc = await podState.
|
|
66
|
+
const doc = await podState.readKeys(remote, urls);
|
|
69
67
|
if (isKeyEnvelope(doc)) throw new KeyPasswordNeeded();
|
|
70
68
|
if (!doc || !doc.rsa) throw new Error('no signing key on the pod — sign up did not finish');
|
|
71
|
-
//
|
|
72
|
-
// the pod's copy alone: re-wrapping it would need the password we do not have.
|
|
69
|
+
// Cache it so the next boot is no read at all.
|
|
73
70
|
return cacheOpenedKeys(podActor, doc);
|
|
74
71
|
}
|
package/web/app/keystore.mjs
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
// keystore.mjs — the signing keys, made in the browser and
|
|
2
|
-
// account password, so the copy that lives on the pod is ciphertext.
|
|
1
|
+
// keystore.mjs — the signing keys, made in the browser and stored on the pod.
|
|
3
2
|
//
|
|
4
3
|
// The Node agent keeps keys.json (RSA + Ed25519 PEMs) on disk, 0600. A browser
|
|
5
4
|
// has no disk it can carry to the next machine, so the durable copy lives on
|
|
6
|
-
// the pod,
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// the agent's keys.json, so once unwrapped it is used unchanged.
|
|
5
|
+
// the pod, in an owner-only container: the same access rule every private
|
|
6
|
+
// document on the pod lives under, reachable through the pod's own login and
|
|
7
|
+
// by nobody else. The shape is exactly the agent's keys.json, so it is used
|
|
8
|
+
// unchanged. A new browser reads it with its pod session and asks for nothing.
|
|
11
9
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// and claude/plans/browser-agent.md all said otherwise. Now they agree.
|
|
10
|
+
// Until 2026-09-22 the pod's copy was encrypted under the pod password, so a
|
|
11
|
+
// new browser had to ask for the password once. That sealed the key against
|
|
12
|
+
// the pod's host, which the access rule cannot do, at the price of fedipod.net
|
|
13
|
+
// asking for a pod password. Jeff decided the host is trusted the way it is
|
|
14
|
+
// for every other document, and the password went. `unwrapKeys`,
|
|
15
|
+
// `isKeyEnvelope` and `KeyPasswordNeeded` remain for accounts made before
|
|
16
|
+
// then: their key is opened once with the password and stored as it is.
|
|
20
17
|
|
|
21
18
|
const PEM = (der, label) => {
|
|
22
19
|
const b64 = btoa(String.fromCharCode(...new Uint8Array(der)));
|
|
@@ -56,38 +53,24 @@ async function deriveAesKey(password, salt, iterations) {
|
|
|
56
53
|
base, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
const toB64 = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)));
|
|
60
56
|
const fromB64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
|
|
61
57
|
|
|
62
|
-
/** Whether a document read off the pod is
|
|
63
|
-
* keys record.
|
|
64
|
-
* those still have to boot — the shape is what tells them apart. */
|
|
58
|
+
/** Whether a document read off the pod is an envelope from before 1.28.0
|
|
59
|
+
* rather than the keys record. The shape is what tells them apart. */
|
|
65
60
|
export function isKeyEnvelope(doc) {
|
|
66
61
|
return !!doc && doc.v === 1 && typeof doc.ct === 'string' && typeof doc.salt === 'string';
|
|
67
62
|
}
|
|
68
63
|
|
|
69
|
-
/** Thrown when the pod holds a
|
|
70
|
-
* it. The page catches it by `code` and asks
|
|
71
|
-
* worker cannot ask anyone anything. */
|
|
64
|
+
/** Thrown when the pod holds a key from before 1.28.0, under a password, and
|
|
65
|
+
* nothing in this browser can open it. The page catches it by `code` and asks
|
|
66
|
+
* for the password once; the worker cannot ask anyone anything. */
|
|
72
67
|
export class KeyPasswordNeeded extends Error {
|
|
73
68
|
constructor() { super('this browser needs your account password to unlock the signing key'); }
|
|
74
69
|
code = 'key-password-needed';
|
|
75
70
|
}
|
|
76
71
|
|
|
77
|
-
/**
|
|
78
|
-
*
|
|
79
|
-
export async function wrapKeys(keysRecord, password) {
|
|
80
|
-
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
81
|
-
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
82
|
-
const aes = await deriveAesKey(password, salt, PBKDF2_ITERATIONS);
|
|
83
|
-
const plaintext = new TextEncoder().encode(JSON.stringify(keysRecord));
|
|
84
|
-
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, aes, plaintext);
|
|
85
|
-
return { v: 1, kdf: 'PBKDF2-SHA256', iterations: PBKDF2_ITERATIONS,
|
|
86
|
-
salt: toB64(salt), iv: toB64(iv), ct: toB64(ct) };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Unwrap an envelope wrapKeys made. Throws on a wrong password (AES-GCM auth
|
|
90
|
-
* failure) — which is how a new browser tells a typo from a real key. */
|
|
72
|
+
/** Open an envelope from before 1.28.0. Throws on a wrong password (AES-GCM
|
|
73
|
+
* auth failure) — which is how the page tells a typo from a real problem. */
|
|
91
74
|
export async function unwrapKeys(envelope, password) {
|
|
92
75
|
if (!envelope || envelope.v !== 1) throw new Error('not a key envelope');
|
|
93
76
|
const aes = await deriveAesKey(password, fromB64(envelope.salt), envelope.iterations);
|
package/web/app/oidc-session.mjs
CHANGED
|
@@ -1,189 +1,6 @@
|
|
|
1
|
-
// oidc-session.mjs — Solid-OIDC
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
// session it yields — a DPoP keypair whose private half is NON-EXTRACTABLE, plus
|
|
6
|
-
// the refresh token, the token endpoint and the WebID — is kept in IndexedDB.
|
|
7
|
-
// IndexedDB survives a browser restart and is readable by the service worker
|
|
8
|
-
// where the agent runs, and a non-extractable key can sign but never be read
|
|
9
|
-
// out, even by injected script. That is the security this buys over uvdsl's
|
|
10
|
-
// sessionStorage (which must hold an extractable key as a string, per-tab).
|
|
11
|
-
//
|
|
12
|
-
// Page: const { authorizationUrl } = await beginLogin({ issuer, redirectUri });
|
|
13
|
-
// location.href = authorizationUrl; // ... redirect ...
|
|
14
|
-
// await completeLogin({ currentUrl: location.href }); // on return
|
|
15
|
-
// Anywhere (page or worker):
|
|
16
|
-
// const session = await getSession(); // { webId, fetch, signOut } | null
|
|
1
|
+
// oidc-session.mjs — FediPod's Solid-OIDC session: the solid-oidc-session
|
|
2
|
+
// library (lib/session/) bound to this app's database and client name. The
|
|
3
|
+
// page and the service worker both import this file, so both read one session.
|
|
4
|
+
import { solidOidcSession } from '../../lib/session/oidc-session.mjs';
|
|
17
5
|
|
|
18
|
-
const
|
|
19
|
-
const STORE = 'session';
|
|
20
|
-
const b64u = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
21
|
-
const enc = (o) => b64u(new TextEncoder().encode(JSON.stringify(o)));
|
|
22
|
-
const sha256 = (s) => crypto.subtle.digest('SHA-256', new TextEncoder().encode(s));
|
|
23
|
-
const rand = (n = 32) => b64u(crypto.getRandomValues(new Uint8Array(n)));
|
|
24
|
-
|
|
25
|
-
function idb() {
|
|
26
|
-
return new Promise((res, rej) => {
|
|
27
|
-
const r = indexedDB.open(DB, 1);
|
|
28
|
-
r.onupgradeneeded = () => r.result.createObjectStore(STORE);
|
|
29
|
-
r.onsuccess = () => res(r.result);
|
|
30
|
-
r.onerror = () => rej(r.error);
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
async function idbGet(key) {
|
|
34
|
-
const db = await idb();
|
|
35
|
-
return new Promise((res, rej) => { const t = db.transaction(STORE).objectStore(STORE).get(key); t.onsuccess = () => res(t.result); t.onerror = () => rej(t.error); });
|
|
36
|
-
}
|
|
37
|
-
async function idbPut(key, val) {
|
|
38
|
-
const db = await idb();
|
|
39
|
-
return new Promise((res, rej) => { const t = db.transaction(STORE, 'readwrite').objectStore(STORE).put(val, key); t.onsuccess = () => res(); t.onerror = () => rej(t.error); });
|
|
40
|
-
}
|
|
41
|
-
async function idbDel(key) {
|
|
42
|
-
const db = await idb();
|
|
43
|
-
return new Promise((res, rej) => { const t = db.transaction(STORE, 'readwrite').objectStore(STORE).delete(key); t.onsuccess = () => res(); t.onerror = () => rej(t.error); });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function dpopKey() { return crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']); }
|
|
47
|
-
async function publicJwk(pair) { const j = await crypto.subtle.exportKey('jwk', pair.publicKey); return { kty: j.kty, crv: j.crv, x: j.x, y: j.y }; }
|
|
48
|
-
async function dpopProof(pair, jwk, htm, htu, ath) {
|
|
49
|
-
const header = { typ: 'dpop+jwt', alg: 'ES256', jwk };
|
|
50
|
-
const payload = { htm, htu: htu.split('#')[0], jti: crypto.randomUUID(), iat: Math.floor(Date.now() / 1000), ...(ath ? { ath } : {}) };
|
|
51
|
-
const data = `${enc(header)}.${enc(payload)}`;
|
|
52
|
-
const sig = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, pair.privateKey, new TextEncoder().encode(data));
|
|
53
|
-
return `${data}.${b64u(sig)}`;
|
|
54
|
-
}
|
|
55
|
-
const jwtPayload = (jwt) => { try { return JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(jwt.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0)))); } catch { return {}; } };
|
|
56
|
-
|
|
57
|
-
async function discover(issuer) {
|
|
58
|
-
const res = await fetch(`${issuer.replace(/\/+$/, '')}/.well-known/openid-configuration`, { headers: { accept: 'application/json' } });
|
|
59
|
-
if (!res.ok) throw new Error(`OIDC discovery failed at ${issuer} (HTTP ${res.status})`);
|
|
60
|
-
return res.json();
|
|
61
|
-
}
|
|
62
|
-
async function registerClient(cfg, redirectUri, clientName) {
|
|
63
|
-
const res = await fetch(cfg.registration_endpoint, {
|
|
64
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
65
|
-
body: JSON.stringify({ client_name: clientName, redirect_uris: [redirectUri], token_endpoint_auth_method: 'none',
|
|
66
|
-
grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], application_type: 'web', scope: 'openid webid offline_access' }),
|
|
67
|
-
});
|
|
68
|
-
if (!res.ok) throw new Error(`client registration failed (HTTP ${res.status})`);
|
|
69
|
-
return (await res.json()).client_id;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Begin login: prepare PKCE + a non-extractable DPoP key, stash the pending
|
|
73
|
-
* state, and return the authorization URL for the caller to navigate to. */
|
|
74
|
-
export async function beginLogin({ issuer, redirectUri, clientName = 'FediPod' }) {
|
|
75
|
-
const cfg = await discover(issuer);
|
|
76
|
-
const client_id = await registerClient(cfg, redirectUri, clientName);
|
|
77
|
-
const pair = await dpopKey();
|
|
78
|
-
const verifier = rand(48);
|
|
79
|
-
const challenge = b64u(await sha256(verifier));
|
|
80
|
-
const state = rand(16);
|
|
81
|
-
await idbPut('pending', { issuer, client_id, redirectUri, verifier, state, pair,
|
|
82
|
-
tokenEndpoint: cfg.token_endpoint, authorizationEndpoint: cfg.authorization_endpoint,
|
|
83
|
-
revocationEndpoint: cfg.revocation_endpoint || null });
|
|
84
|
-
const url = new URL(cfg.authorization_endpoint);
|
|
85
|
-
for (const [k, v] of Object.entries({ client_id, redirect_uri: redirectUri, response_type: 'code',
|
|
86
|
-
scope: 'openid webid offline_access', code_challenge: challenge, code_challenge_method: 'S256', state, prompt: 'consent' })) url.searchParams.set(k, v);
|
|
87
|
-
return { authorizationUrl: url.href };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Complete login from the redirect back. Exchanges the code (DPoP + PKCE),
|
|
91
|
-
* stores the durable session, and returns it. Returns null if there is no code. */
|
|
92
|
-
export async function completeLogin({ currentUrl }) {
|
|
93
|
-
const u = new URL(currentUrl);
|
|
94
|
-
const code = u.searchParams.get('code');
|
|
95
|
-
const state = u.searchParams.get('state');
|
|
96
|
-
if (!code) return null;
|
|
97
|
-
const p = await idbGet('pending');
|
|
98
|
-
if (!p || p.state !== state) throw new Error('login state mismatch');
|
|
99
|
-
const jwk = await publicJwk(p.pair);
|
|
100
|
-
const res = await fetch(p.tokenEndpoint, {
|
|
101
|
-
method: 'POST',
|
|
102
|
-
headers: { 'content-type': 'application/x-www-form-urlencoded', dpop: await dpopProof(p.pair, jwk, 'POST', p.tokenEndpoint) },
|
|
103
|
-
body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: p.redirectUri, client_id: p.client_id, code_verifier: p.verifier }),
|
|
104
|
-
});
|
|
105
|
-
const tok = await res.json().catch(() => ({}));
|
|
106
|
-
if (!res.ok || !tok.access_token) throw new Error(`token exchange failed (HTTP ${res.status}): ${tok.error || ''}`);
|
|
107
|
-
const webId = jwtPayload(tok.access_token).webid || jwtPayload(tok.id_token || '').webid || null;
|
|
108
|
-
const session = { issuer: p.issuer, client_id: p.client_id, tokenEndpoint: p.tokenEndpoint, pair: p.pair,
|
|
109
|
-
// Where to hand the refresh token back at sign-out. Kept on the session
|
|
110
|
-
// because discovery is a network round trip and sign-out should not need
|
|
111
|
-
// one — see signOut().
|
|
112
|
-
revocationEndpoint: p.revocationEndpoint || null,
|
|
113
|
-
refreshToken: tok.refresh_token || null, accessToken: tok.access_token,
|
|
114
|
-
expiresAt: Date.now() + Math.max(30, (tok.expires_in || 300)) * 1000, webId };
|
|
115
|
-
await idbPut('session', session);
|
|
116
|
-
await idbDel('pending');
|
|
117
|
-
return sessionHandle(session);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Restore the session from IndexedDB — works in the page and the worker. */
|
|
121
|
-
export async function getSession() {
|
|
122
|
-
const s = await idbGet('session');
|
|
123
|
-
return s ? sessionHandle(s) : null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Sign out, and tell the issuer.
|
|
128
|
-
*
|
|
129
|
-
* Dropping the row locally left the REFRESH TOKEN alive at the pod for its full
|
|
130
|
-
* life — signing out of a shared or borrowed browser cleared the screen and
|
|
131
|
-
* nothing else, and anyone who had copied the token could go on minting access
|
|
132
|
-
* tokens with it. RFC 7009: hand it back.
|
|
133
|
-
*
|
|
134
|
-
* Best effort, and the local teardown happens either way: an issuer that is
|
|
135
|
-
* unreachable, or that publishes no revocation endpoint, must not be able to
|
|
136
|
-
* keep somebody signed in.
|
|
137
|
-
*/
|
|
138
|
-
export async function signOut() {
|
|
139
|
-
const s = await idbGet('session').catch(() => null);
|
|
140
|
-
if (s?.revocationEndpoint && s.refreshToken) {
|
|
141
|
-
try {
|
|
142
|
-
await fetch(s.revocationEndpoint, {
|
|
143
|
-
method: 'POST',
|
|
144
|
-
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
145
|
-
body: new URLSearchParams({
|
|
146
|
-
token: s.refreshToken, token_type_hint: 'refresh_token', client_id: s.client_id,
|
|
147
|
-
}),
|
|
148
|
-
});
|
|
149
|
-
} catch { /* told if we could; leaving is not conditional on being heard */ }
|
|
150
|
-
}
|
|
151
|
-
await idbDel('session');
|
|
152
|
-
await idbDel('pending');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function sessionHandle(s) {
|
|
156
|
-
let { accessToken, expiresAt, refreshToken } = s;
|
|
157
|
-
const jwkP = publicJwk(s.pair);
|
|
158
|
-
const refresh = async () => {
|
|
159
|
-
if (!refreshToken) throw new Error('session expired and there is no refresh token — sign in again');
|
|
160
|
-
const jwk = await jwkP;
|
|
161
|
-
const res = await fetch(s.tokenEndpoint, {
|
|
162
|
-
method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', dpop: await dpopProof(s.pair, jwk, 'POST', s.tokenEndpoint) },
|
|
163
|
-
body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: s.client_id, scope: 'openid webid offline_access' }),
|
|
164
|
-
});
|
|
165
|
-
const tok = await res.json().catch(() => ({}));
|
|
166
|
-
if (!res.ok || !tok.access_token) { await idbDel('session'); throw new Error('refresh failed — sign in again'); }
|
|
167
|
-
accessToken = tok.access_token;
|
|
168
|
-
expiresAt = Date.now() + Math.max(30, (tok.expires_in || 300)) * 1000;
|
|
169
|
-
if (tok.refresh_token) refreshToken = tok.refresh_token;
|
|
170
|
-
// Re-read before writing back. There is ONE 'session' row, and this handle
|
|
171
|
-
// closed over the session it was made from — so a refresh that was already
|
|
172
|
-
// in flight when the user signed in as somebody else would put the OLD
|
|
173
|
-
// identity back, silently, over the new one. Only write if the row is still
|
|
174
|
-
// the session this handle belongs to.
|
|
175
|
-
const now = await idbGet('session');
|
|
176
|
-
if (now && now.webId !== s.webId) {
|
|
177
|
-
throw new Error('signed in as somebody else while this session was refreshing');
|
|
178
|
-
}
|
|
179
|
-
await idbPut('session', { ...s, accessToken, expiresAt, refreshToken });
|
|
180
|
-
};
|
|
181
|
-
const authFetch = async (url, init = {}) => {
|
|
182
|
-
if (Date.now() > expiresAt - 30_000) await refresh();
|
|
183
|
-
const jwk = await jwkP;
|
|
184
|
-
const ath = b64u(await sha256(accessToken));
|
|
185
|
-
const headers = { ...(init.headers || {}), authorization: `DPoP ${accessToken}`, dpop: await dpopProof(s.pair, jwk, (init.method || 'GET'), url, ath) };
|
|
186
|
-
return fetch(url, { ...init, headers });
|
|
187
|
-
};
|
|
188
|
-
return { webId: s.webId, fetch: authFetch, refresh, signOut };
|
|
189
|
-
}
|
|
6
|
+
export const { beginLogin, completeLogin, getSession, signOut } = solidOidcSession({ dbName: 'fedipod-oidc', clientName: 'FediPod' });
|