@shipfox/api-integration-jira 4.0.0 → 5.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 (99) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +21 -0
  3. package/dist/api/client.d.ts +33 -0
  4. package/dist/api/client.d.ts.map +1 -0
  5. package/dist/api/client.js +143 -0
  6. package/dist/api/client.js.map +1 -0
  7. package/dist/core/disconnect.d.ts +18 -0
  8. package/dist/core/disconnect.d.ts.map +1 -0
  9. package/dist/core/disconnect.js +21 -0
  10. package/dist/core/disconnect.js.map +1 -0
  11. package/dist/core/errors.d.ts +37 -0
  12. package/dist/core/errors.d.ts.map +1 -1
  13. package/dist/core/errors.js +63 -0
  14. package/dist/core/errors.js.map +1 -1
  15. package/dist/core/install.d.ts +55 -0
  16. package/dist/core/install.d.ts.map +1 -0
  17. package/dist/core/install.js +121 -0
  18. package/dist/core/install.js.map +1 -0
  19. package/dist/core/pending.d.ts +43 -0
  20. package/dist/core/pending.d.ts.map +1 -0
  21. package/dist/core/pending.js +107 -0
  22. package/dist/core/pending.js.map +1 -0
  23. package/dist/core/scopes.d.ts +4 -0
  24. package/dist/core/scopes.d.ts.map +1 -0
  25. package/dist/core/scopes.js +23 -0
  26. package/dist/core/scopes.js.map +1 -0
  27. package/dist/core/state.d.ts +12 -0
  28. package/dist/core/state.d.ts.map +1 -0
  29. package/dist/core/state.js +51 -0
  30. package/dist/core/state.js.map +1 -0
  31. package/dist/core/tokens.d.ts +6 -0
  32. package/dist/core/tokens.d.ts.map +1 -1
  33. package/dist/core/tokens.js +82 -6
  34. package/dist/core/tokens.js.map +1 -1
  35. package/dist/db/db.d.ts +188 -0
  36. package/dist/db/db.d.ts.map +1 -1
  37. package/dist/db/db.js +3 -1
  38. package/dist/db/db.js.map +1 -1
  39. package/dist/db/installations.d.ts +21 -0
  40. package/dist/db/installations.d.ts.map +1 -1
  41. package/dist/db/installations.js +82 -19
  42. package/dist/db/installations.js.map +1 -1
  43. package/dist/db/pending-selections.d.ts +17 -0
  44. package/dist/db/pending-selections.d.ts.map +1 -0
  45. package/dist/db/pending-selections.js +27 -0
  46. package/dist/db/pending-selections.js.map +1 -0
  47. package/dist/db/schema/installations.js +2 -2
  48. package/dist/db/schema/installations.js.map +1 -1
  49. package/dist/db/schema/pending-selections.d.ts +105 -0
  50. package/dist/db/schema/pending-selections.d.ts.map +1 -0
  51. package/dist/db/schema/pending-selections.js +24 -0
  52. package/dist/db/schema/pending-selections.js.map +1 -0
  53. package/dist/index.d.ts +26 -3
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +27 -4
  56. package/dist/index.js.map +1 -1
  57. package/dist/presentation/dto/integrations.d.ts +2 -0
  58. package/dist/presentation/dto/integrations.d.ts.map +1 -0
  59. package/dist/presentation/dto/integrations.js +3 -0
  60. package/dist/presentation/dto/integrations.js.map +1 -0
  61. package/dist/presentation/routes/errors.d.ts +2 -0
  62. package/dist/presentation/routes/errors.d.ts.map +1 -0
  63. package/dist/presentation/routes/errors.js +58 -0
  64. package/dist/presentation/routes/errors.js.map +1 -0
  65. package/dist/presentation/routes/install.d.ts +21 -0
  66. package/dist/presentation/routes/install.d.ts.map +1 -0
  67. package/dist/presentation/routes/install.js +130 -0
  68. package/dist/presentation/routes/install.js.map +1 -0
  69. package/dist/tsconfig.test.tsbuildinfo +1 -1
  70. package/drizzle/0000_whole_master_chief.sql +8 -1
  71. package/drizzle/meta/0000_snapshot.json +47 -3
  72. package/package.json +21 -24
  73. package/src/api/client.test.ts +29 -0
  74. package/src/api/client.ts +213 -0
  75. package/src/core/disconnect.test.ts +24 -0
  76. package/src/core/disconnect.ts +25 -0
  77. package/src/core/errors.ts +79 -0
  78. package/src/core/install.test.ts +129 -0
  79. package/src/core/install.ts +191 -0
  80. package/src/core/pending.ts +146 -0
  81. package/src/core/scopes.test.ts +19 -0
  82. package/src/core/scopes.ts +20 -0
  83. package/src/core/state.test.ts +38 -0
  84. package/src/core/state.ts +79 -0
  85. package/src/core/tokens-refresh.test.ts +106 -0
  86. package/src/core/tokens.ts +145 -8
  87. package/src/db/db.ts +2 -1
  88. package/src/db/installations.test.ts +37 -1
  89. package/src/db/installations.ts +120 -23
  90. package/src/db/pending-selections.ts +55 -0
  91. package/src/db/schema/installations.ts +2 -2
  92. package/src/db/schema/pending-selections.ts +31 -0
  93. package/src/index.test.ts +7 -2
  94. package/src/index.ts +66 -2
  95. package/src/presentation/dto/integrations.ts +1 -0
  96. package/src/presentation/routes/errors.ts +61 -0
  97. package/src/presentation/routes/install.ts +147 -0
  98. package/tsconfig.build.tsbuildinfo +1 -1
  99. package/turbo.json +9 -0
