@shipfox/client-integrations 12.0.2 → 14.0.0

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 (145) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +52 -0
  3. package/dist/application/complete-integration-callback.d.ts +11 -0
  4. package/dist/application/complete-integration-callback.d.ts.map +1 -1
  5. package/dist/application/complete-integration-callback.js +38 -8
  6. package/dist/application/complete-integration-callback.js.map +1 -1
  7. package/dist/components/callback-status-shell.d.ts +4 -3
  8. package/dist/components/callback-status-shell.d.ts.map +1 -1
  9. package/dist/components/callback-status-shell.js +11 -11
  10. package/dist/components/callback-status-shell.js.map +1 -1
  11. package/dist/components/installed-integrations-section.d.ts +2 -1
  12. package/dist/components/installed-integrations-section.d.ts.map +1 -1
  13. package/dist/components/installed-integrations-section.js +7 -6
  14. package/dist/components/installed-integrations-section.js.map +1 -1
  15. package/dist/components/integration-gallery-for-workspace.d.ts.map +1 -1
  16. package/dist/components/integration-gallery-for-workspace.js +11 -2
  17. package/dist/components/integration-gallery-for-workspace.js.map +1 -1
  18. package/dist/components/integration-gallery.stories.js +10 -9
  19. package/dist/components/integration-gallery.stories.js.map +1 -1
  20. package/dist/components/provider-grid.d.ts +2 -2
  21. package/dist/components/provider-grid.d.ts.map +1 -1
  22. package/dist/components/provider-grid.js +4 -4
  23. package/dist/components/provider-grid.js.map +1 -1
  24. package/dist/components/redirect-install-page.d.ts +1 -1
  25. package/dist/components/redirect-install-page.js +2 -2
  26. package/dist/components/redirect-install-page.js.map +1 -1
  27. package/dist/components/webhook/webhook-modals.stories.js +2 -2
  28. package/dist/components/webhook/webhook-modals.stories.js.map +1 -1
  29. package/dist/core/models.d.ts +6 -0
  30. package/dist/core/models.d.ts.map +1 -1
  31. package/dist/core/models.js +12 -0
  32. package/dist/core/models.js.map +1 -1
  33. package/dist/feature.d.ts +15 -7
  34. package/dist/feature.d.ts.map +1 -1
  35. package/dist/feature.js +17 -7
  36. package/dist/feature.js.map +1 -1
  37. package/dist/hooks/api/integration-mapper.d.ts +3 -1
  38. package/dist/hooks/api/integration-mapper.d.ts.map +1 -1
  39. package/dist/hooks/api/integration-mapper.js +8 -0
  40. package/dist/hooks/api/integration-mapper.js.map +1 -1
  41. package/dist/hooks/api/integrations.d.ts +25 -1
  42. package/dist/hooks/api/integrations.d.ts.map +1 -1
  43. package/dist/hooks/api/integrations.js +45 -2
  44. package/dist/hooks/api/integrations.js.map +1 -1
  45. package/dist/index.d.ts +4 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +4 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/jira-callback-state.d.ts +9 -0
  50. package/dist/jira-callback-state.d.ts.map +1 -0
  51. package/dist/jira-callback-state.js +16 -0
  52. package/dist/jira-callback-state.js.map +1 -0
  53. package/dist/jira-callback.d.ts +11 -0
  54. package/dist/jira-callback.d.ts.map +1 -0
  55. package/dist/jira-callback.js +59 -0
  56. package/dist/jira-callback.js.map +1 -0
  57. package/dist/jira-form-errors.d.ts +8 -0
  58. package/dist/jira-form-errors.d.ts.map +1 -0
  59. package/dist/jira-form-errors.js +55 -0
  60. package/dist/jira-form-errors.js.map +1 -0
  61. package/dist/pages/gitea-install-page.js +4 -4
  62. package/dist/pages/gitea-install-page.js.map +1 -1
  63. package/dist/pages/jira-callback-page.d.ts +2 -0
  64. package/dist/pages/jira-callback-page.d.ts.map +1 -0
  65. package/dist/pages/jira-callback-page.js +341 -0
  66. package/dist/pages/jira-callback-page.js.map +1 -0
  67. package/dist/pages/jira-install-page.d.ts +2 -0
  68. package/dist/pages/jira-install-page.d.ts.map +1 -0
  69. package/dist/pages/jira-install-page.js +26 -0
  70. package/dist/pages/jira-install-page.js.map +1 -0
  71. package/dist/pages/linear-callback-page.d.ts.map +1 -1
  72. package/dist/pages/linear-callback-page.js +58 -12
  73. package/dist/pages/linear-callback-page.js.map +1 -1
  74. package/dist/pages/sentry-callback-page.d.ts.map +1 -1
  75. package/dist/pages/sentry-callback-page.js +22 -9
  76. package/dist/pages/sentry-callback-page.js.map +1 -1
  77. package/dist/pages/slack-callback-page.d.ts.map +1 -1
  78. package/dist/pages/slack-callback-page.js +50 -18
  79. package/dist/pages/slack-callback-page.js.map +1 -1
  80. package/dist/pages/source-control-onboarding-page.js +1 -1
  81. package/dist/pages/source-control-onboarding-page.js.map +1 -1
  82. package/dist/provider-catalog.d.ts +1 -1
  83. package/dist/provider-catalog.d.ts.map +1 -1
  84. package/dist/provider-catalog.js +10 -5
  85. package/dist/provider-catalog.js.map +1 -1
  86. package/dist/routes/jira-callback.d.ts +8 -0
  87. package/dist/routes/jira-callback.d.ts.map +1 -0
  88. package/dist/routes/jira-callback.js +9 -0
  89. package/dist/routes/jira-callback.js.map +1 -0
  90. package/dist/routes/jira.d.ts +6 -0
  91. package/dist/routes/jira.d.ts.map +1 -0
  92. package/dist/routes/jira.js +7 -0
  93. package/dist/routes/jira.js.map +1 -0
  94. package/dist/sentry-callback.js +2 -2
  95. package/dist/sentry-callback.js.map +1 -1
  96. package/dist/tsconfig.test.tsbuildinfo +1 -1
  97. package/dist/workspace-navigation.d.ts +13 -0
  98. package/dist/workspace-navigation.d.ts.map +1 -0
  99. package/dist/workspace-navigation.js +17 -0
  100. package/dist/workspace-navigation.js.map +1 -0
  101. package/package.json +14 -13
  102. package/src/application/complete-integration-callback.test.ts +44 -1
  103. package/src/application/complete-integration-callback.ts +61 -8
  104. package/src/components/callback-status-shell.tsx +17 -10
  105. package/src/components/installed-integrations-section.tsx +16 -9
  106. package/src/components/integration-gallery-for-workspace.tsx +20 -10
  107. package/src/components/integration-gallery.stories.tsx +10 -10
  108. package/src/components/integration-gallery.test.tsx +30 -15
  109. package/src/components/integration-usage-events.test.ts +13 -0
  110. package/src/components/provider-grid.tsx +6 -6
  111. package/src/components/redirect-install-page.test.tsx +3 -3
  112. package/src/components/redirect-install-page.tsx +2 -2
  113. package/src/components/webhook/webhook-modals.stories.tsx +2 -2
  114. package/src/core/models.ts +18 -0
  115. package/src/feature.ts +17 -7
  116. package/src/hooks/api/integration-mapper.ts +6 -0
  117. package/src/hooks/api/integrations.test.ts +92 -0
  118. package/src/hooks/api/integrations.ts +72 -1
  119. package/src/index.ts +4 -0
  120. package/src/jira-callback-state.ts +19 -0
  121. package/src/jira-callback.test.ts +76 -0
  122. package/src/jira-callback.ts +67 -0
  123. package/src/jira-form-errors.test.ts +57 -0
  124. package/src/jira-form-errors.ts +94 -0
  125. package/src/pages/gitea-install-page.tsx +2 -2
  126. package/src/pages/jira-callback-page.test.tsx +250 -0
  127. package/src/pages/jira-callback-page.tsx +366 -0
  128. package/src/pages/jira-install-page.tsx +28 -0
  129. package/src/pages/linear-callback-page.test.tsx +25 -12
  130. package/src/pages/linear-callback-page.tsx +73 -12
  131. package/src/pages/sentry-callback-page.test.tsx +8 -5
  132. package/src/pages/sentry-callback-page.tsx +32 -8
  133. package/src/pages/slack-callback-page.test.tsx +78 -0
  134. package/src/pages/slack-callback-page.tsx +56 -18
  135. package/src/pages/source-control-onboarding-page.test.tsx +4 -4
  136. package/src/pages/source-control-onboarding-page.tsx +1 -1
  137. package/src/provider-catalog.test.ts +10 -4
  138. package/src/provider-catalog.ts +16 -10
  139. package/src/routes/jira-callback.tsx +8 -0
  140. package/src/routes/jira.tsx +4 -0
  141. package/src/sentry-callback.ts +2 -2
  142. package/src/workspace-navigation.test.ts +43 -0
  143. package/src/workspace-navigation.ts +38 -0
  144. package/test/render.tsx +2 -1
  145. package/tsconfig.build.tsbuildinfo +1 -1
