@shipfox/client-integrations 4.0.0 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +86 -0
  3. package/dist/application/complete-integration-callback.d.ts +13 -0
  4. package/dist/application/complete-integration-callback.d.ts.map +1 -0
  5. package/dist/application/complete-integration-callback.js +26 -0
  6. package/dist/application/complete-integration-callback.js.map +1 -0
  7. package/dist/components/connection-picker.d.ts +2 -2
  8. package/dist/components/connection-picker.d.ts.map +1 -1
  9. package/dist/components/connection-picker.js +4 -4
  10. package/dist/components/connection-picker.js.map +1 -1
  11. package/dist/components/installed-integrations-section.d.ts +8 -6
  12. package/dist/components/installed-integrations-section.d.ts.map +1 -1
  13. package/dist/components/installed-integrations-section.js +22 -15
  14. package/dist/components/installed-integrations-section.js.map +1 -1
  15. package/dist/components/integration-gallery-for-workspace.d.ts +2 -2
  16. package/dist/components/integration-gallery-for-workspace.d.ts.map +1 -1
  17. package/dist/components/integration-gallery-for-workspace.js +14 -7
  18. package/dist/components/integration-gallery-for-workspace.js.map +1 -1
  19. package/dist/components/integration-gallery.d.ts +2 -2
  20. package/dist/components/integration-gallery.d.ts.map +1 -1
  21. package/dist/components/integration-gallery.js.map +1 -1
  22. package/dist/components/integration-usage-events.d.ts +1 -3
  23. package/dist/components/integration-usage-events.d.ts.map +1 -1
  24. package/dist/components/integration-usage-events.js +1 -125
  25. package/dist/components/integration-usage-events.js.map +1 -1
  26. package/dist/components/integration-usage-modal.d.ts +2 -2
  27. package/dist/components/integration-usage-modal.d.ts.map +1 -1
  28. package/dist/components/integration-usage-modal.js +2 -2
  29. package/dist/components/integration-usage-modal.js.map +1 -1
  30. package/dist/components/provider-grid.d.ts +7 -4
  31. package/dist/components/provider-grid.d.ts.map +1 -1
  32. package/dist/components/provider-grid.js +15 -10
  33. package/dist/components/provider-grid.js.map +1 -1
  34. package/dist/components/redirect-install-page.d.ts +2 -3
  35. package/dist/components/redirect-install-page.d.ts.map +1 -1
  36. package/dist/components/redirect-install-page.js +1 -1
  37. package/dist/components/redirect-install-page.js.map +1 -1
  38. package/dist/components/repository-picker.d.ts +2 -2
  39. package/dist/components/repository-picker.d.ts.map +1 -1
  40. package/dist/components/repository-picker.js +4 -4
  41. package/dist/components/repository-picker.js.map +1 -1
  42. package/dist/components/webhook/webhook-create-modal.d.ts +2 -2
  43. package/dist/components/webhook/webhook-create-modal.d.ts.map +1 -1
  44. package/dist/components/webhook/webhook-create-modal.js +8 -8
  45. package/dist/components/webhook/webhook-create-modal.js.map +1 -1
  46. package/dist/components/webhook/webhook-modals.stories.d.ts.map +1 -1
  47. package/dist/components/webhook/webhook-modals.stories.js +7 -7
  48. package/dist/components/webhook/webhook-modals.stories.js.map +1 -1
  49. package/dist/components/webhook/webhook-usage-details.d.ts.map +1 -1
  50. package/dist/components/webhook/webhook-usage-details.js +2 -2
  51. package/dist/components/webhook/webhook-usage-details.js.map +1 -1
  52. package/dist/connection-status-badge.d.ts +2 -2
  53. package/dist/connection-status-badge.d.ts.map +1 -1
  54. package/dist/connection-status-badge.js +11 -15
  55. package/dist/connection-status-badge.js.map +1 -1
  56. package/dist/core/models.d.ts +66 -0
  57. package/dist/core/models.d.ts.map +1 -0
  58. package/dist/core/models.js +156 -0
  59. package/dist/core/models.js.map +1 -0
  60. package/dist/feature.d.ts +18 -0
  61. package/dist/feature.d.ts.map +1 -1
  62. package/dist/feature.js +16 -1
  63. package/dist/feature.js.map +1 -1
  64. package/dist/hooks/api/integration-mapper.d.ts +11 -0
  65. package/dist/hooks/api/integration-mapper.d.ts.map +1 -0
  66. package/dist/hooks/api/integration-mapper.js +54 -0
  67. package/dist/hooks/api/integration-mapper.js.map +1 -0
  68. package/dist/hooks/api/integrations.d.ts +47 -237
  69. package/dist/hooks/api/integrations.d.ts.map +1 -1
  70. package/dist/hooks/api/integrations.js +92 -42
  71. package/dist/hooks/api/integrations.js.map +1 -1
  72. package/dist/hooks/api/webhook-connections.d.ts +12 -64
  73. package/dist/hooks/api/webhook-connections.d.ts.map +1 -1
  74. package/dist/hooks/api/webhook-connections.js +17 -9
  75. package/dist/hooks/api/webhook-connections.js.map +1 -1
  76. package/dist/index.d.ts +2 -1
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +2 -1
  79. package/dist/index.js.map +1 -1
  80. package/dist/linear-callback.d.ts +2 -1
  81. package/dist/linear-callback.d.ts.map +1 -1
  82. package/dist/linear-callback.js +14 -15
  83. package/dist/linear-callback.js.map +1 -1
  84. package/dist/pages/gitea-install-page.d.ts.map +1 -1
  85. package/dist/pages/gitea-install-page.js +1 -10
  86. package/dist/pages/gitea-install-page.js.map +1 -1
  87. package/dist/pages/linear-callback-page.d.ts.map +1 -1
  88. package/dist/pages/linear-callback-page.js +19 -32
  89. package/dist/pages/linear-callback-page.js.map +1 -1
  90. package/dist/pages/linear-install-page.d.ts.map +1 -1
  91. package/dist/pages/linear-install-page.js +2 -1
  92. package/dist/pages/linear-install-page.js.map +1 -1
  93. package/dist/pages/sentry-callback-page.d.ts.map +1 -1
  94. package/dist/pages/sentry-callback-page.js +22 -25
  95. package/dist/pages/sentry-callback-page.js.map +1 -1
  96. package/dist/pages/sentry-install-page.d.ts.map +1 -1
  97. package/dist/pages/sentry-install-page.js +2 -1
  98. package/dist/pages/sentry-install-page.js.map +1 -1
  99. package/dist/pages/slack-callback-page.d.ts.map +1 -1
  100. package/dist/pages/slack-callback-page.js +21 -28
  101. package/dist/pages/slack-callback-page.js.map +1 -1
  102. package/dist/pages/slack-install-page.d.ts.map +1 -1
  103. package/dist/pages/slack-install-page.js +2 -1
  104. package/dist/pages/slack-install-page.js.map +1 -1
  105. package/dist/pages/source-control-onboarding-page.d.ts.map +1 -1
  106. package/dist/pages/source-control-onboarding-page.js +5 -1
  107. package/dist/pages/source-control-onboarding-page.js.map +1 -1
  108. package/dist/provider-catalog.d.ts.map +1 -1
  109. package/dist/provider-catalog.js +7 -6
  110. package/dist/provider-catalog.js.map +1 -1
  111. package/dist/routes/github-callback.d.ts +16 -0
  112. package/dist/routes/github-callback.d.ts.map +1 -1
  113. package/dist/routes/github-callback.js +43 -21
  114. package/dist/routes/github-callback.js.map +1 -1
  115. package/dist/routes/integrations-settings.d.ts +6 -0
  116. package/dist/routes/integrations-settings.d.ts.map +1 -0
  117. package/dist/routes/integrations-settings.js +7 -0
  118. package/dist/routes/integrations-settings.js.map +1 -0
  119. package/dist/routes/linear-callback.d.ts +2 -0
  120. package/dist/routes/linear-callback.d.ts.map +1 -1
  121. package/dist/routes/linear-callback.js +2 -0
  122. package/dist/routes/linear-callback.js.map +1 -1
  123. package/dist/routes/sentry-callback.d.ts +2 -0
  124. package/dist/routes/sentry-callback.d.ts.map +1 -1
  125. package/dist/routes/sentry-callback.js +2 -0
  126. package/dist/routes/sentry-callback.js.map +1 -1
  127. package/dist/routes/slack-callback.d.ts +2 -0
  128. package/dist/routes/slack-callback.d.ts.map +1 -1
  129. package/dist/routes/slack-callback.js +2 -0
  130. package/dist/routes/slack-callback.js.map +1 -1
  131. package/dist/sentry-callback.d.ts +2 -1
  132. package/dist/sentry-callback.d.ts.map +1 -1
  133. package/dist/sentry-callback.js +16 -19
  134. package/dist/sentry-callback.js.map +1 -1
  135. package/dist/slack-callback.d.ts +2 -1
  136. package/dist/slack-callback.d.ts.map +1 -1
  137. package/dist/slack-callback.js +14 -15
  138. package/dist/slack-callback.js.map +1 -1
  139. package/dist/tsconfig.test.tsbuildinfo +1 -1
  140. package/package.json +17 -17
  141. package/src/application/complete-integration-callback.test.ts +41 -0
  142. package/src/application/complete-integration-callback.ts +39 -0
  143. package/src/components/connection-picker.tsx +7 -7
  144. package/src/components/installed-integrations-section.tsx +29 -24
  145. package/src/components/integration-gallery-for-workspace.tsx +23 -16
  146. package/src/components/integration-gallery.tsx +3 -3
  147. package/src/components/integration-usage-events.ts +1 -116
  148. package/src/components/integration-usage-modal.tsx +4 -4
  149. package/src/components/provider-grid.tsx +30 -18
  150. package/src/components/redirect-install-page.test.tsx +2 -6
  151. package/src/components/redirect-install-page.tsx +3 -2
  152. package/src/components/repository-picker.tsx +6 -6
  153. package/src/components/webhook/webhook-create-modal.tsx +12 -11
  154. package/src/components/webhook/webhook-modals.stories.tsx +10 -10
  155. package/src/components/webhook/webhook-usage-details.tsx +2 -4
  156. package/src/connection-status-badge.tsx +8 -14
  157. package/src/core/models.ts +200 -0
  158. package/src/feature.ts +17 -1
  159. package/src/hooks/api/integration-mapper.ts +64 -0
  160. package/src/hooks/api/integrations.test.ts +44 -22
  161. package/src/hooks/api/integrations.ts +258 -89
  162. package/src/hooks/api/webhook-connections.ts +58 -17
  163. package/src/index.ts +2 -1
  164. package/src/linear-callback.ts +21 -16
  165. package/src/pages/gitea-install-page.tsx +1 -10
  166. package/src/pages/linear-callback-page.test.tsx +1 -1
  167. package/src/pages/linear-callback-page.tsx +18 -29
  168. package/src/pages/linear-install-page.tsx +2 -1
  169. package/src/pages/sentry-callback-page.test.tsx +6 -2
  170. package/src/pages/sentry-callback-page.tsx +28 -25
  171. package/src/pages/sentry-install-page.tsx +2 -1
  172. package/src/pages/slack-callback-page.tsx +20 -25
  173. package/src/pages/slack-install-page.tsx +2 -1
  174. package/src/pages/source-control-onboarding-page.tsx +5 -1
  175. package/src/provider-catalog.ts +7 -6
  176. package/src/routes/github-callback.test.ts +35 -1
  177. package/src/routes/github-callback.tsx +47 -21
  178. package/src/routes/integrations-settings.tsx +4 -0
  179. package/src/routes/linear-callback.tsx +5 -1
  180. package/src/routes/sentry-callback.tsx +5 -1
  181. package/src/routes/slack-callback.tsx +6 -1
  182. package/src/sentry-callback.test.ts +2 -1
  183. package/src/sentry-callback.ts +28 -20
  184. package/src/slack-callback.ts +21 -16
  185. package/test/render.tsx +1 -5
  186. package/tsconfig.build.tsbuildinfo +1 -1
  187. package/vercel.json +5 -0
  188. package/dist/integration-icon.d.ts +0 -17
  189. package/dist/integration-icon.d.ts.map +0 -1
  190. package/dist/integration-icon.js +0 -22
  191. package/dist/integration-icon.js.map +0 -1
  192. package/src/integration-icon.react.test.tsx +0 -40
  193. package/src/integration-icon.test.ts +0 -19
  194. package/src/integration-icon.tsx +0 -26
