From cb8263d95973816e30746bb8bed0a2a01f9acf60 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 10 Sep 2026 20:02:57 -0700 Subject: [PATCH 1/3] fix(github): simplify repository setup and indexing outcomes --- apps/docs/content/docs/search/github.mdx | 57 ++- .../search/github-installation-select.jpg | Bin 11092 -> 16234 bytes .../static/search/github-member-connect.png | Bin 0 -> 32397 bytes .../static/search/github-member-connected.png | Bin 0 -> 25572 bytes .../static/search/github-repositories.png | Bin 0 -> 36427 bytes .../static/search/github-repository-setup.png | Bin 0 -> 40073 bytes .../search/github-skipped-documents.png | Bin 0 -> 70381 bytes .../credential-groups/oauth-callback.test.ts | 137 +++++++ .../api/credential-groups/oauth-callback.ts | 56 ++- .../enroll/[token]/page.test.tsx | 14 + .../credential-groups/enroll/[token]/page.tsx | 3 +- .../integrations/connect-account-options.tsx | 117 +++++- .../integrations/integrations.test.tsx | 287 ++++++++++++- .../integrations/integrations.tsx | 67 +-- .../[connectorType]/provider-detail.test.tsx | 24 ++ .../[connectorType]/provider-detail.tsx | 18 +- .../sources/[connectorId]/search-params.ts | 3 +- .../[connectorId]/source-detail.test.tsx | 23 +- .../knowledge/[id]/[documentId]/document.tsx | 8 +- .../[workspaceId]/knowledge/[id]/base.tsx | 17 +- .../add-connector-modal.test.tsx | 36 +- .../add-connector-modal.tsx | 108 +++-- .../connector-documents.tsx | 36 +- .../connector-documents-tab.test.tsx | 67 ++- .../connector-settings-fields.test.tsx | 172 +++++++- .../connector-settings-fields.tsx | 79 +++- .../use-connector-settings-form.test.tsx | 17 + .../use-connector-settings-form.ts | 3 + .../components/github-installation-modal.tsx | 4 +- apps/sim/connectors/github/github.test.ts | 128 ++++++ apps/sim/connectors/github/github.ts | 61 ++- .../github/installation-connection-field.tsx | 72 ++++ apps/sim/hooks/queries/kb/connectors.ts | 11 +- .../hooks/queries/organization-accounts.ts | 8 + .../hooks/queries/utils/search-source-keys.ts | 7 +- .../contracts/knowledge/connectors.test.ts | 23 +- .../lib/api/contracts/knowledge/connectors.ts | 11 +- .../api/contracts/knowledge/documents.test.ts | 45 ++ .../lib/api/contracts/knowledge/documents.ts | 4 +- .../contracts/organization-accounts.test.ts | 31 ++ .../api/contracts/organization-accounts.ts | 26 +- .../application/organization-access.test.ts | 81 +++- .../application/organization-accounts.ts | 23 +- .../personal-organization-accounts.test.ts | 16 +- .../personal-organization-accounts.ts | 14 +- .../enrollment-links.test.ts | 51 +++ .../lib/credential-groups/enrollment-links.ts | 16 + apps/sim/lib/credential-groups/limits.ts | 1 + .../lib/credential-groups/oauth-completion.ts | 8 +- .../lib/credential-groups/provider-adapter.ts | 4 +- .../standard-oauth-provider.test.ts | 26 ++ .../standard-oauth-provider.ts | 6 +- .../lib/credential-groups/viewer-accounts.ts | 57 +++ .../github-member.integration.ts | 286 ++++++++++++- ...rganization-search-overview.integration.ts | 50 ++- .../search-source-progress.integration.ts | 388 +++++++++++++++++- .../knowledge/application/connectors.test.ts | 196 ++++++++- .../lib/knowledge/application/connectors.ts | 33 +- .../application/github-installation-error.ts | 17 + .../github-installation-source.test.ts | 1 + .../application/github-installation-source.ts | 5 +- .../application/github-installations.test.ts | 2 +- .../application/github-installations.ts | 2 +- .../organization-search-overview.ts | 13 +- .../application/search-source-progress.ts | 13 +- .../application/search-sources.test.ts | 19 +- .../knowledge/application/search-sources.ts | 11 +- .../knowledge/connectors/sync-engine.test.ts | 2 +- .../knowledge/connectors/sync-persistence.ts | 4 +- .../connectors/viewer-source-accounts.ts | 43 +- .../processing-outbox-handler.test.ts | 17 +- .../documents/processing-outbox-handler.ts | 7 +- .../documents/processing-status.test.ts | 44 ++ .../knowledge/documents/processing-status.ts | 52 +++ apps/sim/lib/knowledge/documents/service.ts | 64 ++- apps/sim/lib/knowledge/documents/types.ts | 11 + .../lib/knowledge/orchestration/documents.ts | 16 +- apps/sim/lib/knowledge/types.ts | 7 +- .../sim/lib/oauth/github-installation.test.ts | 87 ++++ apps/sim/lib/oauth/github-installation.ts | 98 ++++- .../sim/lib/oauth/github-repositories.test.ts | 130 +++++- apps/sim/lib/oauth/github-repositories.ts | 91 +++- apps/sim/lib/oauth/identity-error.ts | 21 + apps/sim/lib/oauth/types.ts | 1 + .../application/execute-selector.test.ts | 43 +- apps/sim/lib/selectors/manifest.test.ts | 6 +- apps/sim/lib/selectors/manifest.ts | 4 + .../selectors/server/providers/github.test.ts | 128 ++++++ .../lib/selectors/server/providers/github.ts | 100 +++++ apps/sim/lib/selectors/server/registry.ts | 2 + 90 files changed, 3726 insertions(+), 371 deletions(-) create mode 100644 apps/docs/public/static/search/github-member-connect.png create mode 100644 apps/docs/public/static/search/github-member-connected.png create mode 100644 apps/docs/public/static/search/github-repositories.png create mode 100644 apps/docs/public/static/search/github-repository-setup.png create mode 100644 apps/docs/public/static/search/github-skipped-documents.png create mode 100644 apps/sim/app/api/credential-groups/oauth-callback.test.ts create mode 100644 apps/sim/connectors/github/installation-connection-field.tsx create mode 100644 apps/sim/lib/credential-groups/enrollment-links.test.ts create mode 100644 apps/sim/lib/credential-groups/enrollment-links.ts create mode 100644 apps/sim/lib/credential-groups/viewer-accounts.ts create mode 100644 apps/sim/lib/knowledge/application/github-installation-error.ts create mode 100644 apps/sim/lib/knowledge/documents/processing-status.test.ts create mode 100644 apps/sim/lib/knowledge/documents/processing-status.ts create mode 100644 apps/sim/lib/oauth/identity-error.ts create mode 100644 apps/sim/lib/selectors/server/providers/github.test.ts create mode 100644 apps/sim/lib/selectors/server/providers/github.ts diff --git a/apps/docs/content/docs/search/github.mdx b/apps/docs/content/docs/search/github.mdx index 8c3da90651a..17d3b4157a4 100644 --- a/apps/docs/content/docs/search/github.mdx +++ b/apps/docs/content/docs/search/github.mdx @@ -7,7 +7,7 @@ import { Callout } from 'fumadocs-ui/components/callout' import { Step, Steps } from 'fumadocs-ui/components/steps' import { Image } from '@/components/ui/image' -GitHub Search indexes text files from repositories on `github.com`. An organization admin can install the GitHub App once and use it to index selected repositories. Each person connects their own GitHub account once to search the repositories they can access. Installing the App does not connect teammates or give them the installer's permissions. +GitHub Search indexes repository text files and supported PDF and Office documents on `github.com`. An organization admin can install the GitHub App once and use it to index selected repositories. Each person connects their own GitHub account once to search the repositories they can access. Installing the App does not connect teammates or give them the installer's permissions. Admin setup uses your organization's **Settings → Sources** page. Teammates connect from **Integrations** in the main sidebar. Installation indexing is available for organization Search. For workspace Search, use **Search → Add source** with member accounts or a dedicated user account. @@ -31,35 +31,30 @@ Open **Settings → Sources → Add source** and select **GitHub**. This opens * -### Choose how to index +### Connect GitHub -In **Sync documents with**, choose **Connect GitHub App** to index through an installation: +Select **Connect GitHub**. If prompted, connect your own GitHub account first, finish authorization, then return and select **Refresh**. -1. Select **Connect your GitHub account** if prompted. Finish authorization in the new tab, then return and select **Refresh**. +Select **Install GitHub App**, choose your GitHub account or organization, and choose which repositories the App can read. If it is already installed, check its repository selection. Return to Sim, select **Refresh**, choose the installation, and select **Use installation**. Only installations on your own account or organizations you own are available. -Connect GitHub App dialog requiring a personal GitHub connection to verify manageable installations +Connect GitHub dialog with an installation selected -2. Select **Install GitHub App**. Choose your GitHub account or organization and the repositories to include. If it is already installed, check its repository selection. -3. Return to Sim and select **Refresh**. Choose the installation and select **Use installation**. Only installations on your own account or organizations you own are available. - -GitHub App installation chooser with Install GitHub App, Refresh, and Use installation controls; account name redacted - -The installation is now selected under **Sync documents with**. You can reuse it when adding another repository source in the same Sim organization. - -Alternatively, leave **Connected members** selected to use members' accounts for indexing. To use a dedicated indexing account, select it under **Sync documents with**, or choose **Connect GitHub account** to add one. Each method still requires teammates to connect their own accounts for Search access. +Sim reuses this connection when you add more repositories in the same organization. Installing on all repositories does not automatically add all of them to Search. ### Choose what to index -Enter **Repository** as `owner/repo`. For installation indexing, it must belong to the installation's account and be included in the repositories granted to the App. Add one connection per repository; installing on all repositories does not automatically add them to Search. +Search the **Repository** picker and select one repository. It lists repositories available to the selected GitHub installation. Use **Add repository** again to add another. + +Add GitHub repository with one connected GitHub account and a searchable repository picker Open **More options** if you need a different branch, path or extension filters, or metadata tags. | Field | What to enter | |---|---| -| Repository | `owner/repo`. Use **Add repository** for another repository. | +| Repository | Select a repository available to the connected installation. | | Branch | Optional. Leave blank to follow the repository's default branch. | | Path Filter | Optional prefix such as `docs/`. | | File Extensions | Optional comma-separated list, such as `.md, .txt, .mdx`. | @@ -71,17 +66,33 @@ Open **More options** if you need a different branch, path or extension filters, ### Connect your account -Open **Integrations** in the main sidebar, select **Connect** on the GitHub source, and authorize the App. Teammates repeat this step after joining the Sim organization. For private repositories, both the person's account and the App installation must have access. GitHub also permits App user tokens to read public repositories without an installation; see [GitHub's permission rules](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app). +Teammates open **Integrations** in the main sidebar, select **Connect** beside **GitHub**, and authorize their own GitHub account. Members do not choose repositories or an indexing method. + +Member Integrations page with one GitHub Connect action + +Each person connects once per Sim organization. When an admin adds another repository, Sim reuses existing member connections and checks each person's access automatically. No additional repository invitation is needed. The member's GitHub row shows their connection status; **Reconnect** appears if authorization needs renewal, and **Disconnect** remains available for their own account. -An App installation or dedicated indexing account can start syncing after the source is saved. With **Connected members**, indexing begins after someone connects. Each teammate still connects before searching. An existing GitHub connection in the same Sim organization is reused across its GitHub sources. +Connected member GitHub account reused across repositories -Open **Settings → Sources**, open **GitHub**, then open the source to inspect **Documents**, edit **Settings**, or review **Sync history**. +The installation can begin indexing as soon as the admin saves a repository. For private repositories, both the person's account and the App installation must have access. GitHub also permits App user tokens to read public repositories without an installation; see [GitHub's permission rules](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app). -Use **Settings → Sources → People**, filter by **GitHub**, then select **Request connections** to send provider-specific connection requests. These requests do not grant organization membership. For another repository, add another source; members can also select **Connect** beside the GitHub row labeled **Connect a different site or content scope** in Integrations. +Admins can use **Settings → Sources → People**, filter by **GitHub**, then select **Request connections** to request teammates' initial connections. These requests do not grant organization membership. +## Manage repositories + +Open **Settings → Sources → GitHub** to see the repositories and their sync status. Select **Add repository** to add another, or open an existing repository to inspect **Documents**, edit **Settings**, or review **Sync history**. + +Admin GitHub page listing configured repositories and their sync status + +The document filter separates **Failed** processing from intentionally **Skipped** files. Skipped entries explain why they were omitted and do not offer a processing retry. They are reconsidered when the source file changes. Empty files, unsupported binaries, oversized files, and links that do not resolve to files within the repository are skipped. Supported PDFs and Office documents use the shared knowledge-base parser. + +Skipped GitHub files with reasons and no Retry action + +A genuine source download failure remains **Failed** and is retried by scheduled connector sync. A processing failure with downloaded content can be retried from the document view. + ## How access is enforced GitHub App installation access supplies file contents for indexing. Each reader's own connected GitHub account determines which repository's indexed content they can search. Sim organization admins follow the same rule as other readers. @@ -98,8 +109,10 @@ This is an installation plus personal authorization flow. GitHub Search does not | Repository cannot be read | Confirm the App is installed on that repository and your GitHub account has access. For SAML organizations, establish your GitHub SSO session before reconnecting. | | A teammate cannot authorize the App | Check the GitHub organization's App policies and required approvals. Self-hosted deployments must allow the teammate's account in their App visibility settings. | | No eligible installations found | Finish connecting your own GitHub account, install the configured App on your account or an organization you own, then select **Refresh**. An installation of a different App or one you only have repository access to cannot be selected. | -| Repository is not accepted for an installation | Check `owner/repo`, the installation's account and repository selection, and your own access. Update the source's Repository field after a rename. After a transfer, add a source using an installation for the new owner. | -| Identity verification fails | Verify the email used by your Sim account in GitHub's email settings, then reconnect. A public profile email alone is insufficient. | +| Repository is missing or GitHub rejects it | Confirm the repository is included in the selected installation and refresh the picker. Update the source's Repository field after a rename. To index a different repository or one transferred to another owner, use **Add repository** with the appropriate connection. | +| Your GitHub email does not match | Add and verify the email used by your Sim account in GitHub's email settings, then reconnect. A verified secondary or private email is supported; a public profile email alone is insufficient. | +| GitHub does not allow Sim to read your email addresses | Ask the GitHub App administrator to enable read access to user email addresses, then reconnect. This is a user permission, separate from repository permissions. | +| GitHub is temporarily unavailable during authorization | Retry connecting after GitHub recovers or its rate limit resets. | | Authorization fails after installation | Return to Sim and start **Connect** there. Do not enable authorization during installation. | | Account authorization did not complete | Start the connection again from Sim. If it repeats, contact your organization admin or Sim support. For self-hosted Sim, check the [App callback and credentials](/platform/self-hosting/integrations-oauth#github-search). | | GitHub asks for a provider configuration update | An organization admin selects **Settings → Sources → More → Update sign-in settings**, then affected users reconnect GitHub. | @@ -108,5 +121,5 @@ This is an installation plus personal authorization flow. GitHub Search does not | Empty repository returns an error | Add an initial commit, then sync again. GitHub does not return a file tree for an uninitialized repository. | - GitHub Search covers repository text files up to 100 MB, including symbolic links to files within the same repository. Path and extension filters apply to the link's path. Broken or external links, binaries, and submodules are not indexed. Issues, pull requests, separate wikis, GitHub Enterprise Server, and `ghe.com` domains are not supported by this connector. Personal access tokens remain available for general knowledge-base connectors, with that knowledge base's access rules. + GitHub Search covers repository text files and supported PDF, Word, Excel, PowerPoint, and OpenDocument files up to 100 MiB, including symbolic links to files within the same repository. Path and extension filters apply to the link's path. Broken or external links, unsupported binaries, and submodules are not indexed. Issues, pull requests, separate wikis, GitHub Enterprise Server, and `ghe.com` domains are not supported by this connector. Personal access tokens remain available for general knowledge-base connectors, with that knowledge base's access rules. diff --git a/apps/docs/public/static/search/github-installation-select.jpg b/apps/docs/public/static/search/github-installation-select.jpg index a79f975dbffe5b29768db55a08d791fda80affc6..bd297aa4d00f075d9361410bb6d5021cbfdeaba8 100644 GIT binary patch literal 16234 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 ed>EjCk literal 0 HcmV?d00001 diff --git a/apps/docs/public/static/search/github-repositories.png b/apps/docs/public/static/search/github-repositories.png new file mode 100644 index 0000000000000000000000000000000000000000..5c697a25d6c7ddc0f8728d53dae971571537a3ec GIT binary patch literal 36427 zcmce;c{r8p-#%(6*Y{D;QBhD(?30m} zRHdNUK8&A#?b?R_?qYf_fj@TJNozS$Q0zHO{`W76*!W`<6elTUB(JEu#ZUF`whJFE z+qu@>-X1Ui^SPz2%;|AyA#JIKF9lLz7v_ZuxF$Vn!lVX@H0L`_+-nRZ-cWve(i8nK zf#S(-1KxwY54ldUJk013Z2Q6Qzg$EZ4{J%QA7N>?+VMHz*Cp@Sn}-g|n@z@T`quJF zY%Y2yvE11jyxzQ0wZ*a>cXrqJn8Wzi%4jo9ytsa0u)5fz>NC^<^2DbNBdTTc+|gS} z%tQUIgBu$aK6Wek!MVAA>#Oge_e%c?X(89Oe~E|u=i#%8zb0lv>5m^@k!E4`lgLnI zH{#p9aVW@dYq4}|6Q4tIcWaBqIY6EJc>5sV$jy*%b7xYo@E#8}YF?|GD*(4l&=e z#JA$!mL2!*c&%7!J7*gy!A=`=hS~ligP8BkxRUtB;#%?0WOuH9wSb6-$m44Ku-Q4u z`)#5#%W1uVC%0<()8yQi@Tu=Xo4Oh5>_uCw8o$@8iJ={T;S)CRk)%|a+LS z<>tm<2lP@SRBzHv@lO+{mU!y}r-+6wu>oucM)k+Nq(r6rj|rBajEwcZgvs6HWhzFH?lk64Ba zop5c4;)>?cuW}gq6m0(N+Ks4Nwv1i5j`x>>udE`hIQKbz*iQP~c!DGCnkm%>LB_cT zp6cJd?b4o{eOqR&Nus>dJ8T%VTNq#JRxJ0JChgn5e}2`=Wk}- zGLoN3Bp=+hgEO>!Lnz_N^u5?`|0h6_&EH;vVC*mrQ)n8SY=rwWqI3E5TTuU)?@>C8>l1quXzXAQ=uS=JE4}S9% z*4~_NDw=BQKRLbEJZ^Avxl~I_DWgqE7!Hd=4 z157ytzY2S8!6vhj<=hk0d5#92o4vQ1xWXL^FM59c`IC2L`_3*_$pPQ3jkY#b4qt-4 z$|pT1jf;dnk^sAh_*g|$U%(mKAivX+%!figq^=~=uk@@?t#byPh^`tMOFgsZVl=p7 zwWFSTwR=BXXg7BEtAO(F=bXtJS?trwCJ7hqnL|Dd`y@7hdO|93UVciyu4i+asHQ<1 zR2Px)%S&tzDVrpd9P^}MpQ6?8=NegB$>GksGCIQpNza{+Qsq6 zp)Qv)h2s**Ekky{e~*ff{^%~^m*4u`yFa|OV7^%V>~PVEP4{mRxh{28@18i^>G+}X zp-f+)VIv~XHanHDG;Oz&G2(Sur?<@_!CEt@p<7JB#fHiEx4pSJd!?m*ct_7Zk6A6F zQJ)Zp1)=p#Wsb;umkoyB2XEiLT`r1~ori~DdqK93^q#$|x9>dPE2+Ly>&P=x$nnv zf!b7Z)nK1xV`Fo(e!gMd@^qi;4C#_-6GL=EUEs!4?B%HEX{tnxtn}Q1UUBRCHj$;Q zmQ=(1FP~%1YYpxxrB-@CJ5hd!%8xU{kte3pqPTKMZE0h{es7vAH+B3La#Hrqcmu_ zn6m#4;;H)j-rGi4xFY8Qq>|Wm%3kB^(g{ZKt5HPhluACMG5{^b~c#*fc7p2xG{5if#8U zqj&7smai#t`|XZiF2vQsDqfadCd)=+Vq!Ay_B!D3*@7#54|gACRgA}Ldt9lJes7-H zoqLCsvnkEhDl|cLKX39`v&>s9xf2C@rvOfGQk9N_&hR2*g?(&m(QP5s!pmZf!nNz3a zvqdp(UUl%D=!#eIXIAG-$J|Rsr*{HAj4sH@$qjl;=FpFN-d=TcbNe-2I;e+WvCOS> zS)hDod3tpTzuE6!Yiw+c#TyogXb)Z(ZC+VkuKGRSK%!l69Bty#E*kSYs`pCfFVCW0 zk)VD-#pbt7Q6;aP#@oJr{kpm^D!%z^7_Y1R`F4-_#vF;>{AgLhhF$a1lC7V$2Xwh_ znwVs$66JG=1l_t@>gxQ?6Y8h~t!8GAuem1((;r&*GFn?4*D19r*3Q{lRN9J&i|ZR0 z(9_lBq7Im!pQjFZS7hFf{Zl2XXLaHqMS>b@II(l$)2C1K^A3eQ40|cP#q>oO1>+h8 z;&M_`@AMX#D-y!1{2xo*;iQfG=Z+K$b*mXabLdBl;*!$R=J-p61qES!t^2XFc9WWg z+!y?5XlOWf%bk9Fj$saEJAb}?a>-_}%6o=XW>Kt7RM)t1Lm^2_G)Df%$cRe2i-v~A z$%D@XthG-bw~`)lVYvQUePir%|?nW&DqjFcU5C@Zh1n5|sV3$I6Kqvg@_ z+WftGEr`~=QqRBkAM>`fwzjtNav_d}AETrGwYQ1t+Io5e7gC--znt3oIsVeZR8OI0 zSN3ko2ON=~Y{<{*h_yX)=8U?{sNT0{Q5{paVHD!>21BHJI;}U z$#`)rHN?f)xwE@l{P)jVbp8zCLHE&w)7PKu-M4SLuhh1`+&Q;uZQ|$8pNQi1kEcV( zzaOkllG;WSI_NoD8FZ9KZ0q*|LdkO-fra0+6O?BAD^}k6?Ya5#*6P;>2L=$1-TJ;8 zi{tJ7`%kPj+WK7e-~1k~G~hNIEb6%&FXBF*8X}g$@vAQA=uS#X#RQ?H*6U2(^Yv$| zf7S8$Y)tAWU2@DQFE1}HUc%Edc`vpSl6=;F4Tr2OEmdx=O{x>NI&*!N)+TesS4fU$ zl@h&HN20}7zlQ@G`=ZwHR4%sdWb%45J&=5WAztz9zJ*O@Vj(TtT18q~T24+*M#iJ& zQ8lJL`wBI)c31qmE2g*Gzjo&VMpBuh=|u*C82OG{H}v#2@hFU!9MY<*w_1{~hM!g9 z=H$GUZ+J~fDMXo#KSmw}U+cA7e)lAvX$nt@eH>L zO%!h2xM6EMyScGmYSZuW_02tHws)1EXEnIb=VWJ-DqL;ZXi_+;G_v|jtaWT{bMeWKi=Bo9X3LPCCa$4Q_@Wgw4dEdT$ogE!k zeI?V_9<;v3#>OC8E|0JGcE-lV;elRuPoC%Fi{jQT$6Xunx*={2%a?}ynOPt#?kP0gxpSxK=VyoZQX&Yta`m^~Y(Kyt=Jg7@b3?jadwJE%M@DA1AaPLhLbb=z z1R|}}XJbup(Ca)`1i?A+R<+N@{73`Vb9#1mu-LL2yT^0umEFeL(oub{{KCS*va+(Q ztcGzX7bmCJAON|q=Vdt}Eo^KO`HW?qot@?68kBt3)Hs6mU)h~v3^{t_$kOt%7%Cmg zHeKAkxkwP-w|8$xSC?X(02f_wQ=;hl z0pDM>leyIcZEX%naK#wD^ool0Z*@VHm6ZsQOLkxHwvX@Ku?<;J6_OM&(hM;*Vh*j+h~%NljAIuU@o&ke<`5}86hAb&{mwr5%~g11~`Uf z6?2(-);?a;H;n)mLqn30kufzjm5~Y1TPfyPsg$aGHxjIxmAzw*X*V4Y} znXxg=A~O~2-J3UWY<<^tqI$~`NX=sFR!U0E=W<^=jJKtuDIn8ytGwJ1(^!a9p_Qft zVbiAg)nTSBH(dFgM)oVO)p^9e3#h8`r-*2MpBR^=`39cS(o);rqH)A``{@u7KJT@~ zq~G7qMsw@-=H0%E4-Hom>*?>Wv>%qT5G4b?`+x7z!wrtN6Ht0hfFf>Iuie1wG8E!a zo-d#)k>Ahc^|K}m-JwB$)1jxp7!Uj_X=^oUY4T4qfiB_uJ@k4?D_iGglKYRC-rim$ zt0QWjP3d~&%0LA;=8wj>o7tLkDECWCOO3G?Cg0D*s)P&0WnX6w9a~ycX6r!cwzPb8 z5^A`vuAxzWi#x0H6L*-w%~Q%cK~kxq^+xXQ?t5rC-F^-ES5#D(HYbV#4tpHU%F1GL z`+9F?W(Hxa6epm-8VzjO0wmPL9FxNdB1n<*CW>O7(U}qhm}O~QJRr? zTskG$`qfnc#rgU6X75t21#!~uiId)ixc%CdUzSWXiiwUcMIyQ{{1{*IpyCBO{E~OO z7O17`rS9ayDxhR$`?!#h5cx9@P?6_uqI;|5EF^l)sIbw@x&3}~Z~OHp3^C%qRh<^b z=LH77t*YO+fdGl??AS+Hi)Ge$dFvA1iDqg$@VYEF*A|&ZFJSetu`!28OfK=;SYJVx z?H;mY5cTL7vTONy`Q$+(BO{ulN6F3d)hoBR4|k(*4Fax;u8;~%8e@-~(@?*8lS#p=}~ zSDKiJh}Yb=I@2WY<<-$7ai8CAmX?z!h=9Q>pU*eHUh2{zb0h7)eB8b-V{7|uRuD*I z3xSB-%K1;{kWfPQ-*y^E%I}r{KJ4 z(`%FgXcDQ4k?I_sZEXeU8Xp)vt?B}gD2dFy11v#tw+C)R|4_U3jB0=!llQjo1djSW zslVlctY8c|8!9cb)S*_r+GvX=nppRKCv8b7cmYVQ9_`r38%& zDZm)=!Ajuf&6~4*rMac0N4+P=C6@k`TC%B$2_lb2r^ITag9zetm{mTLNbJFLlbe$= zTaoi*m)%9LZe3TR`U_Oq9KFi&!a_$Bn30hY@n1CuKASl5#bgtS_7(ju75$S-SZc?b z`+t+te9l=#;&iJtt`DrR5q)Pp2OTT=D}a-# zyw|~`+(iZu&y+iN%E`*^rK|;_IUV|MkCm9ym@?J|lnDI*-Q*Vc=$xA(feiXtjhQS} zO-sq(7LY^lwOgo(Wm&$?#wVm?uHMG9t#4m@d$??R6f`f3&KJwe%bJ`5{QMn#eHB<` zFk1%{zh861WSEP}fwv*Ru6XMVk7SdauWC9HP(`hJz1XW z0m=RPwdMi9!@$PMU^VVYNo>^!r62vqi4K>x5n&_)Tp%ahh1}t{ryMv_8FIY9ZK z!wIVBL`g}BKck1)(nP0Ag?Cf@r3Vil@C%5#&M-JHEUv5wCn(;#{8XA{{|O;})TgGV zCNKt0&AgL5Jh`Y!;|0-)&tNgJ8 zB0tgeF1xzA0;M8%f&KKAm7hjN7Mr!E+`D)0<;$06`9G-BDp7A=EAMLuG5JX=%A* zD;a@l(Vx=OM-W$lP^nrcY*UrU5t-z>xtcB)4Ibez^wzJa>~onQbW|zbdGU>LCE~ds zzzea0wgXrLAY^TA?GmdV2gC{*>yIBlZt8n40rlr=PHnEwG95d%i}Jy}%Xf8KR=)-D zaNm0MGyel4x#L3NOp<2N$kBEN)?Mad2_>Ot1Vp) zcNy&1}P9_o{ArN!0)M<-9 zb<^|9+7*sSj&tX-Pzezuh*(mQxfkd{;Pd8=83EV;g5)4>nk4)H3y{(?e_{WIgI3y?%>zc$Hq zEwvRT4eAA567K6Z5)Iv-z{&)rVW|tv(ULzFS&HRALgDru9J_PQI@=8W#wNuHSg(P9 zEEfk+{o{Xt3Fx>3mE(F>nGf(T!1U&fYqjU^xesW6z*xWp`Mz75rGq}}h0S8Zp37EX zMdacIwP1FzdaHU1Bc@v$3rSnQKZc$@%A?1?$hi4?p&44>2+~QNqZkawb-;ZTPvrRH zGbpn?AZ`S^+7l-6&2d7~{sRZ}K?;D{Q9=OvLDQ&?bmO+-PMmH3|;CMq&Im_v_Ia;BW2A{DfhcND>q1nIjfs)*ZC^~+l6S~BC&5`Av|cNoRqw6j`vu#KFeoy2HBqc3@E2O z{Ll2m6q*t4*n|p>6%4ZMg+zk^*U6O@Hpvx(jAJ}y*<+l{iHc{iXhxaIV zJ4s@2FuK5=)-R=y&^IyCv>@w|*_QD>*W~J#pz)nSp zeEYyCx8S9&nWGvflo~2XtH;Y$d@qH)u$_9E>>Tk**y!|Mva9;S6ki{i(60EdHs~*} ztW+*IYEBUOd2qa z@7g%Qym)lT{>R#}!@m=H#f3k04>$r9qZ+A>y06itlV3_PNSV}ZYhkgrlke>4QvKJ3Sr9yh&+|+|hUP|l**1GW80&IprzaMdfz%b} z=JK3A{XjQAAdM`9Qn>D|cw7v`4b`s7J~qPlV6EVZQ!N6dJ`^#%QkxUHC32}JZmE!G z{}iszFfya^^AZ~zaj^c9H-tR(*;wlKr8+&pFxQ^6xrk)8ty=v7Wnu{d1YHi%$>=A? zbosx%03p|ph84IURZ-iC%iR@zq%e@9N_=NHhp{_XsI=Kgy!|!4!0Kgt^GyYbvRjbG z<1dDO7$!kPOdO(<2Z^dK^Sg(#uR(-BY1!%iBF=ZsSQcN9DKe zKkHy1BB4tD9ExKhPyYMh4l(kqknA4a;wD_PG2gvC zHeNqjxnXq>9#M>mB*aQLH)bCyZ87>jO^-FWueBD+T*z<=X)J82_BS@p5W!+usugZm9Lqt z&fzDKTw`EnX$g4n3(_H@=Tts;YZAz9b@kxrLO`8iX`eSFNpKDJ$WK#KRv{nm@E%K3 zHP_JS0-XpsAsiJQefIdN=muw@epDQ&j%;IUNB1Rvs#NCy({Z25{}9Ec^FG$-RemC) zU#t-MnQ#7CQ1`eCrD*feR-M+Dr$K|v_+#ZWB$@4ji}25j7aYM-a94b4Xuz(Z(U#ik zdwXwG|8A)MI5L09@dpD0-Mlab14;L((vNN-#Zgb6(kypc=%s;jPE1c?(pQ;6XSCQ1n2^3VK{ zvJlM0|46@&9zDVkhP)!q{tN|^| zCpID*i|yF@hNh-OA?FF`?zmf`o}QZ8!k0TCXRvkI*{RnWo2b^edk zaP!8ErIi&P%{+`3DYxCLsku@;*gU#$fv)kj`vO@Z12#3{Lvr;D4EO-^&kR(qV`#t{ zZVuE2YJ%~5{pZgS^+u4wFct}rl*rLYx@d0*2n~|XZ^FjIBksFN#x1K}VsW2N*q$PX z{8@6yoB5q~5k9z!LB!1t__sp=;`)jG*P#JGzebGW^O5DtvxDM5W}(s;gzje)IkuM) z3c_B>t3X=dBJ!;5@5B#N`9W6^h6vqihD8yv|0e0m=J``x`>+55+knG5QBs+G@m@h^)T{WxIqtXYI6J%2=HNUcQ0}BAq>?Nkp zLVe-F1<=|mA~B#`AH?Tvh95h`R^Lr8$hcBclu@Fvq}`nrU~xWnMB#Q*7mF%KBy_ef z`FEf&frB=D`otI42qppe?F}#sG>=ST@)$eyf`~!~SZtl zRwp+YxtSRoq;!V;;*gQrGZfm*__?Dk5uXqogQZfj%@Fh4kpg|ay#W7>CSVPH&=XZO zH9w9nj5u7%@eeFLNY2b~a%NgNtCrA(YGf&}P-CK^B)ZfAT>u}N=(9GCjDZ@It~%~e zhWUP}Q9X6CvRS^S%hZ=SsJtLiOfPeDA@TjdTjm#lPp`{PAlNQBeE(N3x4E&gA37w| zGhTlFMGVL=97r8)P9l`)lhO6N!{{pZ-#|MtBZJUw#YuY@IF@H$>BIrt+|Lsa()J5p zz4R=sT%I*`{1E!coqSD%3^+_|Y%H7a9>2Ho@$vW(wvXPbyu7^1N^dNhMpiL?CK6?` zIfKn<4k$VcxC|EUP4WlX6yPgXAZeNbolUz?WN9)ThTYMqvd>4%=X@IYG; zF!V&i(SRoA;sh5!mtsnP+k{-$A>3?~j3J@1%RGg?$c5Vaji|4X3{hQnoxs!DRA$MA zQ8nKG;aipR)MYD~o4BjHwg*rJcjugv&5A1UWUGeU2tv_TqRE-D<1_6+2$0O7|8|`- z?nwyV0e??J4cvX3G9OYzxDM{>u(&X-z)7oCVzM65FoHI7={~iH+(JZQ&(FKlNr~I& zS(M*qoX$v~+%tzjVGeCkyQ35@8YQY#a`GH`K}!A8_NN*KL>a4TJdLV1dV2_JXV^b~ zn+3k0DK?To_Xto{Z!4z?0@4&Ya&)8!v`RO#`2u~Fb914)+ zyxLAwJrZv*?IbBA>cLGq^_QSU`PD&T{N=qIS&?YMumk@m-aOVj&QnX0W^Hc zW>NAqN&VYqnCdo{AeEm)x+-K(3$^5bDCo_Vk0ccn^G@x!wpwJSm~$~(+m@eBlrf~v zFnN3HO|2^l0(XKhlW$`1Pq<&RkS#JPyhInw8eT6G&WbUh-9?6w59iLEWB1y#W80^i zh%&9)aoWn6YJSU7Lx~P9sQQ@Qs+J;q{^g031qKb8r3*HJ{MAvYsGn{j=vvAt-yY5O zO)U)TiKpTC#yM74LMtHgJe2ZWxLR<0lRh#?^wOnEb8aMy;=s+v-X3q}Mf+gkrTD5N zf%LE!ehtqobe;8$^z?M~ml{NMRN_`MbwWF=cEM5ra*w4_6$y6|&clFU!2A9CcjYFN zOBX$X1&G<%kLwKoxHt+-?8|PURk0JZ)H$LXB4yK24~n$8>MV0K;x61zV5xYjp00ZQ z`k0dt9j~%fGf+}fQ-)|u=4Iwkbg#mm>`n`KZX&l}8IesV|Gz{AyfY!enxBrnJ+AQw zT6+ta^+g zg!FW>T}|SYf9@wtZ5PsUA;-aGO zFqPOWuWtbP4cT3ck*7U!WVasx09sO?Ju0zpi2dp5X@E1@pmcm%Sy_)s;mD64?+yPEI=noz z-O`Ip0ROWJq?j8R)YKcH{1eqVum>e2E-o&3<%EQ}g$30gFBBdel6P@&=ZFJ%AMD5i zxZB&?^Tl*bF5zB)&<*qR_FU$om17R& zJ+>FPvV_o9eEB3~M$DeO%=q~;TP%vz-@0C8Fe%hZZO!bQQ0KtN`xbX}`#7d67)HkS z+jLtLOQp7231H%VK|lbvL${Zb&<+j?^FrO9@eh*KqL_cy$j0Vns~NmSh%X)+c7+Ma)+b#(XiK4R|A0bI_k;WDh@e?PdA+ue{6iV_OF1nJXpA6-|0RSj5&FUHE!((xh#<%3f( z@`#glFR|F%jFOTrBR(XP%H;!o5<_;cii>aLb^~G~;gRDpFzJ<%njm%=jvEjEUe0lvBpDFREd%SppL}9FX~{GKeksWK~Z3Xxj0*sGYat( z7)Zq!5*-tR-jS`5g>5Ag5dbO9SI=>9AiotApY%=JSXiK9Iqfq;`hujUVrGhAv;ptf zoZCZ*Q#z&{Rsy9ZC0IIOseGhgWn}`Z|IorJU`C95!G-H6P_;wP>uGVx4Dbi`!QnZG zc@Vb!Bmf69VY9Ju6=B5W;P~<5`~4p*>+9(;?%DyGr^&hOCNgBFncLl7G^a>t7h|Rl z_+lj>M9hDSOC3iYDM_##nCPfJT?rG0S%UD`?@IwGQ|q(tN9 z&7((;u6tG4UA(2Ls;aAdB3NqL#nar}T!c|6T@}#!`Sa&6q-A8hwGNaZ7dMPUvqIt^d#k0|di!0d5Z08x{&9 zLNd=vO-0fh@xe{z>f~frtew;xEzN@Y-ySL|l(m~TqcyT}Uc5-zRt*(cJEz{LAV2>f zaq(&l+L2$cUcqj8J++lYx@8fR`{IR#nOV!^5{XEf+t`6ecBLXC!kkH&;%U$oaMywY zKB!$Xy*8zS0s`8cw8h%fPC{{Ual>=324{KL*q%#8U?vV~mHhO>FmAKs;*8xzvzcfp zDoY+9%uwFpFBo^i!p*LQ zU*>+;zQErtnF;^m1i`q9)dgjwAlfj;k!k;~!|Bd`Z-??4MpBj@XR0c8S|%$?XGH-G z(n%?7SEAa&vHyW2^ zLJa*62i0{vR}!?@yYejM5P1zwvN->uJdnapfMX9Lz~*L-vWaBIDZ1x*tJ&oLYeL0m9O+r zA&%j2M!ITDWTY`rGi!K{RmtN#?Hr7*x(-FrKa;x6SZCH3;w+vz-Od-N)6=XOvSdvn z4sRAWXnYk_HU zfo{@%t9Nua1%=?|>yb^LY4F;)~V{7J#N{f;$5Q`y8i1yZ8jIWAl&J}A;K5)*u&sy_mBozs5K)shn2&n zUlpC(cz^NEzw4ko0;@s*R2o#PVNt0!su zOQCRAT_UzO)1XBQ#FcpuL{!@fFZUbDZ1&Cid!C()W12oYUg}ElxzqpO3of-zlv`=Z zJ+=nvnWOCJ-M>f-aEtux)37|bPw{jSb%x4Kp-saG+sSlA10g+<{r~rq%qt3=B+)@J+|)ft+q*dSGwT$ z_wV1od{S9k`$hMpqoX4%NI@M%zL>+*(l4)l|M8;>P_m<=#q+u+lrSgG-bCHI4Uecu zp_HCID5f$eHlv<)47pfLaPI1zUIs4QwFBF%os;opbOnx`U1zLb*cAra+Su6e(*X~l z7{SK>@?{gKgcm@qOGN^sL|F^;(`lEuMydM9URzMiMw3)G*#<6^ZiR~sFJHWXn76vR znw<$FUy-&ous9MM{g0%D<;U8qsomj=0mUpREChYNlYgwU7v~cA>980U#k~f+7~*Sk zYG`Uo2Z^?h=Z}P^v}OXeU2fjjlYMHyq)zU;64uLjQ*U?ipPG*TtiOOGZcIU?vt9 zywchl%fFxU0Zd**BC)bk^w?h2nY1wyxXfNkb`t7ijJyfc?PBfPp}g85GkBs!+#N7i zfRP2bocEZ~Uw~O>!|Q?6CMGO$yFn4y8$B!aD*C|%fw`a);?-ae*d-^ZB3Nta0w0Eh zgM$HZplO0)Glq0`cA`_@8g>`un_s4<8$+mAeDmVP3s6|ubTlORyDRyi!RZA;W2xw$ zSy<)Z;DBlfAwEo=6~NivKEH2TFLSMi3Z?&VWPr zEefq5PN^gTW&gbupYQ5EyxVn@EOPbp*wK@NG)ySS9yDUkE&hTn5bFd7 zrd9j>uco#t-+p#x9!L`hQlg@0sZxQ}sTKg>czBx4_(8uVnN^gPm34Gr_vEJw#$B<7 zYv*(g&yiqE2P;({#Tgru!m`XvHh9TtY1cM3;Z=J3RstevOx&kh)^K)Ek?835;yjRM ztTS#JOg1m?X}LZa_wd}s&JJkSy?uQcU*Sv-vb@lHHs}qxx?0(%H9lthw;u>lH z9IWy2sp_`@cLEtw9~O)Os5~7hiwkOTM*WA{2^rqGbLT%+K@mm(QREwz9aREqM^O>~ zvAsCr0yqnoxWI)Ak&%%XV1euELh^!D1|EXzIQ{f*A76KONZVO2UuLwkadSgZzy<5Tl>g`a{V zOz3wo+Um1#p%I6`uN@nzH*VmxF0nN3z7z4c4&xKmU_1Pv`MY{wW9V8y+} zCIX2sU)q?N!KsL%iogNoO>G62#=5BpV-`Tn%!KpGq_D>@pI=ZA900Tv^FD-7Qu9qM zt?`xh@xpEJI4!KwNp*C0kADA-K_trU`}b0WcC8yXBBfdG*PJ|32fhY<#@w8UPKxOR zH#Zbms4XK7tsm16*Rw0pk&)Q0@_w6gk-K3Y5O$D&Cl8vLnf)u33TBm^{dst?hRUda&>=4+C{FOW3MC{fvx$ojn(Rx!Iq`TM?)sXVnrdoSt;LbM zqYJorMgPE;FVY*l&_PmC?k8eD%%Nf+54tDQm8hp&*0DAu8&{++qyi5w2!SdphZK%W zt>f@!IIBX+j2HB**+UnITh=C>JIE7og;E$ ze0+Rr3cU_$WaFB|`FOaog&Ce8@IyXaU7&mugh<)eYnc#%hwPLOc&MozF7^OYv9O1T0|#!60WX%&gm1aR1v?#{t z6X6ZQE?bT*&(Ci~Ldmlt&~M%}Wd!?00mJ0|aKMg{`ETD0Cw%OM62Mxa5g6txx44$w zvMH^A24Tjpda4>Xj?_f8fAYw&r!(eP{Atw4&%}Li;$YLagLXAqaw)G*M+h z#6!72lL#b1FMj#*$)`$0D})ambvkk41Y^i`LOXP?(msIMO!e7?)v@R52e@&@Iv9`* zdy7&91aalcm5U7A92{+C{E(1~iz7X1U<9-lpkG>rJxF#!AdZWM3ZgC!=gX#>#a%x6 zRGxKw{9)}77SBT~bB`Z4;%tsQ*C>!+%Ij@?wwrmpRGkMn1zW%m0iG5=TgnAqUin07eM7Wq^ zCNelP^Z5n=S)KpsldQ#UoSDV%Q0-9W(NpjP+8b0lXkQQ)^`uzFb zAN*Kr3LV_8RrFC015_({z%dXL=dm$s1RZek`Uu>Ex12KWMO zAuy8q_+Go_*J+VXq+jw7ZM26dZf^4d+RenZ36YhZmFv6ZCL)%Y6J27sU?=sje=0wwW&QOPbK)THXbP9@41Q3%6eJIF(SFmcvGI z`3Ws&b~wGYTGDX&G4@^y4~a-XUUp`D6jd= z2nW+%#XXax-B(|!MEZH!u*nBKj?gN*7peR4<2B<(4~h23rYqF%E%79PG6^sD90 z_`>y>8(~vBzPvXVH`4l^PI#m6G9rM;pqTDDqgFK+nd}I1vKX`7^SG^-sQL4^E^njk z(`&ZV?@{?=T21-o%nmDY^98PgRFct_)xXy~O>ECV3ZWezrqO`YO=UthzQLy~HyUX+R-(5A)5i z5Q93LKf4{o)PL*-`)B(-qa=-kvNp-H8raz2@g;m0ua<{3J0n2UbmG!}D~b zhbZ_+M`85IPECTaunetEyJDE=Xrdl>)o63ru(jO&Jilj%qI`0 zdp~~T61hD7S4EB3{pVDomEPH(noby|Fa3n!3bwHXXub`A#Nd}GL^bb-2f1G>5qkY-& z@|Dcl2NK~$m1DIJno)mEP)q;PNO_iX+`*T@u|jLiC{y%N+}*P6j9rFJ3T@{5&RS_4 zk9yrMmi^TwvvB@WCHvxh!TV#&Y%@ItHI%MnZ(HObyp`VbIedIC<>!@ccl;oGXlp@9 zh8D$YWVFFSlCP&`)NPSb=!+=pdC%EVP~X%eKF-*6Ey3j)dudo>+xbcVWcuuh&rN4@ z79JX_xttYds9=qpF-jPlTD$2YOImn}p(N%K(A=@-f~_4sm=DGPO% zALhD(Q}t~Pb)rta)-&dXL6+lE+UGGXe!N?OCZp91ZIJt{-#?HH|4F@Um9J^Wco}>z zUlU3<7kJ&)n{3|zpKGy=CS>Pg75K00g!;Da<$U&jn`uHk2evbl= zT^~qH-=>I!AKvg)YHH>5x^v8O-(em)FE}1w)kcy{s?tc(GqPi9X86q!@=XYCwp_8m zBL8|n_>6+QzIWbRd)#*YW(q0ujiAre!ORQmJi*b;(n z>tRB8T6dtLQnGShd+PQ4@X@N1i!OQH_bNS{4tP4w>BDILl`JPn)iIy=TP$iJO_hD* z+8XtUT+{-aWn1mI+~N(KaQI^qXlO6`>uE6Tmx zTwFMq*gCo7v<7!P&>v&`eG`JxFM6_3C;f;`k9gVNcVq)2 zwz03;A7(ky%ANs(>WhPFZs~6nR>K$@a$+6#Ok0(zpC&wLJH?iv_R!kK2H&T0KI=?- zYp2D96A@wDezvq@?89w}U)qy%0hGA)UTYHCp&8?=hTiXW+E%2qe0*C!Jb2{8mpG0! z=sM_)?`r@8fsm5bSzvl)I0{^JaIo5CTHC+2Mvn^q*z?Br%Tz79TfzbCBFe^lTKgoM zpOhFe8Md6eoMey}9X&(%+{l&Gp3nBAuv{#d%8Z6Qq4zyXwn5^%daw-(+v~*sOeSHV zCtI5;|6^mi5>Hk1TO1J5OUxIBCKx!3#md!A9?V-lfYPF$?%O72PA~ff(#OU7GifCG z(AX!5p`jQ?nlvXW914Guce zx6NO1`>#xM7wN<`mf%}G5Z{>8NweV0YXX<(QL>xL4X1nIjpaFaF4a^vcl(VB9K?GU z025Xsw%_!MQVg8hn}f?ui-a5}xfKiNbA$-VlR72FIwgMA?oZFUhne;ma6Yanu^RqM ziErWA!IRssJ(t*a%7jWSbBFXZK8j=s4x;SM^l~S~l@C#CN#)dXuKipN(l@#4CKCI$ytoLb)*saY=RbD23gEa-O^AQ)_QzhK(oQ(H0_Zxh zTQwd4Mtqudx=LDeax8LOtca&X6$-%U?d&o=Ey z?K^3Gi9r$PoN%NH95b?E1ePOw#RFY1dXo_~!>XBb*rTU#L-0I+v<){QRUSRh1Y5#T;S!K3^AF}}n^yWfV1i3yx_ zY<&E$BpnuNFoL(V_-3u7<`Ah=&=MSVvb45_D1kXCQ9VjF-7x>6n3xZo;ZPeco`-yc zFN7$^slqQ`G<|)md#rvetiosD=-56y2f5|_uzgTnSfdH_Lwr++*riJ`^+u2(aSR;C z6k(u%F*06};8;e&8AyDGNrIxOqvHbnKe!t2v4^QM)6;OW<1oRpMnu z2X1oOAkb*IJQ4kIjZl-JtNBSB3aUftP=?f5Nj_vB-O%6P4@pd!&1<1i5O=_M;sjhZ ze=Idn{4gVGG@+;eziNB)aIVuo{#QtLl8hv@j8u{kLPDi7k}!=*g``F*B&n1wNs<~B zDxwlvCelJBTT+8IDcWpFl#rTe{XIUP`JU^X>zu#OTyxDdhL89AUSIcfyN3gmU>ijF zT3j4f?io|jVrXdC($W$$Q*^!L{m`KiT9OfcDI+r+j!Nyc*RtbxAUjArbVyrUn?C@> zW4A8c`We?zW$TB>j~{>evPD?^iIh&e_)PEu2qNm3{ z-7y%IEZ!uyNDlv|C-Bfb~H64m^pZS7X|iK*ky8OokT&4bEO& zUHy5!|LBA}7KqoU|7e+xDEe7#?eBVtAXih>_f~r!IePSZK&#!ndDVOl!9DZ{jxpc` zG^p?2>#48j-@wmXx0*+nVaNi@VJcR8=S?Sorm1OUl{#(94Ea*~$MVki?-3KZU9n?c zE8gsq8n?08Uh92?j0wX9{-mM>#TBS3=z50JpDR|dFCs2}*&ev;M#!)OTMFEm2?Rg% zv{WP{B&f@0;v(eF9QK+1j2Z9`mDupZhzHPVbOtTaRY|z>5l0%YOD4!_BRVy=pnzxq zY&XB_s@zL4hxk}tw7s(@7`y}*a?g3XYJ1VozCKr772Gn6CaI`WhOHnAgqViE>@pbe zK+5brv@|T*_M%Wv&t_cz`{%qOV1fQclf%~q?~Z5GcJ}PgpFd-`7uew$2>}JZ#z!^< z1@U!{&UO^Vq!6(eK6_?(q?()!-uOs0dwF8zC)B_k?OFpED4Cg=*fwch~Jv5Up<1vW>K}|qTI%;rH!=%JY zM#109i}aMG*BUfpfyq04)Hg(t`Y_MtmEK zHe-@AIxh~q&8Fny(id`rL-os-FI{yn=jGKPimCMhYI^ecG1Zh=%FCxuuZI4#l=vtm zob=O}^dYhu@q?28jG2k3_dMoLKR-0fg~+L{UniJ^TpqF5a#0y0aPkMM5jLx-tH1Iy z#qf;23X3k#Av+C50Dzv>fd1}oKUO`crY&2TtNHjeF-)+gVRBu&hV(pv_r1PUiciNc zfboL=s}Uc4uWfkTEygMDN8(A*x7JCFJH`;|b6?}H$mxNHM8g~3ywOQ<;1rSWi2SMc z=~FWUgAX{f9Olml5Sx)TS|eVtp3qMGT&qBQHaXdz(q_Z-X_k_!QHxP19fLU{mi1d{KNz8uWdKy7%m34Lnmfrt$Z3c6ceJutEaA{f`N(DbxW zFOl9;m#<)MLlM2YOVq@milOq+$cnIOlP1DSf)oaD_~y-<7;G6}xvps8=uP?*0ukuE zvUTrXd3y5G7DA+u1XVQoReARc_EZA{(amsVV#`OxmOp&^$z;CcnOYwEb5{e!vad0W zNAX-_Sft0W=UN`?!iz+MF;`LKVg3Lk^;(!dQj%V7-V}MjtY@;-i5asF@EG(hI~!RQ zQ>I+HaKUN(LHdgnFk)<;ulvkEHcYO^8-9UR`1JVc)a z2gA}GBg6qTe*JnPY&02H0aJnZ67I09A=YcHOk}#w8ub7>1e!*Aa7!VET3j}yO^7B| z@+_1XifjMSQPx#?i`AJ;O*=tKeO=b@-cL`KZ`!z#tf6;oL8Kk**#j?;CB=}&6gc$U z!b;Cvc4K&8EMMQgg^-WKamOx$K@%KY=X1${tUOuSUFFTtQ6wuMy`(3ckzA<8S1^;n z3sJFd+g7;v+zpCBa=TvKzprm4EK66wL@4`ShQ+t*9$PS4woCjlx zoHNE|s5>3J{nx7IrF|lrGyYOs_+y`#%{R3<*+VqNI=+N2ThElMuOBviIJ-s&&-3u% zUr*i+Y}n-G_3D8X;HHmH2kpj^f>UGV%8a6-W96Rx-vvVi;1aDJJ1Lwv3pHbwhqT^2 zB;3kz!2$yLq@zly`c0-y+lC3btLW!;+O5g&`u6W;tY7RB-g$+ad$m<%iFf;k*^!|` z8A2cs8ZjL1tlJz6ak@Z=Ieu!TI$#YqOLND@)huYge0t73Bxqu;6I!Q2noZC|L*;J z9Ost_(<6_BYKo8nK7?Thu+M6NJDnAoB89d1%o*gd#PP+%#PAClL+{_cyT0}FIzK;s zMM)T-XXjp6LQEFIWW*;-3 z*G)-Dacp$cPVhHXFL0AzS?-+usJXeBJ&Vl^cCOsBy|Z(Qsw}I(xrK~*NNV=({rd-_ zqzM$PxfcryM@Wh@gsD_9bD4;cf{t`bAQl zsrL3$>BgafQ-AzkerR-{!T-9IL(JmN;@>6g*3H=FuTvVA@lSf6iCbivq>0Nr+jNB~ zw@YS)U48Ake}wGTH=m_`!d}}9Tc7nnPbmh$fcs@Fay)%6+hshhuixC)?ZH`N{J||8 z3ZFk$=PA==N0^n=hWRw#4^o!SnDNP9M_TIOUCNes@~4lKrHI3xD@wW_txJ_kr6yp6 zbi8>Z8C~Z1SVHBg&8!Xi>MLp-P0g;Gl`V=IZ{m1hgu|3aE29(Li|$LRq(*-(_AfDR zT2j2tV`)&IiiqmOkVQGV`6SU4%+^bkY0}JJ8xbuPExW#WlKi#NgDFlm2OPmi(&1|? zGi**^eaWH<+g}5<|JMv!;@0gExzal)Ts}K!BeNmjO)oG#+TplTjCiWoRmuCarT+G; z+J5Z5`H+7_oixnCB2pz{q#m2QJu2=++PrA-Vj2JkqF`dz`|g|@fj8N|>a2m(CXJ?! znB7DE9XKzs@p#PoA2~C|TZEmUG(^EmnNUx5oSU0$l#QQTaDBP_+50Badk0Ud$ys!L zh;*CcT@8)VsUdaa{#{$%Q}$fK{IbEMFMSTX&K5rzaPPPI#WNz$OWm+jRmv-t35tuF zwD-DZgx0R*;uDAFWE`+tX?cuwn+?Xjq;aK6=kSO=)02;43`>5UwE9fsb+PtS3(u4c zjP`l^Ce(2Hz>ONoo`aWh{0?qWm|}K&o?}`5J6kQqV%wzQ(POD}oU(V^vQ4||aim-1 z%cka!B9>8}BmRw2F|CqwFK(Q1LejQeVTf$fSk8)dGD06~)#qZzrU4!9reZobJe;fU zh!J9=*)rKPYjB3~ztJ}H6rSxE(70((ujYX#`8%av8&)NY*em!Q7abj5(KqJ%SSpUb zDHv1|?z{@R##a4SoOnl5Q^F@;{TjPojJxBYs2MF-m6A4#tc*FXnD92q*SbM%o9MpN zj$y53)i(MPJ1kGzh727z?P{OSDtv*?3y*h4tvTkV7i}+Ds(iTW-)X9|2gu3&G2b_7 z_iqu6Z@Lfty7+01^Qk*=OD!Ix4l39aKI>?7qw4*@Xi3GIgVA|%335lP8gC`HNmtym zn5dOs^0cI|WunZt_eoaZjHa6I zO8M=BWmRnUIq9mPaZ622wbByVtJHgIQp)`1CsuYTrbBX;S|=IITNFHW#dB4iLAke$ zUL==}xbES7LG+_hZ%WS9!BN^B3tpcUxu~1>c&M{UnWv-dp|lgSD{SNTDyf|8%>HH; z+^P8Cme?w_Dt+0QnGrt2&ld%2DSA%Lxc*?$^n>2k5ANw+j_ehGeNN=dwqI#TFOS!= zfNv^WBSbxazVRP?Y_ebs4>%ZOH@H+))phm~wMSzXx_NwgY~OU(sOZ6B@7GK8uS9S4 zaF9*9oMSmDqwc31-c*irVwJP05 zMs8Lp3)^osSH)XJbNh~Y zwW~oyaM;zq9DZh^oP^k%4fk8Kw#@H;zpA`?L7=DAz*bYkvg)&kBkuTSNk!f;xcUO! z6^9Ntz1Ql+UIwJjo?9prDSk$yVrRkH`wdm@BoN#eWa;igh;nH$Vjd$k|4rTO4ePEv zOL55L`2DQY(RPFvr7$$Cqp^nc(_-sCFD(X2{(TXMH$Ihm--TyzbVLT3sKo^osNWR< zRL3_RIY2&nnB1r(DQpJ`1IywXb@(y+6-qbPQ`GxwSG69*9PVozZ8MUh*8WIXlII;2o>l1Kw0+5r9c&%vvYFU=tnv^CdNRwB|=ill79 zH-V$4Oo5cAjzWpQ@Jy6Vx?P6d7$xMG$L`HOv7m67g0mVr4EBlp_eB;Tt7rks;P749 zN|)riehju?KS{Xr=+Psq&RMyxZV2=2GDsNME_3n$qubIy%~JKTUfI*-|MyUunlX5- z@e=%z?SZTJaiT^IlUEN0IcE$YG}06=gX~(L6o*9Ar@2X7aB%KCc&(o`@^k7xyK=N@w!Si({F7v5tFoL1Rc(LxF-N5QF%17X45Sv_A?Cb4;)Wo#hwHM2D zl(F+Yw+~xd97JZ^_=CWnigT~j``+<#5~d9ZiI-rDRFWcme0-3neEsfdlya|)3JRN- zQ#OZtP?_j5TAR}<9@TRTd;3G;Y-@m(9zXWZb#>PZ|K$4S^FiTgQP*)R7dKgOXVu6b zDjW$`mC7zfh(igPkHkNisKk~aZ^6Xzx_9%g3nUwGzI*hD7sWqT?g{kBP&sIaCm%4EuUEEW zYgxY@Q)jt!D?1VU;z*Tx>ZGX++7_F=W12ccs>6yXKf6?}_CBnOtZ-i(?*`Qp8{k^&wN?3vmXq{!EJtWu0q zwpU&~WuaCDzqz@`H1F90eN|m$X{t*;b+r-6yr>63ggMv&(U-WJrR?0~Z7}TEy+NBb zjE~^&p@C?))_}%V9D=A^g0A^*oA~Os&{Gv{F1w{rL&NG$qVR;Teb5e>wW&HMeL8|1 ztaL{!z1uB45Kn-hb{QY~uk*?UZq(B;GmoP~@-q1Sk3XX4i?sjr$lfTZQHO{>w)QBq zJnQ{$yv8p*!)DNYs;VY04B%pg8TaUDRUBKW>@!r5f>}M*NKNDG(^N9X9DAkAroTDadoi z-OlDPI5uJKo8joAQGVZC!K~qgiFN*<9dfkGmyOAr`9vptao3le+DGNi3Bj?0F6a!J zAr75CEY3Fn5se2+b#}rerI^wtKNdgTiz{4KHpbeI_WB%AOP0IJsxX$8;gzH)gUX_bduh> zGmUF~fa$S0>Lr3t3jK?$KGoi&#ng^vJqPo7{v5WvW4_4z!qcb!5enJx`PpmFNLRe+ z0)VS8wj!^drJ|0v#zT&2~l zQTrF4Lme-69{X|!nmjZ^z`4w!%cM@fJ{Yl=H{+)Pf3l}N4?GZp+OVCMaZ^-Rv(l=?opu?Y?nqwBK8H+- zN3V)cTMB_f7oTGYBSGX9_{RPaY-kI#69^gy{;&3~X*W7q_I>+wiF%ax2WBcuM*)f2 zyxezZu%tLYk!&yV;VqVmuFtf|tNFB%`{P~2Bt!116cK!gR8U9x?AVhve?F=M!G8gj$`nSw8=by>guG$h&(rZ>HKgPLNRQ z$gy@!5ffdit1@HE2=gPJLmb~Ficc_K@YFL}JFD{zHIw|H4dL2G`yvM*Jofe6UHFG} zsNvRKrK&Zt^1 z^896t(Uzvk|E%tlG;MC&d?I+(TDkADOKc=Y#2ctBi}>3%>7z)vjG0EQ#W$I2kyFmU zsV{q9d^=i7Y*%p7NcRS33-Q40_^7r8OHQ=QPcQvV$7{ioW4kw}iQ2B+ z)-j;(xsI1dYM716w*XZIU%63wIm?3;9w*+M-17H>EZd=bm+buG!J6o$ktPb$+T3?k zwUqmpioI5surf($uQ{^we6*cH(c!b0-7q{QQrp|;uoJi71h zee8?rKOdd@6%SljjUSM`yIj#)<;0uWp-Z(BJi11z4>uIuD{tnjS*hkJuhBZCuCveL zo#E4+Q|?te@gJZQ7D-jvh#oF_id2{HdBX&COjcJ%%BRsVPU-`FZ zUV5L3q&Nknt_m{wy!xuMQoU8J?@~5!63NEvckvhzYmB&oa)}=mk z8mB!Ulzz_doG&wEVejv+-&w)5Hp;cv7Tb)9$of<1*9S+T=vnGg&z?TbZD8F>vQ}F} z;qoh5h*FR4Ebir`rkxO;x^p7Z2iENsJL8H){Eq7Dqa1O_ye8Q4hHC6`(;GBZzCthI zPSOl~SL*Vs{QSB}CRK?Y=pcIiW|JSe$%O6dszl!7G*q|WN-OL1>1IFE97N0ja-hzF zxTb&to;KCh!ppCxNp-07z;|jky*cKp6MK~(oHrE=jF3|Tt;9?%#jvMh{<6*I0l3ON zmtGGZ95|BbH0da~U-oeKS(NagtxbODY$Wf&Ho5UFCj57u@4sNd2uNm*qTEe0Ablgg zB61Lv_Kvu)(|>1WHTDGmqUQ!=YBlI^Wh)W=;nm)VzqgaA%S8}t*UlS%kS}tF4p%&+ zU$~&OGkSMF$&`3kwDPIo+jbCt8kt-Tn;v2rT{e}_;jLT0!giOqyXU&X=t9~h{T6`C6-~MDBg|U{w;++_$sh|+JmX;@>I;+o}}_yx+BgC>;;&$ zD3JH>-_Hh2T2QS|l#R!%Sp$K`^Xjq0^Z#goU9~ zI8=;{KlFd2AU#uyS~0eqw2vM2+%;z>B%Vv$6)##IspjGhN*d1anKh8^_oUb*)#V9p zIRAvBsaQC7SX8Bm+&P5ch6oc~72IGO3f$1p=_rR-PET=|9@57E#Vc0Spc!8w6BY5S zQ$;#TBGLmF1+sD-W!ORrm3wW}pFHuJHCqW;hVwU7=STpn!g0aJ*Ow!hU4}u51JMhQ zDT@3BrIV}#CF+dyea|}aBB8%=)up_+v=|8u-%ZIJvc|o`dG}7iie|Z7umy^4H)iv< znuWlCCMEx!^AwT+YHrr0)JH#F@ zrC>vthFi9LA>e!~JPU{PZ{NNZ9fNrVL&2-Ti@*vN^}jv>H+-hn%+OHqMk6#dgDjceDQK{YH|M2tpTTU%^-GxQN67mDI%or3(q4<9bL5dU}r?ilq0Ash&sybOlL z-Qq;r*|}D7S!5;{GVUm7Pl)C_UQbajEn9|M3HGj#i!``9qNS&gv|)^$@ZqUM@=f~r zVG@k=*RCSGl`IRe!2AK~4pQGuk4Sj)u&u zT-P0C$5K;o1h)U>l&wBu1ab=)+}Az9Xs%orE&3DhXHpVco-qXZ+xPO zRqiltnRFNFL(FZ{rja7V4DAduM~t`l9I!TCC5`x^(4RkU#SK@`un7L~ZR(Ng8}uf` z^vqELa*ES>Ru=MK+L^U5=bxkWn4jos)X*ne3O(DpD>d5ddBMVhhYEMi>PwPZi2DVCmEDQq#=v8WfByQNrT_)wtc5EKbs-F#O6jSudiRe08&)6Frqp;IwH=?dM^^zQvdDS;wsuU zV*&Y4-U)8hLKDKEVll+zRa3(~N$tJ;{8C&{7wUZ(VCd0MfuIhp_vMjcNGXAcP};b0 zN@At;OhHs;@pF z%3#o0*FfvT#d=OnX>iN?ORzuf3Kj5R@dKs|@i@0isCHY2N{hXaUIk>+QgmgSE?-V~ z4|;)ZKfY}S6G^NTY(-p}v6yJ8GYAWOjmZV|UAvZD0XYC`IBW?)J1p^CFQ>^zJ8v$f zRAk?%i4FHu2OHiYF& zG+e~;u4gW@h`>|)_D|k!Lm|l8X06%P*$Lo)pn^n9-kG_=O)!&F9OC2S*|1L6b};)0 zfKO!npvT~@Bq(mXQAAk^wpS85X(}S#-CSMsw|x!5$9=C&koeTro1^|O_c5>ywIh{9 z@$u~0@L|JHi>6hQl?c`!IKDgWgzMstjBH=^;1ej1N3%ckwy2jDiiR=V2iK zH*fN{d;(s?9*sl+@f${P0aa3cU>9@GOUJJ$O8R^7e3&eu1%a9Msr z4&G8cn~X0%KfkuSBM9kp%7#)ra`^DK?LV>3v!`>b5MtZi<;U%{0Rve93h7@KJvN7I z7b1M%CU!81rE1k}|dLAd#B z|7rdZ|HkyTvBFX@tI2V6*Mf(v@N zb7FLwved^}5lStE^k z($*i#8tD|mNc~Lp<%)N=INFn}f;ruK(Z2uitUSc^%Y+n-aJs|vWT2zy0TP|an!OST28rrCfn7GWtN%0lVAKZtJa5mdmcXg zfF6qVgcSg(ZvKmPNpq}n)f_wXN7Tt29;WYIHyCa$U3SXT-1&;Z4>NBmj3!NyBCujC zvR(>FA%ZM72<1Bg$;jI|;-(e<`f+a533PxE%=g6hsFa&%m?JnAq}a(8M4i)>1UK@h@(TbpP8U#DKA9Eo1{-2o*5@^qj*54!{z>7zx_7m>SN5aoFs4QcW!WO${*}< z+;72*ufv)r_mMN1>#FwiPZ$}%+|q-SY08dG5$e&;607RW>c#{++&>q+LDXOE^)@m4A?a4Df4?RoB3C_krnSqay$LG$ zyILMe&X-Ib^?b15$-7lDO`F5cmF(E@-(=SWNKv-YKZ@=bz&tcGR6Bv|)Vc=^?E3y! zro;c#3lRzfTbIwvOq(I{Qs2(WabMQyWDSecYv<&J&E2CRrt`x2mcHT3O?EwoW#eoRf3;saaccI|yDdFhmm0c^~+^-)& z@F@s~*)v$}+KbemMUY6(2fy5ZGtwquc#1ESeO;Z7O01m33*bsEVvQCjFqW9Vw!C75CruCibO8y zIZ(#s%PLhXTwh398uiI>t3_m^>wU6y!;GRYC9&J-C7#HK{O8(_*bMh+GYR#Nndx54 zeMTG^u3lZNqs$ow>Tq8lWJ-opN%J3$ov>cnE(Q~>!deq`LaW8h#PxxygD_lfM^cGZ zi`%(ql*GR>OD9tH*pFK$o~XraKlJMOLlFmVq)<6%F^k{=Nxqi1`u8^-5>JO*SJhH%}tMM5RwKC87baCAulPu!zR6< z>oW}A`CsIB6ZhWvud6N+1>8{<8WahzRMp;FEmHkVWxL2^;1o&T0|VpIrJ*64&eTe8 z607nyT-qQU^GWN>#thFsKd{Gy64P{u_kaN03c0S$J^9PdN3VhD145OEED>HgefaQU zDoI%hBKNAj$*1On0qJ@HU8X=@`Wl0>2}ymf_#lzjJ5O;6f(n30dRQqY$^I0eU9Arl z@u>Y_OHn;=1d6leQq%{leb=uiP)syji<{(I{CAy1yg-1EPk^-!+8c=Zb#kxW9BzQb*4d6*Q2XJ4?gVn`qq$MMi*Q%jvQWvno8BP$% z?~VV{4p5`hr;3uKcTq4W+Xoa_2uN0kB;%xN`sOuM9*Mo>o*Oq_s%&M@qfB5{c$=eD zQHd3@Ej{(&a(Y>!qkBI8NAsAy0X%;(_~Q7 zCw$Slu4KK#j%H<%%ZmETU7rYGtMneBnF0YBD{6Zns1yK2Wh=L^*rYcG6aapVlx+zp zKxcAtxm8qV&7X)lOyqz`nfBuka^(ZO?5R`SKfFX2`x(JKIag)Q`b4dW-YW07^|$j+ z;9*cOwDiEdpom&+1PrVZUuKR=*7S>7oP_NJr5N|u?nY`9qu}mXk%UQD7mpu+#>x}h z+A;aq7}%qULTl%lu*hX({V*P3r$OxdmvCtp| zmrQOdQRxD_JapEayyBAsA6J$xD1P{RQNwwc@!UO(jLFL&=OR6Wo}U$`b_KTVS|9j> z#phIfS~$)o+~M*^5YDnDi1l?>mYjGHwk-1~1mDfJfqBjPc~fsq-{N+2l|aJrWZ5Qk zJ|rS7KF4*RUIy05zw}C>Bs1Ga!tRl32{o5=M*x_@W%02M2(9|wH_m`3L6f<12>L&3UQpHK$4p@sBNr?mIR2`Y0db1lBbhZuOvUtM8w5PI z7t*U{j?#C@(uzqW4qhsw9yr1wGxCM?dv#^wzu)4uX{=dMK0K-J&8IEbUdUhaY}8MG zEgQl;-v6`)m^vl>Y&85ZBu#0rO?ue66aSHi`yn8YO4Xv{HWx-xA8QoRl(Pp?(!IPf5%B4UxP`WTf~ zG@}BIgHR#kxX8f#FV6{%;>$`GyJXrqr^^m3lo@8Zx|c}mv|p5h#z$3Tg?VhsU$gJ) z{Vd76Fx3bBep#WHKJQ>K8FV(@h%EKiJ38>#@Jr?|FYU0ld9JQ46|CXz(rtX;Y4^kE zA*l)#Wg!Z^)&ISE$$zi@y*wi$ZO`QG6}~+pKTO0gSPxj?oUV{qcA`n{#jeJ@oZCtR zCm2nQ*yLF_M|J2sd6T^o)1AdzbR#y;%=dq;y>8gbkI|lovww_q5SzW*C$(QyCFw_P zoPNd5vK4pdI!qJ5SO0sv6(Kq%)^efwIU{^O*>8rkrfx8p zQIsfr7pCv_VUJ4e+hc4F4YT&R+P?bOi7h`I*OooIzhmbPshXnbV%gJP_5sI~F4-@t zGWyyhrAcC`_0ddhvv_75OK|wR&bxBY1V8y!Tr?o?%!V@edV$(Ou@wF@_$S> zWx1hsuWI6S&*GLD>3fj67p2unGKmBIC&XzRY{ngn&(K)%x z*?YQPaJjlnh>s*XQ+oryUeEn^0f6=VN2(!Y{RB1n8mQlLbh;peO#{)V<(~e9Qkox`6-h ghmZUs-akE2HegoZLI2x=XpXtF9A^G);~w#U0GS8S2LJ#7 literal 0 HcmV?d00001 diff --git a/apps/docs/public/static/search/github-repository-setup.png b/apps/docs/public/static/search/github-repository-setup.png new file mode 100644 index 0000000000000000000000000000000000000000..a16764058d5d4b13febd9ec17cf4c9c93fc4bd99 GIT binary patch literal 40073 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 literal 0 HcmV?d00001 diff --git a/apps/docs/public/static/search/github-skipped-documents.png b/apps/docs/public/static/search/github-skipped-documents.png new file mode 100644 index 0000000000000000000000000000000000000000..e237cbec26fb3a7bcb0d43eeb48976c86c03dc72 GIT binary patch literal 70381 zcmdSBc|6u@)ID4hLS~XFQxZZ*LZ*ZyAtYo<2qBq?kSSA=kSR$enUZ-5At6aZrjRKi z$vnO5?mW-u_rCx9{(pVW>6~u&@cmxbzSdrQ?X~YPElt(E*p+0U+h@t$KjBc3r5u)F8>&I^=cB=&)$#H6Gaq@uF|`te=;oaW8J z44-fBefI8fMP+~Y_RYU1*V$4mXD+`MTl6(?`q9zqm^!ilTwpd%2clzy*e?2m`65I+OCr!2?AG3PR|#E8{A10k!zUa>^`s;D%= zHCAujGaH~GBi$g0R@f((wyej`fH&l&t95-WHcg}vT6I^MJEmof$bZLlJ?Cj|+O+m~ zHC}-@^(BmD07iV-!9b-+mUD%&-jHIGOB0E2X|On|bS!Nb8x`qby;Y&7Y^q_76|duETr^$So%&%lUOM78 z%G|WkO{30b+%ci6*EKb%lG+-lc)kWH>`SH0q65FNAK8qL+@DX{EWjP3mZkf;_m|OT%zH~Qo4pUHW?x4+gm^GB+-(}g6%i*t z)j8ZBT0i{#npmPnMhEBl4-*UX0&H{->BxvT^vo{rn8z*EUp}3wbC`jguS<=vqeH}M z`CFtFN+$NBjgco#oFESxxvA!wlM{eT!(S5<6E*l|x2d9=t`rz}Rhe+daN1^e*ocXX zulmRdWO~S^YVgIX@d_~s9y&z2Q((L&ftdRhg;j87g2BE&VPc7tL3X9V)2#&yk?!{@UMf40Cj{l+C}Eza5?b@Zm%8^*$yt;-@;-R8)d> zvVJVA3NcYMD=K%;q=>OZmX(!7*o^3q5fiE8Gxr8$BfiRvbG7)DxwTbPNMikXYq<#l zp-l*gJ0@D!9%w$FE1TfR)LA{jP?ph8uEHIs9ugdk&nS*!WxgubJ6s>qo+7i4l4Q); z6CbF+sm0HL<;&7_mP&4I)nM>HcI+6tyl>^RXC&oemqSG5pK)_!oyyXE**V2PzB{Sy zLeCHKz9dVM@Ze7@G({@{#E2RFKy}j|JOTDkJQha#% zsYFSLDI+1JeL|3d9Kky}|HkmTuWxnBSY2nnu8~pSpFe*R#jT4ShXy-43r(8N6Smn@ zuvV$o87nlKWl>;|uJye!EjBjr@|w-gw`0yYRa*S-`23#U)2ioJywYbylQPt5u`MJ zgH1BL;j?X#)=oak`lsbIUW&_nIr>R)aR+169wjApPD%W+J8r0&qi=0(eY4EC$S?)J zifw-V`n8jZ+S_#F%f;kBNKPCJtaVzSDG6_|Ainp*`9yTn1%8J7+(}A0g{r;v<42X~ z>{qXZ3Jj9fc&)6g$}1}7FY#=~$=zQ}gm!)%=agP2zM#i!6ac=E~(g!LTv|-A_PCXU{ zkBwQ@rwoLcUTc#F5tW!5d})kiHCNlcd$(}>*&O}RkC8HOl+r~LH7;GcL=sp#y~xDC zAewz9Q>VZC1e>P0x%uhSyF2f7+Dnek=n8bnZ`8~G7#ka~VYh#TMHgbi(ws>7*x0C& zqDeZco86_8^`K?Up(lT*;ls4Fxp|L#x%)0a3aqkfcIOn%?+@p)F5A=UngVenhEt1P zi5g5)A!pB?-QTOkC7aY%y}8sk;Nb4*c_QjVDS33$DfySJt*!KjG&Rnhn{tsZGQ3{W zH!l#~)cNVt_vLkSCFin!Gf@Uc#&hS-FE1^=(wQVwQz_Nx33b#--b-|l$xDl>EJS8ri` zeSK5(f$iq=zJI>i<+Zi8%C3A51e}FXAE@@2AHkDxa8uj|v-GoR={jwTs89a=w6d~- z2jc`Uj*pMeRxW4;DfxdVr+32n3(dlX#i<{Yk}7@k=1pm-)bZoT#l;J6PH3v7E2Vp^ zj>U^yF8}iNYw?>mE>oW~Z%%x?T;|;1P|`By{3glv+O-q6*8bMj5owxVy?WKxcWXH0 zz}G34Lz>A6DJiF|7g%!wGd_I$D179|Nw;4;*?HCFZ{A3pIN|y8OV*!1&i)Eceb-B} zF7}o<4vmbAusT*2yngLG@-8eYifT%r!^XwIVK(egD&Zn<$o+l?9B zNOSX}qwR9*zdABh zCnhEi9XfP^?IXaqv9Zz1%L|)^*A9A&Ms}Ncw~aZwy1GV2MVVB3+_111L9Woa6Y-C| zcm?2?yUh)5W*pd@ESpHRyLPQ?;iF7@n!LQ6oXLd?AVgnm7k~f$jTl+3+1~V?t2&;j zkt*ZvU~Bs`MwP?7uop=xUih-1iOE(iwXdkC=%=nO*WtP#!Hc<-D3kysB!uDJLT{s#dReyC(ojP^%rns!VjSbj?O_5=`7Q!Hsb1zPqK-~69^mZxY z2j>Rwky}($R8g_su+Q|)6)tJlUp)nc(5mus_3$VyEL@mxI*^u{nwp%f%%A)C@%MLO z^!SJ{ByhEBQ&tT@!or$-vGNguBB}gJ zZBLJZot+(b3?X9imVag({VGaJne{Gk$4ug6q$VW^^7Cs6#9;&DMNBzZS!KMJer+xf z4Rv?B|9BsheKC(i-j~g@5&3C#brq11)L^kYu(qS4BP=W|Jv|*9;iUHxLi*QGjj3=v zH6`WWrKP%K6$s+>^&q56Joa`O$p2!Y#A&T3Y+QNR_6>##~&>_dselSJ&$U z<&44Dzeh1KT(QlAUeh@-F)@HI!t-?w7Z~{bozzZP4y2YVz1s5dd77^m&h*OsC}6k! z%ZMUwgl0Y}m=e%4&Qp-t?<& z(WD$_Y_rL&#V(3{!aXx?z;`S}$)J}Vwhf8JX24~6z+D6x^Ua%3BJ) zEB$F4-;j`ybe6p8wV&_a5!ZPfH+=r=S#ZkX z?@SVZai6J)i71uqB`b=GTmcO}K9w(D5_r}Ax5LB3$O72Nu^&GGc8HQU#l=q^+uGW` zEGygJU`gcv{YP^wZe>M^oSdAAiRtqcYKoXtdQ;b`2M{VLKz^Qekw$ZhL0|c zibK|8xNdiNgR@`_AAz;sXd$ME6P`@ey>Owkug@Fdq;dZI9O5{mqu;grGScF?bLUu( zJ6HyP1w!m5+Q|{!JvbACpXs@$c#ussOt%IEwszapEz-?uotgT&qSWQeEIUlJd@Lr9na1s0l&Sx zy&WrNB*96-8#Gg-7k?GDb5nSZwr>RMRS0|_Zk`i|t8~!-; z>w}e^`0V#FJcc=LL)EvQYo>TjriYM_kl;{!`}Pe7`FyJE>Uff)0K;7;smjVqpOxR{ z4@yw_3dhUf=j6%A&+FLe@O>grbd7oGxDJ+ARvH@_ff;uV4BT4$*<~pfcJ}AI2h+ZN zX&xhc7)~G_zI^#&T;;Vm=skC}z+nIW{ebz4xyFDn5wD_fW$pq4dkGdKttgI@wxZ04 z4q=nZ(rcZ3Kd#TSWp{o1^y!}Fa{!C1y!-)5633d&B?BSJlP9b246FMRY3((0)$7Pq z`HMQ~TFG*^)<9Aw7jGYyE;UwT0PVuX78D%iJ*BPcxAu3cC0-=s-T^(A5Gr;HBIB-}`FWti zJvu6?(c&mlK=<@&=HeSJk*(Dv_6QuOrnNHFh<%s-f1zdnN$;O8g*vJ$o6@slTSbzgShyLWHZ z+Rtpf`n}1uqHh_h(Etm5fjIJ@I@X-duCBP0l#?F17$UBJQGz zqT2cFuGa>yqN1X(1K++G78%aCRe+$3PfXyb3?j3GhAm9AA3t^skn)af`B$&Fq@=!q z0Y6=si}dAFr|QATAhtLl&|Ash-JXa&bRJAfk(aLP>(`HMZHs^Zat?m&>OwFTfjEkb z2Xa?-bZFNMiNvYn6d|5CJb%O_>uK=Ohc}cXMts*g)5gcfZ~%wMiHWcv=Vj-WQK1su zL!m4jkA!{8!s1D8u4cMc<<{CXNSTCN@M{C1M2)373K2xsH%qYrl#1$^_ZuwsQd6f% z-Kd*!v$nKkrX+#jp`Hm!GBUq%WIgI%rIf=Ul&Ry1ki$`#{Z^D+UjD7Vz^G;t?=$pQ%p_ z5(4Np2necen_UGrMp4d$OynB~>58EG^qwtyUQ$w0Q)8Ol^&~g@`E%0CyeKULuJ_b7 z4Ljj@m6w)=y6u>h9Q^X-O@98TzCOYDmh9|ouAAV0cvHkuWMm}S&)=&Xpee{XT#l|8 zuE-O-vCUwCnyIp3gVPC&5C8(3#br4ND+WY2 zgc7*W6?a}qCehJt+%(F9vNvyZ_4E$Xh5h{b6MUDCkI%~5`l7x**lORmZ=yvced0(> z_#3S`D*So|g^@b5fs=>_ zjT7-hbE?uFK0ZD;GWim}uKR#E;C)dBs%O6L`&VEAZ{HXm89y=dEA`e=Z(JN>l}VmX z*0X2NQhnF1Svek#Zj$lcSlQT+6@Q}2Nh7=ZBNj=5FLsZgm2;VFil)esBaDoUx0d96 z3F6`k9hv@>E2BLq!FBfxgbpf40`=RI_=XmPE13NFF_! zuAhyw|LM@4$arER|MrQbw6vjuE2Pg@3W07(Rj5RMyr&IPiqYW1nVGFxHu?Lmj`%I$ zXAlGs75ceiC~~8~fEU%(zBg{%K!pflF!I$MC3Ar|uOBh0PTRg?aTl@1rN+YIgQdmA zOFz4^r(6tq>C!#@jfLZzE!`?o+N?6NiOQv`HvY7bi4_>U0wfF#wObS*sT5UK+7}tp zN(89OA><~n28kM3TOay%>T%lFjA*&?D@{}}YP!0*ce3wji!-ZB$;n~0Zm2c>`0*oL zq&n0td~tnk4WEvL7^F1Z9D4|iG+;^^3%Kh#=Xvs=C!pDNK@>^H9mf||d!FujuKbPa zped`Qy$wVn6O+Lz?-E^s>({O&CMWm5xkjJXE|2%`=}Bm|3@8Kw7ZenPh3$FXc{sjh z#%&!H&Ms=%w^oI;9u+gieH^#`c8rZ(O==5vnE+S{8<&1}F8e)&OY50gYKDl_p^an1 zFpLiw{-mbLA>(FmWkn*N5!*03K!B*fUR6-?I=i>FwovD()6x;7C%sw$Vd1T*^M2)i z+rB|cwPVhReN*q5B82&^^`)zpme3m_qoc9y2V>7j>{eKs=o{K9*2#L6m8B>~H3qG? zeZ~!>U+(XxQ>6zpj!mK_cCTJY`Sr`bu-9kg-5%nqr#dK`uiM)vr=`ie{Yub$-qPBt zyg@Rs7m}!ct7cYhszaS9Io*w2kr$L((e}Df-*5i^A63{HFe%DbPJa`a#bB_Mp zK*jQ(u@*2Y^8y2=@T)jRSFdKid0)4+45;&xwJHJrW?tL zi6?H3Q}XqIpQ4C06o?!2U7yKM9vmD*f(Q;KkxSeDd{zXwLa2c7_PsTJ+ts($iya2; zUcP5h-~l{YSXgkYVBh8Uy09?DBsnRGm4gE`2T@*7fO6Bq-u~6&$2-m&7#p*(vHft8 z5~=)Mm*iM|Nn6|BX9OYOG}~Wxe9(gjRAHt9rEoOc38F>C?%XC0Z^|S=?Yg3CozDn$ zjXsbXnFshCcJ@AqW}5x z8p3!^-NXWL#|`w7PKZ!~2=+f4?od5YH04v3$z0jZiP?%dxfl19xv|vY;oBhF=7VXSXcw;o0-& z+Ywlfj(KXlUO%&*n|RM0`}I-=Y;JzEiBOVDOMmO{2OfecxX+KAemCRr<|8=&H^)Ku zp<76f;JT{UATn%hZLW9O9`kwuxlKq|I4U9n)IRydO_HnDz(24Ei-Okn_94%S6talJ zaq4mELB&)(+61t(8%!aa7yG;*U~NZY{JOzmWC7 zBu_OuVlS&?KjOtONAl32=P1sB4nx~p%SaA@kw*Q*l#~G|#{zFbQ?aO^tf*M*Y;9=* zK;)8>lZ_1xrM>=6uB=FT#eT4SUFE%OQsu>X?trCOFN8!?S9qgW`hte}A1uW-He1`; z(ogl^1je41+r#>>WemCRyxb};l)S&1nL6ws^;k@e<35nw-_f6kiZdd8Wyf1&uoKeK zcL(zcHN|{f^6B?hg}^z2>VZ1J&BX;05sp<|osvcv0>?>;t*`jH_n+oNfafI5=Yh4H zY3=Q;tpFGR9~DU;vmy^&7E z?~gmDKypz!A+ls0`j{t1me;}LsM)1~HK@cT9S0*GcSL?wj9u?I*39i5j_kAnDnx76F)n>L7r znc2p{p^@=a$eNABx6aPkv^1nu>>w3BvU{3#fHu6rt7>awbpeVY2OC?Z&noV}5OS<~ zCfVVxuC5e$KRFaO`}R5Z70;khNs_#NS0$ro@=$EEvG7Hl2t`GIi#RQQmx&a2NLGbd z$l|^kJv}|hVEm_Q=U1En*O%G|Y)^cjEi zrzi`lxR_WXNzyqVSf~4vEq+A5tfKuMQN1l1b(uQmeex>HFS=0kLJ#r(Bl5s3BY-ts zO30?L7j!wzZ_D$+(b^vlucAn@8usvGc8?f5@$>`nqvUsBfxV< zhhi}8o?7HH5J}RCoucGUP&7-w*(~1`+@ttT?Lb6?B?%w5X@osH`Omr1QK~OzYm>Do zp;O|HAMWny-$AR~P_B}p_$*<6I$E8PDm*`u4(HT&JPFOLYIUiNraSMD9KQ~SGdhQN==o!Ij({BLqB?w!;jv6{?^wA zl?wDa3HqbptM2KJxAg4%PLKGLPMwjHptbhD(5PW|hc_`iLUG_8SEf#xc|A$GmIu`6 zTE#@416zhPkLpZYfLced(iuj$y>>2R^K)L=^}e~;Kg4`ZYQ$%S@n0Kvk+{W7RDt1B zjsjoob+j@zW6{ft*g25SEPk8x@r;c6Gv?EQ+FWb9?G$T0Q0dqJ+54rO1JY^0rk&=D)(1|=WV`D*n`zAlbH{>T2B`EujuFif+KV@SP zWyU;We*99%8I~tI57YN6{h+30fy^2clioSyP_yagvR(I4*@j$v2kTCbbgfW_IU3~^ z{x5qomLDgmXF4OAP|S?Z{{r#4%TUSkzg_@{_dY&sdUgoK9FV2kPKZz^8FZC8(GMc< z>Z#7>9pmcRT_ZxD%y+h1<`G?23RTO|7jF~!?pFFfG?7r2wa#ktS>dq1u@6(e9~T#N zEV1g|N}+akSE$D@3wJ`u-se)LuYxZGH`JBQA9u2=X8$J(P45l|2QHwGAU&?Bwxf=< zwTda_2-m65CZ{svQnJ}cp^jIrtVU@2zud@(Yqli2jEBP;h|0_d8J1mz0RG*LzMKYh zVZ=M>esnv@lHGZ0_~|qe1yYPq=t;ZK#Cby7Wyh}fA3i9N(^>mlX&*NX$zrSf%fXjM zw!f~Hr>4<{J2mVr9Tlrc6W4Ig(Whk3_hi$P5hLKW6?iRAbH|wEg=|@Osj{Yl@zDp)hEk5jbu+w%QSAJSMYV- zXNVSOui7^H)W)`pykj2+yYhDXgyz2wn?Fs#WVIusN=oIC8TsB{qzmVPE#;BoJpLU; zY&lgOzu9LS3|KTMcIj*xeY(kZuKlPu=|F4u6#2hbEBd6<3Wd^+7l7(pUcG8QNiM#) zo0d=crD!=bkCqf$lzL{}9Ywx?p#3)!PWr@ZA7UO0U*vKNP0VSUBDBL`jli|*vWGX+ z?NEfA`&bL@N+Bi`w#mtCt-MwwWESLHOtq8JFPN1s0k@E~_P^LD`99+z%b`MJA;FbN zMo^^#RvwWtDU)7ETln+mms^ENUbl^e#_V}c8gw@+2B4varN2+O8KGE*LdLd;n<+?? zITh9lbVniw_t9sp`uL&|4o)<@?|)X=l(ZHdNwl&gnJH1^rK<~xG_!MxmoBW=UXzy&1$cph20sJTK-^+@1Cq3QrhrLM zuSWt(6j4#m_!ce$6b=703QMs|dU}iU`qC;RI{7NUsoU~GNgwx^ylKo zkD4pJeiUL6ME;zK8YF>=y6#v5*K(r&qR})NVqHLOZ7sTbzlb5?3&j2BCCToxE$PeD z$pPj?4LDwP=;%1{dm9M4R@TCQfStdqCELmZ1e_x-S zC^NQ8z3A@UyM{(a+KNNauplp7x)hdh$=%%@az=A=?Oj)7PKvDd6FVURd>k?MQS_o}BTRXdu;9y*q z0_WtfUzWt#Ah6ZdCY&_yEnkDmtMP)tJ=WsaQia>%i*1og#?;Kr-jI5GNf{b4_#rZN znunc4eV@O2^{T4sa=AJ@Nh%qW7w$G1I+RpE^-^k_SkMz-{NKYUu;*d3r8+Mi(#< z)T*Rykzu9|%mAsW9NArHhr~8p3Nc;D(=jqKGBp*U3zJ}p)Z!Q7=U02)>2V^Yz8T>C z`*4aT>IYPYHA6!k9p(}&=vRHH1bKt>jYB06ck<*(oD?CZv%GW?B8y&Cw{G3ixG4mk zz@ormZ0z59z=DRq8Q_G4DWix~yl+{84 z&8f30WMyTg48wDd9ea(&BPLw_v(Xs zp(p3)=w2}(odW;FH~V6!zy?CWXFl7E0Wln#UH@B;QpSKxEq>f2JC!O(26v2&qoX4Z z#P3MAhW@c(CtZ9^A zH!mw|3M+>;D7qZ@K_hE&@?Ntg1O)3i^uy59MWX|{&f1z+VC`dF0rqFG zc+LETy?M=*vrki=$ZN>Se~lNK8CE+6l3- z^<&QSMwK*|l-?zqUFP>~x~(MP)N~e|yx+S*>T9WCdqY+6I2{RLu5){{?0>T3B`aF) z(1R-?W@<02tvRd>bFt&-Buc$q$tAnTHb`zx^hmFOy(F4QFxjbb zxHG!A8b7Yx6y72c4l_ZOCI8Sq|!L<%GL2M>goshIPLkcFI3+ zFU*l^Iph$}vxYHegs)P%Sz)i!VK$O07kLf-OSW;u6kG>4THoc~6STaWmO%4Bi{BE@ z;q#zQZ6BRaU=fnYvq-(&uwTBu!fdw{dnJzxj&bd8RGS(45BPci7i2WvlWW%Bc}-(! zm~E=DQ)VkHH{BxuA9p9BKu|fyQfz-}*s{e|b0x83BrfFyy=LIdQD-+hVTS)U{RoW^ zAMtKM3l1Q4{y`7AfHV0=pGC0O6L$yaCETX}9FDqU_b)c85cnjI8MU$=Ef9qDMqNma zaY3+IfZ?B24>Ax_t)Wr#M|6fOlq@OefQ2+I-oV$V6gAMtGK9;|6dG*Ztwk{^`ZH{(ky~^^}p2f|FfsfH|$}8mvmtj z@cD+EgQ1^!xI3Um`qDUgXoH1plz2mDXx>gc`Q@T$LfF2WV5z0^J7=!^GtVG9T1(i8 zzMHpN>D6W*-@h#5P-b@YC@aYd=^V-L8n|oNgyX~MFM023ymWSp!kB5qZr6f;?xxIh zDMIYtu-k2yW385{lc@2mZ0EPU-Q_iktEMk>1&R!FkacZ~?yi&rpstGT|FT+mEAd}^ z&Cv)w`=Q23gKNX9bh6-hTeJIMc|8 z;YcxYKiXb*BqwaJ4@s?sfGz?;mIo#|+aeN2g{_Nh$*0X2Ylh|)y-spO_YoSp@Sjy@ z>Sy1$80<`Iy30kHT@LcVo+ec5vK-M{O$3lAJs1$QO)nCo1~O)nS7MmsP|`QCaI0!} zc!v<)Yhkt0B(HPoje(HUtD?hoB8HTqi~f7AUXx&nQ%8^J-u`XMApt^mW^K2o7b3_- z&A6prd$)>#S~F3UGBwp3IgX%Tz<;-jHY;%@b`6s-9T!vT-`Y8k%L7v3Ft zReN8J7q})O(muUNLga6T?rqS9uOHZJ=M*JL;JimNtw>29JJkLT8&T%Jt3L3Rq-&Ao zt-bb_<rJh&G6dwpNnfnnO4)K)tb*?@q>f2Tlsk>ta@?kncdSjI$-HF%z?7E zk~7X!pUggL1THTS=UXg7h=lpJ@xmEyte|wtM~9O2O<%P$Al--_VPO@Ly*fb$0%{+3 zPrD-1L?2?N46YyE*g)z(uyKE9QGhNNqsL8(J7!oTG{}pVy&qWCQYT1rM5x+$q zO2$0jao6CYXz@6co^rC&^|mDpye(fj7rlB~E23J#jOGotG7y+rViTwpv zh{Lb-JN=&({B%J=M1*=(?wxmnb2g|GUjtL5$}WEew9FtR4$Zksr^uz{J$}r|6n<+D zj{ySIUttG#XvD;}fgnlkpY=^xS+&`Sb2+(TI<*v2EK7LDgb+r^ zUDdq-nO(LsZZbRuYMF|Ke-;)%CZ5QE$k~k0P=%n~c%^`uetwPas~Ojk_hd|?M#nw( zT*!AU>EqI{6Zg_cAZ7hzrlj#Q{P2nLj)BcQX}MRMjQR>knO;Xu8#!(b)hUlBkpEqb zG&8@%aDQTa9IjLTeRNsmK}xu;!g~r|`YpaLeY~8MUG+a(Io&vP*x(Zd-zC^e`W==} zQJ-}A&q07TYfg2w90PfozJCz}xU}|*1_sO<_x&TR(Lzwkz^dKLNm9#2ueFNE=+Vz!)Opq+x8T#LwWfr`wO%wK_%BO0ct2M-@U zd5y_}fv5TrCw4z2)8>7rtIy>Go&r)0FOaFJ>6`#sRq0yTY}ieItULH7HQ6Ev;OEf4 z=IBTrQg4u!wYR*hc;_(6Su{gX=s=8yrw=7M?4wm*Rm=2DOj6;H`1$igOUo7Rn@%YT zoS*vp*}@y-(n8sJ^6aMj|L0ze0(aM1a2tpok!r$)FMqjkzQLjZRF&|ToRYzE-%T$& zJ3;3lSS4u4%w)_jT}o`T3Va48^uV_EEsFaTO>1p!_ycZ8r*K4*MxH#$L9KHS@L-a6 zTwZ?2p#;UHYDR|^|D6+(9&z=ghJEDSbxBqt(H9+fi554Gzn)%KXXh2{2-@Ff zFVHP~-l?C>$R#Wx1JAaNhM5ElMib!Aq@tT9$PBu=%#kW;dZNtlT4+aM1Jlpe%hDa$ zfzv`HtI=@ZFh}KjtOnmHb#3AJJ}`d)Hp3j4fZwF2qw(5o`5M5BK9H^ea%D_@J)cZr zFV;mXH@D07sKy?SFcrpD4h74#SNhq?+=}J&O~01cVN?9X)PlZ6*4@jozBLCD)rGj@ zTM|te;RwJ_c{z*uq3{OG0Ks?D@por?t@-0}3K7+ZOgoie*pgt;wnNHwaABA0+ZRrd zc&rMk0)isUdFjgBX1j43#%yf1>&S#CO^gSsA4yv z;XLW!f0vn)GXwjVNcGCdmFSajec?12=4+%Wr`jpEW+y-+~Q=Y#H znwDnzj!Q`VSoA_y4>qa|{KmA}$J4{ZWBMz&Lq%TRT+lufkMTrnT;X?1c=2tE#D=qL z7==u2tXYe-?F=cJft2)FOCc{ z(y$Y8U@ht`^nJ`kC!NbENlt;d|6nq9B0K18`$^{M=SmYt36>**f`pNUvNFEd=DFo2 zivqkA(!|7qe70Hh*dMsMP^U@g2W6PuAmjg&4yr*HhV+4A6b>Qbi$07M160vy<#kQD zXg)7|`t<3G7tmxOiB@L`#0fy_sHl*sR993~lwc|9n?bYEO!UEn2MKLfD;I#W5-hG2 z1OGfr2rST``}glVN!ge5fz+Ag;dnz-ffj2qDSUE(80x)BQnTb?XF+H1t5Ss4@kE4 zJ4D_GPpjFSaTGDF-Y87lw7+p<-gJA>)bCGh+Q#_tn$^WTKbXi-{=p$O=(EshF0R{@Kp+yolTyFDkr|I@uH^Eew=ew9Kzcn8nE&E5!&u`lZ9$130V6oeDySLr9 z_)1e01UC8Y)pi(#;6gwfH#!YYnd~kW@}Rrn;fY^{hsc?@kD~>bIf+&aVg-Me-ke_a zg0y6#k9>=Rg;ht4z%dPkH8C-Po`!o?RaM2~+S=MzuZ}@FlupS7IQeZL;l#JZVaZJ3 zJ$MPj2k^C^fVP7(uN$pYh&9tcf8LA7?n7 zhadqRB0`x!AcmbNDJkESl;E!RF)&C-O2WXV=$4~zhb0kZuJnAqvfa5%u+y&u7XV&7 zZnsaY56t<(Fjs9R8oEOR+XlO4?&S(eOiaLag1CvtgvCAdioL1`4t_)o;cE{1Vb=Isjh`^AUig+XR;vpER5k*BuZ*NS!$U71D7cs86hh3;ucpBQj$P0dd?bPdW1R5kym-OP#`bG=HaO$^ z1D+T)?wGesH8v9Dnc_)p2sz|XejY&~A%c+9G6t-If{a(wyoMe&JL?eM5FmOVWmvC0 zvJyOX6AP#k5w8y)A~9MN8N$(r7JS73T)se3R2wsH@o+^&syw}g+bm$D^;Ch2DNdc8 zij**tfy&TfpuFhy>(3Si@axtLrD;CzaBG=zSz27=D)Wq$Gb;IGM;LkZ889HN1>_=k z%gZC>49Pov-!Z}lz`}fqhsVmdB0FS(b?h1rwjeB}7cQW$gf_6uT;)yd1RM?sm9>p<;w$IU5oIe@Wq}9Z$K#xz{oYN85|liMQ++$BUoeLpF~fJl*8jYK;|3- z?Cxnaxra7q-G}_PZ-qC!#+(j<4OYc@Oh#Z@B`qmw{cq>_*ybP0)yq{SL%uLXWBLnk zVx8(azOQB_sBZ>?GCsHiY7W`S<@M3|6&O?pZ!HhN0)t4#phmhJwkULJ*q5i)V$F;z z{&|-z1Vf-YPCXh3P+$#n;B-Hqse_~6kPWvnC;U8F-_6yIam(HDd#NiBQs2?tZCzxD zFc3?`D4bGUoPPE>WF#}KY`96kN*+4~Z!8Wu(kysRoH`>V2^#-;dW<@eD>U--J=G{I zApo^mWxseaIXyi+IVqad24`PhUi+{UL~8_snw}Kf?9ZP{avf%(rN+6+-2UbdP%6U& z+iQ<{4+Duuh_r-D`cl+*K}*20!jzGGb8^}&3KBG)!2nd)tHE~xNQB1nnKMu`kFrFf zgv!)O-x#tKJCpgjxVRWWUDzAJoe!LW6#J%XW^z*DrWo(R#A$sU9hl&ViptOUz5d_w z!r@h*88kze#x+b|q@t(qBo(1KD;z%!TNwPX4uD&joVI3$w%|!>w-iekvqJv`sRUM~ znzgPxu$8*u{5GpoYmH{2u%g^rn|cm^C=LWjbd1_YlivpE?Cb8_%Y9`xt$FEyK+Tqa zI?u}kP=o%;F0_ghvIPow`0^2S3kZV)vL3&Cti$s)2zui9#U@FE z4}+!!1`vtSlT+jMt?@&P$G4`Yw6ruY51Kwur*ZVg8*(&ApamBeii1V(hNcfzbKyb; zMlZQzI;ZXwq14C1f`TxJLMt&4dZxvX69PXSkrj;8a%t3kvjMech>In07&H9zYjyY; z(B30t7qR3F%s!(o2vP#0MA!lhY$T|ts0@Xe>`y?5zQgy^MLN6dF0&3V-PG)?%H_)L z>8p)qJT8`^%y?6=M2Vy}D9@H+$YhY^FXiaRHe>D=B+OWN*+Uj=gq@uoT`OD&|IGR4 zo%0^XXkU!|sjfzDz}!aS85xuxjnT=WPaaA*NG$iw}<&$zhlT<1iuhH-3MwT^{nQ!TPXKLBmEGiI!UfoOmpZxB0Eq zAAnU4=^o_*j)gW3>|F_@0!8OTp(bRdOn16RIPdKT6XuOKrVxb;`t(Lvr+6 z@AHtIbuOzknZwxIGB{7`+%i%qfe2w33n~p$II__d_&xkj!-ii8&v2{5yQE*-%0V(j zylxd3#>xf9KOywbhmPI}j$PB}H5;4!z8MVd3B(x+J@k-`+K1thyLZWdl>xMoF|ZFH zG_|RSHXJ;aD*|x=G;jByABa8~gneXfj2i`bYCfkw<(l6BUpSEb!NFVlwQvd|b-llg z?PSwHSA(AcSr?9mjEoG>RM40K#T@-3fdR;DnAHWLF*oPxCyO)c zNwyJ7L|~!V&kk=(I*w-OlmHTtRUxeN${8I9^hlM!k4DF*&KJv>N)fc2q}nws*o7Aa z?jY;=r+R%B{>!aFKsV;cOv+ud&~XD-#=sYlyP+%lBD#2=r~bl%O<<}x7Lh8Z@K6$V z8ZqIHDQ=7=68=_RE?qO^ha3VjfQ`VI5W&=q(4e(#%)4Asj1P2@`nBkViJn!|C*56L z>7?)A67kz!9|E<;OKLX8MBvc_#X}|qx!ml^^M%Mng@Ok2hGZvMAXvbvICN|IGQ+*i zABW^G85?Un??fGrsSc3ds_EPcc#)BN33C9_Y*8>!7Tv1c($CjA9P}cr!v;A9?tFeh z!Bh6n@OcFV9dpZAGF{JgAGsO2LJv`6fqJzI;A;q~Q5(QRt*`&YNJ!}_ zc#w@}KIYz_8l|N<7WQHQxqag7^G=NsU^5zQ2tcJRa7!qWvCX;=0N@JcA~1^4c^Cn% zT#8f~b&;;9sQ5RXt>P!tqXR*p*B;-M0KS_t1V#mieX{0rgpXGhI=mPSR8(d7mht`; z4mX)_-SA(pTWB?dke@rZ_ag)CvH!}Ovnc1roYVd59DNs(fCiImi_<;N+<$*p{QIiT zcO69m47a1OQLZA(5DdSB%tR=@YB1M=k`xqT3$@K$Z(&ox2SRGcQ@}id=uH^f{Cog| zFz60qH>lw=N0=?GZ=zF-OCkUVNQ)#Z!R0qsvn>~3iQ42nGDHhL?8kY(wayP8)N!^% znSF+X_W5p%9)QB}v56B0IybIPyz&{w5Czids(^yn2CSz zfRm}Gql1|eCg>H14Qe7p8ZBi>E6x;TqZe4U!!HNo#un-#v%CBEed6UEvCs9^X8m*Pw$ zcuxtOI|M)`@m(zf z7ir0ZY(nqSt7=eiNxzC{%`g_mfb)xyCPnT}giOI>dq!h&G?6L7C0~-oonWJi_^Qp% zpptR)`0@BN#oVm;;*=YD=uhUtZh!Ui<&@4TOO{edk1TPg2!#Jq@JV!+lnz2`p@(2@ zGc)N{0lWNwL&<=HYK+v6JqzhaQ)otG@c=@@pkaEkd|(z0RY^iIg%!J7KXnjao{RFsdIWIiC{?b&-jm7;gyyC}MB zPjZ|GNe8h!a|U0Ob67+~m6(DFUjp;{P0f`rl=L0u&$M{DbNj{7hvAtPOZ}o=DbYv_J3`}%Yi{U%w$Y!;W+6~kW6<+XLv$?L^%*J zuW$+Kd%C(Pmzn`op)%tHZTd)+c_S?KB4}-a0p-|F$)qC;;KYIKFCx|Y?$*E z@kt)^{Y=Eg+P?2MX{18x{r7|oJ2BHr*?UK*WZ3B3t*5|a3Ok~9-?$^jeVf#^u-9JW zl7#s9{&esO1qi)6zJiuigCl`&3%KpUowUCz%sr)XZ{ zYkx}hCvQU)44q`D)BIGFr0X<9%%@CjjxUqB$DGme{)_j1N)iOI`01bvKnMvkdFIH* zA};AbA-g2$ zDM^kd5?_*sK`1`H$)!C&lZ%AJUGd}5;t$`1KyuNJn)hJKSQA~@ePwU`u&ALIC^@B> zdEMnyy#&U`cKGF$y)my{%8c)cGBYp9Ke;I#B~KUj{h|j|6nVhdyoXljLP-n+FL|;2`i;a;i+F=+*;TnFL37?FFX$Nw_?8e;Qk#_xlNUedt>Aev4;sgv zGgqGN++u%gb+4sfM@=TTOFfy>IiC6Dt{eVTVjUtRyE?mHQiXw1g=mPPtKc8R9HCl<;IU8PEpkK%tNO_5OsC12_VJ@5YUSW=ji|mReUI zxwHX?rX>Z8F7>vC7I2A&9@8d?st;xu5v9q3`v_mZ1DReUNSHcbg?DcUb-)d|{`_=K zy9>IXX!9kz8-LMQK70K(^S6{qsU+pDHl;75B=rmyqt?F!m3bp>h(RofQ*X0c`1Xm# zLeo$6E=_uAjlibL7S(0et>X0)(jPN^Qmh5IeibHasT~Pz9HwQa`$4kHGvSfImxr5v zOM?qCckRw@r(JTls&?17_a&>mLg}LG$ASBXNABGbQ&Li*Bj=xpP^RMRxxJ@x=b;FC zGYV0I+jm~G9Hb$-X+1u;Z$V`9>*vDpS6k*8cE1|KHa&;C-n`7#OtD>+^;(kimhrV8 zu*&-99RULWrDXN`Js965*)h!mnG*({zTP0o??063zG^gOY z!>~j*!Vaw@HI=HHy!rd&0L2W^8RAR3ZsOa!Sc3-AuQ)q9YdP&q{PjB-D-IasWq-8t zgjdhaP12HHF8qtIlo5JuOv!1DH*`J4RkC%5TyQLTLXzTYCi zHQk@hKiEUt|6q%lFlkj?1otGhaa7_NngBJqK##i1qvytt9QEEaa(L~IMugI)Ssr&? z=xjy*2A#HU(*O(Q#{1gx5t-pBM0AL@bGHG>rvX#Q%lnK%Rfm!s=9riBqugG znX>54Q{g+?PGR7d##Ge|QMLEJI}SU)pAM)A3g?dF2#CI}eJ3N>`%Z3M%)MY51EBdN z0_&8EhoJP^ch1oX(qUhv-@mFO9j?vaWw1p3Zf_;q_{9Q#W(y-Jc{ZU4;_aZUT~|n6 zj-B4k)HfTlUf1Jv!oWpnjpWRoX(c`jGog`Yha-l!m1I<`<%WamhiQm+x(>$hQSWae z|4`^@)~r){sj@Me_e`CX`LESYU$$1U9?>iIv5J4+8u(~Eyn5!o$#@0To_z#;*`S)0 zUH&^pZpoic*`N)pj)(0r!4~6)WI;+c<@+wja7nv|temQUz|^PrcA>J?%;SGvEGU#u zDVT5XZfOATrhLg;B_(FMVeNkZt5OB`_J@zr`Vl=Tp-9l+Q>39J+0}GIWTBhzeRLj0 zH}ex(>4bV7`mzMOqg^xdHB|gqb`l%8E2aR*b^?Z55$+= zknwDlD*KgW1a*sSpCgQHn6R5DnANs;j#ICtm1=F?@Eo`5r4*H?c3+!*B=qhq?^>B^ z4S}Q3)VvJ*&v>e+e2R$BCgUXIZoK#QKf7P(QQrTtI9X8cA&kzUiOc-|*Qfc)_+J%e zCRP+x<{~3%x&g<80ciIhSwt2m8H%zMnNxY)3$kc(orn@=ri7--a5_hS-6;CMRT<5m zHB(fBSw)-S;F%FA)cm8W9&PWw&h1^@zOC^t|KORmoTh(E`3?~VV^O*5G_fMI3Va#V z|3liFhhy2b?ZYL?975*tQpzku88Q`;XwXIGxsp%`iOez&5fQm0giIBM%p_APQ%Olh zQ3yqmZ$I7d`~ALuzwP&I&vrjO-NofR*E-g*_UQn?3N|H@0^hkmO|8{xboN5E7EWyi zp4c(G16|f_8RC(33suC^5AqJBNuh`vo6OFlkr^4Ag#s~Vf76^Qynp+2ie4=K4IWcQ z?w1E&YxLWt;!XeWw}dyEbX~GzThb1H;AES0X-#BO`+xkg zNo()%gq{tqF~Gf1Ux6Bd$9pE3)J}bp*6e$b(6V~1L{4v&*LQRD*7vVo^3TdPTzmai z!hy&P5>WGYsx9z-c=z@iBADMTZ#N=@(WxPR!i%m3?s*4g>s?e{&mV#j@SO{g_( z{?h-aT=(Y<6w`mXxCpW=PCU{m?>zE$U{43r)L`?%uBualI}?(wPnArYv8LZN!o;#2|P zAD0_&L;Nb9ci`xERVuNrQ|DubWp=<->0lx$lG0)a!UvMuz~ zi@yLHVB6uo(+^4dkrdMQ?Z4;e`P6(LARt0o8srX`4%||0WCw?ae84t>?+po|{!|O8 z*^Q2c)qn3-$9w=zK1Egx(opc|fNoCz)>g#j7OHZ%2zNd>DxJ z6k!(O4!CxDq&AXbqn1MY3OW|=&aE4tUTP@OFuhgaI33yd$&hJBLJ}+z+=$_D<~@B0Sf0r@P6g=X^7SVBP6xY zBT@yQ1#kse44^20Rw}Limp;S)2B65;a1x&l+vW-ENA0v_yK|y;A4L4s-($6 z1gRla;GsF#He@Ue!-hf|^y+Jk@4`3O=~=345HA2Lj}R?ni3l450_7e?WIiBPMLd#< z6}S`<3gG*dilhv=KJ^%DZ3qTJ`r$te7KkskCW<8=FE3cCqwGi}xKme`byAbP1tr2h z5*O>9IUEJp*8zy9f^&e44gGW#Qe}V;_nlq{yRe9 zCKm_+3syZ+1IpiRP|w0fy9{JZPqZlnmra1CSfa3RT%7FZj%mfZUG2WLmnY^6px*S< z)Ip>;{2*R8Jk+z^sz7>uAZd`DYHn^ug+sgucstZqtYZ9q{0@=Eh9?e*0Ll{l&hMTO z*fuiYJRVz=OV)Lkh@mgitdXZe@*x0!m{jx=NgeX1treA&kod>J$%%92g10vra)B4?z;Qhe~aOhg?T+w&M4Y5oVM(rYsE>mhGB@v-24R92{vY ze}7|LDxT?wXbD@bBtIYWVGq7aRE~mz9hxkJgaT`wNQ^;}fSUKr2aum-H8mM2DgE!> zsi~{S)@Mj|pkgDX2UWLZATBPB$eqePf`@f+Ehz9QpzxKI72qQ%vLw=s>a@G^1M}}N zJt8wBF+Se-#EIYYUyoM5vpRCbbM@a9@B>wO=EYC((oj!;RAKW1e}+rH%>xoTMA+r! z9Y}G^99QIt5sxGu^jOSp*>LQF>ECmz(}dCNqB7t8!a{(Bs5=OoIIOQvw=o#7E8OZ4 zD(Dly+(IMg&UYg|9tsjz@OtLp(FH(*^9Zh2ypcV(e!x78P%6drF0X1!c3p&&{{9Ow zqkIqU`FOYVLAwLb$x+y7At|7vql={6$<1xx z0vAL^e6rosV;XCjf_A2NZE-oSuI5#liYO4SL9>7;yxH%S?{6C3U0v>3g_czy&e_io zg!UNzviIUlc8gu$2N;XdowOeTVSrZ{B$VIxEjl`y*w^gr=$e|RJ`6!Zit{Q=OV~la zZ(smvPKYr=tgWxB>l986fV+69Q{TS90t;Mn;_Fv@onDBXg9FE^U?0Kxd_6k=(O3|H z2+@kmnjAYe-*>|LUF~He=>?jZ11ZP-{8W49@S#%E)8CfbHnoAoTpG_pN+Ypte0;DJ zPD*l`r3@6AA(8AaC<-)CDApkQvm5(SD9?@5EcKr}hpmKijjr-BGoSL+b$oU{#`d6= z;siAW`1!BE1mB&Z0xTE7uBpk%3wRDMUmh8L+BOFD19T3YQRwLM@4w1B429o&gd%|Z zg`a$BYO2V*_#e)i606$C+6+KhHnPyc>nCC(qOgi}`~=y$bv^fj4YFcogOjuYMu!1=MA#vk0f75RX9#~Wiqo?0&o>F|1uz#lenIutS_v|T6YmX8y}+!&wrMmiE6v!fX@ zO(E*QQIwxgy94YY=FOmMA+s6Bk*=961A(rkB|4d0Fu)+r z5x0Q=?C>Vx$d3>rBHRbsq;1=`qcpU?k$?2?Au!+eazEs8LBYRuYb6d9=v+|kaMB2< zcs3K7nw>f}+Sx2(j|rQsjw>~18zJXH_DCz|BZNFzKlX%$d)TgQ`ET9oWoQv_kc0C- zYGGl4Rts9C(o#h5Fm7x@2rMF6j^a5dB+TN3G&+1Z@6MeF$|5ueXy`zVK?DQ>O3mjd zb#^_D0qAH}=&|_#UGXu4Kf@1%{t^2GB;uo@B3H0`s6!yna4w-~dgA#x^3~U=>1lL% zVaG)~e&U@W`vcidy)Hzb^LM%qSv7zzkuj$%Cuf4_ki^8FkX0hs#6^Lpc{hjd5{PNv z#hFo@k%&-yVj=yYe+fJ`4!{KY_wV0p;O&{2ac|vv2~n|FwD5=G%p)2>eC_vj0UAHc z+zDDxkMy&9?=)nP&29BA^($aWPnKy6#fmntR3SL%9 zKkMX#htTSxfGvr<)w4JPk=}vNQu+2fP7#O*SLbF%n?S5Vi-Sf=*r)LJZ5te`6+P-l z3y$HBpnJqYiPHq9DO6n$4XE}I3NQ3d*dj&;4??|yV~SAEAZH0qX=I0&HMm1{0oDh- zojIADfB*h3-&%wNBBBLH1-l3#SC2?lAwxe3vIRXgM1Y{OQObCOI30fRn{La{^c1u}JzSNq;??hpiFpz_b2t-UlY z&{~1-ilvKV7NwRU2#04SL`m3(P&|>gZSzMUXx03S7sjX-VfW0T$ipi_q1@%%O&e72 z!FO7%2G0sjDgsc50&j1h0|h91%k*z+w7X!Kai-t|BwAw>LY9c;r&_m+2B7J_rh^6= zT3(IC4;!H9inPP?MO92odmOO(5BC5RR>*k}rHv~N+|lfY%6yr(Zb^ZC7;gU8g&;h8 zc|1EbUyADL_r)V|iP*6NDSJGEg4R}6Pkk5e4NN{HOP+q{iak=-($7>}&=@+f+#t zyE3-O7~cp?o(vhbO=^n0_D`}kF8Z=K=~sauO`c;CooM96ze}Tj>eJqLUW{r!E9ZNs zNZZK3(d{lXrT2?@?-$*yHG5ypp5}cv>m}d1-U{;w8hD}hf<;{7PflCi1V@gP$hK{! ztf#rw-Qi$v$|E1zG*OJ>HBRH-A}(22K8vi(`=u`G$tih$Inlsv*LRO{@yNVf&#>a< zEXA!_UdwTfY~w>k`x(jwyT|8ZowsmYvCLQdrMWG8@OpXD?TworauMiT#0}}v)JIpt zB9v4H&v@0(#Rl|7?a#{EajkC?UCX*F^&)57lZ`bMkO<8N!(19x4*bm7)l{Q!HFgp42eU$?V@dzJ?%`Fqg5C zd-9nct<%WaUOH0Zxy-O$vtGL4kD1%p8IZE> z{~n$FShAg{;BX5U1|Bh8fz?)QHQ` z_g`{b=y&I9)3nj8d?A*3;K4oHL#zHwfi325bmPKeuKxG41>!cq9L;e86Jo2&HdSqS{Y zo!=})*Tljx*NJsIfzOtj7h9Fw5a|5|A5{-MV!=~BPkhS1ApwwAt!Hjq(lvbv8$wq> z`!P4L51k*x($()U%A!l6NTjrsuG>Z7c{xl9VENm|vJX5UIC|W=PQdw!UZ1@stGLX{ z(B>Ki)fO7l{a2lr54Rp%Yr$pKJppb6dlo*aSM?QjYvhqQw(oBQJPeZ%;qfsF$&5Je zjue3?nk_UE@XD<6P(_XV6!y`d3T;D3A^cpKmlRNyU6r&IwKwdHL^AOV2OLN6j{XcN z<*j)8-Kipt{}gl#o+qbfd~3@kaU$rN$i_KUEctA*S5-=9I>IJeNtTr3+f0<}WmmXb zDdOhEoIYB7ZSCdJ@9^Eb9pu6B6#14*Hl;Q+GWE7f$}Iz2MH&XqB}~gvN+;=)gJ3i2 z89!=jQ|Mk^XnTmGq5Dkp@z zo{wcK^%CqwrU764Y#BQl_fa-C>N5s6w?e} zh)-RUcPIHfJTy;m;cVJ;tpYu@>aK{IgGSRmQq&2%wzsd2qB+s+uDgYoyvsO&MpqzY(~ z)#qir`kR{#1hZmBZPwP{4XF92y!Q6{A3I;)7#F!gZ)fAZ@P!UA*++4QSra(n^K$A$ zwIp7@9@2FC3A=8?9tcy!^n?C@+6aqHxZ)TJgT`RhrfOO$AudMzFq$l-myV<$7uw-d zrlAC zRcIe<3ZO4`-PPl)8FkTFHoFGQFgCup||IAdL*Gk|=Zi zt5DwdDMcq=+ZTrBnml9cQkplhh8^J2eucah{Y2_G_SI%l3w~V_cdjJIGM@i-1RENv z_tS0V5|JX`lr&u->VW-FM2TFE$4oDUs54yquXs)bqhm^TZi@Xl) z!t?uG<)7jgHXiMDs=%dEY}>Z-=!+4roPQRkD%*Q#YqAKvnb|R6YUo(>rNgdFFY$V6 zDi^<})fN9yOSwq~^S!2HYq@ULRPvdlmcrj}hB82C4>ec8-o+AVlOpJ7XycFl@T!IZ z0zX<}j5kHHLsBMU+y~ki8$QlvJab(WR05zz_whPePR^VecH7-E2RYV5hWMpeYRSsC zOe(C58|^bc3EZL%y@ardm!-EYS~1P z;C7HO^1x^AOJLyo=G$07l=yNZ#7nEn!KGVD#&1dfXmzYQ1qF| z2*t)&ipj2rQyb7bAU=2_pYo6fb_zn2kHrA(D)7C;JBSitUUSaxMTiaLVkYcJKio6w z&bM8k&4P2PF3M_Z*dS1^INyv+|7V*PE4g>^lHc3nA(}FKBC{pBUd5xBm9W6Bja0vg;1s zE4sSrsd$wK7JEmTpSB<8MoB9iix}{t&$iQ`)UxQ=!|+;&2i85h2!0TYV7UPbc{2It ziu%4Ujnu8@cqT8g7nVRlKs|c~ww#~_AkeW?fJ#-&S09RBDHV=Ju|uE|5LKPD&NLYr?@^4qtT{D?BJ?ZHfw{H@f~ zh`Tbe$$if!7bs`t`VVQcWEgE9rblTi#Sl2%vfSLypc%*$Wgdr9KA(HK^aXbl=^KSu zY=~mT-MX|NjHcSIU80e8%6wqg0GP7svKm}%xm!9f+St0&+ji8c;E(tqU7K_AJt_?z zk>$L5J2p}=*v%njwZVp^L*)yP!307q?NBB6t76_QW({Po#Rbt`+;5lV@XW8k? z4q(O@Muy@0;ZGVF#dduDs4OifC#R;yFK3_L#mTr4SdB2ttCug4a{#j?KtlZ5J99HN z6|Uy+CTOn6>Y5;5s%QM|gHuPl={u7FUcki#I=9hTc-3P8EDt+KB~DJQZmjpackv0nwr3 zKm0?-`@k1Q}{h$Kl zMG@N?++c&S*vpsyBE23iRP@H;k%^?-J9m(92CD7{e1kxq@m%2xp@?t0k9lB;Nl64N zJ}@vow*+=V`dhTu5f7$`kh30g^v8FSU&_ki^#)<_5c92B~SpcrcfsVvw|MP zulppRx5kIBCZ_Vee)S47x{jEcJ%$Sw#|uhH>4Tle46zD95!{2pNJ66UWHw+s8(GA1 zpxEGBLTU~fBuc;QNKM+fk4`8I=rC<#bPsG#_*&@fVdh0LpN*N1Y9`Jq2v+0Tk}@-6 zClR%l1Jn>$9{k@RCxK(*v*A-=X;17XJgA`aVV||Oe*gM4(xlV`dSLQq)hjM3K~?)P zF#*vM)<0NIfPVnSC8VUFX$7PUvIW11(u^-X>YiF~+4NaRUwIyzHI`FJQ2ooV7vTP$ zT=2(x!2yAD2NOhEowSdQjsTkl6Lx&(apa8w!v*+idh}>gYN|3g)}bM8`cUi}K&`)F zL?q1RpjnaDjine#!HghgCZ{t6w`N?SWx$6OUQZMeHyc{8gOnF$LM&HjH}(|WzYlTD=0NxxC;$bKw4zJHx`F9U zU!UVcbKy_;Fr|msmYi|&?l-J}b6Q3wZhQ&nJl3MIICX;+cCVF{TgA}rG+6p1I~r_C zv9utUXYIr>iK!*7qr-@MhVdh)6g3xXgQK%WhRyI;>I0dQzIDXW^OuzzVF`a6N-1F% z$8mt65x|tedF@`#92psb65iAlzGQE_c4QbxP5^wy)5b_UECpZRJ(ymU@|l3jVZ;C+ ziOUt%B4O~6v8ss(OioFG*GoX%PnnAm3Wgg*&^-rYkHCRom=A7bZlJ?Jv7rM-GXYo` z6EP4HHz<)v@=?vikBOQO7eg~z#dj}X%B!oZL&||qf{+-86YvvZUaI&`YBkmpu#<=w z$IK#iP7Auqw_P55rf>1ruU}`M&o)!5oWhdej1q%qALi+o$c((kVTU#hymO$zzFc|E z>Xuuje>joA1DXq)7Te_UW7046K&QNNAy29NPa33jl5$*a#C7 zUt)FwPWeH1oLrWc{56O2UYWu1J2;pxZh%^dlLDSgoLBe-C_Ut0TS^b$|AHdM?MFP4 z(02TX9}4y{0QONdsMXb%p~l7NIAG~$Q_0o0jK)C%;QN9(1sj2Z4W$tvdccDPz|wLuS(a4X^&S;7_FnZ9>Ac{VL^IC$BCps=p^!+3_0ni_k@4kU0!D+Nu zA$X(!3M5ZYwjoXOT_~)u;E8cpii#JZu0cxx{tvt;48&uqk~DmiH{VZh)n#>~>X}1~ z3#ttp@@Jn9YV_X!krs7g>;jvYf% zCYV34i!av6W1$rk(Dm6QY}weuvYB!BN3Uu$6;PX?5&YSJXeE?3oK<2Y+wXUlvwFlKQ2~w*B8y9xFSuTEbhm@@j{sqmwGAEK{{|D;-5IFaB$&BM@j+m05IrkBLl4N9PI4%4X{0-0~ju~-QYQc z0d`n4v&N`oc%i*h5soWkhpGK8=KmrZmO`}XMYJ1(agq%hhJ8`N)8&JuwaUXDv@+UO z94y%G($-I-ElwlFU0r?T%^MzCYJ%x=?h$w(|6s@!4-TspZ-xlozIAKi?~3?SWcI`g zYGB7_f>Q*OhO>$odxcvRR2ArgP?hn>Vb)^Ze*79A&F8#XQ) zea`hog@w?(gw$d1AchLjO0_({Ql>RCaVp~MS(MDGszpfo@adlw9oTi?x{_t?uf=ma zN@agw?k8g9`TeiG`ie5zhV?iEr$*-`WiV-hiOW^Fc$RRLLb!1jc_I~14`4y>*inE& z4S~9_6wdvmqzxUIdcd2$0Lvzd1w2ztqfqj|mbmZM1N0zo-aJ7}_^}s<4<1As0oV)N zMk*?j)P$iUK+AUhI=p7A+1iF%&eo27{D_<9&)gzY%wRl)-OeX(!oK1kfF}pL>IFu( zuCEvHpwTq`@q3CQhKm*Lj*@|DtdzTVw*g>+TIbcPCWBG1-3Y~{@Sz#Th%#&uM0$Rf zD9v}5Tsj<~d@>1k`fa1SmXcW-X)#FeL{LYh00|7T-X<0K@K- z*WVW}UO=E^R*?d&5lHf+BoNFPqx1qQFIuYS+7^#JKX=H~85$Ve%*asnTa=|pn-)F< zTZz0|KIXWX7&O7dW3&4vGO#>+eQzG;z1o0~PQ09;h7TCQhY3N(!v|SHuoMx3v5G9; zE!3kBn>>YxfSs*FHcXmbx3`i?l_qoitkOvkwOvlHCKpoGlqF&D(O9`cCz-#6^c!ar zA7-xLGK0R2s2lqF(0}&8XRVdZ(&iDd`Ok`f1`{`-al+jOmjbAvWD5}$QZ`+ZoxT32 z5)u;F*&#QA2olq!gxqeTa3>|b>hIrpAP?sXuAs+@!Q_shIY&l3On_)XFqv!3QHSzU z)O8*Q?uipy4T7O{nG$kAw~O0~p&@Qe&>S&I&;~(^4!Nhq&Yg)T>IXl>Lp+FEJJCGf zxpV!-4MY)x`1e)KgjN+7@HIQn03Oo2ci%7Ufz%9RLf$^XC&DV-5ct`v`oBO9_`KGH zG7TGJ09^>}Wm2(1i5q0Hd1#I@j4YO!19a5Z%6tepgnky)eAHce#|}DPD)eg5P{;|R z%;OpalgnT>_uSn#M8TNuZ$MfZiVJu!jA^yT%?w6zTsSab1ydcN2gTf*pHF)-ROWE~ z0E3?1^=NIX7D7ajHNbLB2Q#bEa8_X$8%Ft|14Zuu2QC7J<>goIF=X zu96KnfBnh+ZZf|jDuBH31X(yPM=+%5#0d;`4y|Si)xwiROMv?hnhcy8NPL|B4YL+P zq9K#TPlq?Cr3P^mESkb`yeXR>nByf9F$Fi=hO5f3sU;*Bc4eXqfR%Gv-1jeZCIB%$ zO^H55KMK_gE)g8M`iBmkg)H^^cPyB9m_mY^Dd`JV7+xRlE|BNqoD#AtO(;dvQ&Lib z5DNST%EI(-Ooi(o7yzh&w1>GRoD@eepof-PITkW8KuB1h`EJR$yy0;T41|9V;X3K> zug>;O_!&}q0GiECmBp1qO^2pzhaFxNx(pmf(l@MGC;|Vg1(3j6F*6%fyBY<72%?yw z%eWT?ID9rwMuGk_sl$^ny}f|E6;pqRQB$GzsQ=L1A%iU8DbcW^3t50Sl(+~iki^s6 z`3)N!8UbreGfhkLfzT5V;l;*HYFn@^P_3Y9#xLNt!g+v|0WD&P8o|%2_Uu+tLZpEV zJ{#0ZCrZ^XYlzCv{oSeX>{;psn!V_%zQpZV zHjy*fI(%*D)8S|Rls}^ym$9Y#ojSzKDEG*#0d`DF{h@|j+>fps^#~1;My=5bdZr@mP%OBy3G|gYJQ9+EE`elymnqYoO9i&WnlVx^1F<6=Vy0xOd%3NXO{c zosmJ~ff1xHtlX!)8-BhKiZh;AznSCk*74?{)E~AQk#09*&I$C)$!<=dWBwd+X(mQ< zN!+w-%Q-FCQFGcSw&|o_x5^LE7tyT?VV~@aFmpV_D`z76)?LahWp*;qdc!dPv2ER9 z{IU60PAjDYDOP6n{q3!qpBDAItc;5C{l-cxA*Bkq>eQ1@$wX(qx>~`(GFj4KQ~weI zdRsr6i@@$9s@BwLH1lH3lqS+K3`GQM0+j1WcrwQdb-;Qnx?dke6Ec>z&G6W?l~Rq7 zD?W70s9GO65_J0Q%;<~KkTK<=tNFXT`>nSf^v|sS({)oVr&{q~cX)l|qnyn3tcOx} zPtm;6_9hjrFmwm|%d>D6UEk2m^h!{fv7fmxH}>O-v5jHuhrBBKDe*7c0z@ zZHtHYg_%~%3Q=1jxrH$2t8rtZlCPN#(p~$$s5Ex4-9j#!?^PpT^@OecIxeXoXP9=tXm&HK~Y`Y zYr#TIt$*7;p03-g!TzGn-C&A1mem?P0re*YSGZw?B}+7Nmu{ZI7}$F2zvSZU$!8k! z>e+((BG1|b0wZq{)7Ppcks2#bPbo)V%xg7gv=;+jr>MwbD^`?8{_2123J25P4cpfL zZHk`^<8IApGqjOADQ0d{EdSKyd*f>^6782L?h{2v1=L%LL{)G&yiayCs`b(GF&55 zkJSi?l`a<_My{XVVq3oHvJ~q> z2db#Cs#H43&aIbad$?RQLN3}hXk=Ta25k90|3UVPz1;Z5q!v4ho_$c;1`{f6+7ymh zXHNF+u!4p&dV;DXKE}KRI(}8vyJeYjF*9oRmzt$Rwl41A)u{M5_=s%5YHi_kRBeu3 zcu6?jUF*^DEsgBf7KTDe=|xxAHm*|%b&&Mhm|Es~ZDs+GsmWg;?Mf%vBe@GSi0a2+bJ7EYmJ5UWXbtN(nW{iU;+futDU^R16v<7IloIA87V?AGi$vQ3gw%=6`q z=KKe_U0d1jGwyP7FGUWS6Wjiv%#kV4-aN;kFfrFME_*?1tJPc|l1+^M6?_^Zsa2;q9I zlNaA}_1)Q-WAWb*3!ISqX>RGoIVM)dFUB^>wg-77*IjsE&%wlE01oZNX;fiMkqIx; zIvPnq5&qK`k)!G9bK>8#1InQ-<*6i_iw47CzD&Z)9<9NjqYv>9fuogCmsz}@O+fyYxNW5EDt4V z#VTD;xNKAOo7t$byg60w8TA&cv7ry^MT1%mtYxPo#Ws{1`uQaZ9U%9d9TdJ6eNFb{rG+e{Z}J zbuo{kl|z(G<^L59aQ`P92(`|8g_n!zuO0F{N%hwnUh>td+@kpa&*T_w%`Kp}6g3?0pj(6ZAIJve5|uq83IY@Gj~(&0dPq zJi65Jq-OazlZbRO&1-I(vK4+|~{618SLi7q|gSQE)5rxb3E z7Y|H{KI;xk4z=3Tyz}-Qm6tiJPA!K245%0)Q`hIBuR`~IP%H11hlI`NmNU1EDkJMZ z`dEk}L z+C5Za#}s&&MsQXwvWD4`g-r+6v_C=KU#Xzld1!u?@R$OBga$T|l+TwhU2C{D5i4Wg;MdFChLHqHybg7QXCm(@WR_d4sR5fBXFnrD>+0hiq z<5TpV9+LgwH2Q?Et)Z&1&))mge;*zSBn03SXKYIwh9EY#v|vAp3*4=>;}r}!0Iw@a zUp~#S^!e?=8PQrR+_!o6GnT9=i$F_~9yck-b1_Qph@=zB=Sw|_FY-cPYb2S_Mu{?u z>$45ZZ^K7wZun&L-zq-9Mg}kpt~*EwogfId*f9uuFb`>R0m;x>;p0BHjM_XhNcwnC zBz=flp)d{9LR3jd`Tm^1sMaD$m^r8Zk}Yri_Y>zbNG={RHo~8xn~tO?^D$?RXP-+g zY?jQmX4Xl^T#m;UPdI?iVVcK2<3tj)O!|rV^~jZ?`~UMZyd-?G$j3q5q=?ni;VhbRY9$u%6xE|e)mF3k3++MBczsXsZ$zk$cPCo zDgxC9_yLa6E!4K|?lUpFNA{I%ykpz&91jZy1>ECoWQ&lo_}LQ=9sP2}Q0H7t-)YM_ zeNOWd!Gd z+%jl*obA8bt?hhO?FTB2oQGn^$R;Gy=xfOiB6b#c!IR#ruEw{%;N=CNM(g)Jo9uZP zNa2A~rgsGiJu9z){ScaYae68rJ<3ZL1=Sv8gkVqrwAHY9!aK3)nov%I4dmG1LZMW5 zkcS@;kR8x}46;QKe&t&aMo&nb4r59lMO^mD?YAIH+8zEtTfeiToFWc}I**A+xu>4Z zu{j>9()e6Tu9hZ5+=a(pvXrFHBOvhO`}brL9d6Uk6yOHv^uF zFkfA~2te6x>04hRfJjW~<>unTG@{Oq4m?9d4a0y7sWsGYlMAp&fQLnHD;Ukn+S+$^ zW&18|Cy_8sILkk8?T0W3y!Wn$oiY4V<9fLgk$-vPEzcD?F|SKeaaL7BLE-iL_i-$v zpafSZ>;6r`&I0fnyboR_UNY})H$uvde+$A35CU}W%BrfW7@>+=4R}1{x^`lLw6uVY zgJ%hB8eoVLpviW2S}fGMCb00`$E;KMRu_>)bQ;7qZt)=FAdT;WJ_2kbs5+xO+@RAk zGk*-ZN<^tkbhm%{$kotWIzt zpL~6$lIu<3CmV!U!O90GDR^MGQn9SAEcBhQDmei!URxXN3nO&_bq^mZ?%FknA-Px{ zK|+Y0$4pzmp|4-hgJHmIdoLInC=|?9I%H@FZ#dVfDSUpya|km2^;~H>mqs=>iRy7ARGliJ4bH7v(9XvAPCZ7hl1Nc~hI9}ulc;i?xnaETH zM)(UC9z1@mA}013?+tz$=wMhvfP8^Hkp_l=VcMWa06GKAhI{u73>q*xy!xs$0;;lU zco-_2l|PduM4Fz(*3dDS+@A)l`m3H`AG!&%JUH*dLL>uvDf6LPCg|M(1PkG3my=lv zJW*KMM5IAC*IeP9UuNhmDMoKuEWA9J?6bvZ%-~f+yGMVvZS0K4&;&F_t=0nrtH6uQRE zx^MngR9qa!(7_(5dgaeN_%oEs%Rt58>jG}}HXM_Cp|k?2@fb1GpgNGBj!`llKYQ}O zf*5c#>Luk>lf`A%F=734y(f$QW%S%voTO;k2xF>bi-?149p#ZD^kqctDLqo3!E(+7!MS z#{1HuqUfLNk@h~hfWn5-5r9{eAIHHQjzCL@BQVgBT{qiuGsZ8!Iw4j>1Hm@);<107bz$FqvkI)1d#Xem zR`U^IzfepYyn9y9s#^x8wz!zdPC{6CeP1Jr6?Tv;m!X~>xz7Iz$}>(iD8X+8wB|nM zh)=0?>f$Nv14``+777+!oRL6UfBpWAnd{t)VQ4Xl%%5>(*wqXqpy-a+j%hPpclu5f z-Z(g=b`rmdiB3)!|3+_zuntnu?X_u71CzBECru9@-uz|#$H4I=2#Q8MiHBuNL#yq5 zfeX{+&;%xu0NQz5R%utiyN~GUP35hfIA4+-(~{qwDD0E{4bl`Gr9T7-IL!@ndf~Nw zrU)VUDVon69)$gt2qHivNWFLSUd|{-I8~lGBRsHFghun~X70DwM2R}8!TIb|YhvV! zRHT&xHZLhzy%Dgq^+tyn(aRPW6{({9A<-H5=#8|r0&xQn%G%l+_=>WGbsk8h_3OpA zC1OLI8?KuDl70^G=VyU;-(O5K|FOllgHEj26#CX*ZD<0y7`61$y1qc?!v_}S8`I0K zQTKiMA4u4C@n$oQu)pGg0p<_N^nb<>uJvMafvfM_w$fCNW?zm`;aSxlyZq8Gw?%}n zQZ91|za;zs8z0fUwvol)U*5}X2!UZ|lbF4FAhrkD$Y{kQMc(|Z|D+=*pwJh1r^#vYsIM`r)X>S0#e=(QELv;iNpH+GAbiPE=h?EE-=z`;7Mt9Bdw5ziKY|l%Z@|9*&%<>wkecFA*2|B+D$_Yw&B6T z_P{sC#s`h~h>I{V!(Yt9EFp1TNHjhB7FiQ$E5KOdWXqa{?;8Xg+NEtbX(5NfV?Z{? zzU?c~Q9PzKTo1PI_y037vD!A09@_qTEgdFvalt_E5w(*xZ1?-)OcOt?D_7ck{{;&rXz(1-;9Lp34P_Kp-~$0 zqV#w5Zht|I_g_C^SODRyMPZtHU$@%6Jqukod=~KC|A8}6Jn|#NAL^^i=QGC-=Dy5A zZg5_vFVWB90)-I>#8GWttd6q?;-h4P&(K?dC3Y$!f0(M7>)s1tA(CpKw92pL<>E?3 zu8I3-#tfddCP=v$Dc0B2?}l;L1CA>+FOjZv@5yb1H=ux8D|3odHgQf+2c_?W3L znb2O1C_!{57o%Q$5I@by@+a50Ys8bKb)aYlCg&7`+!+fipU=s=-}@ac z8K{H*9?>-ZhYn%h5@ztAvnP0OHDzU3BM=OL-6@(7Asz|77B_@pDC$JJ8?^rL4y)#4 z+jjOF6&yppHCJ>5uZSXj3Pv#4ZFmA%^vk^4Ww`4vIxA%k0omIbJQeEQ7?rj+R4x>g z%y*ol52gCNL5~$9$>0Y%g@l^ZYfmbG`B-L|6r0IyjCoa33Y+o-nTu^I$!S|q{{u0X zUoZH2dvp01ZSHNiBzN#@S*gTAiJqZy{!8u+E99^P*4_!kD*Iaisw|d^ItC)#>wk(4 zrlkepXnctspYdO4L*7-M6KwOqN#8F~QWMn8;9L6-Uty2WPg0u)6h-fF3uv%GSzo*Q zSv~d?%j!WqC(i#lbRlR`tKp9)qUfP|0VAa$ALyu*?%`{!`Ya*n3dVHWx(N5td9io& z2XQs>&iXn$x9=S_(ln@eChZ_Uh5PldeNJlkclWbqh0Ta`?s|P&#^V3 zY83u1e0=FYUCxiflBZfn3$dyxo3{V@2<=G+51i%*+SuMRN4yl|1_V>frs~uXw zbyB(hzpLLPSiJ_~vA>WCG@bDS-3s5Es}jV<$p);qE?+;(W1IlI$Z%UzN~Z_kp2M=W zRI}@8!V>l`O3<$x3VSYp|K6rc6^$knjMO5+ib6s*#&SEF1oo-OJ!Wc)a`e@#viTcc z+LWM8|G_4;1Cq)#Q~NQgwEYf8+suxOM(JsFj_Q$5YrNuCHu{z3x*(LAdS6SNvm=N% zh+kIXdc9f27c0Y6&56=jQN6gP?PHCjo*&dr3;V>ELvDvKwLiH(DmCS8^Nq58Q{tA{ z7}tl{Su?kNt&T7F89zAI@caYSDDA+4|27=)|D*>q=8RZ%A3{bJRgNQaly(rj`Bl@$-osAL0Jga zCI-LY%-zmLmqFOwk>#zvXAe{pu=o?N7Oe!jK_nTxhou04{RwoE+t}TYryi!QrCGRo z2yN+D+BaXLsp?Lzpp-^Mr=9fOOh-4%r$v8nK&<{8z3?OV9NAGSDXKG@W-aqghM5?@ z=#5Ugizw@fYfO-8_}hZFd;E#8(oQH{KEAZv`M+9#U5rZ&kF|S*kF$KWo^rDL$Jb$A zX!NH^xWPv&R^XeU$MYB0Sx$Bi+vNpsc0L#r5K3ws{~$TDcav3|wSn=Ah7|Vc#)tLU zjS-De0dwi^_c1iKgtSLzsMN5Xf+QthyogSHePN7)GIH)PcKC70G}D@FN$mqe{cXFm zJRgi~?HhZ%mPu_k)_O^}%K!W4KXrAp<=MQGe{Tuyy&)cnn^X;I1Q3CFOuM+H?C}xZ z114Y??YV`^-3ZikFd~&ccrZUu_YYlme;ch9=1TruftCez8Zvz?s-C8WhL^=> zxh2<8ObekhLs$bqABfH>A$i255k~{s1Hz=l0Bjn1l3PX{pMRoVKo{~Y4fARs7y-Um z)`j#Ghz+J*LtT$!R@r;TCfgVzgiwm{VFrt#>|Q{!4iYGKD=OlvZ)VzY=Lnpu%|f^F zmFUw{;|y(9{tVP1d>sNs+-s1d_wXUd%2wc#B(}ireaxo#)XvV^8CFNXcmB{S*%ZNb zmytZRnTlH?sz&g-xZR&amoDwTy#8%=S~e2mp}n~5>l@kf@!2yyL})m861-oNVOg2Z zWv4P(-fN+&jx^S4l(vseVv=dxpv9!pNcu<%SQ6PFgc`&>pPFHi?`FA$# zYVm_1Y5mZ9JdZAr?texd*IOBsrX7E&ioTEAMDH?Cz@o{H zQ3)ub1_!y03GImz+Rr%2<;10)^;MR(CKg{Tq~`fytIOFed$;8M!M$1KjD784R5JwBMy;%I%(8 zv^6&CMEsWm9H=_RW(mX*Q=|I(aSO(Opt4VDSAbF-!d{Q)!M{zT?idH9Df%VUog;C` z!=!9TTdlq~&?e{`8V+Miv@K)_(B57~^=&$9rW$ytcby|tw&yB^w7MD}nyX99rG)AwRB?=iT7m;=i-Y#Piakxpx z--tm4=g!I0s12UuYr}2;4n2C^*2;ie$cpq+6vvUQ(l8ZXEs8w@G1FVn|78qrH! zD&Xq>MeF)w!jzu7{>T^b2AF2VOD`W$dKT57)++8w_(F}tak~i3>{ILHqugf(UzG(? zDN|vypmj+QTU1PC3O?tfKm~KDp`jtX^QkPr>C4E51;-GXmeqqPgxcB0@MPhtO-w1q zL=D));dsYX^)qKS1g>N2>HRE1_~d^;r-tc0o`c$FsQdR_g*}YKcFkCvJ!U`jjzCRV ze%f=-O7`*Wbar)(7Tb8}@L}MOYm)}|R}tyhEVQuT>4=Oj_zHUfkim|||8eKi^-}(~ zHhyMpDloGC_uP`^VuWmJVxtNB+qZ8K(hr`a*TvP%4VWlj8=xwT!%Z^`TY81Cbar|l zRe@A@_q?0w=?N+rdue+S8H^C+LHX|an9`dMTS{T!+}{=BQ^@x&4}Q&~-a(mVLJl+Q)-%)0q_b-Bb@=!JZh|d`RRg6r!wF3h9;7<3VNdj-NBeKGyv(7| z1QLs`=03}pxqTVMmyiEFd`963JY1knUcPL+*NH?!OuP4nau-)HNT%~%amTf7+PD#0 z4O3ckkHC?@Eg&EuC^$Ve1qu@?zcqt_osG?HvLx6NO$z2WTS(t9$W3gAu>L~zJI}{{ z?(Xgm4)}V}F)>KHN75HrGa_hI+V;wClz1GYT~0{u2N@Bn)i6Fct7=_vOe;IS%Lx-B zaD#&|*VPsIw(#pST7prB;Q$`|++5v~0bovm_;5mikO&M6jE!ZUnE3JI2mV)Q5=vL3 z9YS1>JGC+&2E}586MGB7AoE_e&-qE;;Kdla@87$|=TIpnckE=L#nk2DB5cy{``uk3 zFG$d1rw@f~4={}3ww!^)Z5czFD@ur0r@iaU=n7RTH^N`EEhm^oE2F*4$b;%MaK`0*F>m5lO0 z1N~fDM&i+xD{6_q`hSP5HM#5i3VI@L3-9Gj^255o5TsNG+phKGO3{>cD@n3EF|=;` zn(J=`8ENHtC_4;_CBy~^v3(Sfd~d!px$#SWL4oaEcF)KcU+LvUQtn32#9ByW79olW zq@LZ}H_v6WK7m`m-9gXM$AOAreF>(hNODRfKgY@BD+$XUl+^%h5VO}1bsE-*A3p&2 zV&02`1NE8l-+!Z8D1E0PgMBtW2gG_b8*`uv@}9tgpcnWF^@I|Vea2z1b`1C_icl$+HsT+YbE&JECJAC{8 zT~}2CX6Ph+6Ltt}e=e!sKZ;k00T?%X;cpH5CDc)ZRdl2P%fHw(z1`sjiXq-pKsq$z5sD7Se*q7)6?25`YMey?epOB89Mw+`Ot^`PQ=F7z_$cqp1kE zVyDMyM#$>ZrvmCCj9O%0JVN7_?slN|#%7fZgVv(O0(SYok<>B-l^}-{$_g{a1AJ>K zy%=kTpRZpJ%SSaG%^9|l!(Q8IxEv8*NjNp8^Nti?j3*?)NCbw1P5MT<73QwQMMu9L z7{HVLBIb4R;=()>TCM!Rg2S6^Nky}&?_a)5>~cytfmjt}rfo}-OEbiSheJe~^-Psc0yt&Ns=T9$rMs5sZ5n5iIRj& z6&n$n#g1LEql8L`R7!@BdR}+?cb@h93C~$)opsLH)c1SepW(W$_f%S1ifPLxms;a> zo}TA!+!!?KN0qmc$^qUSpJ$ZfUTZA`S;nY9jEuOOLsfH=lZU;BWRI;!h%{zdl@%2O zBa7P-NGKl{SHebf)V=36p1V~13tnSKJhyC7%Cm4Ve@qU|vu9@ok9R|vSDuEra5?Ld zS;|pY+7PlCA`(<-E;0gL(ObeDHA=v44u?0s3IqcRlcIe|Gt%O@^^HoP_STxhs#jR$}WqapGHYZIvM^ zQwk%s@Gf=K5 zqx{2{(ep+%NA#fwr@2auRl~2*i6C-CWaVRgvBE2QDtX_c7Dq9uUcY|UYg~9gHZ_q_ zsGlUKtfwf+vhX~-YeU%4R|S1XgQ;u$dH>V*Fw11O%*t1<^u##_gGsSWcm#~Qa%bY+ zA~$qo!Jdfr-3|C$oWYj!d8;W7a}|pmbscn-0PzHF`!Boal5-+XJz-qBY+0oBaVD-e zhL2&*p@)U_@6d-lX;5$Z-~WF6%K3-gp=bYF85z6$d`#6~<72&!!1;&2+TitUOPHLC zzs*d^gs*N79bhRh&xq| z6KOZa3~50m!|l~Yj0`d8j>~#uRZ{eH!mvUliR?6v?;k&M)$(2td`^hB<&fa+^t680 z{-d@4r9F+sG!56QRfur;`z*He8w3BT?(w`*+YvM(+M0EKrK*V?ilh*SBTr9~WEL4Od!v{sqxo9<7^}MWGCWaXV+8sT1jCm4@A@QN~gI1dIi4C}GEFSO! z^$B&fyZfrEi{HF#Xorf0H9Cs0kT1i{Iagd{Bre1fOc#%u0v!ub2g#=6loB*f>02 z1n$3m8v9*$>S=(u5f>_OhNEf)$i3-|DHN#WVz(du0X+z;T&OZ{m498m`lKgR8ME=^ zq4$_w6&8xRw_w(aloU_w+S=NV4&&|1`AZ1hK-sw`(WxM~BSPBA34T`1r&$`5{`@0{ z(Xoh#k>L~4l8w|e~t+AR)lLZ6!E$JQhVq{)y?M)8!AgP9G9qP zB8i33RU5?_{$_{EsJpZjB;1a{9dW@|?rUIoDT%we<)_$cBXG+r!XuPoX+?j0vi5raf0lOWP|gZtE4F8rTea2TWSt0wS z&M?FDVR0o@Wz(O^#?0B^F?B*^*2=>hKAYyOexm*KR?NU)x02!{&$IX5{itnzzYcZU zR3jtKN~`(1PZx|?Zk+dr<#{D9i?&{UGBT|^S2ec(IJvZPm$}2VMGF=PX8sQQA(XSs zZe=D{k344Q{UVFIsNFONx?jwLLZ!uWq8PS(IN+%I@Seq_ZSFaBpX;jS$6H;W^6KCk zFS5E_#tFmxJ%3u(Q+17swmw;4mk}}S&BZ?@){G>KqkN9&YlWKd^b2MSj1Qih*;&=H zwg=D5(a5OWZ5=KxF6F4AKf3nGG!xffyVDDtc8@Q(_tw`!Ia6QtqGHyv`;*SEvys^+ zwbqE!I3MCq-7*t%T9imaq;A4RhfQ6r0zf0Ik_X)C4E+a!lofkAsR6fpF&UX z$|lUVZOa#(g7RB0*B#HU^4@cPy6AwgAOl;^S%;e}3M>wuEE>Bpr&g@-Jk~}GFs?`h zE7;4b2hUWv_)`4tv9FJ)q`ZwZO`L028t=^7o>d53v$m%4jg_kM(|$GKGe<$13z0gR z=~%PqBAFWo@zNVll=mL^OnjQNn?Y&mwjIL6pm4>w|BHGFSMT$`te4GuLaOMQ$~*97S$FzNQDRz2-DyK3q=>sSH)PiS(8E;|>>!8mPbV zcP52?M+8M0zdfW+e?MQ=CSAP251&E}@iU*~)h2*p>g)~HKoU^j@U){!46%a{ zrBGP7yT}b9$SIxArs+b!HlB38H=Qpz%ucmWe|Eu6XaCIMQ_M(kECT1li@@E$!W7B_ zOonG4=V)UUia(N&EU|}UxyrkT0YuWSk8x|ITP8j`6gtZXV!e>(TKkN7%%)#GtE65# zLwe1WYl7UuqEoHCK8$(SEl8yq0hNFJzAflcVC#{HD1;|5URijdf`^j+th;ehj)G7+ z*6-3dTRRqqZt-cDxCX)~gLl#$yg0SoGouCeo}r5NQJg#=0zoAr7wR_WskK&5*)&Q~ zl1IL9^w;fEwFZ6BHfyL;5+0jI_}77sKd*3EWlx z{(E?4N)?KAdcjnCL6gcA?CCkJl6OoD1J!>sVqc7uVZl*;!0b{KO$HeO2Y88RGiE;3 z&3pa|79m@nKM{}<$k3BNP2GT0d)w=Xk+A=;b)jQ6RCx)15E^GfLINWaKT|?rRAq3a z#sgwAfH;I3N}pg27)@sr?Me(Y`B9)wq81k_n4SU?!DaXjF+$zHH4h-atE1R_Pr>q3d%-n_8T@hc zz%E%rr2n%@ztGmULD>n~s9-aMXxpE!1SW`1l1d(MF>FcCGS=w&<9tPI*RRtTyn@6Y zMvi}2W-Ck|gX^AYAh8QJsoS$ch2Ca*w@;IcdAs6wlgR9paskL>nJ`P?7tBqy@0nu!nNkkzrWNOSJO)Whzr}5(s}y6c z#m6?^+M-�ay>{etneuxR?h(Nj^BYn|Id9S|1oRW4u~qAQsnH$b=91+x-euix&jM zuxl<}bj}G_nrwG+FIL}ORJ8;UV=whMbv9r_&@@*RuYiQ`2IF|PgHoU2tYo;=xz zy@s$TZf*L&S7EY1)534&U2}dxH`0!Q+O`Nb0@=ZJ#xi1T@Gt3szll&!pL>=n8X6QX zA-rSLQUZ;L;vQ%KcMBIBK!PROF$~<;U1UIXmJ+QXET8FNGU#z)ZALvfAv}7uNkJJ~$lTyN;$K}94R&L3K z3d?p2%MGf7r2DGJ3p8TT$E8kANNh|?)j<{6R-{Uinf9Cyha=Dsk}iB4R5v{27iKr9xl~RGxh%>APUWA z%87nC#N~GN>PAl2oSzlxJ^6G<8a(a{JWO3ZZrE>?N23C7M=$SialQF+U1CGsKnvH& zst0+@NhX^cp|eiqD-oB|1`|w3hY@t@50)EqpC^?z2*vqIV^sh<+7?g4pZ;cXC7U6( z;r<}`s#NcPTmWBH#$6Z_H;pH(_mcKrns%HJS7<3oP2r*1z1El$`FHgWSR3PugJYa?c zXD9~;t+0c!f||21XHt}yYOzG{E~8pv_6T9KA7yI`2z4~}_1Eo;Tus#|R#b}_C(=$T z&o;k%{-4%6xxp4pe4>nZ>(_7$3DM{ci96Pec!43mPKSybbc1DQ|MW_c!_boL(L>hL&C}>uye4W z4l5kjZ+}L?LX(G?b=%B@V{P*O8N5vfWfW5I+xYc6xTFCXG{hOkLPCSo{nF85a56j-LN7iZ+ag^Gij`Pzs)kc7ev{Z2Uy1*>vOUKkKM2VeK)5v0JP|MpAt!+ z*4i7@Viwbm$$2QH47^$8O)CjhE+?M*l-TM;mz~MCvC8(9P7GSI$M7OgsBDc?)!Gh& z2&-U=AShJ6mF@_m;5O|3HHZMbgvM(1Xd-}$TELE@H z^j#*-mQx|>y_qo7*rygbNBAh_&)!B-6U5ec{vk5kaY7hCAA00HaKD!BZb+E&!36=5+c=n-~LTGBfO6{_vI0a>MLuMW4i+*tG&9WOKIZ) z?7*O0e*2Gu2ozS@AiPbA^rZyAK#k*v0S)1CF=e1E9J@U4hCQ`F4w=wG4ef+W4wWHQL;=wkl`I}iZ8DF`l*^^$^c)oECsrK|# z*4|eBi}OaV(UF{Z&rtPCRiruSB;C&2T~p)gjjcrm)u1!D=GiCEsn0$crlBko_Px0p z8^%rV%Z9lhgD$XeH0?oD!dbu4qsk+e+yrq5C5y4cp-M|C#sfDD%2o~l6NSx zSZ<)eg4Te_9zzeNN3!=x6<@vITab|^^PUsg;iJI_;wQzCaXp59yr7QJCGISY4}p(hvoXx(HzeDgyLs~mIuRPC9fyZmUUTXnkJ4}Q!Ge=Rv`$Z# zAO0q6-|l|;T6H?Us%I;_l7mhr$@SWE@p9tr{^P)DhqXzKmi#I4QEEo$qANqhhMJXK zQp}v8dRjxgDXFrtc3ImUzg(-GUr&VXb+G%rUB~p1O2|#Se9+Ll(8=xp!M?9bqC*`E2@dhc4ii(QyL6&X} zO&d#|!h*%h6~&%>;4~Vj+E7ne-Sl%zH-&163Txm2WroXe{UFePnxZPRTWrG_fM!L> zVOBVMZl*UB_BM4Kl0|e*Ikbc+ijMD7555~qPzx~`ebCvhFudYOPu}Z_Zbzal>HTJ&RdYMmR?xlmnA4WGrz&uS1f zR*%)y*{k_;lt#-Ye7dPvV9*iXdit-FhSbuAY>rLgMEGzviRiHGmtEAm-qqLV$RXFt zKqHA+87$We6+i33)LUuXHTr5x=8 zjB58L;%VmlQx2C#)omW1Bbg+zI#_m9WW46`r;2yaIaQ1o>k}Vb;CeRq&G3JvFDvfo z`)!D~)_e2H1G2{99Xf{fxnF1W?tOkh?ZZEpth6<}nh<$nV%I=}!x0{)u2RByqVq*< z*zGFsd-FUxcaKxr2U5WGs&W8SoDUajNhC$)R#$VdskDWXLmoxWD;mG>t!t*G<WlNs6?>L|3yC@i0}s?p-VH~p=O)j%ayicF8ttA z!1pQh6*-5l+`84+!;=Bjp;1Tshlz#|qeNjtrv(|O&CdlzDB3VsM`S87@3`@5`|M;p zOpfs?qP2lS!n4o#+i^SBzSWpe%b<^ix5wPq?))*`qV;>eONX3C*g`4sH$??zd7EcU zzu0*2uh!Lyl41tA)OJ!!lk1fB&F*^iX{EtACozYdD<*E?y9`w|&a}vnp3!(`W{QO= z4ek|tNA0HbDSm+iwCAb&bah#TM}0{4Y7U>^7VqUa&!OLWhYM|{uR~h9H8yTIaP;7A zYe?-`{^E^)q}nSfdTbefIlpFOzK>Mh#H?fHX)3}HqGP1RezBaE*Zs19yao@ZObgb) zlz8wjsm9-RM#Fr@_c$Ggy{s!<^l`}DL5|9sLWZKBA%B$PSO~fj`s;TO1-j#ZOz_Lw4Ol@&%MV`ge z$^AWsH|ZU$H(L8#bffj?Scfo?3yZ>rDp>V;I8gjVze8cIDhmxP*J`}q^CFOP%}xhj z?H&n$bwk+Qyur(NR$j@S6fC}@cEh!)_e4U?Z=cewTr~8W`?0>uL<@NE!@lknBVad` z6>4~{-<7KKG535{$~W&B+;_TE_w1^H?k16ow>KH(t{>|--E)f85Q8Nx#Wg`fscYV6 zpGxMkcbM5}MjI0gtRWX}rHS4GUE!zi1zvRTF6mIzHq&A;aSUHt+t58vffl^Kw$iQs zt|GO7tTb|QPaV;EIXMw$Py+NNyq6QD-bSA&SXQnqa@9w_7@vV*V5>rJC3zMa?6fHT>s+5ZRTNuW`_(;6!a8A)F+a9-OO&pQI3Dp@5LFfQ-VvNQTnheD+*e=(-wblygiHh7 z=*!R77j#F})h$mdGvVp>HOrOyYxe?$ z(#jbAOEPAy>KYAe=)C)O%3T^!);%TYRL**JakJ9C;JhdbSjx}`?r57B*(-|am*W>QW4Ak37Z*5BQyqO;Sxf$%M8>WY zFV~UE9`q}GP2mP-Q#A|+XITyzBkwwEU!|!@PTIor@$rM-Wbz^X$92iN9-VwXaQu`3 z2J#XcjkTO@VD0&SMPJ23yY^1toCU1Q5W9MQlxeQ)+9s*WCGwXJ*`(6<7Aidq$Q(4I zZJ2L{?HZ4MpI^l1=9>kJZuIsCwI#@zuu-6lD;DIC`n2O)Ud-RU-sEYmcNa5jP<#E zvjh9W7U6@=g;d(G;V;kb8cabiFp0TP(dc^<<5)_fu!5+|8O8inQ#B$U>?U(oJo*Zc zSzozOzDIgXNuyZRhE!O->v&X~A)JqtzybSo!FQt=s+gy<8VLGleO7)>zUz)~xx6 zSvo{BtEpaN$vS#ylvA_*Mm6Tr1$7z4^?8es`o@*8-m3+&K8cwIN0tgikR#Sw8sA6+ z>N~m4+{I93hEx};74mKS51%t-%VC^9krq5llA~a|RYkM^x(!;Twp_Duz50QhR+Xrr z79Zm>MG5oj@`nsVQ9vghezyFaq`c*dVJio3Q9P`)e~gaSZMA0Q)|pLi41*Zg1(B(8 zy4P}T!{^r+^-&45|IpAnDWqjfzRJ8=pcYcTD}DN0`Cpp1yLhda9E4*Iu6ZW7{PZ#; zaCkwGh!XQ)p{~-C4}WQhLmz?J&y#N}e4~OAH~^;#okgdJYK0r3CznaB6LM}@Q+YN3>^y1m~ouNOxkV-V z>vx+>;PIkG2SK67Z;?L!uBOXJPW)gy};WeO>tm`pr-7H6vs2kgri z!>5zuN>&=g!;j>Ye*coog2bJaa$^${!Kc>8nD_w3<*YV(f%WT)_tGRo$8|TjnUSGC zemr}a#WGpIl)L9H3|riB?SzwJk@~UeWsdKPrQhsbe)jUax+Aa8PE=vYov{`YD6FA> z-$S6&jD`;Nz~id^5C~?>>}<7%Ak~U1$?%64IXm@Go+}Y%nEEm_vI~YG#jR}+uM5vW zLE?0Ww2Tbu9`$;o_m4`iTuwOiXySaC3y-$rogE z?R-WES_;H$+R5Jknt1FO{u@Ns0~`4iu{-brXXZv(%jG`a00DEFqpszXmA{5P2)gqykXV->hD@i)Y(5(YpjfnjEBKstDhPNcbY9jC&4%IG(<|s9NEL8@9I5F z^mN(~PYhL82crX+f{}M(E>t8@ci2gMhFTRcAkvO`^w+X?=d$LikxcX7<`1IF`qtJFeZco7_@-YWmByD>86!6p+R8u%C9m)^cz zX}F-QhkOeXKK~MTtYtDt+MjyDCYv>DmWCXpXNxO*5DmFeRlg2GUT<-KZELttldpvDp1XlwYjvu5>^zJz^@Tc%E22|PWRyFc$ooN^o; z>+`_D!67Oka_-V#4VD3K_tcDW$3g620p7naUfRx2?-=PKV z>>B77YG0ay7LEpDrVQg~gPY2TdK)Z%-sSc?0e*g2$;o+deQ|Gq#p!2y(9cs#Aq}o{ zd^|(u`;%aca6zP|iUrFa98|o4!(1&gWn7;|*XXi-L!`YV_>{y@_^3`YHEsI-E81O2 zGL%Vk@dnlrLy5G7jhk-S=@5;>lobiut6=*OILn9K5hdv9>TcS)wO|%A<>@9cWf^1G zr26uCh!_95p8*!2qwd*6p&y;JYsAP8=YQJd_m)SvET>u0sQ%XszlEK4OQ-6ue%B!M zH){c|n3=Y2#?S-KB(hX)8V$KxWAs^@HgAqNRr&n6iLo)0_36Msonk-QkoCc@HX+>G+W7!iG`twg{iN>YjXHH-H`)`DvX*PV?~5 zqhWD4-IMvF@n({l*}*ftunW5Q-2=<4)vF_l>*HFCHRuD6)iPj#D)B9`sbcCX{)QiwQL(TKu>aHt3?_@hzISf>2_*5KWka^z0B zW5+@T!nuT?slWc(-8Ye43!T~HYQ?i>>=MafvIvNT=6iGaW;(*t1P_08`zA#YUTCyU zUwiAtaV-A5?gl~E5Itw5%uj59WU6p;lgUfYCH(*2i6{3xjl^(z5!6O(Qv}6$=H<%} zHE|A1`7YY%1dEdq$CvFpsknYBe*Vc#rn46`$J_g1^OgqR$&tlK;*jOvJ5(b-6N0La zqNHa-d1WQ8Y>2D4+(N~{-n-WNoh;Vs^T07&;`uXIgOB4+J1xy@i8bHRqiCd;UA%bP z=LaLpUzWJ)+s;J}JNGaBqdkUFu!A`=gEdZ84bl|2)=K+^ZBFaiDHxz3SMfJXXF{cd z0*6b0>O(42#C9?nniNg=`Laj|i(mtN?ydC_Ipn!9irY}t`kNt4n1Z{zj^eHC?DF#( zNu{KSX1}C(uF@|=7qV;_73uf_-<2zQop6zf9eMTY70Rr{hVftQQpyk801$=#xyPmO z#VH8##(UY3BqTL&DM+n1)&nVQ{{df%U;X%T@VYZc^ zG4r2uS;)Y+XW2&_I*wx$SHs)KnTA)-Z6q#Axl$fCa8xHRzsmb5DQptI@Z@5@wX}Gw zHK*{4{#-#%%net}^3=m+pwNz)bOS%Kof&#Sg43m$uNNiT5f$L+?fu2i)Vg)i=vXG^ zfcj|*xhm0%Q3BxwMIa1|Bi07VXod>(2UxenFjZV-L?_oB@cwSt#Fe`ui!6PQ6iXke zIB0dO@eeKY^xS-{DNjd`@SW0dw4km7d!KRdhhdhZROh4ZnmSE7Spx5%BBg(xkHLL< z!Ge(UCQ9iu4EvuDDVrxf&&@#gq1wM2PX+ZW-oS;|C$TNK@sv$u@$gui@?jzput^BX zO|_43_P#CVL}V(boO!YbouyGisEPZ56Hz~Qr&A*w6+P%MkIIAdXha&!g!CX?w3ODB zvzAG~!#pxSE6Q_GGF#js{3N|9r}1Sq=6I?058Mqpd@iP#*ncSZo^#28)<%2~ZXI?y zK9kEfDV{Kwd%Jt|%1c)BPwZ1u^_>36_a^LF(;2q2a~DIcqO1?e%Ybo8DfRDWt%Z4J zR@*f0B7(A9e7?f#@Uv4ZReqkl?|vmLdjsn~ym;~a-rp7(<%Mo8Hk^87;no&^ji)~r z{8+tKC1dUI1(Nl9*^=;X87Z@_-19zLv>n*L|KyWFMgfOEasI%H#VqHGd;Z+WmQ_c! z!WGs3&Cc97ee*Dvf`k2L?@FuKhZ_5Gyj{L`3Gy3E^`9ad-=VUqDUkiQe{@-rHPuX3E|yRI^Fqbp5NH@}>!*J;b+#L8 z*Hsf$SO47Y*7hsv^~HbP{<%u*={&Kai<0-Ak5txEo!P&uU9znK{p+b2Q&dV`pGl7T zk1ZN$@A~Jw(`icf6{R25SNNO@FUnUv^`MaDiZO+oOlji!|8W6=id>JlZkE3MGqWpG zG4MtKO}p+-1E-L4HL)qT#- z?#{};XVTR{=BV)FJM(`m+ATi#qD_0y#f!xYTp>Ij9&qJ#c5%BgZ=0KJhLmn=pykr3 zC(p@M`5%uw((}`nc^>*DJ0bJ@lBf^r6CC@D{(M;=f(84%5Zum%Ul+Lk|9=eEw)dh( zob{-<={^avb2s?#w00YL4#-ux2Do=-4lmx|$Ecx)lL{ZT$Fqu_p@+5&qn^LIg$4tw zRe|LxH)DQEdF{lRDbdT&K0}*)zsZDSC$0o4OOachF)`K%#{=Eai(Sb zzokEn41HywUk##yg6fU^wS{BD8GmPiQaeUmLYmx4lZFLmydC+HwB>as&j2DRb{ z4}pT(C;_^NpmD+?WBSC;rp&}zCm)*_u%|*8NQu85ln0aUwTi$1(TI7_Y8-0=`d?7r z#uJaQ(!5B-nDN1ob_A>3LXSM)4C6-;YlDIfylU-pLK%~z9(qhTu76H5tN^tqv=dLm zjTpRb<{Ro|@2RiD@LMCFvuIz$W( zM;NWnfEsYX=O$GV;T_p8GirL4u10`MP|*fwe1d?`eg?I{4g=&}!%5sX1kWYZI~N8H zvkV`?EEvteB8gzcF8na5SDd|$c9Nm_Z+$@>C|3hs4#n)3zmoO@7BxbLH+)uyZ#-0SJpM%0C7rPX3Rug88t(Zd4~XTFtjoy?W-% zyX8lS+!CJWND18&a|cO=LJ$)YU33(AI>j5}ZQlVBaS?(TCnO2lBhymMdb#z=Jq4dE zEHwJ`lVR{XCe|jxibVv$2PP*h2L=+Q+^yTUZ24A(x?-W^l?Qb`_=Z|0X8?m@rp1{0 zdre><^*LlNEemr_2Pnn%Y24+E>!~|{iw;6)ZK@p4?e^j#9|11UdeY(0;=1aK2j^}$ z?vl^@QrbdDJpsUMyy;)imv{2xp=uS~s0ql1mv`EB*aUoVHC}CP zt<4jxR58bZiDwKfgyn^R8qqY{>1^T=3-%?QZN+>x{6t^Y)@u0XL$}$9xh>ETfDe4F zdY-V#0fb5~ass=d;PHWPZ~y9b(>dViTR|YoQTvAoKj0$-xR3AOqwf9T--X%Q_3pEG z+Wpskv@Pw5+rJ_~46=oVT&$M(u3i=yyEL{Bw?1`c@4|?`&tIPS?Q-^{Lw8)NhF9CW zkGped#zdRbI(Yx$!bk>pPWmuiB^q^si_K@^+(nm%?;Hfe8L9qPtMC#|d-hTASmQ-R$NA&(10-jq zE{oKFL&f}7E((7k_y#97e%yjIMT0dTHvI%N3dl(!L@G4j&$Np@6j@y0mMJQdM{5I5 z3tvI#hyqiu)$jJ!nw?vB?i|`2M0Z4O3NSPUMj;k!@H`I)ggfYSyitCM!oH%@Q6Ggz zSK(!odWAzlG86>c(~ySOytBX@3PZmN&iY1p=K?{U_e)WW{YTARc(kXl-??#P>&A_A zE!400Y0%PrQ68-*NqBP116blr(_x{Z2BexjH>Xd}7d)I}-yiiO0=FyxMjCGL`sMn{ zuiHA{HerNmpZ3Pbn71aR3ua&caW?ShS>91e+&G}%4u1bH{6Lx52K*>l2BGyV+j4X) zL~&lyb*D=N3}MyCi2(wJXufro`L8PIQe_{*^zzv+M5NI}2!4SAa!J6{Pf1iyBnjO~zUvLqMRPg6U-Xq9 ze91v-iY=Y)_dX0@3bdI&ZWTE$(kL?bIJb)Csv6NFr43UyH!5!b?R)bD+#LaAaUxWY zUsYr;LsnY7|ExpAW;2&fhIOAEe9USFT-SnUg6nC5|62GOitiCQr|`cXk`l+w`0eY+ech3O4xoG zw95CdlMC@|>}iN?Z2qPJORhu5s&1AGY@GnUz|i6CRQ)2Y zgRkWd^DHh@m5vd-6oSedalJE6Zpm<-bC;sc<}56L@;oiq-}zz}>M$ZZ6OC!pgN z7u9pQ-9b9&r)Eo9Imyr~IU1Lziz;>I!d&8G6BHZ5hErqN<6;P5<{tF0fa{N7FmL15 zp_-v`qzj$)?>c;$sS!uV!sHV@!eI1~M?v87>Da6AMoQjl)&_rM66^k?*joAMy8KC! zB2vSbpL|^QdyfX2WBQk~ckWBlX1O|lCfhO7^Z)(z8Cr>M z;XZ-X1>t_$g`NC=kTaJqTSl(dTpSdtL(ecNm0THU(INR{j&*E%ZLKSHyzfow^q7VE zlf?38Fwczr8^>(PP!99GiCcHLRr@jqe^p+817L&F_}2IWOPwMT&F5 z8J@j*g)eTnU+~z;zAJmGc~VH0b^m-u%~0F^^Oh-Jw6z;>nt|s2bvyqvKf+pu)dF$*4}h)>BVCv5?MJ|b>sOBC z78^t;4i`qqTNhf=(_lMfazbB`p{w6#HWA=u+jsvQ-|KH`sZD_>x; zUXs&ULQ(ZNCCZIMWwXs0%TGN^KtrG36yjlF4~R(MKd+NvAX%ah^9 zwI(JX7ZvI4V1Yq{v8t9C9l;S2l)49r5*>n7quhe8a=oKV z-lpIFyq74IjukYUANZWT zGy+^kJRZ>{k0&YdW0f8U(iqG?$K6qbI)0Qdk z9_7aS*JwJH*=9`-UTeKs@wCK;IhE5>=GqSqo7v||u$>dvV%yl`(dYE;>-taWdK-2y zQlc!&TcKt9-Q4^vHPe$=TOKwR|KO$}71}5Nai7w`mz%>lK;_~*uk9KvalW;6W~)cE z@5|=xmbW!W9!Su^{;V;4v$ozS;}7d1yO$|RsvmW!?dL0-H%{#K%Fn%bNq65{u)F$; z$Yj6Ae)~uE-?wK~$QOzFmE9#pr-dqWgt%SaAjut!D>(v++~i#roNJxDY;6Cz2yuT7uX!Wgx1O~s=P1N_Z+y4kQTOhq504Qg9$0#)$PNDeEM@(TkTsEs z9=e=GVN4h^chG^E+;oj!Z!v37mgdNX?;M|$R5K|^EJ@G0-+Sl7B^G~c{2BJSb^Bb*KM0lS{bZZvNOPX!Il2eB69mmw@p=4kau+K_sXbyMw{!B z8Ba#J`C5%KYj7)@xYt%E(OSW6#ClP6GnZ-i-7Ku1h(5@)tc*_8mTVF`IPcX^J>|1< zp9bU)t({ZX|8-jK^x26?gTkFHyDY!gkDqo_Np|4)^nzs{cK4#viHw>iF@6whzsF)L zy1ACFml;E-VMi<%a1XugD2&je_I0HtRj>ZZuvoD`ZeNj)#;o|s+XuqP_-6<0n-HLl zLHmhaG+|sJd;T0~bH-IR0EHbTA=flbO%B_>GH$F=2|ZYMUO)9(MT*0sBaq`SIebLo z_2k1Bx;vt7@$F$&A>>R5kp#jz&=p*mt#mSHhF&hrsSRjaTRdD%Ty?QH=?~0M-o}+c2Z6D}z0}F^jtvZ401DOGj@*WJ3URQ| z?Fk&7uU@&pUp0u=81l_jE+bn-*Zwh>FC5_O{_mBW#)voPZtzOili$2(%8fCaYve9! z_8$4dl7ab|100x59)0-QzTtC-1bCSE#dY8Ry`;>gVU~ z36Fo&1n&r1mFT(5xATO#v16-x!&0XEe|JBeJ7tbz(ip|P0A>>=3|EvqFteU%5M;7| z1$lcE7vHqfnZ{B_+kr_7;a3kJ81bJqJ#?JHQWf7oQbn1aspu5i3!t)wU^IAxP` zg}p>~M`X8O{k{GKLS1tL!xiQR=&@7n|2RIbG|61H)JH9HkhmyEW$T4yzu~z3Zdzq7 zbbqjbob|Bt>qMHBICaO7xo2^F5{7NHxxGC}5>AAqJ$KHW=2d&`Qtk6JgDPFcLoGYz zP1`taOO)2f#9jFs9R|6-MJz_0>JvV`@AdI-K15{VnWIlsr$&Tq^YAH80<%VS%&*zDcAV<^>0$aUVrDY5t)8P)qYj7K2Z5Y? zi#st8ySHW3{BM0xW3KM6@O;(=DN7rsn=ubgcGZb}+gkKZZ)U(BukQ=~iEUsFE zp9qA6sau5AYsy8O7}B4uc;}+4#9UTzA%~mGOQP3MpSqp=X09ic^-K^(D@5So#MEvF zRRZq7>lsX~SW%Cfj9wS7bJQL}iY|HQm;ey5aSMQc?%5fd}zF6sPbS2r^GMSjB-oi z)uPI}#P(@Xje`%}56S70y`yF+I&ROJL09rkP8{4cvBh?BomX9jhe7BuN- zS89%${U*Qd&S{z7fx1V!*IR_>DfS)waCgeEK@rjO&V>fnMU9G(jh2h{OVZe2?785? z+_1BQ_C-hy|Fd;mnc=QsN8ji=PRq)+__IEsxl?%>P3SKXiFr|n!)>)cHa9O_ym+vR z3bs&8c6)-_7zuAO!Qf(MO53mBIBMYu5V|wtboD=d3InBbD8S!ATudoFag?4WgFxYLd7?-ifN>Mj49EX ziS#6iu&p>w*fd{%_|v;9O6F~ICC>()`@{GNoZmD5*<>zu1Qw;0Cb%$q))0%`Q(=lU zJQ0A!@_vdH|<0+2EZ|9^CfQCmYEA9#I$2ydru^ zz&$%AMri;q1$C8GdNxkWmR;Ne|Cm}9?DxP-ZZ;~3a!>jqT=(7-X#VImO-zYKYMEU_ zNV>cyYCn|yF6k5qt>|b>=_p~bQ-cPelVKRp#oUy6KnL8SEux{8Z6YfjA2-X!ze&-) z?%}=4Jp?Mzw6rOY&FB^+&CX~x_jz`fO0sYiOleEmj+ZhQT7|;WUoJm)amJ{a_tMA5 zwas+B7cq9otjc*KzeBOmRlM+P zEKGVJ1<^mBxZH7g*ZVPvvZjvM078d!H|Q@b+g;WMXJuc}>wi9JE}l;bT~m|(!1F`J&cGT@OlGJ-i&%nW`HLYK8pN?7Q2nkcF20?o89j z?PWnI%CYtC%28`Iaw;Auul6687BthLw|#H3IBSDk-JwU~{if>S(nt8jr)_6e4ie!^ z!!N+iPUHDdLM2WACZ*hJn!YlAX3(i8Yu6hYC5-Jc^|qgnAub!K=99Xza%ELsOP5sm z6^&w=S)LaoJ^Co9$+>B*!&^PTpTG{{|hQ|qeSO**vF2DK!;Zes!4gdAq z->k+h)9;Q8;jX!Rs+-Y!G2a97F;(M;c{Sg5D8`~t@+@>=u|^j2iv~*0!0qqQ6Q&hC zWVRK12mH|(eN91}^jFz(vm4cyo_&Ogi@4adxuu1+$<71e^@mNCx4jIH(F0y`VDVQs z->d%q>o+^t?*#>5&8KIchSaT)6*Qxvh)vVL$gc+tEh>#&KZK*@;0tsb6DHJClVtWP zS);Ekr-?e5uSf<0KU+QbB0ll3qUdIIU=d!+qQ81It+mN0J~bYkjx3_5&#%6j0>(>C&HEuVyEja`A`a)z*mDm-Pr zb%j?^K|w=1)g!YZ9){V#x^1m~rWpZwjTYjsPr$2U4J3c$rm3!d9tHdH(?XyEoFFiYZ$bcLe7HzFq zZSY>g|KJqxF@~^6>Nhf@CV_3Kjwb-c+?%VA8W6e4gChAw@zN;R?=Wt;-B5P2HzdSL}zlq%}#2;tZ( zUz(YlGY1Ap;UqIkn6WLuAb$5KA7(lY?c`;u9B`XRIn3}CQ|M95 zIY4!qn|TlS!w1wy@+B@gEJ!>21MAx$Ib+K?X%aF|c6wo{#EBVM_Z*6*ZOSsRp%84l z^UBW@V@wFLtaT<1a{rS4#H(F*BfH$w&s2>>nff+{LdL34yr6>h%#Ps@uoy6e?U7Fr z&?SFLb?{(#3{i8epXBGOTUaXA@RPA7WR$r5j*NvsgMZ{PUv!a>`gWnpJ25`~Lrcp& zjr^G@;9Mu=`Q11K{Y4HaLqd@-fK8xrM2X#Sy2Lv%9AYW1sppjbj2}|k0K_g5RExw_BCLRK@7T-= zULMW1%Y5QvYzpHx+WH{jtq?AZ5^x&Xr2kX0yaq3JNW9pe_|)zKL3Gspx_#(!A!~N> zI!(H;F<_hQE1D<4T+7gKw`L3S z23P%XH^WRvX!}S{V4@yBUf5)gTMu~Zjvbk?%)SV2g9Ptwggf)Ik25AJjJ|xT{5^J< zkI(U|E;$s}Z&3bwh1`Kh9w6vokk%g+_szIb%@!LXoRaT9eoQ!elu;6!$0fbsz4W$u z3x^{RB0Q<7kPYjpgT;?dyl~i6c-`+k{Fg)<2Og7B(IfV0xHYS(s5CY;S=seZxjk$s zb7hesUe~q<1h|Zjbz2_*ApthV$=}S&%R?}-FcSCy9xwk@CQR^PMwVkW_OTbk z;&nH|3hLNw9FbnvBzH=yzp_ac*?{<1M%B2?+XyI{U+Ln)mO@mAVnJdGiqR(@;PhVc zWeA%7tUe|e-ncu$6J$JG0NODTd9G9(3{gT7DapOoC;eA9{ zA>=dI6aKH}o0%%QjD$gjVVDnd#icW>U}nmO9*%GQ)k01nB_$=VYbsXVkw5Bf1vEIv zB8V{v{9>3(uQ4It$*69KV^7>@tPPOO1i`fi9~$&{kaoff7|SNCp<`sXH#QzSabiTk zd?w8lrjns=9C_3pLak`|fv=1(fO)9SF+ysy?-+pGsr0^qr?zDJYBwl4hfzheS6$!J! z6HznT6YZ&@cfW z@;feJ;#l=ZN%`dM-RuX?49IE4fZHnpWTNad7yD)I0w7`DL4w^t_u}Wqh{uw$@*~BM;%4Wq{5HsIR|r>!}Z$mnPtGEj0X9D+1h6a~=7VaATICiqR zIkmz3p{p|;^~XwehgT-gOu?r{PKg~%jE zFb-{2Sq<+A5;3RVC5Nkyy5}mOu^DVx*ad4$S7z@tv>6V{HiSn)%D23g*SY`9|J`4D zqDLycPGIT24$TXvp-}=rONCd3t3I zld9S>nS!h*6$Iu7yPpciEGfxaYy0-?Z+y31s6eOE6K;dT=bF<{$7o9OSWR}!88+b~55!b%!)Fz6fawMsrmgLH7?wL_V;&q&OuY9E zqAQehikIU051&5G8V1P1PUlY5Rie9ip>kp_;Nykp#)CA)~!gZ2)|T_;0(=7&5b}_qv2+^GE!H~EBDXQe%;39Mot9}H$#(BWUX zpY-*KFb&SrILPZ!sIG0|^Pod&+xuz8mvXmE+Sr*X(BR5~cNrfkaw|l8#E0_z@^i85z?=Cvp7RxvbkVEW>pG;0AA;>3_a7u$6rpER?1#(AO!i zf~NH{LV;x(^8nA~lSZnS*0NmVZU3LU zldI^mKT3pIn3k{?zzjhM5nR2$-%WKM)h(wIp5%1z2o5i7FPD9P8~vMGpYhN#cL)4Z zgxkvc$GV0m+uuTsNL}-^uMcuJROkmD`V2=4|P6JOi@KYi0%uBZO;;H20;Kq#t{Y2HHTiu7+enp z@wyQ0iThA_FTbyt3Qe-Evj6|5dbbL46xdDM@Nu3O&qEtj|r^Fo$rYmK7Kn zD4U_a0nm*u^cINt6yyxJ^_LoWj)LIolHy{VnP}h)3z@K576JsxJ!n-+osFUTv5~ZL zz-0r%IwnnK60%vQ*9SQXuf3kQ-W_RrfbP(5Bp zd51_NHSQGUPi>!FZ#`I`&u$4;4DhwmXM}Ar?85KH1n`4FCc){Maa5Sdpt(a~zK6MO zD#RHJ8H~G7+f_M&1jpJ;21m=bx_u>J(9pwi3uGkD{o?IRoHXOxqLXleL+E1@+bE$} z7Gf$_`<)B)vxy^nk!P2AL1l?_IplaHG}=>?H#p8UHuJgWX=`Aj_AMswxy zMo$Zcom*#vCVKdPQOwzs$utqsnyj{d10nqzuCVppZDogJ&X7f4=u zwS$%L&S=MC=yClnvMY=1V{(~a8UabjZ6l?x!ozQ~y<9W}!gR3afFHUFHp=TR^p1f4 z4T`v(Ap{)Ov6Y?x_NS@LUKKC7^G`T=RKKd zZH<_nocl#budx(Frh&k8$B3v~6@03bLG_Z)2@_tLLPgqUy?y7{EzI5fPb(Gdxyf^} z&31nt`f{9hCds5jZEW<}LNWP6%s5rUygZ6oq?d8cyDM#VL+H>@pO%8jW>L*2_QhHC zthKw=Qq&A{>aHv=te-x)G^e@6@#Un0k#$3}Zf2W6*h%;J?4N!|2g}9n(DX!La=K_< z)}(ebgJr>D(6(GJ1b5TdZ^?ReFi}LD_FSF@d|=HFbqiyFKpEgc6q* z+(;Zbbx1vSI9oX0r?H)J<VbDrK*NQ_6qM-1>r0qiFe^@!cco#p+XT^oFtSiEJY6Wb2a+b0>{A-=2PV8;^;8 ztUw;^la{ZqWUoTLoDxMc=u#~ycNDsWOuC*>{rKMN2vycOCZyak^w&*Y$DUDcGlwA# z>20x#inTOW+so8Rfh6&D2#TFrx-h1{+f+00vat5e)RryeGZsE2YW}a@zG{5!Oz;rJ U-TA)gS&afNcQ;Q)6+I;7U-iq)g8%>k literal 0 HcmV?d00001 diff --git a/apps/sim/app/api/credential-groups/oauth-callback.test.ts b/apps/sim/app/api/credential-groups/oauth-callback.test.ts new file mode 100644 index 00000000000..cca93d03e1d --- /dev/null +++ b/apps/sim/app/api/credential-groups/oauth-callback.test.ts @@ -0,0 +1,137 @@ +/** @vitest-environment node */ +import { NextRequest } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import { CredentialGroupOAuthError } from '@/lib/credential-groups/provider-adapter' +import { OAuthIdentityVerificationError } from '@/lib/oauth/identity-error' + +const mocks = vi.hoisted(() => ({ + authenticate: vi.fn(), + completeOAuth: vi.fn(), + consumeAttempt: vi.fn(), + logError: vi.fn(), +})) + +vi.mock('@sim/logger', () => ({ + createLogger: () => ({ error: mocks.logError }), +})) +vi.mock('@/lib/credential-groups/application/enrollment-auth', () => ({ + credentialGroupOAuthAttemptPrincipal: mocks.authenticate, +})) +vi.mock('@/lib/credential-groups/application/public-enrollment', () => ({ + completePublicCredentialGroupOAuth: { execute: mocks.completeOAuth }, +})) +vi.mock('@/lib/credential-groups/oauth-state', () => ({ + consumeCredentialGroupOAuthAttempt: mocks.consumeAttempt, +})) + +import { handleCredentialGroupOAuthCallback } from '@/app/api/credential-groups/oauth-callback' + +const completionId = '550e8400-e29b-41d4-a716-446655440000' +const attempt = { + provider: 'github-repositories', + invitationToken: 'invitation-token', + optionId: 'option-1', + returnTo: 'search', +} as const + +function completeCallback() { + return handleCredentialGroupOAuthCallback({ + request: new NextRequest( + 'https://sim.test/api/auth/oauth2/callback/github-repositories?state=cg_state&code=code-1' + ), + provider: 'github-repositories', + query: { state: 'cg_state', code: 'code-1' }, + limited: null, + }) +} + +describe('GitHub managed OAuth failure presentation', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.authenticate.mockResolvedValue({ kind: 'credential_group_enrollment' }) + }) + + describe.each([false, true])('completion redirect: %s', (completionRedirect) => { + it.each([ + { + failure: new OAuthIdentityVerificationError('email_mismatch', 'emails'), + status: 'github_email_mismatch', + }, + { + failure: new OAuthIdentityVerificationError('email_access_denied', 'emails', 403), + status: 'github_email_access_denied', + }, + { + failure: new OAuthIdentityVerificationError('provider_unavailable', 'profile', 503), + status: 'provider_unavailable', + }, + { + failure: new OAuthIdentityVerificationError('rate_limited', 'emails', 403), + status: 'rate_limited', + }, + { + failure: new OAuthIdentityVerificationError('invalid_response', 'emails'), + status: 'provider_unavailable', + }, + { + failure: new OAuthIdentityVerificationError('provider_rejected', 'profile', 401), + status: 'failed', + }, + ])('presents $status and logs only safe diagnostics', async ({ failure, status }) => { + mocks.consumeAttempt.mockResolvedValue({ ...attempt, completionRedirect, completionId }) + mocks.completeOAuth.mockRejectedValueOnce( + new CredentialGroupOAuthError('Private details: member@example.com ghu_token', 502, failure) + ) + const response = await completeCallback() + const location = new URL(response.headers.get('location')!, 'https://sim.test') + expect(location.searchParams.get('oauth')).toBe(status) + if (completionRedirect) { + expect(location.pathname).toBe('/credential-groups/complete') + expect(location.searchParams.get('completionId')).toBe(completionId) + } else { + expect(location.pathname).toBe('/credential-groups/enroll/invitation-token') + expect(location.searchParams.get('optionId')).toBe(attempt.optionId) + expect(location.searchParams.get('returnTo')).toBe('search') + } + expect(mocks.logError).toHaveBeenCalledExactlyOnceWith('Managed OAuth authorization failed', { + provider: 'github-repositories', + failure: status, + errorClass: 'credential_group_oauth', + statusCode: 502, + identityReason: failure.reason, + identityStage: failure.stage, + providerStatus: failure.httpStatus, + }) + expect(response.headers.get('location')).not.toContain('member@example.com') + expect(response.headers.get('location')).not.toContain('ghu_token') + }) + }) + + it('does not infer an identity failure from an untyped provider exception', async () => { + mocks.consumeAttempt.mockResolvedValue(attempt) + mocks.completeOAuth.mockRejectedValueOnce(new Error('member@example.com ghu_token')) + const response = await completeCallback() + expect(response.headers.get('location')).toContain('oauth=failed') + expect(mocks.logError).toHaveBeenCalledExactlyOnceWith('Managed OAuth authorization failed', { + provider: 'github-repositories', + failure: 'failed', + errorClass: 'unexpected', + }) + }) + + it('retains an application error classification without its private details', async () => { + mocks.consumeAttempt.mockResolvedValue(attempt) + mocks.completeOAuth.mockRejectedValueOnce( + new OrchestrationError('forbidden', 'Private details: member@example.com') + ) + await completeCallback() + expect(mocks.logError).toHaveBeenCalledExactlyOnceWith('Managed OAuth authorization failed', { + provider: 'github-repositories', + failure: 'failed', + errorClass: 'application', + applicationCode: 'forbidden', + statusCode: 403, + }) + }) +}) diff --git a/apps/sim/app/api/credential-groups/oauth-callback.ts b/apps/sim/app/api/credential-groups/oauth-callback.ts index aa565c8be62..d040b135edb 100644 --- a/apps/sim/app/api/credential-groups/oauth-callback.ts +++ b/apps/sim/app/api/credential-groups/oauth-callback.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import type { NextRequest, NextResponse } from 'next/server' import type { CredentialGroupOAuthCallbackQuery } from '@/lib/api/contracts/credential-groups' +import { asOrchestrationError, statusForOrchestrationError } from '@/lib/core/orchestration/types' import { credentialGroupOAuthAttemptPrincipal } from '@/lib/credential-groups/application/enrollment-auth' import { completePublicCredentialGroupOAuth } from '@/lib/credential-groups/application/public-enrollment' import { CredentialGroupOAuthStateVersionError } from '@/lib/credential-groups/oauth-attempt-version' @@ -10,6 +11,7 @@ import { consumeCredentialGroupOAuthAttempt } from '@/lib/credential-groups/oaut import { CredentialGroupInvitationUnavailableError, CredentialGroupOAuthError, + CredentialGroupProviderConfigurationError, } from '@/lib/credential-groups/provider-adapter' import type { CredentialGroupProvider } from '@/lib/credential-groups/providers' import { @@ -80,11 +82,9 @@ export async function handleCredentialGroupOAuthCallback({ connected: attempt.optionId, }) } catch (error) { - logger.error('Managed OAuth authorization failed', { - provider, - error: getErrorMessage(error), - }) - const status = + const identityFailure = + error instanceof CredentialGroupOAuthError ? error.identityFailure : undefined + let status: CredentialGroupOAuthFailure = error instanceof CredentialGroupInvitationUnavailableError ? 'unavailable' : error instanceof CredentialGroupOAuthError && error.statusCode === 403 @@ -94,6 +94,52 @@ export async function handleCredentialGroupOAuthCallback({ : error instanceof CredentialGroupOAuthError && error.statusCode === 409 ? 'configuration_changed' : 'failed' + if (identityFailure) { + switch (identityFailure.reason) { + case 'email_mismatch': + status = provider === 'github-repositories' ? 'github_email_mismatch' : 'account_mismatch' + break + case 'email_access_denied': + status = + provider === 'github-repositories' + ? 'github_email_access_denied' + : 'permissions_required' + break + case 'rate_limited': + status = 'rate_limited' + break + case 'provider_unavailable': + case 'invalid_response': + status = 'provider_unavailable' + break + } + } + const applicationError = asOrchestrationError(error) + logger.error('Managed OAuth authorization failed', { + provider, + failure: status, + errorClass: + error instanceof CredentialGroupInvitationUnavailableError + ? 'invitation_unavailable' + : error instanceof CredentialGroupOAuthError + ? 'credential_group_oauth' + : error instanceof CredentialGroupProviderConfigurationError + ? 'provider_configuration' + : applicationError + ? 'application' + : 'unexpected', + ...(error instanceof CredentialGroupOAuthError && { statusCode: error.statusCode }), + ...(error instanceof CredentialGroupProviderConfigurationError && { statusCode: 503 }), + ...(applicationError && { + applicationCode: applicationError.code, + statusCode: statusForOrchestrationError(applicationError.code), + }), + ...(identityFailure && { + identityReason: identityFailure.reason, + identityStage: identityFailure.stage, + providerStatus: identityFailure.httpStatus, + }), + }) return failureRedirect(status) } } diff --git a/apps/sim/app/credential-groups/enroll/[token]/page.test.tsx b/apps/sim/app/credential-groups/enroll/[token]/page.test.tsx index 970dcd47e6b..2ef014a73a6 100644 --- a/apps/sim/app/credential-groups/enroll/[token]/page.test.tsx +++ b/apps/sim/app/credential-groups/enroll/[token]/page.test.tsx @@ -306,6 +306,20 @@ describe('focused Search enrollment', () => { expect(oauthLinks()[0]?.getAttribute('href')).toContain('/site-two?returnTo=search') }) + it.each([ + ['github_email_mismatch', 'add and verify the email address'], + ['github_email_access_denied', 'Email addresses: Read-only permission'], + ['provider_unavailable', 'Try connecting again in a few minutes'], + ])( + 'renders recovery guidance for %s without treating it as a connection', + async (oauth, message) => { + await render({ returnTo: 'search', optionId: 'site-two', oauth }) + expect(document.querySelector('[role="status"]')?.textContent).toContain(message) + expect(oauthLinks()[0]?.textContent).toBe('Connect') + expect(document.body.textContent).toContain('Not connected') + } + ) + it('does not resolve enrollment metadata or trust a return workspace after authentication fails', async () => { mocks.authenticate.mockResolvedValue(null) await render({ returnTo: 'search', optionId: 'site-two', workspaceId: 'other-workspace' }) diff --git a/apps/sim/app/credential-groups/enroll/[token]/page.tsx b/apps/sim/app/credential-groups/enroll/[token]/page.tsx index 63423f76409..c819e8c122a 100644 --- a/apps/sim/app/credential-groups/enroll/[token]/page.tsx +++ b/apps/sim/app/credential-groups/enroll/[token]/page.tsx @@ -11,6 +11,7 @@ import { authenticateCredentialGroupEnrollment } from '@/lib/credential-groups/a import { readPublicCredentialGroupEnrollment } from '@/lib/credential-groups/application/public-enrollment' import { CredentialGroupEnrollmentError } from '@/lib/credential-groups/enrollments' import { getManagedMcpConnectorIcon } from '@/lib/credential-groups/managed-mcp-connector-icons' +import { CREDENTIAL_GROUP_OAUTH_FAILURE_MESSAGES } from '@/lib/credential-groups/oauth-completion' import { CredentialGroupProviderConfigurationError } from '@/lib/credential-groups/provider-adapter' import { getCredentialGroupProviderService } from '@/lib/credential-groups/providers' import { enforcePublicCredentialGroupIpRateLimit } from '@/lib/credential-groups/rate-limit' @@ -107,11 +108,11 @@ function UnavailableSearchConnection({ } const OAUTH_MESSAGES = { + ...CREDENTIAL_GROUP_OAUTH_FAILURE_MESSAGES, denied: 'Authorization was canceled. Nothing was connected.', account_mismatch: 'Choose the account matching the email address on this invitation.', permissions_required: 'All requested permissions are required to connect this account.', configuration_changed: 'This credential option changed. Reload the page and try again.', - rate_limited: 'Too many authorization attempts. Wait a few minutes and try again.', unavailable: 'Account authorization is temporarily unavailable. Please try again.', failed: 'Account authorization did not complete. Please try again.', } as const diff --git a/apps/sim/app/o/[organizationId]/integrations/connect-account-options.tsx b/apps/sim/app/o/[organizationId]/integrations/connect-account-options.tsx index 8f2482d1b30..9fd17e220e4 100644 --- a/apps/sim/app/o/[organizationId]/integrations/connect-account-options.tsx +++ b/apps/sim/app/o/[organizationId]/integrations/connect-account-options.tsx @@ -8,6 +8,7 @@ import { getConnectorAccessAvailability, SEARCH_CONNECTORS, } from '@/lib/sim-search/connectors' +import { DisconnectAccountMenu } from '@/app/o/[organizationId]/integrations/disconnect-account-menu' import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider' import { SourceSetupModal } from '@/app/workspace/[workspaceId]/home/components/search-sources/source-setup-modal' import { IntegrationTile } from '@/app/workspace/[workspaceId]/integrations/components/integrations-showcase' @@ -22,7 +23,12 @@ import { SettingsResourceRow, } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' import { useSearchSourceOverview, useSearchSources } from '@/hooks/queries/kb/connectors' -import { organizationAccountsKeys } from '@/hooks/queries/organization-accounts' +import { + organizationAccountsKeys, + useConnectOrganizationAccount, + useOrganizationAccounts, + useReconnectPersonalOrganizationAccount, +} from '@/hooks/queries/organization-accounts' import { usePersonalSearchIntegrations } from '@/hooks/queries/personal-search-integrations' import { useSearchIntegrations } from '@/hooks/queries/search-integrations' import { searchSourceKeys } from '@/hooks/queries/utils/search-source-keys' @@ -41,9 +47,38 @@ export function ConnectAccountOptions({ }: ConnectAccountOptionsProps = {}) { const { organization, searchAccess } = useOrganizationContext() const scope: ResourceScope = { kind: 'organization', organizationId: organization.id } - const sources = useSearchSources(scope, { search }) + const organizationAccounts = useOrganizationAccounts(organization.id) + const hasGitHubAccountInventory = organizationAccounts.data?.viewerAccounts !== undefined + const sources = useSearchSources(scope, { + search, + ...(hasGitHubAccountInventory ? { excludeConnectorType: 'github' } : {}), + }) const overview = useSearchSourceOverview(scope) const integrations = useSearchIntegrations(organization.id) + const githubConfigured = + hasGitHubAccountInventory && + overview.data?.providers.some((provider) => provider.connectorType === 'github') === true + const connectOrganizationAccount = useConnectOrganizationAccount() + const reconnectAccount = useReconnectPersonalOrganizationAccount() + const githubAccounts = + organizationAccounts.data?.viewerAccounts?.filter( + (account) => account.providerId === 'github-repositories' + ) ?? [] + const githubAccount = + githubAccounts.find((account) => account.status === 'needs_reauth') ?? githubAccounts[0] + const showGitHubAccount = + Boolean(githubAccount) && + ('github'.includes(search.toLowerCase()) || + githubAccounts.some((account) => + account.displayName.toLowerCase().includes(search.toLowerCase()) + )) + const githubConnector = SEARCH_CONNECTORS.find((connector) => connector.type === 'github') + const githubOption = + organizationAccounts.data?.credentialGroup?.status === 'active' + ? organizationAccounts.data.credentialGroup.options.find( + (option) => option.provider === 'github-repositories' && option.status === 'active' + ) + : undefined const slackInventory = usePersonalSearchIntegrations({ organizationId: organization.id, connectorType: 'slack', @@ -78,6 +113,7 @@ export function ConnectAccountOptions({ const visibleSources = sources.data?.filter( (source) => + (!hasGitHubAccountInventory || source.connectorType !== 'github') && source.connectionRequired && source.enabled && source.approved !== false && @@ -99,6 +135,7 @@ export function ConnectAccountOptions({ ) const sourceChoices = SEARCH_CONNECTORS.filter((connector) => { if ( + (connector.type === 'github' && githubAccounts.length > 0) || (connector.type === 'slack' && !canConnectSharedSlack) || !approvedTypes.has(connector.type) || !connector.meta.name.toLowerCase().includes(search.toLowerCase()) || @@ -135,10 +172,57 @@ export function ConnectAccountOptions({ ? integrations : slackInventory.isError ? slackInventory - : null + : organizationAccounts.isError + ? organizationAccounts + : null + + const connectGitHub = () => { + if (!githubOption || !githubConfigured) return + connectOrganizationAccount.mutate( + { organizationId: organization.id, optionId: githubOption.id }, + { + onSuccess: ({ authorizationUrl, invitationLink }) => + window.location.assign(authorizationUrl ?? invitationLink), + onError: (error) => toast.error(error.message), + } + ) + } return ( <> + {showGitHubAccount && githubAccount && githubConnector && ( + } + title='GitHub' + description={`${githubAccounts.map((account) => account.displayName).join(', ')} · ${githubAccount.status === 'needs_reauth' ? 'Reconnect required' : 'Connected'}`} + trailing={ +
+ {githubAccount.status === 'needs_reauth' && + githubOption && + githubOption.id === githubAccount.optionId && ( + + reconnectAccount.mutate(githubAccount.credentialId, { + onSuccess: ({ authorizationUrl, invitationLink }) => + window.location.assign(authorizationUrl ?? invitationLink), + onError: (error) => toast.error(error.message), + }) + } + > + Reconnect + + )} + +
+ } + /> + )}
{failedQuery ? ( Loading sources… ) : visibleSources.length > 0 || sourceChoices.length > 0 || sources.hasNextPage ? ( <> @@ -196,15 +281,27 @@ export function ConnectAccountOptions({ icon={} title={meta.name} description={ - hasSources - ? 'Connect a different site or content scope' - : 'Connect your account to search this source' + type === 'github' && githubConfigured + ? githubOption + ? 'Connect once to search the repositories your admin adds' + : 'An admin needs to reconnect GitHub' + : hasSources + ? 'Connect a different site or content scope' + : 'Connect your account to search this source' } trailing={ enrollment.connectSearchSource(scope, connector, undefined)} + disabled={ + enrollment.isPending || + connectOrganizationAccount.isPending || + (type === 'github' && githubConfigured && !githubOption) + } + onClick={() => + type === 'github' && githubConfigured + ? connectGitHub() + : enrollment.connectSearchSource(scope, connector, undefined) + } > Connect @@ -214,7 +311,7 @@ export function ConnectAccountOptions({ })} - ) : showEmpty ? ( + ) : showEmpty && !showGitHubAccount ? ( {search ? 'No matching integrations.' : 'No integrations are available to connect.'} diff --git a/apps/sim/app/o/[organizationId]/integrations/integrations.test.tsx b/apps/sim/app/o/[organizationId]/integrations/integrations.test.tsx index f84183c69e4..50f53795943 100644 --- a/apps/sim/app/o/[organizationId]/integrations/integrations.test.tsx +++ b/apps/sim/app/o/[organizationId]/integrations/integrations.test.tsx @@ -4,6 +4,7 @@ import { toast } from '@sim/emcn' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { SearchSourceSummary } from '@/lib/api/contracts/knowledge/connectors' +import type { OrganizationAccountConnectionResponse } from '@/lib/api/contracts/organization-accounts' import { SEARCH_CONNECTORS, type SearchConnector } from '@/lib/sim-search/connectors' import { SEARCH_DEBOUNCE_MS } from '@/lib/url-state' @@ -20,6 +21,23 @@ const mocks = vi.hoisted(() => ({ enrollment: vi.fn(), enrollmentError: null as string | null, setupConnector: null as SearchConnector | null, + organizationAccounts: vi.fn(), + connectOrganizationAccount: vi.fn(), + reconnectOrganizationAccount: vi.fn(), +})) + +vi.mock('@/hooks/queries/organization-accounts', () => ({ + organizationAccountsKeys: { detail: (id: string) => ['organization-accounts', 'detail', id] }, + useOrganizationAccounts: mocks.organizationAccounts, + useConnectOrganizationAccount: () => ({ + mutate: mocks.connectOrganizationAccount, + isPending: false, + }), + useDisconnectPersonalOrganizationAccount: () => ({ mutate: vi.fn(), isPending: false }), + useReconnectPersonalOrganizationAccount: () => ({ + mutate: mocks.reconnectOrganizationAccount, + isPending: false, + }), })) vi.mock('@/app/o/[organizationId]/integrations/slack-search-actions', () => ({ @@ -128,6 +146,10 @@ describe('organization integrations role and source paths', () => { vi.spyOn(toast, 'error').mockReturnValue('toast-id') mocks.enrollmentError = null mocks.setupConnector = null + mocks.organizationAccounts.mockReturnValue({ + data: { credentialGroup: null, viewerAccounts: [] }, + isPending: false, + }) vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true) mocks.context.mockReturnValue({ organization: { id: scope.organizationId }, @@ -182,6 +204,22 @@ describe('organization integrations role and source paths', () => { ) } + function expectConnectionRedirect( + onSuccess: (response: OrganizationAccountConnectionResponse) => void, + authorizationUrl?: string + ) { + const invitationLink = 'https://sim.test/credential-groups/enroll/fixture-token' + const assign = vi.fn() + const browserWindow = window + vi.stubGlobal('window', { location: { assign } }) + try { + onSuccess({ invitationLink, ...(authorizationUrl ? { authorizationUrl } : {}) }) + expect(assign).toHaveBeenCalledExactlyOnceWith(authorizationUrl ?? invitationLink) + } finally { + vi.stubGlobal('window', browserWindow) + } + } + it.each([OrganizationIntegrations, ConnectAccountOptions])( 'shows connection errors in a toast without adding inline error text in %s', async (Component) => { @@ -211,7 +249,10 @@ describe('organization integrations role and source paths', () => { it('uses the actual organization and only asks members to connect identity-dependent sources', async () => { await render() - expect(mocks.sources).toHaveBeenCalledWith(scope, { search: '' }) + expect(mocks.sources).toHaveBeenCalledWith(scope, { + search: '', + excludeConnectorType: 'github', + }) expect(buttons('Add source')).toHaveLength(0) expect(buttons('Manage')).toHaveLength(0) expect(buttons('Connect')).toHaveLength(1) @@ -241,9 +282,17 @@ describe('organization integrations role and source paths', () => { await act(async () => root.render()) mocks.filters.mockReturnValue({ tab: 'all', search: ' drive ', setSearch: vi.fn() }) await act(async () => root.render()) - expect(mocks.sources).toHaveBeenCalledWith(scope, { search: '', mine: true }) + expect(mocks.sources).toHaveBeenCalledWith(scope, { + search: '', + mine: true, + excludeConnectorType: 'github', + }) await act(async () => vi.advanceTimersByTime(SEARCH_DEBOUNCE_MS)) - expect(mocks.sources).toHaveBeenCalledWith(scope, { search: 'drive', mine: true }) + expect(mocks.sources).toHaveBeenCalledWith(scope, { + search: 'drive', + mine: true, + excludeConnectorType: 'github', + }) }) it('refreshes organization Accounts after either direct connection flow completes', async () => { @@ -307,6 +356,227 @@ describe('organization integrations role and source paths', () => { ) }) + it.each([ + undefined, + 'https://sim.test/api/credential-groups/enroll/fixture-token/oauth/github-option?returnTo=search', + ])('connects GitHub once with compatible redirect %s', async (authorizationUrl) => { + mocks.sources.mockReturnValue({ + data: [ + { ...memberSource, connectorType: 'github', connectorId: 'repo-one' }, + { ...memberSource, connectorType: 'github', connectorId: 'repo-two' }, + ], + isPending: false, + }) + mocks.overview.mockReturnValue({ + data: { providers: [{ connectorType: 'github' }] }, + isPending: false, + }) + mocks.integrations.mockReturnValue({ + data: [{ connectorType: 'github', approved: true }], + isPending: false, + }) + mocks.availability.mockReturnValue({ + integrationAvailability: new Map(), + oauthServiceAvailability: new Map([['github-repositories', true]]), + isIntegrationAvailabilityReady: true, + }) + mocks.organizationAccounts.mockReturnValue({ + data: { + viewerAccounts: [], + credentialGroup: { + status: 'active', + options: [{ id: 'github-option', provider: 'github-repositories', status: 'active' }], + }, + }, + isPending: false, + }) + await render() + expect(buttons('Connect')).toHaveLength(1) + expect(document.body.textContent).toContain('Connect once') + await act(async () => buttons('Connect')[0].click()) + expect(mocks.connectOrganizationAccount).toHaveBeenCalledExactlyOnceWith( + { organizationId: scope.organizationId, optionId: 'github-option' }, + expect.any(Object) + ) + expect(mocks.connect).not.toHaveBeenCalled() + expect(document.querySelector('[role="dialog"]')).toBeNull() + expectConnectionRedirect( + mocks.connectOrganizationAccount.mock.calls[0][1].onSuccess, + authorizationUrl + ) + }) + + it('does not ask an already connected GitHub member to connect a future repository', async () => { + mocks.organizationAccounts.mockReturnValue({ + data: { + viewerAccounts: [ + { + credentialId: 'github-account', + providerId: 'github-repositories', + displayName: 'My GitHub', + status: 'active', + }, + ], + }, + isPending: false, + }) + mocks.sources.mockReturnValue({ + data: [ + { ...memberSource, connectorType: 'github', viewerMembership: 'connected' }, + { ...memberSource, connectorType: 'github', connectorId: 'future-repo' }, + ], + isPending: false, + }) + mocks.overview.mockReturnValue({ + data: { providers: [{ connectorType: 'github' }] }, + isPending: false, + }) + mocks.integrations.mockReturnValue({ + data: [{ connectorType: 'github', approved: true }], + isPending: false, + }) + await render() + expect(buttons('Connect')).toHaveLength(0) + expect(document.body.textContent).not.toContain('Connect a different site') + }) + + it.each(['active', 'needs_reauth'])( + 'keeps one GitHub account row with management when source pages omit it and provider is disabled: %s', + async (status) => { + mocks.sources.mockReturnValue({ data: [], isPending: false, hasNextPage: true }) + mocks.overview.mockReturnValue({ data: { providers: [] }, isPending: false }) + mocks.organizationAccounts.mockReturnValue({ + data: { + credentialGroup: { + status: 'active', + options: [{ id: 'github-option', provider: 'github-repositories', status: 'disabled' }], + }, + viewerAccounts: [ + { + credentialId: 'github-account', + providerId: 'github-repositories', + displayName: 'My GitHub', + status, + }, + ], + }, + isPending: false, + }) + await act(async () => root.render()) + expect(buttons('Connect')).toHaveLength(0) + expect(buttons('Reconnect')).toHaveLength(0) + expect(document.querySelectorAll('[aria-label="GitHub account actions"]')).toHaveLength(1) + expect(mocks.reconnectOrganizationAccount).not.toHaveBeenCalled() + } + ) + + it.each([ + undefined, + 'https://sim.test/api/credential-groups/enroll/fixture-token/oauth/github-option?returnTo=accounts', + ])( + 'allows personal reauthorization with compatible redirect %s while Search is disabled', + async (authorizationUrl) => { + mocks.sources.mockReturnValue({ data: [], isPending: false }) + mocks.overview.mockReturnValue({ data: { providers: [] }, isPending: false }) + mocks.integrations.mockReturnValue({ + data: [{ connectorType: 'github', approved: false }], + isPending: false, + }) + mocks.organizationAccounts.mockReturnValue({ + data: { + credentialGroup: { + status: 'active', + options: [{ id: 'github-option', provider: 'github-repositories', status: 'active' }], + }, + viewerAccounts: [ + { + credentialId: 'github-account', + optionId: 'github-option', + providerId: 'github-repositories', + displayName: 'My GitHub', + status: 'needs_reauth', + }, + ], + }, + isPending: false, + }) + await act(async () => root.render()) + expect(buttons('Connect')).toHaveLength(0) + expect(buttons('Reconnect')).toHaveLength(1) + await act(async () => buttons('Reconnect')[0].click()) + expect(mocks.reconnectOrganizationAccount).toHaveBeenCalledWith( + 'github-account', + expect.any(Object) + ) + expectConnectionRedirect( + mocks.reconnectOrganizationAccount.mock.calls[0][1].onSuccess, + authorizationUrl + ) + } + ) + + it('preserves legacy GitHub account management when an older server omits the inventory', async () => { + mocks.organizationAccounts.mockReturnValue({ + data: { credentialGroup: null }, + isPending: false, + }) + mocks.sources.mockReturnValue({ + data: [ + { + ...memberSource, + connectorType: 'github', + viewerMembership: 'connected', + viewerAccounts: [ + { + credentialId: 'github-account', + groupId: 'group', + optionId: 'option', + displayName: 'My GitHub', + status: 'active', + }, + ], + }, + ], + isPending: false, + }) + await act(async () => root.render()) + expect(document.querySelectorAll('[aria-label="GitHub account actions"]')).toHaveLength(1) + expect( + mocks.sources.mock.calls.every(([, input]) => input.excludeConnectorType === undefined) + ).toBe(true) + }) + + it('renders one GitHub account instead of repeating it for each configured repository', async () => { + mocks.sources.mockReturnValue({ + data: ['private-one', 'private-two'].map((connectorId) => ({ + ...memberSource, + connectorId, + connectorType: 'github', + sourceDescription: connectorId, + viewerMembership: 'connected', + })), + isPending: false, + }) + mocks.organizationAccounts.mockReturnValue({ + data: { + viewerAccounts: [ + { + credentialId: 'github-account', + providerId: 'github-repositories', + displayName: 'My GitHub', + status: 'active', + }, + ], + }, + isPending: false, + }) + await act(async () => root.render()) + expect(buttons('Reconnect')).toHaveLength(0) + expect(document.querySelectorAll('[aria-label="GitHub account actions"]')).toHaveLength(1) + expect(document.body.textContent).not.toContain('private-one') + expect(document.body.textContent).not.toContain('private-two') + }) + it('keeps configured sources in alphabetical order with approved providers', async () => { mocks.sources.mockReturnValue({ data: [memberSource], isPending: false }) mocks.integrations.mockReturnValue({ @@ -462,7 +732,11 @@ describe('organization integrations role and source paths', () => { }) mocks.overview.mockReturnValue({ data: { providers: [] }, isPending: false }) await act(async () => root.render()) - expect(mocks.sources).toHaveBeenCalledWith(scope, { search: '', mine: true }) + expect(mocks.sources).toHaveBeenCalledWith(scope, { + search: '', + mine: true, + excludeConnectorType: 'github', + }) expect(document.body.textContent).toContain('Gmail') expect(document.querySelector('[role="dialog"]')).toBeNull() expect(buttons('Connect account')).toHaveLength(0) @@ -487,7 +761,10 @@ describe('organization integrations role and source paths', () => { await act(async () => root.render()) expect(document.body.textContent).toContain('Gmail') expect(document.body.textContent).not.toContain('Jira') - expect(mocks.sources).toHaveBeenCalledWith(scope, { search: 'gmail' }) + expect(mocks.sources).toHaveBeenCalledWith(scope, { + search: 'gmail', + excludeConnectorType: 'github', + }) }) it('lets the viewer reconnect their own expired account from the main page', async () => { diff --git a/apps/sim/app/o/[organizationId]/integrations/integrations.tsx b/apps/sim/app/o/[organizationId]/integrations/integrations.tsx index da715657d45..2aac5745524 100644 --- a/apps/sim/app/o/[organizationId]/integrations/integrations.tsx +++ b/apps/sim/app/o/[organizationId]/integrations/integrations.tsx @@ -18,7 +18,10 @@ import { SearchSourceRow } from '@/app/workspace/[workspaceId]/search/components import { SettingsQueryErrorState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import { RESOURCE_LIST_STACK } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' import { useSearchSources } from '@/hooks/queries/kb/connectors' -import { organizationAccountsKeys } from '@/hooks/queries/organization-accounts' +import { + organizationAccountsKeys, + useOrganizationAccounts, +} from '@/hooks/queries/organization-accounts' import { searchSourceKeys } from '@/hooks/queries/utils/search-source-keys' import { useDebounce } from '@/hooks/use-debounce' import { useMemberEnrollment } from '@/hooks/use-member-enrollment' @@ -40,7 +43,13 @@ export function OrganizationIntegrations({ const scope: ResourceScope = { kind: 'organization', organizationId: organization.id } const { search } = useOrganizationPageFilters() const sourceSearch = useDebounce(search.trim(), SEARCH_DEBOUNCE_MS) - const sources = useSearchSources(scope, { search: sourceSearch, mine: true }) + const organizationAccounts = useOrganizationAccounts(organization.id) + const hasGitHubAccountInventory = organizationAccounts.data?.viewerAccounts !== undefined + const sources = useSearchSources(scope, { + search: sourceSearch, + mine: true, + ...(hasGitHubAccountInventory ? { excludeConnectorType: 'github' } : {}), + }) const membershipQueryKeys = useMemo( () => [ searchSourceKeys.list({ kind: 'organization', organizationId: organization.id }), @@ -94,32 +103,34 @@ export function OrganizationIntegrations({ /> ) : !sources.isPending && (sources.data?.length || sources.hasNextPage) ? ( <> - {sources.data?.map((source) => ( - - ) : undefined - } - available={ - source.accessMode === 'members' - ? searchAccess.memberScoped - : searchAccess.sourceMirrored && - (!source.connectionRequired || searchAccess.memberScoped) - } - waiting={enrollment.isAwaiting(source.connectorId)} - isPending={enrollment.isPending} - onConnect={() => enrollment.connect(source.knowledgeBaseId, source.connectorId)} - /> - ))} + {sources.data + ?.filter((source) => !hasGitHubAccountInventory || source.connectorType !== 'github') + .map((source) => ( + + ) : undefined + } + available={ + source.accessMode === 'members' + ? searchAccess.memberScoped + : searchAccess.sourceMirrored && + (!source.connectionRequired || searchAccess.memberScoped) + } + waiting={enrollment.isAwaiting(source.connectorId)} + isPending={enrollment.isPending} + onConnect={() => enrollment.connect(source.knowledgeBaseId, source.connectorId)} + /> + ))} ) : null} diff --git a/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.test.tsx b/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.test.tsx index 594914fb52f..4d77fb01677 100644 --- a/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.test.tsx +++ b/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.test.tsx @@ -66,6 +66,7 @@ vi.mock('@/connectors/registry', () => ({ auth: { mode: 'oauth', provider: 'google-calendar', adminCredentialType: 'service_account' }, }, slack: { name: 'Slack', auth: { mode: 'oauth', provider: 'slack' } }, + github: { name: 'GitHub', auth: { mode: 'oauth', provider: 'github-repositories' } }, gitlab: { name: 'GitLab', auth: { mode: 'apiKey' } }, }, })) @@ -221,6 +222,29 @@ describe('organization provider management', () => { }) } + it.each(['members', 'admin'])( + 'shows GitHub repositories without an account method label for %s access', + async (accessMode) => { + mocks.overview.mockReturnValue({ + data: { providers: [{ ...provider, connectorType: 'github', sourceCount: 1 }] }, + }) + mocks.sources.mockReturnValue({ + data: [ + { ...source, connectorType: 'github', sourceDescription: 'acme/platform', accessMode }, + ], + isPending: false, + }) + await render('github') + expect(container.textContent).toContain('acme/platform') + expect(container.textContent).toContain('Last synced') + expect(container.textContent).not.toContain('Member accounts') + expect(container.textContent).not.toContain('Admin or service account') + expect( + container.querySelector('a[href="/o/org-one/settings/integrations/sources/source-one"]') + ).not.toBeNull() + } + ) + it.each(['gmail', 'google_calendar', 'google_drive'])( 'offers configuration setup directly for %s without an Accounts or Advanced tab', async (connectorType) => { diff --git a/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.tsx b/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.tsx index 53a5ca6b797..ecf0c0f62b5 100644 --- a/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.tsx +++ b/apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.tsx @@ -239,12 +239,14 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid key={source.connectorId} title={source.sourceDescription || meta.name} description={[ - source.accessMode === 'members' - ? 'Member accounts' - : meta.auth.mode === 'oauth' && - meta.auth.adminCredentialType === 'service_account' - ? 'Service account' - : 'Admin or service account', + connectorType === 'github' + ? null + : source.accessMode === 'members' + ? 'Member accounts' + : meta.auth.mode === 'oauth' && + meta.auth.adminCredentialType === 'service_account' + ? 'Service account' + : 'Admin or service account', !approved ? 'Deactivated' : !source.enabled @@ -260,7 +262,9 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid : source.lastSyncAt ? `Last synced ${format(new Date(source.lastSyncAt), 'MMM d, h:mm a')}` : 'Waiting for the first sync', - ].join(' · ')} + ] + .filter(Boolean) + .join(' · ')} href={organizationRoutes(organization.id).searchSource(source.connectorId)} clickLabel={`Open ${source.sourceDescription || meta.name}`} navigable diff --git a/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/search-params.ts b/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/search-params.ts index b4e2134e517..b4182204393 100644 --- a/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/search-params.ts +++ b/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/search-params.ts @@ -1,4 +1,5 @@ import { parseAsStringLiteral } from 'nuqs/server' +import { connectorDocumentFilterSchema } from '@/lib/api/contracts/knowledge/connectors' export const sourceViewParam = { key: 'view', @@ -7,7 +8,7 @@ export const sourceViewParam = { export const sourceDocumentFilterParam = { key: 'document-filter', - parser: parseAsStringLiteral(['active', 'excluded', 'failed']).withDefault('active'), + parser: parseAsStringLiteral(connectorDocumentFilterSchema.options).withDefault('active'), } as const export type SourceView = NonNullable> diff --git a/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/source-detail.test.tsx b/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/source-detail.test.tsx index c52d4c75b6d..ef8e8d42311 100644 --- a/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/source-detail.test.tsx +++ b/apps/sim/app/o/[organizationId]/settings/integrations/sources/[connectorId]/source-detail.test.tsx @@ -205,15 +205,20 @@ describe('organization source detail navigation', () => { ) }) - it('restores document search and status from the shared URL', async () => { - await render('?search=notes&document-filter=excluded') - expect(mocks.documents).toHaveBeenLastCalledWith( - expect.objectContaining({ search: 'notes', filter: 'excluded' }) - ) - expect(mocks.documents).toHaveBeenLastCalledWith( - expect.objectContaining({ searchControl: { value: 'notes', onChange: expect.any(Function) } }) - ) - }) + it.each(['excluded', 'failed', 'skipped'])( + 'restores document search and %s status from the shared URL', + async (filter) => { + await render(`?search=notes&document-filter=${filter}`) + expect(mocks.documents).toHaveBeenLastCalledWith( + expect.objectContaining({ search: 'notes', filter }) + ) + expect(mocks.documents).toHaveBeenLastCalledWith( + expect.objectContaining({ + searchControl: { value: 'notes', onChange: expect.any(Function) }, + }) + ) + } + ) it.each(['', '?view=history'])( 'shows a concise incomplete-update notice without provider details at %s', diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx index 1de2b420b96..c8e621a49ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx @@ -20,6 +20,7 @@ import { truncate } from '@sim/utils/string' import { useParams, useRouter } from 'next/navigation' import { useQueryStates } from 'nuqs' import { EmptyState } from '@/components/empty-state/empty-state' +import { getDocumentIndexingStatus } from '@/lib/knowledge/documents/types' import type { ChunkData } from '@/lib/knowledge/types' import { formatTokenCount } from '@/lib/tokenization' import type { @@ -989,7 +990,8 @@ export function Document({ ) const hasDocumentData = documentData !== null - const processingStatus = documentData?.processingStatus + const processingStatus = documentData ? getDocumentIndexingStatus(documentData) : undefined + const processingError = documentData?.processingError const chunkRows: ResourceRow[] = useMemo(() => { /** @@ -1012,6 +1014,8 @@ export function Document({ {processingStatus === 'pending' && 'Document processing pending...'} {processingStatus === 'processing' && 'Document processing in progress...'} {processingStatus === 'failed' && 'Document processing failed'} + {processingStatus === 'skipped' && + (processingError ? `Skipped · ${processingError}` : 'Document skipped')} {!processingStatus && 'Document not ready'}
@@ -1056,7 +1060,7 @@ export function Document({ }, } }) - }, [isCompleted, hasDocumentData, processingStatus, displayChunks, searchQuery]) + }, [isCompleted, hasDocumentData, processingStatus, processingError, displayChunks, searchQuery]) const saveLabel = saveStatus === 'saving' diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index 123316bd61c..61693dfadd0 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -47,7 +47,11 @@ import { getFieldTypeForSlot, KNOWLEDGE_DOCUMENT_PROCESSING_STALE_THRESHOLD_MS, } from '@/lib/knowledge/constants' -import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types' +import { + type DocumentSortField, + getDocumentIndexingStatus, + type SortOrder, +} from '@/lib/knowledge/documents/types' import { type FilterFieldType, getOperatorsForFieldType } from '@/lib/knowledge/filters/types' import type { DocumentData } from '@/lib/knowledge/types' import { captureEvent } from '@/lib/posthog/client' @@ -175,7 +179,7 @@ const AnimatedLoader = ({ className }: { className?: string }) => ( ) const getStatusBadge = (doc: DocumentData) => { - switch (doc.processingStatus) { + switch (getDocumentIndexingStatus(doc)) { case 'pending': return ( @@ -188,6 +192,12 @@ const getStatusBadge = (doc: DocumentData) => { Processing ) + case 'skipped': + return ( + + Skipped + + ) case 'failed': return doc.processingError ? ( @@ -1558,7 +1568,8 @@ export function KnowledgeBase({ : undefined } onRetry={ - contextMenuDocument?.processingStatus === 'failed' && + contextMenuDocument && + getDocumentIndexingStatus(contextMenuDocument) === 'failed' && selectedDocumentCount === 1 && userPermissions.canEdit ? () => handleRetryDocument(contextMenuDocument.id) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.test.tsx index 2f8d57c4367..2d2be5db11d 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.test.tsx @@ -20,7 +20,8 @@ const mocks = vi.hoisted(() => ({ selectionLabels: undefined as SourceSelectionLabels | undefined, canonicalModes: {} as Record, isFieldPopulated: vi.fn(() => true), - credentials: [] as Pick[], + credentials: [] as (Pick & + Partial>)[], credentialsState: 'ready' as 'ready' | 'loading' | 'error', refetchCredentials: vi.fn(), oauthModal: vi.fn(), @@ -191,7 +192,12 @@ vi.mock('@/app/workspace/[workspaceId]/search/components/github-installation-mod + ) + }, +})) vi.mock( '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal', () => ({ @@ -92,12 +104,16 @@ vi.mock( mocks.accessField(props) return null }, - ConnectorContentCredentialField: () => null, + ConnectorContentCredentialField: (props: unknown) => { + mocks.contentField(props) + return null + }, }) ) import { ConnectorSettingsFields } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields' import { confluenceConnectorMeta } from '@/connectors/confluence/meta' +import { githubConnectorMeta } from '@/connectors/github/meta' import { googleDriveConnectorMeta } from '@/connectors/google-drive/meta' function fieldProps(connectorConfig: ConnectorMeta): ConnectorSettingsFieldsProps { @@ -185,6 +201,137 @@ describe('connector settings service-account choices', () => { }) } + const installationProps = (): Partial => ({ + usesGitHubInstallation: true, + access: { accessMode: 'members' }, + credentialId: 'installation-1', + contentCredentialId: 'installation-1', + sourceConfig: { repository: 'acme/platform' }, + needsWorkspaceCredential: false, + accessDirty: false, + accessModeChanged: false, + accessComplete: true, + isFieldVisible: () => true, + }) + + it('shows the GitHub connection and preserves the repository rename field for installation sources', async () => { + mocks.credentials = [ + { + id: 'installation-1', + name: 'acme', + provider: 'github-app-installation', + type: 'service_account', + }, + ] + await render(githubConnectorMeta, installationProps()) + expect(container.textContent).toContain('GitHub') + expect(container.textContent).toContain('acme') + expect(mocks.contentField).not.toHaveBeenCalled() + expect(mocks.accessField).not.toHaveBeenCalled() + expect(container.textContent).not.toContain('Account for browsing') + expect(mocks.configFields).toHaveBeenCalledWith( + expect.objectContaining({ + connectorConfig: githubConnectorMeta, + sourceConfig: { repository: 'acme/platform' }, + }) + ) + }) + + it('rotates only among GitHub installation connections through the existing access operation', async () => { + mocks.credentials = [ + { + id: 'installation-1', + name: 'acme', + provider: 'github-app-installation', + type: 'service_account', + }, + { + id: 'installation-2', + name: 'acme-backup', + provider: 'github-app-installation', + type: 'service_account', + }, + { id: 'legacy-reader', name: 'Personal GitHub', provider: 'github', type: 'oauth' }, + ] + const change = vi.fn() + const apply = vi.fn() + const reset = vi.fn() + const changeAccess = vi.fn() + await render(githubConnectorMeta, { ...installationProps(), onContentCredentialChange: change }) + await openAccountChoices() + expect(document.body.textContent).not.toContain('Personal GitHub') + await choose('acme-backup') + expect(change).toHaveBeenCalledWith('installation-2') + await render(githubConnectorMeta, { + ...installationProps(), + contentCredentialId: 'installation-2', + accessDirty: true, + onApplyAccess: apply, + onResetAccess: reset, + onAccessChange: changeAccess, + }) + const applyButton = Array.from(container.querySelectorAll('button')).find( + (button) => button.textContent === 'Change connection' + )! + await act(async () => applyButton.click()) + expect(apply).toHaveBeenCalledOnce() + expect(changeAccess).not.toHaveBeenCalled() + const cancel = Array.from(container.querySelectorAll('button')).find( + (button) => button.textContent === 'Cancel' + )! + await act(async () => cancel.click()) + expect(reset).toHaveBeenCalledOnce() + }) + + it('offers GitHub recovery when the saved installation is unavailable', async () => { + const change = vi.fn() + await render(githubConnectorMeta, { ...installationProps(), onContentCredentialChange: change }) + const connect = Array.from(container.querySelectorAll('button')).find( + (button) => button.textContent === 'Connect GitHub' + )! + await act(async () => connect.click()) + expect(mocks.installationModal).toHaveBeenCalledWith( + expect.objectContaining({ organizationId: 'org-1' }) + ) + const finish = Array.from(container.querySelectorAll('button')).find( + (button) => button.textContent === 'Finish GitHub connection' + )! + await act(async () => finish.click()) + expect(change).toHaveBeenCalledWith('replacement-installation') + expect(mocks.accessField).not.toHaveBeenCalled() + expect(mocks.contentField).not.toHaveBeenCalled() + }) + + it('keeps the existing re-enable operation available without exposing access modes', async () => { + const apply = vi.fn() + await render(githubConnectorMeta, { + ...installationProps(), + canReenableMemberSync: true, + onApplyAccess: apply, + }) + const reenable = Array.from(container.querySelectorAll('button')).find( + (button) => button.textContent === 'Re-enable sync' + )! + await act(async () => reenable.click()) + expect(apply).toHaveBeenCalledOnce() + expect(mocks.accessField).not.toHaveBeenCalled() + }) + + it.each([ + { isSearchIndex: false }, + { scope: { kind: 'workspace' as const, workspaceId: 'workspace-1' } }, + ])( + 'keeps general knowledge-base and workspace settings on their original path: %j', + async (overrides) => { + await render(githubConnectorMeta, { ...installationProps(), ...overrides }) + expect(mocks.contentField).toHaveBeenCalled() + expect(mocks.accessField).toHaveBeenCalled() + expect(mocks.configFields).toHaveBeenCalledWith( + expect.objectContaining({ connectorConfig: githubConnectorMeta }) + ) + } + ) + it.each([true, false])( 'locks the sync method only for Search settings (%s)', async (isSearchIndex) => { @@ -195,6 +342,29 @@ describe('connector settings service-account choices', () => { } ) + it.each([null, 'dedicated-github-account'])( + 'preserves legacy GitHub member source settings with content account %s', + async (contentCredentialId) => { + await render(githubConnectorMeta, { + access: { accessMode: 'members' }, + contentCredentialId, + sourceConfig: { repository: 'team/docs' }, + needsWorkspaceCredential: false, + isFieldVisible: () => true, + }) + expect(mocks.contentField).toHaveBeenLastCalledWith( + expect.objectContaining({ credentialId: contentCredentialId }) + ) + expect(mocks.configFields).toHaveBeenCalledWith( + expect.objectContaining({ + sourceConfig: { repository: 'team/docs' }, + connectorConfig: githubConnectorMeta, + }) + ) + expect(document.body.textContent).not.toContain('Connect GitHub') + } + ) + it('keeps a failed availability check actionable before methods are known', async () => { const refetch = vi.fn() await render(confluenceConnectorMeta, { diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx index 665029c9d65..5b73b89f22d 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx @@ -18,6 +18,7 @@ import { getServiceAccountProviderForProviderId, type OAuthProvider, } from '@/lib/oauth' +import { GITHUB_INSTALLATION_PROVIDER_ID } from '@/lib/oauth/github-installation-types' import type { SourceSelectionLabel, SourceSelectionLabels } from '@/lib/sim-search/source-identity' import { ConnectServiceAccountModal, @@ -42,8 +43,10 @@ import type { ConfigFieldMap, ConfigFieldValue, } from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields' +import { GitHubInstallationModal } from '@/app/workspace/[workspaceId]/search/components/github-installation-modal' import { SettingsQueryErrorState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import { isConnectorCredentialTypeAllowed } from '@/connectors/auth' +import { GitHubInstallationConnectionField } from '@/connectors/github/installation-connection-field' import { GitLabPermissionTabs, GitLabPermissionUploads, @@ -70,6 +73,7 @@ export interface ConnectorSettingsFieldsProps { refetch: () => unknown } isSearchIndex: boolean + usesGitHubInstallation?: boolean connectorConfig: ConnectorMeta | null selectionLabels: SourceSelectionLabels sourceConfig: ConfigFieldMap @@ -115,6 +119,7 @@ export function ConnectorSettingsFields({ gitlabPermissions, availability, isSearchIndex, + usesGitHubInstallation = false, connectorConfig, sourceConfig, selectionLabels, @@ -156,6 +161,12 @@ export function ConnectorSettingsFields({ ? (getProviderIdFromServiceId(connectorConfig.auth.provider) as OAuthProvider) : null const syncsPerMember = access.accessMode === 'members' + const isGitHubInstallationSource = + usesGitHubInstallation && + isSearchIndex && + scope.kind === 'organization' && + connectorConfig?.id === 'github' && + syncsPerMember const requiresServiceAccount = Boolean( connectorConfig && !isConnectorCredentialTypeAllowed(connectorConfig.auth, access.accessMode, 'oauth') @@ -174,11 +185,14 @@ export function ConnectorSettingsFields({ serviceIcon: connectorConfig?.icon, }) const [showServiceAccountModal, setShowServiceAccountModal] = useState(false) + const [showGitHubInstallationModal, setShowGitHubInstallationModal] = useState(false) const [showMoreOptions, setShowMoreOptions] = useState(false) const isContentCredentialChange = accessDirty && !accessModeChanged const { data: rawCredentials = [], isLoading: credentialsLoading, + isFetching: credentialsFetching, + error: credentialsError, refetch: refetchCredentials, } = useOAuthCredentials(providerId ?? undefined, { enabled: (needsWorkspaceCredential || syncsPerMember) && Boolean(providerId), @@ -189,6 +203,9 @@ export function ConnectorSettingsFields({ const [browseCredentialId, setBrowseCredentialId] = useState(null) const selectorCredentialId = syncsPerMember ? browseCredentialId : credentialId const selectorCredential = rawCredentials.find((item) => item.id === selectorCredentialId) + const installations = rawCredentials.filter( + (credential) => credential.provider === GITHUB_INSTALLATION_PROVIDER_ID + ) const credentialOptions = useMemo( () => rawCredentials @@ -257,16 +274,52 @@ export function ConnectorSettingsFields({ /> )} - {syncsPerMember && connectorConfig?.supportsSeparateContentCredential && ( - + onRetry={() => void refetchCredentials()} + onConnect={() => setShowGitHubInstallationModal(true)} + onChange={onContentCredentialChange} + > + {(accessDirty || canReenableMemberSync) && ( +
+ + {isSwitchingAccess + ? 'Updating…' + : canReenableMemberSync + ? 'Re-enable sync' + : 'Change connection'} + + {accessDirty && ( + + Cancel + + )} +
+ )} + )} - {connectorConfig && showAccessField && ( + {!isGitHubInstallationSource && + syncsPerMember && + connectorConfig?.supportsSeparateContentCredential && ( + + )} + {connectorConfig && showAccessField && !isGitHubInstallationSource && ( field.type === 'selector' && isFieldVisible(field) @@ -489,6 +543,19 @@ export function ConnectorSettingsFields({ )} {error} + {showGitHubInstallationModal && + isGitHubInstallationSource && + scope.kind === 'organization' && + canAdmin && ( + setShowGitHubInstallationModal(false)} + onConnected={(credentialId) => { + onContentCredentialChange(credentialId) + setShowGitHubInstallationModal(false) + }} + /> + )} ) } diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.test.tsx index d6559838abf..239c30dc809 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.test.tsx @@ -113,6 +113,23 @@ describe('shared connector settings form', () => { container.remove() }) + it.each([ + { sourceConfig: { repository: 'acme/platform', githubRepositoryId: '9010' }, expected: true }, + { sourceConfig: { repository: 'acme/platform' }, expected: false }, + ])( + 'identifies installation settings from the persisted repository binding: $expected', + ({ sourceConfig, expected }) => { + render( + connector({ connectorType: 'github', credentialId: 'installation-1', sourceConfig }), + 'github' + ) + expect(form.fieldsProps.usesGitHubInstallation).toBe(expected) + expect(form.fieldsProps.access.accessMode).toBe('members') + expect(form.fieldsProps.sourceConfig.repository).toBe('acme/platform') + expect(mocks.applyAccess).not.toHaveBeenCalled() + } + ) + it('treats persisted JSONB label key order as an unchanged draft', () => { render( connector({ diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.ts index 15744d583ba..c764d2b0500 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.ts +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.ts @@ -462,6 +462,9 @@ export function useConnectorSettingsForm({ refetch: refetchIntegrationAvailability, }, isSearchIndex, + usesGitHubInstallation: + connector.connectorType === 'github' && + typeof connector.sourceConfig.githubRepositoryId === 'string', connectorConfig, sourceConfig, selectionLabels, diff --git a/apps/sim/app/workspace/[workspaceId]/search/components/github-installation-modal.tsx b/apps/sim/app/workspace/[workspaceId]/search/components/github-installation-modal.tsx index a966ed0c85c..ea5118d878e 100644 --- a/apps/sim/app/workspace/[workspaceId]/search/components/github-installation-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/search/components/github-installation-modal.tsx @@ -102,13 +102,13 @@ export function GitHubInstallationModal({ return ( { if (!open && !pending) onClose() }} > - Connect GitHub App + Connect GitHub { content: '', contentDeferred: true, contentHash: 'git-sha:blob-sha', + skippedRetryPolicy: 'source-change', }) expect(fetchMock).toHaveBeenCalledTimes(1) expect(fetchMock.mock.calls[0]![1].headers.Authorization).toBe('Bearer member-token') @@ -486,6 +488,94 @@ describe('githubConnector.getDocument', () => { }) }) +describe('githubConnector content outcomes', () => { + afterEach(() => vi.unstubAllGlobals()) + + it.each([ + ['empty.txt', '', 'Empty file was not indexed'], + ['blank.txt', ' \n\t ', 'Empty file was not indexed'], + ['image.png', 'binary\0contents', 'Binary file was not indexed'], + ])( + 'records %s as a verified omission and reuses its unchanged hash', + async (path, body, reason) => { + const fetchMock = vi + .fn() + .mockResolvedValueOnce(treeResponse([treeFile(path, 'blob-sha')])) + .mockResolvedValueOnce(new Response(body)) + vi.stubGlobal('fetch', fetchMock) + const context = {} + const listing = await githubConnector.listDocuments('token', source, undefined, context) + const skipped = await githubConnector.getDocument('token', source, path, context) + expect(skipped).toMatchObject({ + content: '', + contentDeferred: false, + contentHash: listing.documents[0].contentHash, + skippedReason: reason, + skippedExistingDisposition: 'replace', + }) + const existing = { id: 'document', contentHash: skipped!.contentHash, storageKey: null } + expect(classifyExternalDoc(listing.documents[0], existing)).toEqual({ type: 'unchanged' }) + expect( + classifyExternalDoc({ ...listing.documents[0], contentHash: 'git-sha:new-blob' }, existing) + ).toEqual({ type: 'update', existingId: 'document' }) + expect(classifyExternalDoc(listing.documents[0], { ...existing, contentHash: null })).toEqual( + { + type: 'update', + existingId: 'document', + } + ) + expect(fetchMock).toHaveBeenCalledTimes(2) + } + ) + + it('skips a known empty blob before downloading it', async () => { + const fetchMock = vi.fn().mockResolvedValueOnce(treeResponse([treeFile('empty.pdf', 'sha', 0)])) + vi.stubGlobal('fetch', fetchMock) + const context = {} + const listing = await githubConnector.listDocuments('token', source, undefined, context) + const document = await githubConnector.getDocument('token', source, 'empty.pdf', context) + expect(listing.documents[0]).toMatchObject({ + skippedReason: 'Empty file was not indexed', + skippedExistingDisposition: 'replace', + contentDeferred: false, + }) + expect(document?.skippedReason).toBe('Empty file was not indexed') + expect(document?.sourceFile).toBeUndefined() + expect(fetchMock).toHaveBeenCalledTimes(1) + }) + + it.each([ + ['report.pdf', 'application/pdf'], + ['report.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], + ])('hands the original %s bytes to the shared document processor', async (path, mimeType) => { + const bytes = Buffer.from([0x50, 0x4b, 0x00, 0x03, 0x04]) + const fetchMock = vi + .fn() + .mockResolvedValueOnce(treeResponse([treeFile(path, 'blob-sha', bytes.length)])) + .mockResolvedValueOnce(new Response(bytes)) + vi.stubGlobal('fetch', fetchMock) + const context = {} + const listing = await githubConnector.listDocuments('token', source, undefined, context) + const document = await githubConnector.getDocument('token', source, path, context) + expect(document).toMatchObject({ + content: '', + contentDeferred: false, + mimeType, + sourceFile: { bytes, fileName: path, mimeType }, + contentHash: listing.documents[0].contentHash, + }) + expect(document?.skippedReason).toBeUndefined() + expect( + classifyExternalDoc(listing.documents[0], { + id: 'previously-skipped', + contentHash: 'git-sha:blob-sha', + storageKey: null, + }) + ).toEqual({ type: 'update', existingId: 'previously-skipped' }) + expect(fetchMock).toHaveBeenCalledTimes(2) + }) +}) + describe('githubConnector symlinks', () => { afterEach(() => vi.unstubAllGlobals()) @@ -574,6 +664,43 @@ describe('githubConnector symlinks', () => { ]) }) + it('skips a directory link without requesting a directory as file content', async () => { + const directory = { ...treeFile('docs/folder'), mode: '040000', type: 'tree' } + const fetchMock = vi + .fn() + .mockResolvedValueOnce(treeResponse([link, directory])) + .mockResolvedValueOnce(new Response('folder')) + vi.stubGlobal('fetch', fetchMock) + await expect(githubConnector.getDocument('token', source, link.path)).resolves.toMatchObject({ + skippedReason: 'Symbolic link target is not a repository file', + skippedRetryPolicy: 'source-change', + skippedExistingDisposition: 'replace', + }) + expect(fetchMock).toHaveBeenCalledTimes(2) + }) + + it('routes a document symlink through the shared parser using its displayed file format', async () => { + const documentLink = { ...link, path: 'docs/report.pdf' } + const bytes = Buffer.from('%PDF-1.7\n\0binary document content') + const fetchMock = vi + .fn() + .mockResolvedValueOnce(treeResponse([documentLink, target])) + .mockResolvedValueOnce(new Response('target.md')) + .mockResolvedValueOnce(new Response(bytes)) + vi.stubGlobal('fetch', fetchMock) + const context = {} + const listing = await githubConnector.listDocuments('token', source, undefined, context) + const document = await githubConnector.getDocument('token', source, documentLink.path, context) + expect(document).toMatchObject({ + externalId: documentLink.path, + title: 'report.pdf', + sourceFile: { bytes, fileName: 'report.pdf', mimeType: 'application/pdf' }, + contentHash: listing.documents[0].contentHash, + }) + expect(document?.skippedReason).toBeUndefined() + expect(fetchMock).toHaveBeenCalledTimes(3) + }) + it('follows an in-repository link chain outside the configured listing prefix', async () => { const nextLink = { ...treeFile('intermediate.md', 'next-link-sha'), mode: '120000' } vi.stubGlobal( @@ -657,6 +784,7 @@ describe('githubConnector symlinks', () => { await expect(githubConnector.getDocument('token', source, link.path)).resolves.toMatchObject({ content: '', skippedReason: reason, + skippedExistingDisposition: 'replace', }) } }) diff --git a/apps/sim/connectors/github/github.ts b/apps/sim/connectors/github/github.ts index d1f71f951ae..f19991f8486 100644 --- a/apps/sim/connectors/github/github.ts +++ b/apps/sim/connectors/github/github.ts @@ -15,6 +15,7 @@ import { isPerMemberListing, markSkipped, parseTagDate, + pipelineParsedMimeType, readBodyWithLimit, sizeLimitSkipReason, stubOrSkipBySize, @@ -34,6 +35,8 @@ const GITHUB_API_URL = 'https://api.github.com' const BATCH_SIZE = 200 const GIT_SHA_PREFIX = 'git-sha:' const MAX_FILE_SIZE = CONNECTOR_MAX_FILE_BYTES +/** Rehydrates formats formerly skipped or decoded as plain text once through their real parser. */ +const SOURCE_FILE_HASH_SUFFIX = ':source-file-v1' const BINARY_SNIFF_BYTES = 8000 const MAX_SYMLINK_DEPTH = 40 const MAX_SYMLINK_TARGET_BYTES = 4096 @@ -43,6 +46,7 @@ const MAX_SYMLINK_TARGET_BYTES = 4096 * re-downloaded in full on every sync. */ const BINARY_SKIP_REASON = 'Binary file was not indexed' +const EMPTY_SKIP_REASON = 'Empty file was not indexed' /** * Heuristic binary detection: Git treats files containing a NUL byte in the @@ -260,14 +264,14 @@ async function fetchTree( return snapshot } -/** Streams a Git blob with the same binary and byte bounds used for ordinary files. */ -async function fetchBlobContent( +/** Keeps original bytes available to the shared parsers while bounding every blob read. */ +async function fetchBlobBytes( accessToken: string, owner: string, repo: string, sha: string, maxBytes = MAX_FILE_SIZE -): Promise { +): Promise { const url = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/blobs/${encodeURIComponent(sha)}` const label = `git blob ${sha}` const response = await fetchWithRetry(url, { @@ -296,8 +300,7 @@ async function fetchBlobContent( if (!buffer) { throw new ConnectorFileTooLargeError(maxBytes) } - if (isBinaryBuffer(buffer)) return null - return decodeTextBuffer(buffer).text + return buffer } /** Resolves links within one snapshot; Contents can truncate dereferenced targets at 1 MiB. */ @@ -314,13 +317,21 @@ async function resolveSymlinkTarget( if (item.mode !== '120000') return item.type === 'blob' ? item : null if (visited.has(item.path) || (item.size ?? 0) > MAX_SYMLINK_TARGET_BYTES) return null visited.add(item.path) - let target: string | null + let targetBytes: Buffer try { - target = await fetchBlobContent(accessToken, owner, repo, item.sha, MAX_SYMLINK_TARGET_BYTES) + targetBytes = await fetchBlobBytes( + accessToken, + owner, + repo, + item.sha, + MAX_SYMLINK_TARGET_BYTES + ) } catch (error) { if (error instanceof ConnectorFileTooLargeError) return null throw error } + if (isBinaryBuffer(targetBytes)) return null + const target = decodeTextBuffer(targetBytes).text if (!target || posix.isAbsolute(target)) return null const targetPath = posix.normalize(posix.join(posix.dirname(item.path), target)) if (targetPath === '..' || targetPath.startsWith('../')) return null @@ -356,10 +367,13 @@ function treeItemToStub( title: item.path.split('/').pop() || item.path, content: '', contentDeferred: true, + skippedRetryPolicy: 'source-change', + /** Verified immutable omissions replace older content; fetch failures still throw. */ + skippedExistingDisposition: 'replace', mimeType: 'text/plain', sourceUrl: `https://github.com/${owner}/${repo}/blob/${branch.split('/').map(encodeURIComponent).join('/')}/${item.path.split('/').map(encodeURIComponent).join('/')}`, /** Contents dereferences symlinks but retains their SHA even when the target changes. */ - contentHash: `${GIT_SHA_PREFIX}${item.sha}${item.mode === '120000' ? `:${treeSha}` : ''}`, + contentHash: `${GIT_SHA_PREFIX}${item.sha}${item.mode === '120000' ? `:${treeSha}` : ''}${pipelineParsedMimeType(item.path) ? SOURCE_FILE_HASH_SUFFIX : ''}`, metadata: { path: item.path, sha: item.sha, @@ -464,13 +478,14 @@ export const githubConnector: ConnectorConfig = { batchSize: batch.length, }) - const documents = batch.map((item) => - stubOrSkipBySize( + const documents = batch.map((item) => { + const stub = stubOrSkipBySize( treeItemToStub(owner, repo, branch, item, snapshot.sha), item.size, MAX_FILE_SIZE ) - ) + return item.size === 0 ? markSkipped(stub, EMPTY_SKIP_REASON) : stub + }) const nextOffset = offset + BATCH_SIZE const hasMore = nextOffset < capped.length @@ -509,22 +524,32 @@ export const githubConnector: ConnectorConfig = { const size = target?.size ?? 0 const stub = treeItemToStub(owner, repo, branch, { ...treeItem, size }, snapshot.sha) if (!target) { - return { - ...markSkipped(stub, 'Symbolic link target is not a repository file'), - skippedExistingDisposition: 'replace', - } + return markSkipped(stub, 'Symbolic link target is not a repository file') } if (size > MAX_FILE_SIZE) return markSkipped(stub, sizeLimitSkipReason(MAX_FILE_SIZE)) + if (target.size === 0) return markSkipped(stub, EMPTY_SKIP_REASON) /** The immutable listed blob avoids ref drift and a redundant Contents API request. */ - let content: string | null + let bytes: Buffer try { - content = await fetchBlobContent(accessToken, owner, repo, target.sha) + bytes = await fetchBlobBytes(accessToken, owner, repo, target.sha) } catch (error) { if (error instanceof ConnectorFileTooLargeError) return markSkipped(stub, sizeLimitSkipReason(MAX_FILE_SIZE)) throw error } - if (content === null) return markSkipped(stub, BINARY_SKIP_REASON) + if (bytes.length === 0) return markSkipped(stub, EMPTY_SKIP_REASON) + const mimeType = pipelineParsedMimeType(stub.title) + if (mimeType) { + return { + ...stub, + contentDeferred: false, + mimeType, + sourceFile: { bytes, fileName: stub.title, mimeType }, + } + } + if (isBinaryBuffer(bytes)) return markSkipped(stub, BINARY_SKIP_REASON) + const content = decodeTextBuffer(bytes).text + if (!content.trim()) return markSkipped(stub, EMPTY_SKIP_REASON) return { ...stub, diff --git a/apps/sim/connectors/github/installation-connection-field.tsx b/apps/sim/connectors/github/installation-connection-field.tsx new file mode 100644 index 00000000000..2a87c2cf52d --- /dev/null +++ b/apps/sim/connectors/github/installation-connection-field.tsx @@ -0,0 +1,72 @@ +import type { ReactNode } from 'react' +import { Chip, ChipCombobox, ChipModalField } from '@sim/emcn' +import { Plus } from '@sim/emcn/icons' +import type { Credential } from '@/lib/oauth' +import { SettingsQueryErrorState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' + +interface GitHubInstallationConnectionFieldProps { + installations: Credential[] + credentialId: string | null + isLoading: boolean + isFetching: boolean + error: Error | null + disabled: boolean + onRetry: () => void + onConnect: () => void + onChange: (credentialId: string) => void + children?: ReactNode +} + +export function GitHubInstallationConnectionField({ + installations, + credentialId, + isLoading, + isFetching, + error, + disabled, + onRetry, + onConnect, + onChange, + children, +}: GitHubInstallationConnectionFieldProps) { + return ( + + {error && installations.length === 0 ? ( + + ) : installations.length > 1 ? ( + ({ + value: credential.id, + label: credential.name || 'GitHub', + })), + { + value: '__connect_github__', + label: 'Connect GitHub', + icon: Plus, + onSelect: onConnect, + }, + ]} + value={credentialId ?? undefined} + onChange={onChange} + placeholder='Select GitHub connection' + disabled={disabled} + /> + ) : ( + + {isLoading + ? 'Loading GitHub…' + : installations.find((credential) => credential.id === credentialId)?.name || + 'Connect GitHub'} + + )} + {children} + + ) +} diff --git a/apps/sim/hooks/queries/kb/connectors.ts b/apps/sim/hooks/queries/kb/connectors.ts index 807d908b116..f0c284635f2 100644 --- a/apps/sim/hooks/queries/kb/connectors.ts +++ b/apps/sim/hooks/queries/kb/connectors.ts @@ -500,7 +500,13 @@ export function useOrganizationSearchOverview( export function useSearchSources( owner?: string | ResourceScope, - options?: { enabled?: boolean; search?: string; mine?: boolean; connectorType?: string } + options?: { + enabled?: boolean + search?: string + mine?: boolean + connectorType?: string + excludeConnectorType?: string + } ) { const queryClient = useQueryClient() const scope = @@ -516,6 +522,9 @@ export function useSearchSources( search: options?.search?.trim().toLowerCase() ?? '', mine: options?.mine ?? false, ...(options?.connectorType?.trim() ? { connectorType: options.connectorType.trim() } : {}), + ...(options?.excludeConnectorType?.trim() + ? { excludeConnectorType: options.excludeConnectorType.trim() } + : {}), } const summary = useInfiniteQuery({ queryKey: searchSourceKeys.pages(scope, filters), diff --git a/apps/sim/hooks/queries/organization-accounts.ts b/apps/sim/hooks/queries/organization-accounts.ts index 7c96859612d..733c3f03927 100644 --- a/apps/sim/hooks/queries/organization-accounts.ts +++ b/apps/sim/hooks/queries/organization-accounts.ts @@ -28,6 +28,7 @@ import { type OrganizationAccountPeopleQuery, type RemoveOrganizationAccountMcpProviderParams, type ResendOrganizationAccountInvitationQuery, + reconnectPersonalOrganizationAccountContract, removeOrganizationAccountMcpProviderContract, resendOrganizationAccountInvitationContract, revokeOrganizationAccountEnrollmentContract, @@ -43,6 +44,13 @@ import { searchSourceKeys } from '@/hooks/queries/utils/search-source-keys' export const ORGANIZATION_ACCOUNTS_STALE_TIME = 30_000 +export function useReconnectPersonalOrganizationAccount() { + return useMutation({ + mutationFn: (credentialId: string) => + requestJson(reconnectPersonalOrganizationAccountContract, { params: { credentialId } }), + }) +} + /** Disconnects an owned grant; indexing and source setup do not gate this operation. */ export function useDisconnectPersonalOrganizationAccount(organizationId: string) { const queryClient = useQueryClient() diff --git a/apps/sim/hooks/queries/utils/search-source-keys.ts b/apps/sim/hooks/queries/utils/search-source-keys.ts index 17f69eb5683..ee508d3fb1b 100644 --- a/apps/sim/hooks/queries/utils/search-source-keys.ts +++ b/apps/sim/hooks/queries/utils/search-source-keys.ts @@ -12,7 +12,12 @@ export const searchSourceKeys = { ] as const, pages: ( scope: string | ResourceScope | undefined, - filters: { search: string; mine: boolean; connectorType?: string } + filters: { + search: string + mine: boolean + connectorType?: string + excludeConnectorType?: string + } ) => [...searchSourceKeys.list(scope), 'pages', filters] as const, overview: (scope?: string | ResourceScope) => [...searchSourceKeys.list(scope), 'overview'] as const, diff --git a/apps/sim/lib/api/contracts/knowledge/connectors.test.ts b/apps/sim/lib/api/contracts/knowledge/connectors.test.ts index ed6870a25e9..5babbca4b6f 100644 --- a/apps/sim/lib/api/contracts/knowledge/connectors.test.ts +++ b/apps/sim/lib/api/contracts/knowledge/connectors.test.ts @@ -3,6 +3,7 @@ */ import { describe, expect, it } from 'vitest' import { + connectorDocumentDataSchema, connectorDocumentsQuerySchema, createConnectorBodySchema, updateConnectorAccessBodySchema, @@ -16,7 +17,7 @@ const base = { describe('connector document list contracts', () => { it('accepts each document set and trims filename search without interpreting wildcards', () => { - for (const filter of ['active', 'excluded', 'failed']) { + for (const filter of ['active', 'excluded', 'failed', 'skipped']) { expect( connectorDocumentsQuerySchema.parse({ filter, search: ' 50%_report ' }) ).toMatchObject({ @@ -26,6 +27,26 @@ describe('connector document list contracts', () => { } }) + it('adds skipped outcomes without requiring them from older document responses', () => { + const document = { + id: 'document', + filename: 'logo.png', + externalId: 'logo.png', + sourceUrl: null, + enabled: true, + userExcluded: false, + uploadedAt: '2026-01-01T00:00:00Z', + processingStatus: 'failed', + } + expect(connectorDocumentDataSchema.parse(document)).toMatchObject({ + processingStatus: 'failed', + processingOutcome: null, + }) + expect( + connectorDocumentDataSchema.parse({ ...document, processingOutcome: 'skipped' }) + ).toMatchObject({ processingStatus: 'failed', processingOutcome: 'skipped' }) + }) + it('preserves legacy flags when no document filter is supplied', () => { expect( connectorDocumentsQuerySchema.parse({ includeExcluded: 'true', failedOnly: 'false' }) diff --git a/apps/sim/lib/api/contracts/knowledge/connectors.ts b/apps/sim/lib/api/contracts/knowledge/connectors.ts index a84ae2f1ab3..da71e989b1b 100644 --- a/apps/sim/lib/api/contracts/knowledge/connectors.ts +++ b/apps/sim/lib/api/contracts/knowledge/connectors.ts @@ -75,7 +75,7 @@ export const deleteConnectorQuerySchema = z.object({ deleteDocuments: booleanQueryFlagSchema.optional().default(false), }) -export const connectorDocumentFilterSchema = z.enum(['active', 'excluded', 'failed']) +export const connectorDocumentFilterSchema = z.enum(['active', 'excluded', 'failed', 'skipped']) export type ConnectorDocumentFilter = z.output export const connectorDocumentsQuerySchema = z.object({ @@ -236,6 +236,8 @@ export const connectorDocumentDataSchema = z userExcluded: z.boolean(), uploadedAt: z.string(), processingStatus: z.string(), + processingOutcome: z.literal('skipped').nullable().default(null), + processingError: z.string().nullable().default(null), }) .passthrough() export type ConnectorDocumentData = z.output @@ -246,6 +248,7 @@ export const connectorDocumentsDataSchema = z.object({ active: z.number().int().nonnegative(), excluded: z.number().int().nonnegative(), failed: z.number().int().nonnegative().default(0), + skipped: z.number().int().nonnegative().default(0), }), hasMore: z.boolean().optional(), }) @@ -394,6 +397,12 @@ export const searchSourceCursorSchema = z.object({ export const listSearchSourcesQuerySchema = resourceOwnerSchema.safeExtend({ cursor: z.string().min(1).max(1024).optional(), connectorType: z.string().trim().min(1, 'connectorType cannot be empty').max(100).optional(), + excludeConnectorType: z + .string() + .trim() + .min(1, 'excludeConnectorType cannot be empty') + .max(100) + .optional(), search: z.string().trim().max(200).optional(), mine: booleanQueryFlagSchema.optional(), }) diff --git a/apps/sim/lib/api/contracts/knowledge/documents.test.ts b/apps/sim/lib/api/contracts/knowledge/documents.test.ts index 5c0510f5dc4..9a12967d460 100644 --- a/apps/sim/lib/api/contracts/knowledge/documents.test.ts +++ b/apps/sim/lib/api/contracts/knowledge/documents.test.ts @@ -2,12 +2,57 @@ * @vitest-environment node */ import { describe, expect, it } from 'vitest' +import { z } from 'zod' import { bulkCreateDocumentsBodySchema, + documentDataSchema, listKnowledgeDocumentsQuerySchema, parseDocumentTagFiltersParam, upsertDocumentBodySchema, } from '@/lib/api/contracts/knowledge/documents' +import { getDocumentIndexingStatus } from '@/lib/knowledge/documents/types' + +describe('document processing response compatibility', () => { + const document = { + id: 'document', + knowledgeBaseId: 'knowledge-base', + filename: 'logo.png', + fileUrl: '', + fileSize: 0, + mimeType: 'text/plain', + chunkCount: 0, + tokenCount: 0, + characterCount: 0, + processingStatus: 'failed', + enabled: true, + uploadedAt: '2026-01-01T00:00:00Z', + } + const previousSchema = documentDataSchema.omit({ processingOutcome: true }).extend({ + processingStatus: z.enum(['pending', 'processing', 'completed', 'failed']), + }) + + it('keeps skipped responses valid for the previous strict four-status reader', () => { + const response = documentDataSchema.parse({ ...document, processingOutcome: 'skipped' }) + expect(response.processingStatus).toBe('failed') + expect(getDocumentIndexingStatus(response)).toBe('skipped') + expect(previousSchema.parse(response)).toEqual(document) + }) + + it.each(['pending', 'processing', 'completed', 'failed'])( + 'accepts an older server’s %s response without inventing an outcome', + (processingStatus) => { + const response = documentDataSchema.parse({ ...document, processingStatus }) + expect(response.processingOutcome).toBeNull() + expect(getDocumentIndexingStatus(response)).toBe(processingStatus) + } + ) + + it('does not expand the stored-status wire enum to encode an indexing outcome', () => { + expect(documentDataSchema.safeParse({ ...document, processingStatus: 'skipped' }).success).toBe( + false + ) + }) +}) describe('listKnowledgeDocumentsQuerySchema.tagFilters', () => { it('keeps tagFilters a raw string (must NOT transform to an array)', () => { diff --git a/apps/sim/lib/api/contracts/knowledge/documents.ts b/apps/sim/lib/api/contracts/knowledge/documents.ts index 67dc7d8720f..a5219138b05 100644 --- a/apps/sim/lib/api/contracts/knowledge/documents.ts +++ b/apps/sim/lib/api/contracts/knowledge/documents.ts @@ -16,6 +16,7 @@ import { privateSecretProvenanceBundleSchema } from '@/lib/api/contracts/primiti import { defineRouteContract } from '@/lib/api/contracts/types' import { PRIVATE_SECRET_PROVENANCE_FIELD } from '@/lib/execution/private-tool-metadata' import { getFieldTypeForSlot, MAX_KNOWLEDGE_DOCUMENTS_PER_CREATE } from '@/lib/knowledge/constants' +import { DOCUMENT_PROCESSING_STATUSES } from '@/lib/knowledge/documents/types' import { getOperatorsForFieldType, isValidFilterValue } from '@/lib/knowledge/filters/types' import { knowledgeDocumentUploadMetadataSchema } from '@/lib/knowledge/upload-metadata' @@ -257,7 +258,8 @@ export const documentDataSchema = z chunkCount: z.number(), tokenCount: z.number(), characterCount: z.number(), - processingStatus: z.enum(['pending', 'processing', 'completed', 'failed']), + processingStatus: z.enum(DOCUMENT_PROCESSING_STATUSES), + processingOutcome: z.literal('skipped').nullable().default(null), /** When indexing was last dispatched to a worker, which precedes a worker starting it. */ processingQueuedAt: nullableWireDateSchema.optional(), processingStartedAt: nullableWireDateSchema.optional(), diff --git a/apps/sim/lib/api/contracts/organization-accounts.test.ts b/apps/sim/lib/api/contracts/organization-accounts.test.ts index 6e9a19cfb03..6dd57746793 100644 --- a/apps/sim/lib/api/contracts/organization-accounts.test.ts +++ b/apps/sim/lib/api/contracts/organization-accounts.test.ts @@ -3,8 +3,39 @@ import { describe, expect, it } from 'vitest' import { addOrganizationAccountMcpProviderContract, listOrganizationAccountPeopleContract, + reconnectPersonalOrganizationAccountContract, + startOrganizationAccountConnectionContract, } from '@/lib/api/contracts/organization-accounts' +describe.each([ + startOrganizationAccountConnectionContract, + reconnectPersonalOrganizationAccountContract, +])('account connection response $path', (contract) => { + const invitationLink = 'https://sim.test/credential-groups/enroll/fixture-token' + const authorizationUrl = + 'https://sim.test/api/credential-groups/enroll/fixture-token/oauth/option-1?returnTo=search' + + it('accepts older enrollment-only responses and the additive direct OAuth URL', () => { + expect(contract.response.schema.parse({ invitationLink })).toEqual({ invitationLink }) + expect(contract.response.schema.parse({ invitationLink, authorizationUrl })).toEqual({ + invitationLink, + authorizationUrl, + }) + }) + + it('rejects malformed or oversized direct URLs', () => { + expect( + contract.response.schema.safeParse({ invitationLink, authorizationUrl: '/relative' }).success + ).toBe(false) + expect( + contract.response.schema.safeParse({ + invitationLink, + authorizationUrl: `https://sim.test/${'x'.repeat(8192)}`, + }).success + ).toBe(false) + }) +}) + describe('organization MCP provider creation contract', () => { const schema = addOrganizationAccountMcpProviderContract.body if (!schema) throw new Error('MCP provider creation requires a body contract') diff --git a/apps/sim/lib/api/contracts/organization-accounts.ts b/apps/sim/lib/api/contracts/organization-accounts.ts index def09b7e201..7b08459e082 100644 --- a/apps/sim/lib/api/contracts/organization-accounts.ts +++ b/apps/sim/lib/api/contracts/organization-accounts.ts @@ -19,6 +19,7 @@ import { defineRouteContract } from '@/lib/api/contracts/types' import { ORGANIZATION_ACCOUNT_INDEXING_SOURCE_LIMIT, ORGANIZATION_ACCOUNT_WORKSPACE_LIMIT, + ORGANIZATION_VIEWER_ACCOUNT_LIMIT, } from '@/lib/credential-groups/limits' const organizationAccountsParamsSchema = z.object({ id: organizationIdSchema }) @@ -41,6 +42,19 @@ export const getOrganizationAccountsContract = defineRouteContract({ availableProviders: z.array(credentialGroupProviderSchema), canManage: z.boolean(), indexingAvailable: z.boolean(), + viewerAccounts: z + .array( + z.object({ + credentialId: z.string().min(1).max(128), + displayName: z.string().max(512), + providerId: z.string().min(1).max(128), + groupId: z.string().min(1).max(128), + optionId: z.string().min(1).max(128), + status: z.enum(['active', 'needs_reauth']), + }) + ) + .max(ORGANIZATION_VIEWER_ACCOUNT_LIMIT) + .optional(), }), }, }) @@ -58,12 +72,20 @@ export const updateOrganizationAccountsContract = defineRouteContract({ body: updateCredentialGroupBodySchema, response: { mode: 'json', schema: organizationAccountsResponseSchema }, }) +export const organizationAccountConnectionResponseSchema = z.object({ + invitationLink: z.string().url(), + authorizationUrl: z.string().url().max(8192).optional(), +}) +export type OrganizationAccountConnectionResponse = z.output< + typeof organizationAccountConnectionResponseSchema +> + export const startOrganizationAccountConnectionContract = defineRouteContract({ method: 'POST', path: '/api/organizations/[id]/connected-accounts/connect', params: organizationAccountsParamsSchema, body: z.object({ optionId: z.string().min(1, 'Account option is required').max(128) }).strict(), - response: { mode: 'json', schema: z.object({ invitationLink: z.string().url() }) }, + response: { mode: 'json', schema: organizationAccountConnectionResponseSchema }, }) export const startOrganizationSlackConfigurationContract = defineRouteContract({ @@ -313,7 +335,7 @@ export const reconnectPersonalOrganizationAccountContract = defineRouteContract( method: 'POST', path: '/api/users/me/organization-accounts/[credentialId]/reconnect', params: z.object({ credentialId: z.string().min(1).max(128) }), - response: { mode: 'json', schema: z.object({ invitationLink: z.string().url() }) }, + response: { mode: 'json', schema: organizationAccountConnectionResponseSchema }, }) export const disconnectPersonalOrganizationAccountContract = defineRouteContract({ method: 'DELETE', diff --git a/apps/sim/lib/credential-groups/application/organization-access.test.ts b/apps/sim/lib/credential-groups/application/organization-access.test.ts index f86ea9f7790..32c4897be29 100644 --- a/apps/sim/lib/credential-groups/application/organization-access.test.ts +++ b/apps/sim/lib/credential-groups/application/organization-access.test.ts @@ -10,6 +10,8 @@ const mocks = vi.hoisted(() => ({ setup: vi.fn(), write: vi.fn(), policy: vi.fn(), + accountsGroup: vi.fn(), + invite: vi.fn(), })) vi.mock('@/lib/credential-groups/scoped-availability', () => ({ isScopedCredentialGroupsAvailable: mocks.available, @@ -25,14 +27,17 @@ vi.mock('@/lib/permission-groups/resolve.server', () => ({ })) vi.mock('@/lib/credential-groups/service', () => ({ ensureWorkspaceAccountsGroup: vi.fn(), - getOrganizationAccountsGroup: vi.fn(), + getOrganizationAccountsGroup: mocks.accountsGroup, updateCredentialGroup: vi.fn(), })) vi.mock('@/lib/credential-groups/provider-availability', () => ({ listConfiguredCredentialGroupProviders: vi.fn(), })) +vi.mock('@/lib/knowledge/access/availability', () => ({ + isKnowledgeMemberAccessAvailable: vi.fn().mockResolvedValue(true), +})) vi.mock('@/lib/credential-groups/self-enrollment', () => ({ - createViewerCredentialGroupEnrollment: vi.fn(), + createViewerCredentialGroupEnrollment: mocks.invite, })) vi.mock('@/lib/resource-policies/repository', () => ({ requireResourcePolicy: mocks.policy, @@ -40,10 +45,15 @@ vi.mock('@/lib/resource-policies/repository', () => ({ ResourcePolicyRevisionConflictError: class extends Error {}, })) +import { OrchestrationError } from '@/lib/core/orchestration/types' import { getOrganizationAccountWorkspaceAccess, updateOrganizationAccountWorkspaceAccess, } from '@/lib/credential-groups/application/organization-access' +import { + getOrganizationAccountsSettings, + startOrganizationAccountConnection, +} from '@/lib/credential-groups/application/organization-accounts' import { buildOrganizationAccountAccessPolicy } from '@/lib/credential-groups/application/workspace-access-policy' import { ResourcePolicyRevisionConflictError } from '@/lib/resource-policies/repository' @@ -84,6 +94,73 @@ describe('organization workspace sharing administration', () => { } ) + it('returns only the acting member account metadata for the canonical organization group, even when providers are disabled', async () => { + queueTableRows(schemaMock.member, [{ role: 'member' }]) + mocks.accountsGroup.mockResolvedValue({ id: 'group-1', status: 'disabled', options: [] }) + const account = { + credentialId: 'credential-1', + displayName: 'My GitHub', + providerId: 'github-repositories', + groupId: 'group-1', + optionId: 'option-1', + status: 'needs_reauth', + } + queueTableRows(schemaMock.credential, [account]) + const result = await getOrganizationAccountsSettings.execute({ + principal, + input: { organizationId: 'org-1' }, + }) + expect(result.viewerAccounts).toEqual([account]) + expect(result.canManage).toBe(false) + expect(eq).toHaveBeenCalledWith(schemaMock.credentialGroup.id, 'group-1') + expect(eq).toHaveBeenCalledWith(schemaMock.credentialGroupEnrollment.userId, 'admin-user') + expect(eq).toHaveBeenCalledWith(schemaMock.credential.organizationId, 'org-1') + }) + + it('starts an active account through the existing enrollment OAuth boundary', async () => { + queueTableRows(schemaMock.member, [{ role: 'member' }]) + mocks.accountsGroup.mockResolvedValue({ + id: 'group-1', + status: 'active', + options: [{ id: 'option-1', status: 'active' }], + }) + mocks.invite.mockResolvedValue({ + invitationLink: 'https://sim.test/credential-groups/enroll/fixture-token', + }) + await expect( + startOrganizationAccountConnection.execute({ + principal, + input: { organizationId: 'org-1', optionId: 'option-1' }, + }) + ).resolves.toEqual({ + invitationLink: + 'https://sim.test/credential-groups/enroll/fixture-token?optionId=option-1&returnTo=search', + authorizationUrl: + 'https://sim.test/api/credential-groups/enroll/fixture-token/oauth/option-1?returnTo=search', + }) + expect(mocks.invite).toHaveBeenCalledExactlyOnceWith({ + organizationId: 'org-1', + userId: 'admin-user', + credentialGroupId: 'group-1', + }) + }) + + it('does not issue a direct authorization link when enrollment access was revoked', async () => { + queueTableRows(schemaMock.member, [{ role: 'member' }]) + mocks.accountsGroup.mockResolvedValue({ + id: 'group-1', + status: 'active', + options: [{ id: 'option-1', status: 'active' }], + }) + mocks.invite.mockRejectedValue(new OrchestrationError('forbidden', 'Enrollment revoked')) + await expect( + startOrganizationAccountConnection.execute({ + principal, + input: { organizationId: 'org-1', optionId: 'option-1' }, + }) + ).rejects.toMatchObject({ code: 'forbidden' }) + }) + it('uses the routed org and checks every workspace before granting access', async () => { queueTableRows(schemaMock.member, [{ role: 'admin' }]) queueTableRows(schemaMock.workspace, [{ id: 'workspace-1' }]) diff --git a/apps/sim/lib/credential-groups/application/organization-accounts.ts b/apps/sim/lib/credential-groups/application/organization-accounts.ts index ccf1782f170..2eabfa67f15 100644 --- a/apps/sim/lib/credential-groups/application/organization-accounts.ts +++ b/apps/sim/lib/credential-groups/application/organization-accounts.ts @@ -1,4 +1,6 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' +import { credentialGroup as credentialGroupTable } from '@sim/db/schema' +import { eq } from 'drizzle-orm' import type { OperationUseCase } from '@/lib/core/application/operation' import { authorizeOrganizationOperation, @@ -11,6 +13,7 @@ import { import { OrchestrationError } from '@/lib/core/orchestration/types' import { validateUpdateCredentialGroupInput } from '@/lib/credential-groups/application/validation' import { loadScopedAccountsCredentialListContext } from '@/lib/credential-groups/credentials' +import { createCredentialGroupOAuthStartUrl } from '@/lib/credential-groups/enrollment-links' import { CredentialGroupEnrollmentError } from '@/lib/credential-groups/enrollments' import { ManagedMcpConnectorError } from '@/lib/credential-groups/managed-mcp-service' import { requireOrganizationAccountsSetup } from '@/lib/credential-groups/organization-setup' @@ -26,6 +29,7 @@ import type { CredentialGroupOptionInput, UpdateCredentialGroupInput, } from '@/lib/credential-groups/types' +import { listViewerOrganizationAccounts } from '@/lib/credential-groups/viewer-accounts' import { isKnowledgeMemberAccessAvailable } from '@/lib/knowledge/access/availability' export const organizationAccountOperations = { @@ -129,8 +133,16 @@ export function defineOrganizationAccountsUseCase< export const getOrganizationAccountsSettings = defineOrganizationAccountsUseCase({ operation: organizationAccountOperations.read, async execute({ context }) { + const credentialGroup = await getOrganizationAccountsGroup(context.organizationId) return { - credentialGroup: await getOrganizationAccountsGroup(context.organizationId), + credentialGroup, + viewerAccounts: credentialGroup + ? await listViewerOrganizationAccounts({ + organizationId: context.organizationId, + userId: context.userId, + matching: eq(credentialGroupTable.id, credentialGroup.id), + }) + : [], availableProviders: listConfiguredCredentialGroupProviders(), canManage: context.role === 'owner' || context.role === 'admin', indexingAvailable: await isKnowledgeMemberAccessAvailable({ @@ -219,6 +231,13 @@ export const startOrganizationAccountConnection = defineOrganizationAccountsUseC const url = new URL(invitationLink) url.searchParams.set('optionId', input.optionId) url.searchParams.set('returnTo', 'search') - return { invitationLink: url.toString() } + return { + invitationLink: url.toString(), + authorizationUrl: createCredentialGroupOAuthStartUrl({ + invitationLink, + optionId: input.optionId, + returnTo: 'search', + }), + } }, }) diff --git a/apps/sim/lib/credential-groups/application/personal-organization-accounts.test.ts b/apps/sim/lib/credential-groups/application/personal-organization-accounts.test.ts index 867ad51c839..df7d9fe70e3 100644 --- a/apps/sim/lib/credential-groups/application/personal-organization-accounts.test.ts +++ b/apps/sim/lib/credential-groups/application/personal-organization-accounts.test.ts @@ -54,7 +54,9 @@ describe('personal organization contributions', () => { vi.clearAllMocks() resetDbChainMock() mocks.available.mockResolvedValue(true) - mocks.invite.mockResolvedValue({ invitationLink: 'https://sim.test/enroll/token' }) + mocks.invite.mockResolvedValue({ + invitationLink: 'https://sim.test/credential-groups/enroll/fixture-token', + }) }) it('lists only the stable signed-in identity without requiring org membership', async () => { @@ -79,9 +81,21 @@ describe('personal organization contributions', () => { const url = new URL(result.invitationLink) expect(url.searchParams.get('optionId')).toBe('gmail-option') expect(url.searchParams.get('returnTo')).toBe('accounts') + expect(result.authorizationUrl).toBe( + 'https://sim.test/api/credential-groups/enroll/fixture-token/oauth/gmail-option?returnTo=accounts' + ) expect(dbChainMockFns.from).not.toHaveBeenCalledWith(schemaMock.member) }) + it('keeps the enrollment page for an MCP contribution', async () => { + queueTableRows(schemaMock.credential, [row]) + await expect( + reconnectPersonalOrganizationAccount.execute({ principal, input }) + ).resolves.toEqual({ + invitationLink: 'https://sim.test/credential-groups/enroll/fixture-token', + }) + }) + it('refuses disconnect of another contributor’s account', async () => { queueTableRows(schemaMock.credential, []) await expect( diff --git a/apps/sim/lib/credential-groups/application/personal-organization-accounts.ts b/apps/sim/lib/credential-groups/application/personal-organization-accounts.ts index ab7af64f16a..afd6096403a 100644 --- a/apps/sim/lib/credential-groups/application/personal-organization-accounts.ts +++ b/apps/sim/lib/credential-groups/application/personal-organization-accounts.ts @@ -14,6 +14,7 @@ import { and, asc, eq, gt, inArray, isNotNull } from 'drizzle-orm' import { defineOperation } from '@/lib/core/application' import { OrchestrationError } from '@/lib/core/orchestration/types' import { sameResourceScopeCondition } from '@/lib/core/resource-scope.server' +import { createCredentialGroupOAuthStartUrl } from '@/lib/credential-groups/enrollment-links' import { lockCredentialGroupEnrollmentLifecycle } from '@/lib/credential-groups/enrollments' import { isScopedCredentialGroupsAvailable } from '@/lib/credential-groups/scoped-availability' import { createViewerCredentialGroupEnrollment } from '@/lib/credential-groups/self-enrollment' @@ -152,7 +153,18 @@ export const reconnectPersonalOrganizationAccount = defineAuthorizedCredentialUs url.searchParams.set('optionId', account.optionId) url.searchParams.set('returnTo', 'accounts') } - return { invitationLink: url.toString() } + return { + invitationLink: url.toString(), + ...(account.type === 'managed_oauth' && account.optionId + ? { + authorizationUrl: createCredentialGroupOAuthStartUrl({ + invitationLink, + optionId: account.optionId, + returnTo: 'accounts', + }), + } + : {}), + } }, }) diff --git a/apps/sim/lib/credential-groups/enrollment-links.test.ts b/apps/sim/lib/credential-groups/enrollment-links.test.ts new file mode 100644 index 00000000000..69adab606af --- /dev/null +++ b/apps/sim/lib/credential-groups/enrollment-links.test.ts @@ -0,0 +1,51 @@ +/** @vitest-environment node */ +import { describe, expect, it } from 'vitest' +import { createCredentialGroupOAuthStartUrl } from '@/lib/credential-groups/enrollment-links' + +describe('credential group OAuth start links', () => { + it.each(['https://staging.sim.test', 'http://localhost:3015'])( + 'preserves the generated %s origin and encodes both path identifiers once', + (origin) => { + const token = 'fixture/token ?+%' + const optionId = 'option/with ?+%' + const url = new URL( + createCredentialGroupOAuthStartUrl({ + invitationLink: `${origin}/credential-groups/enroll/${encodeURIComponent(token)}?optionId=old&returnTo=accounts#old`, + optionId, + returnTo: 'search', + }) + ) + expect(url.origin).toBe(origin) + expect(url.pathname).toBe( + `/api/credential-groups/enroll/${encodeURIComponent(token)}/oauth/${encodeURIComponent(optionId)}` + ) + expect(url.search).toBe('?returnTo=search') + expect(url.hash).toBe('') + } + ) + + it('returns reconnects to account settings', () => { + expect( + createCredentialGroupOAuthStartUrl({ + invitationLink: 'https://sim.test/credential-groups/enroll/fixture-token', + optionId: 'option-1', + returnTo: 'accounts', + }) + ).toBe( + 'https://sim.test/api/credential-groups/enroll/fixture-token/oauth/option-1?returnTo=accounts' + ) + }) + + it.each(['/enroll/token', '/credential-groups/enroll/', '/credential-groups/enroll/token/extra'])( + 'rejects a noncanonical enrollment path %s', + (path) => { + expect(() => + createCredentialGroupOAuthStartUrl({ + invitationLink: `https://sim.test${path}`, + optionId: 'option-1', + returnTo: 'search', + }) + ).toThrow('Invalid credential group enrollment link') + } + ) +}) diff --git a/apps/sim/lib/credential-groups/enrollment-links.ts b/apps/sim/lib/credential-groups/enrollment-links.ts new file mode 100644 index 00000000000..1d7c76e2214 --- /dev/null +++ b/apps/sim/lib/credential-groups/enrollment-links.ts @@ -0,0 +1,16 @@ +/** Starts OAuth through the existing enrollment boundary, which owns authentication and PKCE. */ +export function createCredentialGroupOAuthStartUrl(input: { + invitationLink: string + optionId: string + returnTo: 'search' | 'accounts' +}): string { + const invitation = new URL(input.invitationLink) + const token = invitation.pathname.match(/^\/credential-groups\/enroll\/([^/]+)$/)?.[1] + if (!token) throw new Error('Invalid credential group enrollment link') + const url = new URL( + `/api/credential-groups/enroll/${encodeURIComponent(decodeURIComponent(token))}/oauth/${encodeURIComponent(input.optionId)}`, + invitation.origin + ) + url.searchParams.set('returnTo', input.returnTo) + return url.toString() +} diff --git a/apps/sim/lib/credential-groups/limits.ts b/apps/sim/lib/credential-groups/limits.ts index cad8c92b28b..60963f14139 100644 --- a/apps/sim/lib/credential-groups/limits.ts +++ b/apps/sim/lib/credential-groups/limits.ts @@ -1,4 +1,5 @@ export const CREDENTIAL_GROUP_MCP_SERVER_LIMIT = 50 +export const ORGANIZATION_VIEWER_ACCOUNT_LIMIT = 100 export const ORGANIZATION_ACCOUNT_WORKSPACE_LIMIT = 1000 export const CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT = 50 export const CREDENTIAL_GROUP_WORKFLOW_CATALOG_LIMIT = 500 diff --git a/apps/sim/lib/credential-groups/oauth-completion.ts b/apps/sim/lib/credential-groups/oauth-completion.ts index 31225358b61..1c732bdfc8c 100644 --- a/apps/sim/lib/credential-groups/oauth-completion.ts +++ b/apps/sim/lib/credential-groups/oauth-completion.ts @@ -4,9 +4,15 @@ export const CREDENTIAL_GROUP_OAUTH_FAILURE_MESSAGES = { expired: 'This connection attempt expired. Try connecting your account again.', denied: 'Authorization was canceled. Try connecting your account again.', account_mismatch: 'Choose the account matching your Sim email address.', + github_email_mismatch: + 'In GitHub Settings → Emails, add and verify the email address used for this Sim connection, then try again. A verified secondary email is supported.', + github_email_access_denied: + 'GitHub did not allow access to your email addresses. Ask an admin to check that the GitHub App has Email addresses: Read-only permission, then authorize the app again.', permissions_required: 'All requested permissions are required to connect this account.', configuration_changed: 'The connection settings changed. Try connecting your account again.', - rate_limited: 'Too many authorization attempts. Wait a few minutes and try again.', + rate_limited: 'Authorization is being rate limited. Wait a few minutes and try again.', + provider_unavailable: + 'The account provider could not verify your account right now. Try connecting again in a few minutes.', unavailable: 'This connection is unavailable. Try connecting your account again.', failed: 'Account authorization did not complete. Try connecting your account again.', } as const diff --git a/apps/sim/lib/credential-groups/provider-adapter.ts b/apps/sim/lib/credential-groups/provider-adapter.ts index 30cd9b36357..897bcf9282b 100644 --- a/apps/sim/lib/credential-groups/provider-adapter.ts +++ b/apps/sim/lib/credential-groups/provider-adapter.ts @@ -5,6 +5,7 @@ import type { CredentialGroupOAuthAttempt } from '@/lib/credential-groups/oauth- import type { CredentialGroupProvider } from '@/lib/credential-groups/providers' import type { DbOrTx } from '@/lib/db/types' import type { RefreshTokenResult } from '@/lib/oauth' +import type { OAuthIdentityVerificationError } from '@/lib/oauth/identity-error' export interface CredentialGroupProviderPolicy { provider: CredentialGroupProvider @@ -83,7 +84,8 @@ export class CredentialGroupProviderConfigurationError extends Error { export class CredentialGroupOAuthError extends Error { constructor( message: string, - readonly statusCode: 400 | 401 | 403 | 404 | 409 | 502 | 503 + readonly statusCode: 400 | 401 | 403 | 404 | 409 | 502 | 503, + readonly identityFailure?: OAuthIdentityVerificationError ) { super(message) this.name = 'CredentialGroupOAuthError' diff --git a/apps/sim/lib/credential-groups/standard-oauth-provider.test.ts b/apps/sim/lib/credential-groups/standard-oauth-provider.test.ts index f603f6def12..ac06e6f834b 100644 --- a/apps/sim/lib/credential-groups/standard-oauth-provider.test.ts +++ b/apps/sim/lib/credential-groups/standard-oauth-provider.test.ts @@ -5,6 +5,7 @@ import { createHash } from 'node:crypto' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { CredentialGroupOAuthContext } from '@/lib/credential-groups/enrollments' import type { CredentialGroupOAuthAttempt } from '@/lib/credential-groups/oauth-state' +import { OAuthIdentityVerificationError } from '@/lib/oauth/identity-error' const { mockGetToken, mockVerifyIdentity } = vi.hoisted(() => ({ mockGetToken: vi.fn(), @@ -237,6 +238,31 @@ describe('standard OAuth Credential Group provider', () => { ).rejects.toMatchObject({ statusCode: 403 }) }) + it.each([ + new OAuthIdentityVerificationError('email_mismatch', 'emails'), + new OAuthIdentityVerificationError('email_access_denied', 'emails', 403), + new OAuthIdentityVerificationError('provider_unavailable', 'profile', 503), + ])('preserves safe identity diagnostics through managed authorization: %s', async (failure) => { + mockVerifyIdentity.mockRejectedValueOnce(failure) + const context = buildContext() + const policy = await adapter.getPolicy(context.option, { + workspaceId: context.workspaceId, + credentialGroupId: context.credentialGroupId, + }) + await expect( + adapter.exchangeAndVerify({ + context, + attempt: buildAttempt(policy.scopeVersion), + code: 'code-1', + policy, + }) + ).rejects.toMatchObject({ + name: 'CredentialGroupOAuthError', + statusCode: 502, + identityFailure: failure, + }) + }) + it('uses the existing Atlassian callback and state-bound identity verification', async () => { const requiredScopes = ['read:me', 'read:jira-work', 'offline_access'] const context: CredentialGroupOAuthContext = { diff --git a/apps/sim/lib/credential-groups/standard-oauth-provider.ts b/apps/sim/lib/credential-groups/standard-oauth-provider.ts index 4a303538ec0..4492ad3812d 100644 --- a/apps/sim/lib/credential-groups/standard-oauth-provider.ts +++ b/apps/sim/lib/credential-groups/standard-oauth-provider.ts @@ -26,6 +26,7 @@ import { import type { CredentialGroupStandardOAuthProvider } from '@/lib/credential-groups/providers' import { getCredentialGroupProviderService } from '@/lib/credential-groups/providers' import { refreshOAuthToken } from '@/lib/oauth' +import { OAuthIdentityVerificationError } from '@/lib/oauth/identity-error' const OAUTH_DISCOVERY_TIMEOUT_MS = 10_000 const OAUTH_DISCOVERY_MAX_BYTES = 256 * 1024 @@ -332,10 +333,11 @@ export function createStandardOAuthCredentialGroupProviderAdapter( clientId: current.connector.clientId, expectedEmail: context.email, }) - } catch { + } catch (error) { throw new CredentialGroupOAuthError( `${service.name} returned an invalid identity token.`, - 502 + 502, + error instanceof OAuthIdentityVerificationError ? error : undefined ) } const nonceMatches = diff --git a/apps/sim/lib/credential-groups/viewer-accounts.ts b/apps/sim/lib/credential-groups/viewer-accounts.ts new file mode 100644 index 00000000000..00c99548a2d --- /dev/null +++ b/apps/sim/lib/credential-groups/viewer-accounts.ts @@ -0,0 +1,57 @@ +import { db } from '@sim/db' +import { credential, credentialGroup, credentialGroupEnrollment } from '@sim/db/schema' +import { and, eq, inArray, isNull, type SQL } from 'drizzle-orm' +import { resourceScopeCondition } from '@/lib/core/resource-scope.server' +import { ORGANIZATION_VIEWER_ACCOUNT_LIMIT } from '@/lib/credential-groups/limits' + +/** Own grant metadata remains manageable even while provider setup or enrollment is disabled. */ +export async function listViewerOrganizationAccounts(input: { + organizationId: string + userId: string + matching: SQL +}) { + const scope = { kind: 'organization', organizationId: input.organizationId } as const + const accounts = await db + .select({ + credentialId: credential.id, + displayName: credential.displayName, + status: credential.managedOauthStatus, + groupId: credentialGroup.id, + optionId: credential.credentialGroupOptionId, + providerId: credential.providerId, + }) + .from(credential) + .innerJoin( + credentialGroupEnrollment, + eq(credentialGroupEnrollment.id, credential.credentialGroupEnrollmentId) + ) + .innerJoin(credentialGroup, eq(credentialGroup.id, credentialGroupEnrollment.credentialGroupId)) + .where( + and( + resourceScopeCondition(credential, scope), + resourceScopeCondition(credentialGroup, scope), + eq(credentialGroupEnrollment.userId, input.userId), + eq(credential.type, 'managed_oauth'), + inArray(credential.managedOauthStatus, ['active', 'needs_reauth']), + isNull(credential.revokedAt), + input.matching + ) + ) + .limit(ORGANIZATION_VIEWER_ACCOUNT_LIMIT + 1) + if (accounts.length > ORGANIZATION_VIEWER_ACCOUNT_LIMIT) + throw new Error('Too many personal accounts for this organization') + return accounts.map((account) => { + if ( + !account.optionId || + !account.providerId || + (account.status !== 'active' && account.status !== 'needs_reauth') + ) + throw new Error('Invalid personal account metadata') + return { + ...account, + optionId: account.optionId, + providerId: account.providerId, + status: account.status, + } + }) +} diff --git a/apps/sim/lib/knowledge/__integration__/github-member.integration.ts b/apps/sim/lib/knowledge/__integration__/github-member.integration.ts index 31cd2ddc32e..133ebdce20d 100644 --- a/apps/sim/lib/knowledge/__integration__/github-member.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/github-member.integration.ts @@ -52,6 +52,7 @@ import { closeRedisConnection, getRedisClient } from '@/lib/core/config/redis' import { encryptSecret } from '@/lib/core/security/encryption' import { resetStorageMethod } from '@/lib/core/storage' import { compileCredentialGroupWorkflowAccessPolicy } from '@/lib/credential-groups/application/workflow-access-policy' +import { buildOrganizationAccountAccessPolicy } from '@/lib/credential-groups/application/workspace-access-policy' import { completeCredentialGroupEnrollment, getCredentialGroupOAuthContext, @@ -77,13 +78,17 @@ import { createKnowledgeAccessProvider } from '@/lib/knowledge/access/scope' import { subjectToken } from '@/lib/knowledge/access/tokens' import { KnowledgeDocumentNotReadyError } from '@/lib/knowledge/application/chunk-errors' import { listKnowledgeChunks } from '@/lib/knowledge/application/chunks' +import { + createKnowledgeConnector, + listKnowledgeConnectorDocuments, +} from '@/lib/knowledge/application/connectors' import { readKnowledgeDocument } from '@/lib/knowledge/application/documents' import { readIndexedKnowledgeDocument } from '@/lib/knowledge/application/read-indexed-document' import { searchKnowledge } from '@/lib/knowledge/application/search' import { readSearchSourceOverview } from '@/lib/knowledge/application/search-source-overview' import { listSearchSources } from '@/lib/knowledge/application/search-sources' import { grantKnowledgeConnectorCredentialAccess } from '@/lib/knowledge/connectors/member-access' -import { executeMemberSync } from '@/lib/knowledge/connectors/member-sync-engine' +import * as memberSyncEngine from '@/lib/knowledge/connectors/member-sync-engine' import { MEMBER_SUSPENDED_PURGE_DAYS, MEMBER_TOMBSTONE_PURGE_DAYS, @@ -120,6 +125,7 @@ interface RepositoryFixture { deniedStatus: 403 | 404 throttledReaders: Set throttledBlobReaders: Set + failedBlobs: Set truncated: boolean } @@ -186,6 +192,7 @@ describe('fixture-backed GitHub member search in PostgreSQL', () => { deniedStatus: 404, throttledReaders: new Set(), throttledBlobReaders: new Set(), + failedBlobs: new Set(), truncated: false, } repositories.set(name, value) @@ -250,18 +257,22 @@ describe('fixture-backed GitHub member search in PostgreSQL', () => { if (url.pathname === '/app/installations/42/access_tokens') { expect(request.method).toBe('POST') const body = await request.json() - expect(body).toMatchObject({ - permissions: { contents: 'read', metadata: 'read' }, - }) - expect(body.repository_ids).toHaveLength(1) - const repositoryId = body.repository_ids[0] + const contentToken = body.permissions.contents === 'read' + expect(body.permissions).toEqual( + contentToken ? { contents: 'read', metadata: 'read' } : { metadata: 'read' } + ) + if (contentToken) expect(body.repository_ids).toHaveLength(1) + else expect(body.repositories).toHaveLength(1) + const repositoryId = contentToken + ? body.repository_ids[0] + : repositories.get(body.repositories[0])?.id expect( [...repositories.values()].some( (repository) => repository.id === repositoryId && repository.installed ) ).toBe(true) return Response.json({ - token: `ghs_fixture_installation_${repositoryId}`, + token: `ghs_fixture_${contentToken ? 'installation' : 'metadata'}_${repositoryId}`, expires_at: new Date(Date.now() + 60 * 60_000).toISOString(), permissions: body.permissions, repositories: [{ id: repositoryId }], @@ -275,7 +286,10 @@ describe('fixture-backed GitHub member search in PostgreSQL', () => { return Response.json(installation()) } if (request.method !== 'GET') throw new Error(`Unexpected GitHub method: ${request.method}`) - const installationRepository = bearer.match(/^ghs_fixture_installation_(\d+)$/)?.[1] + const installationRepository = bearer.match( + /^ghs_fixture_(?:installation|metadata)_(\d+)$/ + )?.[1] + const metadataToken = bearer.startsWith('ghs_fixture_metadata_') const installationToken = Boolean(installationRepository) const member = enrolled.members.find((candidate) => [tokenFor(candidate.userId), `${tokenFor(candidate.userId)}_refreshed`].some( @@ -330,6 +344,13 @@ describe('fixture-backed GitHub member search in PostgreSQL', () => { private: !source.public, default_branch: source.defaultBranch, }) + expect(metadataToken).toBe(false) + if (match[2].startsWith('/branches/')) { + const branch = decodeURIComponent(match[2].slice('/branches/'.length)) + return branch === source.defaultBranch + ? Response.json({ name: branch, commit: { sha: shaFor(branch) }, protected: false }) + : Response.json({ message: 'Not Found' }, { status: 404 }) + } if (match[2].startsWith('/git/ref/heads/')) { referenceObserved?.(match[1]) if (source.stallRef) @@ -377,6 +398,12 @@ describe('fixture-backed GitHub member search in PostgreSQL', () => { { status: 403 } ) const sha = decodeURIComponent(match[2].slice('/git/blobs/'.length)) + if (source.failedBlobs.has(sha)) { + return Response.json( + { message: 'Fixture provider unavailable' }, + { status: 503, headers: { 'Retry-After': '3600' } } + ) + } const content = [...source.files.values(), ...source.symlinks.values()].find( (value) => shaFor(value) === sha ) @@ -547,6 +574,7 @@ describe('fixture-backed GitHub member search in PostgreSQL', () => { await db.delete(workspace).where(eq(workspace.id, ids.workspaceId)) await db.delete(organization).where(eq(organization.id, ids.organizationId)) await db.delete(user).where(inArray(user.id, [ids.aliceId, ids.bobId])) + vi.restoreAllMocks() vi.unstubAllGlobals() } }) @@ -593,7 +621,7 @@ describe('fixture-backed GitHub member search in PostgreSQL', () => { .update(knowledgeConnectorMember) .set({ nextAttemptAt: new Date(0) }) .where(eq(knowledgeConnectorMember.connectorId, connectorId)) - return executeMemberSync(connectorId, { + return memberSyncEngine.executeMemberSync(connectorId, { billingAttribution: billing, forceContentRefresh, }) @@ -644,7 +672,7 @@ describe('fixture-backed GitHub member search in PostgreSQL', () => { } } - it('indexes an organization installation once and denies live user, app, and org revocations before search or reads', async () => { + async function useOrganizationInstallation() { organizationSource = true Object.assign(env, { GITHUB_APP_ID: '1', @@ -663,6 +691,19 @@ describe('fixture-backed GitHub member search in PostgreSQL', () => { .update(credentialGroup) .set({ workspaceId: null, organizationId: ids.organizationId }) .where(eq(credentialGroup.id, enrolled.groupId)) + await db + .update(resourcePolicy) + .set({ + workspaceId: null, + organizationId: ids.organizationId, + document: buildOrganizationAccountAccessPolicy(enrolled.groupId, []), + }) + .where( + and( + eq(resourcePolicy.resourceType, 'credential_group'), + eq(resourcePolicy.resourceId, enrolled.groupId) + ) + ) await db .update(credential) .set({ workspaceId: null, organizationId: ids.organizationId }) @@ -719,6 +760,231 @@ describe('fixture-backed GitHub member search in PostgreSQL', () => { actorUserId: ids.aliceId, organizationId: ids.organizationId, }) + return installationCredentialId + } + + it('reuses connected organization members for a later installation source without another enrollment', async () => { + const installationCredentialId = await useOrganizationInstallation() + expect((await sync()).error).toBeUndefined() + const [shared] = await rows() + const enrollmentsBefore = await db + .select({ + id: credentialGroupEnrollment.id, + userId: credentialGroupEnrollment.userId, + status: credentialGroupEnrollment.status, + }) + .from(credentialGroupEnrollment) + .where(eq(credentialGroupEnrollment.credentialGroupId, enrolled.groupId)) + .orderBy(credentialGroupEnrollment.id) + const credentialsBefore = await db + .select({ + id: credential.id, + enrollmentId: credential.credentialGroupEnrollmentId, + subjectId: credential.providerSubjectId, + }) + .from(credential) + .where(eq(credential.credentialGroupOptionId, enrolled.optionId)) + .orderBy(credential.id) + const later = repository('later', [ids.aliceId]) + const input = { + knowledgeBaseId: ids.knowledgeBaseId, + assertedOrganizationId: ids.organizationId, + connectorType: 'github', + accessMode: 'members' as const, + credentialId: installationCredentialId, + sourceConfig: { repository: 'fixture/later' }, + syncIntervalMinutes: 0, + } + await expect( + createKnowledgeConnector.execute({ principal: actor(ids.bobId), input }) + ).rejects.toThrow() + const dispatchedSync = vi.spyOn(memberSyncEngine, 'executeMemberSync') + const { connector } = await createKnowledgeConnector.execute({ + principal: actor(ids.aliceId), + input, + }) + try { + expect(dispatchedSync).toHaveBeenCalledExactlyOnceWith(connector.id, expect.any(Object)) + expect((await dispatchedSync.mock.results[0].value).error).toBeUndefined() + } finally { + dispatchedSync.mockRestore() + } + const connectorId = connector.id + expect(connector).toMatchObject({ + credentialGroupId: enrolled.groupId, + credentialGroupOptionId: enrolled.optionId, + sourceConfig: { repository: 'fixture/later', githubRepositoryId: String(later.id) }, + }) + const [current] = await db + .select() + .from(knowledgeConnector) + .where(eq(knowledgeConnector.id, connectorId)) + expect(current).toMatchObject({ memberSyncStatus: 'idle' }) + expect(current.lastMemberSyncAt).not.toBeNull() + const memberships = await db + .select() + .from(knowledgeConnectorMember) + .where(eq(knowledgeConnectorMember.connectorId, connectorId)) + expect(memberships).toHaveLength(2) + expect(memberships.map((row) => row.credentialId).sort()).toEqual( + credentialsBefore.map((row) => row.id).sort() + ) + expect( + await db + .select({ + id: credentialGroupEnrollment.id, + userId: credentialGroupEnrollment.userId, + status: credentialGroupEnrollment.status, + }) + .from(credentialGroupEnrollment) + .where(eq(credentialGroupEnrollment.credentialGroupId, enrolled.groupId)) + .orderBy(credentialGroupEnrollment.id) + ).toEqual(enrollmentsBefore) + expect( + await db + .select({ + id: credential.id, + enrollmentId: credential.credentialGroupEnrollmentId, + subjectId: credential.providerSubjectId, + }) + .from(credential) + .where(eq(credential.credentialGroupOptionId, enrolled.optionId)) + .orderBy(credential.id) + ).toEqual(credentialsBefore) + const [indexed] = await rows(connectorId) + expect(await search(actor(ids.aliceId))).toEqual([shared.id, indexed.id].sort()) + expect(await search(actor(ids.bobId))).toEqual([shared.id]) + await assertAccess(actor(ids.aliceId), indexed, true) + await assertAccess(actor(ids.bobId), indexed, false) + const indexedRead = (userId: string) => + readIndexedKnowledgeDocument.execute({ + principal: actor(userId), + input: { + organizationId: ids.organizationId, + target: { kind: 'id', documentId: indexed.id }, + limit: 10, + resultSecretRegistry: new ResolvedSecretTraceRegistry(), + }, + }) + expect( + (await indexedRead(ids.aliceId)).chunks?.map((chunk) => chunk.content).join('\n') + ).toContain('Orion later') + await expect(indexedRead(ids.bobId)).rejects.toThrow('Document not found') + /** Organization cache bytes are internal; members read through the authorized Search operation. */ + await expect( + downloadFileFromUrl(indexed.fileUrl, { userId: ids.aliceId, knowledgeAccess: 'user' }) + ).rejects.toThrow('Access denied') + await expect( + downloadFileFromUrl(indexed.fileUrl, { userId: ids.bobId, knowledgeAccess: 'user' }) + ).rejects.toThrow('Access denied') + for (const userId of [ids.aliceId, ids.bobId]) { + const { sources } = await listSearchSources.execute({ + principal: actor(userId), + input: { organizationId: ids.organizationId, connectorId }, + }) + expect(sources).toMatchObject([ + { + connectorId, + viewerMembership: 'connected', + viewerDocumentCount: userId === ids.aliceId ? 1 : 0, + }, + ]) + } + expect( + requests + .filter((entry) => entry.path.startsWith('/repos/fixture/later/git/blobs/')) + .map((entry) => entry.userId) + ).toEqual(['installation']) + }) + + it('keeps actual skips, legacy skips, and provider failures distinct in authorized lists and source counts', async () => { + await useOrganizationInstallation() + const source = repositories.get('shared')! + source.readers.delete(ids.bobId) + source.files.set('empty.txt', '') + source.files.set('image.png', 'binary\0contents') + expect((await sync()).error).toBeUndefined() + const initial = await rows() + const empty = initial.find((row) => row.externalId === 'empty.txt')! + const binary = initial.find((row) => row.externalId === 'image.png')! + expect(empty).toMatchObject({ processingStatus: 'failed', storageKey: null }) + expect(binary).toMatchObject({ processingStatus: 'failed', storageKey: null }) + expect(empty.contentHash).not.toBeNull() + await db.update(document).set({ processingStatus: 'failed' }).where(eq(document.id, empty.id)) + const summary = async (userId: string) => + ( + await listSearchSources.execute({ + principal: actor(userId), + input: { organizationId: ids.organizationId, connectorId: enrolled.connectorId }, + }) + ).sources[0] + expect(await summary(ids.aliceId)).toMatchObject({ + viewerDocumentCount: 1, + viewerFailedDocumentCount: 0, + hasSyncError: false, + }) + source.files.set('unavailable.txt', 'Orion content whose blob cannot be fetched.') + source.failedBlobs.add(shaFor(source.files.get('unavailable.txt')!)) + await sync() + await db.update(document).set({ processingStatus: 'failed' }).where(eq(document.id, empty.id)) + const failed = (await rows()).find((row) => row.externalId === 'unavailable.txt')! + expect(failed).toMatchObject({ + processingStatus: 'failed', + storageKey: null, + contentHash: null, + }) + for (const userId of [ids.aliceId, ids.bobId]) { + const provider = createKnowledgeAccessProvider(actor(userId), { + organizationId: ids.organizationId, + knowledgeBaseIds: [ids.knowledgeBaseId], + }) + const listed = await getDocuments(ids.knowledgeBaseId, {}, 'github-skip-outcomes', provider) + expect(listed.pagination.total).toBe(userId === ids.aliceId ? 4 : 0) + if (userId === ids.aliceId) { + expect(listed.documents).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: empty.id, + processingStatus: 'failed', + processingOutcome: 'skipped', + }), + expect.objectContaining({ + id: binary.id, + processingStatus: 'failed', + processingOutcome: 'skipped', + }), + expect.objectContaining({ id: failed.id, processingStatus: 'failed' }), + ]) + ) + } + for (const filter of ['failed', 'skipped'] as const) { + const outcomes = await listKnowledgeConnectorDocuments.execute({ + principal: actor(userId), + input: { + connectorId: enrolled.connectorId, + knowledgeBaseId: ids.knowledgeBaseId, + filter, + }, + }) + expect(outcomes.counts).toMatchObject({ + failed: userId === ids.aliceId ? 1 : 0, + skipped: userId === ids.aliceId ? 2 : 0, + }) + expect(outcomes.documents.map((row) => row.id).sort()).toEqual( + userId === ids.aliceId + ? (filter === 'failed' ? [failed.id] : [empty.id, binary.id]).sort() + : [] + ) + } + expect(await summary(userId)).toMatchObject({ + viewerDocumentCount: userId === ids.aliceId ? 1 : 0, + viewerFailedDocumentCount: userId === ids.aliceId ? 1 : 0, + }) + } + }) + + it('indexes an organization installation once and denies live user, app, and org revocations before search or reads', async () => { + const installationCredentialId = await useOrganizationInstallation() const unrelatedSources = Array.from({ length: 105 }, () => generateId()) await db.insert(knowledgeConnector).values( unrelatedSources.map((id) => ({ diff --git a/apps/sim/lib/knowledge/__integration__/organization-search-overview.integration.ts b/apps/sim/lib/knowledge/__integration__/organization-search-overview.integration.ts index b93f9ca5709..1ec01b7aff5 100644 --- a/apps/sim/lib/knowledge/__integration__/organization-search-overview.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/organization-search-overview.integration.ts @@ -153,7 +153,14 @@ beforeEach(async () => { .where(eq(knowledgeConnectorMember.id, memberId)) await db .update(document) - .set({ processingStatus: 'completed', enabled: true, userExcluded: false }) + .set({ + processingStatus: 'completed', + enabled: true, + userExcluded: false, + contentHash: null, + storageKey: null, + fileUrl: 'https://fixture.test/private', + }) .where(eq(document.id, documentId)) }) @@ -276,9 +283,44 @@ describe('organization operational overview with real SQL', () => { }) expect(await provider('gmail')).toMatchObject({ status: 'active' }) }) - it('reports inaccessible processing failures without exposing documents, and ignores excluded failures', async () => { - await db.update(document).set({ processingStatus: 'failed' }).where(eq(document.id, documentId)) - expect(await provider('google_drive')).toMatchObject({ status: 'needs_attention' }) + it('ignores intentional skips while reporting inaccessible source and indexing failures', async () => { + await db + .update(document) + .set({ processingStatus: 'failed', contentHash: 'immutable-sha', fileUrl: '' }) + .where(eq(document.id, documentId)) + expect(await provider('google_drive')).toMatchObject({ + status: 'active', + issue: null, + isSyncing: false, + }) + await db + .update(knowledgeConnector) + .set({ lastSyncError: 'previous sync failed' }) + .where(eq(knowledgeConnector.id, driveId)) + expect(await provider('google_drive')).toMatchObject({ + status: 'needs_attention', + issue: 'sync_failed', + isSyncing: false, + }) + await db + .update(knowledgeConnector) + .set({ lastSyncError: null }) + .where(eq(knowledgeConnector.id, driveId)) + await db.update(document).set({ contentHash: null }).where(eq(document.id, documentId)) + expect(await provider('google_drive')).toMatchObject({ + status: 'needs_attention', + issue: 'document_indexing_failed', + isSyncing: false, + }) + await db + .update(document) + .set({ contentHash: 'immutable-sha', storageKey: 'fixture-retained-artifact' }) + .where(eq(document.id, documentId)) + expect(await provider('google_drive')).toMatchObject({ + status: 'needs_attention', + issue: 'document_indexing_failed', + isSyncing: false, + }) await db.update(document).set({ userExcluded: true }).where(eq(document.id, documentId)) expect(await provider('google_drive')).toMatchObject({ status: 'active' }) await db diff --git a/apps/sim/lib/knowledge/__integration__/search-source-progress.integration.ts b/apps/sim/lib/knowledge/__integration__/search-source-progress.integration.ts index 5a82344815a..8e1cc0d70b6 100644 --- a/apps/sim/lib/knowledge/__integration__/search-source-progress.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/search-source-progress.integration.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { document, + embedding, knowledgeBase, knowledgeConnector, organization, @@ -9,16 +10,29 @@ import { workspace, } from '@sim/db/schema' import { generateId } from '@sim/utils/id' -import { and, eq } from 'drizzle-orm' -import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { and, eq, inArray } from 'drizzle-orm' +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' import type { ConnectorDocumentFilter } from '@/lib/api/contracts/knowledge/connectors' +import * as embeddings from '@/lib/embeddings' import { createKnowledgeAclFixtureIds, seedKnowledgeAclFixture, } from '@/lib/knowledge/__integration__/seed-source-access-fixture' -import { listKnowledgeConnectorDocuments } from '@/lib/knowledge/application/connectors' +import { + deleteKnowledgeConnector, + listKnowledgeConnectorDocuments, +} from '@/lib/knowledge/application/connectors' +import { + listKnowledgeDocuments, + readKnowledgeDocument, + updateKnowledgeDocument, +} from '@/lib/knowledge/application/documents' import { readSearchSourceProgress } from '@/lib/knowledge/application/search-source-progress' import { listSearchSources } from '@/lib/knowledge/application/search-sources' +import { createContentSyncLease } from '@/lib/knowledge/connectors/sync-lock' +import { persistSkippedDocuments } from '@/lib/knowledge/connectors/sync-persistence' +import * as documentProcessor from '@/lib/knowledge/documents/document-processor' +import { processDocumentAsync, retryDocumentProcessing } from '@/lib/knowledge/documents/service' const ids = createKnowledgeAclFixtureIds() const alice = { kind: 'session' as const, userId: ids.aliceId, sessionId: 'fixture-alice' } @@ -207,14 +221,15 @@ describe('connector document filename search and document sets', () => { active: [failureId, ...planIds], excluded: [excludedId], failed: [failureId], + skipped: [], } - for (const filter of ['active', 'excluded', 'failed'] as const) { + for (const filter of ['active', 'excluded', 'failed', 'skipped'] as const) { const result = await listKnowledgeConnectorDocuments.execute({ principal: viewer, input: { ...scope, filter, search: ' nEeDle ' }, }) expect(result.documents.map((row) => row.id)).toEqual(expectedIds[filter]) - expect(result.counts).toEqual({ active: 3, excluded: 1, failed: 1 }) + expect(result.counts).toEqual({ active: 3, excluded: 1, failed: 1, skipped: 0 }) expect(result.hasMore).toBe(false) } }) @@ -244,7 +259,7 @@ describe('connector document filename search and document sets', () => { input: { ...scope, filter: 'active', search }, }) expect(result.documents.map((row) => row.id)).toEqual([id]) - expect(result.counts).toEqual({ active: 1, excluded: 0, failed: 0 }) + expect(result.counts).toEqual({ active: 1, excluded: 0, failed: 0, skipped: 0 }) } }) @@ -269,7 +284,7 @@ describe('connector document filename search and document sets', () => { input: { ...scope, filter: 'active', search: 'needle' }, }) expect(privateResult.documents.map((row) => row.id)).toEqual([privateId]) - expect(privateResult.counts).toEqual({ active: 1, excluded: 0, failed: 1 }) + expect(privateResult.counts).toEqual({ active: 1, excluded: 0, failed: 1, skipped: 0 }) await db .delete(permissions) @@ -284,3 +299,362 @@ describe('connector document filename search and document sets', () => { ).rejects.toThrow('Insufficient workspace permissions') }) }) + +describe('intentional skips and genuine failures across document reads', () => { + const fixture = createKnowledgeAclFixtureIds() + const viewer = { + kind: 'session' as const, + userId: fixture.aliceId, + sessionId: 'fixture-outcomes', + } + const otherViewer = { ...viewer, userId: fixture.bobId } + const legacySkipId = generateId() + const sourceFailureId = generateId() + const indexingFailureId = generateId() + const privateLegacySkipId = generateId() + const scope = { knowledgeBaseId: fixture.knowledgeBaseId, connectorId: fixture.connectorId } + const expectedCounts = { active: 4, excluded: 0, failed: 2, skipped: 2 } + const skipIds = [legacySkipId] + const failureIds = [sourceFailureId, indexingFailureId] + const privateIds = [privateLegacySkipId] + + beforeAll(async () => { + await seedKnowledgeAclFixture(fixture, { connectorType: 'google_drive' }) + await db + .update(knowledgeBase) + .set({ isSearchIndex: true }) + .where(eq(knowledgeBase.id, fixture.knowledgeBaseId)) + const rows: Array & { id: string; filename: string }> = [ + { + id: legacySkipId, + filename: 'outcome-a-legacy.png', + fileUrl: '', + uploadedAt: new Date('2026-01-01T00:00:00Z'), + }, + { + id: sourceFailureId, + filename: 'outcome-c-source.txt', + contentHash: null, + fileUrl: '', + processingError: 'Source download unavailable', + }, + { + id: indexingFailureId, + filename: 'outcome-d-indexing.txt', + storageKey: 'fixture-retained-artifact', + processingError: 'Embedding provider unavailable', + }, + { + id: privateLegacySkipId, + filename: 'outcome-e-private-legacy.png', + fileUrl: '', + acl: [`u:${fixture.bobId}@fixture.test`], + }, + ] + await db.insert(document).values( + rows.map((row) => ({ + knowledgeBaseId: fixture.knowledgeBaseId, + connectorId: fixture.connectorId, + externalId: row.id, + fileUrl: `https://fixture.test/${row.id}`, + fileSize: 10, + mimeType: 'text/plain', + processingStatus: 'failed', + contentHash: `git-sha:${row.id}`, + storageKey: null, + processingError: 'A source omission reason whose wording may change', + acl: [`u:${fixture.aliceId}@fixture.test`], + aclVerifiedAt: new Date(), + ...row, + })) + ) + for (const [externalId, title, userId, documentIds] of [ + ['current-skip', 'outcome-b-skipped.png', fixture.aliceId, skipIds], + ['private-current-skip', 'outcome-f-private-skipped.png', fixture.bobId, privateIds], + ] as const) { + const [persisted] = await persistSkippedDocuments( + fixture.knowledgeBaseId, + fixture.connectorId, + 'google_drive', + [ + { + type: 'skip', + extDoc: { + externalId, + title, + content: '', + mimeType: 'text/plain', + contentHash: `git-sha:${externalId}`, + skippedReason: 'Current worker intentionally omitted this file', + }, + }, + ], + undefined, + 'admin', + createContentSyncLease(fixture.connectorId, fixture.lockId) + ) + documentIds.push(persisted.documentId) + await db + .update(document) + .set({ acl: [`u:${userId}@fixture.test`], aclVerifiedAt: new Date() }) + .where(eq(document.id, persisted.documentId)) + } + await db + .update(knowledgeConnector) + .set({ status: 'active', syncLockToken: null }) + .where(eq(knowledgeConnector.id, fixture.connectorId)) + }) + + afterAll(async () => { + await db.delete(workspace).where(eq(workspace.id, fixture.workspaceId)) + await db.delete(organization).where(eq(organization.id, fixture.organizationId)) + await db.delete(user).where(eq(user.id, fixture.aliceId)) + await db.delete(user).where(eq(user.id, fixture.bobId)) + }) + + it('projects legacy and current skips before authorized filtering, counting, and pagination', async () => { + const expectedIds: Record = { + active: [...skipIds, ...failureIds], + excluded: [], + failed: failureIds, + skipped: skipIds, + } + for (const filter of ['active', 'excluded', 'failed', 'skipped'] as const) { + const result = await listKnowledgeConnectorDocuments.execute({ + principal: viewer, + input: { ...scope, filter, search: 'outcome-' }, + }) + expect(result.documents.map((row) => row.id)).toEqual(expectedIds[filter]) + expect(result.counts).toEqual(expectedCounts) + expect(result.hasMore).toBe(false) + for (const row of result.documents) { + expect(row.processingStatus).toBe('failed') + expect(row.processingOutcome).toBe(skipIds.includes(row.id) ? 'skipped' : null) + expect(row.processingError).toBeTruthy() + } + } + const pages = await Promise.all( + [0, 1].map((offset) => + listKnowledgeConnectorDocuments.execute({ + principal: viewer, + input: { ...scope, filter: 'skipped', limit: 1, offset }, + }) + ) + ) + expect(pages.flatMap((page) => page.documents.map((row) => row.id))).toEqual(skipIds) + expect(pages.map((page) => page.hasMore)).toEqual([true, false]) + expect(pages.map((page) => page.counts)).toEqual([expectedCounts, expectedCounts]) + const legacyFailures = await listKnowledgeConnectorDocuments.execute({ + principal: viewer, + input: { ...scope, failedOnly: true }, + }) + expect(legacyFailures.documents.map((row) => row.id)).toEqual(failureIds) + }) + + it('does not turn another viewer’s skips into indexing errors or expose their documents', async () => { + for (const [principal, failedCount] of [ + [viewer, 2], + [otherViewer, 0], + ] as const) { + const sources = await listSearchSources.execute({ + principal, + input: { workspaceId: fixture.workspaceId }, + }) + expect(sources.sources[0].viewerFailedDocumentCount).toBe(failedCount) + const progress = await readSearchSourceProgress.execute({ + principal, + input: { workspaceId: fixture.workspaceId, connectorIds: [fixture.connectorId] }, + }) + expect(progress.sources).toEqual([ + { + connectorId: fixture.connectorId, + isSyncing: false, + hasSyncError: false, + hasIndexingError: failedCount > 0, + }, + ]) + } + for (const filter of ['active', 'skipped', 'failed'] as const) { + const result = await listKnowledgeConnectorDocuments.execute({ + principal: otherViewer, + input: { ...scope, filter }, + }) + expect(result.documents.map((row) => row.id)).toEqual(filter === 'failed' ? [] : privateIds) + expect(result.counts).toEqual({ active: 2, excluded: 0, failed: 0, skipped: 2 }) + } + await expect( + readKnowledgeDocument.execute({ + principal: viewer, + input: { knowledgeBaseId: fixture.knowledgeBaseId, documentId: privateLegacySkipId }, + }) + ).rejects.toThrow('Document not found') + await expect( + readKnowledgeDocument.execute({ + principal: otherViewer, + input: { knowledgeBaseId: fixture.knowledgeBaseId, documentId: sourceFailureId }, + }) + ).rejects.toThrow('Document not found') + }) + + it('uses the same status for the regular knowledge base list and document detail', async () => { + const result = await listKnowledgeDocuments.execute({ + principal: viewer, + input: { knowledgeBaseId: fixture.knowledgeBaseId }, + }) + expect(result.documents.map((row) => row.id).sort()).toEqual([...skipIds, ...failureIds].sort()) + for (const row of result.documents) { + const expectedOutcome = skipIds.includes(row.id) ? 'skipped' : null + expect(row.processingStatus).toBe('failed') + expect(row.processingOutcome).toBe(expectedOutcome) + const detail = await readKnowledgeDocument.execute({ + principal: viewer, + input: { knowledgeBaseId: fixture.knowledgeBaseId, documentId: row.id }, + }) + expect(detail.document.processingStatus).toBe('failed') + expect(detail.document.processingOutcome).toBe(expectedOutcome) + expect(detail.document.processingError).toBe(row.processingError) + } + }) + + it('rejects skipped and source-failed retries without changing stored outcomes or embedding dispatch', async () => { + const embed = vi + .spyOn(embeddings, 'embedKnowledge') + .mockRejectedValue(new Error('Unexpected embedding dispatch')) + const retryIds = [...skipIds, sourceFailureId] + const before = await db.select().from(document).where(inArray(document.id, retryIds)) + try { + for (const documentId of retryIds) { + await expect( + updateKnowledgeDocument.execute({ + principal: viewer, + input: { knowledgeBaseId: fixture.knowledgeBaseId, documentId, retryProcessing: true }, + }) + ).rejects.toThrow( + documentId === sourceFailureId ? 'Sync the connector' : 'intentionally skipped' + ) + } + for (const documentId of skipIds) { + const stored = before.find((row) => row.id === documentId)! + const result = await retryDocumentProcessing( + fixture.knowledgeBaseId, + documentId, + stored, + 'fixture-skipped-retry', + undefined + ) + expect(result).toMatchObject({ success: false, status: 'skipped' }) + expect(result.message).toContain('intentionally skipped') + } + const after = await db.select().from(document).where(inArray(document.id, retryIds)) + expect(after.sort((left, right) => left.id.localeCompare(right.id))).toEqual( + before.sort((left, right) => left.id.localeCompare(right.id)) + ) + expect( + await db.select().from(embedding).where(inArray(embedding.documentId, retryIds)) + ).toEqual([]) + expect(embed).not.toHaveBeenCalled() + } finally { + embed.mockRestore() + } + }) + + it.each([false, true])( + 'preserves legacy and current skips against delayed tokenless workers (missing context: %s)', + async (missingContext) => { + const processor = vi + .spyOn(documentProcessor, 'processDocument') + .mockRejectedValue(new Error('Unexpected skipped document processing')) + const embed = vi + .spyOn(embeddings, 'embedKnowledge') + .mockRejectedValue(new Error('Unexpected embedding dispatch')) + const before = await db.select().from(document).where(inArray(document.id, skipIds)) + try { + if (missingContext) { + await db + .update(knowledgeBase) + .set({ deletedAt: new Date() }) + .where(eq(knowledgeBase.id, fixture.knowledgeBaseId)) + } + for (const row of before) { + await processDocumentAsync( + fixture.knowledgeBaseId, + row.id, + row, + {}, + undefined, + 'fixture-delayed-tokenless-worker', + { chargedAtDispatch: false } + ) + } + const after = await db.select().from(document).where(inArray(document.id, skipIds)) + expect(after.sort((left, right) => left.id.localeCompare(right.id))).toEqual( + before.sort((left, right) => left.id.localeCompare(right.id)) + ) + expect(processor).not.toHaveBeenCalled() + expect(embed).not.toHaveBeenCalled() + } finally { + if (missingContext) { + await db + .update(knowledgeBase) + .set({ deletedAt: null }) + .where(eq(knowledgeBase.id, fixture.knowledgeBaseId)) + } + processor.mockRestore() + embed.mockRestore() + } + } + ) + + it('preserves skip outcomes and real failures when a workspace source is removed with documents kept', async () => { + await expect( + deleteKnowledgeConnector.execute({ + principal: viewer, + input: { ...scope, deleteDocuments: false }, + }) + ).rejects.toThrow('cannot be kept') + await db + .update(knowledgeBase) + .set({ isSearchIndex: false }) + .where(eq(knowledgeBase.id, fixture.knowledgeBaseId)) + await db + .update(knowledgeConnector) + .set({ accessMode: 'workspace' }) + .where(eq(knowledgeConnector.id, fixture.connectorId)) + /** Workspace syncs write workspace ACLs; detached rows must not retain mirrored grants. */ + await db + .update(document) + .set({ acl: ['ws'], aclRequirements: [] }) + .where(eq(document.connectorId, fixture.connectorId)) + const result = await deleteKnowledgeConnector.execute({ + principal: viewer, + input: { ...scope, deleteDocuments: false }, + }) + expect(result).toMatchObject({ documentsDeleted: 0, documentsKept: 6 }) + const retained = await db + .select() + .from(document) + .where(eq(document.knowledgeBaseId, fixture.knowledgeBaseId)) + expect(retained).toHaveLength(6) + for (const row of retained) { + expect(row.connectorId).toBeNull() + expect(row.acl).toEqual(['ws']) + expect(row.processingStatus).toBe('failed') + } + for (const documentId of skipIds) { + for (const principal of [viewer, otherViewer]) { + const detail = await readKnowledgeDocument.execute({ + principal, + input: { knowledgeBaseId: fixture.knowledgeBaseId, documentId }, + }) + expect(detail.document.processingStatus).toBe('failed') + expect(detail.document.processingOutcome).toBe('skipped') + } + await expect( + updateKnowledgeDocument.execute({ + principal: viewer, + input: { knowledgeBaseId: fixture.knowledgeBaseId, documentId, retryProcessing: true }, + }) + ).rejects.toThrow('intentionally skipped') + } + }) +}) diff --git a/apps/sim/lib/knowledge/application/connectors.test.ts b/apps/sim/lib/knowledge/application/connectors.test.ts index b631ac8fb93..22fbb094de6 100644 --- a/apps/sim/lib/knowledge/application/connectors.test.ts +++ b/apps/sim/lib/knowledge/application/connectors.test.ts @@ -4,7 +4,7 @@ import { document, knowledgeConnector, member } from '@sim/db/schema' import { dbChainMockFns, queueTableRows, resetDbChainMock } from '@sim/testing' -import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ resolveKnowledgeBase: vi.fn(), @@ -151,7 +151,9 @@ vi.mock('@/connectors/registry.server', () => ({ }, })) +import { internalOrchestrationErrorPolicy } from '@/lib/api/server/routes/internal-json-route' import { OrchestrationError } from '@/lib/core/orchestration/types' +import * as encryption from '@/lib/core/security/encryption' import { createApprovedSearchSource, createKnowledgeConnector, @@ -165,6 +167,7 @@ import { validateConnectorSourceConfig, } from '@/lib/knowledge/application/connectors' import { MAX_KNOWLEDGE_CONNECTOR_DOCUMENT_SEARCH_LENGTH } from '@/lib/knowledge/constants' +import * as githubInstallation from '@/lib/oauth/github-installation' import { capabilityRefusal } from '@/lib/permission-groups/capability-assertions' import { DEFAULT_PERMISSION_GROUP_CONFIG } from '@/lib/permission-groups/fields' import { confluenceConnectorMeta } from '@/connectors/confluence/meta' @@ -926,7 +929,7 @@ describe('knowledge connector application use cases', () => { mocks.resolveConnector.mockResolvedValueOnce(sameWorkspaceContext) queueTableRows(document, [{ value: 5 }]) queueTableRows(document, [{ value: 2 }]) - queueTableRows(document, [{ value: 1 }]) + queueTableRows(document, [{ failed: 1, skipped: 3 }]) queueTableRows(document, [ { id: 'document-3', filename: 'c.txt', userExcluded: false }, { id: 'document-4', filename: 'd.txt', userExcluded: true }, @@ -949,7 +952,7 @@ describe('knowledge connector application use cases', () => { { id: 'document-3', filename: 'c.txt', userExcluded: false }, { id: 'document-4', filename: 'd.txt', userExcluded: true }, ], - counts: { active: 5, excluded: 2, failed: 1 }, + counts: { active: 5, excluded: 2, failed: 1, skipped: 3 }, hasMore: false, offset: 2, limit: 2, @@ -1634,3 +1637,190 @@ describe('organization connector credential authorization', () => { expect(mocks.validateConnectorConfig).not.toHaveBeenCalled() }) }) + +describe('GitHub installation source rejection at the application boundary', () => { + const principal = { kind: 'session', userId: 'org-admin', sessionId: 'session' } as const + const sourceConfig = { repository: 'example/private', githubRepositoryId: '123' } + const credential = { + id: 'installation-credential', + organizationId: 'org', + workspaceId: null, + providerId: 'github-app-installation', + type: 'service_account', + encryptedServiceAccountKey: 'encrypted-binding', + providerSubjectId: '42', + providerTenantId: '7', + revokedAt: null, + } + const connector = { + id: 'source', + connectorType: 'github', + credentialId: credential.id, + accessMode: 'members' as const, + sourceConfig, + encryptedApiKey: null, + } + const createInput = { + knowledgeBaseId: 'org-index', + assertedOrganizationId: 'org', + connectorType: 'github', + credentialId: credential.id, + accessMode: 'members' as const, + sourceConfig, + syncIntervalMinutes: 60, + } + const validationMessage = + 'Check that the repository is included in the selected GitHub App installation, then retry.' + + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + queueTableRows(member, [{ role: 'admin' }]) + queueTableRows(member, [{ role: 'admin' }]) + const context = { + organizationId: 'org', + knowledgeBaseId: 'org-index', + knowledgeBase: { id: 'org-index', name: 'Search', isSearchIndex: true }, + } + mocks.resolveKnowledgeBase.mockResolvedValue(context) + mocks.resolveConnector.mockResolvedValue({ ...context, connectorId: connector.id, connector }) + mocks.getUserPermissionConfig.mockResolvedValue(null) + mocks.authorizeOrganizationCredentialUse.mockResolvedValue({ credential }) + mocks.resolveTokenIdentity.mockResolvedValue({ kind: 'service_account' }) + mocks.resolveTokenBundle.mockResolvedValue({ accessToken: 'repository-token' }) + mocks.resolveMembersBinding.mockResolvedValue({ + credentialGroupId: 'group', + credentialGroupOptionId: 'option', + organizationId: 'org', + sourceConfig, + }) + vi.spyOn(encryption, 'decryptSecret').mockResolvedValue({ + decrypted: JSON.stringify({ + type: 'github_app_installation', + version: 1, + appId: '1', + appClientId: 'app-client', + installationId: '42', + accountId: '7', + accountType: 'Organization', + accountLogin: 'example', + repositorySelection: 'selected', + }), + }) + vi.spyOn(githubInstallation, 'resolveGitHubInstallationRepository').mockResolvedValue({ + id: '123', + fullName: sourceConfig.repository, + defaultBranch: 'main', + }) + mocks.createConnector.mockImplementation( + async (input: { resolveAccessToken(id: string): Promise }) => { + await input.resolveAccessToken(credential.id) + throw new Error('Unexpected connector persistence') + } + ) + mocks.updateConnector.mockImplementation( + async (input: { + prepareSourceConfig( + currentConnector: typeof connector, + config: typeof sourceConfig + ): Promise + validateSourceConfig( + currentConnector: typeof connector, + config: typeof sourceConfig + ): Promise + }) => { + const prepared = await input.prepareSourceConfig(connector, sourceConfig) + await input.validateSourceConfig(connector, prepared) + throw new Error('Unexpected connector persistence') + } + ) + }) + + afterEach(() => { + vi.restoreAllMocks() + resetDbChainMock() + }) + + it.each([ + [422, 'repository-token'], + [404, 'repository'], + ] as const)( + 'returns a safe validation response for GitHub %s during %s', + async (status, operation) => { + vi.mocked(githubInstallation.resolveGitHubInstallationRepository).mockRejectedValueOnce( + new githubInstallation.GitHubInstallationError( + 'private provider payload', + status, + operation + ) + ) + const error = await createKnowledgeConnector + .execute({ principal, input: createInput }) + .catch((error: unknown) => error) + expect(error).toBeInstanceOf(OrchestrationError) + expect(internalOrchestrationErrorPolicy.project(error)).toMatchObject({ + status: 400, + body: { error: validationMessage }, + }) + expect(mocks.authorizeOrganizationCredentialUse).toHaveBeenCalledWith( + expect.objectContaining({ principal, organizationId: 'org', credentialId: credential.id }) + ) + expect(mocks.createConnector).not.toHaveBeenCalled() + expect(mocks.recordAudit).not.toHaveBeenCalled() + } + ) + + it.each(['create', 'update'] as const)( + 'classifies a repository token rejected during authorized %s validation', + async (operation) => { + mocks.resolveTokenBundle.mockRejectedValueOnce( + new githubInstallation.GitHubInstallationError( + 'private token response', + 422, + 'repository-token' + ) + ) + const result = + operation === 'create' + ? createKnowledgeConnector.execute({ principal, input: createInput }) + : updateKnowledgeConnector.execute({ + principal, + input: { + knowledgeBaseId: 'org-index', + connectorId: connector.id, + updates: { sourceConfig }, + }, + }) + await expect(result).rejects.toMatchObject({ code: 'validation', message: validationMessage }) + expect(mocks.resolveTokenBundle).toHaveBeenCalledOnce() + expect(mocks.recordAudit).not.toHaveBeenCalled() + } + ) + + it.each([403, 429, 503])( + 'preserves GitHub %s and its operation for retry classification', + async (status) => { + const failure = new githubInstallation.GitHubInstallationError( + 'GitHub provider request failed', + status, + 'repository-token' + ) + mocks.resolveTokenBundle.mockRejectedValueOnce(failure) + await expect( + createKnowledgeConnector.execute({ principal, input: createInput }) + ).rejects.toBe(failure) + expect(internalOrchestrationErrorPolicy.project(failure)).toBeNull() + expect(mocks.recordAudit).not.toHaveBeenCalled() + } + ) + + it('preserves network failures during authorized source creation', async () => { + const failure = new TypeError('Network request failed') + mocks.resolveTokenBundle.mockRejectedValueOnce(failure) + await expect(createKnowledgeConnector.execute({ principal, input: createInput })).rejects.toBe( + failure + ) + expect(internalOrchestrationErrorPolicy.project(failure)).toBeNull() + expect(mocks.recordAudit).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/knowledge/application/connectors.ts b/apps/sim/lib/knowledge/application/connectors.ts index dbe6747d077..f4deff13f95 100644 --- a/apps/sim/lib/knowledge/application/connectors.ts +++ b/apps/sim/lib/knowledge/application/connectors.ts @@ -43,6 +43,7 @@ import { resolveActiveKnowledgeResourceContext, resolveKnowledgeWorkspaceContext, } from '@/lib/knowledge/application/contexts' +import { rethrowGitHubInstallationSourceError } from '@/lib/knowledge/application/github-installation-error' import { prepareGitHubInstallationSource } from '@/lib/knowledge/application/github-installation-source' import { knowledgeOperations } from '@/lib/knowledge/application/operations' import { @@ -79,6 +80,11 @@ import { MAX_KNOWLEDGE_CONNECTOR_DOCUMENT_PAGE_SIZE, MAX_KNOWLEDGE_CONNECTOR_DOCUMENT_SEARCH_LENGTH, } from '@/lib/knowledge/constants' +import { + documentProcessingOutcomeSelection, + failedDocumentCondition, + skippedDocumentCondition, +} from '@/lib/knowledge/documents/processing-status' import { type ResolvedMembersBinding, resolveKnowledgeConnectorMembersBinding, @@ -322,7 +328,7 @@ export async function resolveConnectorCredentialAccessToken(input: { userId: identity.kind === 'oauth' ? identity.userId : input.actingUserId, requestId: input.requestId, sourceConfig: input.sourceConfig, - }) + }).catch(rethrowGitHubInstallationSourceError) return resolved } @@ -420,7 +426,7 @@ export async function validateConnectorSourceConfig(input: { userId: tokenUserId, requestId: input.requestId, sourceConfig: input.sourceConfig, - }) + }).catch(rethrowGitHubInstallationSourceError) if (!resolved) { return { message: 'Failed to refresh access token. Please reconnect your account.', @@ -1268,6 +1274,8 @@ const connectorDocumentSelection = { userExcluded: document.userExcluded, uploadedAt: document.uploadedAt, processingStatus: document.processingStatus, + processingOutcome: documentProcessingOutcomeSelection(), + processingError: document.processingError, } export const listKnowledgeConnectorDocuments = defineAuthorizedKnowledgeUseCase({ @@ -1316,7 +1324,7 @@ export const listKnowledgeConnectorDocuments = defineAuthorizedKnowledgeUseCase( ? sql`${document.filename} ILIKE ${`%${escapeLikePattern(search)}%`} ESCAPE '\\'` : undefined, ] as const - const [[activeCount], excludedCountRows, [failedCount]] = await Promise.all([ + const [[activeCount], excludedCountRows, [outcomeCounts]] = await Promise.all([ db .select({ value: count() }) .from(document) @@ -1328,15 +1336,12 @@ export const listKnowledgeConnectorDocuments = defineAuthorizedKnowledgeUseCase( .where(and(...baseConditions, eq(document.userExcluded, true))) : Promise.resolve([{ value: 0 }]), db - .select({ value: count() }) + .select({ + failed: sql`count(*) FILTER (WHERE ${failedDocumentCondition()})::int`, + skipped: sql`count(*) FILTER (WHERE ${skippedDocumentCondition()})::int`, + }) .from(document) - .where( - and( - ...baseConditions, - eq(document.userExcluded, false), - eq(document.processingStatus, 'failed') - ) - ), + .where(and(...baseConditions, eq(document.userExcluded, false))), ]) const excludedCount = excludedCountRows[0] const rows = await db @@ -1346,7 +1351,8 @@ export const listKnowledgeConnectorDocuments = defineAuthorizedKnowledgeUseCase( and( ...baseConditions, filter ? eq(document.userExcluded, filter === 'excluded') : undefined, - filter === 'failed' ? eq(document.processingStatus, 'failed') : undefined + filter === 'failed' ? failedDocumentCondition() : undefined, + filter === 'skipped' ? skippedDocumentCondition() : undefined ) ) .orderBy(asc(document.userExcluded), asc(document.filename), asc(document.id)) @@ -1359,7 +1365,8 @@ export const listKnowledgeConnectorDocuments = defineAuthorizedKnowledgeUseCase( counts: { active: activeCount?.value ?? 0, excluded: excludedCount?.value ?? 0, - failed: failedCount?.value ?? 0, + failed: outcomeCounts?.failed ?? 0, + skipped: outcomeCounts?.skipped ?? 0, }, hasMore, offset, diff --git a/apps/sim/lib/knowledge/application/github-installation-error.ts b/apps/sim/lib/knowledge/application/github-installation-error.ts new file mode 100644 index 00000000000..0ab93e8c938 --- /dev/null +++ b/apps/sim/lib/knowledge/application/github-installation-error.ts @@ -0,0 +1,17 @@ +import { OrchestrationError } from '@/lib/core/orchestration/types' +import { GitHubInstallationError } from '@/lib/oauth/github-installation' + +/** Maps repository selection refusals while preserving provider failures for their caller. */ +export function rethrowGitHubInstallationSourceError(error: unknown): never { + if ( + error instanceof GitHubInstallationError && + ((error.operation === 'repository-token' && error.status === 422) || + (error.operation === 'repository' && error.status === 404)) + ) { + throw new OrchestrationError( + 'validation', + 'Check that the repository is included in the selected GitHub App installation, then retry.' + ) + } + throw error +} diff --git a/apps/sim/lib/knowledge/application/github-installation-source.test.ts b/apps/sim/lib/knowledge/application/github-installation-source.test.ts index 2c7666faf04..f68d58c33b8 100644 --- a/apps/sim/lib/knowledge/application/github-installation-source.test.ts +++ b/apps/sim/lib/knowledge/application/github-installation-source.test.ts @@ -12,6 +12,7 @@ vi.mock('@/lib/knowledge/application/connector-credential', () => ({ })) vi.mock('@/lib/core/security/encryption', () => ({ decryptSecret: m.decrypt })) vi.mock('@/lib/oauth/github-installation', () => ({ + GitHubInstallationError: class extends Error {}, parseGitHubInstallationBinding: m.parse, resolveGitHubInstallationRepository: m.repository, })) diff --git a/apps/sim/lib/knowledge/application/github-installation-source.ts b/apps/sim/lib/knowledge/application/github-installation-source.ts index 8ee2daa57b4..9ce77014ada 100644 --- a/apps/sim/lib/knowledge/application/github-installation-source.ts +++ b/apps/sim/lib/knowledge/application/github-installation-source.ts @@ -3,6 +3,7 @@ import { OrchestrationError } from '@/lib/core/orchestration/types' import { resourceScopeFromOwner } from '@/lib/core/resource-scope' import { decryptSecret } from '@/lib/core/security/encryption' import { requireConnectorCredential } from '@/lib/knowledge/application/connector-credential' +import { rethrowGitHubInstallationSourceError } from '@/lib/knowledge/application/github-installation-error' import { parseGitHubInstallationBinding, resolveGitHubInstallationRepository, @@ -80,7 +81,9 @@ export async function prepareGitHubInstallationSource( binding.accountId !== contentCredential.providerTenantId ) throw new OrchestrationError('validation', 'Reconnect this GitHub installation before using it') - const resolved = await resolveGitHubInstallationRepository(binding, repository.trim()) + const resolved = await resolveGitHubInstallationRepository(binding, repository.trim()).catch( + rethrowGitHubInstallationSourceError + ) if (wasInstallation && input.previousConfig?.githubRepositoryId !== resolved.id) throw new OrchestrationError( 'validation', diff --git a/apps/sim/lib/knowledge/application/github-installations.test.ts b/apps/sim/lib/knowledge/application/github-installations.test.ts index 645180d446e..9027329a298 100644 --- a/apps/sim/lib/knowledge/application/github-installations.test.ts +++ b/apps/sim/lib/knowledge/application/github-installations.test.ts @@ -155,7 +155,7 @@ describe('GitHub Search installation application operations', () => { const result = await connect() expect(result).toMatchObject({ created: true, - credential: { displayName: 'GitHub App · example' }, + credential: { displayName: 'example' }, }) expect(m.verify).toHaveBeenCalledWith('ghu_reader', '42', { signal: undefined }) expect(m.encrypt).toHaveBeenCalledWith(JSON.stringify(binding)) diff --git a/apps/sim/lib/knowledge/application/github-installations.ts b/apps/sim/lib/knowledge/application/github-installations.ts index 917e9569cfb..c1b6f786ab7 100644 --- a/apps/sim/lib/knowledge/application/github-installations.ts +++ b/apps/sim/lib/knowledge/application/github-installations.ts @@ -194,7 +194,7 @@ export const connectGitHubSearchInstallation = defineAuthorizedKnowledgeUseCase( .limit(1) const id = existing?.id ?? generateId() const now = new Date() - const displayName = `GitHub App · ${binding.accountLogin}` + const displayName = binding.accountLogin const values = { displayName, encryptedServiceAccountKey: encrypted, diff --git a/apps/sim/lib/knowledge/application/organization-search-overview.ts b/apps/sim/lib/knowledge/application/organization-search-overview.ts index eb58334a776..97ebe9209cc 100644 --- a/apps/sim/lib/knowledge/application/organization-search-overview.ts +++ b/apps/sim/lib/knowledge/application/organization-search-overview.ts @@ -8,7 +8,7 @@ import { knowledgeConnectorSyncLog, organizationSearchIntegration, } from '@sim/db/schema' -import { and, eq, exists, inArray, isNotNull, isNull, sql } from 'drizzle-orm' +import { and, eq, exists, inArray, isNotNull, isNull, type SQL, sql } from 'drizzle-orm' import { OrchestrationError } from '@/lib/core/orchestration/types' import { resolveKnowledgeAccessAvailability } from '@/lib/knowledge/access/availability' import { SOURCE_ACL_MAX_AGE_MS } from '@/lib/knowledge/access/freshness' @@ -17,6 +17,7 @@ import { resolveKnowledgeOwnerContext } from '@/lib/knowledge/application/contex import { knowledgeOperations } from '@/lib/knowledge/application/operations' import { SOURCE_CONTENT_ERROR } from '@/lib/knowledge/connectors/sync-limits' import { MAX_SEARCH_SOURCE_PROVIDER_TYPES } from '@/lib/knowledge/constants' +import { failedDocumentCondition } from '@/lib/knowledge/documents/processing-status' import { canConnectWithDefaults, SEARCH_SOURCE_TYPES } from '@/lib/sim-search/connectors' interface OrganizationSearchOverviewInput { @@ -136,7 +137,7 @@ export const readOrganizationSearchOverview = defineAuthorizedKnowledgeUseCase({ ) ) ) - const hasDocumentsInState = (statuses: string[]) => + const hasDocumentsInState = (condition: SQL) => exists( db .select({ id: document.id }) @@ -149,7 +150,7 @@ export const readOrganizationSearchOverview = defineAuthorizedKnowledgeUseCase({ eq(document.userExcluded, false), isNull(document.archivedAt), isNull(document.deletedAt), - inArray(document.processingStatus, statuses) + condition ) ) ) @@ -194,7 +195,7 @@ export const readOrganizationSearchOverview = defineAuthorizedKnowledgeUseCase({ hasError: sql`bool_or(NOT ${paused} AND ( ${knowledgeConnector.status} = 'error' OR ${knowledgeConnector.lastSyncError} IS NOT NULL - OR ${hasDocumentsInState(['failed'])} + OR ${hasDocumentsInState(failedDocumentCondition())} OR (${knowledgeConnector.accessMode} = 'admin' AND ${latestCentralRunHasError}) OR (${knowledgeConnector.accessMode} = 'members' AND ( ${knowledgeConnector.memberSyncStatus} = 'error' @@ -203,12 +204,12 @@ export const readOrganizationSearchOverview = defineAuthorizedKnowledgeUseCase({ )) ))`, hasAccountError: sql`bool_or(NOT ${paused} AND ${knowledgeConnector.accessMode} = 'members' AND ${hasMemberError})`, - hasDocumentError: sql`bool_or(NOT ${paused} AND ${hasDocumentsInState(['failed'])})`, + hasDocumentError: sql`bool_or(NOT ${paused} AND ${hasDocumentsInState(failedDocumentCondition())})`, hasIndexing: sql`bool_or(NOT ${paused} AND (${knowledgeConnector.accessMode} <> 'members' OR ${hasActiveMembers} OR ${knowledgeConnector.credentialId} IS NOT NULL) AND ( ${knowledgeConnector.status} IN ('pending', 'syncing') - OR ${continuing} OR ${hasDocumentsInState(['pending', 'processing'])} + OR ${continuing} OR ${hasDocumentsInState(inArray(document.processingStatus, ['pending', 'processing']))} OR (${knowledgeConnector.accessMode} = 'members' AND ( ${knowledgeConnector.memberSyncStatus} IN ('pending', 'running') OR ${hasMemberFirstListing} )) diff --git a/apps/sim/lib/knowledge/application/search-source-progress.ts b/apps/sim/lib/knowledge/application/search-source-progress.ts index 5543c3ec974..25e30956d0b 100644 --- a/apps/sim/lib/knowledge/application/search-source-progress.ts +++ b/apps/sim/lib/knowledge/application/search-source-progress.ts @@ -1,6 +1,6 @@ import { db } from '@sim/db' import { document, knowledgeBase, knowledgeConnector } from '@sim/db/schema' -import { and, eq, exists, inArray, isNull, sql } from 'drizzle-orm' +import { and, eq, exists, inArray, isNull, type SQL, sql } from 'drizzle-orm' import { OrchestrationError } from '@/lib/core/orchestration/types' import { type ResourceOwner, resourceScopeFromOwner } from '@/lib/core/resource-scope' import { resourceScopeCondition } from '@/lib/core/resource-scope.server' @@ -10,6 +10,7 @@ import { defineAuthorizedKnowledgeUseCase } from '@/lib/knowledge/application/au import { resolveKnowledgeOwnerContext } from '@/lib/knowledge/application/contexts' import { knowledgeOperations } from '@/lib/knowledge/application/operations' import { MAX_SEARCH_SOURCE_PROGRESS_ITEMS } from '@/lib/knowledge/constants' +import { failedDocumentCondition } from '@/lib/knowledge/documents/processing-status' import { searchIntegrationAccessCondition } from '@/lib/knowledge/search/integration-policy' interface ReadSearchSourceProgressInput extends ResourceOwner { @@ -34,7 +35,7 @@ export const readSearchSourceProgress = defineAuthorizedKnowledgeUseCase({ const access = await createKnowledgeAccessProvider(principal, context).getForConnectors( input.connectorIds ) - const hasDocumentsInState = (statuses: string[]) => + const hasDocumentsInState = (condition: SQL) => sql`${exists( db .select({ id: document.id }) @@ -43,7 +44,7 @@ export const readSearchSourceProgress = defineAuthorizedKnowledgeUseCase({ and( eq(document.knowledgeBaseId, knowledgeConnector.knowledgeBaseId), eq(document.connectorId, knowledgeConnector.id), - inArray(document.processingStatus, statuses), + condition, eq(document.enabled, true), eq(document.userExcluded, false), isNull(document.archivedAt), @@ -60,8 +61,10 @@ export const readSearchSourceProgress = defineAuthorizedKnowledgeUseCase({ memberSyncStatus: knowledgeConnector.memberSyncStatus, hasRetainedSyncError: sql`${knowledgeConnector.lastSyncError} IS NOT NULL`, approved: sql`${searchIntegrationAccessCondition()}`, - isIndexing: hasDocumentsInState(['pending', 'processing']), - hasIndexingError: hasDocumentsInState(['failed']), + isIndexing: hasDocumentsInState( + inArray(document.processingStatus, ['pending', 'processing']) + ), + hasIndexingError: hasDocumentsInState(failedDocumentCondition()), }) .from(knowledgeConnector) .innerJoin(knowledgeBase, eq(knowledgeBase.id, knowledgeConnector.knowledgeBaseId)) diff --git a/apps/sim/lib/knowledge/application/search-sources.test.ts b/apps/sim/lib/knowledge/application/search-sources.test.ts index d806ccb77b5..91081980cce 100644 --- a/apps/sim/lib/knowledge/application/search-sources.test.ts +++ b/apps/sim/lib/knowledge/application/search-sources.test.ts @@ -646,7 +646,23 @@ describe('bounded Search source pagination', () => { ]) }) - it.each(['filter', 'provider', 'viewer', 'scope'] as const)( + it('excludes an account-level provider before paginating repository sources', async () => { + seed([source('drive')]) + await listSearchSources.execute({ + principal, + input: { ...input, excludeConnectorType: 'github' }, + }) + expect(dbChainMockFns.where.mock.calls).toContainEqual([ + expect.objectContaining({ + type: 'and', + conditions: expect.arrayContaining([ + { type: 'ne', left: knowledgeConnector.connectorType, right: 'github' }, + ]), + }), + ]) + }) + + it.each(['filter', 'provider', 'excluded-provider', 'viewer', 'scope'] as const)( 'rejects a cursor replayed under a different %s before reading sources', async (change) => { seed(rows(26)) @@ -666,6 +682,7 @@ describe('bounded Search source pagination', () => { cursor: first.nextCursor!, ...(change === 'filter' ? { mine: true } : {}), ...(change === 'provider' ? { connectorType: 'gmail' } : {}), + ...(change === 'excluded-provider' ? { excludeConnectorType: 'github' } : {}), }, }) ).rejects.toMatchObject({ code: 'validation' }) diff --git a/apps/sim/lib/knowledge/application/search-sources.ts b/apps/sim/lib/knowledge/application/search-sources.ts index 8f33d05cfe1..1d6129acac5 100644 --- a/apps/sim/lib/knowledge/application/search-sources.ts +++ b/apps/sim/lib/knowledge/application/search-sources.ts @@ -1,7 +1,7 @@ import { requirePrincipalSubjectUserId } from '@sim/auth/principal' import { db } from '@sim/db' import { document, embedding, knowledgeBase, knowledgeConnector, user } from '@sim/db/schema' -import { and, desc, eq, exists, inArray, isNull, lt, or, sql } from 'drizzle-orm' +import { and, desc, eq, exists, inArray, isNull, lt, ne, or, sql } from 'drizzle-orm' import { listSearchSourcesContract, searchSourceCursorSchema, @@ -22,6 +22,7 @@ import { SEARCH_SOURCE_CANDIDATE_PAGE_SIZE, SEARCH_SOURCE_PAGE_SIZE, } from '@/lib/knowledge/constants' +import { failedDocumentCondition } from '@/lib/knowledge/documents/processing-status' import { listOrganizationSearchApprovals } from '@/lib/knowledge/search/integration-policy' import { describeSearchSource } from '@/lib/sim-search/source-identity' import { getConnectorMeta } from '@/connectors/registry' @@ -30,6 +31,7 @@ export interface ListSearchSourcesInput extends ResourceOwner { cursor?: string connectorId?: string connectorType?: string + excludeConnectorType?: string search?: string mine?: boolean } @@ -43,12 +45,14 @@ export const listSearchSources = defineAuthorizedKnowledgeUseCase({ const userId = requirePrincipalSubjectUserId(principal) const search = input.search?.trim().toLowerCase() ?? '' const connectorType = input.connectorType?.trim() + const excludeConnectorType = input.excludeConnectorType?.trim() const cursorScope = cursorScopeKey(cursorRoute(listSearchSourcesContract), { workspaceId: context.workspaceId, organizationId: context.organizationId, userId: userId, search, connectorType: connectorType ?? '', + ...(excludeConnectorType ? { excludeConnectorType } : {}), connectorId: input.connectorId ?? '', mine: input.mine === true, order: 'newest', @@ -95,6 +99,9 @@ export const listSearchSources = defineAuthorizedKnowledgeUseCase({ isNull(knowledgeConnector.archivedAt), isNull(knowledgeConnector.deletedAt), connectorType ? eq(knowledgeConnector.connectorType, connectorType) : undefined, + excludeConnectorType + ? ne(knowledgeConnector.connectorType, excludeConnectorType) + : undefined, input.connectorId ? eq(knowledgeConnector.id, input.connectorId) : undefined, cursor ? or( @@ -179,7 +186,7 @@ export const listSearchSources = defineAuthorizedKnowledgeUseCase({ .where(and(eq(embedding.documentId, document.id), eq(embedding.enabled, true))) )} )::int`, - failedCount: sql`count(*) FILTER (WHERE ${document.processingStatus} = 'failed')::int`, + failedCount: sql`count(*) FILTER (WHERE ${failedDocumentCondition()})::int`, isIndexing: sql`bool_or(${document.processingStatus} IN ('pending', 'processing'))`, }) .from(document) diff --git a/apps/sim/lib/knowledge/connectors/sync-engine.test.ts b/apps/sim/lib/knowledge/connectors/sync-engine.test.ts index e4b2c551769..dc11c0d278d 100644 --- a/apps/sim/lib/knowledge/connectors/sync-engine.test.ts +++ b/apps/sim/lib/knowledge/connectors/sync-engine.test.ts @@ -276,7 +276,7 @@ describe('resolveTagMapping', () => { describe('classifyExternalDoc', () => { const base = { content: 'hello', contentDeferred: false, contentHash: 'h1' } - it('records a new skipped file as a failed row', async () => { + it('records a new skipped file as an intentional exclusion', async () => { const { classifyExternalDoc } = await import('@/lib/knowledge/connectors/sync-primitives') expect( classifyExternalDoc({ ...base, content: '', skippedReason: 'too big' }, undefined) diff --git a/apps/sim/lib/knowledge/connectors/sync-persistence.ts b/apps/sim/lib/knowledge/connectors/sync-persistence.ts index 0dbdbe4935b..3598bd78e8f 100644 --- a/apps/sim/lib/knowledge/connectors/sync-persistence.ts +++ b/apps/sim/lib/knowledge/connectors/sync-persistence.ts @@ -258,7 +258,7 @@ export interface KnowledgeBaseOwner { userId: string } -/** Builds a content-less `failed` document row for a skipped (e.g. oversized) file. */ +/** Builds a content-less document row for an intentional source exclusion. */ function buildSkippedDocumentRow( knowledgeBaseId: string, connectorId: string, @@ -295,7 +295,7 @@ function buildSkippedDocumentRow( } /** - * Records source files that were intentionally not indexed as content-less `failed` + * Records source files that were intentionally not indexed as content-less * documents. New rows are inserted in bulk; authoritative skips replace stale rows. * This keeps the files visible in the knowledge base UI — with `processingError` * explaining why — instead of silently dropping them. The rows have no storage key, diff --git a/apps/sim/lib/knowledge/connectors/viewer-source-accounts.ts b/apps/sim/lib/knowledge/connectors/viewer-source-accounts.ts index 2c9041e4acb..924dd0f386b 100644 --- a/apps/sim/lib/knowledge/connectors/viewer-source-accounts.ts +++ b/apps/sim/lib/knowledge/connectors/viewer-source-accounts.ts @@ -1,8 +1,6 @@ -import { db } from '@sim/db' -import { credential, credentialGroup, credentialGroupEnrollment } from '@sim/db/schema' -import { and, eq, inArray, isNull, or } from 'drizzle-orm' -import { resourceScopeCondition } from '@/lib/core/resource-scope.server' -import { SEARCH_SOURCE_CANDIDATE_PAGE_SIZE } from '@/lib/knowledge/constants' +import { credential, credentialGroup } from '@sim/db/schema' +import { and, eq, or } from 'drizzle-orm' +import { listViewerOrganizationAccounts } from '@/lib/credential-groups/viewer-accounts' import { getConnectorMeta } from '@/connectors/registry' interface ViewerSourceAccount { @@ -52,36 +50,11 @@ export async function resolveViewerSourceAccounts(input: { }) const result = new Map() if (!matches.length) return result - const scope = { kind: 'organization', organizationId: input.organizationId } as const - const accounts = await db - .select({ - credentialId: credential.id, - displayName: credential.displayName, - status: credential.managedOauthStatus, - groupId: credentialGroup.id, - optionId: credential.credentialGroupOptionId, - providerId: credential.providerId, - }) - .from(credential) - .innerJoin( - credentialGroupEnrollment, - eq(credentialGroupEnrollment.id, credential.credentialGroupEnrollmentId) - ) - .innerJoin(credentialGroup, eq(credentialGroup.id, credentialGroupEnrollment.credentialGroupId)) - .where( - and( - resourceScopeCondition(credential, scope), - resourceScopeCondition(credentialGroup, scope), - eq(credentialGroupEnrollment.userId, input.userId), - eq(credential.type, 'managed_oauth'), - inArray(credential.managedOauthStatus, ['active', 'needs_reauth']), - isNull(credential.revokedAt), - or(...matches) - ) - ) - .limit(SEARCH_SOURCE_CANDIDATE_PAGE_SIZE + 1) - if (accounts.length > SEARCH_SOURCE_CANDIDATE_PAGE_SIZE) - throw new Error('Too many personal accounts for the source page') + const accounts = await listViewerOrganizationAccounts({ + organizationId: input.organizationId, + userId: input.userId, + matching: or(...matches)!, + }) for (const { source, providerId } of bindings) { const own = accounts.filter((account) => source.accessMode === 'members' diff --git a/apps/sim/lib/knowledge/documents/processing-outbox-handler.test.ts b/apps/sim/lib/knowledge/documents/processing-outbox-handler.test.ts index 08669fa0a65..4ea7dc6b006 100644 --- a/apps/sim/lib/knowledge/documents/processing-outbox-handler.test.ts +++ b/apps/sim/lib/knowledge/documents/processing-outbox-handler.test.ts @@ -153,16 +153,17 @@ describe('knowledge document processing outbox handler', () => { ) }) - it.each([null, { ...DOCUMENT, processingStatus: 'completed' }])( - 'completes without redispatch when the document is absent or completed', - async (document) => { - mocks.getKnowledgeDocument.mockResolvedValueOnce(document) + it.each([ + null, + { ...DOCUMENT, processingStatus: 'completed' }, + { ...DOCUMENT, processingStatus: 'failed', processingOutcome: 'skipped' }, + ])('completes without redispatch when the document is absent or terminal', async (document) => { + mocks.getKnowledgeDocument.mockResolvedValueOnce(document) - await handler()(PAYLOAD, createContext()) + await handler()(PAYLOAD, createContext()) - expect(mocks.processDocumentsWithQueue).not.toHaveBeenCalled() - } - ) + expect(mocks.processDocumentsWithQueue).not.toHaveBeenCalled() + }) it('keeps the event retryable while an earlier processing attempt is active', async () => { const processingStartedAt = new Date() diff --git a/apps/sim/lib/knowledge/documents/processing-outbox-handler.ts b/apps/sim/lib/knowledge/documents/processing-outbox-handler.ts index 97691adacaf..e1ee59af466 100644 --- a/apps/sim/lib/knowledge/documents/processing-outbox-handler.ts +++ b/apps/sim/lib/knowledge/documents/processing-outbox-handler.ts @@ -108,7 +108,12 @@ const processKnowledgeDocument: OutboxHandler = async (rawPayload, cont payload.documentId, SYSTEM_ACCESS_SCOPE ) - if (!document || document.processingStatus === 'completed') return + if ( + !document || + document.processingStatus === 'completed' || + document.processingOutcome === 'skipped' + ) + return if (document.processingStatus === 'processing') { const reclaimed = await reclaimStaleDocumentProcessingClaim({ knowledgeBaseId: payload.knowledgeBaseId, diff --git a/apps/sim/lib/knowledge/documents/processing-status.test.ts b/apps/sim/lib/knowledge/documents/processing-status.test.ts new file mode 100644 index 00000000000..db907f8da06 --- /dev/null +++ b/apps/sim/lib/knowledge/documents/processing-status.test.ts @@ -0,0 +1,44 @@ +/** @vitest-environment node */ +import { describe, expect, it } from 'vitest' +import { getDocumentProcessingOutcome } from '@/lib/knowledge/documents/processing-status' +import { DOCUMENT_PROCESSING_STATUSES } from '@/lib/knowledge/documents/types' + +describe('document processing outcomes', () => { + const placeholder = { + processingStatus: 'failed', + externalId: 'source-file', + storageKey: null, + contentHash: 'known-version', + fileUrl: '', + } + + it('recognizes a legacy intentional skip without reading error text or contacting its source', () => { + expect(getDocumentProcessingOutcome(placeholder)).toBe('skipped') + }) + + it.each([ + { ...placeholder, contentHash: null }, + { ...placeholder, storageKey: 'kb/stored.txt' }, + { ...placeholder, externalId: null }, + { ...placeholder, fileUrl: 'https://fixture.test/stored.txt' }, + ])('preserves real source, indexing, and upload failures: %j', (row) => { + expect(getDocumentProcessingOutcome(row)).toBeNull() + }) + + it.each(DOCUMENT_PROCESSING_STATUSES)( + 'preserves a stored %s outcome with an artifact', + (status) => { + expect( + getDocumentProcessingOutcome({ + ...placeholder, + processingStatus: status, + storageKey: 'kb/stored.txt', + }) + ).toBeNull() + } + ) + + it('does not infer a source omission from an unknown state', () => { + expect(getDocumentProcessingOutcome({ ...placeholder, processingStatus: 'invalid' })).toBeNull() + }) +}) diff --git a/apps/sim/lib/knowledge/documents/processing-status.ts b/apps/sim/lib/knowledge/documents/processing-status.ts new file mode 100644 index 00000000000..b3fdb30579b --- /dev/null +++ b/apps/sim/lib/knowledge/documents/processing-status.ts @@ -0,0 +1,52 @@ +import { document } from '@sim/db/schema' +import { and, eq, isNotNull, isNull, not, sql } from 'drizzle-orm' +import type { DocumentProcessingOutcome } from '@/lib/knowledge/documents/types' + +interface StoredDocumentProcessingState { + processingStatus: string + externalId: string | null + storageKey: string | null + contentHash: string | null + fileUrl: string +} + +/** + * Source omissions keep the legacy failed storage state for mixed-version app/worker + * compatibility. Their immutable source identity and version, with no stored or remote + * artifact, distinguish them from download failures (no version) and indexing failures + * (an artifact). Source identity survives disconnecting a connector with documents kept. + * The additive outcome lets current readers distinguish skips without rewriting old rows. + */ +export function getDocumentProcessingOutcome( + row: StoredDocumentProcessingState +): DocumentProcessingOutcome { + if ( + row.processingStatus === 'failed' && + row.externalId !== null && + row.storageKey === null && + row.fileUrl === '' && + row.contentHash !== null + ) { + return 'skipped' + } + return null +} + +/** SQL counterpart used for paging and aggregate counts before document rows are loaded. */ +export function skippedDocumentCondition() { + return and( + eq(document.processingStatus, 'failed'), + isNotNull(document.externalId), + isNull(document.storageKey), + eq(document.fileUrl, ''), + isNotNull(document.contentHash) + )! +} + +export function failedDocumentCondition() { + return and(eq(document.processingStatus, 'failed'), not(skippedDocumentCondition()))! +} + +export function documentProcessingOutcomeSelection() { + return sql`CASE WHEN ${skippedDocumentCondition()} THEN 'skipped' ELSE NULL END` +} diff --git a/apps/sim/lib/knowledge/documents/service.ts b/apps/sim/lib/knowledge/documents/service.ts index baa3a7f4309..d3dea4c4ecc 100644 --- a/apps/sim/lib/knowledge/documents/service.ts +++ b/apps/sim/lib/knowledge/documents/service.ts @@ -24,7 +24,7 @@ import { isNotNull, isNull, lt, - ne, + not, or, type SQL, sql, @@ -116,6 +116,11 @@ import { ProviderCapacityContinuationExhaustedError, } from '@/lib/knowledge/documents/processing-provider-deferral' import { scheduleDocumentProcessingQuotaContinuation } from '@/lib/knowledge/documents/processing-quota-continuation' +import { + documentProcessingOutcomeSelection, + getDocumentProcessingOutcome, + skippedDocumentCondition, +} from '@/lib/knowledge/documents/processing-status' import { DOCUMENT_PROCESSING_STALE_THRESHOLD_MS } from '@/lib/knowledge/documents/processing-timeouts.server' import { enqueueKnowledgeStorageCleanup, @@ -129,7 +134,10 @@ import { type TagFilterCondition, } from '@/lib/knowledge/documents/tag-filter' import { + type DocumentProcessingOutcome, + type DocumentProcessingStatus, type DocumentSortField, + isDocumentProcessingStatus, MAX_PROCESSING_ATTEMPTS, QUEUED_DISPATCH_GRACE_MS, type SortOrder, @@ -1494,7 +1502,8 @@ export async function processDocumentAsync( .where( and( eq(document.id, documentId), - ne(document.processingStatus, 'completed'), + inArray(document.processingStatus, ['pending', 'processing', 'failed']), + not(skippedDocumentCondition()), ...queueGenerationConditions(attemptContext), eq(document.userExcluded, false), isNull(document.archivedAt), @@ -1558,7 +1567,8 @@ export async function processDocumentAsync( .where( and( eq(document.id, documentId), - ne(document.processingStatus, 'completed'), + inArray(document.processingStatus, ['pending', 'processing', 'failed']), + not(skippedDocumentCondition()), ...(predecessor ? [or(and(...queueGenerationConditions(attemptContext)), predecessor)] : queueGenerationConditions(attemptContext)), @@ -2472,7 +2482,8 @@ export async function getDocuments( chunkCount: number tokenCount: number characterCount: number - processingStatus: 'pending' | 'processing' | 'completed' | 'failed' + processingStatus: DocumentProcessingStatus + processingOutcome: DocumentProcessingOutcome processingStartedAt: Date | null processingCompletedAt: Date | null processingError: string | null @@ -2579,6 +2590,7 @@ export async function getDocuments( tokenCount: document.tokenCount, characterCount: document.characterCount, processingStatus: document.processingStatus, + processingOutcome: documentProcessingOutcomeSelection(), processingStartedAt: document.processingStartedAt, processingCompletedAt: document.processingCompletedAt, processingError: document.processingError, @@ -2693,7 +2705,8 @@ export async function getDocuments( chunkCount: doc.chunkCount, tokenCount: doc.tokenCount, characterCount: doc.characterCount, - processingStatus: doc.processingStatus as 'pending' | 'processing' | 'completed' | 'failed', + processingStatus: doc.processingStatus as DocumentProcessingStatus, + processingOutcome: doc.processingOutcome, processingStartedAt: doc.processingStartedAt, processingCompletedAt: doc.processingCompletedAt, processingError: doc.processingError, @@ -2731,6 +2744,7 @@ export async function getDocuments( export type ActiveKnowledgeDocument = typeof document.$inferSelect & { connectorType: string | null + processingOutcome: DocumentProcessingOutcome } /** @@ -2747,6 +2761,7 @@ export async function getKnowledgeDocument( const [row] = await db .select({ ...getTableColumns(document), + processingOutcome: documentProcessingOutcomeSelection(), connectorType: knowledgeConnector.connectorType, }) .from(document) @@ -2775,6 +2790,7 @@ export async function getKnowledgeDocumentById( const [row] = await db .select({ ...getTableColumns(document), + processingOutcome: documentProcessingOutcomeSelection(), connectorType: knowledgeConnector.connectorType, }) .from(document) @@ -3065,7 +3081,7 @@ export async function getDocumentByUploadId( knowledgeBaseId: string ): Promise< | (Omit>, 'processingStatus'> & { - processingStatus: 'pending' | 'processing' | 'completed' | 'failed' + processingStatus: DocumentProcessingStatus }) | null > { @@ -3090,6 +3106,7 @@ export async function getDocumentByUploadId( tag6: document.tag6, tag7: document.tag7, processingStatus: document.processingStatus, + processingOutcome: documentProcessingOutcomeSelection(), }) .from(document) .where( @@ -3102,12 +3119,7 @@ export async function getDocumentByUploadId( .limit(1) if (!existing) return null const processingStatus = existing.processingStatus - if ( - processingStatus !== 'pending' && - processingStatus !== 'processing' && - processingStatus !== 'completed' && - processingStatus !== 'failed' - ) { + if (!isDocumentProcessingStatus(processingStatus)) { throw new Error(`Document ${existing.id} has invalid processing status`) } return { ...existing, processingStatus } @@ -3373,6 +3385,7 @@ export async function retryDocumentProcessing( and( eq(document.id, documentId), or(isNull(document.connectorId), isNotNull(document.contentHash)), + not(skippedDocumentCondition()), or( inArray(document.processingStatus, ['completed', 'failed']), and( @@ -3399,6 +3412,25 @@ export async function retryDocumentProcessing( }) if (!requeued) { + const [skipped] = await db + .select({ id: document.id }) + .from(document) + .where( + and( + eq(document.id, documentId), + eq(document.knowledgeBaseId, knowledgeBaseId), + skippedDocumentCondition() + ) + ) + .limit(1) + if (skipped) { + return { + success: false, + status: 'skipped', + message: + 'This source file was intentionally skipped. Sync the connector after changing the source.', + } + } const [sourceFailure] = await db .select({ id: document.id }) .from(document) @@ -3515,7 +3547,8 @@ export async function updateDocument( chunkCount: number tokenCount: number characterCount: number - processingStatus: 'pending' | 'processing' | 'completed' | 'failed' + processingStatus: DocumentProcessingStatus + processingOutcome: DocumentProcessingOutcome processingStartedAt: Date | null processingCompletedAt: Date | null processingError: string | null @@ -3546,7 +3579,7 @@ export async function updateDocument( chunkCount: number tokenCount: number characterCount: number - processingStatus: 'pending' | 'processing' | 'completed' | 'failed' + processingStatus: DocumentProcessingStatus processingError: string | null processingStartedAt: Date | null processingCompletedAt: Date | null @@ -3709,7 +3742,8 @@ export async function updateDocument( chunkCount: doc.chunkCount, tokenCount: doc.tokenCount, characterCount: doc.characterCount, - processingStatus: doc.processingStatus as 'pending' | 'processing' | 'completed' | 'failed', + processingStatus: doc.processingStatus as DocumentProcessingStatus, + processingOutcome: getDocumentProcessingOutcome(doc), processingStartedAt: doc.processingStartedAt, processingCompletedAt: doc.processingCompletedAt, processingError: doc.processingError, diff --git a/apps/sim/lib/knowledge/documents/types.ts b/apps/sim/lib/knowledge/documents/types.ts index eca8991a467..3b0bd225f99 100644 --- a/apps/sim/lib/knowledge/documents/types.ts +++ b/apps/sim/lib/knowledge/documents/types.ts @@ -95,6 +95,17 @@ export const DOCUMENT_PROCESSING_STATUSES = [ export type DocumentProcessingStatus = (typeof DOCUMENT_PROCESSING_STATUSES)[number] +/** An intentional source omission, separate from the backward-compatible processing state. */ +export type DocumentProcessingOutcome = 'skipped' | null + +/** Displays an intentional source outcome while accepting responses from older servers. */ +export function getDocumentIndexingStatus(document: { + processingStatus: string + processingOutcome?: DocumentProcessingOutcome +}): string { + return document.processingOutcome ?? document.processingStatus +} + /** * Narrows a stored `processing_status` onto the union. * diff --git a/apps/sim/lib/knowledge/orchestration/documents.ts b/apps/sim/lib/knowledge/orchestration/documents.ts index 7416255d29b..4436933ecfd 100644 --- a/apps/sim/lib/knowledge/orchestration/documents.ts +++ b/apps/sim/lib/knowledge/orchestration/documents.ts @@ -17,6 +17,10 @@ import { retryDocumentProcessing, updateDocument, } from '@/lib/knowledge/documents/service' +import type { + DocumentProcessingOutcome, + DocumentProcessingStatus, +} from '@/lib/knowledge/documents/types' import { auditActorFields, classifyKnowledgeFailure, @@ -65,7 +69,7 @@ export type CreatedKnowledgeDocument = Omit< 'processingStatus' > & { /** New documents are pending; idempotent completion may return a later persisted state. */ - processingStatus: 'pending' | 'processing' | 'completed' | 'failed' + processingStatus: DocumentProcessingStatus } export interface PerformUploadKnowledgeDocumentParams extends KnowledgeOperationContext { @@ -546,6 +550,7 @@ export interface PerformRetryKnowledgeDocumentParams { fileSize: number mimeType: string processingStatus: string + processingOutcome?: DocumentProcessingOutcome connectorId?: string | null contentHash?: string | null } @@ -560,6 +565,13 @@ export async function performRetryKnowledgeDocumentProcessing( const { knowledgeBaseId, document, billingAttribution } = params const requestId = params.requestId ?? generateRequestId() + if (document.processingOutcome === 'skipped') { + return fail( + 'This source file was intentionally skipped. Sync the connector after changing the source.', + 'validation' + ) + } + if (document.connectorId && document.contentHash === null) { return fail( 'Source content could not be downloaded. Sync the connector to retry.', @@ -603,7 +615,7 @@ export async function performRetryKnowledgeDocumentProcessing( // got off the ground leaves a dead document, and reporting that as success // paints the UI green over it. if (!result.success) { - return fail(result.message, 'internal') + return fail(result.message, result.status === 'skipped' ? 'validation' : 'internal') } return { success: true, status: result.status, message: result.message } } catch (error) { diff --git a/apps/sim/lib/knowledge/types.ts b/apps/sim/lib/knowledge/types.ts index 18ddcc53c01..1aab5c59142 100644 --- a/apps/sim/lib/knowledge/types.ts +++ b/apps/sim/lib/knowledge/types.ts @@ -1,4 +1,8 @@ import type { ChunkingStrategy, StrategyOptions } from '@/lib/chunkers/types' +import type { + DocumentProcessingOutcome, + DocumentProcessingStatus, +} from '@/lib/knowledge/documents/types' import type { KbEmbeddingDimensions } from '@/lib/knowledge/embedding-models' /** @@ -142,7 +146,8 @@ export interface DocumentData { chunkCount: number tokenCount: number characterCount: number - processingStatus: 'pending' | 'processing' | 'completed' | 'failed' + processingStatus: DocumentProcessingStatus + processingOutcome?: DocumentProcessingOutcome processingStartedAt?: string | null processingCompletedAt?: string | null processingError?: string | null diff --git a/apps/sim/lib/oauth/github-installation.test.ts b/apps/sim/lib/oauth/github-installation.test.ts index 4267804d545..c49835e6514 100644 --- a/apps/sim/lib/oauth/github-installation.test.ts +++ b/apps/sim/lib/oauth/github-installation.test.ts @@ -6,6 +6,7 @@ import { assertGitHubInstallationActive, assertGitHubInstallationRepositoryActive, getGitHubInstallationConfiguration, + listGitHubInstallationRepositories, listUserAdminGitHubInstallations, parseGitHubInstallationBinding, resolveGitHubInstallationAccessToken, @@ -91,6 +92,92 @@ function mockDiscovery( } describe('GitHub installation setup', () => { + it('reports the failing token operation without including provider response contents', async () => { + fetchMock + .mockResolvedValueOnce(json(installation)) + .mockResolvedValueOnce(json({ message: 'sensitive provider detail' }, 422)) + await expect( + resolveGitHubInstallationAccessToken(binding, { repositoryId: '101' }) + ).rejects.toMatchObject({ + operation: 'repository-token', + status: 422, + message: + 'Check that the repository is included in the selected GitHub App installation, then retry.', + }) + }) + it('lists one metadata-only repository page with continuation and account checks', async () => { + fetchMock + .mockResolvedValueOnce(json(installation)) + .mockResolvedValueOnce(json(tokenResponse(101, false))) + .mockResolvedValueOnce( + json({ + total_count: 201, + repositories: Array.from({ length: 100 }, (_, i) => ({ + id: 101 + i, + full_name: `team/repo-${i}`, + owner: { id: 11 }, + default_branch: 'main', + })), + }) + ) + const controller = new AbortController() + const result = await listGitHubInstallationRepositories(binding, { + page: 2, + signal: controller.signal, + }) + expect(result.repositories).toHaveLength(100) + expect(result.repositories[0]).toEqual({ id: '101', fullName: 'team/repo-0' }) + expect(result.hasMore).toBe(true) + expect(fetchMock).toHaveBeenCalledTimes(3) + expect(JSON.parse(String(fetchMock.mock.calls[1][1]?.body))).toEqual({ + permissions: { metadata: 'read' }, + }) + expect(fetchMock.mock.calls[2][0]).toBe( + 'https://api.github.com/installation/repositories?per_page=100&page=2' + ) + controller.abort() + expect(fetchMock.mock.calls[2][1]?.signal?.aborted).toBe(true) + }) + + it.each([0, 101, 1.5, Number.NaN])( + 'rejects an invalid repository page %s before provider reads', + async (page) => { + await expect(listGitHubInstallationRepositories(binding, { page })).rejects.toThrow( + 'page is invalid' + ) + expect(fetchMock).not.toHaveBeenCalled() + } + ) + + it('refuses a suspended installation before minting a listing token', async () => { + fetchMock.mockResolvedValueOnce(json({ ...installation, suspended_at: '2026-01-01T00:00:00Z' })) + await expect(listGitHubInstallationRepositories(binding)).rejects.toThrow('unavailable') + expect(fetchMock).toHaveBeenCalledOnce() + }) + + it('rejects content permissions in repository browsing tokens', async () => { + fetchMock.mockResolvedValueOnce(json(installation)).mockResolvedValueOnce(json(tokenResponse())) + await expect(listGitHubInstallationRepositories(binding)).rejects.toThrow() + expect(fetchMock).toHaveBeenCalledTimes(2) + }) + + it('rejects repositories from another installation account', async () => { + fetchMock + .mockResolvedValueOnce(json(installation)) + .mockResolvedValueOnce(json(tokenResponse(101, false))) + .mockResolvedValueOnce( + json({ + total_count: 1, + repositories: [ + { id: 101, full_name: 'other/repo', owner: { id: 12 }, default_branch: 'main' }, + ], + }) + ) + await expect(listGitHubInstallationRepositories(binding)).rejects.toThrow( + 'another GitHub installation account' + ) + }) + it('rechecks repository installation selection without caching a previous success', async () => { fetchMock .mockResolvedValueOnce(json(installation)) diff --git a/apps/sim/lib/oauth/github-installation.ts b/apps/sim/lib/oauth/github-installation.ts index 1e3c7dfe720..270ba839567 100644 --- a/apps/sim/lib/oauth/github-installation.ts +++ b/apps/sim/lib/oauth/github-installation.ts @@ -64,13 +64,24 @@ const repositorySchema = z.object({ export class GitHubInstallationError extends Error { constructor( message: string, - readonly status?: number + readonly status?: number, + readonly operation?: GitHubInstallationOperation ) { super(message) this.name = 'GitHubInstallationError' } } +type GitHubInstallationOperation = + | 'user' + | 'memberships' + | 'installations' + | 'installation' + | 'repository' + | 'repository-list' + | 'repository-token' + | 'listing-token' + function readConfiguration() { const appId = env.GITHUB_APP_ID?.trim() const clientId = env.GITHUB_APP_CLIENT_ID?.trim() @@ -143,7 +154,8 @@ async function request( path: string, token: string, signal: AbortSignal, - body?: unknown + body?: unknown, + operation: GitHubInstallationOperation = 'installation' ): Promise { const requestSignal = AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]) const response = await fetch(`${API_URL}${path}`, { @@ -162,8 +174,11 @@ async function request( if (!response.ok) { await response.body?.cancel() throw new GitHubInstallationError( - `GitHub installation request failed with HTTP ${response.status}`, - response.status + operation === 'repository-token' && response.status === 422 + ? 'Check that the repository is included in the selected GitHub App installation, then retry.' + : `GitHub ${operation} request failed with HTTP ${response.status}`, + response.status, + operation ) } return readResponseJsonWithLimit(response, { @@ -214,7 +229,7 @@ async function adminAccountIds(userAccessToken: string, signal: AbortSignal) { throw new GitHubInstallationError('Connect your GitHub account before choosing an installation') const user = z .object({ id: apiIdSchema, type: z.literal('User') }) - .parse(await request('/user', userAccessToken, signal)) + .parse(await request('/user', userAccessToken, signal, undefined, 'user')) const organizations = new Set() const membershipsSchema = z .array( @@ -231,7 +246,9 @@ async function adminAccountIds(userAccessToken: string, signal: AbortSignal) { await request( `/user/memberships/orgs?state=active&per_page=${PAGE_SIZE}&page=${page}`, userAccessToken, - signal + signal, + undefined, + 'memberships' ) ) for (const membership of memberships) { @@ -271,7 +288,9 @@ export async function listUserAdminGitHubInstallations( await request( `/user/installations?per_page=${PAGE_SIZE}&page=${page}`, userAccessToken, - signal + signal, + undefined, + 'installations' ) ) for (const installation of data.installations) { @@ -407,7 +426,8 @@ async function mintToken( ...(repositoryId ? { repository_ids: [Number(repositoryId)] } : { repositories: [repositoryName] }), - } + }, + 'repository-token' ) ) const expiresAt = Date.parse(response.expires_at) @@ -449,7 +469,13 @@ export async function resolveGitHubInstallationRepository( await assertGitHubInstallationActive(binding, { signal }) const token = await mintToken(binding, signal, undefined, repo) const resolved = repositorySchema.parse( - await request(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, token, signal) + await request( + `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, + token, + signal, + undefined, + 'repository' + ) ) if (String(resolved.owner.id) !== binding.accountId) throw new GitHubInstallationError('Repository belongs to another GitHub installation account') @@ -460,6 +486,60 @@ export async function resolveGitHubInstallationRepository( } } +/** Browses one bounded page with metadata-only access; content tokens remain repository-scoped. */ +export async function listGitHubInstallationRepositories( + binding: GitHubInstallationBinding, + options: RequestOptions & { page?: number } = {} +) { + const page = options.page ?? 1 + if (!Number.isSafeInteger(page) || page < 1 || page > 100) + throw new GitHubInstallationError('GitHub repository page is invalid') + const signal = operationSignal(options) + await assertGitHubInstallationActive(binding, { signal }) + const configuration = requireConfiguration() + const token = z + .object({ + token: z.string().min(1).max(1024), + expires_at: z.iso.datetime(), + permissions: z.object({ metadata: z.literal('read') }).strict(), + }) + .parse( + await request( + `/app/installations/${binding.installationId}/access_tokens`, + createAppJwt(configuration), + signal, + { permissions: { metadata: 'read' } }, + 'listing-token' + ) + ) + const expiresAt = Date.parse(token.expires_at) + if (expiresAt <= Date.now() || expiresAt > Date.now() + 65 * 60_000) + throw new GitHubInstallationError('GitHub returned an invalid installation token expiration') + const result = z + .object({ + total_count: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER), + repositories: z.array(repositorySchema).max(PAGE_SIZE), + }) + .parse( + await request( + `/installation/repositories?per_page=${PAGE_SIZE}&page=${page}`, + token.token, + signal, + undefined, + 'repository-list' + ) + ) + if (result.repositories.some((repository) => String(repository.owner.id) !== binding.accountId)) + throw new GitHubInstallationError('Repository belongs to another GitHub installation account') + return { + repositories: result.repositories.map((repository) => ({ + id: String(repository.id), + fullName: repository.full_name, + })), + hasMore: result.repositories.length === PAGE_SIZE && page * PAGE_SIZE < result.total_count, + } +} + /** Mints contents access for exactly one source repository; generic unscoped token reads are refused. */ export async function resolveGitHubInstallationAccessToken( binding: GitHubInstallationBinding, diff --git a/apps/sim/lib/oauth/github-repositories.test.ts b/apps/sim/lib/oauth/github-repositories.test.ts index 610e7006877..c2820879cc1 100644 --- a/apps/sim/lib/oauth/github-repositories.test.ts +++ b/apps/sim/lib/oauth/github-repositories.test.ts @@ -8,6 +8,7 @@ import { parseGitHubRepositoriesTokenResponse, verifyGitHubRepositoriesIdentity, } from '@/lib/oauth/github-repositories' +import { OAuthIdentityVerificationError } from '@/lib/oauth/identity-error' import { refreshOAuthToken } from '@/lib/oauth/oauth' const tokenResponse = { @@ -188,27 +189,144 @@ describe('GitHub identity verification', () => { ) }) - it.each([[primary], [{ ...work, verified: false }], []])( - 'refuses absent or unverified invited email %j', - async (emails) => { + it.each([{ emails: [primary] }, { emails: [{ ...work, verified: false }] }, { emails: [] }])( + 'refuses absent or unverified invited email $emails', + async ({ emails }) => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValueOnce(response(user)).mockResolvedValueOnce(response(emails)) ) - await expect(verifyGitHubRepositoriesIdentity('ghu_access', work.email)).rejects.toThrow() + await expect( + verifyGitHubRepositoriesIdentity('ghu_access', work.email) + ).rejects.toMatchObject({ + name: 'OAuthIdentityVerificationError', + reason: 'email_mismatch', + stage: 'emails', + }) } ) it('rejects a bot identity', async () => { const fetchMock = vi.fn().mockResolvedValue(response({ ...user, type: 'Bot' })) vi.stubGlobal('fetch', fetchMock) - await expect(verifyGitHubRepositoriesIdentity('ghu_access')).rejects.toThrow() + await expect(verifyGitHubRepositoriesIdentity('ghu_access')).rejects.toMatchObject({ + reason: 'invalid_response', + stage: 'profile', + }) expect(fetchMock).toHaveBeenCalledTimes(1) }) it.each([401, 403])('fails closed when GitHub rejects identity with %i', async (status) => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue(response({}, status))) - await expect(verifyGitHubRepositoriesIdentity('ghu_access')).rejects.toThrow(`HTTP ${status}`) + await expect(verifyGitHubRepositoriesIdentity('ghu_access')).rejects.toMatchObject({ + reason: 'provider_rejected', + stage: 'profile', + httpStatus: status, + }) + }) + + it('distinguishes denied email-read permission from a verified email mismatch', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValueOnce(response(user)) + .mockResolvedValueOnce(response({ message: 'Resource not accessible by integration' }, 403)) + ) + await expect(verifyGitHubRepositoriesIdentity('ghu_access', work.email)).rejects.toMatchObject({ + reason: 'email_access_denied', + stage: 'emails', + httpStatus: 403, + }) + }) + + it.each([ + { status: 429, headers: {}, message: 'Too many requests' }, + { status: 403, headers: { 'x-ratelimit-remaining': '0' }, message: 'Forbidden' }, + { status: 403, headers: { 'retry-after': '60' }, message: 'Forbidden' }, + { status: 403, headers: {}, message: 'You have exceeded a secondary rate limit.' }, + { status: 403, headers: {}, message: 'You have triggered an abuse detection mechanism.' }, + ])( + 'does not misdiagnose provider rate limiting as missing email permission: %j', + async (error) => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValueOnce(response(user)) + .mockResolvedValueOnce( + new Response(JSON.stringify({ message: error.message }), { + status: error.status, + headers: error.headers, + }) + ) + ) + await expect( + verifyGitHubRepositoriesIdentity('ghu_access', work.email) + ).rejects.toMatchObject({ + reason: 'rate_limited', + stage: 'emails', + httpStatus: error.status, + }) + } + ) + + it('reports GitHub service failure without retaining its response body', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(response({ message: work.email }, 503))) + const failure = await verifyGitHubRepositoriesIdentity('ghu_access', work.email).catch( + (error: unknown) => error + ) + expect(failure).toBeInstanceOf(OAuthIdentityVerificationError) + expect(failure).toMatchObject({ + reason: 'provider_unavailable', + stage: 'profile', + httpStatus: 503, + }) + expect(JSON.stringify(failure)).not.toContain(work.email) + }) + + it('sanitizes network failures instead of retaining a transport error', async () => { + vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('transport failed ghu_access'))) + const failure = await verifyGitHubRepositoriesIdentity('ghu_access', work.email).catch( + (error: unknown) => error + ) + expect(failure).toMatchObject({ reason: 'provider_unavailable', stage: 'profile' }) + expect(String(failure)).not.toContain('ghu_access') + expect(failure).not.toHaveProperty('cause') + }) + + it('distinguishes an invalid email response from a verified email mismatch', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValueOnce(response(user)) + .mockResolvedValueOnce( + response([ + { + email: work.email, + primary: false, + }, + ]) + ) + ) + await expect(verifyGitHubRepositoriesIdentity('ghu_access', work.email)).rejects.toMatchObject({ + reason: 'invalid_response', + stage: 'emails', + }) + }) + + it('does not claim a mismatch when the bounded email scan cannot finish', async () => { + const fetchMock = vi.fn().mockResolvedValueOnce(response(user)) + for (let page = 0; page < 10; page++) { + fetchMock.mockResolvedValueOnce(response(Array.from({ length: 100 }, () => primary))) + } + vi.stubGlobal('fetch', fetchMock) + await expect(verifyGitHubRepositoriesIdentity('ghu_access', work.email)).rejects.toMatchObject({ + reason: 'invalid_response', + stage: 'emails', + }) + expect(fetchMock).toHaveBeenCalledTimes(11) }) }) diff --git a/apps/sim/lib/oauth/github-repositories.ts b/apps/sim/lib/oauth/github-repositories.ts index 5c3ce80c12c..93c9a6361e5 100644 --- a/apps/sim/lib/oauth/github-repositories.ts +++ b/apps/sim/lib/oauth/github-repositories.ts @@ -3,6 +3,10 @@ import { getOAuth2Tokens, type OAuth2Tokens } from 'better-auth/oauth2' import type { GenericOAuthConfig } from 'better-auth/plugins' import { z } from 'zod' import { readResponseJsonWithLimit } from '@/lib/core/utils/stream-limits' +import { + OAuthIdentityVerificationError, + type OAuthIdentityVerificationStage, +} from '@/lib/oauth/identity-error' export const GITHUB_REPOSITORIES_PROVIDER_ID = 'github-repositories' export const GITHUB_AUTHORIZATION_URL = 'https://github.com/login/oauth/authorize' @@ -47,7 +51,7 @@ export async function verifyGitHubRepositoriesIdentity( expectedEmail?: string ) { if (!accessToken.startsWith('ghu_')) { - throw new Error('Connect with a GitHub App user authorization') + throw new OAuthIdentityVerificationError('provider_rejected', 'token') } const headers = { Accept: 'application/vnd.github+json', @@ -55,29 +59,76 @@ export async function verifyGitHubRepositoriesIdentity( 'X-GitHub-Api-Version': '2022-11-28', 'User-Agent': 'Sim', } - async function get(path: string): Promise { + async function get(path: string, stage: OAuthIdentityVerificationStage): Promise { const signal = AbortSignal.timeout(REQUEST_TIMEOUT_MS) - const response = await fetch(`${API_BASE_URL}${path}`, { - headers, - signal, - redirect: 'error', - }) + let response: Response + try { + response = await fetch(`${API_BASE_URL}${path}`, { + headers, + signal, + redirect: 'error', + }) + } catch { + throw new OAuthIdentityVerificationError('provider_unavailable', stage) + } if (!response.ok) { - await response.body?.cancel() - throw new Error(`GitHub identity request failed with HTTP ${response.status}`) + let rateLimited = + response.status === 429 || + (response.status === 403 && + (response.headers.get('x-ratelimit-remaining') === '0' || + response.headers.has('retry-after'))) + if (response.status === 403 && !rateLimited) { + const body = await readResponseJsonWithLimit(response, { + maxBytes: RESPONSE_MAX_BYTES, + signal, + label: 'GitHub identity error response', + }).catch(() => null) + rateLimited = + typeof body === 'object' && + body !== null && + 'message' in body && + typeof body.message === 'string' && + /rate limit|abuse detection/i.test(body.message) + } else { + await response.body?.cancel().catch(() => undefined) + } + const reason = rateLimited + ? 'rate_limited' + : response.status >= 500 + ? 'provider_unavailable' + : response.status === 403 && stage === 'emails' + ? 'email_access_denied' + : 'provider_rejected' + throw new OAuthIdentityVerificationError(reason, stage, response.status) + } + try { + return await readResponseJsonWithLimit(response, { + maxBytes: RESPONSE_MAX_BYTES, + signal, + label: 'GitHub identity response', + }) + } catch { + throw new OAuthIdentityVerificationError( + signal.aborted ? 'provider_unavailable' : 'invalid_response', + stage, + response.status + ) } - return readResponseJsonWithLimit(response, { - maxBytes: RESPONSE_MAX_BYTES, - signal, - label: 'GitHub identity response', - }) } - const user = userSchema.parse(await get('/user')) + const parsedUser = userSchema.safeParse(await get('/user', 'profile')) + if (!parsedUser.success) { + throw new OAuthIdentityVerificationError('invalid_response', 'profile') + } + const user = parsedUser.data const normalizedEmail = expectedEmail?.trim().toLowerCase() for (let page = 1; page <= MAX_EMAIL_PAGES; page++) { - const emails = emailsSchema.parse( - await get(`/user/emails?per_page=${EMAIL_PAGE_SIZE}&page=${page}`) + const parsedEmails = emailsSchema.safeParse( + await get(`/user/emails?per_page=${EMAIL_PAGE_SIZE}&page=${page}`, 'emails') ) + if (!parsedEmails.success) { + throw new OAuthIdentityVerificationError('invalid_response', 'emails') + } + const emails = parsedEmails.data const matching = emails.find( (entry) => entry.verified && @@ -94,9 +145,11 @@ export async function verifyGitHubRepositoriesIdentity( grantedScopes: [], } } - if (emails.length < EMAIL_PAGE_SIZE) break + if (emails.length < EMAIL_PAGE_SIZE) { + throw new OAuthIdentityVerificationError('email_mismatch', 'emails') + } } - throw new Error('GitHub did not verify the required email address') + throw new OAuthIdentityVerificationError('invalid_response', 'emails') } interface GitHubRepositoriesProviderParams { diff --git a/apps/sim/lib/oauth/identity-error.ts b/apps/sim/lib/oauth/identity-error.ts new file mode 100644 index 00000000000..8fbac5f2a1c --- /dev/null +++ b/apps/sim/lib/oauth/identity-error.ts @@ -0,0 +1,21 @@ +export type OAuthIdentityFailureReason = + | 'email_mismatch' + | 'email_access_denied' + | 'provider_rejected' + | 'rate_limited' + | 'provider_unavailable' + | 'invalid_response' + +export type OAuthIdentityVerificationStage = 'token' | 'profile' | 'emails' + +/** Carries safe diagnostics without retaining provider bodies, tokens, or email addresses. */ +export class OAuthIdentityVerificationError extends Error { + constructor( + readonly reason: OAuthIdentityFailureReason, + readonly stage: OAuthIdentityVerificationStage, + readonly httpStatus?: number + ) { + super(`OAuth identity verification failed: ${reason}`) + this.name = 'OAuthIdentityVerificationError' + } +} diff --git a/apps/sim/lib/oauth/types.ts b/apps/sim/lib/oauth/types.ts index d2bc4cacce3..b97bc4fd9e8 100644 --- a/apps/sim/lib/oauth/types.ts +++ b/apps/sim/lib/oauth/types.ts @@ -33,6 +33,7 @@ export const SLACK_CUSTOM_BOT_SECRET_TYPE = 'slack_custom_bot' as const export type OAuthProvider = | 'github-repositories' + | 'github-app-installation' | 'google' | 'google-email' | 'google-drive' diff --git a/apps/sim/lib/selectors/application/execute-selector.test.ts b/apps/sim/lib/selectors/application/execute-selector.test.ts index a0aa015b5b8..e0f2a9e0625 100644 --- a/apps/sim/lib/selectors/application/execute-selector.test.ts +++ b/apps/sim/lib/selectors/application/execute-selector.test.ts @@ -166,19 +166,38 @@ describe('executeSelector', () => { ]) }) - it('keeps generic organization browsing admin-only', async () => { - mocks.requireOrganizationMembership.mockRejectedValueOnce(new Error('Admin required')) - await expect( - execute({ scope: { kind: 'organization', organizationId: 'org-1' } }) - ).rejects.toThrow('Admin required') - expect(mocks.requireOrganizationMembership).toHaveBeenCalledWith( - principal, - 'org-1', - 'admin', - 'knowledge.use' + it.each(['gmail.labels', 'github.installationRepositories'])( + 'keeps generic organization browsing admin-only for %s', + async (selectorKey) => { + mocks.requireOrganizationMembership.mockRejectedValueOnce(new Error('Admin required')) + await expect( + execute({ selectorKey, scope: { kind: 'organization', organizationId: 'org-1' } }) + ).rejects.toThrow('Admin required') + expect(mocks.requireOrganizationMembership).toHaveBeenCalledWith( + principal, + 'org-1', + 'admin', + 'knowledge.use' + ) + expect(mocks.authorizePersonalSearch).not.toHaveBeenCalled() + expect(mocks.getAttachment).not.toHaveBeenCalled() + } + ) + + it('refuses organization GitHub installation browsing from workspace scope before resolving credentials', async () => { + mocks.resolveScope.mockResolvedValue({ + workspaceId: 'workspace-1', + workspaceOrganizationId: null, + allowPersonalApiKeys: true, + selectorKey: 'github.installationRepositories', + selectorManifest: getSelectorManifestEntry('github.installationRepositories'), + selectorScope: scope, + }) + await expect(execute({ selectorKey: 'github.installationRepositories' })).rejects.toThrow( + 'Context unavailable' ) - expect(mocks.authorizePersonalSearch).not.toHaveBeenCalled() - expect(mocks.getAttachment).not.toHaveBeenCalled() + expect(mocks.authorizeCredential).not.toHaveBeenCalled() + expect(mocks.executeAttachment).not.toHaveBeenCalled() }) it('rejects a personal setup marker outside its approved provider selector and organization scope', async () => { diff --git a/apps/sim/lib/selectors/manifest.test.ts b/apps/sim/lib/selectors/manifest.test.ts index e9cb015bf5f..f8a41558c4b 100644 --- a/apps/sim/lib/selectors/manifest.test.ts +++ b/apps/sim/lib/selectors/manifest.test.ts @@ -9,8 +9,8 @@ describe('selector manifest', () => { const count = (classification: (typeof classifications)[number]) => classifications.filter((value) => value === classification).length - expect(Object.keys(selectorManifest)).toHaveLength(97) - expect(count('provider-server')).toBe(84) + expect(Object.keys(selectorManifest)).toHaveLength(98) + expect(count('provider-server')).toBe(85) expect(count('internal-server')).toBe(12) expect(count('local')).toBe(1) expect(classifications).not.toContain('provider-legacy') @@ -36,7 +36,7 @@ describe('selector manifest', () => { const rawConnectionKeys = providerKeys.filter( (key) => !serverSelectorRegistry[key as keyof typeof serverSelectorRegistry].credential ) - expect(providerKeys).toHaveLength(84) + expect(providerKeys).toHaveLength(85) expect(rawConnectionKeys.sort()).toEqual([ 'cloudwatch.logGroups', 'cloudwatch.logStreams', diff --git a/apps/sim/lib/selectors/manifest.ts b/apps/sim/lib/selectors/manifest.ts index b6596873305..6d65ad50ea5 100644 --- a/apps/sim/lib/selectors/manifest.ts +++ b/apps/sim/lib/selectors/manifest.ts @@ -147,6 +147,10 @@ export const selectorManifest = { detail: true, }), 'gmail.labels': providerSelector(['impersonateUserEmail']), + 'github.installationRepositories': { + ...providerSelector([], { listMode: 'paginated', detail: true, unknownDetail: true }), + scopeKinds: ['organization'], + }, 'google.calendar': providerSelector(['impersonateUserEmail'], { listMode: 'paginated', detail: true, diff --git a/apps/sim/lib/selectors/server/providers/github.test.ts b/apps/sim/lib/selectors/server/providers/github.test.ts new file mode 100644 index 00000000000..2785a57a37f --- /dev/null +++ b/apps/sim/lib/selectors/server/providers/github.test.ts @@ -0,0 +1,128 @@ +/** @vitest-environment node */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { ExecuteServerSelectorArgs } from '@/lib/selectors/server/types' + +const mocks = vi.hoisted(() => ({ + authorize: vi.fn(), + decrypt: vi.fn(), + parseBinding: vi.fn(), + list: vi.fn(), + resolve: vi.fn(), +})) +vi.mock('@/lib/core/security/encryption', () => ({ decryptSecret: mocks.decrypt })) +vi.mock('@/lib/credentials/application/organization-credentials', () => ({ + authorizeOrganizationCredentialUse: mocks.authorize, +})) +vi.mock('@/lib/oauth/github-installation', () => ({ + GitHubInstallationError: class extends Error {}, + parseGitHubInstallationBinding: mocks.parseBinding, + listGitHubInstallationRepositories: mocks.list, + resolveGitHubInstallationRepository: mocks.resolve, +})) + +import { githubSelectorAttachments } from '@/lib/selectors/server/providers/github' + +const principal = { kind: 'session', userId: 'admin', sessionId: 'session' } as const +const binding = { installationId: '21', accountId: '11' } +const row = { + organizationId: 'org-1', + workspaceId: null, + type: 'service_account', + providerSubjectId: '21', + providerTenantId: '11', + encryptedServiceAccountKey: 'encrypted', +} +function args(): ExecuteServerSelectorArgs { + return { + selectorKey: 'github.installationRepositories', + principal, + requesterUserId: principal.userId, + scope: { kind: 'organization', organizationId: 'org-1' }, + organizationId: 'org-1', + context: { oauthCredential: 'credential-1' }, + credential: { + suppliedId: 'credential-1', + providerId: 'github-app-installation', + organization: { principal, organizationId: 'org-1' }, + }, + request: { kind: 'list' }, + references: new Map(), + protectedValues: { add: vi.fn(), contains: () => false, containsExceptExact: () => false }, + } +} +const execute = (input: ExecuteServerSelectorArgs) => + githubSelectorAttachments['github.installationRepositories'].execute(input) + +describe('GitHub installation repository selector', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.authorize.mockResolvedValue({ credential: row }) + mocks.decrypt.mockResolvedValue({ decrypted: '{}' }) + mocks.parseBinding.mockReturnValue(binding) + mocks.list.mockResolvedValue({ + repositories: [{ id: '101', fullName: 'team/repo' }], + hasMore: true, + }) + }) + it('rechecks the acting admin connection and passes only bounded repository options', async () => { + const input = args() + input.request = { kind: 'list', cursor: '2' } + input.signal = new AbortController().signal + expect(await execute(input)).toEqual({ + kind: 'list', + items: [{ id: 'team/repo', label: 'team/repo' }], + nextCursor: '3', + }) + expect(mocks.authorize).toHaveBeenCalledWith( + expect.objectContaining({ + principal, + organizationId: 'org-1', + credentialId: 'credential-1', + expectedProviderId: 'github-app-installation', + }) + ) + expect(mocks.list).toHaveBeenCalledWith(binding, { page: 2, signal: input.signal }) + }) + it.each(['0', '101', '2.5', 'https://evil.example', '02'])( + 'refuses cursor %s before reading secrets', + async (cursor) => { + const input = args() + input.request = { kind: 'list', cursor } + await expect(execute(input)).rejects.toThrow('Context unavailable') + expect(mocks.authorize).not.toHaveBeenCalled() + expect(mocks.list).not.toHaveBeenCalled() + } + ) + it('refuses workspace and mismatched organization scope', async () => { + await expect( + execute({ ...args(), scope: { kind: 'workspace', workspaceId: 'ws-1' } }) + ).rejects.toThrow('Context unavailable') + await expect(execute({ ...args(), organizationId: 'org-2' })).rejects.toThrow( + 'Context unavailable' + ) + expect(mocks.authorize).not.toHaveBeenCalled() + }) + it('never reads provider data after credential authorization fails', async () => { + mocks.authorize.mockRejectedValue(new Error('Forbidden')) + await expect(execute(args())).rejects.toThrow('Forbidden') + expect(mocks.decrypt).not.toHaveBeenCalled() + expect(mocks.list).not.toHaveBeenCalled() + }) + it.each([ + { ...row, organizationId: 'other' }, + { ...row, workspaceId: 'ws' }, + { ...row, providerSubjectId: '22' }, + { ...row, providerTenantId: '12' }, + ])('refuses a stale or mismatched stored binding %#', async (credential) => { + mocks.authorize.mockResolvedValue({ credential }) + await expect(execute(args())).rejects.toThrow('Connection unavailable') + expect(mocks.list).not.toHaveBeenCalled() + }) + it('bounds the last repository page explicitly', async () => { + const input = args() + input.request = { kind: 'list', cursor: '100' } + expect(await execute(input)).toMatchObject({ + diagnostics: { truncated: { reason: 'provider-cap', pages: 100 } }, + }) + }) +}) diff --git a/apps/sim/lib/selectors/server/providers/github.ts b/apps/sim/lib/selectors/server/providers/github.ts new file mode 100644 index 00000000000..f6007104014 --- /dev/null +++ b/apps/sim/lib/selectors/server/providers/github.ts @@ -0,0 +1,100 @@ +import { decryptSecret } from '@/lib/core/security/encryption' +import { authorizeOrganizationCredentialUse } from '@/lib/credentials/application/organization-credentials' +import { + GitHubInstallationError, + listGitHubInstallationRepositories, + parseGitHubInstallationBinding, + resolveGitHubInstallationRepository, +} from '@/lib/oauth/github-installation' +import { GITHUB_INSTALLATION_PROVIDER_ID } from '@/lib/oauth/github-installation-types' +import { parseGitHubRepository } from '@/lib/oauth/github-repository' +import { + SelectorConnectionUnavailableError, + SelectorContextUnavailableError, +} from '@/lib/selectors/server/errors' +import { + detailSelectorResult, + listSelectorResult, + type ServerSelectorAttachmentMap, +} from '@/lib/selectors/server/types' + +export const githubSelectorAttachments = { + 'github.installationRepositories': { + credential: { kind: 'stored', field: 'oauthCredential', serviceIds: ['github-repositories'] }, + destination: 'fixed', + async execute(args) { + if ( + args.scope.kind !== 'organization' || + args.workspaceId || + args.organizationId !== args.scope.organizationId || + !args.credential?.organization || + args.credential.providerId !== GITHUB_INSTALLATION_PROVIDER_ID + ) + throw new SelectorContextUnavailableError() + const page = args.request.kind === 'list' ? Number(args.request.cursor ?? '1') : 1 + if ( + !Number.isSafeInteger(page) || + page < 1 || + page > 100 || + (args.request.kind === 'list' && + args.request.cursor !== undefined && + String(page) !== args.request.cursor) + ) + throw new SelectorContextUnavailableError() + const { credential } = await authorizeOrganizationCredentialUse({ + ...args.credential.organization, + credentialId: args.credential.suppliedId, + expectedProviderId: GITHUB_INSTALLATION_PROVIDER_ID, + requestId: 'selector-execution', + purpose: 'browsing', + }) + if ( + credential.type !== 'service_account' || + credential.organizationId !== args.scope.organizationId || + credential.workspaceId !== null || + !credential.encryptedServiceAccountKey || + credential.encryptedServiceAccountKey.length > 16_384 + ) + throw new SelectorConnectionUnavailableError() + const { decrypted } = await decryptSecret(credential.encryptedServiceAccountKey) + args.protectedValues.add(decrypted) + const binding = parseGitHubInstallationBinding(JSON.parse(decrypted)) + if ( + binding.installationId !== credential.providerSubjectId || + binding.accountId !== credential.providerTenantId + ) + throw new SelectorConnectionUnavailableError() + try { + if (args.request.kind === 'detail') { + try { + parseGitHubRepository(args.request.id) + } catch { + return detailSelectorResult(null) + } + const repository = await resolveGitHubInstallationRepository(binding, args.request.id, { + signal: args.signal, + }) + return detailSelectorResult({ id: repository.fullName, label: repository.fullName }) + } + const result = await listGitHubInstallationRepositories(binding, { + page, + signal: args.signal, + }) + return listSelectorResult( + result.repositories.map((repository) => ({ + id: repository.fullName, + label: repository.fullName, + })), + result.hasMore && page < 100 ? String(page + 1) : undefined, + result.hasMore && page === 100 + ? { truncated: { reason: 'provider-cap', pages: 100 } } + : undefined + ) + } catch (error) { + if (args.signal?.aborted) throw error + if (error instanceof GitHubInstallationError) throw new SelectorConnectionUnavailableError() + throw error + } + }, + }, +} satisfies ServerSelectorAttachmentMap<'github.installationRepositories'> diff --git a/apps/sim/lib/selectors/server/registry.ts b/apps/sim/lib/selectors/server/registry.ts index 4e067cc778e..09441960e09 100644 --- a/apps/sim/lib/selectors/server/registry.ts +++ b/apps/sim/lib/selectors/server/registry.ts @@ -9,6 +9,7 @@ import { calcomSelectorAttachments } from '@/lib/selectors/server/providers/calc import { clickupSelectorAttachments } from '@/lib/selectors/server/providers/clickup' import { cloudWatchSelectorAttachments } from '@/lib/selectors/server/providers/cloudwatch' import { confluenceSelectorAttachments } from '@/lib/selectors/server/providers/confluence' +import { githubSelectorAttachments } from '@/lib/selectors/server/providers/github' import { googleSelectorAttachments } from '@/lib/selectors/server/providers/google' import { harmonicSelectorAttachments } from '@/lib/selectors/server/providers/harmonic' import { hubspotSelectorAttachments } from '@/lib/selectors/server/providers/hubspot' @@ -46,6 +47,7 @@ export const serverSelectorRegistry = { ...cloudWatchSelectorAttachments, ...confluenceSelectorAttachments, ...googleSelectorAttachments, + ...githubSelectorAttachments, ...harmonicSelectorAttachments, ...hubspotSelectorAttachments, ...imapSelectorAttachments, From 5beb20664ae95940d8f0011f908478874907856a Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 10 Sep 2026 21:20:01 -0700 Subject: [PATCH 2/3] fix(github): complete repository setup through one approval flow --- .../self-hosting/integrations-oauth.mdx | 9 +- apps/docs/content/docs/search/github.mdx | 24 +- .../static/search/github-app-callback.jpg | Bin 82464 -> 0 bytes .../public/static/search/github-connect.png | Bin 0 -> 34406 bytes .../search/github-installation-connect.jpg | Bin 18761 -> 0 bytes .../search/github-installation-select.jpg | Bin 16234 -> 0 bytes .../static/search/github-member-connect.png | Bin 32397 -> 31829 bytes .../static/search/github-member-connected.png | Bin 25572 -> 26667 bytes .../static/search/github-repositories.png | Bin 36427 -> 47545 bytes .../search/github-repository-picker.png | Bin 0 -> 49125 bytes .../static/search/github-repository-setup.png | Bin 40073 -> 0 bytes .../credential-groups/oauth-callback.test.ts | 54 ++ .../api/credential-groups/oauth-callback.ts | 32 +- .../knowledge/github/setup/callback/route.ts | 51 ++ .../knowledge/github/setup/continue/route.ts | 50 ++ .../knowledge/github/setup/redirects.test.ts | 94 +++ .../app/api/knowledge/github/setup/route.ts | 52 ++ .../knowledge/github/setup/selection/route.ts | 21 + .../app/api/knowledge/migrated-routes.test.ts | 2 +- apps/sim/app/knowledge/github/setup/page.tsx | 30 + .../app/knowledge/github/setup/setup.test.tsx | 118 ++++ apps/sim/app/knowledge/github/setup/setup.tsx | 85 +++ .../integrations/integrations.test.tsx | 254 ++++++++ .../add-connector-modal.test.tsx | 50 +- .../add-connector-modal.tsx | 41 +- .../connector-settings-fields.test.tsx | 30 +- .../connector-settings-fields.tsx | 33 +- .../use-connector-settings-form.test.tsx | 48 +- .../components/github-installation-modal.tsx | 222 ------- .../github/installation-connection-field.tsx | 32 +- .../queries/github-search-setup.test.tsx | 162 +++++ apps/sim/hooks/queries/github-search-setup.ts | 77 +++ ...e-github-installation-setup-cache.test.tsx | 178 +++++ .../use-github-installation-setup.test.tsx | 224 +++++++ .../hooks/use-github-installation-setup.ts | 172 +++++ .../api/contracts/knowledge/github-setup.ts | 108 ++++ apps/sim/lib/credential-groups/oauth-state.ts | 18 +- apps/sim/lib/credential-groups/oauth.ts | 2 +- .../application/connector-access.test.ts | 176 +++++ .../application/github-installation-error.ts | 9 +- .../application/github-installation-source.ts | 2 +- .../application/github-installations.test.ts | 73 ++- .../application/github-installations.ts | 52 +- .../application/github-setup.test.ts | 608 ++++++++++++++++++ .../lib/knowledge/application/github-setup.ts | 515 +++++++++++++++ .../lib/knowledge/application/operations.ts | 63 ++ .../lib/knowledge/github-setup-navigation.ts | 12 + apps/sim/lib/knowledge/github-setup-state.ts | 171 +++++ .../lib/knowledge/github-setup-urls.test.ts | 64 ++ apps/sim/lib/knowledge/github-setup-urls.ts | 27 + 50 files changed, 3700 insertions(+), 345 deletions(-) delete mode 100644 apps/docs/public/static/search/github-app-callback.jpg create mode 100644 apps/docs/public/static/search/github-connect.png delete mode 100644 apps/docs/public/static/search/github-installation-connect.jpg delete mode 100644 apps/docs/public/static/search/github-installation-select.jpg create mode 100644 apps/docs/public/static/search/github-repository-picker.png delete mode 100644 apps/docs/public/static/search/github-repository-setup.png create mode 100644 apps/sim/app/api/knowledge/github/setup/callback/route.ts create mode 100644 apps/sim/app/api/knowledge/github/setup/continue/route.ts create mode 100644 apps/sim/app/api/knowledge/github/setup/redirects.test.ts create mode 100644 apps/sim/app/api/knowledge/github/setup/route.ts create mode 100644 apps/sim/app/api/knowledge/github/setup/selection/route.ts create mode 100644 apps/sim/app/knowledge/github/setup/page.tsx create mode 100644 apps/sim/app/knowledge/github/setup/setup.test.tsx create mode 100644 apps/sim/app/knowledge/github/setup/setup.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/search/components/github-installation-modal.tsx create mode 100644 apps/sim/hooks/queries/github-search-setup.test.tsx create mode 100644 apps/sim/hooks/queries/github-search-setup.ts create mode 100644 apps/sim/hooks/use-github-installation-setup-cache.test.tsx create mode 100644 apps/sim/hooks/use-github-installation-setup.test.tsx create mode 100644 apps/sim/hooks/use-github-installation-setup.ts create mode 100644 apps/sim/lib/api/contracts/knowledge/github-setup.ts create mode 100644 apps/sim/lib/knowledge/application/github-setup.test.ts create mode 100644 apps/sim/lib/knowledge/application/github-setup.ts create mode 100644 apps/sim/lib/knowledge/github-setup-navigation.ts create mode 100644 apps/sim/lib/knowledge/github-setup-state.ts create mode 100644 apps/sim/lib/knowledge/github-setup-urls.test.ts create mode 100644 apps/sim/lib/knowledge/github-setup-urls.ts diff --git a/apps/docs/content/docs/platform/self-hosting/integrations-oauth.mdx b/apps/docs/content/docs/platform/self-hosting/integrations-oauth.mdx index c57b18f4991..26062069c3f 100644 --- a/apps/docs/content/docs/platform/self-hosting/integrations-oauth.mdx +++ b/apps/docs/content/docs/platform/self-hosting/integrations-oauth.mdx @@ -142,14 +142,15 @@ Replace `` with your configured public origin, such as `htt | Expire user authorization tokens | Enabled | | Request user authorization (OAuth) during installation | Disabled | | Enable Device Flow | Disabled | -| Post installation → Setup URL | Empty | +| Post installation → Setup URL | `/api/knowledge/github/setup/callback` | +| Post installation → Redirect on update | Enabled | | Webhook → Active | Disabled | -Authorization starts from Sim so the callback can finish the pending connection. The connector polls GitHub's API and does not need a webhook. +Authorization starts from Sim so the callback can finish the pending connection. The **Setup URL** returns installation approval to that same setup attempt. Keep **Request user authorization (OAuth) during installation** disabled: Sim starts account authorization when it is needed, before installation. See GitHub's [Setup URL guide](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/about-the-setup-url). -GitHub App registration with the Redirect URI, expiring tokens enabled, and installation authorization, Device Flow, and webhooks disabled +For an existing deployment, deploy the application with the setup callback before changing the App registration. Use a separate GitHub App for each deployment origin, such as staging and production, so installation approval returns to the instance that started it. Keep the existing user authorization callback above unchanged. -*Example registration. Replace `sim.example.com` with your Sim domain.* +The connector polls GitHub's API and does not need a webhook. diff --git a/apps/docs/content/docs/search/github.mdx b/apps/docs/content/docs/search/github.mdx index 17d3b4157a4..516e03cf2a6 100644 --- a/apps/docs/content/docs/search/github.mdx +++ b/apps/docs/content/docs/search/github.mdx @@ -33,13 +33,13 @@ Open **Settings → Sources → Add source** and select **GitHub**. This opens * ### Connect GitHub -Select **Connect GitHub**. If prompted, connect your own GitHub account first, finish authorization, then return and select **Refresh**. +Select **Connect GitHub** and approve the requested access on GitHub. If you need to install the App, choose your account or organization and the repositories it can read. Sim automatically returns you to the repository picker when approval finishes. Your open form stays in place. -Select **Install GitHub App**, choose your GitHub account or organization, and choose which repositories the App can read. If it is already installed, check its repository selection. Return to Sim, select **Refresh**, choose the installation, and select **Use installation**. Only installations on your own account or organizations you own are available. +Add GitHub repository with one Connect GitHub action -Connect GitHub dialog with an installation selected +Sim reuses an eligible installation when one is already available. If you have more than one, choose the GitHub account to use. Only installations on your own account or organizations you own are eligible. -Sim reuses this connection when you add more repositories in the same organization. Installing on all repositories does not automatically add all of them to Search. +When adding more repositories, your connected GitHub account is already selected. To connect a different organization, open the **GitHub** picker and select **Connect another organization**. Installing on all repositories does not automatically add all of them to Search. @@ -48,7 +48,7 @@ Sim reuses this connection when you add more repositories in the same organizati Search the **Repository** picker and select one repository. It lists repositories available to the selected GitHub installation. Use **Add repository** again to add another. -Add GitHub repository with one connected GitHub account and a searchable repository picker +Searchable repository picker for the connected GitHub account Open **More options** if you need a different branch, path or extension filters, or metadata tags. @@ -68,11 +68,11 @@ Open **More options** if you need a different branch, path or extension filters, Teammates open **Integrations** in the main sidebar, select **Connect** beside **GitHub**, and authorize their own GitHub account. Members do not choose repositories or an indexing method. -Member Integrations page with one GitHub Connect action +Member Integrations page with one GitHub Connect action Each person connects once per Sim organization. When an admin adds another repository, Sim reuses existing member connections and checks each person's access automatically. No additional repository invitation is needed. The member's GitHub row shows their connection status; **Reconnect** appears if authorization needs renewal, and **Disconnect** remains available for their own account. -Connected member GitHub account reused across repositories +Connected member GitHub account reused across repositories The installation can begin indexing as soon as the admin saves a repository. For private repositories, both the person's account and the App installation must have access. GitHub also permits App user tokens to read public repositories without an installation; see [GitHub's permission rules](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app). @@ -85,7 +85,7 @@ Admins can use **Settings → Sources → People**, filter by **GitHub**, then s Open **Settings → Sources → GitHub** to see the repositories and their sync status. Select **Add repository** to add another, or open an existing repository to inspect **Documents**, edit **Settings**, or review **Sync history**. -Admin GitHub page listing configured repositories and their sync status +Admin GitHub page listing configured repositories and their sync status The document filter separates **Failed** processing from intentionally **Skipped** files. Skipped entries explain why they were omitted and do not offer a processing retry. They are reconsidered when the source file changes. Empty files, unsupported binaries, oversized files, and links that do not resolve to files within the repository are skipped. Supported PDFs and Office documents use the shared knowledge-base parser. @@ -93,6 +93,8 @@ The document filter separates **Failed** processing from intentionally **Skipped A genuine source download failure remains **Failed** and is retried by scheduled connector sync. A processing failure with downloaded content can be retried from the document view. +**Change connection** in a repository's Settings repairs its connection while keeping the same repository. The replacement connection must have access to that repository. To index a different repository, use **Add repository**. + ## How access is enforced GitHub App installation access supplies file contents for indexing. Each reader's own connected GitHub account determines which repository's indexed content they can search. Sim organization admins follow the same rule as other readers. @@ -108,8 +110,10 @@ This is an installation plus personal authorization flow. GitHub Search does not | GitHub is unavailable in Search | Ask your Sim organization admin to enable GitHub under **Settings → Sources**. For self-hosted Sim, also check the [GitHub App configuration](/platform/self-hosting/integrations-oauth#github-search). | | Repository cannot be read | Confirm the App is installed on that repository and your GitHub account has access. For SAML organizations, establish your GitHub SSO session before reconnecting. | | A teammate cannot authorize the App | Check the GitHub organization's App policies and required approvals. Self-hosted deployments must allow the teammate's account in their App visibility settings. | -| No eligible installations found | Finish connecting your own GitHub account, install the configured App on your account or an organization you own, then select **Refresh**. An installation of a different App or one you only have repository access to cannot be selected. | -| Repository is missing or GitHub rejects it | Confirm the repository is included in the selected installation and refresh the picker. Update the source's Repository field after a rename. To index a different repository or one transferred to another owner, use **Add repository** with the appropriate connection. | +| GitHub approval did not finish | Select **Connect GitHub** again and complete approval. If GitHub requires owner approval, ask an organization owner to install the App first, then retry. Requesting an installation does not complete it. | +| No approval window opens | Allow pop-ups for Sim in your browser and select **Connect GitHub** again. You can cancel an unfinished connection without losing your repository form. | +| GitHub does not return to Sim after installation | For self-hosted Sim, verify the App’s **Setup URL** and **Redirect on update** settings. See [GitHub Search configuration](/platform/self-hosting/integrations-oauth#github-search). | +| Repository is missing or GitHub rejects it | Confirm the repository is included in the selected installation and reopen the picker. Update the source's Repository field after a rename. To index a different repository or one transferred to another owner, use **Add repository** with the appropriate connection. | | Your GitHub email does not match | Add and verify the email used by your Sim account in GitHub's email settings, then reconnect. A verified secondary or private email is supported; a public profile email alone is insufficient. | | GitHub does not allow Sim to read your email addresses | Ask the GitHub App administrator to enable read access to user email addresses, then reconnect. This is a user permission, separate from repository permissions. | | GitHub is temporarily unavailable during authorization | Retry connecting after GitHub recovers or its rate limit resets. | diff --git a/apps/docs/public/static/search/github-app-callback.jpg b/apps/docs/public/static/search/github-app-callback.jpg deleted file mode 100644 index 5ae4a20392b9d66aa0e58b458bdbbd2bf693f7be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82464 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�of0Sy>rJALs!H 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 literal 0 HcmV?d00001 diff --git a/apps/docs/public/static/search/github-repository-setup.png b/apps/docs/public/static/search/github-repository-setup.png deleted file mode 100644 index a16764058d5d4b13febd9ec17cf4c9c93fc4bd99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40073 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 diff --git a/apps/sim/app/api/credential-groups/oauth-callback.test.ts b/apps/sim/app/api/credential-groups/oauth-callback.test.ts index cca93d03e1d..4a15fcadf1a 100644 --- a/apps/sim/app/api/credential-groups/oauth-callback.test.ts +++ b/apps/sim/app/api/credential-groups/oauth-callback.test.ts @@ -10,11 +10,21 @@ const mocks = vi.hoisted(() => ({ completeOAuth: vi.fn(), consumeAttempt: vi.fn(), logError: vi.fn(), + completeSetupOAuth: vi.fn(), })) vi.mock('@sim/logger', () => ({ createLogger: () => ({ error: mocks.logError }), })) +vi.mock('@/lib/knowledge/application/github-setup', () => ({ + completeGitHubSetupReaderOAuth: { execute: mocks.completeSetupOAuth }, +})) +vi.mock('@/lib/api/server/routes', () => ({ + internalSessionAuth: { + authenticate: async () => ({ kind: 'session', userId: 'admin', sessionId: 'browser' }), + }, +})) +vi.mock('@/lib/core/utils/urls', () => ({ getBaseUrl: () => 'https://sim.test' })) vi.mock('@/lib/credential-groups/application/enrollment-auth', () => ({ credentialGroupOAuthAttemptPrincipal: mocks.authenticate, })) @@ -135,3 +145,47 @@ describe('GitHub managed OAuth failure presentation', () => { }) }) }) + +describe('GitHub installation setup OAuth return target', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + it('resumes only the server-owned setup after the guarded OAuth completion', async () => { + mocks.consumeAttempt.mockResolvedValue({ + ...attempt, + returnTo: 'github-installation', + organizationId: 'organization', + completionId, + completionRedirect: true, + }) + mocks.completeSetupOAuth.mockResolvedValue({ credentialId: 'reader' }) + const response = await completeCallback() + const url = new URL(response.headers.get('location')!, 'https://sim.test') + expect(url.pathname).toBe('/api/knowledge/github/setup/continue') + expect(url.searchParams.get('organizationId')).toBe('organization') + expect(url.searchParams.get('setupId')).toBe(completionId) + expect(response.headers.get('referrer-policy')).toBe('no-referrer') + expect(mocks.completeSetupOAuth).toHaveBeenCalled() + expect(mocks.completeOAuth).not.toHaveBeenCalled() + }) + it('returns classified OAuth failure to setup status without accepting a client redirect URL', async () => { + mocks.consumeAttempt.mockResolvedValue({ + ...attempt, + returnTo: 'github-installation', + organizationId: 'organization', + completionId, + completionRedirect: true, + }) + const response = await handleCredentialGroupOAuthCallback({ + request: new NextRequest('https://sim.test/api/auth/oauth2/callback/github-repositories'), + provider: 'github-repositories', + query: { state: 'cg_state', error: 'access_denied' }, + limited: null, + }) + const url = new URL(response.headers.get('location')!, 'https://sim.test') + expect(url.origin).toBe('https://sim.test') + expect(url.pathname).toBe('/api/knowledge/github/setup/continue') + expect(url.searchParams.get('oauth')).toBe('denied') + expect(url.searchParams.get('setupId')).toBe(completionId) + }) +}) diff --git a/apps/sim/app/api/credential-groups/oauth-callback.ts b/apps/sim/app/api/credential-groups/oauth-callback.ts index d040b135edb..1883ecdcb21 100644 --- a/apps/sim/app/api/credential-groups/oauth-callback.ts +++ b/apps/sim/app/api/credential-groups/oauth-callback.ts @@ -1,7 +1,8 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import type { NextRequest, NextResponse } from 'next/server' +import { type NextRequest, NextResponse } from 'next/server' import type { CredentialGroupOAuthCallbackQuery } from '@/lib/api/contracts/credential-groups' +import { internalSessionAuth } from '@/lib/api/server/routes' import { asOrchestrationError, statusForOrchestrationError } from '@/lib/core/orchestration/types' import { credentialGroupOAuthAttemptPrincipal } from '@/lib/credential-groups/application/enrollment-auth' import { completePublicCredentialGroupOAuth } from '@/lib/credential-groups/application/public-enrollment' @@ -14,6 +15,8 @@ import { CredentialGroupProviderConfigurationError, } from '@/lib/credential-groups/provider-adapter' import type { CredentialGroupProvider } from '@/lib/credential-groups/providers' +import { completeGitHubSetupReaderOAuth } from '@/lib/knowledge/application/github-setup' +import { githubSetupContinueUrl } from '@/lib/knowledge/github-setup-urls' import { createCredentialGroupCompletionRedirect, createCredentialGroupEnrollmentRedirect, @@ -54,10 +57,26 @@ export async function handleCredentialGroupOAuthCallback({ const focus: Record = attempt.returnTo ? { optionId: attempt.optionId, returnTo: attempt.returnTo } : {} + const setupRedirect = (oauth?: CredentialGroupOAuthFailure) => + new NextResponse(null, { + status: 303, + headers: { + Location: githubSetupContinueUrl( + { organizationId: attempt.organizationId!, setupId: attempt.completionId! }, + oauth + ), + 'Cache-Control': 'no-store', + 'Referrer-Policy': 'no-referrer', + }, + }) + const installationSetup = + attempt.returnTo === 'github-installation' && attempt.organizationId && attempt.completionId const failureRedirect = (oauth: CredentialGroupOAuthFailure) => - attempt.completionRedirect - ? createCredentialGroupCompletionRedirect(oauth, attempt.completionId) - : createCredentialGroupEnrollmentRedirect(attempt.invitationToken, { ...focus, oauth }) + installationSetup + ? setupRedirect(oauth) + : attempt.completionRedirect + ? createCredentialGroupCompletionRedirect(oauth, attempt.completionId) + : createCredentialGroupEnrollmentRedirect(attempt.invitationToken, { ...focus, oauth }) if (limited) { return failureRedirect('rate_limited') } @@ -69,6 +88,11 @@ export async function handleCredentialGroupOAuthCallback({ } try { + if (installationSetup) { + const principal = await internalSessionAuth.authenticate() + await completeGitHubSetupReaderOAuth.execute({ principal, input: { attempt, code }, request }) + return setupRedirect() + } const principal = await credentialGroupOAuthAttemptPrincipal(attempt) await completePublicCredentialGroupOAuth.execute({ principal, diff --git a/apps/sim/app/api/knowledge/github/setup/callback/route.ts b/apps/sim/app/api/knowledge/github/setup/callback/route.ts new file mode 100644 index 00000000000..c951aea0316 --- /dev/null +++ b/apps/sim/app/api/knowledge/github/setup/callback/route.ts @@ -0,0 +1,51 @@ +import type { SessionPrincipal } from '@sim/auth/principal' +import { NextResponse } from 'next/server' +import { completeGitHubSearchSetupContract } from '@/lib/api/contracts/knowledge/github-setup' +import { parseRequest } from '@/lib/api/server' +import { + InternalUnauthenticatedError, + internalRateLimits, + internalSessionAuth, +} from '@/lib/api/server/routes' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { completeGitHubSearchSetup } from '@/lib/knowledge/application/github-setup' +import { createCredentialGroupCompletionRedirect } from '@/app/api/credential-groups/enrollment-redirect' + +/** GitHub's Setup URL carries untrusted installation IDs; the use case verifies ownership. */ +export const GET = withRouteHandler(async (request) => { + let principal: SessionPrincipal + try { + principal = await internalSessionAuth.authenticate() + } catch (error) { + if (error instanceof InternalUnauthenticatedError) + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401, headers: { 'Cache-Control': 'no-store' } } + ) + throw error + } + const limited = await internalRateLimits + .user({ bucketName: 'github-search-setup' }) + .enforce(request, principal) + if (limited) return limited + const parsed = await parseRequest(completeGitHubSearchSetupContract, request, {}) + if (!parsed.success) return parsed.response + try { + const { state, installation_id, setup_action } = parsed.data.query + const result = await completeGitHubSearchSetup.execute({ + principal, + input: { state, installationId: installation_id, setupAction: setup_action }, + request, + }) + return new NextResponse(null, { + status: 303, + headers: { + Location: result.url, + 'Cache-Control': 'no-store', + 'Referrer-Policy': 'no-referrer', + }, + }) + } catch { + return createCredentialGroupCompletionRedirect('unavailable') + } +}) diff --git a/apps/sim/app/api/knowledge/github/setup/continue/route.ts b/apps/sim/app/api/knowledge/github/setup/continue/route.ts new file mode 100644 index 00000000000..cdd918855de --- /dev/null +++ b/apps/sim/app/api/knowledge/github/setup/continue/route.ts @@ -0,0 +1,50 @@ +import type { SessionPrincipal } from '@sim/auth/principal' +import { NextResponse } from 'next/server' +import { continueGitHubSearchSetupContract } from '@/lib/api/contracts/knowledge/github-setup' +import { parseRequest } from '@/lib/api/server' +import { + InternalUnauthenticatedError, + internalRateLimits, + internalSessionAuth, +} from '@/lib/api/server/routes' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { continueGitHubSearchSetup } from '@/lib/knowledge/application/github-setup' +import { createCredentialGroupCompletionRedirect } from '@/app/api/credential-groups/enrollment-redirect' + +/** OAuth completion resumes the existing setup attempt through current session authorization. */ +export const GET = withRouteHandler(async (request) => { + let principal: SessionPrincipal + try { + principal = await internalSessionAuth.authenticate() + } catch (error) { + if (error instanceof InternalUnauthenticatedError) + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401, headers: { 'Cache-Control': 'no-store' } } + ) + throw error + } + const limited = await internalRateLimits + .user({ bucketName: 'github-search-setup' }) + .enforce(request, principal) + if (limited) return limited + const parsed = await parseRequest(continueGitHubSearchSetupContract, request, {}) + if (!parsed.success) return parsed.response + try { + const result = await continueGitHubSearchSetup.execute({ + principal, + input: parsed.data.query, + request, + }) + return new NextResponse(null, { + status: 303, + headers: { + Location: result.url, + 'Cache-Control': 'no-store', + 'Referrer-Policy': 'no-referrer', + }, + }) + } catch { + return createCredentialGroupCompletionRedirect('unavailable', parsed.data.query.setupId) + } +}) diff --git a/apps/sim/app/api/knowledge/github/setup/redirects.test.ts b/apps/sim/app/api/knowledge/github/setup/redirects.test.ts new file mode 100644 index 00000000000..522fe7d4ef7 --- /dev/null +++ b/apps/sim/app/api/knowledge/github/setup/redirects.test.ts @@ -0,0 +1,94 @@ +/** @vitest-environment node */ +import { NextRequest, NextResponse } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const m = vi.hoisted(() => ({ + authenticate: vi.fn(), + parse: vi.fn(), + limit: vi.fn(), + complete: vi.fn(), + resume: vi.fn(), +})) +vi.mock('@/lib/api/server', () => ({ parseRequest: m.parse })) +vi.mock('@/lib/api/server/routes', () => ({ + internalSessionAuth: { authenticate: m.authenticate }, + internalRateLimits: { user: () => ({ enforce: m.limit }) }, + InternalUnauthenticatedError: class extends Error {}, +})) +vi.mock('@/lib/knowledge/application/github-setup', () => ({ + completeGitHubSearchSetup: { execute: m.complete }, + continueGitHubSearchSetup: { execute: m.resume }, +})) + +import { InternalUnauthenticatedError } from '@/lib/api/server/routes' +import { GET as callback } from '@/app/api/knowledge/github/setup/callback/route' +import { GET as resume } from '@/app/api/knowledge/github/setup/continue/route' + +const principal = { kind: 'session', userId: 'admin', sessionId: 'browser' } +const setupId = '550e8400-e29b-41d4-a716-446655440000' +const state = '660e8400-e29b-41d4-a716-446655440000' +const completeUrl = `https://sim.example/credential-groups/complete?completionId=${setupId}` + +beforeEach(() => { + vi.clearAllMocks() + m.authenticate.mockResolvedValue(principal) + m.limit.mockResolvedValue(null) + m.complete.mockResolvedValue({ url: completeUrl }) + m.resume.mockResolvedValue({ url: 'https://github.com/apps/test/installations/new?state=opaque' }) +}) + +describe.each([ + { name: 'callback', handler: callback }, + { name: 'continue', handler: resume }, +])('GitHub setup $name ingress', ({ name, handler }) => { + const request = () => new NextRequest(`https://sim.example/api/knowledge/github/setup/${name}`) + it('rejects unauthenticated callers before parsing untrusted query fields', async () => { + m.authenticate.mockRejectedValueOnce(new InternalUnauthenticatedError()) + const response = await handler(request()) + expect(response.status).toBe(401) + expect(response.headers.get('cache-control')).toBe('no-store') + expect(m.parse).not.toHaveBeenCalled() + expect(m.complete).not.toHaveBeenCalled() + expect(m.resume).not.toHaveBeenCalled() + }) + it('applies rate admission before parsing', async () => { + m.limit.mockResolvedValueOnce(NextResponse.json({ error: 'Rate limited' }, { status: 429 })) + expect((await handler(request())).status).toBe(429) + expect(m.parse).not.toHaveBeenCalled() + }) + it('does not enter protected work after a malformed query', async () => { + m.parse.mockResolvedValueOnce({ + success: false, + response: NextResponse.json({ error: 'Invalid query' }, { status: 400 }), + }) + expect((await handler(request())).status).toBe(400) + expect(m.complete).not.toHaveBeenCalled() + expect(m.resume).not.toHaveBeenCalled() + }) +}) + +it('passes untrusted callback values to the authorized use case and redirects only to its result', async () => { + m.parse.mockResolvedValueOnce({ + success: true, + data: { query: { state, installation_id: '42', setup_action: 'install' } }, + }) + const response = await callback( + new NextRequest('https://sim.example/api/knowledge/github/setup/callback') + ) + expect(m.complete).toHaveBeenCalledWith( + expect.objectContaining({ + principal, + input: { state, installationId: '42', setupAction: 'install' }, + }) + ) + expect(response.status).toBe(303) + expect(response.headers.get('location')).toBe(completeUrl) + expect(response.headers.get('referrer-policy')).toBe('no-referrer') +}) + +it('keeps continuation scope and OAuth failure classification in the application input', async () => { + const query = { organizationId: 'org', setupId, oauth: 'denied' } + m.parse.mockResolvedValueOnce({ success: true, data: { query } }) + await resume(new NextRequest('https://sim.example/api/knowledge/github/setup/continue')) + expect(m.resume).toHaveBeenCalledWith(expect.objectContaining({ principal, input: query })) +}) diff --git a/apps/sim/app/api/knowledge/github/setup/route.ts b/apps/sim/app/api/knowledge/github/setup/route.ts new file mode 100644 index 00000000000..fa1b0f64517 --- /dev/null +++ b/apps/sim/app/api/knowledge/github/setup/route.ts @@ -0,0 +1,52 @@ +import { + cancelGitHubSearchSetupContract, + readGitHubSearchSetupContract, + startGitHubSearchSetupContract, +} from '@/lib/api/contracts/knowledge/github-setup' +import { + defineInternalJsonRoute, + internalOrchestrationErrorPolicy, + internalRateLimits, + internalSessionAuth, +} from '@/lib/api/server/routes' +import { + cancelGitHubSearchSetup, + readGitHubSearchSetup, + startGitHubSearchSetup, +} from '@/lib/knowledge/application/github-setup' +import { knowledgeOperations } from '@/lib/knowledge/application/operations' + +export const POST = defineInternalJsonRoute({ + contract: startGitHubSearchSetupContract, + auth: internalSessionAuth, + operation: knowledgeOperations.startGitHubSetup, + rateLimit: internalRateLimits.user({ bucketName: 'github-search-setup' }), + errorPolicy: internalOrchestrationErrorPolicy, + mapInput: ({ body }) => body, + useCase: startGitHubSearchSetup, + present: (result) => ({ success: true, ...result }), + staticResponseHeaders: { 'Cache-Control': 'private, no-store' }, +}) + +export const GET = defineInternalJsonRoute({ + contract: readGitHubSearchSetupContract, + auth: internalSessionAuth, + operation: knowledgeOperations.readGitHubSetup, + rateLimit: internalRateLimits.user({ bucketName: 'github-search-setup-status' }), + errorPolicy: internalOrchestrationErrorPolicy, + mapInput: ({ query }) => query, + useCase: readGitHubSearchSetup, + present: (data) => ({ success: true, data }), + staticResponseHeaders: { 'Cache-Control': 'private, no-store' }, +}) + +export const DELETE = defineInternalJsonRoute({ + contract: cancelGitHubSearchSetupContract, + auth: internalSessionAuth, + operation: knowledgeOperations.cancelGitHubSetup, + rateLimit: internalRateLimits.user({ bucketName: 'github-search-setup' }), + errorPolicy: internalOrchestrationErrorPolicy, + mapInput: ({ body }) => body, + useCase: cancelGitHubSearchSetup, + staticResponseHeaders: { 'Cache-Control': 'private, no-store' }, +}) diff --git a/apps/sim/app/api/knowledge/github/setup/selection/route.ts b/apps/sim/app/api/knowledge/github/setup/selection/route.ts new file mode 100644 index 00000000000..a03b293c502 --- /dev/null +++ b/apps/sim/app/api/knowledge/github/setup/selection/route.ts @@ -0,0 +1,21 @@ +import { selectGitHubSearchSetupContract } from '@/lib/api/contracts/knowledge/github-setup' +import { + defineInternalJsonRoute, + internalOrchestrationErrorPolicy, + internalRateLimits, + internalSessionAuth, +} from '@/lib/api/server/routes' +import { selectGitHubSearchSetup } from '@/lib/knowledge/application/github-setup' +import { knowledgeOperations } from '@/lib/knowledge/application/operations' + +export const POST = defineInternalJsonRoute({ + contract: selectGitHubSearchSetupContract, + auth: internalSessionAuth, + operation: knowledgeOperations.selectGitHubSetup, + rateLimit: internalRateLimits.user({ bucketName: 'github-search-setup' }), + errorPolicy: internalOrchestrationErrorPolicy, + mapInput: ({ body }) => body, + useCase: selectGitHubSearchSetup, + present: (result) => ({ success: true, ...result }), + staticResponseHeaders: { 'Cache-Control': 'private, no-store' }, +}) diff --git a/apps/sim/app/api/knowledge/migrated-routes.test.ts b/apps/sim/app/api/knowledge/migrated-routes.test.ts index b53310dd55b..4111d75993a 100644 --- a/apps/sim/app/api/knowledge/migrated-routes.test.ts +++ b/apps/sim/app/api/knowledge/migrated-routes.test.ts @@ -316,7 +316,7 @@ describe('migrated internal Knowledge routes', () => { documents: [ expect.objectContaining({ id: 'document-1', uploadedAt: '2026-01-01T00:00:00.000Z' }), ], - counts: { active: 1, excluded: 0, failed: 0 }, + counts: { active: 1, excluded: 0, failed: 0, skipped: 0 }, hasMore: false, }, }) diff --git a/apps/sim/app/knowledge/github/setup/page.tsx b/apps/sim/app/knowledge/github/setup/page.tsx new file mode 100644 index 00000000000..4cfcc4876f7 --- /dev/null +++ b/apps/sim/app/knowledge/github/setup/page.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from 'next' +import { gitHubSearchSetupScopeSchema } from '@/lib/api/contracts/knowledge/github-setup' +import { AuthHeader, AuthShell } from '@/app/(auth)/components' +import { GitHubSetup } from '@/app/knowledge/github/setup/setup' + +export const metadata: Metadata = { + title: 'Connect GitHub', + robots: { index: false, follow: false }, +} + +export default async function GitHubSetupPage({ + searchParams, +}: { + searchParams: Promise<{ organizationId?: string | string[]; setupId?: string | string[] }> +}) { + const { organizationId, setupId } = await searchParams + const scope = gitHubSearchSetupScopeSchema.safeParse({ organizationId, setupId }) + return ( + + {scope.success ? ( + + ) : ( + + )} + + ) +} diff --git a/apps/sim/app/knowledge/github/setup/setup.test.tsx b/apps/sim/app/knowledge/github/setup/setup.test.tsx new file mode 100644 index 00000000000..30e6696c027 --- /dev/null +++ b/apps/sim/app/knowledge/github/setup/setup.test.tsx @@ -0,0 +1,118 @@ +/** + * @vitest-environment jsdom + */ +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { GitHubSearchSetupStatus } from '@/lib/api/contracts/knowledge/github-setup' + +const mocks = vi.hoisted(() => ({ + status: { status: 'pending' } as GitHubSearchSetupStatus, + select: vi.fn(), + complete: vi.fn(), + pending: false, +})) +vi.mock('@/hooks/queries/github-search-setup', () => ({ + useGitHubSearchSetup: () => ({ data: mocks.status, error: null }), + useSelectGitHubSearchSetup: () => ({ mutateAsync: mocks.select, isPending: mocks.pending }), + isGitHubSetupTerminalError: () => false, +})) +vi.mock('@/app/(auth)/components', () => ({ + AuthHeader: ({ title, description }: { title: string; description?: string }) => ( + <> +

{title}

+

{description}

+ + ), +})) +vi.mock('@/app/credential-groups/complete/completion-handoff', () => ({ + CredentialGroupCompletionHandoff: (props: { completionId: string }) => { + mocks.complete(props) + return null + }, +})) + +import { GitHubSetup } from '@/app/knowledge/github/setup/setup' + +const scope = { organizationId: 'org-1', setupId: 'c5b10b45-ffde-42a9-b6ae-39355a2abfe2' } +describe('GitHub account choice', () => { + let root: Root + let container: HTMLDivElement + beforeEach(() => { + vi.clearAllMocks() + mocks.pending = false + mocks.status = { + status: 'choosing', + installations: [ + { installationId: '1', accountId: '11', accountLogin: 'acme', accountType: 'Organization' }, + { + installationId: '2', + accountId: '22', + accountLogin: 'research', + accountType: 'Organization', + }, + ], + } + mocks.select.mockRejectedValue(new Error('GitHub is temporarily unavailable. Try again.')) + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + act(() => root.render()) + }) + afterEach(() => { + act(() => root.unmount()) + container.remove() + }) + + it('connects the selected account directly and retains choices on provider failure', async () => { + const combobox = container.querySelector('[role="combobox"]')! + await act(async () => combobox.click()) + const account = Array.from(document.querySelectorAll('[role="option"]')).find( + (option) => option.textContent?.includes('research') + )! + expect(account).toBeDefined() + await act(async () => account.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))) + expect(mocks.select).toHaveBeenCalledWith({ + ...scope, + action: { kind: 'select', installationId: '2' }, + }) + expect(container.querySelector('[role="alert"]')?.textContent).toContain( + 'temporarily unavailable' + ) + expect(container.textContent).not.toContain('Use installation') + expect(container.textContent).not.toContain('Refresh') + expect(combobox).toBeEnabled() + }) + + it('offers installing into another organization without selecting an existing account', async () => { + const button = Array.from(container.querySelectorAll('button')).find((item) => + item.textContent?.includes('Connect another organization') + )! + await act(async () => button.click()) + expect(mocks.select).toHaveBeenCalledWith({ ...scope, action: { kind: 'install' } }) + }) + + it('shows progress and disables choices while connecting', () => { + mocks.pending = true + act(() => root.render()) + expect(container.querySelector('[role="combobox"]')).toHaveAttribute('aria-disabled', 'true') + expect(container.textContent).toContain('Connecting GitHub…') + }) + + it('only closes the handoff after an authoritative completion', () => { + expect(mocks.complete).not.toHaveBeenCalled() + mocks.status = { status: 'completed', credential: { id: 'connection-1', displayName: 'Acme' } } + act(() => root.render()) + expect(mocks.complete).toHaveBeenCalledWith({ completionId: scope.setupId }) + expect(container.textContent).toContain('GitHub connected') + }) + + it('removes account choices after the attempt expires', () => { + mocks.status = { status: 'expired' } + act(() => root.render()) + expect(container.querySelector('[role="combobox"]')).toBeNull() + expect(container.textContent).toContain('expired') + expect(mocks.complete).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/app/knowledge/github/setup/setup.tsx b/apps/sim/app/knowledge/github/setup/setup.tsx new file mode 100644 index 00000000000..57d40e49160 --- /dev/null +++ b/apps/sim/app/knowledge/github/setup/setup.tsx @@ -0,0 +1,85 @@ +'use client' + +import { useState } from 'react' +import { Chip, ChipCombobox } from '@sim/emcn' +import { getErrorMessage } from '@sim/utils/errors' +import type { + GitHubSearchSetupScope, + SelectGitHubSearchSetupBody, +} from '@/lib/api/contracts/knowledge/github-setup' +import { resolveGitHubSetupUrl } from '@/lib/knowledge/github-setup-navigation' +import { AuthHeader } from '@/app/(auth)/components' +import { CredentialGroupCompletionHandoff } from '@/app/credential-groups/complete/completion-handoff' +import { + isGitHubSetupTerminalError, + useGitHubSearchSetup, + useSelectGitHubSearchSetup, +} from '@/hooks/queries/github-search-setup' + +interface GitHubSetupProps { + scope: GitHubSearchSetupScope +} + +export function GitHubSetup({ scope }: GitHubSetupProps) { + const query = useGitHubSearchSetup(scope) + const { mutateAsync: select, isPending } = useSelectGitHubSearchSetup() + const [error, setError] = useState(null) + const result = query.data + + const choose = async (action: SelectGitHubSearchSetupBody['action']) => { + setError(null) + try { + const response = await select({ ...scope, action }) + window.location.replace(resolveGitHubSetupUrl(response.url, window.location.origin)) + } catch (failure) { + setError(getErrorMessage(failure, 'Could not connect GitHub. Try again.')) + } + } + + const failure = + (isGitHubSetupTerminalError(query.error) ? query.error?.message : null) ?? + (result?.status === 'failed' + ? result.error + : result?.status === 'expired' + ? 'This connection attempt expired. Close this window and connect GitHub again from Sim.' + : null) + + if (failure) return + if (result?.status === 'completed') { + return ( + <> + + + + ) + } + if (result?.status !== 'choosing') { + return + } + + return ( + <> + +
+ ({ + value: installation.installationId, + label: installation.accountLogin, + }))} + placeholder={isPending ? 'Connecting GitHub…' : 'Select an account'} + disabled={isPending} + onChange={(installationId) => void choose({ kind: 'select', installationId })} + /> + void choose({ kind: 'install' })}> + Connect another organization + + {error && ( +

+ {error} +

+ )} +
+ + ) +} diff --git a/apps/sim/app/o/[organizationId]/integrations/integrations.test.tsx b/apps/sim/app/o/[organizationId]/integrations/integrations.test.tsx index 745b951f6ca..fa22c0f7467 100644 --- a/apps/sim/app/o/[organizationId]/integrations/integrations.test.tsx +++ b/apps/sim/app/o/[organizationId]/integrations/integrations.test.tsx @@ -5,6 +5,7 @@ import { NuqsTestingAdapter } from 'nuqs/adapters/testing' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { SearchSourceSummary } from '@/lib/api/contracts/knowledge/connectors' +import type { OrganizationAccountConnectionResponse } from '@/lib/api/contracts/organization-accounts' import type { SearchConnector } from '@/lib/sim-search/connectors' const mocks = vi.hoisted(() => ({ @@ -24,6 +25,22 @@ const mocks = vi.hoisted(() => ({ request: vi.fn(), updateUrl: vi.fn(), setupConnector: null as SearchConnector | null, + organizationAccounts: vi.fn(), + connectOrganizationAccount: vi.fn(), + reconnectOrganizationAccount: vi.fn(), + refetchAccounts: vi.fn(), +})) +vi.mock('@/hooks/queries/organization-accounts', () => ({ + organizationAccountsKeys: { detail: (id: string) => ['organization-accounts', 'detail', id] }, + useOrganizationAccounts: mocks.organizationAccounts, + useConnectOrganizationAccount: () => ({ + mutate: mocks.connectOrganizationAccount, + isPending: false, + }), + useReconnectPersonalOrganizationAccount: () => ({ + mutate: mocks.reconnectOrganizationAccount, + isPending: false, + }), })) vi.mock('@/app/o/[organizationId]/integrations/slack-search-actions', () => ({ SlackSearchActions: () => Return to Slack, @@ -159,6 +176,12 @@ beforeEach(() => { vi.spyOn(toast, 'error').mockReturnValue('toast') vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true) mocks.setupConnector = null + mocks.organizationAccounts.mockReturnValue({ + data: { credentialGroup: null, viewerAccounts: [] }, + isPending: false, + isError: false, + refetch: mocks.refetchAccounts, + }) rows = [memberSource] queryOverrides = {} mocks.context.mockReturnValue({ @@ -247,6 +270,237 @@ function menuItem(label: string) { )! } +function expectConnectionRedirect( + onSuccess: (response: OrganizationAccountConnectionResponse) => void, + authorizationUrl?: string +) { + const invitationLink = 'https://sim.test/credential-groups/enroll/fixture-token' + const assign = vi.fn() + const browserWindow = window + vi.stubGlobal('window', { location: { assign } }) + try { + onSuccess({ invitationLink, ...(authorizationUrl ? { authorizationUrl } : {}) }) + expect(assign).toHaveBeenCalledExactlyOnceWith(authorizationUrl ?? invitationLink) + } finally { + vi.stubGlobal('window', browserWindow) + } +} + +describe('GitHub member account inventory', () => { + const githubAccount = { + credentialId: 'github-account', + providerId: 'github-repositories', + groupId: 'accounts-group', + optionId: 'github-option', + displayName: 'My GitHub', + status: 'active' as const, + } + const githubGroup = { + id: 'accounts-group', + status: 'active', + options: [{ id: 'github-option', provider: 'github-repositories', status: 'active' }], + } + + beforeEach(() => { + rows = ['repo-one', 'repo-two'].map((connectorId) => ({ + ...memberSource, + connectorId, + connectorType: 'github', + sourceDescription: connectorId, + })) + mocks.overview.mockReturnValue({ + data: { providers: [{ connectorType: 'github' }] }, + isPending: false, + }) + mocks.integrations.mockReturnValue({ + data: [{ connectorType: 'github', approved: true }], + isPending: false, + }) + mocks.availability.mockReturnValue({ + integrationAvailability: new Map(), + oauthServiceAvailability: new Map([['github-repositories', true]]), + isIntegrationAvailabilityReady: true, + }) + mocks.organizationAccounts.mockReturnValue({ + data: { credentialGroup: githubGroup, viewerAccounts: [] }, + isPending: false, + isError: false, + refetch: mocks.refetchAccounts, + }) + }) + + it.each([ + undefined, + 'https://sim.test/api/credential-groups/enroll/fixture-token/oauth/github-option?returnTo=search', + ])('connects once through the account operation with compatible redirect %s', async (url) => { + await render() + expect(buttons('Connect')).toHaveLength(1) + expect(container.textContent).toContain('Connect once') + await act(async () => buttons('Connect')[0].click()) + expect(mocks.connectOrganizationAccount).toHaveBeenCalledExactlyOnceWith( + { organizationId: scope.organizationId, optionId: 'github-option' }, + expect.any(Object) + ) + expectConnectionRedirect(mocks.connectOrganizationAccount.mock.calls[0][1].onSuccess, url) + expect(mocks.sources).not.toHaveBeenCalled() + expect(mocks.connect).not.toHaveBeenCalled() + expect(mocks.connectSearchSource).not.toHaveBeenCalled() + expect(document.querySelector('[role="dialog"]')).toBeNull() + expect(container.textContent).not.toContain('repo-one') + }) + + it('keeps one account row when an admin adds another repository', async () => { + mocks.organizationAccounts.mockReturnValue({ + data: { credentialGroup: githubGroup, viewerAccounts: [githubAccount] }, + isPending: false, + }) + await render() + rows.push({ + ...rows[0], + connectorId: 'future-repository', + sourceDescription: 'future-repository', + }) + await render() + expect(document.querySelectorAll('[aria-label="GitHub integration actions"]')).toHaveLength(1) + expect(mocks.accountMenu).toHaveBeenLastCalledWith( + expect.objectContaining({ accounts: [githubAccount] }) + ) + expect(buttons('Connect')).toHaveLength(0) + expect(buttons('Reconnect')).toHaveLength(0) + expect(mocks.sources).not.toHaveBeenCalled() + expect(container.textContent).not.toContain('future-repository') + }) + + it('keeps an owned account visible before any repository source exists', async () => { + mocks.overview.mockReturnValue({ data: { providers: [] }, isPending: false }) + mocks.integrations.mockReturnValue({ data: [], isPending: false }) + mocks.organizationAccounts.mockReturnValue({ + data: { credentialGroup: githubGroup, viewerAccounts: [githubAccount] }, + isPending: false, + }) + await render('', ) + expect(container.textContent).toContain('My GitHub · Connected') + expect(document.querySelectorAll('[aria-label="GitHub integration actions"]')).toHaveLength(1) + expect(mocks.sources).not.toHaveBeenCalled() + expect(buttons('Connect')).toHaveLength(0) + expect(container.textContent).not.toContain('No integrations are available') + }) + + it.each(['group', 'option'] as const)( + 'keeps Disconnect but hides Reconnect when the canonical %s is disabled', + async (disabled) => { + const expired = { ...githubAccount, status: 'needs_reauth' } + mocks.organizationAccounts.mockReturnValue({ + data: { + credentialGroup: { + ...githubGroup, + status: disabled === 'group' ? 'disabled' : 'active', + options: [ + { ...githubGroup.options[0], status: disabled === 'option' ? 'disabled' : 'active' }, + ], + }, + viewerAccounts: [expired], + }, + isPending: false, + }) + await render() + expect(buttons('Reconnect')).toHaveLength(0) + expect(mocks.accountMenu).toHaveBeenLastCalledWith( + expect.objectContaining({ accounts: [expired] }) + ) + await openMenu('GitHub') + expect(menuItem('Disconnect github-account')).toBeDefined() + } + ) + + it.each([ + undefined, + 'https://sim.test/api/credential-groups/enroll/fixture-token/oauth/github-option?returnTo=accounts', + ])('allows personal reauthorization while Search is disabled with redirect %s', async (url) => { + mocks.overview.mockReturnValue({ data: { providers: [] }, isPending: false }) + mocks.integrations.mockReturnValue({ + data: [{ connectorType: 'github', approved: false }], + isPending: false, + }) + mocks.context.mockReturnValue({ + organization: { id: scope.organizationId }, + searchAccess: { memberScoped: false, sourceMirrored: false }, + }) + mocks.organizationAccounts.mockReturnValue({ + data: { + credentialGroup: githubGroup, + viewerAccounts: [{ ...githubAccount, status: 'needs_reauth' }], + }, + isPending: false, + }) + await render() + expect(buttons('Reconnect')).toHaveLength(1) + await act(async () => buttons('Reconnect')[0].click()) + expect(mocks.reconnectOrganizationAccount).toHaveBeenCalledExactlyOnceWith( + 'github-account', + expect.any(Object) + ) + expectConnectionRedirect(mocks.reconnectOrganizationAccount.mock.calls[0][1].onSuccess, url) + expect(mocks.connect).not.toHaveBeenCalled() + }) + + it('does not reconnect an account through a different active option', async () => { + mocks.organizationAccounts.mockReturnValue({ + data: { + credentialGroup: githubGroup, + viewerAccounts: [{ ...githubAccount, optionId: 'other-option', status: 'needs_reauth' }], + }, + isPending: false, + }) + await render() + expect(buttons('Reconnect')).toHaveLength(0) + expect(document.querySelector('[aria-label="GitHub integration actions"]')).not.toBeNull() + }) + + it.each(['pending', 'error'] as const)( + 'does not fall through to repository setup when the inventory is %s', + async (state) => { + mocks.organizationAccounts.mockReturnValue({ + data: undefined, + isPending: state === 'pending', + isError: state === 'error', + error: state === 'error' ? new Error('Could not load accounts') : null, + isFetching: false, + refetch: mocks.refetchAccounts, + }) + await render() + expect(container.textContent).toContain('GitHub') + expect(mocks.sources).not.toHaveBeenCalled() + expect(buttons('Connect')).toHaveLength(0) + expect(mocks.connectSearchSource).not.toHaveBeenCalled() + if (state === 'error') { + await act(async () => buttons('Retry')[0].click()) + expect(mocks.refetchAccounts).toHaveBeenCalledOnce() + } + } + ) + + it('retains legacy account management only after a successful response omits the inventory', async () => { + mocks.organizationAccounts.mockReturnValue({ + data: { credentialGroup: githubGroup }, + isPending: false, + isError: false, + }) + rows = rows.map((source) => ({ + ...source, + viewerMembership: 'connected', + viewerAccounts: [githubAccount], + })) + await render() + expect(mocks.sources).toHaveBeenCalledWith(scope, { connectorType: 'github', enabled: true }) + expect(document.querySelectorAll('[aria-label="GitHub integration actions"]')).toHaveLength(1) + expect(mocks.accountMenu).toHaveBeenLastCalledWith( + expect.objectContaining({ accounts: [githubAccount] }) + ) + expect(buttons('Connect')).toHaveLength(0) + }) +}) + describe('grouped member integrations', () => { it('renders one provider row and loads bounded pages per configured provider', async () => { mocks.overview.mockReturnValue({ diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.test.tsx index 2d2be5db11d..4c848f9bc01 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.test.tsx @@ -26,7 +26,7 @@ const mocks = vi.hoisted(() => ({ refetchCredentials: vi.fn(), oauthModal: vi.fn(), serviceAccountModal: vi.fn(), - githubInstallationModal: vi.fn(), + githubSetup: vi.fn(), serviceAccountTarget: null as ServiceAccountConnectTarget | null, memberAccess: true, mirroredAccess: true, @@ -182,29 +182,28 @@ vi.mock('@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-confi return null }, })) -vi.mock('@/app/workspace/[workspaceId]/search/components/github-installation-modal', () => ({ - GitHubInstallationModal: (props: { - organizationId: string +vi.mock('@/hooks/use-github-installation-setup', () => ({ + useGitHubInstallationSetup: (props: { + organizationId?: string onConnected: (id: string) => void }) => { - mocks.githubInstallationModal(props) - return ( - - ) + mocks.githubSetup(props) + return { + pending: false, + error: null, + cancel: vi.fn(), + connect: () => { + mocks.credentials = [ + { + id: 'github-app-credential', + name: 'GitHub App: acme', + type: 'service_account', + provider: 'github-app-installation', + }, + ] + props.onConnected('github-app-credential') + }, + } }, })) vi.mock('@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields', () => ({ @@ -487,11 +486,12 @@ describe('Search methods requiring member identity', () => { expect(document.body.textContent).not.toContain('Connected members') expect(button('Add repository')).toBeDisabled() await act(async () => button('Connect GitHub').click()) - expect(mocks.githubInstallationModal).toHaveBeenCalledWith( + expect(mocks.githubSetup).toHaveBeenCalledWith( expect.objectContaining({ organizationId: 'org-1' }) ) - await act(async () => button('Use GitHub installation').click()) - expect(button('GitHub App: acme')).toBeDefined() + expect(document.body.textContent).not.toContain('Use installation') + expect(document.body.textContent).not.toContain('Refresh') + expect(document.body.textContent).toContain('GitHub App: acme') expect( configFieldsProps().connectorConfig.configFields.find((field) => field.id === 'repository') ).toMatchObject({ diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx index 9a6b695f756..92da9801726 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx @@ -54,7 +54,6 @@ import { } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/consts' import { useConnectorConfigFields } from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields' import { useConnectorScope } from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-scope' -import { GitHubInstallationModal } from '@/app/workspace/[workspaceId]/search/components/github-installation-modal' import { SettingsEmptyState, SettingsQueryErrorState, @@ -74,6 +73,7 @@ import { useCreateConnector } from '@/hooks/queries/kb/connectors' import { useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials' import { useSourceAccounts } from '@/hooks/queries/source-accounts' import { useCredentialRefreshTriggers } from '@/hooks/use-credential-refresh-triggers' +import { useGitHubInstallationSetup } from '@/hooks/use-github-installation-setup' import { useOAuthReturnForKBConnectors } from '@/hooks/use-oauth-return' import { usePermissionConfig } from '@/hooks/use-permission-config' import { useConnectorSetupStore } from '@/stores/connector-setup/store' @@ -162,7 +162,6 @@ export function AddConnectorModal({ const [serviceAccountField, setServiceAccountField] = useState<'browsing' | 'content' | null>( null ) - const [showGitHubInstallationModal, setShowGitHubInstallationModal] = useState(false) const gitlabPermissions = useGitLabPermissionForm() const [apiKeyValue, setApiKeyValue] = useState('') @@ -332,6 +331,17 @@ export function AddConnectorModal({ initialSelectionLabels: draft?.selectionLabels, }) + const githubSetup = useGitHubInstallationSetup({ + organizationId: + canSetUpGitHubInstallation && scope.kind === 'organization' + ? scope.organizationId + : undefined, + onConnected: (credentialId) => { + if (credentialId !== installationCredentialId) handleFieldChange('repository', '') + setContentCredentialId(credentialId) + }, + }) + const indexingCredentialId = isApiKeyMode ? null : isMembersMode @@ -813,10 +823,16 @@ export function AddConnectorModal({ isLoading={credentialsLoading} isFetching={credentialsFetching} onRetry={() => void refetchCredentials()} - onConnect={() => setShowGitHubInstallationModal(true)} + onConnect={() => + void githubSetup.connect(installations.length ? 'install' : undefined) + } + connecting={githubSetup.pending} + connectionError={githubSetup.error} + onCancel={githubSetup.cancel} onChange={(credentialId) => { + if (credentialId !== installationCredentialId) + handleFieldChange('repository', '') setContentCredentialId(credentialId) - handleFieldChange('repository', '') }} disabled={isCreating} /> @@ -974,7 +990,7 @@ export function AddConnectorModal({ : 'Create & Invite' : 'Connect & Sync', onClick: handleSubmit, - disabled: !canSubmit || isCreating, + disabled: !canSubmit || isCreating || githubSetup.pending, }} /> ))} @@ -1000,21 +1016,6 @@ export function AddConnectorModal({ } /> )} - {showGitHubInstallationModal && - canSetUpGitHubInstallation && - isMembersMode && - scope.kind === 'organization' && ( - setShowGitHubInstallationModal(false)} - onConnected={(credentialId) => { - setContentCredentialId(credentialId) - handleFieldChange('repository', '') - setShowGitHubInstallationModal(false) - }} - /> - )} {showOAuthModal && connectorConfig && connectorConfig.auth.mode === 'oauth' && diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.test.tsx index ad93556a7e5..d5776048408 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.test.tsx @@ -28,7 +28,7 @@ const mocks = vi.hoisted(() => ({ selectorOptions: vi.fn(), accessField: vi.fn(), contentField: vi.fn(), - installationModal: vi.fn(), + githubSetup: vi.fn(), })) vi.mock('next/navigation', () => ({ useParams: () => ({}) })) @@ -55,14 +55,15 @@ vi.mock('@/hooks/queries/oauth/oauth-credentials', () => ({ vi.mock('@/hooks/use-credential-refresh-triggers', () => ({ useCredentialRefreshTriggers: vi.fn(), })) -vi.mock('@/app/workspace/[workspaceId]/search/components/github-installation-modal', () => ({ - GitHubInstallationModal: (props: { onConnected: (credentialId: string) => void }) => { - mocks.installationModal(props) - return ( - - ) +vi.mock('@/hooks/use-github-installation-setup', () => ({ + useGitHubInstallationSetup: (props: { onConnected: (credentialId: string) => void }) => { + mocks.githubSetup(props) + return { + pending: false, + error: null, + cancel: vi.fn(), + connect: () => props.onConnected('replacement-installation'), + } }, })) vi.mock( @@ -229,6 +230,7 @@ describe('connector settings service-account choices', () => { expect(mocks.contentField).not.toHaveBeenCalled() expect(mocks.accessField).not.toHaveBeenCalled() expect(container.textContent).not.toContain('Account for browsing') + expect(container.textContent).not.toContain('Keeps the current repository') expect(mocks.configFields).toHaveBeenCalledWith( expect.objectContaining({ connectorConfig: githubConnectorMeta, @@ -270,6 +272,9 @@ describe('connector settings service-account choices', () => { onResetAccess: reset, onAccessChange: changeAccess, }) + expect(container.textContent).toContain( + 'Keeps the current repository. To add another repository, add a new source.' + ) const applyButton = Array.from(container.querySelectorAll('button')).find( (button) => button.textContent === 'Change connection' )! @@ -290,13 +295,10 @@ describe('connector settings service-account choices', () => { (button) => button.textContent === 'Connect GitHub' )! await act(async () => connect.click()) - expect(mocks.installationModal).toHaveBeenCalledWith( + expect(mocks.githubSetup).toHaveBeenCalledWith( expect.objectContaining({ organizationId: 'org-1' }) ) - const finish = Array.from(container.querySelectorAll('button')).find( - (button) => button.textContent === 'Finish GitHub connection' - )! - await act(async () => finish.click()) + expect(container.textContent).not.toContain('Refresh') expect(change).toHaveBeenCalledWith('replacement-installation') expect(mocks.accessField).not.toHaveBeenCalled() expect(mocks.contentField).not.toHaveBeenCalled() diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx index cfdcd3e2e3a..a149de2e6be 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.tsx @@ -41,7 +41,6 @@ import type { ConfigFieldMap, ConfigFieldValue, } from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-connector-config-fields' -import { GitHubInstallationModal } from '@/app/workspace/[workspaceId]/search/components/github-installation-modal' import { SettingsQueryErrorState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import { isConnectorCredentialTypeAllowed } from '@/connectors/auth' import { GitHubInstallationConnectionField } from '@/connectors/github/installation-connection-field' @@ -53,6 +52,7 @@ import type { GitLabPermissionForm } from '@/connectors/gitlab/permission-config import type { ConnectorConfigField, ConnectorMeta } from '@/connectors/types' import { useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials' import { useCredentialRefreshTriggers } from '@/hooks/use-credential-refresh-triggers' +import { useGitHubInstallationSetup } from '@/hooks/use-github-installation-setup' const SWITCH_NOTICE: Record = { workspace: 'Every workspace member can read every synced document once the next sync completes.', @@ -183,7 +183,13 @@ export function ConnectorSettingsFields({ serviceIcon: connectorConfig?.icon, }) const [showServiceAccountModal, setShowServiceAccountModal] = useState(false) - const [showGitHubInstallationModal, setShowGitHubInstallationModal] = useState(false) + const githubSetup = useGitHubInstallationSetup({ + organizationId: + isGitHubInstallationSource && canAdmin && scope.kind === 'organization' + ? scope.organizationId + : undefined, + onConnected: onContentCredentialChange, + }) const [showMoreOptions, setShowMoreOptions] = useState(false) const isContentCredentialChange = accessDirty && !accessModeChanged const { @@ -281,7 +287,15 @@ export function ConnectorSettingsFields({ error={credentialsError} disabled={isSaving || !canAdmin} onRetry={() => void refetchCredentials()} - onConnect={() => setShowGitHubInstallationModal(true)} + onConnect={() => void githubSetup.connect(installations.length ? 'install' : undefined)} + connecting={githubSetup.pending} + connectionError={githubSetup.error} + hint={ + accessDirty + ? 'Keeps the current repository. To add another repository, add a new source.' + : undefined + } + onCancel={githubSetup.cancel} onChange={onContentCredentialChange} > {(accessDirty || canReenableMemberSync) && ( @@ -539,19 +553,6 @@ export function ConnectorSettingsFields({ )} {error} - {showGitHubInstallationModal && - isGitHubInstallationSource && - scope.kind === 'organization' && - canAdmin && ( - setShowGitHubInstallationModal(false)} - onConnected={(credentialId) => { - onContentCredentialChange(credentialId) - setShowGitHubInstallationModal(false) - }} - /> - )} ) } diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.test.tsx index 239c30dc809..dea50c56b85 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/use-connector-settings-form.test.tsx @@ -32,7 +32,7 @@ vi.mock('@/hooks/use-permission-config', () => ({ ['slack', { oauthAvailable: true, state: 'ready' }], ['slack_v2', { oauthAvailable: true, state: 'ready' }], ]), - oauthServiceAvailability: new Map(), + oauthServiceAvailability: new Map([['github-repositories', true]]), isIntegrationAvailabilityReady: true, isIntegrationAvailabilityFetching: false, integrationAvailabilityError: null, @@ -326,6 +326,52 @@ describe('shared connector settings form', () => { expect(onSaved).not.toHaveBeenCalled() }) + it('preserves the GitHub repository and pending connection after an incompatible replacement is refused', () => { + const sourceConfig = { repository: 'acme/platform', branch: 'main' } + render( + connector({ + connectorType: 'github', + credentialId: 'installation-1', + sourceConfig: { ...sourceConfig, githubRepositoryId: '123' }, + }), + 'github-replacement' + ) + act(() => form.fieldsProps.onContentCredentialChange('installation-1')) + expect(form.fieldsProps.accessDirty).toBe(false) + expect(form.fieldsProps.sourceConfig).toMatchObject(sourceConfig) + expect(form.fieldsProps.usesGitHubInstallation).toBe(true) + + act(() => form.fieldsProps.onFieldChange('pathPrefix', 'docs/')) + act(() => form.fieldsProps.onContentCredentialChange('installation-2')) + expect(form.fieldsProps.sourceConfig).toMatchObject({ ...sourceConfig, pathPrefix: 'docs/' }) + expect(form.fieldsProps.accessDirty).toBe(true) + expect(form.canSave).toBe(false) + act(() => form.fieldsProps.onApplyAccess()) + expect(mocks.applyAccess).toHaveBeenCalledExactlyOnceWith( + { + knowledgeBaseId: 'kb-search', + connectorId: 'connector-1', + access: { accessMode: 'members', credentialId: 'installation-2' }, + }, + expect.any(Object) + ) + const message = + "This GitHub connection cannot access this source's repository. Choose a connection with access to the same repository, or add a new source for a different repository." + act(() => mocks.applyAccess.mock.calls[0][1].onError(new Error(message))) + expect(form.fieldsProps.error).toBe(message) + expect(form.fieldsProps.contentCredentialId).toBe('installation-2') + expect(form.fieldsProps.sourceConfig).toMatchObject({ ...sourceConfig, pathPrefix: 'docs/' }) + expect(form.fieldsProps.accessDirty).toBe(true) + expect(onSaved).not.toHaveBeenCalled() + expect(mocks.update).not.toHaveBeenCalled() + + act(() => form.fieldsProps.onResetAccess()) + expect(form.fieldsProps.contentCredentialId).toBe('installation-1') + expect(form.fieldsProps.accessDirty).toBe(false) + expect(form.fieldsProps.sourceConfig).toMatchObject({ ...sourceConfig, pathPrefix: 'docs/' }) + expect(form.canSave).toBe(true) + }) + it.each(['settingsPending', 'accessPending'] as const)( 'blocks page saving while %s is pending', (pending) => { diff --git a/apps/sim/app/workspace/[workspaceId]/search/components/github-installation-modal.tsx b/apps/sim/app/workspace/[workspaceId]/search/components/github-installation-modal.tsx deleted file mode 100644 index ea5118d878e..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/search/components/github-installation-modal.tsx +++ /dev/null @@ -1,222 +0,0 @@ -'use client' - -import { useState } from 'react' -import { - Chip, - ChipCombobox, - ChipLink, - ChipModal, - ChipModalBody, - ChipModalError, - ChipModalField, - ChipModalFooter, - ChipModalHeader, -} from '@sim/emcn' -import { - SettingsEmptyState, - SettingsQueryErrorState, -} from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' -import { - useConnectGitHubSearchInstallation, - useGitHubSearchInstallations, -} from '@/hooks/queries/github-search-installations' -import { - useConnectOrganizationAccount, - useEnsureOrganizationAccounts, -} from '@/hooks/queries/organization-accounts' - -interface GitHubInstallationModalProps { - organizationId: string - onClose: () => void - onConnected: (credentialId: string) => void -} - -/** Installs a content account while preserving each reader's own GitHub authorization. */ -export function GitHubInstallationModal({ - organizationId, - onClose, - onConnected, -}: GitHubInstallationModalProps) { - const installations = useGitHubSearchInstallations(organizationId) - const connectInstallation = useConnectGitHubSearchInstallation() - const ensureAccounts = useEnsureOrganizationAccounts() - const connectAccount = useConnectOrganizationAccount() - const [installationId, setInstallationId] = useState(null) - const [waitingForAccount, setWaitingForAccount] = useState(false) - const [connectionError, setConnectionError] = useState(null) - const data = installations.isSuccess ? installations.data : undefined - const choices = data?.installations ?? [] - const selected = - choices.find((item) => item.installationId === installationId) ?? - (choices.length === 1 ? choices[0] : undefined) - const pending = - connectInstallation.isPending || ensureAccounts.isPending || connectAccount.isPending - const canConnect = - data?.available === true && - !data.needsUserConnection && - selected !== undefined && - !installations.isFetching && - !pending - - const connectGitHubAccount = () => { - const tab = window.open('about:blank', '_blank') - if (!tab) { - setConnectionError('Allow pop-ups for this site to connect your GitHub account.') - return - } - tab.opener = null - setConnectionError(null) - ensureAccounts.mutate( - { - organizationId, - option: { provider: 'github-repositories', label: 'GitHub', required: false }, - }, - { - onSuccess: ({ credentialGroup }) => { - const option = credentialGroup.options.find( - (item) => item.provider === 'github-repositories' && item.status === 'active' - ) - if (!option) { - tab.close() - setConnectionError('GitHub account setup is unavailable. Refresh and try again.') - return - } - if (tab.closed) return - connectAccount.mutate( - { organizationId, optionId: option.id }, - { - onSuccess: ({ invitationLink }) => { - if (tab.closed) return - tab.location.href = invitationLink - setWaitingForAccount(true) - }, - onError: () => tab.close(), - } - ) - }, - onError: () => tab.close(), - } - ) - } - - return ( - { - if (!open && !pending) onClose() - }} - > - Connect GitHub - - - {(aria) => - installations.isError ? ( - void installations.refetch()} - variant='inline' - /> - ) : !data ? ( - Loading GitHub setup… - ) : !data.available ? ( - - GitHub App indexing is unavailable in this deployment. - - ) : data.needsUserConnection ? ( -
-

- {waitingForAccount - ? 'Finish connecting your account in the other tab, then refresh.' - : 'Connect your GitHub account to verify the installations you can manage.'} -

-
- - {waitingForAccount ? 'Open account connection' : 'Connect your GitHub account'} - - void installations.refetch()} - > - Refresh - -
-
- ) : ( -
- {choices.length > 0 ? ( - ({ - value: item.installationId, - label: item.accountLogin, - }))} - value={selected?.installationId} - onChange={setInstallationId} - placeholder='Select an installation' - disabled={pending || installations.isFetching} - /> - ) : ( - - No eligible installations found. - - )} -
- {data.installUrl && ( - - Install GitHub App - - )} - void installations.refetch()} - > - Refresh - -
-
- ) - } -
- - {connectionError ?? - ensureAccounts.error?.message ?? - connectAccount.error?.message ?? - connectInstallation.error?.message} - -
- { - if (!canConnect || !selected) return - connectInstallation.mutate( - { organizationId, installationId: selected.installationId }, - { onSuccess: ({ credential }) => onConnected(credential.id) } - ) - }, - }} - /> -
- ) -} diff --git a/apps/sim/connectors/github/installation-connection-field.tsx b/apps/sim/connectors/github/installation-connection-field.tsx index 2a87c2cf52d..f5f23f9324f 100644 --- a/apps/sim/connectors/github/installation-connection-field.tsx +++ b/apps/sim/connectors/github/installation-connection-field.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react' -import { Chip, ChipCombobox, ChipModalField } from '@sim/emcn' +import { Chip, ChipCombobox, ChipModalError, ChipModalField } from '@sim/emcn' import { Plus } from '@sim/emcn/icons' import type { Credential } from '@/lib/oauth' import { SettingsQueryErrorState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' @@ -14,6 +14,10 @@ interface GitHubInstallationConnectionFieldProps { onRetry: () => void onConnect: () => void onChange: (credentialId: string) => void + connecting?: boolean + connectionError?: string | null + hint?: string + onCancel?: () => void children?: ReactNode } @@ -27,11 +31,22 @@ export function GitHubInstallationConnectionField({ onRetry, onConnect, onChange, + connecting = false, + connectionError, + hint, + onCancel, children, }: GitHubInstallationConnectionFieldProps) { return ( - - {error && installations.length === 0 ? ( + + {connecting ? ( +
+ Connecting GitHub… + + Cancel + +
+ ) : error && installations.length === 0 ? ( - ) : installations.length > 1 ? ( + ) : installations.length > 0 ? ( ({ value: credential.id, @@ -48,7 +64,7 @@ export function GitHubInstallationConnectionField({ })), { value: '__connect_github__', - label: 'Connect GitHub', + label: 'Connect another organization', icon: Plus, onSelect: onConnect, }, @@ -60,12 +76,10 @@ export function GitHubInstallationConnectionField({ /> ) : ( - {isLoading - ? 'Loading GitHub…' - : installations.find((credential) => credential.id === credentialId)?.name || - 'Connect GitHub'} + {isLoading ? 'Loading GitHub…' : 'Connect GitHub'} )} + {connectionError} {children}
) diff --git a/apps/sim/hooks/queries/github-search-setup.test.tsx b/apps/sim/hooks/queries/github-search-setup.test.tsx new file mode 100644 index 00000000000..e94931c00d2 --- /dev/null +++ b/apps/sim/hooks/queries/github-search-setup.test.tsx @@ -0,0 +1,162 @@ +/** @vitest-environment jsdom */ +import { act } from 'react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { ApiClientError } from '@/lib/api/client/errors' +import { + type GitHubSearchSetupScope, + type GitHubSearchSetupStatus, + readGitHubSearchSetupContract, +} from '@/lib/api/contracts/knowledge/github-setup' + +const mocks = vi.hoisted(() => ({ request: vi.fn() })) +vi.mock('@/lib/api/client/request', () => ({ requestJson: mocks.request })) + +import { githubSearchSetupKeys, useGitHubSearchSetup } from '@/hooks/queries/github-search-setup' + +const SCOPE: GitHubSearchSetupScope = { + organizationId: 'org-1', + setupId: 'cbe393bf-eb93-4b61-8e53-ce1dd602ba77', +} + +interface ProbeProps { + scope?: GitHubSearchSetupScope +} + +describe('GitHub setup status queries', () => { + let root: Root + let client: QueryClient + let result: ReturnType + + function Probe({ scope }: ProbeProps) { + result = useGitHubSearchSetup(scope) + return null + } + + async function advance(milliseconds: number) { + await act(async () => { + await vi.advanceTimersByTimeAsync(milliseconds) + }) + await act(async () => { + await vi.advanceTimersByTimeAsync(1) + }) + } + + async function render(scope?: GitHubSearchSetupScope) { + await act(async () => + root.render( + + + + ) + ) + await advance(1) + } + + beforeEach(() => { + vi.useFakeTimers() + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true) + mocks.request.mockReset() + mocks.request.mockResolvedValue({ success: true, data: { status: 'pending' } }) + client = new QueryClient({ defaultOptions: { queries: { retryDelay: 1 } } }) + root = createRoot(document.createElement('div')) + }) + + afterEach(async () => { + await act(async () => root.unmount()) + client.clear() + vi.useRealTimers() + vi.unstubAllGlobals() + }) + + it('does not request or poll without an active scope', async () => { + await render() + await advance(30_000) + expect(mocks.request).not.toHaveBeenCalled() + }) + + it('forwards the exact scope and aborts a read when its scope is replaced', async () => { + const signals: AbortSignal[] = [] + mocks.request.mockImplementation((_contract: unknown, input: { signal: AbortSignal }) => { + signals.push(input.signal) + return new Promise((_resolve, reject) => { + input.signal.addEventListener('abort', () => reject(new Error('Aborted')), { + once: true, + }) + }) + }) + await render(SCOPE) + expect(mocks.request).toHaveBeenCalledWith(readGitHubSearchSetupContract, { + query: SCOPE, + signal: expect.any(AbortSignal), + }) + expect(signals[0].aborted).toBe(false) + + const otherScope = { ...SCOPE, organizationId: 'org-2' } + await render(otherScope) + expect(signals[0].aborted).toBe(true) + expect(signals[1].aborted).toBe(false) + expect(mocks.request).toHaveBeenLastCalledWith(readGitHubSearchSetupContract, { + query: otherScope, + signal: signals[1], + }) + expect(client.getQueryData(githubSearchSetupKeys.detail(SCOPE))).toBeUndefined() + }) + + it.each([ + new TypeError('Failed to fetch'), + new ApiClientError({ status: 503, message: 'Temporarily unavailable', body: null }), + new ApiClientError({ status: 429, message: 'Try again later', body: null }), + ])('recovers a transient read failure: $message', async (failure) => { + mocks.request.mockRejectedValueOnce(failure) + await render(SCOPE) + await advance(5) + expect(result.data).toEqual({ status: 'pending' }) + expect(result.error).toBeNull() + expect(mocks.request).toHaveBeenCalledTimes(2) + + await advance(1500) + expect(mocks.request).toHaveBeenCalledTimes(3) + }) + + it('continues polling after transient errors exhaust the immediate retry budget', async () => { + const failure = new ApiClientError({ status: 503, message: 'Try again', body: null }) + mocks.request.mockRejectedValue(failure) + await render(SCOPE) + await advance(10) + expect(mocks.request).toHaveBeenCalledTimes(3) + expect(result.error).toBe(failure) + + mocks.request.mockResolvedValue({ success: true, data: { status: 'pending' } }) + await advance(5000) + expect(result.data).toEqual({ status: 'pending' }) + expect(result.error).toBeNull() + expect(mocks.request).toHaveBeenCalledTimes(4) + }) + + it('does not retry or poll after a terminal authorization denial', async () => { + const failure = new ApiClientError({ status: 403, message: 'Access denied', body: null }) + mocks.request.mockRejectedValue(failure) + await render(SCOPE) + await advance(30_000) + expect(result.error).toBe(failure) + expect(mocks.request).toHaveBeenCalledOnce() + }) + + it.each([ + { status: 'completed', credential: { id: 'installation-1', displayName: 'Acme' } }, + { status: 'failed', error: 'Choose a GitHub organization you own.' }, + { status: 'expired' }, + ])('stops polling after the server returns $status', async (terminalStatus) => { + await render(SCOPE) + expect(result.data).toEqual({ status: 'pending' }) + mocks.request.mockResolvedValue({ success: true, data: terminalStatus }) + await advance(1500) + expect(result.data).toEqual(terminalStatus) + expect(mocks.request).toHaveBeenCalledTimes(2) + + await advance(30_000) + expect(mocks.request).toHaveBeenCalledTimes(2) + }) +}) diff --git a/apps/sim/hooks/queries/github-search-setup.ts b/apps/sim/hooks/queries/github-search-setup.ts new file mode 100644 index 00000000000..2baf9dcd99a --- /dev/null +++ b/apps/sim/hooks/queries/github-search-setup.ts @@ -0,0 +1,77 @@ +'use client' + +import { useMutation, useQuery } from '@tanstack/react-query' +import { isApiClientError } from '@/lib/api/client/errors' +import { requestJson } from '@/lib/api/client/request' +import { + cancelGitHubSearchSetupContract, + type GitHubSearchSetupScope, + readGitHubSearchSetupContract, + type SelectGitHubSearchSetupBody, + type StartGitHubSearchSetupBody, + selectGitHubSearchSetupContract, + startGitHubSearchSetupContract, +} from '@/lib/api/contracts/knowledge/github-setup' + +export function isGitHubSetupTerminalError(error: unknown): boolean { + return ( + isApiClientError(error) && + error.status >= 400 && + error.status < 500 && + ![408, 429].includes(error.status) + ) +} + +export const githubSearchSetupKeys = { + all: ['github-search-setup'] as const, + details: () => [...githubSearchSetupKeys.all, 'detail'] as const, + detail: (scope?: GitHubSearchSetupScope) => + [ + ...githubSearchSetupKeys.details(), + scope?.organizationId ?? '', + scope?.setupId ?? '', + ] as const, +} + +export function useGitHubSearchSetup(scope?: GitHubSearchSetupScope) { + return useQuery({ + queryKey: githubSearchSetupKeys.detail(scope), + queryFn: async ({ signal }) => { + if (!scope) throw new Error('GitHub connection attempt is required') + return (await requestJson(readGitHubSearchSetupContract, { query: scope, signal })).data + }, + enabled: Boolean(scope), + staleTime: 0, + gcTime: 5 * 60_000, + retry: (failures, error) => !isGitHubSetupTerminalError(error) && failures < 2, + refetchInterval: (query) => + scope && + !isGitHubSetupTerminalError(query.state.error) && + (!query.state.data || ['pending', 'choosing'].includes(query.state.data.status)) + ? query.state.error + ? 5000 + : 1500 + : false, + }) +} + +export function useStartGitHubSearchSetup() { + return useMutation({ + mutationFn: (body: StartGitHubSearchSetupBody) => + requestJson(startGitHubSearchSetupContract, { body }), + }) +} + +export function useCancelGitHubSearchSetup() { + return useMutation({ + mutationFn: (body: GitHubSearchSetupScope) => + requestJson(cancelGitHubSearchSetupContract, { body }), + }) +} + +export function useSelectGitHubSearchSetup() { + return useMutation({ + mutationFn: (body: SelectGitHubSearchSetupBody) => + requestJson(selectGitHubSearchSetupContract, { body }), + }) +} diff --git a/apps/sim/hooks/use-github-installation-setup-cache.test.tsx b/apps/sim/hooks/use-github-installation-setup-cache.test.tsx new file mode 100644 index 00000000000..b57c63752cd --- /dev/null +++ b/apps/sim/hooks/use-github-installation-setup-cache.test.tsx @@ -0,0 +1,178 @@ +/** @vitest-environment jsdom */ +import { act } from 'react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + readGitHubSearchSetupContract, + startGitHubSearchSetupContract, +} from '@/lib/api/contracts/knowledge/github-setup' +import { + listOrganizationCredentialsContract, + listOrganizationOAuthCredentialsContract, +} from '@/lib/api/contracts/organization-credentials' + +const mocks = vi.hoisted(() => ({ + request: vi.fn<(contract: unknown, input: RequestInput) => Promise>(), + connected: vi.fn(), +})) +vi.mock('@/lib/api/client/request', () => ({ requestJson: mocks.request })) +vi.mock('@/hooks/queries/credentials', () => ({ useWorkspaceCredential: vi.fn() })) + +import { oauthCredentialKeys, useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials' +import { useGitHubInstallationSetup } from '@/hooks/use-github-installation-setup' + +interface RequestInput { + query?: { organizationId?: string; purpose?: string } +} + +describe('GitHub setup credential cache reconciliation', () => { + let root: Root + let client: QueryClient + let setup: ReturnType + let indexing: ReturnType + let browsing: ReturnType + let unrelated: ReturnType + let completed: boolean + let channels: Array<{ onmessage: ((event: MessageEvent) => void) | null }> + + function Probe() { + indexing = useOAuthCredentials('github-repositories', { organizationId: 'org-1' }) + browsing = useOAuthCredentials('github-repositories', { + organizationId: 'org-1', + purpose: 'browsing', + }) + unrelated = useOAuthCredentials('github-repositories', { organizationId: 'org-2' }) + setup = useGitHubInstallationSetup({ + organizationId: 'org-1', + onConnected: mocks.connected, + }) + return null + } + + async function flush() { + await act(async () => { + await vi.advanceTimersByTimeAsync(1) + }) + await act(async () => { + await vi.advanceTimersByTimeAsync(1) + }) + } + + function accountRequests(organizationId: string) { + return mocks.request.mock.calls.filter( + ([contract, input]: [unknown, RequestInput]) => + contract === listOrganizationCredentialsContract && + input.query?.organizationId === organizationId + ) + } + + beforeEach(() => { + vi.useFakeTimers() + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true) + mocks.request.mockReset() + mocks.connected.mockReset() + completed = false + channels = [] + vi.stubGlobal( + 'BroadcastChannel', + class { + onmessage = null + close = vi.fn() + constructor() { + channels.push(this) + } + } + ) + const tab = { opener: {}, location: { href: 'about:blank' }, focus: vi.fn(), close: vi.fn() } + vi.spyOn(window, 'open').mockReturnValue(tab as unknown as Window) + mocks.request.mockImplementation(async (contract: unknown, input: RequestInput) => { + if (contract === listOrganizationOAuthCredentialsContract) return { credentials: [] } + if (contract === listOrganizationCredentialsContract) { + const organizationId = input.query?.organizationId + return { + credentials: [ + { + id: `${organizationId}-existing`, + displayName: 'Existing installation', + providerId: 'github-app-installation', + }, + ...(completed && organizationId === 'org-1' + ? [ + { + id: 'installation-new', + displayName: 'New organization', + providerId: 'github-app-installation', + }, + ] + : []), + ], + } + } + if (contract === startGitHubSearchSetupContract) + return { success: true, url: 'https://github.com/apps/sim/installations/new' } + if (contract === readGitHubSearchSetupContract) + return { + success: true, + data: completed + ? { + status: 'completed', + credential: { id: 'installation-new', displayName: 'New organization' }, + } + : { status: 'pending' }, + } + throw new Error('Unexpected request') + }) + client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + root = createRoot(document.createElement('div')) + }) + + afterEach(async () => { + await act(async () => root.unmount()) + client.clear() + vi.useRealTimers() + vi.restoreAllMocks() + vi.unstubAllGlobals() + }) + + it('refreshes mounted Add and Settings lists on completion without a visibility event', async () => { + await act(async () => + root.render( + + + + ) + ) + await flush() + expect(indexing.data?.map(({ id }) => id)).toEqual(['org-1-existing']) + expect(browsing.data?.map(({ id }) => id)).toEqual(['org-1-existing']) + expect(accountRequests('org-1')).toHaveLength(2) + expect(accountRequests('org-2')).toHaveLength(1) + + await act(async () => setup.connect()) + await flush() + expect(setup.pending).toBe(true) + completed = true + await act(async () => { + channels[0].onmessage?.({ data: 'connected' } as MessageEvent) + }) + await flush() + await flush() + + expect(mocks.connected).toHaveBeenCalledExactlyOnceWith('installation-new') + expect(setup.pending).toBe(false) + expect(indexing.data?.map(({ id }) => id)).toEqual(['org-1-existing', 'installation-new']) + expect(browsing.data?.map(({ id }) => id)).toEqual(['org-1-existing', 'installation-new']) + expect(accountRequests('org-1')).toHaveLength(4) + expect(accountRequests('org-2')).toHaveLength(1) + expect(unrelated.data?.map(({ id }) => id)).toEqual(['org-2-existing']) + expect( + client.getQueryData(oauthCredentialKeys.list('github-repositories', '', '', 'org-1')) + ).toEqual(indexing.data) + expect( + client.getQueryData( + oauthCredentialKeys.list('github-repositories', '', '', 'org-1', 'browsing') + ) + ).toEqual(browsing.data) + }) +}) diff --git a/apps/sim/hooks/use-github-installation-setup.test.tsx b/apps/sim/hooks/use-github-installation-setup.test.tsx new file mode 100644 index 00000000000..688e036e2c8 --- /dev/null +++ b/apps/sim/hooks/use-github-installation-setup.test.tsx @@ -0,0 +1,224 @@ +/** + * @vitest-environment jsdom + */ +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { ApiClientError } from '@/lib/api/client/errors' +import type { GitHubSearchSetupStatus } from '@/lib/api/contracts/knowledge/github-setup' + +const mocks = vi.hoisted(() => ({ + start: vi.fn(), + cancel: vi.fn(), + refetch: vi.fn(), + invalidate: vi.fn(), + connected: vi.fn(), + status: undefined as GitHubSearchSetupStatus | undefined, + error: null as Error | null, +})) +const client = { invalidateQueries: mocks.invalidate } +vi.mock('@tanstack/react-query', () => ({ useQueryClient: () => client })) +vi.mock('@/hooks/queries/github-search-setup', () => ({ + isGitHubSetupTerminalError: (error: unknown) => + error instanceof ApiClientError && error.status === 403, + useStartGitHubSearchSetup: () => ({ mutateAsync: mocks.start, isPending: false }), + useCancelGitHubSearchSetup: () => ({ mutateAsync: mocks.cancel }), + useGitHubSearchSetup: () => ({ + data: mocks.status, + error: mocks.error, + refetch: mocks.refetch, + }), +})) + +import { useGitHubInstallationSetup } from '@/hooks/use-github-installation-setup' + +describe('GitHub installation setup handoff', () => { + let root: Root + let container: HTMLDivElement + let current: ReturnType + let channels: Array<{ + onmessage: ((event: MessageEvent) => void) | null + close: ReturnType + }> + let tab: { + opener: unknown + location: { href: string } + focus: ReturnType + close: ReturnType + } + function Probe({ organizationId = 'org-1' }: { organizationId?: string }) { + current = useGitHubInstallationSetup({ organizationId, onConnected: mocks.connected }) + return null + } + beforeEach(() => { + vi.clearAllMocks() + vi.useFakeTimers() + mocks.status = undefined + mocks.error = null + mocks.start.mockResolvedValue({ url: 'https://github.com/login/oauth/authorize' }) + mocks.cancel.mockResolvedValue({ success: true }) + channels = [] + vi.stubGlobal( + 'BroadcastChannel', + class { + onmessage = null + close = vi.fn() + constructor() { + channels.push(this) + } + } + ) + tab = { opener: {}, location: { href: 'about:blank' }, focus: vi.fn(), close: vi.fn() } + vi.spyOn(window, 'open').mockReturnValue(tab as unknown as Window) + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + act(() => root.render()) + }) + afterEach(() => { + act(() => root.unmount()) + container.remove() + vi.useRealTimers() + vi.restoreAllMocks() + vi.unstubAllGlobals() + }) + + it('opens approval synchronously and completes only from the authorized server receipt', async () => { + await act(async () => current.connect()) + expect(tab.opener).toBeNull() + expect(tab.location.href).toBe('https://github.com/login/oauth/authorize') + expect(mocks.start).toHaveBeenCalledWith({ + organizationId: 'org-1', + setupId: expect.any(String), + }) + expect(current.pending).toBe(true) + act(() => channels[0].onmessage?.({ data: 'connected' } as MessageEvent)) + expect(mocks.refetch).toHaveBeenCalledOnce() + expect(mocks.connected).not.toHaveBeenCalled() + mocks.status = { + status: 'completed', + credential: { id: 'installation-1', displayName: 'Acme' }, + } + act(() => root.render()) + expect(current.pending).toBe(false) + expect(mocks.connected).toHaveBeenCalledExactlyOnceWith('installation-1') + expect(tab.close).toHaveBeenCalledOnce() + expect(mocks.invalidate).toHaveBeenCalledTimes(4) + expect(mocks.invalidate).toHaveBeenCalledWith({ + queryKey: ['oauthCredentials', 'list', 'github-repositories', '', '', 'org-1', 'browsing'], + }) + act(() => root.render()) + expect(mocks.connected).toHaveBeenCalledOnce() + }) + + it('keeps one approval attempt and focuses it when Connect is clicked again', async () => { + await act(async () => current.connect()) + await act(async () => current.connect()) + expect(window.open).toHaveBeenCalledOnce() + expect(mocks.start).toHaveBeenCalledOnce() + expect(tab.focus).toHaveBeenCalledOnce() + }) + + it('cancels without navigating a late start response or accepting its completion', async () => { + let resolve!: (value: { url: string }) => void + mocks.start.mockReturnValue( + new Promise((done) => { + resolve = done + }) + ) + let connecting!: Promise + act(() => { + connecting = current.connect() + }) + act(() => current.cancel()) + await act(async () => { + resolve({ url: 'https://github.com/apps/sim/installations/new' }) + await connecting + }) + expect(tab.location.href).toBe('about:blank') + expect(mocks.cancel).toHaveBeenCalledWith(mocks.start.mock.calls[0][0]) + mocks.status = { status: 'completed', credential: { id: 'cancelled', displayName: 'Acme' } } + act(() => root.render()) + expect(mocks.connected).not.toHaveBeenCalled() + expect(current.pending).toBe(false) + }) + + it('shows denied approval inline and allows a new attempt', async () => { + await act(async () => current.connect()) + act(() => channels[0].onmessage?.({ data: 'denied' } as MessageEvent)) + expect(current.pending).toBe(true) + expect(mocks.refetch).toHaveBeenCalledOnce() + mocks.status = { status: 'failed', error: 'Choose a GitHub organization you own.' } + act(() => root.render()) + expect(current.error).toBe('Choose a GitHub organization you own.') + expect(current.pending).toBe(false) + mocks.status = undefined + await act(async () => current.connect()) + expect(mocks.start).toHaveBeenCalledTimes(2) + expect(current.error).toBeNull() + }) + + it('cancels on organization change and ignores the previous receipt', async () => { + await act(async () => current.connect()) + mocks.status = { status: 'completed', credential: { id: 'wrong-org', displayName: 'Acme' } } + act(() => root.render()) + expect(mocks.cancel).toHaveBeenCalledWith(mocks.start.mock.calls[0][0]) + expect(mocks.connected).not.toHaveBeenCalled() + expect(current.pending).toBe(false) + }) + + it('keeps the approval open through transient polling failures but stops on lost access', async () => { + await act(async () => current.connect()) + mocks.error = new Error('Failed to fetch') + act(() => root.render()) + expect(current.pending).toBe(true) + expect(mocks.cancel).not.toHaveBeenCalled() + mocks.error = new ApiClientError({ + status: 403, + message: 'Organization access denied', + body: null, + }) + act(() => root.render()) + expect(current.pending).toBe(false) + expect(current.error).toBe('Organization access denied') + expect(mocks.cancel).toHaveBeenCalledOnce() + }) + + it('forwards an explicit new-organization request instead of reusing an existing installation', async () => { + await act(async () => current.connect('install')) + expect(mocks.start).toHaveBeenCalledWith({ + organizationId: 'org-1', + setupId: expect.any(String), + intent: 'install', + }) + }) + + it('bounds the attempt lifetime and cleans up its channel', async () => { + await act(async () => current.connect()) + act(() => vi.advanceTimersByTime(10 * 60_000)) + expect(current.pending).toBe(false) + expect(current.error).toContain('timed out') + expect(channels[0].close).toHaveBeenCalledOnce() + expect(mocks.cancel).toHaveBeenCalledOnce() + }) + + it('reports popup blocking before creating any server attempt', async () => { + vi.mocked(window.open).mockReturnValue(null) + await act(async () => current.connect()) + expect(mocks.start).not.toHaveBeenCalled() + expect(current.error).toContain('Allow pop-ups') + }) + + it.each([ + 'https://attacker.example/authorize', + 'javascript:alert(1)', + 'https://github.com@attacker.example', + ])('rejects an unexpected approval destination: %s', async (url) => { + mocks.start.mockResolvedValue({ url }) + await act(async () => current.connect()) + expect(tab.location.href).toBe('about:blank') + expect(current.error).toBe('GitHub returned an invalid setup URL') + expect(mocks.cancel).toHaveBeenCalledOnce() + }) +}) diff --git a/apps/sim/hooks/use-github-installation-setup.ts b/apps/sim/hooks/use-github-installation-setup.ts new file mode 100644 index 00000000000..762a6dd222d --- /dev/null +++ b/apps/sim/hooks/use-github-installation-setup.ts @@ -0,0 +1,172 @@ +'use client' + +import { useCallback, useEffect, useRef, useState } from 'react' +import { getErrorMessage } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' +import { useQueryClient } from '@tanstack/react-query' +import type { StartGitHubSearchSetupBody } from '@/lib/api/contracts/knowledge/github-setup' +import { + credentialGroupOAuthCompletionChannel, + isCredentialGroupOAuthFailure, +} from '@/lib/credential-groups/oauth-completion' +import { resolveGitHubSetupUrl } from '@/lib/knowledge/github-setup-navigation' +import { githubSearchInstallationKeys } from '@/hooks/queries/github-search-installations' +import { + isGitHubSetupTerminalError, + useCancelGitHubSearchSetup, + useGitHubSearchSetup, + useStartGitHubSearchSetup, +} from '@/hooks/queries/github-search-setup' +import { oauthCredentialKeys } from '@/hooks/queries/oauth/oauth-credentials' +import { organizationAccountsKeys } from '@/hooks/queries/organization-accounts' + +interface GitHubInstallationSetupProps { + organizationId?: string + onConnected: (credentialId: string) => void +} + +/** Keeps the source form in place; only an authorized server result completes setup. */ +export function useGitHubInstallationSetup({ + organizationId, + onConnected, +}: GitHubInstallationSetupProps) { + const active = useRef<{ setupId: string; tab: Window } | null>(null) + const callback = useRef(onConnected) + const [setupId, setSetupId] = useState() + const [error, setError] = useState(null) + const [previousOrganizationId, setPreviousOrganizationId] = useState(organizationId) + if (previousOrganizationId !== organizationId) { + setPreviousOrganizationId(organizationId) + setSetupId(undefined) + setError(null) + } + const client = useQueryClient() + const { mutateAsync: start, isPending: isStarting } = useStartGitHubSearchSetup() + const { mutateAsync: cancelSetup } = useCancelGitHubSearchSetup() + const scope = organizationId && setupId ? { organizationId, setupId } : undefined + const query = useGitHubSearchSetup(scope) + const { refetch } = query + + useEffect(() => { + callback.current = onConnected + }, [onConnected]) + + useEffect(() => { + return () => { + const attempt = active.current + active.current = null + attempt?.tab.close() + if (attempt && organizationId) + void cancelSetup({ organizationId, setupId: attempt.setupId }).catch(() => undefined) + } + }, [organizationId, cancelSetup]) + + useEffect(() => { + if (!setupId || !organizationId) return + const fail = (message: string) => { + if (active.current?.setupId !== setupId) return + active.current.tab.close() + active.current = null + setSetupId(undefined) + setError(message) + void cancelSetup({ organizationId, setupId }).catch(() => undefined) + } + const channel = new BroadcastChannel(credentialGroupOAuthCompletionChannel(setupId)) + channel.onmessage = ({ data }: MessageEvent) => { + if (isCredentialGroupOAuthFailure(data) || data === 'connected') void refetch() + } + const timer = window.setTimeout( + () => fail('GitHub connection timed out. Try connecting again.'), + 10 * 60_000 + ) + return () => { + channel.close() + window.clearTimeout(timer) + } + }, [organizationId, setupId, cancelSetup, refetch]) + + useEffect(() => { + if (!setupId || !organizationId || active.current?.setupId !== setupId) return + const result = query.data + if (result?.status === 'completed') { + active.current.tab.close() + active.current = null + setSetupId(undefined) + for (const purpose of [undefined, 'browsing'] as const) { + void client.invalidateQueries({ + queryKey: oauthCredentialKeys.list( + 'github-repositories', + '', + '', + organizationId, + purpose + ), + }) + } + void client.invalidateQueries({ queryKey: githubSearchInstallationKeys.list(organizationId) }) + void client.invalidateQueries({ queryKey: organizationAccountsKeys.detail(organizationId) }) + callback.current(result.credential.id) + } else if ( + isGitHubSetupTerminalError(query.error) || + result?.status === 'failed' || + result?.status === 'expired' + ) { + active.current.tab.close() + active.current = null + setSetupId(undefined) + setError( + query.error?.message ?? + (result?.status === 'failed' + ? result.error + : 'This GitHub connection attempt expired. Try connecting again.') + ) + void cancelSetup({ organizationId, setupId }).catch(() => undefined) + } + }, [query.data, query.error, setupId, organizationId, client, cancelSetup]) + + const connect = useCallback( + async (intent?: StartGitHubSearchSetupBody['intent']) => { + if (!organizationId) return + if (active.current) { + active.current.tab.focus() + return + } + const tab = window.open('about:blank', '_blank', 'width=600,height=700') + if (!tab) { + setError('Allow pop-ups for this site to connect GitHub, then try again.') + return + } + tab.opener = null + const id = generateId() + active.current = { setupId: id, tab } + setError(null) + try { + const result = await start({ organizationId, setupId: id, ...(intent ? { intent } : {}) }) + if (active.current?.setupId !== id) return + const url = resolveGitHubSetupUrl(result.url, window.location.origin) + setSetupId(id) + tab.location.href = url + } catch (failure) { + if (active.current?.setupId !== id) return + tab.close() + active.current = null + setSetupId(undefined) + setError(getErrorMessage(failure, 'Could not connect GitHub')) + void cancelSetup({ organizationId, setupId: id }).catch(() => undefined) + } + }, + [organizationId, start, cancelSetup] + ) + + const cancel = useCallback(() => { + const attempt = active.current + if (!attempt || !organizationId) return + active.current = null + attempt.tab.close() + setSetupId(undefined) + setError(null) + void cancelSetup({ organizationId, setupId: attempt.setupId }).catch(() => undefined) + }, [organizationId, cancelSetup]) + + return { connect, cancel, pending: isStarting || Boolean(setupId), error } +} diff --git a/apps/sim/lib/api/contracts/knowledge/github-setup.ts b/apps/sim/lib/api/contracts/knowledge/github-setup.ts new file mode 100644 index 00000000000..dfd0d3c16e0 --- /dev/null +++ b/apps/sim/lib/api/contracts/knowledge/github-setup.ts @@ -0,0 +1,108 @@ +import { z } from 'zod' +import { + githubInstallationIdSchema, + githubSearchInstallationSchema, +} from '@/lib/api/contracts/knowledge/github-installations' +import { organizationIdSchema } from '@/lib/api/contracts/primitives' +import { defineRouteContract } from '@/lib/api/contracts/types' + +export const gitHubSearchSetupScopeSchema = z + .object({ + organizationId: organizationIdSchema, + setupId: z.string().uuid('GitHub setup ID must be a UUID'), + }) + .strict() +export type GitHubSearchSetupScope = z.input + +export const gitHubSearchSetupStatusSchema = z.discriminatedUnion('status', [ + z.object({ status: z.literal('pending') }), + z.object({ + status: z.literal('choosing'), + installations: z.array(githubSearchInstallationSchema).min(1).max(1000), + }), + z.object({ + status: z.literal('completed'), + credential: z.object({ + id: z.string().min(1).max(200), + displayName: z.string().min(1).max(500), + }), + }), + z.object({ status: z.literal('failed'), error: z.string().min(1).max(1000) }), + z.object({ status: z.literal('expired') }), +]) +export type GitHubSearchSetupStatus = z.output + +export const startGitHubSearchSetupResponseSchema = z.object({ + success: z.literal(true), + url: z.string().url().max(8192), +}) +export type StartGitHubSearchSetupResponse = z.output +export const startGitHubSearchSetupBodySchema = gitHubSearchSetupScopeSchema.extend({ + intent: z.literal('install').optional(), +}) +export type StartGitHubSearchSetupBody = z.input +export const startGitHubSearchSetupContract = defineRouteContract({ + method: 'POST', + path: '/api/knowledge/github/setup', + body: startGitHubSearchSetupBodySchema, + response: { mode: 'json', schema: startGitHubSearchSetupResponseSchema }, +}) + +export const readGitHubSearchSetupResponseSchema = z.object({ + success: z.literal(true), + data: gitHubSearchSetupStatusSchema, +}) +export type ReadGitHubSearchSetupResponse = z.output +export const readGitHubSearchSetupContract = defineRouteContract({ + method: 'GET', + path: '/api/knowledge/github/setup', + query: gitHubSearchSetupScopeSchema, + response: { mode: 'json', schema: readGitHubSearchSetupResponseSchema }, +}) + +export const cancelGitHubSearchSetupResponseSchema = z.object({ success: z.literal(true) }) +export type CancelGitHubSearchSetupResponse = z.output +export const cancelGitHubSearchSetupContract = defineRouteContract({ + method: 'DELETE', + path: '/api/knowledge/github/setup', + body: gitHubSearchSetupScopeSchema, + response: { mode: 'json', schema: cancelGitHubSearchSetupResponseSchema }, +}) + +export const selectGitHubSearchSetupBodySchema = gitHubSearchSetupScopeSchema.extend({ + action: z.discriminatedUnion('kind', [ + z.object({ kind: z.literal('select'), installationId: githubInstallationIdSchema }).strict(), + z.object({ kind: z.literal('install') }).strict(), + ]), +}) +export type SelectGitHubSearchSetupBody = z.input +export const selectGitHubSearchSetupContract = defineRouteContract({ + method: 'POST', + path: '/api/knowledge/github/setup/selection', + body: selectGitHubSearchSetupBodySchema, + response: { mode: 'json', schema: startGitHubSearchSetupResponseSchema }, +}) + +export const continueGitHubSearchSetupQuerySchema = gitHubSearchSetupScopeSchema.extend({ + oauth: z.string().min(1).max(100).optional(), +}) +export type ContinueGitHubSearchSetupQuery = z.input +export const continueGitHubSearchSetupContract = defineRouteContract({ + method: 'GET', + path: '/api/knowledge/github/setup/continue', + query: continueGitHubSearchSetupQuerySchema, + response: { mode: 'redirect' }, +}) + +export const completeGitHubSearchSetupQuerySchema = z.object({ + state: z.string().uuid('GitHub setup state must be a UUID'), + installation_id: githubInstallationIdSchema.optional(), + setup_action: z.string().min(1).max(40).optional(), +}) +export type CompleteGitHubSearchSetupQuery = z.input +export const completeGitHubSearchSetupContract = defineRouteContract({ + method: 'GET', + path: '/api/knowledge/github/setup/callback', + query: completeGitHubSearchSetupQuerySchema, + response: { mode: 'redirect' }, +}) diff --git a/apps/sim/lib/credential-groups/oauth-state.ts b/apps/sim/lib/credential-groups/oauth-state.ts index edd588c1f37..97abfbb9663 100644 --- a/apps/sim/lib/credential-groups/oauth-state.ts +++ b/apps/sim/lib/credential-groups/oauth-state.ts @@ -44,7 +44,7 @@ interface StoredCredentialGroupOAuthAttempt { completionRedirect?: boolean connectionIntent?: CredentialGroupConnectionIntent completionId?: string - returnTo?: 'search' | 'accounts' + returnTo?: 'search' | 'accounts' | 'github-installation' nonceHash: string encryptedCodeVerifier?: string encryptedInvitationToken: string @@ -69,7 +69,7 @@ export interface CredentialGroupOAuthAttempt { completionRedirect?: boolean connectionIntent?: CredentialGroupConnectionIntent completionId?: string - returnTo?: 'search' | 'accounts' + returnTo?: 'search' | 'accounts' | 'github-installation' codeVerifier?: string invitationToken: string createdAt: number @@ -91,7 +91,7 @@ interface CreateCredentialGroupOAuthAttemptParams { completionRedirect?: boolean connectionIntent?: CredentialGroupConnectionIntent completionId?: string - returnTo?: 'search' | 'accounts' + returnTo?: 'search' | 'accounts' | 'github-installation' codeVerifier?: string invitationToken: string } @@ -147,7 +147,11 @@ function isStoredAttempt(value: unknown): value is StoredCredentialGroupOAuthAtt credentialGroupConnectionIntentSchema.safeParse(candidate.connectionIntent).success) && (candidate.returnTo === undefined || candidate.returnTo === 'search' || - candidate.returnTo === 'accounts') && + candidate.returnTo === 'accounts' || + (candidate.returnTo === 'github-installation' && + candidate.provider === 'github-repositories' && + typeof candidate.organizationId === 'string' && + typeof candidate.completionId === 'string')) && typeof candidate.nonceHash === 'string' && (candidate.encryptedCodeVerifier === undefined || typeof candidate.encryptedCodeVerifier === 'string') && @@ -166,6 +170,12 @@ export async function createCredentialGroupOAuthAttempt( ) { throw new Error('OAuth completion requires a valid correlation ID and completion redirect') } + if ( + params.returnTo === 'github-installation' && + (params.provider !== 'github-repositories' || !params.organizationId || !params.completionId) + ) { + throw new Error('GitHub installation OAuth requires an organization-bound setup attempt') + } if (params.connectionIntent) credentialGroupConnectionIntentSchema.parse(params.connectionIntent) const redis = requireRedis() const state = `${OAUTH_ATTEMPT_STATE_PREFIX}${generateId()}` diff --git a/apps/sim/lib/credential-groups/oauth.ts b/apps/sim/lib/credential-groups/oauth.ts index b905f373604..6699f6a7456 100644 --- a/apps/sim/lib/credential-groups/oauth.ts +++ b/apps/sim/lib/credential-groups/oauth.ts @@ -116,7 +116,7 @@ export async function startCredentialGroupOAuth( completionRedirect?: boolean connectionIntent?: CredentialGroupConnectionIntent completionId?: string - returnTo?: 'search' | 'accounts' + returnTo?: 'search' | 'accounts' | 'github-installation' } = {} ): Promise { if (!context.credentialOwnerId) throw new CredentialGroupInvitationUnavailableError() diff --git a/apps/sim/lib/knowledge/application/connector-access.test.ts b/apps/sim/lib/knowledge/application/connector-access.test.ts index 0e0358850c4..d80c90158a6 100644 --- a/apps/sim/lib/knowledge/application/connector-access.test.ts +++ b/apps/sim/lib/knowledge/application/connector-access.test.ts @@ -21,6 +21,11 @@ const mocks = vi.hoisted(() => ({ sourceAccess: vi.fn(), oauthContext: vi.fn(), startOAuth: vi.fn(), + authorizeOrganization: vi.fn(), + credential: vi.fn(), + decrypt: vi.fn(), + installationBinding: vi.fn(), + repository: vi.fn(), })) vi.mock('@sim/audit', () => ({ AuditAction: {}, AuditResourceType: {}, recordAudit: vi.fn() })) @@ -32,6 +37,26 @@ vi.mock('@sim/platform-authz/workspace', () => ({ vi.mock('@/lib/permission-groups/resolve.server', () => ({ getUserPermissionConfig: async () => null, })) +vi.mock('@/lib/core/application/organization-authorization', () => ({ + authorizeOrganizationOperation: mocks.authorizeOrganization, +})) +vi.mock('@/lib/knowledge/application/connector-credential', () => ({ + requireConnectorCredential: mocks.credential, +})) +vi.mock('@/lib/core/security/encryption', () => ({ decryptSecret: mocks.decrypt })) +vi.mock('@/lib/oauth/github-installation', () => ({ + GitHubInstallationError: class extends Error { + constructor( + message: string, + readonly status?: number, + readonly operation?: string + ) { + super(message) + } + }, + parseGitHubInstallationBinding: mocks.installationBinding, + resolveGitHubInstallationRepository: mocks.repository, +})) vi.mock('@/lib/knowledge/application/contexts', () => ({ resolveActiveKnowledgeConnectorContext: mocks.context, })) @@ -80,10 +105,12 @@ vi.mock('@/lib/knowledge/connectors/member-provisioning', () => ({ provisionKnowledgeConnectorMembersBinding: mocks.provision, })) +import { OrchestrationError } from '@/lib/core/orchestration/types' import { startKnowledgeConnectorMemberEnrollment, updateKnowledgeConnectorAccess, } from '@/lib/knowledge/application/connector-access' +import { GitHubInstallationError } from '@/lib/oauth/github-installation' const principal = { kind: 'session' as const, userId: 'admin', sessionId: 'session' } const input = { knowledgeBaseId: 'kb', connectorId: 'source', assertedWorkspaceId: 'workspace' } @@ -131,6 +158,155 @@ beforeEach(() => { mocks.sourceAccess.mockResolvedValue(undefined) mocks.oauthContext.mockResolvedValue({ credentialOwnerId: 'admin', option: { id: 'option' } }) mocks.startOAuth.mockResolvedValue('https://provider.example.test/authorize') + mocks.authorizeOrganization.mockResolvedValue({ + organizationId: 'org', + userId: 'admin', + role: 'admin', + }) +}) + +describe('GitHub installation connection replacement', () => { + const sourceConfig = { repository: 'acme/platform', githubRepositoryId: '123', branch: 'main' } + const replacementInput = { + knowledgeBaseId: 'kb', + connectorId: 'source', + accessMode: 'members' as const, + credentialId: 'replacement-installation', + } + + beforeEach(() => { + mocks.context.mockResolvedValue({ + organizationId: 'org', + knowledgeBaseId: 'kb', + connectorId: 'source', + knowledgeBase: { organizationId: 'org', id: 'kb', name: 'Search', isSearchIndex: true }, + }) + mocks.connector.mockResolvedValue({ + ...row, + connectorType: 'github', + accessMode: 'members', + credentialId: 'previous-installation', + sourceConfig, + }) + mocks.meta.mockReturnValue({ + name: 'GitHub', + search: true, + auth: { mode: 'oauth', provider: 'github-repositories' }, + supportsSeparateContentCredential: true, + }) + mocks.credential.mockResolvedValue({ + id: replacementInput.credentialId, + providerId: 'github-app-installation', + organizationId: 'org', + workspaceId: null, + type: 'service_account', + revokedAt: null, + encryptedServiceAccountKey: 'encrypted-binding', + providerSubjectId: '42', + providerTenantId: '7', + }) + mocks.decrypt.mockResolvedValue({ decrypted: '{}' }) + mocks.installationBinding.mockReturnValue({ installationId: '42', accountId: '7' }) + mocks.repository.mockResolvedValue({ id: '123', fullName: 'acme/platform' }) + mocks.binding.mockImplementation(async ({ sourceConfig }) => ({ + credentialGroupId: 'group', + credentialGroupOptionId: 'github-members', + sourceConfig, + })) + }) + + it.each([ + { status: 404, operation: 'repository' as const }, + { status: 422, operation: 'repository-token' as const }, + ])( + 'rejects an incompatible installation with actionable validation ($status)', + async ({ status, operation }) => { + mocks.repository.mockRejectedValue( + new GitHubInstallationError('Provider detail', status, operation) + ) + await expect( + updateKnowledgeConnectorAccess.execute({ principal, input: replacementInput }) + ).rejects.toMatchObject({ + code: 'validation', + message: + "This GitHub connection cannot access this source's repository. Choose a connection with access to the same repository, or add a new source for a different repository.", + }) + expect(mocks.repository).toHaveBeenCalledExactlyOnceWith( + { installationId: '42', accountId: '7' }, + 'acme/platform' + ) + expect(mocks.update).not.toHaveBeenCalled() + expect(mocks.binding).not.toHaveBeenCalled() + expect(mocks.token).not.toHaveBeenCalled() + } + ) + + it('rejects a recreated repository at the same path before changing the binding', async () => { + mocks.repository.mockResolvedValue({ id: '999', fullName: 'acme/platform' }) + await expect( + updateKnowledgeConnectorAccess.execute({ principal, input: replacementInput }) + ).rejects.toMatchObject({ + code: 'validation', + message: 'Create a new source to index a different GitHub repository', + }) + expect(mocks.update).not.toHaveBeenCalled() + }) + + it('passes a verified replacement for the same repository to the atomic access update', async () => { + await updateKnowledgeConnectorAccess.execute({ principal, input: replacementInput }) + expect(mocks.authorizeOrganization).toHaveBeenCalledWith( + principal, + expect.objectContaining({ minimumRole: 'admin' }), + expect.objectContaining({ organizationId: 'org' }) + ) + expect(mocks.credential).toHaveBeenCalledWith( + expect.objectContaining({ + principal, + credentialId: replacementInput.credentialId, + scope: { kind: 'organization', organizationId: 'org' }, + }) + ) + expect(mocks.update).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ + userId: 'admin', + connectorId: 'source', + knowledgeBase: { id: 'kb', name: 'Search', organizationId: 'org' }, + target: { + accessMode: 'members', + credentialId: replacementInput.credentialId, + binding: { + credentialGroupId: 'group', + credentialGroupOptionId: 'github-members', + sourceConfig, + }, + }, + }) + ) + }) + + it('rechecks organization administration before reading the installation or mutating the source', async () => { + mocks.authorizeOrganization.mockRejectedValue( + new OrchestrationError('forbidden', 'Organization administrator access is required') + ) + await expect( + updateKnowledgeConnectorAccess.execute({ principal, input: replacementInput }) + ).rejects.toMatchObject({ code: 'forbidden' }) + expect(mocks.credential).not.toHaveBeenCalled() + expect(mocks.repository).not.toHaveBeenCalled() + expect(mocks.update).not.toHaveBeenCalled() + }) + + it.each([403, 429, 503])( + 'preserves a real provider failure (%s) without changing the binding', + async (status) => { + const error = new GitHubInstallationError('Provider unavailable', status, 'repository') + mocks.repository.mockRejectedValue(error) + await expect( + updateKnowledgeConnectorAccess.execute({ principal, input: replacementInput }) + ).rejects.toBe(error) + expect(mocks.update).not.toHaveBeenCalled() + } + ) }) describe('source member enrollment', () => { diff --git a/apps/sim/lib/knowledge/application/github-installation-error.ts b/apps/sim/lib/knowledge/application/github-installation-error.ts index 0ab93e8c938..bad83142274 100644 --- a/apps/sim/lib/knowledge/application/github-installation-error.ts +++ b/apps/sim/lib/knowledge/application/github-installation-error.ts @@ -2,7 +2,10 @@ import { OrchestrationError } from '@/lib/core/orchestration/types' import { GitHubInstallationError } from '@/lib/oauth/github-installation' /** Maps repository selection refusals while preserving provider failures for their caller. */ -export function rethrowGitHubInstallationSourceError(error: unknown): never { +export function rethrowGitHubInstallationSourceError( + error: unknown, + { existingSource = false }: { existingSource?: boolean } = {} +): never { if ( error instanceof GitHubInstallationError && ((error.operation === 'repository-token' && error.status === 422) || @@ -10,7 +13,9 @@ export function rethrowGitHubInstallationSourceError(error: unknown): never { ) { throw new OrchestrationError( 'validation', - 'Check that the repository is included in the selected GitHub App installation, then retry.' + existingSource + ? "This GitHub connection cannot access this source's repository. Choose a connection with access to the same repository, or add a new source for a different repository." + : 'Check that the repository is included in the selected GitHub App installation, then retry.' ) } throw error diff --git a/apps/sim/lib/knowledge/application/github-installation-source.ts b/apps/sim/lib/knowledge/application/github-installation-source.ts index 9ce77014ada..788216aac32 100644 --- a/apps/sim/lib/knowledge/application/github-installation-source.ts +++ b/apps/sim/lib/knowledge/application/github-installation-source.ts @@ -82,7 +82,7 @@ export async function prepareGitHubInstallationSource( ) throw new OrchestrationError('validation', 'Reconnect this GitHub installation before using it') const resolved = await resolveGitHubInstallationRepository(binding, repository.trim()).catch( - rethrowGitHubInstallationSourceError + (error) => rethrowGitHubInstallationSourceError(error, { existingSource: wasInstallation }) ) if (wasInstallation && input.previousConfig?.githubRepositoryId !== resolved.id) throw new OrchestrationError( diff --git a/apps/sim/lib/knowledge/application/github-installations.test.ts b/apps/sim/lib/knowledge/application/github-installations.test.ts index 9027329a298..8e6355ffc27 100644 --- a/apps/sim/lib/knowledge/application/github-installations.test.ts +++ b/apps/sim/lib/knowledge/application/github-installations.test.ts @@ -33,11 +33,30 @@ vi.mock('@/lib/permission-groups/resolve.server', () => ({ getUserPermissionConfigForOrganization: async () => null, })) vi.mock('@/lib/oauth/github-installation', () => ({ + GitHubInstallationError: class extends Error { + constructor( + message: string, + readonly status?: number + ) { + super(message) + } + }, getGitHubInstallationConfiguration: m.configuration, listUserAdminGitHubInstallations: m.list, verifyGitHubInstallationBinding: m.verify, })) -vi.mock('@/lib/credentials/managed-oauth', () => ({ resolveManagedOAuthToken: m.token })) +vi.mock('@/lib/credentials/managed-oauth', () => ({ + resolveManagedOAuthToken: m.token, + ManagedOAuthCredentialError: class extends Error { + constructor( + readonly code: string, + message: string, + readonly statusCode: number + ) { + super(message) + } + }, +})) vi.mock('@/lib/credential-groups/provider-registry', () => ({ getCredentialGroupProviderAdapter: () => ({ getPolicy: async () => ({ authorizationAppId: 'current-app', scopeVersion: 1 }), @@ -45,10 +64,12 @@ vi.mock('@/lib/credential-groups/provider-registry', () => ({ })) vi.mock('@/lib/core/security/encryption', () => ({ encryptSecret: m.encrypt })) +import { ManagedOAuthCredentialError } from '@/lib/credentials/managed-oauth' import { connectGitHubSearchInstallation, listGitHubSearchInstallations, } from '@/lib/knowledge/application/github-installations' +import { GitHubInstallationError } from '@/lib/oauth/github-installation' const principal = { kind: 'session', userId: 'admin', sessionId: 'session' } as const const input = { organizationId: 'org', installationId: '42' } @@ -142,6 +163,56 @@ describe('GitHub Search installation application operations', () => { }) expect(m.list).toHaveBeenCalledWith('ghu_reader', { signal }) }) + it('reenters reader OAuth when GitHub revoked a token still recorded as active', async () => { + setupReader() + m.list.mockRejectedValueOnce(new GitHubInstallationError('Bad credentials', 401)) + await expect( + listGitHubSearchInstallations.execute({ principal, input }) + ).resolves.toMatchObject({ + needsUserConnection: true, + installations: [], + }) + }) + it('reenters reader OAuth when token refresh detects a revoked grant', async () => { + setupReader() + m.token.mockRejectedValueOnce( + new ManagedOAuthCredentialError( + 'MANAGED_CREDENTIAL_NEEDS_REAUTH', + 'Refresh grant revoked', + 401 + ) + ) + await expect( + listGitHubSearchInstallations.execute({ principal, input }) + ).resolves.toMatchObject({ + needsUserConnection: true, + installations: [], + }) + expect(m.list).not.toHaveBeenCalled() + }) + it('preserves provider infrastructure failures during discovery', async () => { + setupReader() + const error = new GitHubInstallationError('Provider unavailable', 503) + m.list.mockRejectedValueOnce(error) + await expect(listGitHubSearchInstallations.execute({ principal, input })).rejects.toBe(error) + }) + it('preserves transient token refresh failures instead of requesting OAuth', async () => { + setupReader() + const error = new ManagedOAuthCredentialError( + 'MANAGED_CREDENTIAL_REFRESH_FAILED', + 'Transient refresh error', + 502 + ) + m.token.mockRejectedValueOnce(error) + await expect(listGitHubSearchInstallations.execute({ principal, input })).rejects.toBe(error) + }) + it('does not reclassify App JWT authorization failures during installation verification', async () => { + setupReader() + const error = new GitHubInstallationError('App JWT rejected', 401) + m.verify.mockRejectedValueOnce(error) + await expect(connect()).rejects.toBe(error) + expect(m.encrypt).not.toHaveBeenCalled() + }) it('reverifies GitHub admin authority before persisting an installation', async () => { setupReader() m.verify.mockRejectedValue(new Error('GitHub administrator access required')) diff --git a/apps/sim/lib/knowledge/application/github-installations.ts b/apps/sim/lib/knowledge/application/github-installations.ts index c1b6f786ab7..7add172b1c2 100644 --- a/apps/sim/lib/knowledge/application/github-installations.ts +++ b/apps/sim/lib/knowledge/application/github-installations.ts @@ -13,18 +13,25 @@ import { OrchestrationError } from '@/lib/core/orchestration/types' import { encryptSecret } from '@/lib/core/security/encryption' import { LIVE_ENROLLMENT_STATUSES } from '@/lib/credential-groups/credentials' import { getCredentialGroupProviderAdapter } from '@/lib/credential-groups/provider-registry' -import { resolveManagedOAuthToken } from '@/lib/credentials/managed-oauth' +import { + ManagedOAuthCredentialError, + resolveManagedOAuthToken, +} from '@/lib/credentials/managed-oauth' import type { DbOrTx } from '@/lib/db/types' import { requireOrganizationSearchAvailable } from '@/lib/knowledge/access/availability' import { defineAuthorizedKnowledgeUseCase } from '@/lib/knowledge/application/authorized-knowledge-use-case' import { resolveKnowledgeOrganizationContext } from '@/lib/knowledge/application/contexts' import { knowledgeOperations } from '@/lib/knowledge/application/operations' import { + GitHubInstallationError, getGitHubInstallationConfiguration, listUserAdminGitHubInstallations, verifyGitHubInstallationBinding, } from '@/lib/oauth/github-installation' -import { GITHUB_INSTALLATION_PROVIDER_ID } from '@/lib/oauth/github-installation-types' +import { + GITHUB_INSTALLATION_PROVIDER_ID, + type GitHubInstallationSummary, +} from '@/lib/oauth/github-installation-types' interface InstallationInput { organizationId: string @@ -36,7 +43,11 @@ interface ConnectInstallationInput extends InstallationInput { } /** Selects only the acting person's live, organization-bound GitHub connection. */ -async function findReaderCredential(executor: DbOrTx, organizationId: string, userId: string) { +export async function findGitHubSearchReaderCredential( + executor: DbOrTx, + organizationId: string, + userId: string +) { const policy = await getCredentialGroupProviderAdapter('github-repositories').getPolicy( undefined, { organizationId } @@ -99,18 +110,31 @@ export const listGitHubSearchInstallations = defineAuthorizedKnowledgeUseCase({ await requireOrganizationSearchAvailable(context.organizationId) const configuration = getGitHubInstallationConfiguration() const reader = configuration.configured - ? await findReaderCredential(db, context.organizationId, principal.userId) + ? await findGitHubSearchReaderCredential(db, context.organizationId, principal.userId) : null - const installations = reader - ? await listUserAdminGitHubInstallations( + let needsUserConnection = configuration.configured && !reader + let installations: GitHubInstallationSummary[] = [] + if (reader) { + try { + installations = await listUserAdminGitHubInstallations( (await readerToken(context.organizationId, reader.id)).accessToken, { signal: input.signal } ) - : [] + } catch (error) { + /** Only reader-token discovery can request reauthorization; App JWT failures stay errors. */ + if ( + (error instanceof GitHubInstallationError && error.status === 401) || + (error instanceof ManagedOAuthCredentialError && + error.code === 'MANAGED_CREDENTIAL_NEEDS_REAUTH') + ) + needsUserConnection = true + else throw error + } + } return { available: configuration.configured, installUrl: configuration.installUrl, - needsUserConnection: configuration.configured && !reader, + needsUserConnection, installations, } }, @@ -127,7 +151,11 @@ export const connectGitHubSearchInstallation = defineAuthorizedKnowledgeUseCase( 'validation', 'GitHub App installation indexing is not configured for this environment' ) - const reader = await findReaderCredential(db, context.organizationId, principal.userId) + const reader = await findGitHubSearchReaderCredential( + db, + context.organizationId, + principal.userId + ) if (!reader) throw new OrchestrationError( 'validation', @@ -168,7 +196,11 @@ export const connectGitHubSearchInstallation = defineAuthorizedKnowledgeUseCase( ) .for('update') .limit(1) - const current = await findReaderCredential(tx, context.organizationId, principal.userId) + const current = await findGitHubSearchReaderCredential( + tx, + context.organizationId, + principal.userId + ) if ( current?.id !== reader.id || current.authorizationAppId !== reader.authorizationAppId || diff --git a/apps/sim/lib/knowledge/application/github-setup.test.ts b/apps/sim/lib/knowledge/application/github-setup.test.ts new file mode 100644 index 00000000000..bd1d9939517 --- /dev/null +++ b/apps/sim/lib/knowledge/application/github-setup.test.ts @@ -0,0 +1,608 @@ +/** @vitest-environment node */ +import { db } from '@sim/db' +import { credential, member, user } from '@sim/db/schema' +import { queueTableRows, resetDbChainMock } from '@sim/testing' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const m = vi.hoisted(() => { + const values = new Map() + return { + values, + get: vi.fn(async (key: string) => { + const entry = values.get(key) + return entry && entry.expires > Date.now() ? entry.value : null + }), + set: vi.fn( + async (key: string, value: string, _unit: string, ttl: number, condition: string) => { + const entry = values.get(key) + const exists = entry && entry.expires > Date.now() + if ((condition === 'NX' && exists) || (condition === 'XX' && !exists)) return null + values.set(key, { value, expires: Date.now() + ttl }) + return 'OK' + } + ), + eval: vi.fn( + async ( + _script: string, + _count: number, + key: string, + nextValue: string, + expected: string, + ttl: number + ) => { + const entry = values.get(key) + if (!entry || entry.expires <= Date.now()) return 0 + const current = JSON.parse(entry.value) as Record + const next = JSON.parse(nextValue) as Record + if ( + current.phase !== expected || + ['organizationId', 'userId', 'sessionId', 'setupId', 'createdAt'].some( + (field) => current[field] !== next[field] + ) + ) + return 0 + values.set(key, { value: nextValue, expires: Date.now() + ttl }) + return 1 + } + ), + list: vi.fn(), + connect: vi.fn(), + reader: vi.fn(), + receipt: vi.fn(), + provision: vi.fn(), + enrollment: vi.fn(), + oauthContext: vi.fn(), + oauthStart: vi.fn(), + oauthComplete: vi.fn(), + } +}) +vi.mock('@/lib/core/config/redis', () => ({ + getRedisClient: () => ({ get: m.get, set: m.set, eval: m.eval }), +})) +vi.mock('@/lib/core/utils/urls', () => ({ getBaseUrl: () => 'https://sim.example' })) +vi.mock('@/lib/knowledge/application/contexts', () => ({ + resolveKnowledgeOrganizationContext: async ({ organizationId }: { organizationId: string }) => ({ + organizationId, + workspaceId: undefined, + }), +})) +vi.mock('@/lib/permission-groups/resolve.server', () => ({ + getUserPermissionConfigForOrganization: async () => null, +})) +vi.mock('@/lib/knowledge/application/github-installations', () => ({ + listGitHubSearchInstallations: { execute: m.list }, + connectGitHubSearchInstallation: { execute: m.connect }, + findGitHubSearchReaderCredential: m.reader, +})) +vi.mock('@/lib/knowledge/connectors/member-provisioning', () => ({ + provisionKnowledgeConnectorMembersBinding: m.provision, +})) +vi.mock('@/lib/credential-groups/self-enrollment', () => ({ + createViewerCredentialGroupEnrollment: m.enrollment, +})) +vi.mock('@/lib/credential-groups/enrollments', () => ({ + getCredentialGroupOAuthContextForEnrollment: m.oauthContext, +})) +vi.mock('@/lib/credential-groups/oauth', () => ({ startCredentialGroupOAuth: m.oauthStart })) +vi.mock('@/lib/credential-groups/application/public-enrollment', () => ({ + completePublicCredentialGroupOAuth: { execute: m.oauthComplete }, +})) +vi.mock('@/lib/credential-groups/search-connection-completion', () => ({ + readSearchConnectionCompletion: m.receipt, +})) +vi.mock('@/connectors/registry', () => ({ + CONNECTOR_META_REGISTRY: { + github: { name: 'GitHub', auth: { mode: 'oauth', provider: 'github-repositories' } }, + }, +})) +vi.mock('@/lib/credentials/managed-oauth', () => ({ + ManagedOAuthCredentialError: class extends Error {}, +})) +vi.mock('@/lib/oauth/github-installation', () => ({ + GitHubInstallationError: class extends Error {}, + getGitHubInstallationConfiguration: () => ({ + configured: true, + installUrl: 'https://github.com/apps/test-search/installations/new', + }), +})) + +import { + cancelGitHubSearchSetup, + completeGitHubSearchSetup, + completeGitHubSetupReaderOAuth, + continueGitHubSearchSetup, + readGitHubSearchSetup, + selectGitHubSearchSetup, + startGitHubSearchSetup, +} from '@/lib/knowledge/application/github-setup' +import { + GITHUB_SETUP_TTL_MS, + readGitHubSetupAttempt, + saveGitHubSetupAttempt, +} from '@/lib/knowledge/github-setup-state' + +const principal = { kind: 'session', userId: 'admin', sessionId: 'browser-1' } as const +const input = { organizationId: 'organization', setupId: '550e8400-e29b-41d4-a716-446655440000' } +const scope = { ...input, userId: principal.userId, sessionId: principal.sessionId } +const installation = { + installationId: '42', + accountId: '7', + accountLogin: 'example', + accountType: 'Organization' as const, +} +const secondInstallation = { + ...installation, + installationId: '43', + accountId: '8', + accountLogin: 'another', +} +const reader = { id: 'reader' } +const resultCredential = { id: 'installation-credential', displayName: 'example' } +function admin(role = 'admin') { + queueTableRows(member, [{ role }]) +} +async function start(intent?: 'install') { + admin() + return startGitHubSearchSetup.execute({ + principal, + input: { ...input, ...(intent ? { intent } : {}) }, + }) +} +async function status() { + admin() + return readGitHubSearchSetup.execute({ principal, input }) +} +async function cancel() { + admin() + return cancelGitHubSearchSetup.execute({ principal, input }) +} +async function callback(state: string, installationId = '42') { + admin() + return completeGitHubSearchSetup.execute({ + principal, + input: { state, installationId, setupAction: 'install' }, + }) +} +async function resume() { + admin() + return continueGitHubSearchSetup.execute({ principal, input }) +} +const oauthAttempt = { + state: 'cg_fixture', + provider: 'github-repositories' as const, + userId: principal.userId, + organizationId: input.organizationId, + email: 'admin@example.test', + credentialGroupId: 'group', + enrollmentId: 'enrollment', + optionId: 'option', + authorizationAppId: 'app', + scopeVersion: 1, + nonceHash: 'hash', + requiredScopes: [], + redirectUri: 'https://sim.example/api/auth/oauth2/callback/github-repositories', + completionRedirect: true, + completionId: input.setupId, + returnTo: 'github-installation' as const, + invitationToken: 'fixture-invitation', + createdAt: 0, +} + +beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + m.values.clear() + m.list.mockResolvedValue({ available: true, needsUserConnection: false, installations: [] }) + m.connect.mockResolvedValue({ credential: resultCredential, created: true }) + m.reader.mockResolvedValue(reader) + m.receipt.mockResolvedValue(null) + m.provision.mockResolvedValue({ credentialGroupId: 'group', credentialGroupOptionId: 'option' }) + m.enrollment.mockResolvedValue({ + enrollment: { id: 'enrollment', email: 'admin@example.test' }, + invitationLink: 'https://sim.example/credential-groups/enroll/fixture-invitation', + }) + m.oauthContext.mockResolvedValue({ fixture: 'oauth-context' }) + m.oauthStart.mockResolvedValue('https://github.com/login/oauth/authorize?state=cg_fixture') + m.oauthComplete.mockResolvedValue({ credentialId: 'reader' }) +}) +afterEach(() => { + vi.useRealTimers() + resetDbChainMock() +}) + +describe('GitHub setup lifecycle', () => { + it.each(['member', 'missing'])( + 'requires current organization admin access before state or provider work: %s', + async (role) => { + queueTableRows(member, role === 'missing' ? [] : [{ role }]) + await expect(startGitHubSearchSetup.execute({ principal, input })).rejects.toMatchObject({ + code: role === 'missing' ? 'not_found' : 'forbidden', + }) + expect(m.set).not.toHaveBeenCalled() + expect(m.list).not.toHaveBeenCalled() + } + ) + it('refuses non-session principals before state lookup', async () => { + await expect( + startGitHubSearchSetup.execute({ + principal: { kind: 'personal_api_key', userId: 'admin', keyId: 'key' }, + input, + }) + ).rejects.toMatchObject({ code: 'forbidden' }) + expect(m.get).not.toHaveBeenCalled() + }) + it('issues opaque state, verifies installation binding through the existing operation, and makes refresh idempotent', async () => { + const { url } = await start() + const state = new URL(url).searchParams.get('state')! + expect(state).not.toBe(input.setupId) + expect(url).not.toContain(input.organizationId) + expect(new URL(url).pathname).toBe('/apps/test-search/installations/new') + await callback(state) + expect(m.connect).toHaveBeenCalledWith( + expect.objectContaining({ + principal, + input: expect.objectContaining({ + organizationId: input.organizationId, + installationId: '42', + }), + }) + ) + await expect(status()).resolves.toEqual({ status: 'completed', credential: resultCredential }) + await callback(state) + expect(m.connect).toHaveBeenCalledTimes(1) + }) + it('reuses a sole eligible installation without forcing GitHub configuration', async () => { + m.list.mockResolvedValue({ + available: true, + needsUserConnection: false, + installations: [installation], + }) + const { url } = await start() + expect(new URL(url).pathname).toBe('/credential-groups/complete') + expect(new URL(url).searchParams.get('completionId')).toBe(input.setupId) + await expect(status()).resolves.toEqual({ status: 'completed', credential: resultCredential }) + }) + it('preserves explicit install intent even when one installation exists', async () => { + m.list.mockResolvedValue({ + available: true, + needsUserConnection: false, + installations: [installation], + }) + const { url } = await start('install') + expect(new URL(url).hostname).toBe('github.com') + expect(m.connect).not.toHaveBeenCalled() + }) + it('offers multiple installations using bounded metadata and rejects choices not offered', async () => { + m.list.mockResolvedValue({ + available: true, + needsUserConnection: false, + installations: [{ ...installation, appClientId: 'private-extra' }, secondInstallation], + }) + expect(new URL((await start()).url).pathname).toBe('/knowledge/github/setup') + await expect(status()).resolves.toEqual({ + status: 'choosing', + installations: [installation, secondInstallation], + }) + admin() + await expect( + selectGitHubSearchSetup.execute({ + principal, + input: { ...input, action: { kind: 'select', installationId: '999' } }, + }) + ).rejects.toMatchObject({ code: 'validation' }) + expect(m.connect).not.toHaveBeenCalled() + admin() + await selectGitHubSearchSetup.execute({ + principal, + input: { ...input, action: { kind: 'select', installationId: '43' } }, + }) + expect(m.connect).toHaveBeenCalledWith( + expect.objectContaining({ input: expect.objectContaining({ installationId: '43' }) }) + ) + }) + it('status reads never poll GitHub or return OAuth/state material', async () => { + await start() + m.list.mockClear() + await expect(status()).resolves.toEqual({ status: 'pending' }) + expect(m.list).not.toHaveBeenCalled() + expect(m.reader).not.toHaveBeenCalled() + }) + it.each([{ userId: 'other-user' }, { sessionId: 'other-browser' }])( + 'cannot read or consume another initiating session: %s', + async (change) => { + const state = new URL((await start()).url).searchParams.get('state')! + const other = { ...principal, ...change } + admin() + await expect(readGitHubSearchSetup.execute({ principal: other, input })).resolves.toEqual({ + status: 'expired', + }) + await expect( + completeGitHubSearchSetup.execute({ + principal: other, + input: { state, installationId: '42' }, + }) + ).rejects.toMatchObject({ code: 'validation' }) + expect(m.connect).not.toHaveBeenCalled() + await callback(state) + } + ) + it('keeps organization scopes isolated even when the correlation ID is known', async () => { + await start() + admin() + await expect( + readGitHubSearchSetup.execute({ + principal, + input: { ...input, organizationId: 'another-org' }, + }) + ).resolves.toEqual({ status: 'expired' }) + }) + it('cancellation prevents late callbacks and repeated begin with the same ID', async () => { + const state = new URL((await start()).url).searchParams.get('state')! + await cancel() + await expect(callback(state)).rejects.toMatchObject({ code: 'validation' }) + await expect(start()).rejects.toMatchObject({ code: 'validation' }) + await expect(status()).resolves.toEqual({ status: 'expired' }) + expect(m.connect).not.toHaveBeenCalled() + }) + it('a cancellation arriving before start leaves a tombstone', async () => { + await cancel() + await expect(start()).rejects.toMatchObject({ code: 'validation' }) + expect(m.list).not.toHaveBeenCalled() + }) + it('cancellation after committed completion preserves the result', async () => { + const state = new URL((await start()).url).searchParams.get('state')! + await callback(state) + await cancel() + await expect(status()).resolves.toEqual({ status: 'completed', credential: resultCredential }) + }) + it('expires without a sliding refresh window', async () => { + vi.useFakeTimers() + const { url } = await start() + vi.advanceTimersByTime(GITHUB_SETUP_TTL_MS - 1) + await expect(status()).resolves.toEqual({ status: 'pending' }) + vi.advanceTimersByTime(1) + await expect(status()).resolves.toEqual({ status: 'expired' }) + await expect(callback(new URL(url).searchParams.get('state')!)).rejects.toMatchObject({ + code: 'validation', + }) + expect(m.connect).not.toHaveBeenCalled() + }) + it('rechecks current administrator access on the callback', async () => { + const state = new URL((await start()).url).searchParams.get('state')! + admin('member') + await expect( + completeGitHubSearchSetup.execute({ principal, input: { state, installationId: '42' } }) + ).rejects.toMatchObject({ code: 'forbidden' }) + expect(m.connect).not.toHaveBeenCalled() + }) + it('records a safe failure and never exposes provider response bodies', async () => { + const state = new URL((await start()).url).searchParams.get('state')! + m.connect.mockRejectedValueOnce(new Error('secret_token fixture@example.test provider-body')) + await callback(state) + await expect(status()).resolves.toEqual({ + status: 'failed', + error: 'GitHub setup could not finish. Try again.', + }) + expect(JSON.stringify([...m.values.values()])).not.toContain('secret_token') + }) + it('an installation request awaiting GitHub approval cannot create a credential', async () => { + const state = new URL((await start()).url).searchParams.get('state')! + admin() + await completeGitHubSearchSetup.execute({ principal, input: { state, setupAction: 'request' } }) + expect(m.connect).not.toHaveBeenCalled() + await expect(status()).resolves.toMatchObject({ status: 'failed' }) + }) +}) + +describe('GitHub setup reader OAuth continuation', () => { + async function startOAuth(intent?: 'install') { + m.list.mockResolvedValueOnce({ available: true, needsUserConnection: true, installations: [] }) + queueTableRows(credential, []) + return start(intent) + } + it('starts the existing OAuth boundary and requires its receipt plus a current reader to continue', async () => { + await startOAuth() + expect(m.oauthStart).toHaveBeenCalledWith({ fixture: 'oauth-context' }, 'fixture-invitation', { + completionRedirect: true, + completionId: input.setupId, + returnTo: 'github-installation', + connectionIntent: { kind: 'create' }, + }) + await expect(resume()).rejects.toMatchObject({ code: 'forbidden' }) + m.receipt.mockResolvedValue('wrong-reader') + await expect(resume()).rejects.toMatchObject({ code: 'forbidden' }) + m.receipt.mockResolvedValue(reader.id) + expect(new URL((await resume()).url).hostname).toBe('github.com') + }) + it('preserves explicit install intent after OAuth with existing installations', async () => { + await startOAuth('install') + m.receipt.mockResolvedValue(reader.id) + m.list.mockResolvedValue({ + available: true, + needsUserConnection: false, + installations: [installation], + }) + expect(new URL((await resume()).url).hostname).toBe('github.com') + expect(m.connect).not.toHaveBeenCalled() + }) + it('a revoked enrollment stays denied before provider authorization', async () => { + m.enrollment.mockRejectedValueOnce(new Error('revoked')) + await startOAuth() + expect(m.oauthStart).not.toHaveBeenCalled() + await expect(status()).resolves.toMatchObject({ status: 'failed' }) + }) + it('requires the exact OAuth attempt and verified current user before completing the reader grant', async () => { + await startOAuth() + admin() + await expect( + completeGitHubSetupReaderOAuth.execute({ + principal, + input: { attempt: { ...oauthAttempt, state: 'wrong-state' }, code: 'fixture-code' }, + }) + ).rejects.toMatchObject({ code: 'validation' }) + expect(m.oauthComplete).not.toHaveBeenCalled() + admin() + queueTableRows(user, [{ emailVerified: false }]) + await expect( + completeGitHubSetupReaderOAuth.execute({ + principal, + input: { attempt: oauthAttempt, code: 'fixture-code' }, + }) + ).rejects.toMatchObject({ code: 'forbidden' }) + admin() + queueTableRows(user, [{ emailVerified: true }]) + await completeGitHubSetupReaderOAuth.execute({ + principal, + input: { attempt: oauthAttempt, code: 'fixture-code' }, + }) + expect(m.oauthComplete).toHaveBeenCalledWith( + expect.objectContaining({ + principal: expect.objectContaining({ + kind: 'credential_group_enrollment', + userId: 'admin', + organizationId: 'organization', + enrollmentId: 'enrollment', + }), + }) + ) + }) + it('cancellation prevents the intermediate OAuth grant as well as installation', async () => { + await startOAuth() + await cancel() + admin() + await expect( + completeGitHubSetupReaderOAuth.execute({ + principal, + input: { attempt: oauthAttempt, code: 'fixture-code' }, + }) + ).rejects.toMatchObject({ code: 'validation' }) + expect(m.oauthComplete).not.toHaveBeenCalled() + }) + it('maps OAuth failure classifications into the authoritative setup status', async () => { + await startOAuth() + admin() + await continueGitHubSearchSetup.execute({ + principal, + input: { ...input, oauth: 'github_email_mismatch' }, + }) + await expect(status()).resolves.toMatchObject({ + status: 'failed', + error: expect.stringContaining('verified secondary email'), + }) + expect(m.connect).not.toHaveBeenCalled() + }) +}) + +function deferred() { + let resolve!: (value: T) => void + const promise = new Promise((complete) => { + resolve = complete + }) + return { promise, resolve } +} + +describe('GitHub setup atomic claims', () => { + it('never wraps nested authorized operations in a global-pool transaction', async () => { + m.list.mockImplementationOnce(async () => { + expect(db.transaction).not.toHaveBeenCalled() + return { available: true, needsUserConnection: false, installations: [installation] } + }) + m.connect.mockImplementationOnce(async () => { + expect(db.transaction).not.toHaveBeenCalled() + return { credential: resultCredential } + }) + await start() + }) + it('cancellation during discovery prevents the final grant claim', async () => { + const entered = deferred() + const listed = deferred<{ + available: boolean + needsUserConnection: boolean + installations: (typeof installation)[] + }>() + m.list.mockImplementationOnce(() => { + entered.resolve() + return listed.promise + }) + const pending = start() + await entered.promise + await cancel() + listed.resolve({ available: true, needsUserConnection: false, installations: [installation] }) + await expect(pending).rejects.toMatchObject({ code: 'conflict' }) + expect(m.connect).not.toHaveBeenCalled() + await expect(status()).resolves.toEqual({ status: 'expired' }) + }) + it('a claimed final grant survives cancellation and concurrent callback replay without a duplicate grant', async () => { + const state = new URL((await start()).url).searchParams.get('state')! + const entered = deferred() + const connected = deferred<{ credential: typeof resultCredential }>() + m.connect.mockImplementationOnce(() => { + entered.resolve() + return connected.promise + }) + const pending = callback(state) + await entered.promise + await cancel() + await callback(state) + expect(m.connect).toHaveBeenCalledTimes(1) + connected.resolve({ credential: resultCredential }) + await pending + await expect(status()).resolves.toEqual({ status: 'completed', credential: resultCredential }) + }) + it('cancellation during a reader grant prevents subsequent installation continuation', async () => { + m.list.mockResolvedValueOnce({ available: true, needsUserConnection: true, installations: [] }) + queueTableRows(credential, []) + await start() + const entered = deferred() + const authorized = deferred<{ credentialId: string }>() + m.oauthComplete.mockImplementationOnce(() => { + entered.resolve() + return authorized.promise + }) + admin() + queueTableRows(user, [{ emailVerified: true }]) + const pending = completeGitHubSetupReaderOAuth.execute({ + principal, + input: { attempt: oauthAttempt, code: 'fixture-code' }, + }) + await entered.promise + await cancel() + authorized.resolve({ credentialId: 'reader' }) + await pending + m.receipt.mockResolvedValue('reader') + await expect(resume()).rejects.toMatchObject({ code: 'validation' }) + expect(m.connect).not.toHaveBeenCalled() + await expect(status()).resolves.toEqual({ status: 'expired' }) + }) + it('an interrupted claim is not replayed and expires into a recoverable new attempt', async () => { + vi.useFakeTimers() + const state = new URL((await start()).url).searchParams.get('state')! + const attempt = (await readGitHubSetupAttempt(scope))! + await saveGitHubSetupAttempt( + { ...scope, createdAt: attempt.createdAt, phase: 'connecting', installationId: '42', state }, + 'installing' + ) + await callback(state) + await cancel() + expect(m.connect).not.toHaveBeenCalled() + vi.advanceTimersByTime(GITHUB_SETUP_TTL_MS) + await expect(status()).resolves.toEqual({ status: 'expired' }) + m.list.mockResolvedValue({ + available: true, + needsUserConnection: false, + installations: [installation], + }) + admin() + await startGitHubSearchSetup.execute({ + principal, + input: { ...input, setupId: '660e8400-e29b-41d4-a716-446655440000' }, + }) + expect(m.connect).toHaveBeenCalledTimes(1) + }) + it('fails closed when Redis refuses the transition before the grant', async () => { + const state = new URL((await start()).url).searchParams.get('state')! + m.eval.mockRejectedValueOnce(new Error('Redis unavailable')) + await expect(callback(state)).rejects.toThrow('Redis unavailable') + expect(m.connect).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/knowledge/application/github-setup.ts b/apps/sim/lib/knowledge/application/github-setup.ts new file mode 100644 index 00000000000..9fdc061c52b --- /dev/null +++ b/apps/sim/lib/knowledge/application/github-setup.ts @@ -0,0 +1,515 @@ +import type { SessionPrincipal } from '@sim/auth/principal' +import { db } from '@sim/db' +import { credential, user } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { sha256Hex } from '@sim/security/hash' +import { and, eq } from 'drizzle-orm' +import { asOrchestrationError, OrchestrationError } from '@/lib/core/orchestration/types' +import { completePublicCredentialGroupOAuth } from '@/lib/credential-groups/application/public-enrollment' +import { getCredentialGroupOAuthContextForEnrollment } from '@/lib/credential-groups/enrollments' +import { startCredentialGroupOAuth } from '@/lib/credential-groups/oauth' +import { + CREDENTIAL_GROUP_OAUTH_FAILURE_MESSAGES, + isCredentialGroupOAuthFailure, +} from '@/lib/credential-groups/oauth-completion' +import type { CredentialGroupOAuthAttempt } from '@/lib/credential-groups/oauth-state' +import { readSearchConnectionCompletion } from '@/lib/credential-groups/search-connection-completion' +import { createViewerCredentialGroupEnrollment } from '@/lib/credential-groups/self-enrollment' +import { ManagedOAuthCredentialError } from '@/lib/credentials/managed-oauth' +import { defineAuthorizedKnowledgeUseCase } from '@/lib/knowledge/application/authorized-knowledge-use-case' +import { resolveKnowledgeOrganizationContext } from '@/lib/knowledge/application/contexts' +import { + connectGitHubSearchInstallation, + findGitHubSearchReaderCredential, + listGitHubSearchInstallations, +} from '@/lib/knowledge/application/github-installations' +import { knowledgeOperations } from '@/lib/knowledge/application/operations' +import { provisionKnowledgeConnectorMembersBinding } from '@/lib/knowledge/connectors/member-provisioning' +import { + expiredGitHubSetupError, + type GitHubSetupAttempt, + type GitHubSetupScope, + githubSetupScope, + issueGitHubSetupCallback, + readGitHubSetupAttempt, + resolveGitHubSetupCallback, + saveGitHubSetupAttempt, +} from '@/lib/knowledge/github-setup-state' +import { githubSetupCompletionUrl, githubSetupPageUrl } from '@/lib/knowledge/github-setup-urls' +import { + GitHubInstallationError, + getGitHubInstallationConfiguration, +} from '@/lib/oauth/github-installation' +import { CONNECTOR_META_REGISTRY } from '@/connectors/registry' + +const logger = createLogger('GitHubSearchSetup') +interface SetupInput { + organizationId: string + setupId: string +} +interface SetupSelectionInput extends SetupInput { + action: { kind: 'select'; installationId: string } | { kind: 'install' } +} +interface SetupCallbackInput { + state: string + installationId?: string + setupAction?: string +} + +function safeSetupError(error: unknown): string { + if (error instanceof ManagedOAuthCredentialError) + return 'Reconnect your GitHub account, then start setup again.' + if (error instanceof GitHubInstallationError) + return error.status === 403 + ? 'Choose a GitHub account or organization you own and allow the required app permissions.' + : 'GitHub could not finish setup right now. Try again in a few minutes.' + const classified = asOrchestrationError(error) + if (classified && ['validation', 'forbidden', 'conflict'].includes(classified.code)) + return classified.message + return 'GitHub setup could not finish. Try again.' +} + +function installationUrl(state: string) { + const { installUrl } = getGitHubInstallationConfiguration() + if (!installUrl) throw new OrchestrationError('validation', 'GitHub App setup is unavailable.') + const url = new URL(installUrl) + url.searchParams.set('state', state) + return url.toString() +} + +function currentUrl(attempt: GitHubSetupAttempt): string { + switch (attempt.phase) { + case 'oauth': + return attempt.url + case 'authorizing': + return githubSetupPageUrl(attempt) + case 'installing': + return installationUrl(attempt.state) + case 'completed': + return githubSetupCompletionUrl(attempt.setupId) + case 'failed': + return githubSetupCompletionUrl(attempt.setupId, 'failed') + case 'cancelled': + throw expiredGitHubSetupError() + default: + return githubSetupPageUrl(attempt) + } +} + +async function requireAttempt(scope: GitHubSetupScope) { + const attempt = await readGitHubSetupAttempt(scope) + if (!attempt || attempt.phase === 'cancelled') throw expiredGitHubSetupError() + return attempt +} + +function base(attempt: GitHubSetupAttempt) { + return { + ...githubSetupScope(attempt), + createdAt: attempt.createdAt, + ...(attempt.intent ? { intent: attempt.intent } : {}), + } +} + +async function failAttempt( + attempt: GitHubSetupAttempt, + error: unknown, + stage: string = attempt.phase +) { + logger.warn('GitHub setup failed', { + stage, + errorType: + error instanceof TypeError + ? 'type_error' + : error instanceof SyntaxError + ? 'syntax_error' + : error instanceof Error + ? 'error' + : 'unknown', + ...(error instanceof Error && { fingerprint: sha256Hex(error.message).slice(0, 12) }), + failure: + asOrchestrationError(error)?.code ?? + (error instanceof GitHubInstallationError ? 'provider' : 'unexpected'), + ...(error instanceof GitHubInstallationError && { providerStatus: error.status }), + }) + await saveGitHubSetupAttempt( + { ...base(attempt), phase: 'failed', error: safeSetupError(error) }, + attempt.phase + ) + return { url: githubSetupCompletionUrl(attempt.setupId, 'failed') } +} + +async function connectAttempt( + principal: SessionPrincipal, + attempt: GitHubSetupAttempt, + installationId: string +) { + const connecting: GitHubSetupAttempt = { + ...base(attempt), + phase: 'connecting', + installationId, + ...('state' in attempt && attempt.state ? { state: attempt.state } : {}), + } + await saveGitHubSetupAttempt(connecting, attempt.phase) + let result + try { + result = await connectGitHubSearchInstallation.execute({ + principal, + input: { + organizationId: attempt.organizationId, + installationId, + signal: AbortSignal.timeout(60_000), + }, + }) + } catch (error) { + return failAttempt(connecting, error) + } + /** An interrupted claim expires; a fresh attempt reuses the idempotent installation binding. */ + await saveGitHubSetupAttempt( + { + ...base(attempt), + phase: 'completed', + credential: result.credential, + }, + 'connecting' + ) + return { url: githubSetupCompletionUrl(attempt.setupId) } +} + +async function installAttempt(attempt: GitHubSetupAttempt) { + const state = await issueGitHubSetupCallback(attempt) + await saveGitHubSetupAttempt({ ...base(attempt), phase: 'installing', state }, attempt.phase) + return { url: installationUrl(state) } +} + +async function discoverInstallations(principal: SessionPrincipal, attempt: GitHubSetupAttempt) { + const result = await listGitHubSearchInstallations.execute({ + principal, + input: { organizationId: attempt.organizationId, signal: AbortSignal.timeout(60_000) }, + }) + if (!result.available) + throw new OrchestrationError('validation', 'GitHub App setup is unavailable.') + if (result.needsUserConnection) return null + if (attempt.intent === 'install' || result.installations.length === 0) + return installAttempt(attempt) + if (result.installations.length === 1) + return connectAttempt(principal, attempt, result.installations[0]!.installationId) + await saveGitHubSetupAttempt( + { + ...base(attempt), + phase: 'choosing', + installations: result.installations.map( + ({ installationId, accountId, accountLogin, accountType }) => ({ + installationId, + accountId, + accountLogin, + accountType, + }) + ), + }, + attempt.phase + ) + return { url: githubSetupPageUrl(attempt) } +} + +async function authorizeReader(attempt: GitHubSetupAttempt) { + const binding = await provisionKnowledgeConnectorMembersBinding({ + organizationId: attempt.organizationId, + userId: attempt.userId, + connectorMeta: CONNECTOR_META_REGISTRY.github!, + }) + const { enrollment, invitationLink } = await createViewerCredentialGroupEnrollment({ + organizationId: attempt.organizationId, + userId: attempt.userId, + credentialGroupId: binding.credentialGroupId, + }) + const token = new URL(invitationLink).pathname.split('/').at(-1) + if (!token) throw new Error('Account enrollment did not return an invitation token') + const context = await getCredentialGroupOAuthContextForEnrollment( + { + organizationId: attempt.organizationId, + credentialGroupId: binding.credentialGroupId, + enrollmentId: enrollment.id, + email: enrollment.email, + userId: attempt.userId, + }, + binding.credentialGroupOptionId + ) + if (!context) + throw new OrchestrationError('forbidden', 'This account connection is no longer available.') + const [existing] = await db + .select({ id: credential.id }) + .from(credential) + .where( + and( + eq(credential.organizationId, attempt.organizationId), + eq(credential.type, 'managed_oauth'), + eq(credential.credentialGroupEnrollmentId, enrollment.id), + eq(credential.credentialGroupOptionId, binding.credentialGroupOptionId) + ) + ) + .limit(1) + const url = await startCredentialGroupOAuth(context, token, { + completionRedirect: true, + completionId: attempt.setupId, + returnTo: 'github-installation', + connectionIntent: existing + ? { kind: 'reconnect', credentialId: existing.id } + : { kind: 'create' }, + }) + await saveGitHubSetupAttempt({ ...base(attempt), phase: 'oauth', url }, attempt.phase) + return { url } +} + +export const startGitHubSearchSetup = defineAuthorizedKnowledgeUseCase({ + operation: knowledgeOperations.startGitHubSetup, + resolveContext: ({ input }: { input: SetupInput & { intent?: 'install' } }) => + resolveKnowledgeOrganizationContext(input), + async execute({ principal, input }) { + const scope = { + organizationId: input.organizationId, + setupId: input.setupId, + userId: principal.userId, + sessionId: principal.sessionId, + } + const existing = await readGitHubSetupAttempt(scope) + if (existing && existing.phase !== 'starting') return { url: currentUrl(existing) } + if (await readSearchConnectionCompletion({ ...scope, completionId: input.setupId })) + throw new OrchestrationError('conflict', 'Start a new GitHub setup attempt.') + const attempt: GitHubSetupAttempt = existing ?? { + ...scope, + phase: 'starting', + createdAt: Date.now(), + ...(input.intent ? { intent: input.intent } : {}), + } + if (!existing) await saveGitHubSetupAttempt(attempt, 'new') + let stage = 'discovery' + try { + const discovered = await discoverInstallations(principal, attempt) + if (discovered) return discovered + stage = 'reader_authorization' + return await authorizeReader(attempt) + } catch (error) { + return failAttempt(attempt, error, stage) + } + }, +}) + +export const readGitHubSearchSetup = defineAuthorizedKnowledgeUseCase({ + operation: knowledgeOperations.readGitHubSetup, + resolveContext: ({ input }: { input: SetupInput }) => resolveKnowledgeOrganizationContext(input), + async execute({ principal, input }) { + const attempt = await readGitHubSetupAttempt({ + ...input, + userId: principal.userId, + sessionId: principal.sessionId, + }) + if (!attempt || attempt.phase === 'cancelled') return { status: 'expired' as const } + if (attempt.phase === 'completed') + return { status: 'completed' as const, credential: attempt.credential } + if (attempt.phase === 'failed') return { status: 'failed' as const, error: attempt.error } + if (attempt.phase === 'choosing') + return { status: 'choosing' as const, installations: attempt.installations } + return { status: 'pending' as const } + }, +}) + +export const cancelGitHubSearchSetup = defineAuthorizedKnowledgeUseCase({ + operation: knowledgeOperations.cancelGitHubSetup, + resolveContext: ({ input }: { input: SetupInput }) => resolveKnowledgeOrganizationContext(input), + async execute({ principal, input }) { + const scope = { ...input, userId: principal.userId, sessionId: principal.sessionId } + for (let retry = 0; retry < 3; retry++) { + const attempt = await readGitHubSetupAttempt(scope) + if (attempt?.phase === 'completed' || attempt?.phase === 'connecting') break + try { + await saveGitHubSetupAttempt( + attempt + ? { ...base(attempt), phase: 'cancelled' } + : { ...scope, createdAt: Date.now(), phase: 'cancelled' }, + attempt?.phase ?? 'new' + ) + break + } catch (error) { + if (asOrchestrationError(error)?.code !== 'conflict' || retry === 2) throw error + } + } + return { success: true as const } + }, +}) + +export const continueGitHubSearchSetup = defineAuthorizedKnowledgeUseCase({ + operation: knowledgeOperations.continueGitHubSetup, + resolveContext: ({ input }: { input: SetupInput & { oauth?: string } }) => + resolveKnowledgeOrganizationContext(input), + async execute({ principal, input }) { + const scope = { + organizationId: input.organizationId, + setupId: input.setupId, + userId: principal.userId, + sessionId: principal.sessionId, + } + const attempt = await requireAttempt(scope) + if (attempt.phase !== 'oauth' && attempt.phase !== 'authorizing') + return { url: currentUrl(attempt) } + if (isCredentialGroupOAuthFailure(input.oauth)) { + await saveGitHubSetupAttempt( + { + ...base(attempt), + phase: 'failed', + error: CREDENTIAL_GROUP_OAUTH_FAILURE_MESSAGES[input.oauth], + }, + attempt.phase + ) + return { url: githubSetupCompletionUrl(input.setupId, input.oauth) } + } + const receipt = await readSearchConnectionCompletion({ + ...scope, + completionId: input.setupId, + }) + const reader = await findGitHubSearchReaderCredential( + db, + input.organizationId, + principal.userId + ) + if (!receipt || reader?.id !== receipt) + throw new OrchestrationError( + 'forbidden', + 'Complete GitHub authorization before continuing setup.' + ) + try { + const result = await discoverInstallations(principal, attempt) + if (!result) + throw new OrchestrationError('forbidden', 'Your GitHub connection is no longer available.') + return result + } catch (error) { + return failAttempt(attempt, error) + } + }, +}) + +export const selectGitHubSearchSetup = defineAuthorizedKnowledgeUseCase({ + operation: knowledgeOperations.selectGitHubSetup, + resolveContext: ({ input }: { input: SetupSelectionInput }) => + resolveKnowledgeOrganizationContext(input), + async execute({ principal, input }) { + const scope = { + organizationId: input.organizationId, + setupId: input.setupId, + userId: principal.userId, + sessionId: principal.sessionId, + } + const attempt = await requireAttempt(scope) + if (attempt.phase === 'completed') return { url: currentUrl(attempt) } + if (attempt.phase === 'connecting') return { url: currentUrl(attempt) } + if (attempt.phase !== 'choosing') + throw new OrchestrationError('conflict', 'Refresh GitHub setup before making a selection.') + if (input.action.kind === 'install') return installAttempt(attempt) + const { installationId } = input.action + if ( + !attempt.installations.some((installation) => installation.installationId === installationId) + ) + throw new OrchestrationError( + 'validation', + 'Choose one of the available GitHub installations.' + ) + return connectAttempt(principal, attempt, installationId) + }, +}) + +export const completeGitHubSearchSetup = defineAuthorizedKnowledgeUseCase({ + operation: knowledgeOperations.completeGitHubSetup, + async resolveContext({ + principal, + input, + }: { + principal: SessionPrincipal + input: SetupCallbackInput + }) { + const scope = await resolveGitHubSetupCallback(input.state, principal) + return { ...(await resolveKnowledgeOrganizationContext(scope)), setupScope: scope } + }, + async execute({ principal, input, context }) { + const attempt = await requireAttempt(context.setupScope) + if (attempt.phase === 'completed' || attempt.phase === 'failed') + return { url: currentUrl(attempt) } + if ( + (attempt.phase !== 'installing' && attempt.phase !== 'connecting') || + attempt.state !== input.state + ) + throw expiredGitHubSetupError() + if (attempt.phase === 'connecting') { + if (attempt.installationId !== input.installationId) throw expiredGitHubSetupError() + return { url: currentUrl(attempt) } + } + if ( + !input.installationId || + (input.setupAction && !['install', 'update'].includes(input.setupAction)) + ) + return failAttempt( + attempt, + new OrchestrationError( + 'validation', + 'GitHub installation was not completed. Ask a GitHub organization owner to install the app, then try again.' + ) + ) + return connectAttempt(principal, attempt, input.installationId) + }, +}) + +/** An installation setup keeps its admin authority and cancellation boundary during reader OAuth. */ +export const completeGitHubSetupReaderOAuth = defineAuthorizedKnowledgeUseCase({ + operation: knowledgeOperations.completeGitHubSetupOAuth, + resolveContext: ({ + principal, + input, + }: { + principal: SessionPrincipal + input: { attempt: CredentialGroupOAuthAttempt; code: string } + }) => { + const { attempt } = input + if ( + attempt.provider !== 'github-repositories' || + attempt.returnTo !== 'github-installation' || + !attempt.organizationId || + !attempt.completionId || + attempt.userId !== principal.userId + ) + throw expiredGitHubSetupError() + return resolveKnowledgeOrganizationContext({ organizationId: attempt.organizationId }) + }, + async execute({ principal, input, context, request }) { + const scope = { + organizationId: context.organizationId, + setupId: input.attempt.completionId!, + userId: principal.userId, + sessionId: principal.sessionId, + } + const setup = await requireAttempt(scope) + if ( + setup.phase !== 'oauth' || + new URL(setup.url).searchParams.get('state') !== input.attempt.state + ) + throw expiredGitHubSetupError() + const [viewer] = await db + .select({ emailVerified: user.emailVerified }) + .from(user) + .where(eq(user.id, principal.userId)) + .limit(1) + if (!viewer?.emailVerified) + throw new OrchestrationError('forbidden', 'Verify your Sim email before connecting GitHub.') + await saveGitHubSetupAttempt({ ...base(setup), phase: 'authorizing', url: setup.url }, 'oauth') + return completePublicCredentialGroupOAuth.execute({ + principal: { + kind: 'credential_group_enrollment', + userId: principal.userId, + organizationId: scope.organizationId, + credentialGroupId: input.attempt.credentialGroupId, + enrollmentId: input.attempt.enrollmentId, + email: input.attempt.email, + invitationTokenHash: sha256Hex(input.attempt.invitationToken), + }, + input, + request, + }) + }, +}) diff --git a/apps/sim/lib/knowledge/application/operations.ts b/apps/sim/lib/knowledge/application/operations.ts index 87d98e6e53c..09f8225cb62 100644 --- a/apps/sim/lib/knowledge/application/operations.ts +++ b/apps/sim/lib/knowledge/application/operations.ts @@ -102,6 +102,69 @@ const HUMAN_COPILOT_AND_EXECUTOR_PRINCIPAL_POLICY = { } as const export const knowledgeOperations = { + completeGitHubSetupOAuth: defineKnowledgeOperation( + defineWorkspaceOperation({ + id: 'knowledge.github.setup.oauth.complete', + minimumRole: 'admin', + workspaceApiKey: 'deny', + capability: 'knowledge.use', + principalKinds: ['session'], + }) + ), + startGitHubSetup: defineKnowledgeOperation( + defineWorkspaceOperation({ + id: 'knowledge.github.setup.start', + minimumRole: 'admin', + workspaceApiKey: 'deny', + capability: 'knowledge.use', + principalKinds: ['session'], + }) + ), + readGitHubSetup: defineKnowledgeOperation( + defineWorkspaceOperation({ + id: 'knowledge.github.setup.read', + minimumRole: 'admin', + workspaceApiKey: 'deny', + capability: 'knowledge.use', + principalKinds: ['session'], + }) + ), + cancelGitHubSetup: defineKnowledgeOperation( + defineWorkspaceOperation({ + id: 'knowledge.github.setup.cancel', + minimumRole: 'admin', + workspaceApiKey: 'deny', + capability: 'knowledge.use', + principalKinds: ['session'], + }) + ), + continueGitHubSetup: defineKnowledgeOperation( + defineWorkspaceOperation({ + id: 'knowledge.github.setup.continue', + minimumRole: 'admin', + workspaceApiKey: 'deny', + capability: 'knowledge.use', + principalKinds: ['session'], + }) + ), + selectGitHubSetup: defineKnowledgeOperation( + defineWorkspaceOperation({ + id: 'knowledge.github.setup.select', + minimumRole: 'admin', + workspaceApiKey: 'deny', + capability: 'knowledge.use', + principalKinds: ['session'], + }) + ), + completeGitHubSetup: defineKnowledgeOperation( + defineWorkspaceOperation({ + id: 'knowledge.github.setup.complete', + minimumRole: 'admin', + workspaceApiKey: 'deny', + capability: 'knowledge.use', + principalKinds: ['session'], + }) + ), listGitHubInstallations: defineKnowledgeOperation( defineWorkspaceOperation({ id: 'knowledge.github.installations.list', diff --git a/apps/sim/lib/knowledge/github-setup-navigation.ts b/apps/sim/lib/knowledge/github-setup-navigation.ts new file mode 100644 index 00000000000..07e0a73d46a --- /dev/null +++ b/apps/sim/lib/knowledge/github-setup-navigation.ts @@ -0,0 +1,12 @@ +/** Setup can navigate only within Sim or to GitHub's approval pages. */ +export function resolveGitHubSetupUrl(value: string, origin: string): string { + const url = new URL(value, origin) + if ( + url.username || + url.password || + (url.origin !== origin && !(url.protocol === 'https:' && url.hostname === 'github.com')) + ) { + throw new Error('GitHub returned an invalid setup URL') + } + return url.href +} diff --git a/apps/sim/lib/knowledge/github-setup-state.ts b/apps/sim/lib/knowledge/github-setup-state.ts new file mode 100644 index 00000000000..deeff458dbd --- /dev/null +++ b/apps/sim/lib/knowledge/github-setup-state.ts @@ -0,0 +1,171 @@ +import type { SessionPrincipal } from '@sim/auth/principal' +import { sha256Hex } from '@sim/security/hash' +import { generateId } from '@sim/utils/id' +import { z } from 'zod' +import { getRedisClient } from '@/lib/core/config/redis' +import { OrchestrationError } from '@/lib/core/orchestration/types' + +export const GITHUB_SETUP_TTL_MS = 10 * 60 * 1000 +const scopeSchema = z + .object({ + organizationId: z.string().min(1).max(200), + setupId: z.string().uuid(), + userId: z.string().min(1).max(200), + sessionId: z.string().min(1).max(200), + }) + .strict() +export type GitHubSetupScope = z.infer + +const installationSchema = z + .object({ + installationId: z + .string() + .regex(/^[1-9]\d*$/) + .max(32), + accountId: z + .string() + .regex(/^[1-9]\d*$/) + .max(32), + accountLogin: z.string().min(1).max(100), + accountType: z.enum(['User', 'Organization']), + }) + .strict() +const baseSchema = scopeSchema.extend({ + createdAt: z.number().int().nonnegative(), + intent: z.literal('install').optional(), +}) +const attemptSchema = z.discriminatedUnion('phase', [ + baseSchema.extend({ phase: z.literal('starting') }), + baseSchema.extend({ phase: z.literal('oauth'), url: z.string().url().max(8192) }), + baseSchema.extend({ phase: z.literal('authorizing'), url: z.string().url().max(8192) }), + baseSchema.extend({ + phase: z.literal('choosing'), + installations: z.array(installationSchema).min(1).max(1000), + }), + baseSchema.extend({ phase: z.literal('installing'), state: z.string().uuid() }), + baseSchema.extend({ + phase: z.literal('connecting'), + installationId: z + .string() + .regex(/^[1-9]\d*$/) + .max(32), + state: z.string().uuid().optional(), + }), + baseSchema.extend({ + phase: z.literal('completed'), + credential: z + .object({ id: z.string().min(1).max(200), displayName: z.string().min(1).max(500) }) + .strict(), + }), + baseSchema.extend({ phase: z.literal('failed'), error: z.string().min(1).max(1000) }), + baseSchema.extend({ phase: z.literal('cancelled') }), +]) +export type GitHubSetupAttempt = z.infer +export type GitHubSetupPhase = GitHubSetupAttempt['phase'] + +function redis() { + const client = getRedisClient() + if (!client) throw new Error('GitHub setup requires Redis') + return client +} + +function attemptKey(scope: GitHubSetupScope) { + return `github-search:setup:${sha256Hex(JSON.stringify([scope.organizationId, scope.userId, scope.setupId]))}` +} + +function callbackKey(state: string) { + return `github-search:setup-callback:${sha256Hex(state)}` +} + +export function expiredGitHubSetupError() { + return new OrchestrationError('validation', 'GitHub setup expired or was canceled. Start again.') +} + +/** Retains the original expiry across refreshes and retries. */ +function remainingTtl(createdAt: number) { + const remaining = createdAt + GITHUB_SETUP_TTL_MS - Date.now() + if (createdAt > Date.now() || remaining <= 0) throw expiredGitHubSetupError() + return remaining +} + +export function githubSetupScope(attempt: GitHubSetupScope): GitHubSetupScope { + return scopeSchema.parse({ + organizationId: attempt.organizationId, + setupId: attempt.setupId, + userId: attempt.userId, + sessionId: attempt.sessionId, + }) +} + +const TRANSITION = ` +local raw = redis.call('GET', KEYS[1]) +if not raw then return 0 end +local current = cjson.decode(raw) +local next = cjson.decode(ARGV[1]) +if current.phase ~= ARGV[2] or current.userId ~= next.userId or current.sessionId ~= next.sessionId or current.organizationId ~= next.organizationId or current.setupId ~= next.setupId or current.createdAt ~= next.createdAt then return 0 end +redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[3]) +return 1 +` + +/** Atomically claims a lifecycle transition; cancellation cannot be overwritten by stale work. */ +export async function saveGitHubSetupAttempt( + attempt: GitHubSetupAttempt, + expected: GitHubSetupPhase | 'new' +) { + const key = attemptKey(attempt) + const value = JSON.stringify(attemptSchema.parse(attempt)) + const ttl = remainingTtl(attempt.createdAt) + const saved = + expected === 'new' + ? (await redis().set(key, value, 'PX', ttl, 'NX')) === 'OK' + : (await redis().eval(TRANSITION, 1, key, value, expected, ttl)) === 1 + if (!saved) + throw new OrchestrationError( + 'conflict', + 'GitHub setup changed. Refresh to continue or start again.' + ) +} + +export async function readGitHubSetupAttempt( + scope: GitHubSetupScope +): Promise { + const raw = await redis().get(attemptKey(scope)) + if (!raw) return null + const attempt = attemptSchema.parse(JSON.parse(raw)) + if ( + attempt.organizationId !== scope.organizationId || + attempt.userId !== scope.userId || + attempt.setupId !== scope.setupId || + attempt.sessionId !== scope.sessionId || + attempt.createdAt > Date.now() || + Date.now() - attempt.createdAt >= GITHUB_SETUP_TTL_MS + ) + return null + return attempt +} + +/** Only a random, server-issued nonce is sent to GitHub; the owner remains in Redis. */ +export async function issueGitHubSetupCallback(attempt: GitHubSetupAttempt) { + const state = generateId() + const stored = await redis().set( + callbackKey(state), + JSON.stringify(githubSetupScope(attempt)), + 'PX', + remainingTtl(attempt.createdAt), + 'NX' + ) + if (stored !== 'OK') throw new Error('Could not create GitHub setup state') + return state +} + +/** A different browser session cannot consume or discover an attempt through its callback. */ +export async function resolveGitHubSetupCallback(state: string, principal: SessionPrincipal) { + const raw = await redis().get(callbackKey(state)) + if (!raw) throw expiredGitHubSetupError() + const scope = scopeSchema.parse(JSON.parse(raw)) + if (scope.userId !== principal.userId || scope.sessionId !== principal.sessionId) + throw expiredGitHubSetupError() + const attempt = await readGitHubSetupAttempt(scope) + if (!attempt) throw expiredGitHubSetupError() + return scope +} diff --git a/apps/sim/lib/knowledge/github-setup-urls.test.ts b/apps/sim/lib/knowledge/github-setup-urls.test.ts new file mode 100644 index 00000000000..97aa5587390 --- /dev/null +++ b/apps/sim/lib/knowledge/github-setup-urls.test.ts @@ -0,0 +1,64 @@ +/** @vitest-environment node */ +import { describe, expect, it, vi } from 'vitest' +import { + completeGitHubSearchSetupQuerySchema, + gitHubSearchSetupScopeSchema, + selectGitHubSearchSetupBodySchema, + startGitHubSearchSetupBodySchema, +} from '@/lib/api/contracts/knowledge/github-setup' + +vi.mock('@/lib/core/utils/urls', () => ({ getBaseUrl: () => 'https://sim.example' })) + +import { + githubSetupCompletionUrl, + githubSetupContinueUrl, + githubSetupPageUrl, +} from '@/lib/knowledge/github-setup-urls' + +const scope = { + organizationId: 'org/with?special&characters', + setupId: '550e8400-e29b-41d4-a716-446655440000', +} +describe('GitHub setup URLs and contracts', () => { + it.each([githubSetupPageUrl, githubSetupContinueUrl])( + 'uses the configured origin and encodes scope as values', + (build) => { + const url = new URL(build(scope)) + expect(url.origin).toBe('https://sim.example') + expect(url.searchParams.get('organizationId')).toBe(scope.organizationId) + expect(url.searchParams.get('setupId')).toBe(scope.setupId) + expect(url.hash).toBe('') + } + ) + it('correlates the generic completion page without installation IDs or provider state', () => { + const url = new URL(githubSetupCompletionUrl(scope.setupId, 'denied')) + expect(url.pathname).toBe('/credential-groups/complete') + expect([...url.searchParams]).toEqual([ + ['completionId', scope.setupId], + ['oauth', 'denied'], + ]) + }) + it('admits explicit install intent only on start and accepts no client redirect target', () => { + const valid = { organizationId: 'org', setupId: scope.setupId } + expect( + startGitHubSearchSetupBodySchema.safeParse({ ...valid, intent: 'install' }).success + ).toBe(true) + expect(gitHubSearchSetupScopeSchema.safeParse({ ...valid, intent: 'install' }).success).toBe( + false + ) + expect( + startGitHubSearchSetupBodySchema.safeParse({ ...valid, returnTo: 'https://evil.example' }) + .success + ).toBe(false) + expect( + selectGitHubSearchSetupBodySchema.safeParse({ + ...valid, + action: { kind: 'select', installationId: '0' }, + }).success + ).toBe(false) + expect( + completeGitHubSearchSetupQuerySchema.safeParse({ state: 'guessed', installation_id: '42' }) + .success + ).toBe(false) + }) +}) diff --git a/apps/sim/lib/knowledge/github-setup-urls.ts b/apps/sim/lib/knowledge/github-setup-urls.ts new file mode 100644 index 00000000000..7d6b721aa44 --- /dev/null +++ b/apps/sim/lib/knowledge/github-setup-urls.ts @@ -0,0 +1,27 @@ +import { getBaseUrl } from '@/lib/core/utils/urls' +import type { CredentialGroupOAuthFailure } from '@/lib/credential-groups/oauth-completion' + +export function githubSetupPageUrl(scope: { organizationId: string; setupId: string }) { + const url = new URL('/knowledge/github/setup', getBaseUrl()) + url.searchParams.set('organizationId', scope.organizationId) + url.searchParams.set('setupId', scope.setupId) + return url.toString() +} + +export function githubSetupContinueUrl( + scope: { organizationId: string; setupId: string }, + oauth?: CredentialGroupOAuthFailure +) { + const url = new URL('/api/knowledge/github/setup/continue', getBaseUrl()) + url.searchParams.set('organizationId', scope.organizationId) + url.searchParams.set('setupId', scope.setupId) + if (oauth) url.searchParams.set('oauth', oauth) + return url.toString() +} + +export function githubSetupCompletionUrl(setupId: string, oauth?: CredentialGroupOAuthFailure) { + const url = new URL('/credential-groups/complete', getBaseUrl()) + url.searchParams.set('completionId', setupId) + if (oauth) url.searchParams.set('oauth', oauth) + return url.toString() +} From 74842006ad745040efa3fe3d2baeae99ca15c9fa Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 10 Sep 2026 21:30:54 -0700 Subject: [PATCH 3/3] fix(github): align shared setup test fixtures --- .../github-installation-modal.test.tsx | 199 ------------------ .../components/search-source-setup.test.tsx | 8 + .../knowledge/application/operations.test.ts | 7 + 3 files changed, 15 insertions(+), 199 deletions(-) delete mode 100644 apps/sim/app/workspace/[workspaceId]/search/components/github-installation-modal.test.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/search/components/github-installation-modal.test.tsx b/apps/sim/app/workspace/[workspaceId]/search/components/github-installation-modal.test.tsx deleted file mode 100644 index af00ad3f4ec..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/search/components/github-installation-modal.test.tsx +++ /dev/null @@ -1,199 +0,0 @@ -/** - * @vitest-environment jsdom - */ -import { act } from 'react' -import { createRoot, type Root } from 'react-dom/client' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { ListGitHubSearchInstallationsResponse } from '@/lib/api/contracts/knowledge/github-installations' - -const mocks = vi.hoisted(() => ({ - data: undefined as ListGitHubSearchInstallationsResponse | undefined, - error: null as Error | null, - fetching: false, - pending: false, - refetch: vi.fn(), - connectInstallation: vi.fn(), - ensureAccounts: vi.fn(), - connectAccount: vi.fn(), - onConnected: vi.fn(), -})) - -vi.mock('@/hooks/queries/github-search-installations', () => ({ - useGitHubSearchInstallations: () => ({ - data: mocks.data, - error: mocks.error, - isSuccess: Boolean(mocks.data) && !mocks.error, - isError: Boolean(mocks.error), - isFetching: mocks.fetching, - refetch: mocks.refetch, - }), - useConnectGitHubSearchInstallation: () => ({ - mutate: mocks.connectInstallation, - isPending: mocks.pending, - error: null, - }), -})) -vi.mock('@/hooks/queries/organization-accounts', () => ({ - useEnsureOrganizationAccounts: () => ({ - mutate: mocks.ensureAccounts, - isPending: false, - error: null, - }), - useConnectOrganizationAccount: () => ({ - mutate: mocks.connectAccount, - isPending: false, - error: null, - }), -})) - -import { GitHubInstallationModal } from '@/app/workspace/[workspaceId]/search/components/github-installation-modal' - -let root: Root -let container: HTMLDivElement - -async function render() { - await act(async () => { - root.render( - - ) - }) -} - -function button(label: string): HTMLButtonElement { - const match = Array.from(document.querySelectorAll('button')).find( - (node) => node.textContent?.trim() === label - ) - if (!match) throw new Error(`Missing button: ${label}`) - return match -} - -beforeEach(() => { - vi.clearAllMocks() - mocks.data = { - success: true, - available: true, - installUrl: 'https://github.com/apps/sim-search/installations/new', - needsUserConnection: false, - installations: [ - { - installationId: '123', - accountId: '456', - accountLogin: 'acme', - accountType: 'Organization', - }, - ], - } - mocks.error = null - mocks.fetching = false - mocks.pending = false - ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true - vi.stubGlobal( - 'ResizeObserver', - class { - observe() {} - unobserve() {} - disconnect() {} - } - ) - container = document.createElement('div') - document.body.appendChild(container) - root = createRoot(container) -}) - -afterEach(async () => { - await act(async () => root.unmount()) - container.remove() - vi.restoreAllMocks() -}) - -describe('GitHub installation setup', () => { - it('uses only a server-verified installation and returns its credential', async () => { - await render() - const installLink = document.querySelector('a[href*="installations/new"]') - expect(installLink?.href).toBe('https://github.com/apps/sim-search/installations/new') - expect(installLink?.target).toBe('_blank') - expect(installLink?.rel).toContain('noopener') - await act(async () => button('Use installation').click()) - expect(mocks.connectInstallation).toHaveBeenCalledWith( - { organizationId: 'org-1', installationId: '123' }, - expect.any(Object) - ) - const callbacks = mocks.connectInstallation.mock.calls[0][1] - callbacks.onSuccess({ success: true, credential: { id: 'credential-1', displayName: 'acme' } }) - expect(mocks.onConnected).toHaveBeenCalledWith('credential-1') - }) - - it('requires a connected personal account before selecting an installation', async () => { - mocks.data!.needsUserConnection = true - await render() - expect(button('Use installation').disabled).toBe(true) - expect(button('Connect your GitHub account')).toBeTruthy() - expect(document.querySelector('a[href*="installations/new"]')).toBeNull() - expect(document.querySelector('[role="combobox"]')).toBeNull() - }) - - it('opens the existing managed-account connection after preserving provider setup', async () => { - mocks.data!.needsUserConnection = true - const tab = { opener: {}, closed: false, location: { href: '' }, close: vi.fn() } - vi.spyOn(window, 'open').mockReturnValue(tab as unknown as Window) - await render() - await act(async () => button('Connect your GitHub account').click()) - expect(tab.opener).toBeNull() - expect(mocks.ensureAccounts).toHaveBeenCalledWith( - { - organizationId: 'org-1', - option: { provider: 'github-repositories', label: 'GitHub', required: false }, - }, - expect.any(Object) - ) - const setup = mocks.ensureAccounts.mock.calls[0][1] - await act(async () => - setup.onSuccess({ - credentialGroup: { - options: [ - { id: 'other-option', provider: 'confluence', status: 'active' }, - { id: 'github-option', provider: 'github-repositories', status: 'active' }, - ], - }, - }) - ) - expect(mocks.connectAccount).toHaveBeenCalledWith( - { organizationId: 'org-1', optionId: 'github-option' }, - expect.any(Object) - ) - await act(async () => - mocks.connectAccount.mock.calls[0][1].onSuccess({ - invitationLink: 'https://sim.ai/credential-groups/invite/test', - }) - ) - expect(tab.location.href).toBe('https://sim.ai/credential-groups/invite/test') - expect(document.body.textContent).toContain('Finish connecting your account in the other tab') - }) - - it('does not create an enrollment when the browser blocks the account tab', async () => { - mocks.data!.needsUserConnection = true - vi.spyOn(window, 'open').mockReturnValue(null) - await render() - await act(async () => button('Connect your GitHub account').click()) - expect(mocks.ensureAccounts).not.toHaveBeenCalled() - expect(document.body.textContent).toContain('Allow pop-ups') - }) - - it.each(['unavailable', 'error', 'refreshing', 'empty'] as const)( - 'refuses installation changes while %s', - async (state) => { - if (state === 'unavailable') mocks.data!.available = false - if (state === 'error') mocks.error = new Error('Installation lookup failed') - if (state === 'refreshing') mocks.fetching = true - if (state === 'empty') mocks.data!.installations = [] - await render() - expect(button('Use installation').disabled).toBe(true) - await act(async () => button('Use installation').click()) - expect(mocks.connectInstallation).not.toHaveBeenCalled() - } - ) -}) diff --git a/apps/sim/app/workspace/[workspaceId]/search/components/search-source-setup.test.tsx b/apps/sim/app/workspace/[workspaceId]/search/components/search-source-setup.test.tsx index 1b54c95d232..3266b86d157 100644 --- a/apps/sim/app/workspace/[workspaceId]/search/components/search-source-setup.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/search/components/search-source-setup.test.tsx @@ -73,6 +73,14 @@ vi.mock('@/lib/auth/auth-client', () => ({ useSession: () => ({ data: { user: { id: mocks.userId } } }), })) vi.mock('@/hooks/use-oauth-return', () => ({ useOAuthReturnForKBConnectors: mocks.oauthReturn })) +vi.mock('@/hooks/use-github-installation-setup', () => ({ + useGitHubInstallationSetup: () => ({ + connect: vi.fn(), + cancel: vi.fn(), + pending: false, + error: null, + }), +})) vi.mock('@/hooks/use-permission-config', () => ({ usePermissionConfig: () => ({ integrationAvailability: new Map([ diff --git a/apps/sim/lib/knowledge/application/operations.test.ts b/apps/sim/lib/knowledge/application/operations.test.ts index a2d8f28daaf..0dabc3ef7fc 100644 --- a/apps/sim/lib/knowledge/application/operations.test.ts +++ b/apps/sim/lib/knowledge/application/operations.test.ts @@ -16,6 +16,13 @@ describe('knowledge operation registry', () => { it('defines unique stable semantic operation IDs', () => { const ids = Object.values(knowledgeOperations).map((operation) => operation.id) expect(ids).toEqual([ + 'knowledge.github.setup.oauth.complete', + 'knowledge.github.setup.start', + 'knowledge.github.setup.read', + 'knowledge.github.setup.cancel', + 'knowledge.github.setup.continue', + 'knowledge.github.setup.select', + 'knowledge.github.setup.complete', 'knowledge.github.installations.list', 'knowledge.github.installations.connect', 'knowledge.slack.prepare',

L>Aw)CVg4#K3kIuIdL*)zq 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 diff --git a/apps/docs/public/static/search/github-connect.png b/apps/docs/public/static/search/github-connect.png new file mode 100644 index 0000000000000000000000000000000000000000..587d03437b01dc136077e1c8377c1b3b9d6a3d89 GIT binary patch literal 34406 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 literal 0 HcmV?d00001 diff --git a/apps/docs/public/static/search/github-installation-connect.jpg b/apps/docs/public/static/search/github-installation-connect.jpg deleted file mode 100644 index d64bff70f834cea490a0459f9ea96b0634752630..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18761 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 literal 32397 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%0Dd-q3yJUT z@3)p{gjI%rN;&4_T9_LftP{>3z8z(2q z-#PuubCjG;Q8&*-S~@v7LH)0)swz&~4!wP{FvY)s6%nwq?8 z#egfY6Ein0==FkO0c2!k_5)-gSgig6m>wG9!dmemu&Q~047s{$qh3&6yO!72`#Cvb zZO1Kyp2TO4JOAAe$Pj;^$a&7|i;p)8D{Gq)S-X(W*AQ2%ap2cuH$vton8beJ0sxGy z5hH`SkHhtA*B(k`RTxrbs49hXtKTy*xvK}{jgyVd#~%V9fJV~akdnHG!Gwh3)VM`t zWq}=r9E*vG2|NOz6j(&~2^fJJbPz}l6C_@ZoT6g?kmw~Jc#8EFppzhpy!-#|1pq@3 zV%97e3gsLg9tJ~iAy=e;dJ=_%H3qd#^;6bDZGJuvKmT(Lz!%mfOF9+xq{yf!I2(LZ za`)oYRA$*KtjqJ~&l3!}g{E8{SzANCs%%B3z#Y{N@+{B*q607?%ppE%YiZ?@lU@TR9gBlw;9L&kCQ^4z7mPl>@q`k5`Ng7I5-g^u zh)8IGqs{$kmtDw2Ra816soB1)K)TA;^!wllGTroi2)(a|9DS&0qO9HA-sa|pEI={I z6y1@qMokt5G6oWVQ7jZIqMk$?LWGR)l)CU#dBE^=^C|aI6M?9pmymq{I83|;=>7G} z#n8TC+U(xF^j^C}Lts{dnuGoPCRRLwk?ZU2y_g)YQ3|LTw?KC03}l*HNrN6hDS=m2 z-r?{0Ix+%jNK|{GCeIl%vb%Z_A6y(9HunR_9|HUFhDP65Uq1z!87f}j7g~oNxw;A_ z(cidn1B-j1$rEYm=kE{JXKMt32aYnlE~%!w^XCs}&o5WWp!BKgW$|F&wy6AUta zp|-M8B%A{?gnX*Z!^@kcZ4bNL)kR)kGJ8or=loNW%+t@vAWXtVE9TIpeO}&nn@un) z0{rr|gvA20V(fJmU#-tpx#@QQ{xuO%0D406M3N>Ch)l{DWo>EJSeylwmqtWH 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 Ltn<$W=cxY!oe^YC literal 25572 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 ed>EjCk diff --git a/apps/docs/public/static/search/github-repositories.png b/apps/docs/public/static/search/github-repositories.png index 5c697a25d6c7ddc0f8728d53dae971571537a3ec..6ae32abc07b078990f5ddf9d21593ce339f047a3 100644 GIT binary patch literal 47545 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 Hwj};PvI(6*Y{D;QBhD(?30m} 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 diff --git a/apps/docs/public/static/search/github-repository-picker.png b/apps/docs/public/static/search/github-repository-picker.png new file mode 100644 index 0000000000000000000000000000000000000000..43e1a77bf6e1a41ad2624433aaef929a77743b84 GIT binary patch literal 49125 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