@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,28 +1,29 @@
1
1
  import { ApiError } from '@shipfox/client-api';
2
+ import { createTypedBrowserStorage } from '@shipfox/client-ui';
2
3
  export const SENTRY_INSTALL_WORKSPACE_KEY = 'shipfox.sentry-install.workspace-id';
4
+ const sentryInstallWorkspaceStorageKey = {
5
+ key: SENTRY_INSTALL_WORKSPACE_KEY,
6
+ lifetime: 'session',
7
+ // This is a one-shot navigation hint, not private workspace state. The
8
+ // callback validates it against the authenticated workspace list before use.
9
+ principalScope: 'global',
10
+ serialize: (workspaceId)=>workspaceId,
11
+ parse: (value)=>value || undefined
12
+ };
3
13
  // Storage helpers swallow failures (quota, private mode, disabled storage):
4
14
  // the handoff is an optimization, never a requirement — the callback always
5
15
  // asks for explicit confirmation and only uses the stored id to pre-select.
6
16
  export function saveSentryInstallWorkspace(storage, workspaceId) {
7
- try {
8
- storage.setItem(SENTRY_INSTALL_WORKSPACE_KEY, workspaceId);
9
- } catch {
10
- // The callback falls back to its sole-workspace pre-selection.
11
- }
17
+ installWorkspaceStorage(storage).write(workspaceId);
12
18
  }
13
19
  export function readSentryInstallWorkspace(storage) {
14
- try {
15
- return storage.getItem(SENTRY_INSTALL_WORKSPACE_KEY) ?? undefined;
16
- } catch {
17
- return undefined;
18
- }
20
+ return installWorkspaceStorage(storage).read();
19
21
  }
20
22
  export function clearSentryInstallWorkspace(storage) {
21
- try {
22
- storage.removeItem(SENTRY_INSTALL_WORKSPACE_KEY);
23
- } catch {
24
- // A tab-scoped leftover key is harmless: it only pre-selects.
25
- }
23
+ installWorkspaceStorage(storage).remove();
24
+ }
25
+ function installWorkspaceStorage(storage) {
26
+ return createTypedBrowserStorage(()=>storage, sentryInstallWorkspaceStorageKey);
26
27
  }
27
28
  export function parseSentryCallbackParams(search) {
28
29
  const code = stringParam(search.code);
@@ -91,7 +92,7 @@ export function classifySentryConnectError(error) {
91
92
  };
92
93
  }
93
94
  if (error.status === 0 || error.code === 'network-error') {
94
- // A network failure (apiRequest throws status 0) never reached the
95
+ // A network failure (the client transport reports status 0) never reached the
95
96
  // server, so the grant code is untouched and a plain Retry can recover.
96
97
  // Must precede the status < 500 branch, which would otherwise treat it as
97
98
  // a spent code and force a full restart.
@@ -101,11 +102,9 @@ export function classifySentryConnectError(error) {
101
102
  };
102
103
  }
