@shipfox/api-integration-jira 4.0.0 → 6.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 (102) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +52 -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 +26 -0
  66. package/dist/presentation/routes/install.d.ts.map +1 -0
  67. package/dist/presentation/routes/install.js +133 -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 +22 -26
  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.test.ts +16 -2
  87. package/src/core/tokens.ts +145 -8
  88. package/src/db/db.ts +2 -1
  89. package/src/db/installations.test.ts +37 -1
  90. package/src/db/installations.ts +120 -23
  91. package/src/db/pending-selections.ts +55 -0
  92. package/src/db/schema/installations.ts +2 -2
  93. package/src/db/schema/pending-selections.ts +31 -0
  94. package/src/index.test.ts +7 -2
  95. package/src/index.ts +66 -2
  96. package/src/presentation/dto/integrations.ts +1 -0
  97. package/src/presentation/routes/errors.ts +61 -0
  98. package/src/presentation/routes/install.ts +161 -0
  99. package/test/globalSetup.ts +0 -9
  100. package/tsconfig.build.tsbuildinfo +1 -1
  101. package/turbo.json +9 -0
  102. package/test/api-secrets.d.ts +0 -26
@@ -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,161 @@
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 {defineRoute, type RouteGroup} from '@shipfox/node-fastify';
16
+ import type {JiraApiClient} from '#api/client.js';
17
+ import {config} from '#config.js';
18
+ import {
19
+ type ConnectJiraInstallationInput,
20
+ handleJiraCallback,
21
+ handleJiraOAuthCallbackError,
22
+ handleJiraSiteSelection,
23
+ } from '#core/install.js';
24
+ import type {JiraPendingSelectionStore} from '#core/pending.js';
25
+ import {formatJiraOAuthScopes} from '#core/scopes.js';
26
+ import {signJiraInstallState} from '#core/state.js';
27
+ import type {JiraTokenStore} from '#core/tokens.js';
28
+ import {toIntegrationConnectionDto} from '#presentation/dto/integrations.js';
29
+ import {jiraRouteErrorHandler} from './errors.js';
30
+
31
+ export interface CreateJiraIntegrationRoutesOptions {
32
+ jira: JiraApiClient;
33
+ tokenStore: Pick<JiraTokenStore, 'storeTokens'>;
34
+ pendingStore: Pick<JiraPendingSelectionStore, 'save' | 'load' | 'clear'>;
35
+ getExistingJiraConnection(input: {
36
+ cloudId: string;
37
+ }): Promise<IntegrationConnection<'jira'> | undefined>;
38
+ connectJiraInstallation(
39
+ input: ConnectJiraInstallationInput,
40
+ ): Promise<IntegrationConnection<'jira'>>;
41
+ disconnectJiraInstallation(input: {connectionId: string}): Promise<void>;
42
+ connectionCapabilities: IntegrationCapability[];
43
+ requireActiveWorkspaceMembership?: (input: {
44
+ workspaceId: string;
45
+ userId: string;
46
+ memberships: ReadonlyArray<import('@shipfox/api-auth-context').UserContextMembership>;
47
+ }) => Promise<unknown>;
48
+ }
49
+
50
+ export function createJiraIntegrationRoutes(
51
+ options: CreateJiraIntegrationRoutesOptions,
52
+ ): RouteGroup {
53
+ const requireMembership =
54
+ options.requireActiveWorkspaceMembership ?? unavailableWorkspaceMembershipCheck;
55
+ const installRoute = defineRoute({
56
+ method: 'POST',
57
+ path: '/install',
58
+ auth: AUTH_USER,
59
+ description: 'Create a Jira OAuth authorization URL for a workspace.',
60
+ schema: {body: createJiraInstallBodySchema, response: {200: createJiraInstallResponseSchema}},
61
+ handler: (request) => {
62
+ const {workspace_id: workspaceId} = request.body;
63
+ const actor = requireUserContext(request);
64
+ requireWorkspaceAccess({request, workspaceId});
65
+ const installUrl = new URL(`${config.JIRA_AUTH_BASE_URL}/authorize`);
66
+ installUrl.searchParams.set('audience', 'api.atlassian.com');
67
+ installUrl.searchParams.set('client_id', config.JIRA_OAUTH_CLIENT_ID);
68
+ installUrl.searchParams.set('scope', formatJiraOAuthScopes());
69
+ installUrl.searchParams.set('redirect_uri', config.JIRA_OAUTH_REDIRECT_URL);
70
+ installUrl.searchParams.set(
71
+ 'state',
72
+ signJiraInstallState({workspaceId, userId: actor.userId}),
73
+ );
74
+ installUrl.searchParams.set('response_type', 'code');
75
+ installUrl.searchParams.set('prompt', 'consent');
76
+ return {install_url: installUrl.toString()};
77
+ },
78
+ });
79
+ const callbackApiRoute = defineRoute({
80
+ method: 'GET',
81
+ path: '/callback/api',
82
+ auth: AUTH_USER,
83
+ description: 'Handle the Jira OAuth callback.',
84
+ schema: {querystring: jiraCallbackQuerySchema, response: {200: jiraCallbackResponseSchema}},
85
+ errorHandler: jiraRouteErrorHandler,
86
+ handler: async (request) => {
87
+ const actor = requireUserContext(request);
88
+ const query = request.query;
89
+ if (isJiraOAuthErrorCallback(query))
90
+ return await handleJiraOAuthCallbackError({
91
+ state: query.state,
92
+ error: query.error,
93
+ errorDescription: query.error_description,
94
+ sessionUserId: actor.userId,
95
+ sessionMemberships: actor.memberships,
96
+ requireWorkspaceMembership: requireMembership,
97
+ });
98
+ const result = await handleJiraCallback({
99
+ ...options,
100
+ code: query.code,
101
+ state: query.state,
102
+ sessionUserId: actor.userId,
103
+ sessionMemberships: actor.memberships,
104
+ requireWorkspaceMembership: requireMembership,
105
+ });
106
+ return 'sites' in result
107
+ ? {
108
+ sites: result.sites.map((site) => ({
109
+ cloud_id: site.cloudId,
110
+ name: site.name,
111
+ url: site.url,
112
+ scopes: site.scopes,
113
+ })),
114
+ }
115
+ : toIntegrationConnectionDto(result, {capabilities: options.connectionCapabilities});
116
+ },
117
+ });
118
+ const siteSelectionRoute = defineRoute({
119
+ method: 'POST',
120
+ path: '/callback/site',
121
+ auth: AUTH_USER,
122
+ description: 'Complete Jira OAuth site selection.',
123
+ schema: {
124
+ body: completeJiraSiteSelectionBodySchema,
125
+ response: {200: integrationConnectionDtoSchema},
126
+ },
127
+ errorHandler: jiraRouteErrorHandler,
128
+ handler: async (request) => {
129
+ const actor = requireUserContext(request);
130
+ const connection = await handleJiraSiteSelection({
131
+ ...options,
132
+ cloudId: request.body.cloud_id,
133
+ state: request.body.state,
134
+ sessionUserId: actor.userId,
135
+ sessionMemberships: actor.memberships,
136
+ requireWorkspaceMembership: requireMembership,
137
+ });
138
+ return toIntegrationConnectionDto(connection, {capabilities: options.connectionCapabilities});
139
+ },
140
+ });
141
+ return {
142
+ prefix: '/integrations/jira',
143
+ routes: [installRoute, callbackApiRoute, siteSelectionRoute],
144
+ };
145
+ }
146
+
147
+ function unavailableWorkspaceMembershipCheck(_input: {
148
+ workspaceId: string;
149
+ userId: string;
150
+ memberships: ReadonlyArray<import('@shipfox/api-auth-context').UserContextMembership>;
151
+ }): Promise<never> {
152
+ return Promise.reject(new Error('Workspaces inter-module client is not configured'));
153
+ }
154
+
155
+ function isJiraOAuthErrorCallback(query: JiraCallbackQueryDto): query is JiraCallbackQueryDto & {
156
+ error: string;
157
+ error_description?: string | undefined;
158
+ state: string;
159
+ } {
160
+ return 'error' in query && typeof query.error === 'string';
161
+ }
@@ -8,15 +8,6 @@ export async function setup() {
8
8
  createPostgresClient();
9
9
 
10
10
  await runMigrations(db(), migrationsPath, '__drizzle_migrations_integrations_jira');
11
- const {secretsModule} = await import('@shipfox/api-secrets');
12
- if (!secretsModule.database || Array.isArray(secretsModule.database)) {
13
- throw new Error('Secrets module database is not configured');
14
- }
15
- await runMigrations(
16
- secretsModule.database.db(),
17
- secretsModule.database.migrationsPath,
18
- '__drizzle_migrations_secrets',
19
- );
20
11
 
21
12
  closeDb();
22
13
  await closePostgresClient();