@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
@@ -1,2 +1,2 @@
1
1
  $ shipfox-swc
2
- Successfully compiled: 9 files with swc (49.37ms)
2
+ Successfully compiled: 20 files with swc (321.85ms)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @shipfox/api-integration-jira
2
2
 
3
+ ## 5.0.0
4
+
5
+ ### Patch Changes
6
+
7
+ - bb037af: Resolves workspace packages from source during development while published consumers continue to use compiled output.
8
+ - 43d8e66: Adds Jira OAuth connection support with site selection and rotating access tokens.
9
+ - Updated dependencies [2875241]
10
+ - Updated dependencies [bb037af]
11
+ - Updated dependencies [43d8e66]
12
+ - Updated dependencies [fb70438]
13
+ - @shipfox/api-integration-core-dto@5.0.0
14
+ - @shipfox/api-auth-context@5.0.0
15
+ - @shipfox/api-integration-jira-dto@5.0.0
16
+ - @shipfox/api-secrets@5.0.0
17
+ - @shipfox/api-workspaces@5.0.0
18
+ - @shipfox/config@1.2.2
19
+ - @shipfox/node-drizzle@0.3.1
20
+ - @shipfox/node-fastify@0.2.3
21
+ - @shipfox/node-opentelemetry@0.5.2
22
+ - @shipfox/node-postgres@0.4.2
23
+
3
24
  ## 4.0.0
4
25
 
5
26
  ### Patch Changes