@@ -10,8 +10,8 @@ import {
10
10
 
11
11
  function connection(overrides: Partial<IntegrationConnectionDto> = {}): IntegrationConnectionDto {
12
12
  return {
13
- id: 'c1',
14
- workspace_id: 'ws-1',
13
+ id: '11111111-1111-4111-8111-111111111111',
14
+ workspace_id: '22222222-2222-4222-8222-222222222222',
15
15
  provider: 'github',
16
16
  external_account_id: 'acct',
17
17
  slug: 'github_acct',
@@ -40,19 +40,23 @@ describe('listSourceConnections', () => {
40
40
  return Promise.resolve(
41
41
  jsonResponse({
42
42
  connections: [
43
- connection({id: 'active-1', lifecycle_status: 'active'}),
44
- connection({id: 'disabled-1', lifecycle_status: 'disabled'}),
45
- connection({id: 'error-1', lifecycle_status: 'error'}),
43
+ connection({id: '33333333-3333-4333-8333-333333333333', lifecycle_status: 'active'}),
44
+ connection({id: '44444444-4444-4444-8444-444444444444', lifecycle_status: 'disabled'}),
45
+ connection({id: '55555555-5555-4555-8555-555555555555', lifecycle_status: 'error'}),
46
46
  ],
47
47
  }),
48
48
  );
49
49
  });
50
50
  configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
51
51
 
52
- const result = await listSourceConnections({workspaceId: 'ws-1'});
52
+ const result = await listSourceConnections({
53
+ workspaceId: '22222222-2222-4222-8222-222222222222',
54
+ });
53
55
 
54
56
  expect(requestedUrl).toContain('capability=source_control');
55
- expect(result.connections.map((connection) => connection.id)).toEqual(['active-1']);
57
+ expect(result.map((connection) => connection.id)).toEqual([
58
+ '33333333-3333-4333-8333-333333333333',
59
+ ]);
56
60
  });
57
61
  });
