@shipfox/api-integration-jira 12.0.0 → 12.1.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 (86) hide show
  1. package/.turbo/turbo-build.log +17 -2
  2. package/CHANGELOG.md +10 -0
  3. package/README.md +58 -2
  4. package/dist/api/client.d.ts +19 -0
  5. package/dist/api/client.d.ts.map +1 -1
  6. package/dist/api/client.js +78 -0
  7. package/dist/api/client.js.map +1 -1
  8. package/dist/core/agent-tools-provider.d.ts +26 -0
  9. package/dist/core/agent-tools-provider.d.ts.map +1 -0
  10. package/dist/core/agent-tools-provider.js +130 -0
  11. package/dist/core/agent-tools-provider.js.map +1 -0
  12. package/dist/core/agent-tools.d.ts +16 -0
  13. package/dist/core/agent-tools.d.ts.map +1 -0
  14. package/dist/core/agent-tools.js +441 -0
  15. package/dist/core/agent-tools.js.map +1 -0
  16. package/dist/core/tokens.d.ts +2 -0
  17. package/dist/core/tokens.d.ts.map +1 -1
  18. package/dist/core/tokens.js +129 -37
  19. package/dist/core/tokens.js.map +1 -1
  20. package/dist/db/db.d.ts +68 -0
  21. package/dist/db/db.d.ts.map +1 -1
  22. package/dist/db/installations.d.ts +23 -7
  23. package/dist/db/installations.d.ts.map +1 -1
  24. package/dist/db/installations.js +42 -19
  25. package/dist/db/installations.js.map +1 -1
  26. package/dist/db/schema/installations.d.ts +34 -0
  27. package/dist/db/schema/installations.d.ts.map +1 -1
  28. package/dist/db/schema/installations.js +12 -2
  29. package/dist/db/schema/installations.js.map +1 -1
  30. package/dist/index.d.ts +20 -5
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +17 -5
  33. package/dist/index.js.map +1 -1
  34. package/dist/temporal/activities/index.d.ts +2 -0
  35. package/dist/temporal/activities/index.d.ts.map +1 -0
  36. package/dist/temporal/activities/index.js +3 -0
  37. package/dist/temporal/activities/index.js.map +1 -0
  38. package/dist/temporal/activities/refresh-jira-tokens.d.ts +20 -0
  39. package/dist/temporal/activities/refresh-jira-tokens.d.ts.map +1 -0
  40. package/dist/temporal/activities/refresh-jira-tokens.js +58 -0
  41. package/dist/temporal/activities/refresh-jira-tokens.js.map +1 -0
  42. package/dist/temporal/constants.d.ts +6 -0
  43. package/dist/temporal/constants.d.ts.map +1 -0
  44. package/dist/temporal/constants.js +8 -0
  45. package/dist/temporal/constants.js.map +1 -0
  46. package/dist/temporal/worker.d.ts +9 -0
  47. package/dist/temporal/worker.d.ts.map +1 -0
  48. package/dist/temporal/worker.js +26 -0
  49. package/dist/temporal/worker.js.map +1 -0
  50. package/dist/temporal/workflows/index.bundle.js +25225 -0
  51. package/dist/temporal/workflows/index.bundle.meta.json +1 -0
  52. package/dist/temporal/workflows/index.d.ts +2 -0
  53. package/dist/temporal/workflows/index.d.ts.map +1 -0
  54. package/dist/temporal/workflows/index.js +3 -0
  55. package/dist/temporal/workflows/index.js.map +1 -0
  56. package/dist/temporal/workflows/refresh-jira-tokens-cron.d.ts +2 -0
  57. package/dist/temporal/workflows/refresh-jira-tokens-cron.d.ts.map +1 -0
  58. package/dist/temporal/workflows/refresh-jira-tokens-cron.js +20 -0
  59. package/dist/temporal/workflows/refresh-jira-tokens-cron.js.map +1 -0
  60. package/dist/tsconfig.test.tsbuildinfo +1 -1
  61. package/drizzle/0001_jira-token-refresh-state.sql +12 -0
  62. package/drizzle/meta/0001_snapshot.json +245 -0
  63. package/drizzle/meta/_journal.json +7 -0
  64. package/package.json +12 -5
  65. package/src/api/client.test.ts +133 -12
  66. package/src/api/client.ts +107 -0
  67. package/src/core/agent-tools-provider.test.ts +299 -0
  68. package/src/core/agent-tools-provider.ts +175 -0
  69. package/src/core/agent-tools.ts +450 -0
  70. package/src/core/tokens-refresh.test.ts +221 -4
  71. package/src/core/tokens.test.ts +1 -1
  72. package/src/core/tokens.ts +153 -33
  73. package/src/db/installations.test.ts +84 -2
  74. package/src/db/installations.ts +81 -21
  75. package/src/db/schema/installations.ts +21 -1
  76. package/src/index.test.ts +34 -0
  77. package/src/index.ts +47 -3
  78. package/src/temporal/activities/index.ts +5 -0
  79. package/src/temporal/activities/refresh-jira-tokens.test.ts +156 -0
  80. package/src/temporal/activities/refresh-jira-tokens.ts +88 -0
  81. package/src/temporal/constants.ts +8 -0
  82. package/src/temporal/worker.ts +39 -0
  83. package/src/temporal/workflows/index.ts +1 -0
  84. package/src/temporal/workflows/refresh-jira-tokens-cron.ts +19 -0
  85. package/test/factories/jira-installation.ts +4 -0
  86. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,19 +1,49 @@
1
+ import { logger } from '@shipfox/node-opentelemetry';
1
2
  import { createJiraApiClient } from '#api/client.js';
2
3
  import { JiraAccessTokenMissingError, JiraConnectionNotFoundError, JiraIntegrationProviderError, JiraTokenUnrefreshableError } from '#core/errors.js';
3
- import { getJiraInstallationByConnectionId, updateJiraInstallationTokenExpiry, withJiraRefreshLock } from '#db/installations.js';
4
+ import { getJiraInstallationByConnectionId, updateJiraInstallationTokenExpiry, withJiraRefreshLock, withJiraRefreshLockAndWait } from '#db/installations.js';
4
5
  const ACCESS_TOKEN_KEY = 'ACCESS_TOKEN';
5
6
  const REFRESH_TOKEN_KEY = 'REFRESH_TOKEN';
6
7
  const TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000;
7
- const tokenRefreshes = new Map();
8
8
  export function jiraSecretsNamespace(connectionId) {
9
9
  return `system/integrations/jira/${connectionId}`;
10
10
  }
