@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.
- package/.turbo/turbo-build.log +17 -2
- package/CHANGELOG.md +10 -0
- package/README.md +58 -2
- package/dist/api/client.d.ts +19 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +78 -0
- package/dist/api/client.js.map +1 -1
- package/dist/core/agent-tools-provider.d.ts +26 -0
- package/dist/core/agent-tools-provider.d.ts.map +1 -0
- package/dist/core/agent-tools-provider.js +130 -0
- package/dist/core/agent-tools-provider.js.map +1 -0
- package/dist/core/agent-tools.d.ts +16 -0
- package/dist/core/agent-tools.d.ts.map +1 -0
- package/dist/core/agent-tools.js +441 -0
- package/dist/core/agent-tools.js.map +1 -0
- package/dist/core/tokens.d.ts +2 -0
- package/dist/core/tokens.d.ts.map +1 -1
- package/dist/core/tokens.js +129 -37
- package/dist/core/tokens.js.map +1 -1
- package/dist/db/db.d.ts +68 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/installations.d.ts +23 -7
- package/dist/db/installations.d.ts.map +1 -1
- package/dist/db/installations.js +42 -19
- package/dist/db/installations.js.map +1 -1
- package/dist/db/schema/installations.d.ts +34 -0
- package/dist/db/schema/installations.d.ts.map +1 -1
- package/dist/db/schema/installations.js +12 -2
- package/dist/db/schema/installations.js.map +1 -1
- package/dist/index.d.ts +20 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -5
- package/dist/index.js.map +1 -1
- package/dist/temporal/activities/index.d.ts +2 -0
- package/dist/temporal/activities/index.d.ts.map +1 -0
- package/dist/temporal/activities/index.js +3 -0
- package/dist/temporal/activities/index.js.map +1 -0
- package/dist/temporal/activities/refresh-jira-tokens.d.ts +20 -0
- package/dist/temporal/activities/refresh-jira-tokens.d.ts.map +1 -0
- package/dist/temporal/activities/refresh-jira-tokens.js +58 -0
- package/dist/temporal/activities/refresh-jira-tokens.js.map +1 -0
- package/dist/temporal/constants.d.ts +6 -0
- package/dist/temporal/constants.d.ts.map +1 -0
- package/dist/temporal/constants.js +8 -0
- package/dist/temporal/constants.js.map +1 -0
- package/dist/temporal/worker.d.ts +9 -0
- package/dist/temporal/worker.d.ts.map +1 -0
- package/dist/temporal/worker.js +26 -0
- package/dist/temporal/worker.js.map +1 -0
- package/dist/temporal/workflows/index.bundle.js +25225 -0
- package/dist/temporal/workflows/index.bundle.meta.json +1 -0
- package/dist/temporal/workflows/index.d.ts +2 -0
- package/dist/temporal/workflows/index.d.ts.map +1 -0
- package/dist/temporal/workflows/index.js +3 -0
- package/dist/temporal/workflows/index.js.map +1 -0
- package/dist/temporal/workflows/refresh-jira-tokens-cron.d.ts +2 -0
- package/dist/temporal/workflows/refresh-jira-tokens-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/refresh-jira-tokens-cron.js +20 -0
- package/dist/temporal/workflows/refresh-jira-tokens-cron.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0001_jira-token-refresh-state.sql +12 -0
- package/drizzle/meta/0001_snapshot.json +245 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +12 -5
- package/src/api/client.test.ts +133 -12
- package/src/api/client.ts +107 -0
- package/src/core/agent-tools-provider.test.ts +299 -0
- package/src/core/agent-tools-provider.ts +175 -0
- package/src/core/agent-tools.ts +450 -0
- package/src/core/tokens-refresh.test.ts +221 -4
- package/src/core/tokens.test.ts +1 -1
- package/src/core/tokens.ts +153 -33
- package/src/db/installations.test.ts +84 -2
- package/src/db/installations.ts +81 -21
- package/src/db/schema/installations.ts +21 -1
- package/src/index.test.ts +34 -0
- package/src/index.ts +47 -3
- package/src/temporal/activities/index.ts +5 -0
- package/src/temporal/activities/refresh-jira-tokens.test.ts +156 -0
- package/src/temporal/activities/refresh-jira-tokens.ts +88 -0
- package/src/temporal/constants.ts +8 -0
- package/src/temporal/worker.ts +39 -0
- package/src/temporal/workflows/index.ts +1 -0
- package/src/temporal/workflows/refresh-jira-tokens-cron.ts +19 -0
- package/test/factories/jira-installation.ts +4 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/dist/core/tokens.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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), ()=>
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
108
|
-
await
|
|
109
|
-
|
|
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
|
package/dist/core/tokens.js.map
CHANGED
|
@@ -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";
|
package/dist/db/db.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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":"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,
|
|
1
|
+
{"version":3,"file":"installations.d.ts","sourceRoot":"","sources":["../../src/db/installations.ts"],"names":[],"mappings":"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"}
|
package/dist/db/installations.js
CHANGED
|
@@ -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
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
export
|
|
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
|
-
|
|
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:
|
|
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);
|