@shipfox/client-integrations 5.0.0 → 6.0.2

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 (187) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +84 -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 +7 -0
  61. package/dist/feature.d.ts.map +1 -1
  62. package/dist/feature.js +10 -9
  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 +45 -240
  69. package/dist/hooks/api/integrations.d.ts.map +1 -1
  70. package/dist/hooks/api/integrations.js +78 -31
  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 +17 -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/linear-callback.d.ts +2 -0
  116. package/dist/routes/linear-callback.d.ts.map +1 -1
  117. package/dist/routes/linear-callback.js +2 -0
  118. package/dist/routes/linear-callback.js.map +1 -1
  119. package/dist/routes/sentry-callback.d.ts +2 -0
  120. package/dist/routes/sentry-callback.d.ts.map +1 -1
  121. package/dist/routes/sentry-callback.js +2 -0
  122. package/dist/routes/sentry-callback.js.map +1 -1
  123. package/dist/routes/slack-callback.d.ts +2 -0
  124. package/dist/routes/slack-callback.d.ts.map +1 -1
  125. package/dist/routes/slack-callback.js +2 -0
  126. package/dist/routes/slack-callback.js.map +1 -1
  127. package/dist/sentry-callback.d.ts +2 -1
  128. package/dist/sentry-callback.d.ts.map +1 -1
  129. package/dist/sentry-callback.js +18 -19
  130. package/dist/sentry-callback.js.map +1 -1
  131. package/dist/slack-callback.d.ts +2 -1
  132. package/dist/slack-callback.d.ts.map +1 -1
  133. package/dist/slack-callback.js +17 -15
  134. package/dist/slack-callback.js.map +1 -1
  135. package/dist/tsconfig.test.tsbuildinfo +1 -1
  136. package/package.json +17 -17
  137. package/src/application/complete-integration-callback.test.ts +41 -0
  138. package/src/application/complete-integration-callback.ts +39 -0
  139. package/src/components/connection-picker.tsx +7 -7
  140. package/src/components/installed-integrations-section.tsx +29 -24
  141. package/src/components/integration-gallery-for-workspace.tsx +23 -16
  142. package/src/components/integration-gallery.tsx +3 -3
  143. package/src/components/integration-usage-events.ts +1 -116
  144. package/src/components/integration-usage-modal.tsx +4 -4
  145. package/src/components/provider-grid.tsx +30 -18
  146. package/src/components/redirect-install-page.test.tsx +2 -6
  147. package/src/components/redirect-install-page.tsx +3 -2
  148. package/src/components/repository-picker.tsx +6 -6
  149. package/src/components/webhook/webhook-create-modal.tsx +12 -11
  150. package/src/components/webhook/webhook-modals.stories.tsx +10 -10
  151. package/src/components/webhook/webhook-usage-details.tsx +2 -4
  152. package/src/connection-status-badge.tsx +8 -14
  153. package/src/core/models.ts +200 -0
  154. package/src/feature.ts +12 -10
  155. package/src/hooks/api/integration-mapper.ts +64 -0
  156. package/src/hooks/api/integrations.test.ts +44 -22
  157. package/src/hooks/api/integrations.ts +229 -84
  158. package/src/hooks/api/webhook-connections.ts +58 -17
  159. package/src/index.ts +2 -1
  160. package/src/linear-callback.ts +24 -16
  161. package/src/pages/gitea-install-page.tsx +1 -10
  162. package/src/pages/linear-callback-page.tsx +18 -30
  163. package/src/pages/linear-install-page.tsx +2 -1
  164. package/src/pages/sentry-callback-page.test.tsx +5 -1
  165. package/src/pages/sentry-callback-page.tsx +28 -25
  166. package/src/pages/sentry-install-page.tsx +2 -1
  167. package/src/pages/slack-callback-page.tsx +20 -26
  168. package/src/pages/slack-install-page.tsx +2 -1
  169. package/src/pages/source-control-onboarding-page.tsx +5 -1
  170. package/src/provider-catalog.ts +7 -6
  171. package/src/routes/github-callback.test.ts +35 -1
  172. package/src/routes/github-callback.tsx +47 -21
  173. package/src/routes/linear-callback.tsx +5 -1
  174. package/src/routes/sentry-callback.tsx +5 -1
  175. package/src/routes/slack-callback.tsx +6 -1
  176. package/src/sentry-callback.test.ts +2 -1
  177. package/src/sentry-callback.ts +30 -20
  178. package/src/slack-callback.ts +24 -16
  179. package/tsconfig.build.tsbuildinfo +1 -1
  180. package/vercel.json +5 -0
  181. package/dist/integration-icon.d.ts +0 -17
  182. package/dist/integration-icon.d.ts.map +0 -1
  183. package/dist/integration-icon.js +0 -22
  184. package/dist/integration-icon.js.map +0 -1
  185. package/src/integration-icon.react.test.tsx +0 -40
  186. package/src/integration-icon.test.ts +0 -19
  187. package/src/integration-icon.tsx +0 -26