11
11
  export function createJiraTokenStore(params) {
12
12
  const client = params.client ?? createJiraApiClient();
13
- async function resolveWorkspaceId(connectionId) {
13
+ const tokenRefreshes = new Map();
14
+ const refreshStateUnknownConnections = new Set();
15
+ const credentialGenerations = new Map();
16
+ const credentialWriteQueues = new Map();
17
+ const currentCredentialGeneration = (connectionId)=>credentialGenerations.get(connectionId) ?? 0;
18
+ const advanceCredentialGeneration = (connectionId)=>{
19
+ const generation = currentCredentialGeneration(connectionId) + 1;
20
+ credentialGenerations.set(connectionId, generation);
21
+ return generation;
22
+ };
23
+ function withCredentialWrite(connectionId, operation) {
24
+ const previous = credentialWriteQueues.get(connectionId) ?? Promise.resolve();
25
+ const operationResult = previous.then(operation);
26
+ const completion = operationResult.then(()=>undefined, ()=>undefined);
27
+ credentialWriteQueues.set(connectionId, completion);
28
+ return operationResult.finally(()=>{
29
+ if (credentialWriteQueues.get(connectionId) === completion) {
30
+ credentialWriteQueues.delete(connectionId);
31
+ }
32
+ });
33
+ }
34
+ async function waitForCredentialWrites(connectionId) {
35
+ await credentialWriteQueues.get(connectionId);
36
+ }
37
+ async function resolveConnection(connectionId) {
14
38
  const connection = await params.resolveConnection(connectionId);
15
39
  if (!connection) throw new JiraConnectionNotFoundError(connectionId);
16
- return connection.workspaceId;
40
+ return connection;
41
+ }
42
+ async function resolveWorkspaceId(connectionId) {
43
+ return (await resolveConnection(connectionId)).workspaceId;
44
+ }
45
+ function clearTokenRefresh(connectionId, refresh) {
46
+ if (tokenRefreshes.get(connectionId) === refresh) tokenRefreshes.delete(connectionId);
17
47
  }
18
48
  const readSecretToken = (workspaceId, connectionId, key)=>params.secrets.getSecret({
19
49
  workspaceId,
@@ -27,20 +57,39 @@ export function createJiraTokenStore(params) {
27
57
  }
28
58
  return {
29
59
  async storeTokens (input) {
30
- const workspaceId = await resolveWorkspaceId(input.connectionId);
31
- const values = {
32
- [ACCESS_TOKEN_KEY]: input.accessToken
33
- };
34
- if (input.refreshToken) values[REFRESH_TOKEN_KEY] = input.refreshToken;
35
- await params.secrets.setSecrets({
36
- workspaceId,
37
- namespace: jiraSecretsNamespace(input.connectionId),
38
- values,
39
- editedBy: input.editedBy
40
- });
60
+ const credentialGeneration = advanceCredentialGeneration(input.connectionId);
61
+ tokenRefreshes.delete(input.connectionId);
62
+ await withJiraRefreshLockAndWait(input.connectionId, ()=>withCredentialWrite(input.connectionId, async ()=>{
63
+ if (currentCredentialGeneration(input.connectionId) !== credentialGeneration) return;
64
+ const workspaceId = await resolveWorkspaceId(input.connectionId);
65
+ if (currentCredentialGeneration(input.connectionId) !== credentialGeneration) return;
66
+ const values = {
67
+ [ACCESS_TOKEN_KEY]: input.accessToken
68
+ };
69
+ if (input.refreshToken) values[REFRESH_TOKEN_KEY] = input.refreshToken;
70
+ await params.secrets.setSecrets({
71
+ workspaceId,
72
+ namespace: jiraSecretsNamespace(input.connectionId),
73
+ values,
74
+ editedBy: input.editedBy
75
+ });
76
+ if (currentCredentialGeneration(input.connectionId) === credentialGeneration) {
77
+ refreshStateUnknownConnections.delete(input.connectionId);
78
+ }
79
+ }));
41
80
  },
42
81
  async getAccessToken (input) {
43
- const workspaceId = await resolveWorkspaceId(input.connectionId);
82
+ await waitForCredentialWrites(input.connectionId);
83
+ const connection = await resolveConnection(input.connectionId);
84
+ if (connection.lifecycleStatus !== 'active') {
85
+ throw new JiraTokenUnrefreshableError(input.connectionId);
86
+ }
87
+ await waitForCredentialWrites(input.connectionId);
88
+ if (refreshStateUnknownConnections.has(input.connectionId)) {
89
+ throw new JiraTokenUnrefreshableError(input.connectionId);
90
+ }
91
+ const credentialGeneration = currentCredentialGeneration(input.connectionId);
92
+ const workspaceId = connection.workspaceId;
44
93
  const accessToken = await readAccessToken(workspaceId, input.connectionId);
45
94
  if (!input.forceRefresh && !shouldRefresh((await getJiraInstallationByConnectionId(input.connectionId))?.tokenExpiresAt ?? null)) return accessToken;
46
95
  const inFlight = tokenRefreshes.get(input.connectionId);
@@ -54,10 +103,18 @@ export function createJiraTokenStore(params) {
54
103
  secrets: params.secrets,
55
104
  readAccessToken,
56
105
  readSecretToken,
57
- markConnectionError: params.markConnectionError
106
+ markConnectionError: params.markConnectionError,
107
+ withCredentialWrite,
108
+ waitForCredentialWrites,
109
+ isCurrentCredentialGeneration: ()=>currentCredentialGeneration(input.connectionId) === credentialGeneration
58
110
  });
59
111
  tokenRefreshes.set(input.connectionId, refresh);
60
- void refresh.then(()=>clearTokenRefresh(input.connectionId, refresh), ()=>clearTokenRefresh(input.connectionId, refresh));
112
+ void refresh.then(()=>clearTokenRefresh(input.connectionId, refresh), (error)=>{
113
+ if (isTerminalRefreshFailure(error) && currentCredentialGeneration(input.connectionId) === credentialGeneration) {
114
+ refreshStateUnknownConnections.add(input.connectionId);
115
+ }
116
+ clearTokenRefresh(input.connectionId, refresh);
117
+ });
61
118
  return refresh;
62
119
  }
63
120
  };
@@ -65,17 +122,20 @@ export function createJiraTokenStore(params) {
65
122
  function shouldRefresh(expiresAt) {
66
123
  return expiresAt !== null && expiresAt.getTime() <= Date.now() + TOKEN_REFRESH_MARGIN_MS;
67
124
  }
68
- function clearTokenRefresh(connectionId, refresh) {
69
- if (tokenRefreshes.get(connectionId) === refresh) tokenRefreshes.delete(connectionId);
70
- }
71
125
  async function refreshAccessTokenWithLock(params) {
72
126
  const lock = await withJiraRefreshLock(params.connectionId, ()=>refreshAccessTokenForConnection(params));
73
127
  if (lock.acquired) return lock.value;
128
+ if (!params.isCurrentCredentialGeneration()) {
129
+ return params.readAccessToken(params.workspaceId, params.connectionId);
130
+ }
74
131
  const reread = await params.readAccessToken(params.workspaceId, params.connectionId);
75
132
  if (reread !== params.originalAccessToken) return reread;
76
133
  throw new JiraIntegrationProviderError('provider-unavailable', 'Jira token refresh is already in progress');
77
134
  }
78
135
  async function refreshAccessTokenForConnection(params) {
136
+ if (!params.isCurrentCredentialGeneration()) {
137
+ return params.readAccessToken(params.workspaceId, params.connectionId);
138
+ }
79
139
  const current = await params.readAccessToken(params.workspaceId, params.connectionId);
80
140
  const installation = await getJiraInstallationByConnectionId(params.connectionId);
81
141
  if (current !== params.originalAccessToken || !params.forceRefresh && !shouldRefresh(installation?.tokenExpiresAt ?? null)) return current;
@@ -88,29 +148,61 @@ async function refreshAccessTokenForConnection(params) {
88
148
  const refreshed = await params.client.refreshAccessToken({
89
149
  refreshToken
90
150
  });
91
- if (!refreshed.refreshToken) throw new JiraTokenUnrefreshableError(params.connectionId);
92
- await params.secrets.setSecrets({
93
- workspaceId: params.workspaceId,
94
- namespace: jiraSecretsNamespace(params.connectionId),
95
- values: {
96
- [ACCESS_TOKEN_KEY]: refreshed.accessToken,
97
- [REFRESH_TOKEN_KEY]: refreshed.refreshToken
98
- }
99
- });
100
- await updateJiraInstallationTokenExpiry({
101
- connectionId: params.connectionId,
102
- tokenExpiresAt: refreshed.expiresAt ?? null,
103
- scopes: refreshed.scopes.length > 0 ? refreshed.scopes : undefined
151
+ const refreshedRefreshToken = refreshed.refreshToken;
152
+ if (!refreshedRefreshToken) throw new JiraTokenUnrefreshableError(params.connectionId);
153
+ const persisted = await params.withCredentialWrite(params.connectionId, async ()=>{
154
+ if (!params.isCurrentCredentialGeneration()) return false;
155
+ await params.secrets.setSecrets({
156
+ workspaceId: params.workspaceId,
157
+ namespace: jiraSecretsNamespace(params.connectionId),
158
+ values: {
159
+ [ACCESS_TOKEN_KEY]: refreshed.accessToken,
160
+ [REFRESH_TOKEN_KEY]: refreshedRefreshToken
161
+ }
162
+ });
163
+ if (!params.isCurrentCredentialGeneration()) return false;
164
+ await updateJiraInstallationTokenExpiry({
165
+ connectionId: params.connectionId,
166
+ tokenExpiresAt: refreshed.expiresAt ?? null,
167
+ scopes: refreshed.scopes.length > 0 ? refreshed.scopes : undefined
168
+ });
169
+ return true;
104
170
  });
171
+ if (!persisted || !params.isCurrentCredentialGeneration()) {
172
+ await params.waitForCredentialWrites(params.connectionId);
173
+ return params.readAccessToken(params.workspaceId, params.connectionId);
174
+ }
105
175
  return refreshed.accessToken;
106
176
  } catch (error) {
107
- if (error instanceof JiraIntegrationProviderError && error.reason === 'access-denied') {
108
- await params.markConnectionError?.({
109
- connectionId: params.connectionId
177
+ if (isTerminalRefreshFailure(error)) {
178
+ await markConnectionError({
179
+ callback: params.markConnectionError,
180
+ connectionId: params.connectionId,
181
+ isCurrentCredentialGeneration: params.isCurrentCredentialGeneration,
182
+ withCredentialWrite: params.withCredentialWrite
110
183
  });
111
184
  }
112
185
  throw error;
113
186
  }
114
187
  }
188
+ function isTerminalRefreshFailure(error) {
189
+ return error instanceof JiraIntegrationProviderError && (error.reason === 'access-denied' || error.reason === 'timeout');
190
+ }
191
+ async function markConnectionError(params) {
192
+ if (!params.isCurrentCredentialGeneration()) return;
193
+ try {
194
+ await params.withCredentialWrite(params.connectionId, async ()=>{
195
+ if (!params.isCurrentCredentialGeneration()) return;
196
+ await params.callback?.({
197
+ connectionId: params.connectionId
198
+ });
199
+ });
200
+ } catch (error) {
201
+ logger().warn({
202
+ err: error,
203
+ connectionId: params.connectionId
204
+ }, 'Jira connection error-state update failed after token refresh failure');
205
+ }
206
+ }
115
207
 
116
208
  //# sourceMappingURL=tokens.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/tokens.ts"],"sourcesContent":["import {createJiraApiClient, type JiraApiClient} from '#api/client.js';\nimport {\n JiraAccessTokenMissingError,\n JiraConnectionNotFoundError,\n JiraIntegrationProviderError,\n JiraTokenUnrefreshableError,\n} from '#core/errors.js';\nimport {\n getJiraInstallationByConnectionId,\n updateJiraInstallationTokenExpiry,\n withJiraRefreshLock,\n} from '#db/installations.js';\n\nconst ACCESS_TOKEN_KEY = 'ACCESS_TOKEN';\nconst REFRESH_TOKEN_KEY = 'REFRESH_TOKEN';\nconst TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000;\nconst tokenRefreshes = new Map<string, Promise<string>>();\n\nexport interface JiraConnectionResolverResult {\n workspaceId: string;\n}\n\nexport interface JiraSecretsStore {\n getSecret(params: {workspaceId: string; namespace: string; key: string}): Promise<string | null>;\n setSecrets(params: {\n workspaceId: string;\n namespace: string;\n values: Record<string, string>;\n editedBy?: string | null | undefined;\n }): Promise<void>;\n}\n\nexport interface CreateJiraTokenStoreParams {\n resolveConnection(connectionId: string): Promise<JiraConnectionResolverResult | undefined>;\n secrets: JiraSecretsStore;\n client?: JiraApiClient | undefined;\n markConnectionError?: (params: {connectionId: string}) => Promise<void>;\n}\n\nexport interface StoreJiraTokensParams {\n connectionId: string;\n accessToken: string;\n refreshToken?: string | undefined;\n editedBy?: string | null | undefined;\n}\n\nexport interface GetJiraAccessTokenParams {\n connectionId: string;\n forceRefresh?: boolean | undefined;\n}\n\nexport interface JiraTokenStore {\n storeTokens(params: StoreJiraTokensParams): Promise<void>;\n getAccessToken(params: GetJiraAccessTokenParams): Promise<string>;\n}\n\nexport function jiraSecretsNamespace(connectionId: string): string {\n return `system/integrations/jira/${connectionId}`;\n}\n\nexport function createJiraTokenStore(params: CreateJiraTokenStoreParams): JiraTokenStore {\n const client = params.client ?? createJiraApiClient();\n async function resolveWorkspaceId(connectionId: string): Promise<string> {\n const connection = await params.resolveConnection(connectionId);\n if (!connection) throw new JiraConnectionNotFoundError(connectionId);\n return connection.workspaceId;\n }\n const readSecretToken = (\n workspaceId: string,\n connectionId: string,\n key: typeof ACCESS_TOKEN_KEY | typeof REFRESH_TOKEN_KEY,\n ) => params.secrets.getSecret({workspaceId, namespace: jiraSecretsNamespace(connectionId), key});\n async function readAccessToken(workspaceId: string, connectionId: string): Promise<string> {\n const token = await readSecretToken(workspaceId, connectionId, ACCESS_TOKEN_KEY);\n if (!token) throw new JiraAccessTokenMissingError(connectionId);\n return token;\n }\n\n return {\n async storeTokens(input) {\n const workspaceId = await resolveWorkspaceId(input.connectionId);\n const values: Record<string, string> = {[ACCESS_TOKEN_KEY]: input.accessToken};\n if (input.refreshToken) values[REFRESH_TOKEN_KEY] = input.refreshToken;\n await params.secrets.setSecrets({\n workspaceId,\n namespace: jiraSecretsNamespace(input.connectionId),\n values,\n editedBy: input.editedBy,\n });\n },\n async getAccessToken(input) {\n const workspaceId = await resolveWorkspaceId(input.connectionId);\n const accessToken = await readAccessToken(workspaceId, input.connectionId);\n if (\n !input.forceRefresh &&\n !shouldRefresh(\n (await getJiraInstallationByConnectionId(input.connectionId))?.tokenExpiresAt ?? null,\n )\n )\n return accessToken;\n const inFlight = tokenRefreshes.get(input.connectionId);\n if (inFlight) return inFlight;\n const refresh = refreshAccessTokenWithLock({\n connectionId: input.connectionId,\n workspaceId,\n originalAccessToken: accessToken,\n forceRefresh: input.forceRefresh === true,\n client,\n secrets: params.secrets,\n readAccessToken,\n readSecretToken,\n markConnectionError: params.markConnectionError,\n });\n tokenRefreshes.set(input.connectionId, refresh);\n void refresh.then(\n () => clearTokenRefresh(input.connectionId, refresh),\n () => clearTokenRefresh(input.connectionId, refresh),\n );\n return refresh;\n },\n };\n}\n\nfunction shouldRefresh(expiresAt: Date | null): boolean {\n return expiresAt !== null && expiresAt.getTime() <= Date.now() + TOKEN_REFRESH_MARGIN_MS;\n}\n\nfunction clearTokenRefresh(connectionId: string, refresh: Promise<string>): void {\n if (tokenRefreshes.get(connectionId) === refresh) tokenRefreshes.delete(connectionId);\n}\n\nasync function refreshAccessTokenWithLock(params: {\n connectionId: string;\n workspaceId: string;\n originalAccessToken: string;\n forceRefresh: boolean;\n client: JiraApiClient;\n secrets: JiraSecretsStore;\n readAccessToken(workspaceId: string, connectionId: string): Promise<string>;\n readSecretToken(\n workspaceId: string,\n connectionId: string,\n key: typeof ACCESS_TOKEN_KEY | typeof REFRESH_TOKEN_KEY,\n ): Promise<string | null>;\n markConnectionError?: ((params: {connectionId: string}) => Promise<void>) | undefined;\n}): Promise<string> {\n const lock = await withJiraRefreshLock(params.connectionId, () =>\n refreshAccessTokenForConnection(params),\n );\n if (lock.acquired) return lock.value;\n const reread = await params.readAccessToken(params.workspaceId, params.connectionId);\n if (reread !== params.originalAccessToken) return reread;\n throw new JiraIntegrationProviderError(\n 'provider-unavailable',\n 'Jira token refresh is already in progress',\n );\n}\n\nasync function refreshAccessTokenForConnection(params: {\n connectionId: string;\n workspaceId: string;\n originalAccessToken: string;\n forceRefresh: boolean;\n client: JiraApiClient;\n secrets: JiraSecretsStore;\n readAccessToken(workspaceId: string, connectionId: string): Promise<string>;\n readSecretToken(\n workspaceId: string,\n connectionId: string,\n key: typeof ACCESS_TOKEN_KEY | typeof REFRESH_TOKEN_KEY,\n ): Promise<string | null>;\n markConnectionError?: ((params: {connectionId: string}) => Promise<void>) | undefined;\n}): Promise<string> {\n const current = await params.readAccessToken(params.workspaceId, params.connectionId);\n const installation = await getJiraInstallationByConnectionId(params.connectionId);\n if (\n current !== params.originalAccessToken ||\n (!params.forceRefresh && !shouldRefresh(installation?.tokenExpiresAt ?? null))\n )\n return current;\n const refreshToken = await params.readSecretToken(\n params.workspaceId,\n params.connectionId,\n REFRESH_TOKEN_KEY,\n );\n if (!refreshToken) {\n if (params.forceRefresh || shouldRefresh(installation?.tokenExpiresAt ?? null))\n throw new JiraTokenUnrefreshableError(params.connectionId);\n return current;\n }\n try {\n const refreshed = await params.client.refreshAccessToken({refreshToken});\n if (!refreshed.refreshToken) throw new JiraTokenUnrefreshableError(params.connectionId);\n await params.secrets.setSecrets({\n workspaceId: params.workspaceId,\n namespace: jiraSecretsNamespace(params.connectionId),\n values: {\n [ACCESS_TOKEN_KEY]: refreshed.accessToken,\n [REFRESH_TOKEN_KEY]: refreshed.refreshToken,\n },\n });\n await updateJiraInstallationTokenExpiry({\n connectionId: params.connectionId,\n tokenExpiresAt: refreshed.expiresAt ?? null,\n scopes: refreshed.scopes.length > 0 ? refreshed.scopes : undefined,\n });\n return refreshed.accessToken;\n } catch (error) {\n if (error instanceof JiraIntegrationProviderError && error.reason === 'access-denied') {\n await params.markConnectionError?.({connectionId: params.connectionId});\n }\n throw error;\n }\n}\n"],"names":["createJiraApiClient","JiraAccessTokenMissingError","JiraConnectionNotFoundError","JiraIntegrationProviderError","JiraTokenUnrefreshableError","getJiraInstallationByConnectionId","updateJiraInstallationTokenExpiry","withJiraRefreshLock","ACCESS_TOKEN_KEY","REFRESH_TOKEN_KEY","TOKEN_REFRESH_MARGIN_MS","tokenRefreshes","Map","jiraSecretsNamespace","connectionId","createJiraTokenStore","params","client","resolveWorkspaceId","connection","resolveConnection","workspaceId","readSecretToken","key","secrets","getSecret","namespace","readAccessToken","token","storeTokens","input","values","accessToken","refreshToken","setSecrets","editedBy","getAccessToken","forceRefresh","shouldRefresh","tokenExpiresAt","inFlight","get","refresh","refreshAccessTokenWithLock","originalAccessToken","markConnectionError","set","then","clearTokenRefresh","expiresAt","getTime","Date","now","delete","lock","refreshAccessTokenForConnection","acquired","value","reread","current","installation","refreshed","refreshAccessToken","scopes","length","undefined","error","reason"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/core/tokens.ts"],"sourcesContent":["import type {IntegrationConnectionLifecycleStatus} from '@shipfox/api-integration-spi';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {createJiraApiClient, type JiraApiClient} from '#api/client.js';\nimport {\n JiraAccessTokenMissingError,\n JiraConnectionNotFoundError,\n JiraIntegrationProviderError,\n JiraTokenUnrefreshableError,\n} from '#core/errors.js';\nimport {\n getJiraInstallationByConnectionId,\n updateJiraInstallationTokenExpiry,\n withJiraRefreshLock,\n withJiraRefreshLockAndWait,\n} from '#db/installations.js';\n\nconst ACCESS_TOKEN_KEY = 'ACCESS_TOKEN';\nconst REFRESH_TOKEN_KEY = 'REFRESH_TOKEN';\nconst TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000;\n\nexport interface JiraConnectionResolverResult {\n workspaceId: string;\n lifecycleStatus: IntegrationConnectionLifecycleStatus;\n}\n\nexport interface JiraSecretsStore {\n getSecret(params: {workspaceId: string; namespace: string; key: string}): Promise<string | null>;\n setSecrets(params: {\n workspaceId: string;\n namespace: string;\n values: Record<string, string>;\n editedBy?: string | null | undefined;\n }): Promise<void>;\n}\n\nexport interface CreateJiraTokenStoreParams {\n resolveConnection(connectionId: string): Promise<JiraConnectionResolverResult | undefined>;\n secrets: JiraSecretsStore;\n client?: JiraApiClient | undefined;\n markConnectionError?: (params: {connectionId: string}) => Promise<void>;\n}\n\nexport interface StoreJiraTokensParams {\n connectionId: string;\n accessToken: string;\n refreshToken?: string | undefined;\n editedBy?: string | null | undefined;\n}\n\nexport interface GetJiraAccessTokenParams {\n connectionId: string;\n forceRefresh?: boolean | undefined;\n}\n\nexport interface JiraTokenStore {\n storeTokens(params: StoreJiraTokensParams): Promise<void>;\n getAccessToken(params: GetJiraAccessTokenParams): Promise<string>;\n}\n\nexport function jiraSecretsNamespace(connectionId: string): string {\n return `system/integrations/jira/${connectionId}`;\n}\n\nexport function createJiraTokenStore(params: CreateJiraTokenStoreParams): JiraTokenStore {\n const client = params.client ?? createJiraApiClient();\n const tokenRefreshes = new Map<string, Promise<string>>();\n const refreshStateUnknownConnections = new Set<string>();\n const credentialGenerations = new Map<string, number>();\n const credentialWriteQueues = new Map<string, Promise<void>>();\n const currentCredentialGeneration = (connectionId: string): number =>\n credentialGenerations.get(connectionId) ?? 0;\n const advanceCredentialGeneration = (connectionId: string): number => {\n const generation = currentCredentialGeneration(connectionId) + 1;\n credentialGenerations.set(connectionId, generation);\n return generation;\n };\n function withCredentialWrite<T>(connectionId: string, operation: () => Promise<T>): Promise<T> {\n const previous = credentialWriteQueues.get(connectionId) ?? Promise.resolve();\n const operationResult = previous.then(operation);\n const completion = operationResult.then(\n () => undefined,\n () => undefined,\n );\n credentialWriteQueues.set(connectionId, completion);\n return operationResult.finally(() => {\n if (credentialWriteQueues.get(connectionId) === completion) {\n credentialWriteQueues.delete(connectionId);\n }\n });\n }\n async function waitForCredentialWrites(connectionId: string): Promise<void> {\n await credentialWriteQueues.get(connectionId);\n }\n async function resolveConnection(connectionId: string): Promise<JiraConnectionResolverResult> {\n const connection = await params.resolveConnection(connectionId);\n if (!connection) throw new JiraConnectionNotFoundError(connectionId);\n return connection;\n }\n async function resolveWorkspaceId(connectionId: string): Promise<string> {\n return (await resolveConnection(connectionId)).workspaceId;\n }\n function clearTokenRefresh(connectionId: string, refresh: Promise<string>): void {\n if (tokenRefreshes.get(connectionId) === refresh) tokenRefreshes.delete(connectionId);\n }\n const readSecretToken = (\n workspaceId: string,\n connectionId: string,\n key: typeof ACCESS_TOKEN_KEY | typeof REFRESH_TOKEN_KEY,\n ) => params.secrets.getSecret({workspaceId, namespace: jiraSecretsNamespace(connectionId), key});\n async function readAccessToken(workspaceId: string, connectionId: string): Promise<string> {\n const token = await readSecretToken(workspaceId, connectionId, ACCESS_TOKEN_KEY);\n if (!token) throw new JiraAccessTokenMissingError(connectionId);\n return token;\n }\n\n return {\n async storeTokens(input) {\n const credentialGeneration = advanceCredentialGeneration(input.connectionId);\n tokenRefreshes.delete(input.connectionId);\n await withJiraRefreshLockAndWait(input.connectionId, () =>\n withCredentialWrite(input.connectionId, async () => {\n if (currentCredentialGeneration(input.connectionId) !== credentialGeneration) return;\n const workspaceId = await resolveWorkspaceId(input.connectionId);\n if (currentCredentialGeneration(input.connectionId) !== credentialGeneration) return;\n const values: Record<string, string> = {[ACCESS_TOKEN_KEY]: input.accessToken};\n if (input.refreshToken) values[REFRESH_TOKEN_KEY] = input.refreshToken;\n await params.secrets.setSecrets({\n workspaceId,\n namespace: jiraSecretsNamespace(input.connectionId),\n values,\n editedBy: input.editedBy,\n });\n if (currentCredentialGeneration(input.connectionId) === credentialGeneration) {\n refreshStateUnknownConnections.delete(input.connectionId);\n }\n }),\n );\n },\n async getAccessToken(input) {\n await waitForCredentialWrites(input.connectionId);\n const connection = await resolveConnection(input.connectionId);\n if (connection.lifecycleStatus !== 'active') {\n throw new JiraTokenUnrefreshableError(input.connectionId);\n }\n await waitForCredentialWrites(input.connectionId);\n if (refreshStateUnknownConnections.has(input.connectionId)) {\n throw new JiraTokenUnrefreshableError(input.connectionId);\n }\n const credentialGeneration = currentCredentialGeneration(input.connectionId);\n const workspaceId = connection.workspaceId;\n const accessToken = await readAccessToken(workspaceId, input.connectionId);\n if (\n !input.forceRefresh &&\n !shouldRefresh(\n (await getJiraInstallationByConnectionId(input.connectionId))?.tokenExpiresAt ?? null,\n )\n )\n return accessToken;\n const inFlight = tokenRefreshes.get(input.connectionId);\n if (inFlight) return inFlight;\n const refresh = refreshAccessTokenWithLock({\n connectionId: input.connectionId,\n workspaceId,\n originalAccessToken: accessToken,\n forceRefresh: input.forceRefresh === true,\n client,\n secrets: params.secrets,\n readAccessToken,\n readSecretToken,\n markConnectionError: params.markConnectionError,\n withCredentialWrite,\n waitForCredentialWrites,\n isCurrentCredentialGeneration: () =>\n currentCredentialGeneration(input.connectionId) === credentialGeneration,\n });\n tokenRefreshes.set(input.connectionId, refresh);\n void refresh.then(\n () => clearTokenRefresh(input.connectionId, refresh),\n (error) => {\n if (\n isTerminalRefreshFailure(error) &&\n currentCredentialGeneration(input.connectionId) === credentialGeneration\n ) {\n refreshStateUnknownConnections.add(input.connectionId);\n }\n clearTokenRefresh(input.connectionId, refresh);\n },\n );\n return refresh;\n },\n };\n}\n\nfunction shouldRefresh(expiresAt: Date | null): boolean {\n return expiresAt !== null && expiresAt.getTime() <= Date.now() + TOKEN_REFRESH_MARGIN_MS;\n}\n\nasync function refreshAccessTokenWithLock(params: {\n connectionId: string;\n workspaceId: string;\n originalAccessToken: string;\n forceRefresh: boolean;\n client: JiraApiClient;\n secrets: JiraSecretsStore;\n readAccessToken(workspaceId: string, connectionId: string): Promise<string>;\n readSecretToken(\n workspaceId: string,\n connectionId: string,\n key: typeof ACCESS_TOKEN_KEY | typeof REFRESH_TOKEN_KEY,\n ): Promise<string | null>;\n markConnectionError?: ((params: {connectionId: string}) => Promise<void>) | undefined;\n withCredentialWrite<T>(connectionId: string, operation: () => Promise<T>): Promise<T>;\n waitForCredentialWrites(connectionId: string): Promise<void>;\n isCurrentCredentialGeneration(): boolean;\n}): Promise<string> {\n const lock = await withJiraRefreshLock(params.connectionId, () =>\n refreshAccessTokenForConnection(params),\n );\n if (lock.acquired) return lock.value;\n if (!params.isCurrentCredentialGeneration()) {\n return params.readAccessToken(params.workspaceId, params.connectionId);\n }\n const reread = await params.readAccessToken(params.workspaceId, params.connectionId);\n if (reread !== params.originalAccessToken) return reread;\n throw new JiraIntegrationProviderError(\n 'provider-unavailable',\n 'Jira token refresh is already in progress',\n );\n}\n\nasync function refreshAccessTokenForConnection(params: {\n connectionId: string;\n workspaceId: string;\n originalAccessToken: string;\n forceRefresh: boolean;\n client: JiraApiClient;\n secrets: JiraSecretsStore;\n readAccessToken(workspaceId: string, connectionId: string): Promise<string>;\n readSecretToken(\n workspaceId: string,\n connectionId: string,\n key: typeof ACCESS_TOKEN_KEY | typeof REFRESH_TOKEN_KEY,\n ): Promise<string | null>;\n markConnectionError?: ((params: {connectionId: string}) => Promise<void>) | undefined;\n withCredentialWrite<T>(connectionId: string, operation: () => Promise<T>): Promise<T>;\n waitForCredentialWrites(connectionId: string): Promise<void>;\n isCurrentCredentialGeneration(): boolean;\n}): Promise<string> {\n if (!params.isCurrentCredentialGeneration()) {\n return params.readAccessToken(params.workspaceId, params.connectionId);\n }\n const current = await params.readAccessToken(params.workspaceId, params.connectionId);\n const installation = await getJiraInstallationByConnectionId(params.connectionId);\n if (\n current !== params.originalAccessToken ||\n (!params.forceRefresh && !shouldRefresh(installation?.tokenExpiresAt ?? null))\n )\n return current;\n const refreshToken = await params.readSecretToken(\n params.workspaceId,\n params.connectionId,\n REFRESH_TOKEN_KEY,\n );\n if (!refreshToken) {\n if (params.forceRefresh || shouldRefresh(installation?.tokenExpiresAt ?? null))\n throw new JiraTokenUnrefreshableError(params.connectionId);\n return current;\n }\n try {\n const refreshed = await params.client.refreshAccessToken({refreshToken});\n const refreshedRefreshToken = refreshed.refreshToken;\n if (!refreshedRefreshToken) throw new JiraTokenUnrefreshableError(params.connectionId);\n const persisted = await params.withCredentialWrite(params.connectionId, async () => {\n if (!params.isCurrentCredentialGeneration()) return false;\n await params.secrets.setSecrets({\n workspaceId: params.workspaceId,\n namespace: jiraSecretsNamespace(params.connectionId),\n values: {\n [ACCESS_TOKEN_KEY]: refreshed.accessToken,\n [REFRESH_TOKEN_KEY]: refreshedRefreshToken,\n },\n });\n if (!params.isCurrentCredentialGeneration()) return false;\n await updateJiraInstallationTokenExpiry({\n connectionId: params.connectionId,\n tokenExpiresAt: refreshed.expiresAt ?? null,\n scopes: refreshed.scopes.length > 0 ? refreshed.scopes : undefined,\n });\n return true;\n });\n if (!persisted || !params.isCurrentCredentialGeneration()) {\n await params.waitForCredentialWrites(params.connectionId);\n return params.readAccessToken(params.workspaceId, params.connectionId);\n }\n return refreshed.accessToken;\n } catch (error) {\n if (isTerminalRefreshFailure(error)) {\n await markConnectionError({\n callback: params.markConnectionError,\n connectionId: params.connectionId,\n isCurrentCredentialGeneration: params.isCurrentCredentialGeneration,\n withCredentialWrite: params.withCredentialWrite,\n });\n }\n throw error;\n }\n}\n\nfunction isTerminalRefreshFailure(error: unknown): boolean {\n return (\n error instanceof JiraIntegrationProviderError &&\n (error.reason === 'access-denied' || error.reason === 'timeout')\n );\n}\n\nasync function markConnectionError(params: {\n callback: CreateJiraTokenStoreParams['markConnectionError'];\n connectionId: string;\n isCurrentCredentialGeneration(): boolean;\n withCredentialWrite<T>(connectionId: string, operation: () => Promise<T>): Promise<T>;\n}): Promise<void> {\n if (!params.isCurrentCredentialGeneration()) return;\n try {\n await params.withCredentialWrite(params.connectionId, async () => {\n if (!params.isCurrentCredentialGeneration()) return;\n await params.callback?.({connectionId: params.connectionId});\n });\n } catch (error) {\n logger().warn(\n {err: error, connectionId: params.connectionId},\n 'Jira connection error-state update failed after token refresh failure',\n );\n }\n}\n"],"names":["logger","createJiraApiClient","JiraAccessTokenMissingError","JiraConnectionNotFoundError","JiraIntegrationProviderError","JiraTokenUnrefreshableError","getJiraInstallationByConnectionId","updateJiraInstallationTokenExpiry","withJiraRefreshLock","withJiraRefreshLockAndWait","ACCESS_TOKEN_KEY","REFRESH_TOKEN_KEY","TOKEN_REFRESH_MARGIN_MS","jiraSecretsNamespace","connectionId","createJiraTokenStore","params","client","tokenRefreshes","Map","refreshStateUnknownConnections","Set","credentialGenerations","credentialWriteQueues","currentCredentialGeneration","get","advanceCredentialGeneration","generation","set","withCredentialWrite","operation","previous","Promise","resolve","operationResult","then","completion","undefined","finally","delete","waitForCredentialWrites","resolveConnection","connection","resolveWorkspaceId","workspaceId","clearTokenRefresh","refresh","readSecretToken","key","secrets","getSecret","namespace","readAccessToken","token","storeTokens","input","credentialGeneration","values","accessToken","refreshToken","setSecrets","editedBy","getAccessToken","lifecycleStatus","has","forceRefresh","shouldRefresh","tokenExpiresAt","inFlight","refreshAccessTokenWithLock","originalAccessToken","markConnectionError","isCurrentCredentialGeneration","error","isTerminalRefreshFailure","add","expiresAt","getTime","Date","now","lock","refreshAccessTokenForConnection","acquired","value","reread","current","installation","refreshed","refreshAccessToken","refreshedRefreshToken","persisted","scopes","length","callback","reason","warn","err"],"mappings":"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"}
package/dist/db/db.d.ts CHANGED
@@ -197,6 +197,40 @@ export declare const schema: {
197
197
  identity: undefined;
198
198
  generated: undefined;
199
199
  }, {}, {}>;
200
+ refreshTokenLastUsedAt: import("drizzle-orm/pg-core").PgColumn<{
201
+ name: "refresh_token_last_used_at";
202
+ tableName: "installations";
203
+ dataType: "date";
204
+ columnType: "PgTimestamp";
205
+ data: Date;
206
+ driverParam: string;
207
+ notNull: true;
208
+ hasDefault: true;
209
+ isPrimaryKey: false;
210
+ isAutoincrement: false;
211
+ hasRuntimeDefault: false;
212
+ enumValues: undefined;
213
+ baseColumn: never;
214
+ identity: undefined;
215
+ generated: undefined;
216
+ }, {}, {}>;
217
+ refreshTokenLastAttemptedAt: import("drizzle-orm/pg-core").PgColumn<{
218
+ name: "refresh_token_last_attempted_at";
219
+ tableName: "installations";
220
+ dataType: "date";
221
+ columnType: "PgTimestamp";
222
+ data: Date;
223
+ driverParam: string;
224
+ notNull: true;
225
+ hasDefault: true;
226
+ isPrimaryKey: false;
227
+ isAutoincrement: false;
228
+ hasRuntimeDefault: false;
229
+ enumValues: undefined;
230
+ baseColumn: never;
231
+ identity: undefined;
232
+ generated: undefined;
233
+ }, {}, {}>;
200
234
  createdAt: import("drizzle-orm/pg-core").PgColumn<{
201
235
  name: "created_at";
202
236
  tableName: "installations";
@@ -527,6 +561,40 @@ export declare function db(): NodePgDatabase<{
527
561
  identity: undefined;
528
562
  generated: undefined;
529
563
  }, {}, {}>;
564
+ refreshTokenLastUsedAt: import("drizzle-orm/pg-core").PgColumn<{
565
+ name: "refresh_token_last_used_at";
566
+ tableName: "installations";
567
+ dataType: "date";
568
+ columnType: "PgTimestamp";
569
+ data: Date;
570
+ driverParam: string;
571
+ notNull: true;
572
+ hasDefault: true;
573
+ isPrimaryKey: false;
574
+ isAutoincrement: false;
575
+ hasRuntimeDefault: false;
576
+ enumValues: undefined;
577
+ baseColumn: never;
578
+ identity: undefined;
579
+ generated: undefined;
580
+ }, {}, {}>;
581
+ refreshTokenLastAttemptedAt: import("drizzle-orm/pg-core").PgColumn<{
582
+ name: "refresh_token_last_attempted_at";
583
+ tableName: "installations";
584
+ dataType: "date";
585
+ columnType: "PgTimestamp";
586
+ data: Date;
587
+ driverParam: string;
588
+ notNull: true;
589
+ hasDefault: true;
590
+ isPrimaryKey: false;
591
+ isAutoincrement: false;
592
+ hasRuntimeDefault: false;
593
+ enumValues: undefined;
594
+ baseColumn: never;
595
+ identity: undefined;
596
+ generated: undefined;
597
+ }, {}, {}>;
530
598
  createdAt: import("drizzle-orm/pg-core").PgColumn<{
531
599
  name: "created_at";
532
600
  tableName: "installations";
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/db/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAKnE,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAA6C,CAAC;AAIjE,wBAAgB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAGjB;AAED,wBAAgB,OAAO,IAAI,IAAI,CAE9B"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/db/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAKnE,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAA6C,CAAC;AAIjE,wBAAgB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAGjB;AAED,wBAAgB,OAAO,IAAI,IAAI,CAE9B"}
@@ -1,5 +1,11 @@
1
1
  export type JiraInstallationLock = <T>(lockKey: string, fn: () => Promise<T>) => Promise<T>;
2
- export declare function withJiraInstallationLock<T>(lockKey: string, fn: () => Promise<T>): Promise<T>;
2
+ type JiraAdvisoryLockAttempt<T> = {
3
+ acquired: true;
4
+ value: T;
5
+ } | {
6
+ acquired: false;
7
+ };
8
+ export declare function withJiraInstallationLock<T>(lockKey: string, fn: () => Promise<T>): Promise<Awaited<T>>;
3
9
  export type JiraInstallationStatus = 'installed' | 'revoked';
4
10
  export interface JiraInstallation {
5
11
  id: string;
@@ -13,6 +19,8 @@ export interface JiraInstallation {
13
19
  webhookExpiresAt: Date | null;
14
20
  status: JiraInstallationStatus;
15
21
  tokenExpiresAt: Date | null;
22
+ refreshTokenLastUsedAt: Date;
23
+ refreshTokenLastAttemptedAt: Date;
16
24
  createdAt: Date;
17
25
  updatedAt: Date;
18
26
  }
@@ -27,6 +35,8 @@ export interface UpsertJiraInstallationParams {
27
35
  webhookExpiresAt?: Date | null | undefined;
28
36
  status: JiraInstallationStatus;
29
37
  tokenExpiresAt?: Date | null | undefined;
38
+ refreshTokenLastUsedAt?: Date | undefined;
39
+ refreshTokenLastAttemptedAt?: Date | undefined;
30
40
  }
31
41
  export interface UpdateJiraInstallationTokenExpiryParams {
32
42
  connectionId: string;
@@ -47,19 +57,24 @@ export declare function getJiraInstallationByCloudId(cloudId: string, options?:
47
57
  export declare function updateJiraInstallationTokenExpiry(params: UpdateJiraInstallationTokenExpiryParams, options?: {
48
58
  tx?: unknown;
49
59
  }): Promise<JiraInstallation | undefined>;
60
+ export declare function markJiraInstallationTokenRefreshAttempt(connectionId: string, options?: {
61
+ tx?: unknown;
62
+ }): Promise<JiraInstallation | undefined>;
63
+ export declare function listJiraInstallationsDueForTokenRefresh(params: {
64
+ before: Date;
65
+ limit?: number | undefined;
66
+ }, options?: {
67
+ tx?: unknown;
68
+ }): Promise<JiraInstallation[]>;
50
69
  export declare function updateJiraInstallationWebhook(params: UpdateJiraInstallationWebhookParams, options?: {
51
70
  tx?: unknown;
52
71
  }): Promise<JiraInstallation | undefined>;
53
72
  export declare function deleteJiraInstallationByConnectionId(connectionId: string, options?: {
54
73
  tx?: unknown;
55
74
  }): Promise<boolean>;
56
- export type JiraRefreshLockResult<T> = {
57
- acquired: true;
58
- value: T;
59
- } | {
60
- acquired: false;
61
- };
75
+ export type JiraRefreshLockResult<T> = JiraAdvisoryLockAttempt<T>;
62
76
  export declare function withJiraRefreshLock<T>(connectionId: string, fn: () => Promise<T>): Promise<JiraRefreshLockResult<T>>;
77
+ export declare function withJiraRefreshLockAndWait<T>(connectionId: string, fn: () => Promise<T>): Promise<T>;
63
78
  export declare function withJiraWebhookRegistrationLock<T>(lockKey: string, fn: (tx?: unknown) => Promise<T>): Promise<T>;
64
79
  export declare function getJiraInstallationByConnectionId(connectionId: string, options?: {
65
80
  tx?: unknown;
@@ -70,4 +85,5 @@ export declare function getJiraInstallationByWebhookId(webhookId: number, option
70
85
  export declare function markJiraInstallationRevoked(connectionId: string, options?: {
71
86
  tx?: unknown;
72
87
  }): Promise<JiraInstallation | undefined>;
88
+ export {};
73
89
  //# sourceMappingURL=installations.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"installations.d.ts","sourceRoot":"","sources":["../../src/db/installations.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"installations.d.ts","sourceRoot":"","sources":["../../src/db/installations.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;AAM5F,KAAK,uBAAuB,CAAC,CAAC,IAAI;IAAC,QAAQ,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAC,GAAG;IAAC,QAAQ,EAAE,KAAK,CAAA;CAAC,CAAC;AAEjF,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,uBAiBhF;AAED,MAAM,MAAM,sBAAsB,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7D,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,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,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,gBAAgB,EAAE,IAAI,GAAG,IAAI,CAAC;IAC9B,MAAM,EAAE,sBAAsB,CAAC;IAC/B,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,sBAAsB,EAAE,IAAI,CAAC;IAC7B,2BAA2B,EAAE,IAAI,CAAC;IAClC,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,4BAA4B;IAC3C,YAAY,EAAE,MAAM,CAAC;IACrB,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,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAClC,gBAAgB,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC;IAC3C,MAAM,EAAE,sBAAsB,CAAC;IAC/B,cAAc,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC;IACzC,sBAAsB,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;IAC1C,2BAA2B,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;CAChD;AAED,MAAM,WAAW,uCAAuC;IACtD,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CAC/B;AAED,MAAM,WAAW,mCAAmC;IAClD,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,gBAAgB,EAAE,IAAI,GAAG,IAAI,CAAC;CAC/B;AAKD,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,4BAA4B,EACpC,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAM,GAC3B,OAAO,CAAC,gBAAgB,CAAC,CAsD3B;AAED,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAM,GAC3B,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CASvC;AAED,wBAAsB,iCAAiC,CACrD,MAAM,EAAE,uCAAuC,EAC/C,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAM,GAC3B,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAevC;AAED,wBAAsB,uCAAuC,CAC3D,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAM,GAC3B,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAQvC;AAED,wBAAsB,uCAAuC,CAC3D,MAAM,EAAE;IAAC,MAAM,EAAE,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAC,EAClD,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAM,GAC3B,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAc7B;AAED,wBAAsB,6BAA6B,CACjD,MAAM,EAAE,mCAAmC,EAC3C,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAM,GAC3B,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAYvC;AAED,wBAAsB,oCAAoC,CACxD,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAM,GAC3B,OAAO,CAAC,OAAO,CAAC,CAMlB;AAED,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI,uBAAuB,CAAC,CAAC,CAAC,CAAC;AAElE,wBAAgB,mBAAmB,CAAC,CAAC,EACnC,YAAY,EAAE,MAAM,EACpB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC,CAEnC;AAED,wBAAgB,0BAA0B,CAAC,CAAC,EAC1C,YAAY,EAAE,MAAM,EACpB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAIZ;AAED,wBAAgB,+BAA+B,CAAC,CAAC,EAC/C,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,GAC/B,OAAO,CAAC,CAAC,CAAC,CAEZ;AAyCD,wBAAsB,iCAAiC,CACrD,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAM,GAC3B,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAUvC;AAED,wBAAsB,8BAA8B,CAClD,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAM,GAC3B,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAUvC;AAED,wBAAsB,2BAA2B,CAC/C,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAA;CAAM,GAC3B,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CASvC"}
@@ -1,18 +1,15 @@
1
1
  import { isUniqueViolation } from '@shipfox/node-drizzle';
2
2
  import { pgClient, withPostgresSession } from '@shipfox/node-postgres';
3
- import { eq, sql } from 'drizzle-orm';
3
+ import { and, asc, eq, lte, sql } from 'drizzle-orm';
4
4
  import { JiraConnectionAlreadyLinkedError, JiraInstallationAlreadyLinkedError, JiraInstallationSiteMismatchError } from '#core/errors.js';
5
5
  import { db } from './db.js';
6
6
  import { jiraInstallations, toJiraInstallation } from './schema/installations.js';
7
- const JIRA_INSTALLATION_LOCK_RETRY_DELAY_MS = 100;
8
- const JIRA_INSTALLATION_LOCK_MAX_RETRY_DELAY_MS = 1_000;
9
- const JIRA_INSTALLATION_LOCK_TIMEOUT_MS = 30_000;
10
- export async function withJiraInstallationLock(lockKey, fn) {
7
+ const JIRA_ADVISORY_LOCK_RETRY_DELAY_MS = 100;
8
+ const JIRA_ADVISORY_LOCK_MAX_RETRY_DELAY_MS = 1_000;
9
+ const JIRA_ADVISORY_LOCK_TIMEOUT_MS = 30_000;
10
+ export function withJiraInstallationLock(lockKey, fn) {
11
11
  const advisoryKey = `jira-installation:${lockKey}`;
12
- const deadline = Date.now() + JIRA_INSTALLATION_LOCK_TIMEOUT_MS;
13
- let retryDelayMs = JIRA_INSTALLATION_LOCK_RETRY_DELAY_MS;
14
- while(true){
15
- const attempt = await withPostgresSession(async (client)=>{
12
+ return withJiraAdvisoryLockWait(`Jira installation lock: ${lockKey}`, ()=>withPostgresSession(async (client)=>{
16
13
  const lock = await client.query('SELECT pg_try_advisory_lock(hashtext($1)) AS acquired', [
17
14
  advisoryKey
18
15
  ]);
@@ -29,14 +26,7 @@ export async function withJiraInstallationLock(lockKey, fn) {
29
26
  advisoryKey
30
27
  ]);
31
28
  }
32
- });
33
- if (attempt.acquired) return attempt.value;
34
- if (Date.now() >= deadline) {
35
- throw new Error(`Timed out waiting for Jira installation lock: ${lockKey}`);
36
- }
37
- await new Promise((resolve)=>setTimeout(resolve, retryDelayMs));
38
- retryDelayMs = Math.min(retryDelayMs * 2, JIRA_INSTALLATION_LOCK_MAX_RETRY_DELAY_MS);
39
- }
29
+ }));
40
30
  }
41
31
  export async function upsertJiraInstallation(params, options = {}) {
42
32
  const executor = options.tx ?? db();
@@ -53,7 +43,9 @@ export async function upsertJiraInstallation(params, options = {}) {
53
43
  webhookIds: params.webhookIds ?? [],
54
44
  webhookExpiresAt: params.webhookExpiresAt ?? null,
55
45
  status: params.status,
56
- tokenExpiresAt: params.tokenExpiresAt ?? null
46
+ tokenExpiresAt: params.tokenExpiresAt ?? null,
47
+ refreshTokenLastUsedAt: params.refreshTokenLastUsedAt ?? now,
48
+ refreshTokenLastAttemptedAt: params.refreshTokenLastAttemptedAt ?? now
57
49
  }).onConflictDoUpdate({
58
50
  target: jiraInstallations.connectionId,
59
51
  setWhere: eq(jiraInstallations.cloudId, params.cloudId),
@@ -71,6 +63,8 @@ export async function upsertJiraInstallation(params, options = {}) {
71
63
  },
72
64
  status: params.status,
73
65
  tokenExpiresAt: params.tokenExpiresAt ?? null,
66
+ refreshTokenLastUsedAt: params.refreshTokenLastUsedAt ?? now,
67
+ refreshTokenLastAttemptedAt: params.refreshTokenLastAttemptedAt ?? now,
74
68
  updatedAt: now
75
69
  }
76
70
  }).returning();
@@ -94,15 +88,30 @@ export async function getJiraInstallationByCloudId(cloudId, options = {}) {
94
88
  }
95
89
  export async function updateJiraInstallationTokenExpiry(params, options = {}) {
96
90
  const executor = options.tx ?? db();
91
+ const now = new Date();
97
92
  const [row] = await executor.update(jiraInstallations).set({
98
93
  tokenExpiresAt: params.tokenExpiresAt,
94
+ refreshTokenLastUsedAt: now,
95
+ refreshTokenLastAttemptedAt: now,
99
96
  ...params.scopes === undefined ? {} : {
100
97
  scopes: params.scopes
101
98
  },
102
- updatedAt: new Date()
99
+ updatedAt: now
103
100
  }).where(eq(jiraInstallations.connectionId, params.connectionId)).returning();
104
101
  return row ? toJiraInstallation(row) : undefined;
105
102
  }
103
+ export async function markJiraInstallationTokenRefreshAttempt(connectionId, options = {}) {
104
+ const executor = options.tx ?? db();
105
+ const [row] = await executor.update(jiraInstallations).set({
106
+ refreshTokenLastAttemptedAt: new Date()
107
+ }).where(eq(jiraInstallations.connectionId, connectionId)).returning();
108
+ return row ? toJiraInstallation(row) : undefined;
109
+ }
110
+ export async function listJiraInstallationsDueForTokenRefresh(params, options = {}) {
111
+ const executor = options.tx ?? db();
112
+ const rows = await executor.select().from(jiraInstallations).where(and(eq(jiraInstallations.status, 'installed'), lte(jiraInstallations.refreshTokenLastUsedAt, params.before))).orderBy(asc(jiraInstallations.refreshTokenLastAttemptedAt)).limit(params.limit ?? 100);
113
+ return rows.map(toJiraInstallation);
114
+ }
106
115
  export async function updateJiraInstallationWebhook(params, options = {}) {
107
116
  const executor = options.tx ?? db();
108
117
  const [row] = await executor.update(jiraInstallations).set({
@@ -120,6 +129,9 @@ export async function deleteJiraInstallationByConnectionId(connectionId, options
120
129
  export function withJiraRefreshLock(connectionId, fn) {
121
130
  return withJiraRefreshLockClient(connectionId, fn);
122
131
  }
132
+ export function withJiraRefreshLockAndWait(connectionId, fn) {
133
+ return withJiraAdvisoryLockWait(`Jira refresh lock: ${connectionId}`, ()=>withJiraRefreshLock(connectionId, fn));
134
+ }
123
135
  export function withJiraWebhookRegistrationLock(lockKey, fn) {
124
136
  return withJiraInstallationLock(lockKey, ()=>fn());
125
137
  }
@@ -148,6 +160,17 @@ async function withJiraRefreshLockClient(connectionId, fn) {
148
160
  }
149
161
  }
150
162
  }
163
+ async function withJiraAdvisoryLockWait(lockDescription, attemptLock) {
164
+ const deadline = Date.now() + JIRA_ADVISORY_LOCK_TIMEOUT_MS;
165
+ let retryDelayMs = JIRA_ADVISORY_LOCK_RETRY_DELAY_MS;
166
+ while(true){
167
+ const attempt = await attemptLock();
168
+ if (attempt.acquired) return attempt.value;
169
+ if (Date.now() >= deadline) throw new Error(`Timed out waiting for ${lockDescription}`);
170
+ await new Promise((resolve)=>setTimeout(resolve, retryDelayMs));
171
+ retryDelayMs = Math.min(retryDelayMs * 2, JIRA_ADVISORY_LOCK_MAX_RETRY_DELAY_MS);
172
+ }
173
+ }
151
174
  export async function getJiraInstallationByConnectionId(connectionId, options = {}) {
152
175
  const executor = options.tx ?? db();
153
176
  const rows = await executor.select().from(jiraInstallations).where(eq(jiraInstallations.connectionId, connectionId)).limit(1);