@@ -0,0 +1,33 @@
1
+ export interface JiraAuthorization {
2
+ accessToken: string;
3
+ refreshToken?: string | undefined;
4
+ expiresAt?: Date | undefined;
5
+ scopes: string[];
6
+ }
7
+ export interface JiraAccessibleResource {
8
+ cloudId: string;
9
+ name: string;
10
+ url: string;
11
+ scopes: string[];
12
+ }
13
+ export interface JiraIdentity {
14
+ accountId: string;
15
+ }
16
+ export interface JiraApiClient {
17
+ exchangeAuthorizationCode(input: {
18
+ code: string;
19
+ }): Promise<JiraAuthorization>;
20
+ refreshAccessToken(input: {
21
+ refreshToken: string;
22
+ }): Promise<JiraAuthorization>;
23
+ getAccessibleResources(input: {
24
+ accessToken: string;
25
+ }): Promise<JiraAccessibleResource[]>;
26
+ getMyself(input: {
27
+ accessToken: string;
28
+ cloudId: string;
29
+ }): Promise<JiraIdentity>;
30
+ }
31
+ export declare function createJiraApiClient(): JiraApiClient;
32
+ export declare function mapJiraError<T>(operation: string, request: () => Promise<T>): Promise<T>;
33
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,SAAS,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,yBAAyB,CAAC,KAAK,EAAE;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC7E,kBAAkB,CAAC,KAAK,EAAE;QAAC,YAAY,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC9E,sBAAsB,CAAC,KAAK,EAAE;QAAC,WAAW,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAAC;IACxF,SAAS,CAAC,KAAK,EAAE;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CACjF;AAoBD,wBAAgB,mBAAmB,IAAI,aAAa,CAmEnD;AAmDD,wBAAsB,YAAY,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAgC9F"}
@@ -0,0 +1,143 @@
1
+ import { logger } from '@shipfox/node-opentelemetry';
2
+ import ky, { HTTPError, TimeoutError } from 'ky';
3
+ import { config } from '#config.js';
4
+ import { JiraIntegrationProviderError } from '#core/errors.js';
5
+ const JIRA_API_TIMEOUT_MS = 10_000;
6
+ const SCOPE_SEPARATOR_RE = /[,\s]+/;
7
+ export function createJiraApiClient() {
8
+ return {
9
+ async exchangeAuthorizationCode (input) {
10
+ const body = await mapJiraError('exchange-authorization-code', ()=>ky.post(`${config.JIRA_AUTH_BASE_URL}/oauth/token`, {
11
+ body: new URLSearchParams({
12
+ grant_type: 'authorization_code',
13
+ client_id: config.JIRA_OAUTH_CLIENT_ID,
14
+ client_secret: config.JIRA_OAUTH_CLIENT_SECRET,
15
+ code: input.code,
16
+ redirect_uri: config.JIRA_OAUTH_REDIRECT_URL
17
+ }),
18
+ timeout: JIRA_API_TIMEOUT_MS
19
+ }).json());
20
+ return parseAuthorization(body);
21
+ },
22
+ async refreshAccessToken (input) {
23
+ const body = await mapJiraError('refresh-access-token', ()=>ky.post(`${config.JIRA_AUTH_BASE_URL}/oauth/token`, {
24
+ body: new URLSearchParams({
25
+ grant_type: 'refresh_token',
26
+ client_id: config.JIRA_OAUTH_CLIENT_ID,
27
+ client_secret: config.JIRA_OAUTH_CLIENT_SECRET,
28
+ refresh_token: input.refreshToken
29
+ }),
30
+ timeout: JIRA_API_TIMEOUT_MS
31
+ }).json());
32
+ return parseAuthorization(body);
33
+ },
34
+ async getAccessibleResources (input) {
35
+ const body = await mapJiraError('get-accessible-resources', ()=>ky.get(`${config.JIRA_API_BASE_URL}/oauth/token/accessible-resources`, {
36
+ headers: {
37
+ authorization: `Bearer ${input.accessToken}`
38
+ },
39
+ timeout: JIRA_API_TIMEOUT_MS
40
+ }).json());
41
+ if (!Array.isArray(body)) {
42
+ throw malformed('Jira accessible-resources response was not an array');
43
+ }
44
+ return body.map(parseAccessibleResource);
45
+ },
46
+ async getMyself (input) {
47
+ const body = await mapJiraError('get-myself', ()=>ky.get(`${config.JIRA_API_BASE_URL}/ex/jira/${input.cloudId}/rest/api/3/myself`, {
48
+ headers: {
49
+ authorization: `Bearer ${input.accessToken}`
50
+ },
51
+ timeout: JIRA_API_TIMEOUT_MS
52
+ }).json());
53
+ if (typeof body.accountId !== 'string' || body.accountId.length === 0) {
54
+ throw malformed('Jira identity response did not include an accountId');
55
+ }
56
+ return {
57
+ accountId: body.accountId
58
+ };
59
+ }
60
+ };
61
+ }
62
+ function parseAuthorization(body) {
63
+ if (typeof body.access_token !== 'string' || body.access_token.length === 0) {
64
+ throw malformed('Jira authorization response did not include an access token');
65
+ }
66
+ return {
67
+ accessToken: body.access_token,
68
+ refreshToken: typeof body.refresh_token === 'string' ? body.refresh_token : undefined,
69
+ expiresAt: parseExpiresAt(body.expires_in),
70
+ scopes: parseScopes(body.scope)
71
+ };
72
+ }
73
+ function parseAccessibleResource(value) {
74
+ if (!value || typeof value !== 'object') throw malformed('Jira site response was malformed');
75
+ const { id, name, url, scopes } = value;
76
+ if (typeof id !== 'string' || id.length === 0 || typeof name !== 'string' || name.length === 0 || typeof url !== 'string' || url.length === 0 || !Array.isArray(scopes) || !scopes.every((scope)=>typeof scope === 'string')) {
77
+ throw malformed('Jira site response did not include a valid cloud id, name, URL, and scopes');
78
+ }
79
+ return {
80
+ cloudId: id,
81
+ name,
82
+ url,
83
+ scopes
84
+ };
85
+ }
86
+ function parseScopes(scope) {
87
+ if (typeof scope === 'string') return scope.split(SCOPE_SEPARATOR_RE).map((value)=>value.trim()).filter(Boolean);
88
+ if (Array.isArray(scope) && scope.every((value)=>typeof value === 'string')) return scope;
89
+ if (scope === undefined) return [];
90
+ throw malformed('Jira authorization response included malformed scopes');
91
+ }
92
+ function parseExpiresAt(expiresIn) {
93
+ if (expiresIn === undefined) return undefined;
94
+ if (typeof expiresIn !== 'number' || !Number.isFinite(expiresIn) || expiresIn <= 0) {
95
+ throw malformed('Jira authorization response included a malformed expiry');
96
+ }
97
+ return new Date(Date.now() + expiresIn * 1000);
98
+ }
99
+ export async function mapJiraError(operation, request) {
100
+ try {
101
+ return await request();
102
+ } catch (error) {
103
+ if (error instanceof JiraIntegrationProviderError) throw error;
104
+ if (error instanceof HTTPError) {
105
+ const { status, statusText, headers } = error.response;
106
+ logger().warn({
107
+ operation,
108
+ status,
109
+ statusText
110
+ }, 'Jira API request rejected');
111
+ if (status === 429) {
112
+ throw new JiraIntegrationProviderError('rate-limited', 'Jira request was rate limited', retryAfterSeconds(headers));
113
+ }
114
+ if (status >= 500) throw new JiraIntegrationProviderError('provider-unavailable', 'Jira request failed');
115
+ if (status === 401 || status === 403) {
116
+ throw new JiraIntegrationProviderError('access-denied', 'Jira request was rejected');
117
+ }
118
+ throw malformed('Jira request was rejected');
119
+ }
120
+ if (error instanceof TimeoutError) {
121
+ logger().warn({
122
+ operation
123
+ }, 'Jira API request timed out');
124
+ throw new JiraIntegrationProviderError('timeout', 'Jira request timed out');
125
+ }
126
+ logger().warn({
127
+ operation,
128
+ errName: error instanceof Error ? error.name : typeof error
129
+ }, 'Jira API request failed');
130
+ throw new JiraIntegrationProviderError('provider-unavailable', 'Jira request failed');
131
+ }
132
+ }
133
+ function malformed(message) {
134
+ return new JiraIntegrationProviderError('malformed-provider-response', message);
135
+ }
136
+ function retryAfterSeconds(headers) {
137
+ const value = headers.get('retry-after');
138
+ if (!value) return undefined;
139
+ const parsed = Number.parseInt(value, 10);
140
+ return Number.isNaN(parsed) ? undefined : parsed;
141
+ }
142
+
143
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/api/client.ts"],"sourcesContent":["import {logger} from '@shipfox/node-opentelemetry';\nimport ky, {HTTPError, TimeoutError} from 'ky';\nimport {config} from '#config.js';\nimport {JiraIntegrationProviderError} from '#core/errors.js';\n\nconst JIRA_API_TIMEOUT_MS = 10_000;\nconst SCOPE_SEPARATOR_RE = /[,\\s]+/;\n\nexport interface JiraAuthorization {\n accessToken: string;\n refreshToken?: string | undefined;\n expiresAt?: Date | undefined;\n scopes: string[];\n}\n\nexport interface JiraAccessibleResource {\n cloudId: string;\n name: string;\n url: string;\n scopes: string[];\n}\n\nexport interface JiraIdentity {\n accountId: string;\n}\n\nexport interface JiraApiClient {\n exchangeAuthorizationCode(input: {code: string}): Promise<JiraAuthorization>;\n refreshAccessToken(input: {refreshToken: string}): Promise<JiraAuthorization>;\n getAccessibleResources(input: {accessToken: string}): Promise<JiraAccessibleResource[]>;\n getMyself(input: {accessToken: string; cloudId: string}): Promise<JiraIdentity>;\n}\n\ninterface JiraTokenResponse {\n access_token?: unknown;\n refresh_token?: unknown;\n expires_in?: unknown;\n scope?: unknown;\n}\n\ninterface JiraResourceResponse {\n id?: unknown;\n name?: unknown;\n url?: unknown;\n scopes?: unknown;\n}\n\ninterface JiraMyselfResponse {\n accountId?: unknown;\n}\n\nexport function createJiraApiClient(): JiraApiClient {\n return {\n async exchangeAuthorizationCode(input) {\n const body = await mapJiraError('exchange-authorization-code', () =>\n ky\n .post(`${config.JIRA_AUTH_BASE_URL}/oauth/token`, {\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: config.JIRA_OAUTH_CLIENT_ID,\n client_secret: config.JIRA_OAUTH_CLIENT_SECRET,\n code: input.code,\n redirect_uri: config.JIRA_OAUTH_REDIRECT_URL,\n }),\n timeout: JIRA_API_TIMEOUT_MS,\n })\n .json<JiraTokenResponse>(),\n );\n return parseAuthorization(body);\n },\n\n async refreshAccessToken(input) {\n const body = await mapJiraError('refresh-access-token', () =>\n ky\n .post(`${config.JIRA_AUTH_BASE_URL}/oauth/token`, {\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: config.JIRA_OAUTH_CLIENT_ID,\n client_secret: config.JIRA_OAUTH_CLIENT_SECRET,\n refresh_token: input.refreshToken,\n }),\n timeout: JIRA_API_TIMEOUT_MS,\n })\n .json<JiraTokenResponse>(),\n );\n return parseAuthorization(body);\n },\n\n async getAccessibleResources(input) {\n const body = await mapJiraError('get-accessible-resources', () =>\n ky\n .get(`${config.JIRA_API_BASE_URL}/oauth/token/accessible-resources`, {\n headers: {authorization: `Bearer ${input.accessToken}`},\n timeout: JIRA_API_TIMEOUT_MS,\n })\n .json<unknown>(),\n );\n if (!Array.isArray(body)) {\n throw malformed('Jira accessible-resources response was not an array');\n }\n return body.map(parseAccessibleResource);\n },\n\n async getMyself(input) {\n const body = await mapJiraError('get-myself', () =>\n ky\n .get(`${config.JIRA_API_BASE_URL}/ex/jira/${input.cloudId}/rest/api/3/myself`, {\n headers: {authorization: `Bearer ${input.accessToken}`},\n timeout: JIRA_API_TIMEOUT_MS,\n })\n .json<JiraMyselfResponse>(),\n );\n if (typeof body.accountId !== 'string' || body.accountId.length === 0) {\n throw malformed('Jira identity response did not include an accountId');\n }\n return {accountId: body.accountId};\n },\n };\n}\n\nfunction parseAuthorization(body: JiraTokenResponse): JiraAuthorization {\n if (typeof body.access_token !== 'string' || body.access_token.length === 0) {\n throw malformed('Jira authorization response did not include an access token');\n }\n return {\n accessToken: body.access_token,\n refreshToken: typeof body.refresh_token === 'string' ? body.refresh_token : undefined,\n expiresAt: parseExpiresAt(body.expires_in),\n scopes: parseScopes(body.scope),\n };\n}\n\nfunction parseAccessibleResource(value: unknown): JiraAccessibleResource {\n if (!value || typeof value !== 'object') throw malformed('Jira site response was malformed');\n const {id, name, url, scopes} = value as JiraResourceResponse;\n if (\n typeof id !== 'string' ||\n id.length === 0 ||\n typeof name !== 'string' ||\n name.length === 0 ||\n typeof url !== 'string' ||\n url.length === 0 ||\n !Array.isArray(scopes) ||\n !scopes.every((scope) => typeof scope === 'string')\n ) {\n throw malformed('Jira site response did not include a valid cloud id, name, URL, and scopes');\n }\n return {cloudId: id, name, url, scopes};\n}\n\nfunction parseScopes(scope: unknown): string[] {\n if (typeof scope === 'string')\n return scope\n .split(SCOPE_SEPARATOR_RE)\n .map((value) => value.trim())\n .filter(Boolean);\n if (Array.isArray(scope) && scope.every((value) => typeof value === 'string')) return scope;\n if (scope === undefined) return [];\n throw malformed('Jira authorization response included malformed scopes');\n}\n\nfunction parseExpiresAt(expiresIn: unknown): Date | undefined {\n if (expiresIn === undefined) return undefined;\n if (typeof expiresIn !== 'number' || !Number.isFinite(expiresIn) || expiresIn <= 0) {\n throw malformed('Jira authorization response included a malformed expiry');\n }\n return new Date(Date.now() + expiresIn * 1000);\n}\n\nexport async function mapJiraError<T>(operation: string, request: () => Promise<T>): Promise<T> {\n try {\n return await request();\n } catch (error) {\n if (error instanceof JiraIntegrationProviderError) throw error;\n if (error instanceof HTTPError) {\n const {status, statusText, headers} = error.response;\n logger().warn({operation, status, statusText}, 'Jira API request rejected');\n if (status === 429) {\n throw new JiraIntegrationProviderError(\n 'rate-limited',\n 'Jira request was rate limited',\n retryAfterSeconds(headers),\n );\n }\n if (status >= 500)\n throw new JiraIntegrationProviderError('provider-unavailable', 'Jira request failed');\n if (status === 401 || status === 403) {\n throw new JiraIntegrationProviderError('access-denied', 'Jira request was rejected');\n }\n throw malformed('Jira request was rejected');\n }\n if (error instanceof TimeoutError) {\n logger().warn({operation}, 'Jira API request timed out');\n throw new JiraIntegrationProviderError('timeout', 'Jira request timed out');\n }\n logger().warn(\n {operation, errName: error instanceof Error ? error.name : typeof error},\n 'Jira API request failed',\n );\n throw new JiraIntegrationProviderError('provider-unavailable', 'Jira request failed');\n }\n}\n\nfunction malformed(message: string): JiraIntegrationProviderError {\n return new JiraIntegrationProviderError('malformed-provider-response', message);\n}\n\nfunction retryAfterSeconds(headers: Headers): number | undefined {\n const value = headers.get('retry-after');\n if (!value) return undefined;\n const parsed = Number.parseInt(value, 10);\n return Number.isNaN(parsed) ? undefined : parsed;\n}\n"],"names":["logger","ky","HTTPError","TimeoutError","config","JiraIntegrationProviderError","JIRA_API_TIMEOUT_MS","SCOPE_SEPARATOR_RE","createJiraApiClient","exchangeAuthorizationCode","input","body","mapJiraError","post","JIRA_AUTH_BASE_URL","URLSearchParams","grant_type","client_id","JIRA_OAUTH_CLIENT_ID","client_secret","JIRA_OAUTH_CLIENT_SECRET","code","redirect_uri","JIRA_OAUTH_REDIRECT_URL","timeout","json","parseAuthorization","refreshAccessToken","refresh_token","refreshToken","getAccessibleResources","get","JIRA_API_BASE_URL","headers","authorization","accessToken","Array","isArray","malformed","map","parseAccessibleResource","getMyself","cloudId","accountId","length","access_token","undefined","expiresAt","parseExpiresAt","expires_in","scopes","parseScopes","scope","value","id","name","url","every","split","trim","filter","Boolean","expiresIn","Number","isFinite","Date","now","operation","request","error","status","statusText","response","warn","retryAfterSeconds","errName","Error","message","parsed","parseInt","isNaN"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ export interface DisconnectJiraInstallationParams<Tx = unknown> {
2
+ connectionId: string;
3
+ getConnection(connectionId: string): Promise<{
4
+ workspaceId: string;
5
+ } | undefined>;
6
+ deleteSecrets(params: {
7
+ workspaceId: string;
8
+ namespace: string;
9
+ }): Promise<number>;
10
+ transaction<T>(fn: (tx: Tx) => Promise<T>): Promise<T>;
11
+ deleteConnection(params: {
12
+ connectionId: string;
13
+ }, options: {
14
+ tx: Tx;
15
+ }): Promise<boolean>;
16
+ }
17
+ export declare function disconnectJiraInstallation<Tx = unknown>(params: DisconnectJiraInstallationParams<Tx>): Promise<void>;
18
+ //# sourceMappingURL=disconnect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disconnect.d.ts","sourceRoot":"","sources":["../../src/core/disconnect.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,gCAAgC,CAAC,EAAE,GAAG,OAAO;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAC,GAAG,SAAS,CAAC,CAAC;IAChF,aAAa,CAAC,MAAM,EAAE;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjF,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACvD,gBAAgB,CAAC,MAAM,EAAE;QAAC,YAAY,EAAE,MAAM,CAAA;KAAC,EAAE,OAAO,EAAE;QAAC,EAAE,EAAE,EAAE,CAAA;KAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvF;AAED,wBAAsB,0BAA0B,CAAC,EAAE,GAAG,OAAO,EAC3D,MAAM,EAAE,gCAAgC,CAAC,EAAE,CAAC,GAC3C,OAAO,CAAC,IAAI,CAAC,CAWf"}
@@ -0,0 +1,21 @@
1
+ import { deleteJiraInstallationByConnectionId } from '#db/installations.js';
2
+ import { jiraSecretsNamespace } from './tokens.js';
3
+ export async function disconnectJiraInstallation(params) {
4
+ const connection = await params.getConnection(params.connectionId);
5
+ if (connection) await params.deleteSecrets({
6
+ workspaceId: connection.workspaceId,
7
+ namespace: jiraSecretsNamespace(params.connectionId)
8
+ });
9
+ await params.transaction(async (tx)=>{
10
+ await deleteJiraInstallationByConnectionId(params.connectionId, {
11
+ tx
12
+ });
13
+ await params.deleteConnection({
14
+ connectionId: params.connectionId
15
+ }, {
16
+ tx
17
+ });
18
+ });
19
+ }
20
+
21
+ //# sourceMappingURL=disconnect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/disconnect.ts"],"sourcesContent":["import {deleteJiraInstallationByConnectionId} from '#db/installations.js';\nimport {jiraSecretsNamespace} from './tokens.js';\n\nexport interface DisconnectJiraInstallationParams<Tx = unknown> {\n connectionId: string;\n getConnection(connectionId: string): Promise<{workspaceId: string} | undefined>;\n deleteSecrets(params: {workspaceId: string; namespace: string}): Promise<number>;\n transaction<T>(fn: (tx: Tx) => Promise<T>): Promise<T>;\n deleteConnection(params: {connectionId: string}, options: {tx: Tx}): Promise<boolean>;\n}\n\nexport async function disconnectJiraInstallation<Tx = unknown>(\n params: DisconnectJiraInstallationParams<Tx>,\n): Promise<void> {\n const connection = await params.getConnection(params.connectionId);\n if (connection)\n await params.deleteSecrets({\n workspaceId: connection.workspaceId,\n namespace: jiraSecretsNamespace(params.connectionId),\n });\n await params.transaction(async (tx) => {\n await deleteJiraInstallationByConnectionId(params.connectionId, {tx});\n await params.deleteConnection({connectionId: params.connectionId}, {tx});\n });\n}\n"],"names":["deleteJiraInstallationByConnectionId","jiraSecretsNamespace","disconnectJiraInstallation","params","connection","getConnection","connectionId","deleteSecrets","workspaceId","namespace","transaction","tx","deleteConnection"],"mappings":"AAAA,SAAQA,oCAAoC,QAAO,uBAAuB;AAC1E,SAAQC,oBAAoB,QAAO,cAAc;AAUjD,OAAO,eAAeC,2BACpBC,MAA4C;IAE5C,MAAMC,aAAa,MAAMD,OAAOE,aAAa,CAACF,OAAOG,YAAY;IACjE,IAAIF,YACF,MAAMD,OAAOI,aAAa,CAAC;QACzBC,aAAaJ,WAAWI,WAAW;QACnCC,WAAWR,qBAAqBE,OAAOG,YAAY;IACrD;IACF,MAAMH,OAAOO,WAAW,CAAC,OAAOC;QAC9B,MAAMX,qCAAqCG,OAAOG,YAAY,EAAE;YAACK;QAAE;QACnE,MAAMR,OAAOS,gBAAgB,CAAC;YAACN,cAAcH,OAAOG,YAAY;QAAA,GAAG;YAACK;QAAE;IACxE;AACF"}
@@ -1,3 +1,4 @@
1
+ import { IntegrationProviderError } from '@shipfox/api-integration-core-dto';
1
2
  export declare class JiraConnectionNotFoundError extends Error {
2
3
  constructor(connectionId: string);
3
4
  }