@@ -3,27 +3,32 @@ export const PROVIDER_CATALOG = {
3
3
  github: {
4
4
  kind: 'redirect-install',
5
5
  iconName: PROVIDER_ICONS.github,
6
- setupPath: '/workspaces/$wid/integrations/github'
6
+ setupPath: '/w/$workspaceSlug/integrations/github'
7
7
  },
8
8
  sentry: {
9
9
  kind: 'redirect-install',
10
10
  iconName: PROVIDER_ICONS.sentry,
11
- setupPath: '/workspaces/$wid/integrations/sentry'
11
+ setupPath: '/w/$workspaceSlug/integrations/sentry'
12
12
  },
13
13
  linear: {
14
14
  kind: 'redirect-install',
15
15
  iconName: PROVIDER_ICONS.linear,
16
- setupPath: '/workspaces/$wid/integrations/linear'
16
+ setupPath: '/w/$workspaceSlug/integrations/linear'
17
17
  },
18
18
  slack: {
19
19
  kind: 'redirect-install',
20
20
  iconName: PROVIDER_ICONS.slack,
21
- setupPath: '/workspaces/$wid/integrations/slack'
21
+ setupPath: '/w/$workspaceSlug/integrations/slack'
22
+ },
23
+ jira: {
24
+ kind: 'redirect-install',
25
+ iconName: PROVIDER_ICONS.jira,
26
+ setupPath: '/w/$workspaceSlug/integrations/jira'
22
27
  },
23
28
  gitea: {
24
29
  kind: 'direct-connect',
25
30
  iconName: PROVIDER_ICONS.gitea,
26
- setupPath: '/workspaces/$wid/integrations/gitea'
31
+ setupPath: '/w/$workspaceSlug/integrations/gitea'
27
32
  },
28
33
  webhook: {
29
34
  kind: 'modal-connect',
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/provider-catalog.ts"],"sourcesContent":["import {PROVIDER_ICONS} from '@shipfox/integration-icons';\nimport type {IconName} from '@shipfox/react-ui/icon';\n\n// Literal union (not string) so `<Link to={catalog.setupPath}>` stays typed\n// against TanStack Router's route tree.\nexport type ProviderSetupPath =\n | '/workspaces/$wid/integrations/github'\n | '/workspaces/$wid/integrations/gitea'\n | '/workspaces/$wid/integrations/sentry'\n | '/workspaces/$wid/integrations/linear'\n | '/workspaces/$wid/integrations/slack';\n\ninterface RouteProviderCatalogEntry {\n kind: 'redirect-install' | 'direct-connect';\n iconName: IconName;\n setupPath: ProviderSetupPath;\n}\n\ninterface ModalProviderCatalogEntry {\n kind: 'modal-connect';\n iconName: IconName;\n}\n\nexport type ProviderCatalogEntry = RouteProviderCatalogEntry | ModalProviderCatalogEntry;\n\nexport const PROVIDER_CATALOG: Record<string, ProviderCatalogEntry> = {\n github: {\n kind: 'redirect-install',\n iconName: PROVIDER_ICONS.github,\n setupPath: '/workspaces/$wid/integrations/github',\n },\n sentry: {\n kind: 'redirect-install',\n iconName: PROVIDER_ICONS.sentry,\n setupPath: '/workspaces/$wid/integrations/sentry',\n },\n linear: {\n kind: 'redirect-install',\n iconName: PROVIDER_ICONS.linear,\n setupPath: '/workspaces/$wid/integrations/linear',\n },\n slack: {\n kind: 'redirect-install',\n iconName: PROVIDER_ICONS.slack,\n setupPath: '/workspaces/$wid/integrations/slack',\n },\n gitea: {\n kind: 'direct-connect',\n iconName: PROVIDER_ICONS.gitea,\n setupPath: '/workspaces/$wid/integrations/gitea',\n },\n webhook: {\n kind: 'modal-connect',\n iconName: PROVIDER_ICONS.webhook,\n },\n};\n"],"names":["PROVIDER_ICONS","PROVIDER_CATALOG","github","kind","iconName","setupPath","sentry","linear","slack","gitea","webhook"],"mappings":"AAAA,SAAQA,cAAc,QAAO,6BAA6B;AAyB1D,OAAO,MAAMC,mBAAyD;IACpEC,QAAQ;QACNC,MAAM;QACNC,UAAUJ,eAAeE,MAAM;QAC/BG,WAAW;IACb;IACAC,QAAQ;QACNH,MAAM;QACNC,UAAUJ,eAAeM,MAAM;QAC/BD,WAAW;IACb;IACAE,QAAQ;QACNJ,MAAM;QACNC,UAAUJ,eAAeO,MAAM;QAC/BF,WAAW;IACb;IACAG,OAAO;QACLL,MAAM;QACNC,UAAUJ,eAAeQ,KAAK;QAC9BH,WAAW;IACb;IACAI,OAAO;QACLN,MAAM;QACNC,UAAUJ,eAAeS,KAAK;QAC9BJ,WAAW;IACb;IACAK,SAAS;QACPP,MAAM;QACNC,UAAUJ,eAAeU,OAAO;IAClC;AACF,EAAE"}
1
+ {"version":3,"sources":["../src/provider-catalog.ts"],"sourcesContent":["import {PROVIDER_ICONS} from '@shipfox/integration-icons';\nimport type {IconName} from '@shipfox/react-ui/icon';\n\n// Literal union (not string) so `<Link to={catalog.setupPath}>` stays typed\n// against TanStack Router's route tree.\nexport type ProviderSetupPath =\n | '/w/$workspaceSlug/integrations/github'\n | '/w/$workspaceSlug/integrations/gitea'\n | '/w/$workspaceSlug/integrations/sentry'\n | '/w/$workspaceSlug/integrations/linear'\n | '/w/$workspaceSlug/integrations/slack'\n | '/w/$workspaceSlug/integrations/jira';\n\ninterface RouteProviderCatalogEntry {\n kind: 'redirect-install' | 'direct-connect';\n iconName: IconName;\n setupPath: ProviderSetupPath;\n}\n\ninterface ModalProviderCatalogEntry {\n kind: 'modal-connect';\n iconName: IconName;\n}\n\nexport type ProviderCatalogEntry = RouteProviderCatalogEntry | ModalProviderCatalogEntry;\n\nexport const PROVIDER_CATALOG: Record<string, ProviderCatalogEntry> = {\n github: {\n kind: 'redirect-install',\n iconName: PROVIDER_ICONS.github,\n setupPath: '/w/$workspaceSlug/integrations/github',\n },\n sentry: {\n kind: 'redirect-install',\n iconName: PROVIDER_ICONS.sentry,\n setupPath: '/w/$workspaceSlug/integrations/sentry',\n },\n linear: {\n kind: 'redirect-install',\n iconName: PROVIDER_ICONS.linear,\n setupPath: '/w/$workspaceSlug/integrations/linear',\n },\n slack: {\n kind: 'redirect-install',\n iconName: PROVIDER_ICONS.slack,\n setupPath: '/w/$workspaceSlug/integrations/slack',\n },\n jira: {\n kind: 'redirect-install',\n iconName: PROVIDER_ICONS.jira,\n setupPath: '/w/$workspaceSlug/integrations/jira',\n },\n gitea: {\n kind: 'direct-connect',\n iconName: PROVIDER_ICONS.gitea,\n setupPath: '/w/$workspaceSlug/integrations/gitea',\n },\n webhook: {\n kind: 'modal-connect',\n iconName: PROVIDER_ICONS.webhook,\n },\n};\n"],"names":["PROVIDER_ICONS","PROVIDER_CATALOG","github","kind","iconName","setupPath","sentry","linear","slack","jira","gitea","webhook"],"mappings":"AAAA,SAAQA,cAAc,QAAO,6BAA6B;AA0B1D,OAAO,MAAMC,mBAAyD;IACpEC,QAAQ;QACNC,MAAM;QACNC,UAAUJ,eAAeE,MAAM;QAC/BG,WAAW;IACb;IACAC,QAAQ;QACNH,MAAM;QACNC,UAAUJ,eAAeM,MAAM;QAC/BD,WAAW;IACb;IACAE,QAAQ;QACNJ,MAAM;QACNC,UAAUJ,eAAeO,MAAM;QAC/BF,WAAW;IACb;IACAG,OAAO;QACLL,MAAM;QACNC,UAAUJ,eAAeQ,KAAK;QAC9BH,WAAW;IACb;IACAI,MAAM;QACJN,MAAM;QACNC,UAAUJ,eAAeS,IAAI;QAC7BJ,WAAW;IACb;IACAK,OAAO;QACLP,MAAM;QACNC,UAAUJ,eAAeU,KAAK;QAC9BL,WAAW;IACb;IACAM,SAAS;QACPR,MAAM;QACNC,UAAUJ,eAAeW,OAAO;IAClC;AACF,EAAE"}
@@ -0,0 +1,8 @@
1
+ import { JiraCallbackPage } from '#pages/jira-callback-page.js';
2
+ import { parseJiraCallbackQuery } from '../jira-callback.js';
3
+ declare const _default: import("@shipfox/client-shell/runtime").RouteImpl<{
4
+ readonly validateSearch: typeof parseJiraCallbackQuery;
5
+ readonly component: typeof JiraCallbackPage;
6
+ }>;
7
+ export default _default;
8
+ //# sourceMappingURL=jira-callback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jira-callback.d.ts","sourceRoot":"","sources":["../../src/routes/jira-callback.tsx"],"names":[],"mappings":"AACA,OAAO,EAAC,gBAAgB,EAAC,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAC,sBAAsB,EAAC,MAAM,qBAAqB,CAAC;;;;;AAE3D,wBAGG"}
@@ -0,0 +1,9 @@
1
+ import { defineRoute } from '@shipfox/client-shell/runtime';
2
+ import { JiraCallbackPage } from '#pages/jira-callback-page.js';
3
+ import { parseJiraCallbackQuery } from '../jira-callback.js';
4
+ export default defineRoute({
5
+ validateSearch: parseJiraCallbackQuery,
6
+ component: JiraCallbackPage
7
+ });
8
+
9
+ //# sourceMappingURL=jira-callback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/routes/jira-callback.tsx"],"sourcesContent":["import {defineRoute} from '@shipfox/client-shell/runtime';\nimport {JiraCallbackPage} from '#pages/jira-callback-page.js';\nimport {parseJiraCallbackQuery} from '../jira-callback.js';\n\nexport default defineRoute({\n validateSearch: parseJiraCallbackQuery,\n component: JiraCallbackPage,\n});\n"],"names":["defineRoute","JiraCallbackPage","parseJiraCallbackQuery","validateSearch","component"],"mappings":"AAAA,SAAQA,WAAW,QAAO,gCAAgC;AAC1D,SAAQC,gBAAgB,QAAO,+BAA+B;AAC9D,SAAQC,sBAAsB,QAAO,sBAAsB;AAE3D,eAAeF,YAAY;IACzBG,gBAAgBD;IAChBE,WAAWH;AACb,GAAG"}
@@ -0,0 +1,6 @@
1
+ import { JiraInstallPage } from '#pages/jira-install-page.js';
2
+ declare const _default: import("@shipfox/client-shell/runtime").RouteImpl<{
3
+ readonly component: typeof JiraInstallPage;
4
+ }>;
5
+ export default _default;
6
+ //# sourceMappingURL=jira.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jira.d.ts","sourceRoot":"","sources":["../../src/routes/jira.tsx"],"names":[],"mappings":"AACA,OAAO,EAAC,eAAe,EAAC,MAAM,6BAA6B,CAAC;;;;AAE5D,wBAAyD"}
@@ -0,0 +1,7 @@
1
+ import { defineRoute } from '@shipfox/client-shell/runtime';
2
+ import { JiraInstallPage } from '#pages/jira-install-page.js';
3
+ export default defineRoute({
4
+ component: JiraInstallPage
5
+ });
6
+
7
+ //# sourceMappingURL=jira.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/routes/jira.tsx"],"sourcesContent":["import {defineRoute} from '@shipfox/client-shell/runtime';\nimport {JiraInstallPage} from '#pages/jira-install-page.js';\n\nexport default defineRoute({component: JiraInstallPage});\n"],"names":["defineRoute","JiraInstallPage","component"],"mappings":"AAAA,SAAQA,WAAW,QAAO,gCAAgC;AAC1D,SAAQC,eAAe,QAAO,8BAA8B;AAE5D,eAAeD,YAAY;IAACE,WAAWD;AAAe,GAAG"}
@@ -11,7 +11,7 @@ const sentryInstallWorkspaceStorageKey = {
11
11
  parse: (value)=>value || undefined
12
12
  };
13
13
  // Storage helpers swallow failures (quota, private mode, disabled storage):
14
- // the handoff is an optimization, never a requirement the callback always
14
+ // the handoff is an optimization, never a requirement: the callback always
15
15
  // asks for explicit confirmation and only uses the stored id to pre-select.
16
16
  export function saveSentryInstallWorkspace(storage, workspaceId) {
17
17
  installWorkspaceStorage(storage).write(workspaceId);
@@ -38,7 +38,7 @@ export function parseSentryCallbackParams(search) {
38
38
  };
39
39
  }
40
40
  // Sentry's redirect carries no state token, so the callback can never prove it
41
- // belongs to an install the user started installation always requires an
41
+ // belongs to an install the user started. Installation always requires an
42
42
  // explicit click. The stored id (and a sole workspace) only pre-select.
43
43
  export function preselectSentryWorkspace(storedId, workspaces) {
44
44
  if (workspaces.length === 0) return {
@@ -1 +1 @@
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
+ {"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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"}