58
62
 
@@ -63,11 +67,20 @@ describe('Slack transport', () => {
63
67
  baseUrl: 'https://api.example.test',
64
68
  fetchImpl: vi.fn((input, init) => {
65
69
  requests.push(new Request(input, init));
66
- return Promise.resolve(jsonResponse(connection({provider: 'slack'})));
70
+ const url = input instanceof Request ? input.url : String(input);
71
+ return Promise.resolve(
72
+ jsonResponse(
73
+ url.endsWith('/install')
74
+ ? {install_url: 'https://slack.example.test/install'}
75
+ : connection({provider: 'slack'}),
76
+ ),
77
+ );
67
78
  }),
68
79
  });
69
80
 
70
- await createSlackInstall({workspace_id: '11111111-1111-4111-8111-111111111111'});
81
+ const install = await createSlackInstall({
82
+ workspace_id: '11111111-1111-4111-8111-111111111111',
83
+ });
71
84
  await completeSlackCallback({
72
85
  query: {code: 'grant code', state: 'signed state'},
73
86
  token: 'session-token',
@@ -77,6 +90,7 @@ describe('Slack transport', () => {
77
90
  expect(await requests[0]?.json()).toEqual({
78
91
  workspace_id: '11111111-1111-4111-8111-111111111111',
79
92
  });
93
+ expect(install).toEqual({installUrl: 'https://slack.example.test/install'});
80
94
  expect(requests[1]?.url).toBe(
81
95
  'https://api.example.test/integrations/slack/callback/api?code=grant+code&state=signed+state',
82
96
  );
@@ -89,24 +103,31 @@ describe('Linear transport', () => {
89
103
  const requests: Request[] = [];
90
104
  const fetchImpl = vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
91
105
  requests.push(new Request(input, init));
106
+ const url = input instanceof Request ? input.url : String(input);
92
107
  return Promise.resolve(
93
- jsonResponse({
94
- id: 'connection-1',
95
- workspace_id: '11111111-1111-4111-8111-111111111111',
96
- provider: 'linear',
97
- external_account_id: 'linear-org',
98
- slug: 'linear_org',
99
- display_name: 'Linear org',
100
- lifecycle_status: 'active',
101
- capabilities: [],
102
- created_at: '2026-01-01T00:00:00.000Z',
103
- updated_at: '2026-01-01T00:00:00.000Z',
104
- }),
108
+ jsonResponse(
109
+ url.endsWith('/install')
110
+ ? {install_url: 'https://linear.example.test/install'}
111
+ : {
112
+ id: '33333333-3333-4333-8333-333333333333',
113
+ workspace_id: '11111111-1111-4111-8111-111111111111',
114
+ provider: 'linear',
115
+ external_account_id: 'linear-org',
116
+ slug: 'linear_org',
117
+ display_name: 'Linear org',
118
+ lifecycle_status: 'active',
119
+ capabilities: [],
120
+ created_at: '2026-01-01T00:00:00.000Z',
121
+ updated_at: '2026-01-01T00:00:00.000Z',
122
+ },
123
+ ),
105
124
  );
106
125
  });
107
126
  configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
108
127
 
109
- await createLinearInstall({workspace_id: '11111111-1111-4111-8111-111111111111'});
128
+ const install = await createLinearInstall({
129
+ workspace_id: '11111111-1111-4111-8111-111111111111',
130
+ });
110
131
  await completeLinearCallback({
111
132
  query: {code: 'grant code', state: 'signed state'},
112
133
  token: 'session-token',
@@ -124,6 +145,7 @@ describe('Linear transport', () => {
124
145
  expect(await requests[0]?.json()).toEqual({
125
146
  workspace_id: '11111111-1111-4111-8111-111111111111',
126
147
  });
148
+ expect(install).toEqual({installUrl: 'https://linear.example.test/install'});
127
149
  expect(requests[1]?.url).toBe(
128
150
  'https://api.example.test/integrations/linear/callback/api?code=grant+code&state=signed+state',
129
151
  );
@@ -1,41 +1,71 @@
1
1
  import type {
2
2
  IntegrationCapabilityDto,
3
- IntegrationConnectionDto,
4
- ListIntegrationConnectionsResponseDto,
5
- ListIntegrationProvidersResponseDto,
6
- ListRepositoriesResponseDto,
7
3
  UpdateIntegrationConnectionBodyDto,
8
4
  } from '@shipfox/api-integration-core-dto';
9
- import type {
10
- CreateGiteaConnectionBodyDto,
11
- CreateGiteaConnectionResponseDto,
12
- } from '@shipfox/api-integration-gitea-dto';
13
- import type {
14
- CreateGithubInstallBodyDto,
15
- CreateGithubInstallResponseDto,
5
+ import {
6
+ integrationConnectionDtoSchema,
7
+ listIntegrationConnectionsResponseSchema,
8
+ listIntegrationProvidersResponseSchema,
9
+ listRepositoriesResponseSchema,
10
+ } from '@shipfox/api-integration-core-dto';
11
+ import type {CreateGiteaConnectionBodyDto} from '@shipfox/api-integration-gitea-dto';
12
+ import {createGiteaConnectionResponseSchema} from '@shipfox/api-integration-gitea-dto';
13
+ import type {CreateGithubInstallBodyDto} from '@shipfox/api-integration-github-dto';
14
+ import {
15
+ createGithubInstallResponseSchema,
16
+ githubCallbackResponseSchema,
16
17
  } from '@shipfox/api-integration-github-dto';
17
18
  import type {
18
19
  CreateLinearInstallBodyDto,
19
- CreateLinearInstallResponseDto,
20
20
  LinearCallbackQueryDto,
21
- LinearCallbackResponseDto,
21
+ } from '@shipfox/api-integration-linear-dto';
22
+ import {
23
+ createLinearInstallResponseSchema,
24
+ linearCallbackResponseSchema,
22
25
  } from '@shipfox/api-integration-linear-dto';
23
26
  import type {
24
27
  CreateSentryInstallBodyDto,
25
- CreateSentryInstallResponseDto,
26
28
  SentryConnectBodyDto,
27
- SentryConnectResponseDto,
29
+ } from '@shipfox/api-integration-sentry-dto';
30
+ import {
31
+ createSentryInstallResponseSchema,
32
+ sentryConnectResponseSchema,
28
33
  } from '@shipfox/api-integration-sentry-dto';
29
34
  import type {
30
35
  CreateSlackInstallBodyDto,
31
- CreateSlackInstallResponseDto,
32
36
  SlackCallbackQueryDto,
33
- SlackCallbackResponseDto,
34
37
  } from '@shipfox/api-integration-slack-dto';
35
- import {apiRequest} from '@shipfox/client-api';
36
- import {useInfiniteQuery, useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
38
+ import {
39
+ createSlackInstallResponseSchema,
40
+ slackCallbackResponseSchema,
41
+ } from '@shipfox/api-integration-slack-dto';
42
+ import {checkedApiRequest, emptyResponseSchema} from '@shipfox/client-api';
43
+ import {
44
+ type FetchQueryOptions,
45
+ type InfiniteData,
46
+ infiniteQueryOptions,
47
+ queryOptions,
48
+ type UseInfiniteQueryOptions,
49
+ useInfiniteQuery,
50
+ useMutation,
51
+ useQuery,
52
+ useQueryClient,
53
+ } from '@tanstack/react-query';
54
+ import {
55
+ type InstallRedirect,
56
+ type IntegrationConnection,
57
+ type IntegrationProvider,
58
+ isUsableConnection,
59
+ type RepositoryPage,
60
+ } from '#core/models.js';
37
61
  import {serializeLinearCallbackQuery} from '#linear-callback.js';
38
62
  import {serializeSlackCallbackQuery} from '#slack-callback.js';
63
+ import {
64
+ toInstallRedirect,
65
+ toIntegrationConnection,
66
+ toIntegrationProvider,
67
+ toRepository,
68
+ } from './integration-mapper.js';
39
69
 
40
70
  export const integrationsQueryKeys = {
41
71
  all: ['integrations'] as const,
@@ -54,18 +84,62 @@ export const integrationsQueryKeys = {
54
84
  [...integrationsQueryKeys.all, 'repositories', connectionId, search] as const,
55
85
  };
56
86
 
87
+ type SourceConnectionsQueryKey =
88
+ | ReturnType<typeof integrationsQueryKeys.sourceConnections>
89
+ | readonly ['integrations', 'source-connections'];
90
+
91
+ type SourceConnectionsQueryOptions = FetchQueryOptions<
92
+ IntegrationConnection[],
93
+ Error,
94
+ IntegrationConnection[],
95
+ SourceConnectionsQueryKey
96
+ >;
97
+
98
+ type ProvidersQueryKey = ReturnType<typeof integrationsQueryKeys.providers>;
99
+
100
+ type ProvidersQueryOptions = FetchQueryOptions<
101
+ IntegrationProvider[],
102
+ Error,
103
+ IntegrationProvider[],
104
+ ProvidersQueryKey
105
+ >;
106
+
107
+ type ConnectionsQueryKey =
108
+ | ReturnType<typeof integrationsQueryKeys.connections>
109
+ | readonly ['integrations', 'connections'];
110
+
111
+ type ConnectionsQueryOptions = FetchQueryOptions<
112
+ IntegrationConnection[],
113
+ Error,
114
+ IntegrationConnection[],
115
+ ConnectionsQueryKey
116
+ >;
117
+
118
+ type RepositoriesQueryKey =
119
+ | ReturnType<typeof integrationsQueryKeys.repositories>
120
+ | readonly ['integrations', 'repositories'];
121
+
122
+ type RepositoriesInfiniteQueryOptions = UseInfiniteQueryOptions<
123
+ RepositoryPage,
124
+ Error,
125
+ InfiniteData<RepositoryPage, string | undefined>,
126
+ RepositoriesQueryKey,
127
+ string | undefined
128
+ >;
129
+
57
130
  export async function listIntegrationProviders({
58
131
  capability,
59
132
  signal,
60
133
  }: {
61
134
  capability?: IntegrationCapabilityDto;
62
135
  signal?: AbortSignal;
63
- }) {
136
+ }): Promise<IntegrationProvider[]> {
64
137
  const search = new URLSearchParams();
65
138
  if (capability) search.set('capability', capability);
66
139
  const query = search.toString();
67
140
  const path = query ? `/integration-providers?${query}` : '/integration-providers';
68
- return await apiRequest<ListIntegrationProvidersResponseDto>(path, {signal});
141
+ const response = await checkedApiRequest(listIntegrationProvidersResponseSchema, path, {signal});
142
+ return response.providers.map(toIntegrationProvider);
69
143
  }
70
144
 
71
145
  export async function listIntegrationConnections({
@@ -76,13 +150,15 @@ export async function listIntegrationConnections({
76
150
  workspaceId: string;
77
151
  capability?: IntegrationCapabilityDto | undefined;
78
152
  signal?: AbortSignal | undefined;
79
- }) {
153
+ }): Promise<IntegrationConnection[]> {
80
154
  const search = new URLSearchParams({workspace_id: workspaceId});
81
155
  if (capability) search.set('capability', capability);
82
- return await apiRequest<ListIntegrationConnectionsResponseDto>(
156
+ const response = await checkedApiRequest(
157
+ listIntegrationConnectionsResponseSchema,
83
158
  `/integration-connections?${search.toString()}`,
84
159
  {signal},
85
160
  );
161
+ return response.connections.map(toIntegrationConnection);
86
162
  }
87
163
 
88
164
  export async function listSourceConnections({
@@ -91,8 +167,8 @@ export async function listSourceConnections({
91
167
  }: {
92
168
  workspaceId: string;
93
169
  signal?: AbortSignal;
94
- }) {
95
- const result = await listIntegrationConnections({
170
+ }): Promise<IntegrationConnection[]> {
171
+ const connections = await listIntegrationConnections({
96
172
  workspaceId,
97
173
  capability: 'source_control',
98
174
  signal,
@@ -100,47 +176,100 @@ export async function listSourceConnections({
100
176
  // The endpoint returns every lifecycle status (the settings hub needs that),
101
177
  // but source-control consumers (onboarding redirect, project creation) only
102
178
  // act on usable connections — a disabled/error one must read as "not there".
103
- return {
104
- ...result,
105
- connections: result.connections.filter(
106
- (connection) => connection.lifecycle_status === 'active',
107
- ),
108
- };
179
+ return connections.filter(isUsableConnection);
109
180
  }
110
181
 
111
- export async function createGiteaConnection(body: CreateGiteaConnectionBodyDto) {
112
- return await apiRequest<CreateGiteaConnectionResponseDto>('/integrations/gitea/connections', {
113
- method: 'POST',
114
- body,
182
+ export function sourceConnectionsQueryOptions(
183
+ workspaceId: string | undefined,
184
+ ): SourceConnectionsQueryOptions {
185
+ return queryOptions({
186
+ queryKey: workspaceId
187
+ ? integrationsQueryKeys.sourceConnections(workspaceId)
188
+ : ([...integrationsQueryKeys.all, 'source-connections'] as const),
189
+ enabled: Boolean(workspaceId),
190
+ queryFn: ({signal}) => listSourceConnections({workspaceId: workspaceId ?? '', signal}),
115
191
  });
116
192
  }
117
193
 
118
- export async function createGithubInstall(body: CreateGithubInstallBodyDto) {
119
- return await apiRequest<CreateGithubInstallResponseDto>('/integrations/github/install', {
120
- method: 'POST',
121
- body,
122
- });
194
+ export async function createGiteaConnection(
195
+ body: CreateGiteaConnectionBodyDto,
196
+ ): Promise<IntegrationConnection> {
197
+ const response = await checkedApiRequest(
198
+ createGiteaConnectionResponseSchema,
199
+ '/integrations/gitea/connections',
200
+ {
201
+ method: 'POST',
202
+ body,
203
+ },
204
+ );
205
+ return toIntegrationConnection(response);
123
206
  }
124
207
 
125
- export async function createSentryInstall(body: CreateSentryInstallBodyDto) {
126
- return await apiRequest<CreateSentryInstallResponseDto>('/integrations/sentry/install', {
127
- method: 'POST',
128
- body,
129
- });
208
+ export async function createGithubInstall(
209
+ body: CreateGithubInstallBodyDto,
210
+ ): Promise<InstallRedirect> {
211
+ return toInstallRedirect(
212
+ await checkedApiRequest(createGithubInstallResponseSchema, '/integrations/github/install', {
213
+ method: 'POST',
214
+ body,
215
+ }),
216
+ );
130
217
  }
131
218
 
132
- export async function createLinearInstall(body: CreateLinearInstallBodyDto) {
133
- return await apiRequest<CreateLinearInstallResponseDto>('/integrations/linear/install', {
134
- method: 'POST',
135
- body,
136
- });
219
+ export async function completeGithubCallback({
220
+ code,
221
+ installationId,
222
+ state,
223
+ setupAction,
224
+ token,
225
+ }: {
226
+ code: string;
227
+ installationId: number;
228
+ state: string;
229
+ setupAction?: string;
230
+ token: string;
231
+ }): Promise<IntegrationConnection> {
232
+ const query = new URLSearchParams({code, installation_id: String(installationId), state});
233
+ if (setupAction) query.set('setup_action', setupAction);
234
+ const response = await checkedApiRequest(
235
+ githubCallbackResponseSchema,
236
+ `/integrations/github/callback/api?${query.toString()}`,
237
+ {headers: {authorization: `Bearer ${token}`}},
238
+ );
239
+ return toIntegrationConnection(response);
137
240
  }
138
241
 
139
- export async function createSlackInstall(body: CreateSlackInstallBodyDto) {
140
- return await apiRequest<CreateSlackInstallResponseDto>('/integrations/slack/install', {
141
- method: 'POST',
142
- body,
143
- });
242
+ export async function createSentryInstall(
243
+ body: CreateSentryInstallBodyDto,
244
+ ): Promise<InstallRedirect> {
245
+ return toInstallRedirect(
246
+ await checkedApiRequest(createSentryInstallResponseSchema, '/integrations/sentry/install', {
247
+ method: 'POST',
248
+ body,
249
+ }),
250
+ );
251
+ }
252
+
253
+ export async function createLinearInstall(
254
+ body: CreateLinearInstallBodyDto,
255
+ ): Promise<InstallRedirect> {
256
+ return toInstallRedirect(
257
+ await checkedApiRequest(createLinearInstallResponseSchema, '/integrations/linear/install', {
258
+ method: 'POST',
259
+ body,
260
+ }),
261
+ );
262
+ }
263
+
264
+ export async function createSlackInstall(
265
+ body: CreateSlackInstallBodyDto,
266
+ ): Promise<InstallRedirect> {
267
+ return toInstallRedirect(
268
+ await checkedApiRequest(createSlackInstallResponseSchema, '/integrations/slack/install', {
269
+ method: 'POST',
270
+ body,
271
+ }),
272
+ );
144
273
  }
145
274
 
146
275
  export async function completeLinearCallback({
@@ -149,11 +278,13 @@ export async function completeLinearCallback({
149
278
  }: {
150
279
  query: LinearCallbackQueryDto;
151
280
  token: string;
152
- }) {
153
- return await apiRequest<LinearCallbackResponseDto>(
281
+ }): Promise<IntegrationConnection> {
282
+ const response = await checkedApiRequest(
283
+ linearCallbackResponseSchema,
154
284
  `/integrations/linear/callback/api?${serializeLinearCallbackQuery(query)}`,
155
285
  {headers: {authorization: `Bearer ${token}`}},
156
286
  );
287
+ return toIntegrationConnection(response);
157
288
  }
158
289
 
159
290
  export async function completeSlackCallback({
@@ -162,21 +293,28 @@ export async function completeSlackCallback({
162
293
  }: {
163
294
  query: SlackCallbackQueryDto;
164
295
  token: string;
165
- }) {
166
- return await apiRequest<SlackCallbackResponseDto>(
296
+ }): Promise<IntegrationConnection> {
297
+ const response = await checkedApiRequest(
298
+ slackCallbackResponseSchema,
167
299
  `/integrations/slack/callback/api?${serializeSlackCallbackQuery(query)}`,
168
300
  {headers: {authorization: `Bearer ${token}`}},
169
301
  );
302
+ return toIntegrationConnection(response);
170
303
  }
171
304
 
172
305
  // Called from the callback route with an explicit bearer (same as the GitHub
173
306
  // callback): the route refreshes auth itself before forwarding the grant code.
174
307
  export async function connectSentry({body, token}: {body: SentryConnectBodyDto; token: string}) {
175
- return await apiRequest<SentryConnectResponseDto>('/integrations/sentry/connect', {
176
- method: 'POST',
177
- body,
178
- headers: {authorization: `Bearer ${token}`},
179
- });
308
+ const response = await checkedApiRequest(
309
+ sentryConnectResponseSchema,
310
+ '/integrations/sentry/connect',
311
+ {
312
+ method: 'POST',
313
+ body,
314
+ headers: {authorization: `Bearer ${token}`},
315
+ },
316
+ );
317
+ return toIntegrationConnection(response);
180
318
  }
181
319
 
182
320
  export async function listRepositories({
@@ -189,7 +327,7 @@ export async function listRepositories({
189
327
  cursor?: string;
190
328
  search?: string;
191
329
  signal?: AbortSignal;
192
- }) {
330
+ }): Promise<RepositoryPage> {
193
331
  const params = new URLSearchParams();
194
332
  if (cursor) params.set('cursor', cursor);
195
333
  if (search) params.set('search', search);
@@ -197,7 +335,11 @@ export async function listRepositories({
197
335
  const path = query
198
336
  ? `/integration-connections/${connectionId}/repositories?${query}`
199
337
  : `/integration-connections/${connectionId}/repositories`;
200
- return await apiRequest<ListRepositoriesResponseDto>(path, {signal});
338
+ const response = await checkedApiRequest(listRepositoriesResponseSchema, path, {signal});
339
+ return {
340
+ repositories: response.repositories.map(toRepository),
341
+ ...(response.next_cursor == null ? {} : {nextCursor: response.next_cursor}),
342
+ };
201
343
  }
202
344
 
203
345
  export async function updateIntegrationConnection({
@@ -206,56 +348,67 @@ export async function updateIntegrationConnection({
206
348
  }: {
207
349
  connectionId: string;
208
350
  body: UpdateIntegrationConnectionBodyDto;
209
- }) {
210
- return await apiRequest<IntegrationConnectionDto>(
351
+ }): Promise<IntegrationConnection> {
352
+ const response = await checkedApiRequest(
353
+ integrationConnectionDtoSchema,
211
354
  `/integration-connections/${encodeURIComponent(connectionId)}`,
212
355
  {method: 'PATCH', body},
213
356
  );
357
+ return toIntegrationConnection(response);
214
358
  }
215
359
 
216
360
  export async function deleteIntegrationConnection({connectionId}: {connectionId: string}) {
217
- await apiRequest<void>(`/integration-connections/${encodeURIComponent(connectionId)}`, {
218
- method: 'DELETE',
219
- });
361
+ await checkedApiRequest(
362
+ emptyResponseSchema,
363
+ `/integration-connections/${encodeURIComponent(connectionId)}`,
364
+ {
365
+ method: 'DELETE',
366
+ },
367
+ );
220
368
  }
221
369
 
222
370
  export function useIntegrationProvidersQuery(params?: {capability?: IntegrationCapabilityDto}) {
223
- const capability = params?.capability;
224
- return useQuery({
371
+ return useQuery(integrationProvidersQueryOptions(params?.capability));
372
+ }
373
+
374
+ export function integrationProvidersQueryOptions(
375
+ capability?: IntegrationCapabilityDto,
376
+ ): ProvidersQueryOptions {
377
+ return queryOptions({
225
378
  queryKey: integrationsQueryKeys.providers(capability ?? 'all'),
226
379
  queryFn: ({signal}) => listIntegrationProviders(capability ? {capability, signal} : {signal}),
227
380
  });
228
381
  }
229
382
 
230
383
  export function useSourceConnectionsQuery(workspaceId: string | undefined) {
231
- return useQuery({
232
- queryKey: workspaceId
233
- ? integrationsQueryKeys.sourceConnections(workspaceId)
234
- : [...integrationsQueryKeys.all, 'source-connections'],
235
- enabled: Boolean(workspaceId),
236
- queryFn: ({signal}) => listSourceConnections({workspaceId: workspaceId ?? '', signal}),
237
- });
384
+ return useQuery(sourceConnectionsQueryOptions(workspaceId));
238
385
  }
239
386
 
240
387
  export function useIntegrationConnectionsQuery(workspaceId: string | undefined) {
241
- return useQuery({
388
+ return useQuery(integrationConnectionsQueryOptions(workspaceId));
389
+ }
390
+
391
+ export function integrationConnectionsQueryOptions(
392
+ workspaceId: string | undefined,
393
+ ): ConnectionsQueryOptions {
394
+ return queryOptions({
242
395
  queryKey: workspaceId
243
396
  ? integrationsQueryKeys.connections(workspaceId, 'all')
244
- : [...integrationsQueryKeys.all, 'connections'],
397
+ : ([...integrationsQueryKeys.all, 'connections'] as const),
245
398
  enabled: Boolean(workspaceId),
246
399
  queryFn: ({signal}) => listIntegrationConnections({workspaceId: workspaceId ?? '', signal}),
247
400
  });
248
401
  }
249
402
 
250
- export function useRepositoriesInfiniteQuery(
403
+ export function repositoriesInfiniteQueryOptions(
251
404
  connectionId: string | undefined,
252
405
  options?: {search?: string},
253
- ) {
406
+ ): RepositoriesInfiniteQueryOptions {
254
407
  const trimmedSearch = options?.search?.trim() ?? '';
255
- return useInfiniteQuery({
408
+ return infiniteQueryOptions({
256
409
  queryKey: connectionId
257
410
  ? integrationsQueryKeys.repositories(connectionId, trimmedSearch)
258
- : [...integrationsQueryKeys.all, 'repositories'],
411
+ : ([...integrationsQueryKeys.all, 'repositories'] as const),
259
412
  enabled: Boolean(connectionId),
260
413
  initialPageParam: undefined as string | undefined,
261
414
  queryFn: ({pageParam, signal}) => {
@@ -267,12 +420,28 @@ export function useRepositoriesInfiniteQuery(
267
420
  if (trimmedSearch) args.search = trimmedSearch;
268
421
  return listRepositories(args);
269
422
  },
270
- getNextPageParam: (lastPage) => lastPage.next_cursor ?? undefined,
423
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
271
424
  });
272
425
  }
273
426
 
427
+ export function useRepositoriesInfiniteQuery(
428
+ connectionId: string | undefined,
429
+ options?: {search?: string},
430
+ ) {
431
+ return useInfiniteQuery(repositoriesInfiniteQueryOptions(connectionId, options));
432
+ }
433
+
274
434
  export function useCreateGiteaConnectionMutation() {
275
- return useMutation({mutationFn: createGiteaConnection});
435
+ const queryClient = useQueryClient();
436
+ return useMutation({
437
+ mutationFn: createGiteaConnection,
438
+ onSuccess: async (connection) => {
439
+ await queryClient.invalidateQueries({
440
+ queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspaceId),
441
+ refetchType: 'all',
442
+ });
443
+ },
444
+ });
276
445
  }
277
446
 
278
447
  export function useCreateLinearInstallMutation() {
@@ -297,7 +466,7 @@ export function useUpdateIntegrationConnectionMutation() {
297
466
  mutationFn: updateIntegrationConnection,
298
467
  onSuccess: async (connection) => {
299
468
  await queryClient.invalidateQueries({
300
- queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspace_id),
469
+ queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspaceId),
301
470
  });
302
471
  },
303
472
  });