@@ -7,4 +8,40 @@ export declare class JiraAccessTokenMissingError extends Error {
7
8
  export declare class JiraInstallationSiteMismatchError extends Error {
8
9
  constructor(connectionId: string, cloudId: string);
9
10
  }
11
+ export declare class JiraIntegrationProviderError extends IntegrationProviderError {
12
+ }
13
+ export declare class JiraInstallStateError extends Error {
14
+ constructor(message?: string);
15
+ }
16
+ export declare class JiraInstallStateActorMismatchError extends Error {
17
+ constructor();
18
+ }
19
+ export declare class JiraOAuthCallbackError extends Error {
20
+ readonly providerError: string;
21
+ readonly providerDescription: string | undefined;
22
+ constructor(providerError: string, providerDescription: string | undefined);
23
+ }
24
+ export declare class JiraAuthorizationScopeMismatchError extends Error {
25
+ readonly missingScopes: string[];
26
+ constructor(missingScopes: string[]);
27
+ }
28
+ export declare class JiraOfflineAccessNotGrantedError extends Error {
29
+ constructor();
30
+ }
31
+ export declare class JiraInstallationAlreadyLinkedError extends Error {
32
+ constructor(cloudId: string);
33
+ }
34
+ export declare class JiraConnectionAlreadyLinkedError extends Error {
35
+ constructor(connectionId: string);
36
+ }
37
+ export declare class JiraTokenUnrefreshableError extends Error {
38
+ readonly connectionId: string;
39
+ constructor(connectionId: string);
40
+ }
41
+ export declare class JiraSiteSelectionMismatchError extends Error {
42
+ constructor(cloudId: string);
43
+ }
44
+ export declare class JiraPendingSelectionNotFoundError extends Error {
45
+ constructor();
46
+ }
10
47
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,YAAY,EAAE,MAAM;CAIjC;AAED,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,YAAY,EAAE,MAAM;CAIjC;AAED,qBAAa,iCAAkC,SAAQ,KAAK;gBAC9C,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAMlD"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,wBAAwB,EAAC,MAAM,mCAAmC,CAAC;AAE3E,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,YAAY,EAAE,MAAM;CAIjC;AAED,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,YAAY,EAAE,MAAM;CAIjC;AAED,qBAAa,iCAAkC,SAAQ,KAAK;gBAC9C,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAMlD;AAED,qBAAa,4BAA6B,SAAQ,wBAAwB;CAAG;AAE7E,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,SAA+B;CAInD;AAED,qBAAa,kCAAmC,SAAQ,KAAK;;CAK5D;AAED,qBAAa,sBAAuB,SAAQ,KAAK;aAE7B,aAAa,EAAE,MAAM;aACrB,mBAAmB,EAAE,MAAM,GAAG,SAAS;gBADvC,aAAa,EAAE,MAAM,EACrB,mBAAmB,EAAE,MAAM,GAAG,SAAS;CAK1D;AAED,qBAAa,mCAAoC,SAAQ,KAAK;aAChC,aAAa,EAAE,MAAM,EAAE;gBAAvB,aAAa,EAAE,MAAM,EAAE;CAIpD;AAED,qBAAa,gCAAiC,SAAQ,KAAK;;CAO1D;AAED,qBAAa,kCAAmC,SAAQ,KAAK;gBAC/C,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,gCAAiC,SAAQ,KAAK;gBAC7C,YAAY,EAAE,MAAM;CAIjC;AAED,qBAAa,2BAA4B,SAAQ,KAAK;aACxB,YAAY,EAAE,MAAM;gBAApB,YAAY,EAAE,MAAM;CAIjD;AAED,qBAAa,8BAA+B,SAAQ,KAAK;gBAC3C,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,iCAAkC,SAAQ,KAAK;;CAK3D"}
@@ -1,3 +1,4 @@
1
+ import { IntegrationProviderError } from '@shipfox/api-integration-core-dto';
1
2
  export class JiraConnectionNotFoundError extends Error {
2
3
  constructor(connectionId){
3
4
  super(`Jira integration connection was not found: ${connectionId}`);
@@ -16,5 +17,67 @@ export class JiraInstallationSiteMismatchError extends Error {
16
17
  this.name = 'JiraInstallationSiteMismatchError';
17
18
  }
18
19
  }
20
+ export class JiraIntegrationProviderError extends IntegrationProviderError {
21
+ }
22
+ export class JiraInstallStateError extends Error {
23
+ constructor(message = 'Invalid Jira install state'){
24
+ super(message);
25
+ this.name = 'JiraInstallStateError';
26
+ }
27
+ }
28
+ export class JiraInstallStateActorMismatchError extends Error {
29
+ constructor(){
30
+ super('Jira install state was created by a different user');
31
+ this.name = 'JiraInstallStateActorMismatchError';
32
+ }
33
+ }
34
+ export class JiraOAuthCallbackError extends Error {
35
+ constructor(providerError, providerDescription){
36
+ super(providerDescription ?? `Jira OAuth callback failed: ${providerError}`), this.providerError = providerError, this.providerDescription = providerDescription;
37
+ this.name = 'JiraOAuthCallbackError';
38
+ }
39
+ }
40
+ export class JiraAuthorizationScopeMismatchError extends Error {
41
+ constructor(missingScopes){
42
+ super(`Jira authorization is missing usable site scopes: ${missingScopes.join(', ')}`), this.missingScopes = missingScopes;
43
+ this.name = 'JiraAuthorizationScopeMismatchError';
44
+ }
45
+ }
46
+ export class JiraOfflineAccessNotGrantedError extends Error {
47
+ constructor(){
48
+ super('Jira authorization did not grant offline access; reconnect and approve the requested scopes');
49
+ this.name = 'JiraOfflineAccessNotGrantedError';
50
+ }
51
+ }
52
+ export class JiraInstallationAlreadyLinkedError extends Error {
53
+ constructor(cloudId){
54
+ super(`Jira site is already linked to another Shipfox workspace: ${cloudId}`);
55
+ this.name = 'JiraInstallationAlreadyLinkedError';
56
+ }
57
+ }
58
+ export class JiraConnectionAlreadyLinkedError extends Error {
59
+ constructor(connectionId){
60
+ super(`Integration connection is already linked to another Jira site: ${connectionId}`);
61
+ this.name = 'JiraConnectionAlreadyLinkedError';
62
+ }
63
+ }
64
+ export class JiraTokenUnrefreshableError extends Error {
65
+ constructor(connectionId){
66
+ super(`Jira token cannot be refreshed; reconnect is required: ${connectionId}`), this.connectionId = connectionId;
67
+ this.name = 'JiraTokenUnrefreshableError';
68
+ }
69
+ }
70
+ export class JiraSiteSelectionMismatchError extends Error {
71
+ constructor(cloudId){
72
+ super(`Jira site was not included in the authorized site selection: ${cloudId}`);
73
+ this.name = 'JiraSiteSelectionMismatchError';
74
+ }
75
+ }
76
+ export class JiraPendingSelectionNotFoundError extends Error {
77
+ constructor(){
78
+ super('The Jira site selection expired or has already been completed; start over');
79
+ this.name = 'JiraPendingSelectionNotFoundError';
80
+ }
81
+ }
19
82
 
20
83
  //# sourceMappingURL=errors.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/errors.ts"],"sourcesContent":["export class JiraConnectionNotFoundError extends Error {\n constructor(connectionId: string) {\n super(`Jira integration connection was not found: ${connectionId}`);\n this.name = 'JiraConnectionNotFoundError';\n }\n}\n\nexport class JiraAccessTokenMissingError extends Error {\n constructor(connectionId: string) {\n super(`Jira access token is missing for connection: ${connectionId}`);\n this.name = 'JiraAccessTokenMissingError';\n }\n}\n\nexport class JiraInstallationSiteMismatchError extends Error {\n constructor(connectionId: string, cloudId: string) {\n super(\n `Jira connection is already linked to a different site: ${connectionId} (attempted ${cloudId})`,\n );\n this.name = 'JiraInstallationSiteMismatchError';\n }\n}\n"],"names":["JiraConnectionNotFoundError","Error","connectionId","name","JiraAccessTokenMissingError","JiraInstallationSiteMismatchError","cloudId"],"mappings":"AAAA,OAAO,MAAMA,oCAAoCC;IAC/C,YAAYC,YAAoB,CAAE;QAChC,KAAK,CAAC,CAAC,2CAA2C,EAAEA,cAAc;QAClE,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA,OAAO,MAAMC,oCAAoCH;IAC/C,YAAYC,YAAoB,CAAE;QAChC,KAAK,CAAC,CAAC,6CAA6C,EAAEA,cAAc;QACpE,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA,OAAO,MAAME,0CAA0CJ;IACrD,YAAYC,YAAoB,EAAEI,OAAe,CAAE;QACjD,KAAK,CACH,CAAC,uDAAuD,EAAEJ,aAAa,YAAY,EAAEI,QAAQ,CAAC,CAAC;QAEjG,IAAI,CAACH,IAAI,GAAG;IACd;AACF"}
1
+ {"version":3,"sources":["../../src/core/errors.ts"],"sourcesContent":["import {IntegrationProviderError} from '@shipfox/api-integration-core-dto';\n\nexport class JiraConnectionNotFoundError extends Error {\n constructor(connectionId: string) {\n super(`Jira integration connection was not found: ${connectionId}`);\n this.name = 'JiraConnectionNotFoundError';\n }\n}\n\nexport class JiraAccessTokenMissingError extends Error {\n constructor(connectionId: string) {\n super(`Jira access token is missing for connection: ${connectionId}`);\n this.name = 'JiraAccessTokenMissingError';\n }\n}\n\nexport class JiraInstallationSiteMismatchError extends Error {\n constructor(connectionId: string, cloudId: string) {\n super(\n `Jira connection is already linked to a different site: ${connectionId} (attempted ${cloudId})`,\n );\n this.name = 'JiraInstallationSiteMismatchError';\n }\n}\n\nexport class JiraIntegrationProviderError extends IntegrationProviderError {}\n\nexport class JiraInstallStateError extends Error {\n constructor(message = 'Invalid Jira install state') {\n super(message);\n this.name = 'JiraInstallStateError';\n }\n}\n\nexport class JiraInstallStateActorMismatchError extends Error {\n constructor() {\n super('Jira install state was created by a different user');\n this.name = 'JiraInstallStateActorMismatchError';\n }\n}\n\nexport class JiraOAuthCallbackError extends Error {\n constructor(\n public readonly providerError: string,\n public readonly providerDescription: string | undefined,\n ) {\n super(providerDescription ?? `Jira OAuth callback failed: ${providerError}`);\n this.name = 'JiraOAuthCallbackError';\n }\n}\n\nexport class JiraAuthorizationScopeMismatchError extends Error {\n constructor(public readonly missingScopes: string[]) {\n super(`Jira authorization is missing usable site scopes: ${missingScopes.join(', ')}`);\n this.name = 'JiraAuthorizationScopeMismatchError';\n }\n}\n\nexport class JiraOfflineAccessNotGrantedError extends Error {\n constructor() {\n super(\n 'Jira authorization did not grant offline access; reconnect and approve the requested scopes',\n );\n this.name = 'JiraOfflineAccessNotGrantedError';\n }\n}\n\nexport class JiraInstallationAlreadyLinkedError extends Error {\n constructor(cloudId: string) {\n super(`Jira site is already linked to another Shipfox workspace: ${cloudId}`);\n this.name = 'JiraInstallationAlreadyLinkedError';\n }\n}\n\nexport class JiraConnectionAlreadyLinkedError extends Error {\n constructor(connectionId: string) {\n super(`Integration connection is already linked to another Jira site: ${connectionId}`);\n this.name = 'JiraConnectionAlreadyLinkedError';\n }\n}\n\nexport class JiraTokenUnrefreshableError extends Error {\n constructor(public readonly connectionId: string) {\n super(`Jira token cannot be refreshed; reconnect is required: ${connectionId}`);\n this.name = 'JiraTokenUnrefreshableError';\n }\n}\n\nexport class JiraSiteSelectionMismatchError extends Error {\n constructor(cloudId: string) {\n super(`Jira site was not included in the authorized site selection: ${cloudId}`);\n this.name = 'JiraSiteSelectionMismatchError';\n }\n}\n\nexport class JiraPendingSelectionNotFoundError extends Error {\n constructor() {\n super('The Jira site selection expired or has already been completed; start over');\n this.name = 'JiraPendingSelectionNotFoundError';\n }\n}\n"],"names":["IntegrationProviderError","JiraConnectionNotFoundError","Error","connectionId","name","JiraAccessTokenMissingError","JiraInstallationSiteMismatchError","cloudId","JiraIntegrationProviderError","JiraInstallStateError","message","JiraInstallStateActorMismatchError","JiraOAuthCallbackError","providerError","providerDescription","JiraAuthorizationScopeMismatchError","missingScopes","join","JiraOfflineAccessNotGrantedError","JiraInstallationAlreadyLinkedError","JiraConnectionAlreadyLinkedError","JiraTokenUnrefreshableError","JiraSiteSelectionMismatchError","JiraPendingSelectionNotFoundError"],"mappings":"AAAA,SAAQA,wBAAwB,QAAO,oCAAoC;AAE3E,OAAO,MAAMC,oCAAoCC;IAC/C,YAAYC,YAAoB,CAAE;QAChC,KAAK,CAAC,CAAC,2CAA2C,EAAEA,cAAc;QAClE,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA,OAAO,MAAMC,oCAAoCH;IAC/C,YAAYC,YAAoB,CAAE;QAChC,KAAK,CAAC,CAAC,6CAA6C,EAAEA,cAAc;QACpE,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA,OAAO,MAAME,0CAA0CJ;IACrD,YAAYC,YAAoB,EAAEI,OAAe,CAAE;QACjD,KAAK,CACH,CAAC,uDAAuD,EAAEJ,aAAa,YAAY,EAAEI,QAAQ,CAAC,CAAC;QAEjG,IAAI,CAACH,IAAI,GAAG;IACd;AACF;AAEA,OAAO,MAAMI,qCAAqCR;AAA0B;AAE5E,OAAO,MAAMS,8BAA8BP;IACzC,YAAYQ,UAAU,4BAA4B,CAAE;QAClD,KAAK,CAACA;QACN,IAAI,CAACN,IAAI,GAAG;IACd;AACF;AAEA,OAAO,MAAMO,2CAA2CT;IACtD,aAAc;QACZ,KAAK,CAAC;QACN,IAAI,CAACE,IAAI,GAAG;IACd;AACF;AAEA,OAAO,MAAMQ,+BAA+BV;IAC1C,YACE,AAAgBW,aAAqB,EACrC,AAAgBC,mBAAuC,CACvD;QACA,KAAK,CAACA,uBAAuB,CAAC,4BAA4B,EAAED,eAAe,QAH3DA,gBAAAA,oBACAC,sBAAAA;QAGhB,IAAI,CAACV,IAAI,GAAG;IACd;AACF;AAEA,OAAO,MAAMW,4CAA4Cb;IACvD,YAAY,AAAgBc,aAAuB,CAAE;QACnD,KAAK,CAAC,CAAC,kDAAkD,EAAEA,cAAcC,IAAI,CAAC,OAAO,QAD3DD,gBAAAA;QAE1B,IAAI,CAACZ,IAAI,GAAG;IACd;AACF;AAEA,OAAO,MAAMc,yCAAyChB;IACpD,aAAc;QACZ,KAAK,CACH;QAEF,IAAI,CAACE,IAAI,GAAG;IACd;AACF;AAEA,OAAO,MAAMe,2CAA2CjB;IACtD,YAAYK,OAAe,CAAE;QAC3B,KAAK,CAAC,CAAC,0DAA0D,EAAEA,SAAS;QAC5E,IAAI,CAACH,IAAI,GAAG;IACd;AACF;AAEA,OAAO,MAAMgB,yCAAyClB;IACpD,YAAYC,YAAoB,CAAE;QAChC,KAAK,CAAC,CAAC,+DAA+D,EAAEA,cAAc;QACtF,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA,OAAO,MAAMiB,oCAAoCnB;IAC/C,YAAY,AAAgBC,YAAoB,CAAE;QAChD,KAAK,CAAC,CAAC,uDAAuD,EAAEA,cAAc,QADpDA,eAAAA;QAE1B,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA,OAAO,MAAMkB,uCAAuCpB;IAClD,YAAYK,OAAe,CAAE;QAC3B,KAAK,CAAC,CAAC,6DAA6D,EAAEA,SAAS;QAC/E,IAAI,CAACH,IAAI,GAAG;IACd;AACF;AAEA,OAAO,MAAMmB,0CAA0CrB;IACrD,aAAc;QACZ,KAAK,CAAC;QACN,IAAI,CAACE,IAAI,GAAG;IACd;AACF"}
@@ -0,0 +1,55 @@
1
+ import type { UserContextMembership } from '@shipfox/api-auth-context';
2
+ import type { IntegrationConnection } from '@shipfox/api-integration-core-dto';
3
+ import type { JiraAccessibleResource, JiraApiClient } from '#api/client.js';
4
+ import type { JiraPendingSelectionStore } from '#core/pending.js';
5
+ import type { JiraTokenStore } from '#core/tokens.js';
6
+ export interface ConnectJiraInstallationInput {
7
+ workspaceId: string;
8
+ cloudId: string;
9
+ siteUrl: string;
10
+ siteName: string;
11
+ authorizingAccountId: string;
12
+ scopes: string[];
13
+ tokenExpiresAt: Date | null;
14
+ displayName: string;
15
+ }
16
+ export interface HandleJiraCallbackParams {
17
+ jira: JiraApiClient;
18
+ tokenStore: Pick<JiraTokenStore, 'storeTokens'>;
19
+ pendingStore: Pick<JiraPendingSelectionStore, 'save' | 'load' | 'clear'>;
20
+ code: string;
21
+ state: string;
22
+ sessionUserId: string;
23
+ sessionMemberships: ReadonlyArray<UserContextMembership>;
24
+ requireWorkspaceMembership(input: {
25
+ workspaceId: string;
26
+ userId: string;
27
+ memberships: ReadonlyArray<UserContextMembership>;
28
+ }): Promise<unknown>;
29
+ getExistingJiraConnection(input: {
30
+ cloudId: string;
31
+ }): Promise<IntegrationConnection<'jira'> | undefined>;
32
+ connectJiraInstallation(input: ConnectJiraInstallationInput): Promise<IntegrationConnection<'jira'>>;
33
+ disconnectJiraInstallation(input: {
34
+ connectionId: string;
35
+ }): Promise<void>;
36
+ }
37
+ export declare function handleJiraCallback(params: HandleJiraCallbackParams): Promise<IntegrationConnection<'jira'> | {
38
+ sites: JiraAccessibleResource[];
39
+ }>;
40
+ export declare function handleJiraSiteSelection(params: Omit<HandleJiraCallbackParams, 'code'> & {
41
+ cloudId: string;
42
+ }): Promise<IntegrationConnection<'jira'>>;
43
+ export declare function handleJiraOAuthCallbackError(params: {
44
+ state: string;
45
+ error: string;
46
+ errorDescription?: string | undefined;
47
+ sessionUserId: string;
48
+ sessionMemberships: ReadonlyArray<UserContextMembership>;
49
+ requireWorkspaceMembership(input: {
50
+ workspaceId: string;
51
+ userId: string;
52
+ memberships: ReadonlyArray<UserContextMembership>;
53
+ }): Promise<unknown>;
54
+ }): Promise<never>;
55
+ //# sourceMappingURL=install.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/core/install.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,2BAA2B,CAAC;AACrE,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,mCAAmC,CAAC;AAE7E,OAAO,KAAK,EAAC,sBAAsB,EAAE,aAAa,EAAoB,MAAM,gBAAgB,CAAC;AAC7F,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,kBAAkB,CAAC;AAChE,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,iBAAiB,CAAC;AAapD,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,aAAa,CAAC;IACpB,UAAU,EAAE,IAAI,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC;IAChD,YAAY,EAAE,IAAI,CAAC,yBAAyB,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;IACzD,0BAA0B,CAAC,KAAK,EAAE;QAChC,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;KACnD,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrB,yBAAyB,CAAC,KAAK,EAAE;QAC/B,OAAO,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,qBAAqB,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC;IACvD,uBAAuB,CACrB,KAAK,EAAE,4BAA4B,GAClC,OAAO,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC,CAAC;IAC1C,0BAA0B,CAAC,KAAK,EAAE;QAAC,YAAY,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1E;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,qBAAqB,CAAC,MAAM,CAAC,GAAG;IAAC,KAAK,EAAE,sBAAsB,EAAE,CAAA;CAAC,CAAC,CA4B5E;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,IAAI,CAAC,wBAAwB,EAAE,MAAM,CAAC,GAAG;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GACjE,OAAO,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC,CAkBxC;AAED,wBAAsB,4BAA4B,CAAC,MAAM,EAAE;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;IACzD,0BAA0B,CAAC,KAAK,EAAE;QAChC,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;KACnD,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB,GAAG,OAAO,CAAC,KAAK,CAAC,CAGjB"}
@@ -0,0 +1,121 @@
1
+ import { logger } from '@shipfox/node-opentelemetry';
2
+ import { JiraInstallationAlreadyLinkedError, JiraInstallStateActorMismatchError, JiraIntegrationProviderError, JiraOAuthCallbackError, JiraOfflineAccessNotGrantedError, JiraPendingSelectionNotFoundError, JiraSiteSelectionMismatchError } from './errors.js';
3
+ import { assertJiraAuthorizationScopes } from './scopes.js';
4
+ import { verifyJiraInstallState } from './state.js';
5
+ export async function handleJiraCallback(params) {
6
+ const claims = await verifyClaims(params, params.state);
7
+ const authorization = await params.jira.exchangeAuthorizationCode({
8
+ code: params.code
9
+ });
10
+ if (!authorization.refreshToken) throw new JiraOfflineAccessNotGrantedError();
11
+ const sites = await params.jira.getAccessibleResources({
12
+ accessToken: authorization.accessToken
13
+ });
14
+ if (sites.length === 0) throw new JiraIntegrationProviderError('access-denied', 'Jira authorization did not grant access to a site');
15
+ if (sites.length > 1) {
16
+ await params.pendingStore.save({
17
+ workspaceId: claims.workspaceId,
18
+ state: params.state,
19
+ authorization,
20
+ sites
21
+ });
22
+ return {
23
+ sites
24
+ };
25
+ }
26
+ const site = sites[0];
27
+ if (!site) {
28
+ throw new JiraIntegrationProviderError('malformed-provider-response', 'Jira accessible-resources response did not contain a selected site');
29
+ }
30
+ assertJiraAuthorizationScopes(site.scopes);
31
+ return resolveJiraSite({
32
+ ...params,
33
+ authorization,
34
+ site,
35
+ claims
36
+ });
37
+ }
38
+ export async function handleJiraSiteSelection(params) {
39
+ const claims = await verifyClaims(params, params.state);
40
+ const pending = await params.pendingStore.load({
41
+ workspaceId: claims.workspaceId,
42
+ state: params.state
43
+ });
44
+ if (!pending) throw new JiraPendingSelectionNotFoundError();
45
+ const site = pending.sites.find((candidate)=>candidate.cloudId === params.cloudId);
46
+ if (!site) throw new JiraSiteSelectionMismatchError(params.cloudId);
47
+ assertJiraAuthorizationScopes(site.scopes);
48
+ const connection = await resolveJiraSite({
49
+ ...params,
50
+ authorization: pending.authorization,
51
+ site,
52
+ claims
53
+ });
54
+ await params.pendingStore.clear({
55
+ workspaceId: claims.workspaceId,
56
+ state: params.state
57
+ });
58
+ return connection;
59
+ }
60
+ export async function handleJiraOAuthCallbackError(params) {
61
+ await verifyClaims(params, params.state);
62
+ throw new JiraOAuthCallbackError(params.error, params.errorDescription);
63
+ }
64
+ async function verifyClaims(params, state) {
65
+ const claims = verifyJiraInstallState(state);
66
+ if (claims.userId !== params.sessionUserId) throw new JiraInstallStateActorMismatchError();
67
+ await params.requireWorkspaceMembership({
68
+ workspaceId: claims.workspaceId,
69
+ userId: claims.userId,
70
+ memberships: params.sessionMemberships
71
+ });
72
+ return claims;
73
+ }
74
+ async function resolveJiraSite(params) {
75
+ const existing = await params.getExistingJiraConnection({
76
+ cloudId: params.site.cloudId
77
+ });
78
+ if (existing && existing.workspaceId !== params.claims.workspaceId) throw new JiraInstallationAlreadyLinkedError(params.site.cloudId);
79
+ let connectionId;
80
+ try {
81
+ const identity = await params.jira.getMyself({
82
+ accessToken: params.authorization.accessToken,
83
+ cloudId: params.site.cloudId
84
+ });
85
+ const connection = await params.connectJiraInstallation({
86
+ workspaceId: params.claims.workspaceId,
87
+ cloudId: params.site.cloudId,
88
+ siteUrl: params.site.url,
89
+ siteName: params.site.name,
90
+ authorizingAccountId: identity.accountId,
91
+ scopes: params.site.scopes,
92
+ tokenExpiresAt: params.authorization.expiresAt ?? null,
93
+ displayName: `Jira ${params.site.name}`
94
+ });
95
+ connectionId = connection.id;
96
+ await params.tokenStore.storeTokens({
97
+ connectionId: connection.id,
98
+ accessToken: params.authorization.accessToken,
99
+ refreshToken: params.authorization.refreshToken,
100
+ editedBy: params.claims.userId
101
+ });
102
+ return connection;
103
+ } catch (error) {
104
+ if (connectionId && !existing) await bestEffortDisconnect(params, connectionId);
105
+ throw error;
106
+ }
107
+ }
108
+ async function bestEffortDisconnect(params, connectionId) {
109
+ try {
110
+ await params.disconnectJiraInstallation({
111
+ connectionId
112
+ });
113
+ } catch (error) {
114
+ logger().warn({
115
+ err: error,
116
+ connectionId
117
+ }, 'Jira connect compensation failed after token storage rejection');
118
+ }
119
+ }
120
+
121
+ //# sourceMappingURL=install.js.map