103
104
  if (error.status < 500) {
104
- // 4xx (access-denied, malformed-provider-response, validation): the grant
105
- // code is spent or rejected — only a fresh install can recover.
106
105
  return {
107
106
  kind: 'terminal',
108
- message: error.message,
107
+ message: 'Sentry did not accept this install. Start the install again from workspace settings.',
109
108
  startOver: true
110
109
  };
111
110
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sentry-callback.ts"],"sourcesContent":["import {ApiError} from '@shipfox/client-api';\n\nexport const SENTRY_INSTALL_WORKSPACE_KEY = 'shipfox.sentry-install.workspace-id';\n\ntype WorkspaceStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;\n\n// Storage helpers swallow failures (quota, private mode, disabled storage):\n// the handoff is an optimization, never a requirement — the callback always\n// asks for explicit confirmation and only uses the stored id to pre-select.\nexport function saveSentryInstallWorkspace(storage: WorkspaceStorage, workspaceId: string): void {\n try {\n storage.setItem(SENTRY_INSTALL_WORKSPACE_KEY, workspaceId);\n } catch {\n // The callback falls back to its sole-workspace pre-selection.\n }\n}\n\nexport function readSentryInstallWorkspace(storage: WorkspaceStorage): string | undefined {\n try {\n return storage.getItem(SENTRY_INSTALL_WORKSPACE_KEY) ?? undefined;\n } catch {\n return undefined;\n }\n}\n\nexport function clearSentryInstallWorkspace(storage: WorkspaceStorage): void {\n try {\n storage.removeItem(SENTRY_INSTALL_WORKSPACE_KEY);\n } catch {\n // A tab-scoped leftover key is harmless: it only pre-selects.\n }\n}\n\nexport interface SentryCallbackParams {\n code: string;\n installationId: string;\n /** Display-only: rendered in the confirm copy, never sent to the API. */\n orgSlug?: string | undefined;\n}\n\nexport function parseSentryCallbackParams(\n search: Record<string, unknown>,\n): SentryCallbackParams | undefined {\n const code = stringParam(search.code);\n // Sentry documents camelCase params; accept snake_case defensively.\n const installationId = stringParam(search.installationId) ?? stringParam(search.installation_id);\n if (!code || !installationId) return undefined;\n\n const orgSlug = stringParam(search.orgSlug) ?? stringParam(search.org_slug);\n return {code, installationId, orgSlug};\n}\n\nexport interface SentryWorkspaceOption {\n id: string;\n name: string;\n}\n\nexport type SentryWorkspacePreselection =\n | {kind: 'pick'; preselectedId: string | undefined}\n | {kind: 'none'};\n\n// Sentry's redirect carries no state token, so the callback can never prove it\n// belongs to an install the user started — installation always requires an\n// explicit click. The stored id (and a sole workspace) only pre-select.\nexport function preselectSentryWorkspace(\n storedId: string | undefined,\n workspaces: SentryWorkspaceOption[],\n): SentryWorkspacePreselection {\n if (workspaces.length === 0) return {kind: 'none'};\n if (storedId && workspaces.some((workspace) => workspace.id === storedId)) {\n return {kind: 'pick', preselectedId: storedId};\n }\n if (workspaces.length === 1) return {kind: 'pick', preselectedId: workspaces[0]?.id};\n return {kind: 'pick', preselectedId: undefined};\n}\n\nexport type SentryConnectFailure =\n | {kind: 'retryable'; message: string; retryAfterSeconds?: number | undefined}\n | {kind: 'terminal'; message: string; startOver: boolean};\n\nexport function classifySentryConnectError(error: unknown): SentryConnectFailure {\n if (error instanceof ApiError) {\n if (error.code === 'sentry-installation-already-linked') {\n return {\n kind: 'terminal',\n message: 'This Sentry org is already installed in another workspace.',\n // Retrying or starting over would fail identically.\n startOver: false,\n };\n }\n if (error.code === 'sentry-verification-in-progress') {\n // A concurrent signed webhook is still verifying the install. The grant code\n // is untouched, so the existing backoff re-calls and finds the verified row.\n return {\n kind: 'retryable',\n message: 'Finishing Sentry verification. This only takes a moment.',\n retryAfterSeconds: retryAfterSeconds(error.details),\n };\n }\n if (error.code === 'rate-limited') {\n return {\n kind: 'retryable',\n message: 'Sentry is rate limiting requests. Try again in a moment.',\n retryAfterSeconds: retryAfterSeconds(error.details),\n };\n }\n if (error.code === 'timeout' || error.code === 'provider-unavailable') {\n return {kind: 'retryable', message: 'Sentry is unreachable. Try again.'};\n }\n if (error.status === 0 || error.code === 'network-error') {\n // A network failure (apiRequest throws status 0) never reached the\n // server, so the grant code is untouched and a plain Retry can recover.\n // Must precede the status < 500 branch, which would otherwise treat it as\n // a spent code and force a full restart.\n return {\n kind: 'retryable',\n message: 'Could not reach Shipfox. Check your connection and try again.',\n };\n }\n if (error.status < 500) {\n // 4xx (access-denied, malformed-provider-response, validation): the grant\n // code is spent or rejected only a fresh install can recover.\n return {kind: 'terminal', message: error.message, startOver: true};\n }\n }\n return {kind: 'retryable', message: 'Could not install Sentry. Try again.'};\n}\n\nfunction retryAfterSeconds(details: unknown): number | undefined {\n // client-api's toApiError stores the whole response body as ApiError.details,\n // so the structured payload lives one level deeper at details.details (mirrors\n // project-error.ts apiDetails()).\n if (typeof details !== 'object' || details === null) return undefined;\n const inner = (details as Record<string, unknown>).details;\n if (typeof inner !== 'object' || inner === null) return undefined;\n const value = (inner as Record<string, unknown>).retry_after_seconds;\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction stringParam(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n"],"names":["ApiError","SENTRY_INSTALL_WORKSPACE_KEY","saveSentryInstallWorkspace","storage","workspaceId","setItem","readSentryInstallWorkspace","getItem","undefined","clearSentryInstallWorkspace","removeItem","parseSentryCallbackParams","search","code","stringParam","installationId","installation_id","orgSlug","org_slug","preselectSentryWorkspace","storedId","workspaces","length","kind","some","workspace","id","preselectedId","classifySentryConnectError","error","message","startOver","retryAfterSeconds","details","status","inner","value","retry_after_seconds","Number","isFinite"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/sentry-callback.ts"],"sourcesContent":["import {ApiError} from '@shipfox/client-api';\nimport {\n type BrowserStorage,\n type BrowserStorageKey,\n createTypedBrowserStorage,\n} from '@shipfox/client-ui';\n\nexport const SENTRY_INSTALL_WORKSPACE_KEY = 'shipfox.sentry-install.workspace-id';\n\ntype WorkspaceStorage = BrowserStorage | undefined;\n\nconst sentryInstallWorkspaceStorageKey = {\n key: SENTRY_INSTALL_WORKSPACE_KEY,\n lifetime: 'session',\n // This is a one-shot navigation hint, not private workspace state. The\n // callback validates it against the authenticated workspace list before use.\n principalScope: 'global',\n serialize: (workspaceId: string) => workspaceId,\n parse: (value: string) => value || undefined,\n} satisfies BrowserStorageKey<string>;\n\n// Storage helpers swallow failures (quota, private mode, disabled storage):\n// the handoff is an optimization, never a requirement — the callback always\n// asks for explicit confirmation and only uses the stored id to pre-select.\nexport function saveSentryInstallWorkspace(storage: WorkspaceStorage, workspaceId: string): void {\n installWorkspaceStorage(storage).write(workspaceId);\n}\n\nexport function readSentryInstallWorkspace(storage: WorkspaceStorage): string | undefined {\n return installWorkspaceStorage(storage).read();\n}\n\nexport function clearSentryInstallWorkspace(storage: WorkspaceStorage): void {\n installWorkspaceStorage(storage).remove();\n}\n\nfunction installWorkspaceStorage(storage: WorkspaceStorage) {\n return createTypedBrowserStorage(() => storage, sentryInstallWorkspaceStorageKey);\n}\n\nexport interface SentryCallbackParams {\n code: string;\n installationId: string;\n /** Display-only: rendered in the confirm copy, never sent to the API. */\n orgSlug?: string | undefined;\n}\n\nexport function parseSentryCallbackParams(\n search: Record<string, unknown>,\n): SentryCallbackParams | undefined {\n const code = stringParam(search.code);\n // Sentry documents camelCase params; accept snake_case defensively.\n const installationId = stringParam(search.installationId) ?? stringParam(search.installation_id);\n if (!code || !installationId) return undefined;\n\n const orgSlug = stringParam(search.orgSlug) ?? stringParam(search.org_slug);\n return {code, installationId, orgSlug};\n}\n\nexport interface SentryWorkspaceOption {\n id: string;\n name: string;\n}\n\nexport type SentryWorkspacePreselection =\n | {kind: 'pick'; preselectedId: string | undefined}\n | {kind: 'none'};\n\n// Sentry's redirect carries no state token, so the callback can never prove it\n// belongs to an install the user started — installation always requires an\n// explicit click. The stored id (and a sole workspace) only pre-select.\nexport function preselectSentryWorkspace(\n storedId: string | undefined,\n workspaces: SentryWorkspaceOption[],\n): SentryWorkspacePreselection {\n if (workspaces.length === 0) return {kind: 'none'};\n if (storedId && workspaces.some((workspace) => workspace.id === storedId)) {\n return {kind: 'pick', preselectedId: storedId};\n }\n if (workspaces.length === 1) return {kind: 'pick', preselectedId: workspaces[0]?.id};\n return {kind: 'pick', preselectedId: undefined};\n}\n\nexport type SentryConnectFailure =\n | {kind: 'retryable'; message: string; retryAfterSeconds?: number | undefined}\n | {kind: 'terminal'; message: string; startOver: boolean};\n\nexport function classifySentryConnectError(error: unknown): SentryConnectFailure {\n if (error instanceof ApiError) {\n if (error.code === 'sentry-installation-already-linked') {\n return {\n kind: 'terminal',\n message: 'This Sentry org is already installed in another workspace.',\n // Retrying or starting over would fail identically.\n startOver: false,\n };\n }\n if (error.code === 'sentry-verification-in-progress') {\n // A concurrent signed webhook is still verifying the install. The grant code\n // is untouched, so the existing backoff re-calls and finds the verified row.\n return {\n kind: 'retryable',\n message: 'Finishing Sentry verification. This only takes a moment.',\n retryAfterSeconds: retryAfterSeconds(error.details),\n };\n }\n if (error.code === 'rate-limited') {\n return {\n kind: 'retryable',\n message: 'Sentry is rate limiting requests. Try again in a moment.',\n retryAfterSeconds: retryAfterSeconds(error.details),\n };\n }\n if (error.code === 'timeout' || error.code === 'provider-unavailable') {\n return {kind: 'retryable', message: 'Sentry is unreachable. Try again.'};\n }\n if (error.status === 0 || error.code === 'network-error') {\n // A network failure (the client transport reports status 0) never reached the\n // server, so the grant code is untouched and a plain Retry can recover.\n // Must precede the status < 500 branch, which would otherwise treat it as\n // a spent code and force a full restart.\n return {\n kind: 'retryable',\n message: 'Could not reach Shipfox. Check your connection and try again.',\n };\n }\n if (error.status < 500) {\n return {\n kind: 'terminal',\n message:\n 'Sentry did not accept this install. Start the install again from workspace settings.',\n startOver: true,\n };\n }\n }\n return {kind: 'retryable', message: 'Could not install Sentry. Try again.'};\n}\n\nfunction retryAfterSeconds(details: unknown): number | undefined {\n // client-api's toApiError stores the whole response body as ApiError.details,\n // so the structured payload lives one level deeper at details.details (mirrors\n // project-error.ts apiDetails()).\n if (typeof details !== 'object' || details === null) return undefined;\n const inner = (details as Record<string, unknown>).details;\n if (typeof inner !== 'object' || inner === null) return undefined;\n const value = (inner as Record<string, unknown>).retry_after_seconds;\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction stringParam(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n"],"names":["ApiError","createTypedBrowserStorage","SENTRY_INSTALL_WORKSPACE_KEY","sentryInstallWorkspaceStorageKey","key","lifetime","principalScope","serialize","workspaceId","parse","value","undefined","saveSentryInstallWorkspace","storage","installWorkspaceStorage","write","readSentryInstallWorkspace","read","clearSentryInstallWorkspace","remove","parseSentryCallbackParams","search","code","stringParam","installationId","installation_id","orgSlug","org_slug","preselectSentryWorkspace","storedId","workspaces","length","kind","some","workspace","id","preselectedId","classifySentryConnectError","error","message","startOver","retryAfterSeconds","details","status","inner","retry_after_seconds","Number","isFinite"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import type { SlackCallbackQueryDto } from '@shipfox/api-integration-slack-dto';
2
+ import { type BrowserStorage } from '@shipfox/client-ui';
2
3
  export declare const SLACK_INSTALL_WORKSPACE_KEY = "shipfox.slack-install.workspace-id";
3
- type WorkspaceStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
4
+ type WorkspaceStorage = BrowserStorage | undefined;
4
5
  export declare function saveSlackInstallWorkspace(storage: WorkspaceStorage, workspaceId: string): void;
5
6
  export declare function readSlackInstallWorkspace(storage: WorkspaceStorage): string | undefined;
6
7
  export declare function clearSlackInstallWorkspace(storage: WorkspaceStorage): void;
@@ -1 +1 @@
1
- {"version":3,"file":"slack-callback.d.ts","sourceRoot":"","sources":["../src/slack-callback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,oCAAoC,CAAC;AAG9E,eAAO,MAAM,2BAA2B,uCAAuC,CAAC;AAChF,KAAK,gBAAgB,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,YAAY,CAAC,CAAC;AAE5E,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAM9F;AACD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAMvF;AACD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAM1E;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,qBAAqB,GAAG,SAAS,CASnC;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,qBAAqB,GAAG,MAAM,CAShF;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AACF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,CA4E/E"}
1
+ {"version":3,"file":"slack-callback.d.ts","sourceRoot":"","sources":["../src/slack-callback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,oCAAoC,CAAC;AAE9E,OAAO,EACL,KAAK,cAAc,EAGpB,MAAM,oBAAoB,CAAC;AAE5B,eAAO,MAAM,2BAA2B,uCAAuC,CAAC;AAChF,KAAK,gBAAgB,GAAG,cAAc,GAAG,SAAS,CAAC;AAanD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAE9F;AACD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAEvF;AACD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAE1E;AAMD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,qBAAqB,GAAG,SAAS,CASnC;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,qBAAqB,GAAG,MAAM,CAShF;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AACF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,CA4E/E"}
@@ -1,25 +1,27 @@
1
1
  import { ApiError } from '@shipfox/client-api';
2
+ import { createTypedBrowserStorage } from '@shipfox/client-ui';
2
3
  export const SLACK_INSTALL_WORKSPACE_KEY = 'shipfox.slack-install.workspace-id';
4
+ const slackInstallWorkspaceStorageKey = {
5
+ key: SLACK_INSTALL_WORKSPACE_KEY,
6
+ lifetime: 'session',
7
+ // This is a one-shot navigation hint, not private workspace state. It must
8
+ // remain readable on an OAuth callback before workspace hydration completes;
9
+ // the callback state and returned connection are the authoritative checks.
10
+ principalScope: 'global',
11
+ serialize: (workspaceId)=>workspaceId,
12
+ parse: (value)=>value || undefined
13
+ };
3
14
  export function saveSlackInstallWorkspace(storage, workspaceId) {
4
- try {
5
- storage.setItem(SLACK_INSTALL_WORKSPACE_KEY, workspaceId);
6
- } catch {
7
- // Storage only supports recovery navigation.
8
- }
15
+ installWorkspaceStorage(storage).write(workspaceId);
9
16
  }
10
17
  export function readSlackInstallWorkspace(storage) {
11
- try {
12
- return storage.getItem(SLACK_INSTALL_WORKSPACE_KEY) ?? undefined;
13
- } catch {
14
- return undefined;
15
- }
18
+ return installWorkspaceStorage(storage).read();
16
19
  }
17
20
  export function clearSlackInstallWorkspace(storage) {
18
- try {
19
- storage.removeItem(SLACK_INSTALL_WORKSPACE_KEY);
20
- } catch {
21
- // A stale recovery hint cannot affect the API callback.
22
- }
21
+ installWorkspaceStorage(storage).remove();
22
+ }
23
+ function installWorkspaceStorage(storage) {
24
+ return createTypedBrowserStorage(()=>storage, slackInstallWorkspaceStorageKey);
23
25
  }
24
26
  export function parseSlackCallbackQuery(search) {
25
27
  const state = stringParam(search.state);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/slack-callback.ts"],"sourcesContent":["import type {SlackCallbackQueryDto} from '@shipfox/api-integration-slack-dto';\nimport {ApiError} from '@shipfox/client-api';\n\nexport const SLACK_INSTALL_WORKSPACE_KEY = 'shipfox.slack-install.workspace-id';\ntype WorkspaceStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;\n\nexport function saveSlackInstallWorkspace(storage: WorkspaceStorage, workspaceId: string): void {\n try {\n storage.setItem(SLACK_INSTALL_WORKSPACE_KEY, workspaceId);\n } catch {\n // Storage only supports recovery navigation.\n }\n}\nexport function readSlackInstallWorkspace(storage: WorkspaceStorage): string | undefined {\n try {\n return storage.getItem(SLACK_INSTALL_WORKSPACE_KEY) ?? undefined;\n } catch {\n return undefined;\n }\n}\nexport function clearSlackInstallWorkspace(storage: WorkspaceStorage): void {\n try {\n storage.removeItem(SLACK_INSTALL_WORKSPACE_KEY);\n } catch {\n // A stale recovery hint cannot affect the API callback.\n }\n}\n\nexport function parseSlackCallbackQuery(\n search: Record<string, unknown>,\n): SlackCallbackQueryDto | undefined {\n const state = stringParam(search.state);\n if (!state) return undefined;\n const code = stringParam(search.code);\n if (code) return {code, state};\n const error = stringParam(search.error);\n if (!error) return undefined;\n const errorDescription = stringParam(search.error_description);\n return errorDescription ? {error, error_description: errorDescription, state} : {error, state};\n}\n\nexport function serializeSlackCallbackQuery(query: SlackCallbackQueryDto): string {\n const params = new URLSearchParams();\n if ('code' in query) params.set('code', query.code);\n else {\n params.set('error', query.error);\n if (query.error_description) params.set('error_description', query.error_description);\n }\n params.set('state', query.state);\n return params.toString();\n}\n\nexport type SlackCallbackFailure = {\n title: string;\n message: string;\n startOver: boolean;\n signIn: boolean;\n};\nexport function classifySlackCallbackError(error: unknown): SlackCallbackFailure {\n if (error instanceof ApiError) {\n if (error.code === 'invalid-slack-install-state')\n return failure(\n 'Slack install link expired',\n 'Slack install link expired. Start again from workspace settings.',\n true,\n );\n if (error.code === 'slack-install-state-actor-mismatch' || error.code === 'unauthorized')\n return {\n ...failure(\n 'Different Shipfox account',\n 'Different Shipfox account. Sign in with the account that started this install.',\n true,\n ),\n signIn: true,\n };\n if (['not-found', 'forbidden', 'workspace-inactive'].includes(error.code))\n return failure(\n 'Workspace access changed',\n 'You no longer have access to this workspace. Return to Shipfox to continue.',\n false,\n );\n if (\n [\n 'slack-installation-already-linked',\n 'slack-connection-already-linked',\n 'slug-conflict',\n ].includes(error.code)\n )\n return failure(\n 'Slack already linked',\n 'This Slack workspace is already linked and cannot be installed again here.',\n false,\n );\n if (\n [\n 'slack-authorization-scope-mismatch',\n 'slack-oauth-callback-error',\n 'access-denied',\n ].includes(error.code)\n )\n return failure(\n 'Slack permissions needed',\n 'Slack did not authorize the permissions Shipfox needs. Review the consent and start again.',\n true,\n );\n if (\n error.code === 'slack-enterprise-install-unsupported' ||\n error.code === 'slack-token-rotation-unsupported'\n )\n return failure(\n 'Slack install unsupported',\n 'This Slack installation is not supported. Return to Shipfox to continue.',\n false,\n );\n if (error.status === 0 || error.code === 'network-error')\n return failure('Could not reach Shipfox', 'Check your connection and start again.', true);\n if (\n error.status >= 500 ||\n error.status === 429 ||\n ['rate-limited', 'timeout', 'provider-unavailable', 'malformed-provider-response'].includes(\n error.code,\n )\n )\n return failure(\n 'Slack is temporarily unavailable',\n 'Start a new install when Slack is available.',\n true,\n );\n }\n return failure(\n 'Slack install could not be completed',\n 'Could not complete the Slack install. Start again from workspace settings.',\n true,\n );\n}\nfunction failure(title: string, message: string, startOver: boolean): SlackCallbackFailure {\n return {title, message, startOver, signIn: false};\n}\nfunction stringParam(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n"],"names":["ApiError","SLACK_INSTALL_WORKSPACE_KEY","saveSlackInstallWorkspace","storage","workspaceId","setItem","readSlackInstallWorkspace","getItem","undefined","clearSlackInstallWorkspace","removeItem","parseSlackCallbackQuery","search","state","stringParam","code","error","errorDescription","error_description","serializeSlackCallbackQuery","query","params","URLSearchParams","set","toString","classifySlackCallbackError","failure","signIn","includes","status","title","message","startOver","value","length"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/slack-callback.ts"],"sourcesContent":["import type {SlackCallbackQueryDto} from '@shipfox/api-integration-slack-dto';\nimport {ApiError} from '@shipfox/client-api';\nimport {\n type BrowserStorage,\n type BrowserStorageKey,\n createTypedBrowserStorage,\n} from '@shipfox/client-ui';\n\nexport const SLACK_INSTALL_WORKSPACE_KEY = 'shipfox.slack-install.workspace-id';\ntype WorkspaceStorage = BrowserStorage | undefined;\n\nconst slackInstallWorkspaceStorageKey = {\n key: SLACK_INSTALL_WORKSPACE_KEY,\n lifetime: 'session',\n // This is a one-shot navigation hint, not private workspace state. It must\n // remain readable on an OAuth callback before workspace hydration completes;\n // the callback state and returned connection are the authoritative checks.\n principalScope: 'global',\n serialize: (workspaceId: string) => workspaceId,\n parse: (value: string) => value || undefined,\n} satisfies BrowserStorageKey<string>;\n\nexport function saveSlackInstallWorkspace(storage: WorkspaceStorage, workspaceId: string): void {\n installWorkspaceStorage(storage).write(workspaceId);\n}\nexport function readSlackInstallWorkspace(storage: WorkspaceStorage): string | undefined {\n return installWorkspaceStorage(storage).read();\n}\nexport function clearSlackInstallWorkspace(storage: WorkspaceStorage): void {\n installWorkspaceStorage(storage).remove();\n}\n\nfunction installWorkspaceStorage(storage: WorkspaceStorage) {\n return createTypedBrowserStorage(() => storage, slackInstallWorkspaceStorageKey);\n}\n\nexport function parseSlackCallbackQuery(\n search: Record<string, unknown>,\n): SlackCallbackQueryDto | undefined {\n const state = stringParam(search.state);\n if (!state) return undefined;\n const code = stringParam(search.code);\n if (code) return {code, state};\n const error = stringParam(search.error);\n if (!error) return undefined;\n const errorDescription = stringParam(search.error_description);\n return errorDescription ? {error, error_description: errorDescription, state} : {error, state};\n}\n\nexport function serializeSlackCallbackQuery(query: SlackCallbackQueryDto): string {\n const params = new URLSearchParams();\n if ('code' in query) params.set('code', query.code);\n else {\n params.set('error', query.error);\n if (query.error_description) params.set('error_description', query.error_description);\n }\n params.set('state', query.state);\n return params.toString();\n}\n\nexport type SlackCallbackFailure = {\n title: string;\n message: string;\n startOver: boolean;\n signIn: boolean;\n};\nexport function classifySlackCallbackError(error: unknown): SlackCallbackFailure {\n if (error instanceof ApiError) {\n if (error.code === 'invalid-slack-install-state')\n return failure(\n 'Slack install link expired',\n 'Slack install link expired. Start again from workspace settings.',\n true,\n );\n if (error.code === 'slack-install-state-actor-mismatch' || error.code === 'unauthorized')\n return {\n ...failure(\n 'Different Shipfox account',\n 'Different Shipfox account. Sign in with the account that started this install.',\n true,\n ),\n signIn: true,\n };\n if (['not-found', 'forbidden', 'workspace-inactive'].includes(error.code))\n return failure(\n 'Workspace access changed',\n 'You no longer have access to this workspace. Return to Shipfox to continue.',\n false,\n );\n if (\n [\n 'slack-installation-already-linked',\n 'slack-connection-already-linked',\n 'slug-conflict',\n ].includes(error.code)\n )\n return failure(\n 'Slack already linked',\n 'This Slack workspace is already linked and cannot be installed again here.',\n false,\n );\n if (\n [\n 'slack-authorization-scope-mismatch',\n 'slack-oauth-callback-error',\n 'access-denied',\n ].includes(error.code)\n )\n return failure(\n 'Slack permissions needed',\n 'Slack did not authorize the permissions Shipfox needs. Review the consent and start again.',\n true,\n );\n if (\n error.code === 'slack-enterprise-install-unsupported' ||\n error.code === 'slack-token-rotation-unsupported'\n )\n return failure(\n 'Slack install unsupported',\n 'This Slack installation is not supported. Return to Shipfox to continue.',\n false,\n );\n if (error.status === 0 || error.code === 'network-error')\n return failure('Could not reach Shipfox', 'Check your connection and start again.', true);\n if (\n error.status >= 500 ||\n error.status === 429 ||\n ['rate-limited', 'timeout', 'provider-unavailable', 'malformed-provider-response'].includes(\n error.code,\n )\n )\n return failure(\n 'Slack is temporarily unavailable',\n 'Start a new install when Slack is available.',\n true,\n );\n }\n return failure(\n 'Slack install could not be completed',\n 'Could not complete the Slack install. Start again from workspace settings.',\n true,\n );\n}\nfunction failure(title: string, message: string, startOver: boolean): SlackCallbackFailure {\n return {title, message, startOver, signIn: false};\n}\nfunction stringParam(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n"],"names":["ApiError","createTypedBrowserStorage","SLACK_INSTALL_WORKSPACE_KEY","slackInstallWorkspaceStorageKey","key","lifetime","principalScope","serialize","workspaceId","parse","value","undefined","saveSlackInstallWorkspace","storage","installWorkspaceStorage","write","readSlackInstallWorkspace","read","clearSlackInstallWorkspace","remove","parseSlackCallbackQuery","search","state","stringParam","code","error","errorDescription","error_description","serializeSlackCallbackQuery","query","params","URLSearchParams","set","toString","classifySlackCallbackError","failure","signIn","includes","status","title","message","startOver","length"],"mappings":"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"}