@@ -1,5 +1,10 @@
1
1
  import {ApiError} from '@shipfox/client-api';
2
- import {githubCallbackErrorMessage} from './github-callback.js';
2
+ import {
3
+ githubCallbackErrorMessage,
4
+ githubCallbackParams,
5
+ missingCallbackParams,
6
+ validateGithubCallbackSearch,
7
+ } from './github-callback.js';
3
8
 
4
9
  describe('githubCallbackErrorMessage', () => {
5
10
  it('does not expose an API error message or request URL', () => {
@@ -15,3 +20,32 @@ describe('githubCallbackErrorMessage', () => {
15
20
  expect(message).not.toContain('https://');
16
21
  });
17
22
  });
23
+
24
+ describe('github callback route inputs', () => {
25
+ it('normalizes valid values and reports missing or malformed inputs', () => {
26
+ const search = validateGithubCallbackSearch({
27
+ code: 'grant-code',
28
+ installation_id: '42',
29
+ state: 'signed-state',
30
+ setup_action: 'install',
31
+ });
32
+
33
+ expect(search).toEqual({
34
+ code: 'grant-code',
35
+ installationId: 42,
36
+ state: 'signed-state',
37
+ setupAction: 'install',
38
+ });
39
+ expect(githubCallbackParams(search)).toEqual({
40
+ code: 'grant-code',
41
+ installationId: 42,
42
+ state: 'signed-state',
43
+ setupAction: 'install',
44
+ });
45
+ expect(missingCallbackParams(validateGithubCallbackSearch({code: ['bad'], state: 7}))).toEqual([
46
+ 'code',
47
+ 'installation_id',
48
+ 'state',
49
+ ]);
50
+ });
51
+ });
@@ -1,30 +1,43 @@
1
- import {ApiError, apiRequest} from '@shipfox/client-api';
1
+ import {ApiError} from '@shipfox/client-api';
2
2
  import {useRefreshAuth} from '@shipfox/client-auth';
3
- import {defineRoute} from '@shipfox/client-shell/runtime';
3
+ import {defineRoute, useRouteSearch} from '@shipfox/client-shell/runtime';
4
4
  import {createSingleFlight} from '@shipfox/client-ui';
5
5
  import {FullPageLoader} from '@shipfox/react-ui/loader';
6
6
  import {toast} from '@shipfox/react-ui/toast';
7
- import {useNavigate, useSearch} from '@tanstack/react-router';
7
+ import {useNavigate} from '@tanstack/react-router';
8
8
  import {useEffect} from 'react';
9
+ import {useCompleteIntegrationCallback} from '#application/complete-integration-callback.js';
10
+ import {completeGithubCallback} from '#hooks/api/integrations.js';
9
11
 
10
- interface GithubCallbackParams {
12
+ export interface GithubCallbackParams {
11
13
  code: string;
12
14
  installationId: number;
13
15
  state: string;
14
16
  setupAction?: string;
15
17
  }
16
18
 
19
+ export interface GithubCallbackSearch {
20
+ code?: string;
21
+ installationId?: number;
22
+ state?: string;
23
+ setupAction?: string;
24
+ }
25
+
17
26
  // Retain only recent completions: this bounds long-lived callback pages while
18
27
  // still covering StrictMode and immediate Back/Forward remounts.
19
28
  const callbackRequests = createSingleFlight<string, void>({maxTerminalResults: 32});
20
29
  const toastedCallbacks = new Set<string>();
21
30
 
22
- export default defineRoute({component: GithubCallbackRoute});
31
+ export default defineRoute({
32
+ validateSearch: validateGithubCallbackSearch,
33
+ component: GithubCallbackRoute,
34
+ });
23
35
 
24
36
  function GithubCallbackRoute() {
25
- const search = useSearch({strict: false});
37
+ const search = useRouteSearch(validateGithubCallbackSearch);
26
38
  const navigate = useNavigate();
27
39
  const refreshAuth = useRefreshAuth();
40
+ const completeIntegrationCallback = useCompleteIntegrationCallback();
28
41
 
29
42
  useEffect(() => {
30
43
  const missing = missingCallbackParams(search);
@@ -46,11 +59,11 @@ function GithubCallbackRoute() {
46
59
  const request = callbackRequests.run(
47
60
  key,
48
61
  async () =>
49
- await refreshAuth().then(async (session) => {
50
- await apiRequest(`/integrations/github/callback/api?${key}`, {
51
- headers: {authorization: `Bearer ${session.accessToken}`},
52
- });
53
- }),
62
+ void (await completeIntegrationCallback({
63
+ input: params,
64
+ refreshAuth,
65
+ complete: async (input, token) => await completeGithubCallback({...input, token}),
66
+ })),
54
67
  );
55
68
  request
56
69
  .then(() => {
@@ -72,24 +85,37 @@ function GithubCallbackRoute() {
72
85
  return () => {
73
86
  disposed = true;
74
87
  };
75
- }, [navigate, refreshAuth, search]);
88
+ }, [completeIntegrationCallback, navigate, refreshAuth, search]);
76
89
 
77
90
  return <FullPageLoader />;
78
91
  }
79
92
 
80
- function missingCallbackParams(search: Record<string, unknown>): string[] {
93
+ export function validateGithubCallbackSearch(input: Record<string, unknown>): GithubCallbackSearch {
94
+ const code = stringParam(input.code);
95
+ const installationId = numberParam(input.installation_id);
96
+ const state = stringParam(input.state);
97
+ const setupAction = stringParam(input.setup_action);
98
+ return {
99
+ ...(code ? {code} : {}),
100
+ ...(installationId === undefined ? {} : {installationId}),
101
+ ...(state ? {state} : {}),
102
+ ...(setupAction ? {setupAction} : {}),
103
+ };
104
+ }
105
+
106
+ export function missingCallbackParams(search: GithubCallbackSearch): string[] {
81
107
  const missing: string[] = [];
82
- if (!stringParam(search.code)) missing.push('code');
83
- if (!numberParam(search.installation_id)) missing.push('installation_id');
84
- if (!stringParam(search.state)) missing.push('state');
108
+ if (!search.code) missing.push('code');
109
+ if (search.installationId === undefined) missing.push('installation_id');
110
+ if (!search.state) missing.push('state');
85
111
  return missing;
86
112
  }
87
- function githubCallbackParams(search: Record<string, unknown>): GithubCallbackParams | undefined {
88
- const code = stringParam(search.code);
89
- const installationId = numberParam(search.installation_id);
90
- const state = stringParam(search.state);
113
+ export function githubCallbackParams(
114
+ search: GithubCallbackSearch,
115
+ ): GithubCallbackParams | undefined {
116
+ const {code, installationId, state} = search;
91
117
  if (!code || installationId === undefined || !state) return undefined;
92
- const setupAction = stringParam(search.setup_action);
118
+ const {setupAction} = search;
93
119
  return setupAction ? {code, installationId, state, setupAction} : {code, installationId, state};
94
120
  }
95
121
  function encodeCallbackQuery(params: GithubCallbackParams): string {
@@ -1,4 +1,8 @@
1
1
  import {defineRoute} from '@shipfox/client-shell/runtime';
2
2
  import {LinearCallbackPage} from '#pages/linear-callback-page.js';
3
+ import {parseLinearCallbackQuery} from '../linear-callback.js';
3
4
 
4
- export default defineRoute({component: LinearCallbackPage});
5
+ export default defineRoute({
6
+ validateSearch: parseLinearCallbackQuery,
7
+ component: LinearCallbackPage,
8
+ });
@@ -1,4 +1,8 @@
1
1
  import {defineRoute} from '@shipfox/client-shell/runtime';
2
2
  import {SentryCallbackPage} from '#pages/sentry-callback-page.js';
3
+ import {parseSentryCallbackParams} from '../sentry-callback.js';
3
4
 
4
- export default defineRoute({component: SentryCallbackPage});
5
+ export default defineRoute({
6
+ validateSearch: parseSentryCallbackParams,
7
+ component: SentryCallbackPage,
8
+ });
@@ -1,3 +1,8 @@
1
1
  import {defineRoute} from '@shipfox/client-shell/runtime';
2
2
  import {SlackCallbackPage} from '#pages/slack-callback-page.js';
3
- export default defineRoute({component: SlackCallbackPage});
3
+ import {parseSlackCallbackQuery} from '../slack-callback.js';
4
+
5
+ export default defineRoute({
6
+ validateSearch: parseSlackCallbackQuery,
7
+ component: SlackCallbackPage,
8
+ });
@@ -181,7 +181,8 @@ describe('classifySentryConnectError', () => {
181
181
 
182
182
  expect(result).toEqual({
183
183
  kind: 'terminal',
184
- message: 'Sentry rejected the code.',
184
+ message:
185
+ 'Sentry did not accept this install. Start the install again from workspace settings.',
185
186
  startOver: true,
186
187
  });
187
188
  });
@@ -1,34 +1,41 @@
1
1
  import {ApiError} from '@shipfox/client-api';
2
+ import {
3
+ type BrowserStorage,
4
+ type BrowserStorageKey,
5
+ createTypedBrowserStorage,
6
+ } from '@shipfox/client-ui';
2
7
 
3
8
  export const SENTRY_INSTALL_WORKSPACE_KEY = 'shipfox.sentry-install.workspace-id';
4
9
 
5
- type WorkspaceStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
10
+ type WorkspaceStorage = BrowserStorage | undefined;
11
+
12
+ const sentryInstallWorkspaceStorageKey = {
13
+ key: SENTRY_INSTALL_WORKSPACE_KEY,
14
+ lifetime: 'session',
15
+ // This is a one-shot navigation hint, not private workspace state. The
16
+ // callback validates it against the authenticated workspace list before use.
17
+ principalScope: 'global',
18
+ serialize: (workspaceId: string) => workspaceId,
19
+ parse: (value: string) => value || undefined,
20
+ } satisfies BrowserStorageKey<string>;
6
21
 
7
22
  // Storage helpers swallow failures (quota, private mode, disabled storage):
8
23
  // the handoff is an optimization, never a requirement — the callback always
9
24
  // asks for explicit confirmation and only uses the stored id to pre-select.
10
25
  export function saveSentryInstallWorkspace(storage: WorkspaceStorage, workspaceId: string): void {
11
- try {
12
- storage.setItem(SENTRY_INSTALL_WORKSPACE_KEY, workspaceId);
13
- } catch {
14
- // The callback falls back to its sole-workspace pre-selection.
15
- }
26
+ installWorkspaceStorage(storage).write(workspaceId);
16
27
  }
17
28
 
18
29
  export function readSentryInstallWorkspace(storage: WorkspaceStorage): string | undefined {
19
- try {
20
- return storage.getItem(SENTRY_INSTALL_WORKSPACE_KEY) ?? undefined;
21
- } catch {
22
- return undefined;
23
- }
30
+ return installWorkspaceStorage(storage).read();
24
31
  }
25
32
 
26
33
  export function clearSentryInstallWorkspace(storage: WorkspaceStorage): void {
27
- try {
28
- storage.removeItem(SENTRY_INSTALL_WORKSPACE_KEY);
29
- } catch {
30
- // A tab-scoped leftover key is harmless: it only pre-selects.
31
- }
34
+ installWorkspaceStorage(storage).remove();
35
+ }
36
+
37
+ function installWorkspaceStorage(storage: WorkspaceStorage) {
38
+ return createTypedBrowserStorage(() => storage, sentryInstallWorkspaceStorageKey);
32
39
  }
33
40
 
34
41
  export interface SentryCallbackParams {
@@ -108,7 +115,7 @@ export function classifySentryConnectError(error: unknown): SentryConnectFailure
108
115
  return {kind: 'retryable', message: 'Sentry is unreachable. Try again.'};
109
116
  }
110
117
  if (error.status === 0 || error.code === 'network-error') {
111
- // A network failure (apiRequest throws status 0) never reached the
118
+ // A network failure (the client transport reports status 0) never reached the
112
119
  // server, so the grant code is untouched and a plain Retry can recover.
113
120
  // Must precede the status < 500 branch, which would otherwise treat it as
114
121
  // a spent code and force a full restart.
@@ -118,9 +125,12 @@ export function classifySentryConnectError(error: unknown): SentryConnectFailure
118
125
  };
119
126
  }
120
127
  if (error.status < 500) {
121
- // 4xx (access-denied, malformed-provider-response, validation): the grant
122
- // code is spent or rejected — only a fresh install can recover.
123
- return {kind: 'terminal', message: error.message, startOver: true};
128
+ return {
129
+ kind: 'terminal',
130
+ message:
131
+ 'Sentry did not accept this install. Start the install again from workspace settings.',
132
+ startOver: true,
133
+ };
124
134
  }
125
135
  }
126
136
  return {kind: 'retryable', message: 'Could not install Sentry. Try again.'};
@@ -1,29 +1,37 @@
1
1
  import type {SlackCallbackQueryDto} from '@shipfox/api-integration-slack-dto';
2
2
  import {ApiError} from '@shipfox/client-api';
3
+ import {
4
+ type BrowserStorage,
5
+ type BrowserStorageKey,
6
+ createTypedBrowserStorage,
7
+ } from '@shipfox/client-ui';
3
8
 
4
9
  export const SLACK_INSTALL_WORKSPACE_KEY = 'shipfox.slack-install.workspace-id';
5
- type WorkspaceStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
10
+ type WorkspaceStorage = BrowserStorage | undefined;
11
+
12
+ const slackInstallWorkspaceStorageKey = {
13
+ key: SLACK_INSTALL_WORKSPACE_KEY,
14
+ lifetime: 'session',
15
+ // This is a one-shot navigation hint, not private workspace state. It must
16
+ // remain readable on an OAuth callback before workspace hydration completes;
17
+ // the callback state and returned connection are the authoritative checks.
18
+ principalScope: 'global',
19
+ serialize: (workspaceId: string) => workspaceId,
20
+ parse: (value: string) => value || undefined,
21
+ } satisfies BrowserStorageKey<string>;
6
22
 
7
23
  export function saveSlackInstallWorkspace(storage: WorkspaceStorage, workspaceId: string): void {
8
- try {
9
- storage.setItem(SLACK_INSTALL_WORKSPACE_KEY, workspaceId);
10
- } catch {
11
- // Storage only supports recovery navigation.
12
- }
24
+ installWorkspaceStorage(storage).write(workspaceId);
13
25
  }
14
26
  export function readSlackInstallWorkspace(storage: WorkspaceStorage): string | undefined {
15
- try {
16
- return storage.getItem(SLACK_INSTALL_WORKSPACE_KEY) ?? undefined;
17
- } catch {
18
- return undefined;
19
- }
27
+ return installWorkspaceStorage(storage).read();
20
28
  }
21
29
  export function clearSlackInstallWorkspace(storage: WorkspaceStorage): void {
22
- try {
23
- storage.removeItem(SLACK_INSTALL_WORKSPACE_KEY);
24
- } catch {
25
- // A stale recovery hint cannot affect the API callback.
26
- }
30
+ installWorkspaceStorage(storage).remove();
31
+ }
32
+
33
+ function installWorkspaceStorage(storage: WorkspaceStorage) {
34
+ return createTypedBrowserStorage(() => storage, slackInstallWorkspaceStorageKey);
27
35
  }
28
36
 
29
37
  export function parseSlackCallbackQuery(