@@ -0,0 +1,61 @@
1
+ import {
2
+ ConnectionSlugConflictError,
3
+ type IntegrationProviderErrorReason,
4
+ } from '@shipfox/api-integration-core-dto';
5
+ import {ClientError} from '@shipfox/node-fastify';
6
+ import {
7
+ JiraAuthorizationScopeMismatchError,
8
+ JiraConnectionAlreadyLinkedError,
9
+ JiraInstallationAlreadyLinkedError,
10
+ JiraInstallStateActorMismatchError,
11
+ JiraInstallStateError,
12
+ JiraIntegrationProviderError,
13
+ JiraOAuthCallbackError,
14
+ JiraOfflineAccessNotGrantedError,
15
+ JiraPendingSelectionNotFoundError,
16
+ JiraSiteSelectionMismatchError,
17
+ } from '#core/errors.js';
18
+
19
+ function providerStatus(reason: IntegrationProviderErrorReason): number {
20
+ if (reason === 'rate-limited') return 429;
21
+ if (reason === 'timeout' || reason === 'provider-unavailable') return 503;
22
+ return 422;
23
+ }
24
+
25
+ export function jiraRouteErrorHandler(error: unknown): never {
26
+ if (error instanceof JiraInstallStateError)
27
+ throw new ClientError(error.message, 'invalid-jira-install-state', {status: 400});
28
+ if (error instanceof JiraInstallStateActorMismatchError)
29
+ throw new ClientError(error.message, 'jira-install-state-actor-mismatch', {status: 403});
30
+ if (error instanceof JiraInstallationAlreadyLinkedError)
31
+ throw new ClientError(error.message, 'jira-installation-already-linked', {status: 409});
32
+ if (error instanceof JiraConnectionAlreadyLinkedError)
33
+ throw new ClientError(error.message, 'jira-connection-already-linked', {status: 409});
34
+ if (error instanceof JiraAuthorizationScopeMismatchError)
35
+ throw new ClientError(error.message, 'jira-authorization-scope-mismatch', {
36
+ status: 422,
37
+ details: {missing_scopes: error.missingScopes},
38
+ });
39
+ if (error instanceof JiraOfflineAccessNotGrantedError)
40
+ throw new ClientError(error.message, 'jira-offline-access-not-granted', {status: 422});
41
+ if (error instanceof JiraSiteSelectionMismatchError)
42
+ throw new ClientError(error.message, 'jira-site-selection-mismatch', {status: 422});
43
+ if (error instanceof JiraPendingSelectionNotFoundError)
44
+ throw new ClientError(error.message, 'jira-pending-selection-not-found', {status: 410});
45
+ if (error instanceof JiraOAuthCallbackError)
46
+ throw new ClientError(error.message, 'jira-oauth-callback-error', {
47
+ status: 422,
48
+ details: {
49
+ error: error.providerError,
50
+ ...(error.providerDescription ? {error_description: error.providerDescription} : {}),
51
+ },
52
+ });
53
+ if (error instanceof ConnectionSlugConflictError)
54
+ throw new ClientError(error.message, 'slug-conflict', {status: 409});
55
+ if (error instanceof JiraIntegrationProviderError)
56
+ throw new ClientError(error.message, error.reason, {
57
+ status: providerStatus(error.reason),
58
+ details: {retry_after_seconds: error.retryAfterSeconds},
59
+ });
60
+ throw error;
61
+ }
@@ -0,0 +1,147 @@
1
+ import {AUTH_USER, requireUserContext, requireWorkspaceAccess} from '@shipfox/api-auth-context';
2
+ import {
3
+ type IntegrationCapability,
4
+ type IntegrationConnection,
5
+ integrationConnectionDtoSchema,
6
+ } from '@shipfox/api-integration-core-dto';
7
+ import {
8
+ completeJiraSiteSelectionBodySchema,
9
+ createJiraInstallBodySchema,
10
+ createJiraInstallResponseSchema,
11
+ type JiraCallbackQueryDto,
12
+ jiraCallbackQuerySchema,
13
+ jiraCallbackResponseSchema,
14
+ } from '@shipfox/api-integration-jira-dto';
15
+ import {requireWorkspaceMembership} from '@shipfox/api-workspaces';
16
+ import {defineRoute, type RouteGroup} from '@shipfox/node-fastify';
17
+ import type {JiraApiClient} from '#api/client.js';
18
+ import {config} from '#config.js';
19
+ import {
20
+ type ConnectJiraInstallationInput,
21
+ handleJiraCallback,
22
+ handleJiraOAuthCallbackError,
23
+ handleJiraSiteSelection,
24
+ } from '#core/install.js';
25
+ import type {JiraPendingSelectionStore} from '#core/pending.js';
26
+ import {formatJiraOAuthScopes} from '#core/scopes.js';
27
+ import {signJiraInstallState} from '#core/state.js';
28
+ import type {JiraTokenStore} from '#core/tokens.js';
29
+ import {toIntegrationConnectionDto} from '#presentation/dto/integrations.js';
30
+ import {jiraRouteErrorHandler} from './errors.js';
31
+
32
+ export interface CreateJiraIntegrationRoutesOptions {
33
+ jira: JiraApiClient;
34
+ tokenStore: Pick<JiraTokenStore, 'storeTokens'>;
35
+ pendingStore: Pick<JiraPendingSelectionStore, 'save' | 'load' | 'clear'>;
36
+ getExistingJiraConnection(input: {
37
+ cloudId: string;
38
+ }): Promise<IntegrationConnection<'jira'> | undefined>;
39
+ connectJiraInstallation(
40
+ input: ConnectJiraInstallationInput,
41
+ ): Promise<IntegrationConnection<'jira'>>;
42
+ disconnectJiraInstallation(input: {connectionId: string}): Promise<void>;
43
+ connectionCapabilities: IntegrationCapability[];
44
+ }
45
+
46
+ export function createJiraIntegrationRoutes(
47
+ options: CreateJiraIntegrationRoutesOptions,
48
+ ): RouteGroup {
49
+ const installRoute = defineRoute({
50
+ method: 'POST',
51
+ path: '/install',
52
+ auth: AUTH_USER,
53
+ description: 'Create a Jira OAuth authorization URL for a workspace.',
54
+ schema: {body: createJiraInstallBodySchema, response: {200: createJiraInstallResponseSchema}},
55
+ handler: (request) => {
56
+ const {workspace_id: workspaceId} = request.body;
57
+ const actor = requireUserContext(request);
58
+ requireWorkspaceAccess({request, workspaceId});
59
+ const installUrl = new URL(`${config.JIRA_AUTH_BASE_URL}/authorize`);
60
+ installUrl.searchParams.set('audience', 'api.atlassian.com');
61
+ installUrl.searchParams.set('client_id', config.JIRA_OAUTH_CLIENT_ID);
62
+ installUrl.searchParams.set('scope', formatJiraOAuthScopes());
63
+ installUrl.searchParams.set('redirect_uri', config.JIRA_OAUTH_REDIRECT_URL);
64
+ installUrl.searchParams.set(
65
+ 'state',
66
+ signJiraInstallState({workspaceId, userId: actor.userId}),
67
+ );
68
+ installUrl.searchParams.set('response_type', 'code');
69
+ installUrl.searchParams.set('prompt', 'consent');
70
+ return {install_url: installUrl.toString()};
71
+ },
72
+ });
73
+ const callbackApiRoute = defineRoute({
74
+ method: 'GET',
75
+ path: '/callback/api',
76
+ auth: AUTH_USER,
77
+ description: 'Handle the Jira OAuth callback.',
78
+ schema: {querystring: jiraCallbackQuerySchema, response: {200: jiraCallbackResponseSchema}},
79
+ errorHandler: jiraRouteErrorHandler,
80
+ handler: async (request) => {
81
+ const actor = requireUserContext(request);
82
+ const query = request.query;
83
+ if (isJiraOAuthErrorCallback(query))
84
+ return await handleJiraOAuthCallbackError({
85
+ state: query.state,
86
+ error: query.error,
87
+ errorDescription: query.error_description,
88
+ sessionUserId: actor.userId,
89
+ sessionMemberships: actor.memberships,
90
+ requireWorkspaceMembership,
91
+ });
92
+ const result = await handleJiraCallback({
93
+ ...options,
94
+ code: query.code,
95
+ state: query.state,
96
+ sessionUserId: actor.userId,
97
+ sessionMemberships: actor.memberships,
98
+ requireWorkspaceMembership,
99
+ });
100
+ return 'sites' in result
101
+ ? {
102
+ sites: result.sites.map((site) => ({
103
+ cloud_id: site.cloudId,
104
+ name: site.name,
105
+ url: site.url,
106
+ scopes: site.scopes,
107
+ })),
108
+ }
109
+ : toIntegrationConnectionDto(result, {capabilities: options.connectionCapabilities});
110
+ },
111
+ });
112
+ const siteSelectionRoute = defineRoute({
113
+ method: 'POST',
114
+ path: '/callback/site',
115
+ auth: AUTH_USER,
116
+ description: 'Complete Jira OAuth site selection.',
117
+ schema: {
118
+ body: completeJiraSiteSelectionBodySchema,
119
+ response: {200: integrationConnectionDtoSchema},
120
+ },
121
+ errorHandler: jiraRouteErrorHandler,
122
+ handler: async (request) => {
123
+ const actor = requireUserContext(request);
124
+ const connection = await handleJiraSiteSelection({
125
+ ...options,
126
+ cloudId: request.body.cloud_id,
127
+ state: request.body.state,
128
+ sessionUserId: actor.userId,
129
+ sessionMemberships: actor.memberships,
130
+ requireWorkspaceMembership,
131
+ });
132
+ return toIntegrationConnectionDto(connection, {capabilities: options.connectionCapabilities});
133
+ },
134
+ });
135
+ return {
136
+ prefix: '/integrations/jira',
137
+ routes: [installRoute, callbackApiRoute, siteSelectionRoute],
138
+ };
139
+ }
140
+
141
+ function isJiraOAuthErrorCallback(query: JiraCallbackQueryDto): query is JiraCallbackQueryDto & {
142
+ error: string;
143
+ error_description?: string | undefined;
144
+ state: string;
145
+ } {
146
+ return 'error' in query && typeof query.error === 'string';
147
+ }