@vantran-se/openchamber-web 1.23.1 → 1.24.4
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/bin/cli.test.js +29 -6
- package/bin/lib/cli-http.js +5 -1
- package/bin/lib/commands-status.js +5 -0
- package/dist/assets/{CodeMirrorCommentWidgets-BOpqXpFU.js → CodeMirrorCommentWidgets-B4a0JkUz.js} +1 -1
- package/dist/assets/DiffView-B2CD9wC8.js +4 -0
- package/dist/assets/FilesView-C4vMFclc.js +13 -0
- package/dist/assets/GitView-Byu2DY7s.js +6 -0
- package/dist/assets/GuestAttachDialog-B808E0-d.js +1 -0
- package/dist/assets/{JsonTreeView-nd_E8eVM.js → JsonTreeView-VW_7czo3.js} +1 -1
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/LinearIssuesView-EzGNCGrd.js +1 -0
- package/dist/assets/{MarkdownImageGallery-CVYds3sQ.js → MarkdownImageGallery-B2YUDFaB.js} +1 -1
- package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +12 -0
- package/dist/assets/{OnboardingScreen-Dd2IbPTn.js → OnboardingScreen-CQHBSTEi.js} +1 -1
- package/dist/assets/{PatchDiff-nUu5i4bG.js → PatchDiff-DCjGhlYV.js} +1 -1
- package/dist/assets/PierreDiffViewer-CEDZNkU2.js +210 -0
- package/dist/assets/PlanView-D3x-pTkZ.js +5 -0
- package/dist/assets/PluginPane-DEcgDQ9h.js +3 -0
- package/dist/assets/SettingsView-CSinuH-1.js +29 -0
- package/dist/assets/{SettingsWindow-BccxY81J.js → SettingsWindow-B5CHoJFZ.js} +1 -1
- package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +1 -0
- package/dist/assets/ToolOutputDialog-tLNWmfcW.js +15 -0
- package/dist/assets/{ToolPartDiffPreview-BFVgk0UK.js → ToolPartDiffPreview-CflzmK-x.js} +2 -2
- package/dist/assets/{VirtualizedFileDiff-z3_H74UR.js → VirtualizedFileDiff-NU0FHFhT.js} +3 -3
- package/dist/assets/{WalkthroughView-BqM9GFbJ.js → WalkthroughView-BZyjhqHv.js} +2 -2
- package/dist/assets/appThemeRegistry-DLicw4dH.js +1 -0
- package/dist/assets/commitAndPush-BDftrjr6.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-Bdazp9Qo.js → computeVirtualFileMetrics-DlWlEM3x.js} +1 -1
- package/dist/assets/de-Bro6_zyl.js +10 -0
- package/dist/assets/es-Bb9XQZvd.js +16 -0
- package/dist/assets/fr-Dgfzn-IL.js +14 -0
- package/dist/assets/{index-eo1-D_7z.js → index-CtQNmqUL.js} +1 -1
- package/dist/assets/index-DqaMWNrC.css +1 -0
- package/dist/assets/index-Dqc6USl3.js +40 -0
- package/dist/assets/{isDiffPlainText-BvKH712-.js → isDiffPlainText-CO4e5J_S.js} +1 -1
- package/dist/assets/{isFilePlainText-BdVVPukf.js → isFilePlainText-ue6kCou7.js} +1 -1
- package/dist/assets/ja-BYsakGFh.js +16 -0
- package/dist/assets/ko-BSaQz9aN.js +16 -0
- package/dist/assets/main-B62_pjL5.js +2 -0
- package/dist/assets/main-BEdcOI2P.js +896 -0
- package/dist/assets/{markdown-shiki.worker-D5fBU-lw.js → markdown-shiki.worker-CahLvHIq.js} +5 -5
- package/dist/assets/markdownCore-CK48LG0z.js +5 -0
- package/dist/assets/miniChat-LT8Rqofr.js +2 -0
- package/dist/assets/mobile-CqarEynh.js +2 -0
- package/dist/assets/{patchFileDiff-B7LjppSS.js → patchFileDiff-D7QVlovD.js} +3 -3
- package/dist/assets/pl-OIviPdoj.js +16 -0
- package/dist/assets/pt-BR-Cltg-E6s.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +2 -0
- package/dist/assets/renderMobileApp-BC1MHg6g.js +25 -0
- package/dist/assets/runtimeEndpointReset-DdrazOVH.js +25 -0
- package/dist/assets/{scheduledTasksApi-hceovDei.js → scheduledTasksApi-B2SWMGEU.js} +1 -1
- package/dist/assets/shikiHighlight-BPu3JiQs.js +1 -0
- package/dist/assets/textMateThemeFromAppTheme-DWqRPo9A.js +1 -0
- package/dist/assets/tr-CQI-KPre.js +7 -0
- package/dist/assets/uk-CqW7R4DA.js +16 -0
- package/dist/assets/useAppFontEffects-ne84JBVL.js +7333 -0
- package/dist/assets/useInlineCommentController-CgS0fnw_.js +2 -0
- package/dist/assets/useRootScrollLock-Cpf1qoGp.js +2 -0
- package/dist/assets/{vendor-base-ui-BY_5qla_.js → vendor-base-ui-CsXYnR5c.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DWA1bm1F.js → vendor-codemirror-language-data-Bx2EHc1O.js} +2 -2
- package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +1 -0
- package/dist/assets/vendor-katex-Cp579UoJ.css +1 -0
- package/dist/assets/{vendor-motion-dom-D_o3BTI3.js → vendor-motion-dom-DcCgeG69.js} +1 -1
- package/dist/assets/{vendor-remixicon-react-Dil0zKKT.js → vendor-remixicon-react-BDmGmFCA.js} +1 -1
- package/dist/assets/vendor-strip-json-comments-5dJlOkP1.js +4 -0
- package/dist/assets/{vendor-tanstack-react-virtual-g05Cx3z9.js → vendor-tanstack-react-virtual-ClYN8CzB.js} +1 -1
- package/dist/assets/{vendor-tanstack-virtual-core-Dubg10g5.js → vendor-tanstack-virtual-core-BewVQryB.js} +1 -1
- package/dist/assets/{vendor-zod-B6551dwG.js → vendor-zod-CgAdXWZO.js} +4 -4
- package/dist/assets/{web-components-B_Z64So5.js → web-components-DxX8bZ1o.js} +2 -2
- package/dist/assets/zh-CN-CVZG_o_J.js +16 -0
- package/dist/assets/zh-TW-D5ZxzCZp.js +16 -0
- package/dist/index.html +6 -4
- package/dist/mini-chat.html +6 -4
- package/dist/mobile.html +6 -4
- package/package.json +12 -9
- package/server/built-in-extensions/registry.json +4 -0
- package/server/index.d.ts +2 -0
- package/server/index.js +44 -10
- package/server/lib/agent-tool/DOCUMENTATION.md +17 -4
- package/server/lib/agent-tool/runtime.js +62 -15
- package/server/lib/agent-tool/runtime.test.js +88 -2
- package/server/lib/changelog/update-notes.js +2 -1
- package/server/lib/changelog/update-notes.test.js +19 -0
- package/server/lib/event-stream/DOCUMENTATION.md +23 -0
- package/server/lib/event-stream/delta-coalescer.js +0 -0
- package/server/lib/event-stream/delta-coalescer.test.js +292 -0
- package/server/lib/event-stream/global-hub.js +53 -23
- package/server/lib/event-stream/global-hub.test.js +160 -0
- package/server/lib/event-stream/global-ws-bridge.js +1 -0
- package/server/lib/event-stream/index.js +4 -0
- package/server/lib/fs/DOCUMENTATION.md +10 -1
- package/server/lib/fs/git-process.test.js +77 -0
- package/server/lib/fs/routes.js +58 -5
- package/server/lib/fs/routes.test.js +116 -0
- package/server/lib/fs/search.js +2 -1
- package/server/lib/git/DOCUMENTATION.md +20 -5
- package/server/lib/git/routes.js +16 -3
- package/server/lib/git/serial-refresh.js +93 -0
- package/server/lib/git/serial-refresh.test.js +112 -0
- package/server/lib/git/service.js +429 -70
- package/server/lib/git/service.test.js +418 -0
- package/server/lib/github/pr-status.js +5 -5
- package/server/lib/github/pr-status.test.js +1 -1
- package/server/lib/github/routes.js +4 -4
- package/server/lib/guests/DOCUMENTATION.md +68 -0
- package/server/lib/guests/auth-store.js +184 -0
- package/server/lib/guests/auth-store.test.js +112 -0
- package/server/lib/guests/background.test.js +83 -0
- package/server/lib/guests/builtins.js +34 -0
- package/server/lib/guests/builtins.test.js +174 -0
- package/server/lib/guests/catalog.js +477 -0
- package/server/lib/guests/catalog.test.js +390 -0
- package/server/lib/guests/clone.js +297 -0
- package/server/lib/guests/extract-zip.js +136 -0
- package/server/lib/guests/extract-zip.test.js +109 -0
- package/server/lib/guests/files.js +308 -0
- package/server/lib/guests/files.test.js +262 -0
- package/server/lib/guests/grant-scope.js +81 -0
- package/server/lib/guests/grant-scope.test.js +75 -0
- package/server/lib/guests/host-session.js +73 -0
- package/server/lib/guests/host-session.test.js +30 -0
- package/server/lib/guests/html-styles.js +6 -0
- package/server/lib/guests/html-styles.test.js +45 -0
- package/server/lib/guests/html-tokens.js +73 -0
- package/server/lib/guests/html-tokens.test.js +53 -0
- package/server/lib/guests/install.js +406 -0
- package/server/lib/guests/install.test.js +338 -0
- package/server/lib/guests/oauth.js +501 -0
- package/server/lib/guests/oauth.test.js +481 -0
- package/server/lib/guests/persist.js +336 -0
- package/server/lib/guests/persist.test.js +288 -0
- package/server/lib/guests/request.js +112 -0
- package/server/lib/guests/request.test.js +206 -0
- package/server/lib/guests/routes.js +838 -0
- package/server/lib/guests/service.js +538 -0
- package/server/lib/guests/service.test.js +277 -0
- package/server/lib/guests/sockets.js +129 -0
- package/server/lib/guests/sockets.test.js +110 -0
- package/server/lib/guests/ssh-install.test.js +188 -0
- package/server/lib/guests/storage.js +68 -0
- package/server/lib/guests/storage.test.js +59 -0
- package/server/lib/guests/updates.js +294 -0
- package/server/lib/guests/updates.test.js +334 -0
- package/server/lib/guests/upload.js +50 -0
- package/server/lib/guests/upload.test.js +131 -0
- package/server/lib/linear/DOCUMENTATION.md +1 -1
- package/server/lib/message-queue/DOCUMENTATION.md +10 -1
- package/server/lib/message-queue/runtime.js +21 -1
- package/server/lib/message-queue/runtime.test.js +52 -5
- package/server/lib/notifications/routes.js +5 -0
- package/server/lib/openchamber-control/DOCUMENTATION.md +6 -0
- package/server/lib/openchamber-sessions/routes.js +62 -29
- package/server/lib/openchamber-sessions/routes.test.js +96 -0
- package/server/lib/opencode/DOCUMENTATION.md +55 -1
- package/server/lib/opencode/bootstrap-runtime.js +1 -0
- package/server/lib/opencode/core-routes.js +39 -1
- package/server/lib/opencode/feature-routes-runtime.js +15 -0
- package/server/lib/opencode/lifecycle-process.test.js +84 -0
- package/server/lib/opencode/lifecycle.js +54 -67
- package/server/lib/opencode/openchamber-routes.js +72 -14
- package/server/lib/opencode/openchamber-routes.test.js +71 -3
- package/server/lib/opencode/session-runtime.js +68 -0
- package/server/lib/opencode/session-runtime.test.js +43 -0
- package/server/lib/opencode/settings-helpers.js +8 -0
- package/server/lib/opencode/settings-helpers.test.js +13 -1
- package/server/lib/opencode/settings-normalization-runtime.js +2 -0
- package/server/lib/opencode/settings-normalization-runtime.test.js +13 -0
- package/server/lib/opencode/settings-registry.json +15 -0
- package/server/lib/opencode/shutdown-runtime.js +3 -0
- package/server/lib/opencode/shutdown-runtime.test.js +24 -0
- package/server/lib/opencode/theme-archive.js +53 -0
- package/server/lib/opencode/theme-catalog.js +171 -0
- package/server/lib/opencode/theme-catalog.test.js +125 -0
- package/server/lib/opencode/theme-import.test.js +144 -0
- package/server/lib/opencode/theme-runtime.js +100 -28
- package/server/lib/opencode/theme-runtime.test.js +15 -0
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +8 -0
- package/server/lib/permission-auto-accept/runtime.js +13 -0
- package/server/lib/permission-auto-accept/runtime.test.js +37 -1
- package/server/lib/projects/project-config.js +2 -2
- package/server/lib/quota/DOCUMENTATION.md +0 -1
- package/server/lib/quota/providers/index.js +0 -7
- package/server/lib/realtime-proxy-terminal.test.js +150 -0
- package/server/lib/realtime-proxy.js +2 -0
- package/server/lib/routing/DOCUMENTATION.md +83 -0
- package/server/lib/routing/defaults.js +98 -0
- package/server/lib/routing/feature-flag.js +11 -0
- package/server/lib/routing/history.js +34 -0
- package/server/lib/routing/jev.js +98 -0
- package/server/lib/routing/routes.http.test.js +100 -0
- package/server/lib/routing/routes.js +81 -0
- package/server/lib/routing/runtime.js +217 -0
- package/server/lib/routing/runtime.test.js +188 -0
- package/server/lib/routing/store.js +249 -0
- package/server/lib/routing/store.test.js +96 -0
- package/server/lib/scheduled-tasks/runtime.js +2 -2
- package/server/lib/scheduled-tasks/runtime.test.js +14 -0
- package/server/lib/security/request-security.js +9 -5
- package/server/lib/security/request-security.test.js +37 -0
- package/server/lib/small-model/DOCUMENTATION.md +14 -2
- package/server/lib/small-model/call.js +37 -6
- package/server/lib/small-model/call.test.js +182 -0
- package/server/lib/terminal/DOCUMENTATION.md +23 -2
- package/server/lib/terminal/runtime.js +27 -5
- package/server/lib/terminal/runtime.test.js +67 -0
- package/server/lib/terminal/shutdown.js +106 -0
- package/server/lib/terminal/shutdown.test.js +84 -0
- package/server/lib/tts/service.test.js +78 -0
- package/server/lib/ui-auth/DOCUMENTATION.md +8 -0
- package/server/lib/ui-auth/session-cookie.js +51 -0
- package/server/lib/ui-auth/session-cookie.test.js +46 -0
- package/server/lib/ui-auth/ui-auth.js +56 -17
- package/server/lib/ui-auth/ui-auth.test.js +96 -0
- package/dist/assets/DiffView-CyLmb-wF.js +0 -4
- package/dist/assets/DirtyBranchSwitchDialog-pu5kcoir.js +0 -1
- package/dist/assets/FilesView-WtJB0y_l.js +0 -13
- package/dist/assets/GitView-DkNrV0BK.js +0 -6
- package/dist/assets/LinearIssuesView-B0H0Otii.js +0 -1
- package/dist/assets/MarkdownRendererImpl-DSqVN2d8.js +0 -12
- package/dist/assets/PierreDiffViewer-Bsq9z-sw.js +0 -161
- package/dist/assets/PlanView-CAFziKH5.js +0 -5
- package/dist/assets/SettingsView-BRittavZ.js +0 -28
- package/dist/assets/ToolOutputDialog-B0uYaJio.js +0 -15
- package/dist/assets/appThemeRegistry-BK0dHKa6.js +0 -1
- package/dist/assets/de-c7CYLCAW.js +0 -10
- package/dist/assets/es-BKAVPQmQ.js +0 -16
- package/dist/assets/fr-Bj1glqfV.js +0 -14
- package/dist/assets/index-BYFyOu_D.css +0 -1
- package/dist/assets/index-BfwRClDe.js +0 -40
- package/dist/assets/ja-5jwSIbK0.js +0 -16
- package/dist/assets/ko-BaDbg-Eb.js +0 -16
- package/dist/assets/main-CGG2_uHq.js +0 -2
- package/dist/assets/main-CcrSaL4J.js +0 -930
- package/dist/assets/markdownCore-DsmQr3bt.js +0 -5
- package/dist/assets/miniChat-DEaAhP4b.js +0 -2
- package/dist/assets/mobile-BC3rsvv0.js +0 -2
- package/dist/assets/pl-BFKVBDFu.js +0 -16
- package/dist/assets/pt-BR-qKfHvkDL.js +0 -16
- package/dist/assets/registerCustomTheme-BBXUCRnG.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-CR9d1Mr0.js +0 -2
- package/dist/assets/renderMobileApp-Da5dwobZ.js +0 -25
- package/dist/assets/runtimeEndpointReset-CLYVRw2H.js +0 -41
- package/dist/assets/shikiHighlight-D1Z9G58W.js +0 -1
- package/dist/assets/tr-DYXPoEDG.js +0 -7
- package/dist/assets/uk-CwAoloR8.js +0 -16
- package/dist/assets/useAppFontEffects-DeStGcUT.js +0 -7282
- package/dist/assets/useGitComparison-CpxioduU.js +0 -1
- package/dist/assets/useInlineCommentController-gmUnJtrb.js +0 -2
- package/dist/assets/useRootScrollLock--a3cPz8s.js +0 -1
- package/dist/assets/vendor-framer-motion-livSKyyV.js +0 -1
- package/dist/assets/zh-CN-D9MG0lqF.js +0 -16
- package/dist/assets/zh-TW-1HNK7shh.js +0 -16
- package/server/lib/quota/providers/crof.js +0 -96
- package/server/lib/quota/providers/crof.test.js +0 -98
- /package/dist/assets/{vendor-katex-k3Ppq1Ha.js → vendor-katex-CHD9BdDO.js} +0 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
GUEST_ACCOUNT_MAX,
|
|
5
|
+
GUEST_REQUEST_TIMEOUT_MS,
|
|
6
|
+
isGuestRequestPath,
|
|
7
|
+
resolveIntegrationApi,
|
|
8
|
+
resolveIntegrationAuth,
|
|
9
|
+
} from '@openchamber/sdk';
|
|
10
|
+
|
|
11
|
+
import { dropGuestTokens, getGuestAuth, patchGuestAuth } from './auth-store.js';
|
|
12
|
+
|
|
13
|
+
export const PENDING_AUTHORIZATION_TTL_MS = 10 * 60_000;
|
|
14
|
+
|
|
15
|
+
const pendingByState = new Map();
|
|
16
|
+
const pendingByGuestId = new Map();
|
|
17
|
+
|
|
18
|
+
export class GuestOAuthError extends Error {
|
|
19
|
+
constructor(message, code = 'GUEST_OAUTH_FAILED') {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'GuestOAuthError';
|
|
22
|
+
this.code = code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const createPkcePair = () => {
|
|
27
|
+
const verifier = crypto.randomBytes(32).toString('base64url');
|
|
28
|
+
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
|
|
29
|
+
return { verifier, challenge };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const pruneExpiredPending = (now = Date.now()) => {
|
|
33
|
+
for (const [state, entry] of pendingByState.entries()) {
|
|
34
|
+
if (!entry || entry.expiresAt <= now) {
|
|
35
|
+
pendingByState.delete(state);
|
|
36
|
+
if (entry?.guestId) {
|
|
37
|
+
const current = pendingByGuestId.get(entry.guestId);
|
|
38
|
+
if (current?.state === state) {
|
|
39
|
+
pendingByGuestId.delete(entry.guestId);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const dropPendingForGuest = (guestId) => {
|
|
47
|
+
const current = pendingByGuestId.get(guestId);
|
|
48
|
+
if (current?.state) {
|
|
49
|
+
pendingByState.delete(current.state);
|
|
50
|
+
}
|
|
51
|
+
pendingByGuestId.delete(guestId);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const rememberPending = (state, entry) => {
|
|
55
|
+
dropPendingForGuest(entry.guestId);
|
|
56
|
+
pendingByState.set(state, entry);
|
|
57
|
+
pendingByGuestId.set(entry.guestId, { ...entry, state });
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// `state` is the only thing that ties a callback to the click that started
|
|
61
|
+
// it. Accepting a callback without it would let any link visited during the
|
|
62
|
+
// pending window attach a stranger's account, so a missing state is a refusal.
|
|
63
|
+
const takePending = (guestId, state) => {
|
|
64
|
+
const trimmed = readTrimmedString(state);
|
|
65
|
+
if (!trimmed) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const pending = pendingByState.get(trimmed);
|
|
69
|
+
if (!pending || pending.guestId !== guestId) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
pendingByState.delete(trimmed);
|
|
73
|
+
if (pendingByGuestId.get(guestId)?.state === trimmed) {
|
|
74
|
+
pendingByGuestId.delete(guestId);
|
|
75
|
+
}
|
|
76
|
+
return pending;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const isPlainObject = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
80
|
+
|
|
81
|
+
const readTrimmedString = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
82
|
+
|
|
83
|
+
const readExpiresAt = (expiresIn, now = Date.now()) => {
|
|
84
|
+
const seconds = typeof expiresIn === 'number' && Number.isFinite(expiresIn) ? expiresIn : Number(expiresIn);
|
|
85
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
86
|
+
return now + 24 * 60 * 60 * 1000;
|
|
87
|
+
}
|
|
88
|
+
return now + Math.floor(seconds) * 1000;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const parseTokenPayload = (payload) => {
|
|
92
|
+
if (!isPlainObject(payload)) {
|
|
93
|
+
throw new GuestOAuthError('Token response was empty');
|
|
94
|
+
}
|
|
95
|
+
if (readTrimmedString(payload.error)) {
|
|
96
|
+
throw new GuestOAuthError(
|
|
97
|
+
readTrimmedString(payload.error_description) || readTrimmedString(payload.error),
|
|
98
|
+
readTrimmedString(payload.error).toUpperCase() || 'GUEST_OAUTH_FAILED',
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const accessToken = readTrimmedString(payload.access_token);
|
|
102
|
+
if (!accessToken) {
|
|
103
|
+
throw new GuestOAuthError('Token response was missing access_token');
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
accessToken,
|
|
107
|
+
refreshToken: readTrimmedString(payload.refresh_token) || null,
|
|
108
|
+
tokenType: readTrimmedString(payload.token_type) || 'bearer',
|
|
109
|
+
expiresAt: readExpiresAt(payload.expires_in),
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const readTokenError = (payload, status) => {
|
|
114
|
+
const description = isPlainObject(payload)
|
|
115
|
+
? (readTrimmedString(payload.error_description) || readTrimmedString(payload.error))
|
|
116
|
+
: '';
|
|
117
|
+
return description || `Token request failed (${status})`;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const postForm = async (url, body) => {
|
|
121
|
+
const response = await fetch(url, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: {
|
|
124
|
+
Accept: 'application/json',
|
|
125
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
126
|
+
},
|
|
127
|
+
body: new URLSearchParams(body).toString(),
|
|
128
|
+
signal: AbortSignal.timeout(GUEST_REQUEST_TIMEOUT_MS),
|
|
129
|
+
});
|
|
130
|
+
const payload = await response.json().catch(() => null);
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
throw new GuestOAuthError(readTokenError(payload, response.status));
|
|
133
|
+
}
|
|
134
|
+
return parseTokenPayload(payload);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const postJson = async (url, body) => {
|
|
138
|
+
const response = await fetch(url, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: {
|
|
141
|
+
Accept: 'application/json',
|
|
142
|
+
'Content-Type': 'application/json',
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify(body),
|
|
145
|
+
signal: AbortSignal.timeout(GUEST_REQUEST_TIMEOUT_MS),
|
|
146
|
+
});
|
|
147
|
+
const payload = await response.json().catch(() => null);
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
throw new GuestOAuthError(readTokenError(payload, response.status));
|
|
150
|
+
}
|
|
151
|
+
return parseTokenPayload(payload);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const exchangeAuthorizationCode = async (tokenUrl, formBody, jsonBody) => {
|
|
155
|
+
try {
|
|
156
|
+
return await postForm(tokenUrl, formBody);
|
|
157
|
+
} catch (first) {
|
|
158
|
+
try {
|
|
159
|
+
return await postJson(tokenUrl, jsonBody);
|
|
160
|
+
} catch {
|
|
161
|
+
throw first;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const guestAuthorizationHeader = (token, authorization) => {
|
|
167
|
+
if (authorization === 'bearer') {
|
|
168
|
+
return `Bearer ${token}`;
|
|
169
|
+
}
|
|
170
|
+
if (authorization === 'basic') {
|
|
171
|
+
// `basic` stores the already-encoded `username:token` pair; see saveGuestAccessToken.
|
|
172
|
+
return `Basic ${token}`;
|
|
173
|
+
}
|
|
174
|
+
return token;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/** Encodes `username:token` for a `basic` integration. Stored as the access token so requests need no username. */
|
|
178
|
+
export const encodeBasicCredential = (username, token) => Buffer.from(`${username}:${token}`, 'utf8').toString('base64');
|
|
179
|
+
|
|
180
|
+
export const guestRedirectUri = (origin, guestId) => `${origin.replace(/\/+$/, '')}/api/guests/${guestId}/oauth/callback`;
|
|
181
|
+
|
|
182
|
+
/** With `integration`, `hasClient` is false for a client entered for endpoints the package no longer names. */
|
|
183
|
+
export const toPublicGuestAuth = (entry, integration) => ({
|
|
184
|
+
connected: Boolean(entry?.accessToken),
|
|
185
|
+
account: typeof entry?.account === 'string' ? entry.account : '',
|
|
186
|
+
hasClient: integration ? Boolean(usableClientCredentials(entry, integration).clientId) : Boolean(entry?.clientId),
|
|
187
|
+
settings: entry?.settings && typeof entry.settings === 'object' ? { ...entry.settings } : {},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const readAccountLabel = (payload, name) => {
|
|
191
|
+
if (!isPlainObject(payload) || typeof name !== 'string' || name === '') {
|
|
192
|
+
return '';
|
|
193
|
+
}
|
|
194
|
+
let current = payload;
|
|
195
|
+
for (const part of name.split('.')) {
|
|
196
|
+
if (!isPlainObject(current) || !(part in current)) {
|
|
197
|
+
return '';
|
|
198
|
+
}
|
|
199
|
+
current = current[part];
|
|
200
|
+
}
|
|
201
|
+
return typeof current === 'string' ? current.trim().slice(0, GUEST_ACCOUNT_MAX) : '';
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const fetchAccountLabel = async (api, accessToken) => {
|
|
205
|
+
if (!api.account) {
|
|
206
|
+
return { ok: true, account: '' };
|
|
207
|
+
}
|
|
208
|
+
if (!isGuestRequestPath(api.account.path)) {
|
|
209
|
+
return { ok: false, account: '' };
|
|
210
|
+
}
|
|
211
|
+
const url = new URL(api.account.path, `${api.apiOrigin}/`);
|
|
212
|
+
if (url.origin !== api.apiOrigin) {
|
|
213
|
+
return { ok: false, account: '' };
|
|
214
|
+
}
|
|
215
|
+
const response = await fetch(url, {
|
|
216
|
+
headers: {
|
|
217
|
+
Accept: 'application/json',
|
|
218
|
+
Authorization: guestAuthorizationHeader(accessToken, api.authorization),
|
|
219
|
+
},
|
|
220
|
+
signal: AbortSignal.timeout(GUEST_REQUEST_TIMEOUT_MS),
|
|
221
|
+
});
|
|
222
|
+
if (!response.ok) {
|
|
223
|
+
return { ok: false, account: '' };
|
|
224
|
+
}
|
|
225
|
+
const payload = await response.json().catch(() => null);
|
|
226
|
+
return { ok: true, account: readAccountLabel(payload, api.account.name) };
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The addresses credentials for this integration go to: the API origin and,
|
|
231
|
+
* for OAuth, the authorize and token endpoints.
|
|
232
|
+
* @returns {{ apiOrigin: string, authorizeUrl?: string, tokenUrl?: string } | null}
|
|
233
|
+
*/
|
|
234
|
+
export const credentialTarget = (integration) => {
|
|
235
|
+
const api = resolveIntegrationApi(integration ?? {});
|
|
236
|
+
if (!api) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const target = { apiOrigin: api.apiOrigin };
|
|
240
|
+
if (integration?.oauth) {
|
|
241
|
+
target.authorizeUrl = integration.oauth.authorizeUrl;
|
|
242
|
+
target.tokenUrl = integration.oauth.tokenUrl;
|
|
243
|
+
}
|
|
244
|
+
return target;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const sameTarget = (a, b) => Boolean(a && b)
|
|
248
|
+
&& a.apiOrigin === b.apiOrigin
|
|
249
|
+
&& (a.authorizeUrl ?? null) === (b.authorizeUrl ?? null)
|
|
250
|
+
&& (a.tokenUrl ?? null) === (b.tokenUrl ?? null);
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Whether the stored tokens were minted for the integration as it is now.
|
|
254
|
+
* Tokens without a recorded target never count.
|
|
255
|
+
*/
|
|
256
|
+
export const storedTokensUsable = (stored, integration) => (
|
|
257
|
+
Boolean(stored?.accessToken) && sameTarget(stored.target, credentialTarget(integration))
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The client id and secret, only when they were entered for the endpoints
|
|
262
|
+
* the package names now. Anything else is treated as missing.
|
|
263
|
+
* @returns {{ clientId: string, clientSecret: string }}
|
|
264
|
+
*/
|
|
265
|
+
export const usableClientCredentials = (stored, integration) => {
|
|
266
|
+
if (!sameTarget(stored?.clientTarget, credentialTarget(integration))) {
|
|
267
|
+
return { clientId: '', clientSecret: '' };
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
clientId: readTrimmedString(stored?.clientId),
|
|
271
|
+
clientSecret: readTrimmedString(stored?.clientSecret),
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Tokens that no longer match the package's addresses are dropped, so the
|
|
277
|
+
* card shows disconnected and nothing is sent to the new addresses.
|
|
278
|
+
* @returns {Promise<object | null>} the stored entry when its tokens are usable
|
|
279
|
+
*/
|
|
280
|
+
export const takeUsableGuestAuth = async (guest, persistPath) => {
|
|
281
|
+
const stored = await getGuestAuth(guest.id, persistPath);
|
|
282
|
+
if (!stored?.accessToken) {
|
|
283
|
+
return stored;
|
|
284
|
+
}
|
|
285
|
+
if (storedTokensUsable(stored, guest.integration)) {
|
|
286
|
+
return stored;
|
|
287
|
+
}
|
|
288
|
+
await dropGuestTokens(guest.id, persistPath);
|
|
289
|
+
return getGuestAuth(guest.id, persistPath);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Save the OAuth client the user entered on the Integrations card. The
|
|
294
|
+
* client is bound to the endpoints the package names now. An empty secret
|
|
295
|
+
* keeps the stored one only for the same client id and the same endpoints;
|
|
296
|
+
* a new id or moved endpoints drop the old secret instead of carrying it
|
|
297
|
+
* over. Decided under the store lock against the entry as it is then.
|
|
298
|
+
*/
|
|
299
|
+
export const saveGuestOAuthClient = async ({ guest, persistPath, clientId, clientSecret }) => {
|
|
300
|
+
if (resolveIntegrationAuth(guest.integration ?? {}) !== 'oauth') {
|
|
301
|
+
throw new GuestOAuthError('This guest does not declare OAuth.', 'NO_INTEGRATION');
|
|
302
|
+
}
|
|
303
|
+
const id = readTrimmedString(clientId);
|
|
304
|
+
const secret = readTrimmedString(clientSecret);
|
|
305
|
+
if (!id) {
|
|
306
|
+
throw new GuestOAuthError('Client id is missing.', 'CLIENT_MISSING');
|
|
307
|
+
}
|
|
308
|
+
const clientTarget = credentialTarget(guest.integration);
|
|
309
|
+
return patchGuestAuth(guest.id, (current) => {
|
|
310
|
+
const next = { clientId: id, clientTarget };
|
|
311
|
+
if (secret) {
|
|
312
|
+
next.clientSecret = secret;
|
|
313
|
+
} else if (readTrimmedString(current?.clientId) !== id || !sameTarget(current?.clientTarget, clientTarget)) {
|
|
314
|
+
next.clientSecret = undefined;
|
|
315
|
+
}
|
|
316
|
+
return next;
|
|
317
|
+
}, persistPath);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export const saveGuestAccessToken = async ({ guest, persistPath, token, username }) => {
|
|
321
|
+
if (resolveIntegrationAuth(guest.integration ?? {}) !== 'token') {
|
|
322
|
+
throw new GuestOAuthError('This guest does not accept a pasted token.', 'NO_TOKEN_AUTH');
|
|
323
|
+
}
|
|
324
|
+
const api = resolveIntegrationApi(guest.integration);
|
|
325
|
+
const pastedToken = readTrimmedString(token);
|
|
326
|
+
if (!api || !pastedToken) {
|
|
327
|
+
throw new GuestOAuthError('API token is missing.', 'TOKEN_MISSING');
|
|
328
|
+
}
|
|
329
|
+
const pastedUsername = readTrimmedString(username);
|
|
330
|
+
if (api.authorization === 'basic' && !pastedUsername) {
|
|
331
|
+
throw new GuestOAuthError('Username is missing.', 'USERNAME_MISSING');
|
|
332
|
+
}
|
|
333
|
+
const accessToken = api.authorization === 'basic'
|
|
334
|
+
? encodeBasicCredential(pastedUsername, pastedToken)
|
|
335
|
+
: pastedToken;
|
|
336
|
+
const probed = await fetchAccountLabel(api, accessToken);
|
|
337
|
+
if (api.account && !probed.ok) {
|
|
338
|
+
throw new GuestOAuthError('That API token was refused.', 'TOKEN_INVALID');
|
|
339
|
+
}
|
|
340
|
+
const account = probed.account || (api.authorization === 'basic' ? pastedUsername.slice(0, GUEST_ACCOUNT_MAX) : '');
|
|
341
|
+
await patchGuestAuth(guest.id, {
|
|
342
|
+
accessToken,
|
|
343
|
+
refreshToken: null,
|
|
344
|
+
tokenType: api.authorization,
|
|
345
|
+
expiresAt: null,
|
|
346
|
+
account,
|
|
347
|
+
authorizedAt: Date.now(),
|
|
348
|
+
target: credentialTarget(guest.integration),
|
|
349
|
+
}, persistPath);
|
|
350
|
+
return { connected: true, account };
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
export const startGuestAuthorization = async ({ guest, persistPath, origin }) => {
|
|
354
|
+
const oauth = guest.integration?.oauth;
|
|
355
|
+
if (resolveIntegrationAuth(guest.integration ?? {}) !== 'oauth' || !oauth) {
|
|
356
|
+
throw new GuestOAuthError('This guest does not declare OAuth.', 'NO_INTEGRATION');
|
|
357
|
+
}
|
|
358
|
+
const stored = await getGuestAuth(guest.id, persistPath);
|
|
359
|
+
const { clientId } = usableClientCredentials(stored, guest.integration);
|
|
360
|
+
if (!clientId) {
|
|
361
|
+
throw new GuestOAuthError('Client id is missing. Save it in Integrations first.', 'CLIENT_MISSING');
|
|
362
|
+
}
|
|
363
|
+
pruneExpiredPending();
|
|
364
|
+
const { verifier, challenge } = createPkcePair();
|
|
365
|
+
const state = crypto.randomBytes(32).toString('base64url');
|
|
366
|
+
const redirectUri = guestRedirectUri(origin, guest.id);
|
|
367
|
+
rememberPending(state, {
|
|
368
|
+
guestId: guest.id,
|
|
369
|
+
codeVerifier: verifier,
|
|
370
|
+
redirectUri,
|
|
371
|
+
// The exchange goes to the endpoints the user saw when they clicked
|
|
372
|
+
// Connect, and only if the package still names the same ones.
|
|
373
|
+
target: credentialTarget(guest.integration),
|
|
374
|
+
expiresAt: Date.now() + PENDING_AUTHORIZATION_TTL_MS,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const url = new URL(oauth.authorizeUrl);
|
|
378
|
+
url.searchParams.set('response_type', 'code');
|
|
379
|
+
url.searchParams.set('client_id', clientId);
|
|
380
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
381
|
+
url.searchParams.set('state', state);
|
|
382
|
+
url.searchParams.set('code_challenge', challenge);
|
|
383
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
384
|
+
if (oauth.scopes && oauth.scopes.length > 0) {
|
|
385
|
+
url.searchParams.set('scope', oauth.scopes.join(' '));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
authorizationUrl: url.toString(),
|
|
390
|
+
expiresIn: Math.floor(PENDING_AUTHORIZATION_TTL_MS / 1000),
|
|
391
|
+
};
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
export const consumeGuestAuthorization = async ({ guest, persistPath, code, state, error, errorDescription }) => {
|
|
395
|
+
pruneExpiredPending();
|
|
396
|
+
if (readTrimmedString(error)) {
|
|
397
|
+
dropPendingForGuest(guest.id);
|
|
398
|
+
throw new GuestOAuthError(readTrimmedString(errorDescription) || readTrimmedString(error));
|
|
399
|
+
}
|
|
400
|
+
const pending = takePending(guest.id, state);
|
|
401
|
+
if (!pending) {
|
|
402
|
+
throw new GuestOAuthError('Authorization state was missing or expired.', 'STATE_MISMATCH');
|
|
403
|
+
}
|
|
404
|
+
if (!sameTarget(pending.target, credentialTarget(guest.integration))) {
|
|
405
|
+
throw new GuestOAuthError('The extension changed its endpoints while you were signing in. Review it in Settings → Extensions and connect again.', 'TARGET_CHANGED');
|
|
406
|
+
}
|
|
407
|
+
const stored = await getGuestAuth(guest.id, persistPath);
|
|
408
|
+
const { clientId, clientSecret } = usableClientCredentials(stored, guest.integration);
|
|
409
|
+
if (!clientId || !code) {
|
|
410
|
+
throw new GuestOAuthError('Authorization code or client credentials were missing.');
|
|
411
|
+
}
|
|
412
|
+
const tokens = await exchangeAuthorizationCode(
|
|
413
|
+
pending.target.tokenUrl,
|
|
414
|
+
{
|
|
415
|
+
grant_type: 'authorization_code',
|
|
416
|
+
code,
|
|
417
|
+
redirect_uri: pending.redirectUri,
|
|
418
|
+
client_id: clientId,
|
|
419
|
+
client_secret: clientSecret,
|
|
420
|
+
code_verifier: pending.codeVerifier,
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
client_id: clientId,
|
|
424
|
+
client_secret: clientSecret,
|
|
425
|
+
code,
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
let account = '';
|
|
429
|
+
try {
|
|
430
|
+
const api = resolveIntegrationApi(guest.integration);
|
|
431
|
+
if (api) {
|
|
432
|
+
const probed = await fetchAccountLabel(api, tokens.accessToken);
|
|
433
|
+
account = probed.account;
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
account = '';
|
|
437
|
+
}
|
|
438
|
+
// The client that made this exchange must still be the one on file, for
|
|
439
|
+
// the same endpoints, when the tokens land.
|
|
440
|
+
const saved = await patchGuestAuth(guest.id, {
|
|
441
|
+
accessToken: tokens.accessToken,
|
|
442
|
+
refreshToken: tokens.refreshToken,
|
|
443
|
+
tokenType: tokens.tokenType,
|
|
444
|
+
expiresAt: tokens.expiresAt,
|
|
445
|
+
account,
|
|
446
|
+
authorizedAt: Date.now(),
|
|
447
|
+
target: pending.target,
|
|
448
|
+
}, persistPath, (current) => (
|
|
449
|
+
readTrimmedString(current?.clientId) === clientId && sameTarget(current?.clientTarget, pending.target)
|
|
450
|
+
));
|
|
451
|
+
if (!saved || saved.accessToken !== tokens.accessToken) {
|
|
452
|
+
throw new GuestOAuthError('The client credentials changed while you were signing in. Connect again.', 'TARGET_CHANGED');
|
|
453
|
+
}
|
|
454
|
+
return { connected: true, account };
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
export const refreshGuestAccessToken = async ({ guest, persistPath }) => {
|
|
458
|
+
if (resolveIntegrationAuth(guest.integration ?? {}) !== 'oauth' || !guest.integration?.oauth) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
const stored = await takeUsableGuestAuth(guest, persistPath);
|
|
462
|
+
const refreshToken = readTrimmedString(stored?.refreshToken);
|
|
463
|
+
const { clientId, clientSecret } = usableClientCredentials(stored, guest.integration);
|
|
464
|
+
if (!refreshToken || !clientId) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
const target = stored.target;
|
|
468
|
+
const previousAccessToken = stored.accessToken;
|
|
469
|
+
const tokens = await postForm(target.tokenUrl, {
|
|
470
|
+
grant_type: 'refresh_token',
|
|
471
|
+
refresh_token: refreshToken,
|
|
472
|
+
client_id: clientId,
|
|
473
|
+
client_secret: clientSecret,
|
|
474
|
+
});
|
|
475
|
+
// The round trip took time. If the entry moved on meanwhile (a new
|
|
476
|
+
// Connect for other endpoints, a disconnect), this answer belongs to the
|
|
477
|
+
// old entry and is dropped rather than written under the new target.
|
|
478
|
+
const saved = await patchGuestAuth(guest.id, {
|
|
479
|
+
accessToken: tokens.accessToken,
|
|
480
|
+
refreshToken: tokens.refreshToken ?? refreshToken,
|
|
481
|
+
tokenType: tokens.tokenType,
|
|
482
|
+
expiresAt: tokens.expiresAt,
|
|
483
|
+
}, persistPath, (current) => (
|
|
484
|
+
current?.accessToken === previousAccessToken
|
|
485
|
+
&& readTrimmedString(current?.refreshToken) === refreshToken
|
|
486
|
+
&& sameTarget(current?.target, target)
|
|
487
|
+
));
|
|
488
|
+
if (!saved || saved.accessToken !== tokens.accessToken) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
return tokens.accessToken;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
export const disconnectGuestAuth = async (guestId, persistPath) => {
|
|
495
|
+
await dropGuestTokens(guestId, persistPath);
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
export const clearGuestPendingForTests = () => {
|
|
499
|
+
pendingByState.clear();
|
|
500
|
+
pendingByGuestId.clear();
|
|
501
|
+
};
|