@shipfox/api-integration-linear 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +89 -0
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/dist/api/client.d.ts +29 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +189 -0
- package/dist/api/client.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +22 -0
- package/dist/config.js.map +1 -0
- package/dist/core/agent-tools-provider.d.ts +32 -0
- package/dist/core/agent-tools-provider.d.ts.map +1 -0
- package/dist/core/agent-tools-provider.js +63 -0
- package/dist/core/agent-tools-provider.js.map +1 -0
- package/dist/core/agent-tools.d.ts +10 -0
- package/dist/core/agent-tools.d.ts.map +1 -0
- package/dist/core/agent-tools.js +920 -0
- package/dist/core/agent-tools.js.map +1 -0
- package/dist/core/disconnect.d.ts +18 -0
- package/dist/core/disconnect.d.ts.map +1 -0
- package/dist/core/disconnect.js +23 -0
- package/dist/core/disconnect.js.map +1 -0
- package/dist/core/errors.d.ts +35 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +59 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/install.d.ts +47 -0
- package/dist/core/install.d.ts.map +1 -0
- package/dist/core/install.js +102 -0
- package/dist/core/install.js.map +1 -0
- package/dist/core/scopes.d.ts +4 -0
- package/dist/core/scopes.d.ts.map +1 -0
- package/dist/core/scopes.js +17 -0
- package/dist/core/scopes.js.map +1 -0
- package/dist/core/state.d.ts +12 -0
- package/dist/core/state.d.ts.map +1 -0
- package/dist/core/state.js +54 -0
- package/dist/core/state.js.map +1 -0
- package/dist/core/tokens.d.ts +39 -0
- package/dist/core/tokens.d.ts.map +1 -0
- package/dist/core/tokens.js +117 -0
- package/dist/core/tokens.js.map +1 -0
- package/dist/core/webhook.d.ts +16 -0
- package/dist/core/webhook.d.ts.map +1 -0
- package/dist/core/webhook.js +105 -0
- package/dist/core/webhook.js.map +1 -0
- package/dist/db/db.d.ts +369 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +18 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/installations.d.ts +53 -0
- package/dist/db/installations.d.ts.map +1 -0
- package/dist/db/installations.js +125 -0
- package/dist/db/installations.js.map +1 -0
- package/dist/db/migrations.d.ts +2 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +5 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/schema/common.d.ts +2 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +4 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/installations.d.ts +186 -0
- package/dist/db/schema/installations.d.ts.map +1 -0
- package/dist/db/schema/installations.js +40 -0
- package/dist/db/schema/installations.js.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/presentation/dto/integrations.d.ts +17 -0
- package/dist/presentation/dto/integrations.d.ts.map +1 -0
- package/dist/presentation/dto/integrations.js +6 -0
- package/dist/presentation/dto/integrations.js.map +1 -0
- package/dist/presentation/e2eRoutes/create-connection.d.ts +16 -0
- package/dist/presentation/e2eRoutes/create-connection.d.ts.map +1 -0
- package/dist/presentation/e2eRoutes/create-connection.js +64 -0
- package/dist/presentation/e2eRoutes/create-connection.js.map +1 -0
- package/dist/presentation/e2eRoutes/index.d.ts +5 -0
- package/dist/presentation/e2eRoutes/index.d.ts.map +1 -0
- package/dist/presentation/e2eRoutes/index.js +11 -0
- package/dist/presentation/e2eRoutes/index.js.map +1 -0
- package/dist/presentation/routes/errors.d.ts +2 -0
- package/dist/presentation/routes/errors.d.ts.map +1 -0
- package/dist/presentation/routes/errors.js +65 -0
- package/dist/presentation/routes/errors.js.map +1 -0
- package/dist/presentation/routes/install.d.ts +19 -0
- package/dist/presentation/routes/install.d.ts.map +1 -0
- package/dist/presentation/routes/install.js +100 -0
- package/dist/presentation/routes/install.js.map +1 -0
- package/dist/presentation/routes/webhooks.d.ts +11 -0
- package/dist/presentation/routes/webhooks.d.ts.map +1 -0
- package/dist/presentation/routes/webhooks.js +136 -0
- package/dist/presentation/routes/webhooks.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_initial.sql +15 -0
- package/drizzle/meta/0000_snapshot.json +126 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/package.json +72 -0
- package/src/api/client.test.ts +366 -0
- package/src/api/client.ts +281 -0
- package/src/config.test.ts +34 -0
- package/src/config.ts +22 -0
- package/src/connection-external-url.test.ts +64 -0
- package/src/core/agent-tools-provider.test.ts +245 -0
- package/src/core/agent-tools-provider.ts +106 -0
- package/src/core/agent-tools.ts +918 -0
- package/src/core/disconnect.test.ts +45 -0
- package/src/core/disconnect.ts +27 -0
- package/src/core/errors.ts +71 -0
- package/src/core/install.ts +162 -0
- package/src/core/scopes.ts +18 -0
- package/src/core/state.test.ts +51 -0
- package/src/core/state.ts +85 -0
- package/src/core/tokens.test.ts +281 -0
- package/src/core/tokens.ts +219 -0
- package/src/core/webhook.test.ts +318 -0
- package/src/core/webhook.ts +146 -0
- package/src/db/db.ts +18 -0
- package/src/db/installations.test.ts +182 -0
- package/src/db/installations.ts +215 -0
- package/src/db/migrations.ts +4 -0
- package/src/db/schema/common.ts +3 -0
- package/src/db/schema/installations.ts +42 -0
- package/src/index.test.ts +20 -0
- package/src/index.ts +153 -0
- package/src/presentation/dto/integrations.ts +9 -0
- package/src/presentation/e2eRoutes/create-connection.ts +85 -0
- package/src/presentation/e2eRoutes/index.test.ts +193 -0
- package/src/presentation/e2eRoutes/index.ts +14 -0
- package/src/presentation/routes/errors.ts +60 -0
- package/src/presentation/routes/install.test.ts +460 -0
- package/src/presentation/routes/install.ts +132 -0
- package/src/presentation/routes/webhooks.test.ts +407 -0
- package/src/presentation/routes/webhooks.ts +154 -0
- package/test/api-secrets.d.ts +28 -0
- package/test/env.ts +13 -0
- package/test/factories/index.ts +1 -0
- package/test/globalSetup.ts +23 -0
- package/test/index.ts +1 -0
- package/test/setup.ts +17 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +9 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type LinearApiClient } from '#api/client.js';
|
|
2
|
+
export interface LinearConnectionResolverResult {
|
|
3
|
+
workspaceId: string;
|
|
4
|
+
}
|
|
5
|
+
export interface CreateLinearTokenStoreParams {
|
|
6
|
+
resolveConnection(connectionId: string): Promise<LinearConnectionResolverResult | undefined>;
|
|
7
|
+
secrets: LinearSecretsStore;
|
|
8
|
+
client?: LinearApiClient | undefined;
|
|
9
|
+
}
|
|
10
|
+
export interface LinearSecretsStore {
|
|
11
|
+
getSecret(params: {
|
|
12
|
+
workspaceId: string;
|
|
13
|
+
namespace: string;
|
|
14
|
+
key: string;
|
|
15
|
+
}): Promise<string | null>;
|
|
16
|
+
setSecrets(params: {
|
|
17
|
+
workspaceId: string;
|
|
18
|
+
namespace: string;
|
|
19
|
+
values: Record<string, string>;
|
|
20
|
+
editedBy?: string | null | undefined;
|
|
21
|
+
}): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export interface StoreLinearTokensParams {
|
|
24
|
+
connectionId: string;
|
|
25
|
+
accessToken: string;
|
|
26
|
+
refreshToken?: string | undefined;
|
|
27
|
+
editedBy?: string | null | undefined;
|
|
28
|
+
}
|
|
29
|
+
export interface GetLinearAccessTokenParams {
|
|
30
|
+
connectionId: string;
|
|
31
|
+
forceRefresh?: boolean | undefined;
|
|
32
|
+
}
|
|
33
|
+
export interface LinearTokenStore {
|
|
34
|
+
storeTokens(params: StoreLinearTokensParams): Promise<void>;
|
|
35
|
+
getAccessToken(params: GetLinearAccessTokenParams): Promise<string>;
|
|
36
|
+
}
|
|
37
|
+
export declare function linearSecretsNamespace(connectionId: string): string;
|
|
38
|
+
export declare function createLinearTokenStore(params: CreateLinearTokenStoreParams): LinearTokenStore;
|
|
39
|
+
//# sourceMappingURL=tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/core/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAkB3E,MAAM,WAAW,8BAA8B;IAC7C,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,4BAA4B;IAC3C,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,8BAA8B,GAAG,SAAS,CAAC,CAAC;IAC7F,OAAO,EAAE,kBAAkB,CAAC;IAC5B,MAAM,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;CACtC;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,MAAM,EAAE;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACjG,UAAU,CAAC,MAAM,EAAE;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC/B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;KACtC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACtC;AAED,MAAM,WAAW,0BAA0B;IACzC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACpC;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,CAAC,MAAM,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,cAAc,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACrE;AAED,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,4BAA4B,GAAG,gBAAgB,CAwE7F"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createLinearApiClient } from '#api/client.js';
|
|
2
|
+
import { LinearAccessTokenMissingError, LinearConnectionNotFoundError, LinearIntegrationProviderError, LinearTokenUnrefreshableError } from '#core/errors.js';
|
|
3
|
+
import { getLinearInstallationByConnectionId, updateLinearInstallationTokenExpiry, withLinearRefreshLock } from '#db/installations.js';
|
|
4
|
+
const ACCESS_TOKEN_KEY = 'ACCESS_TOKEN';
|
|
5
|
+
const REFRESH_TOKEN_KEY = 'REFRESH_TOKEN';
|
|
6
|
+
const TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000;
|
|
7
|
+
const tokenRefreshes = new Map();
|
|
8
|
+
export function linearSecretsNamespace(connectionId) {
|
|
9
|
+
return `system/integrations/linear/${connectionId}`;
|
|
10
|
+
}
|
|
11
|
+
export function createLinearTokenStore(params) {
|
|
12
|
+
const client = params.client ?? createLinearApiClient();
|
|
13
|
+
async function resolveWorkspaceId(connectionId) {
|
|
14
|
+
const connection = await params.resolveConnection(connectionId);
|
|
15
|
+
if (!connection) throw new LinearConnectionNotFoundError(connectionId);
|
|
16
|
+
return connection.workspaceId;
|
|
17
|
+
}
|
|
18
|
+
function readSecretToken(workspaceId, connectionId, key) {
|
|
19
|
+
return params.secrets.getSecret({
|
|
20
|
+
workspaceId,
|
|
21
|
+
namespace: linearSecretsNamespace(connectionId),
|
|
22
|
+
key
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async function readAccessToken(workspaceId, connectionId) {
|
|
26
|
+
const token = await readSecretToken(workspaceId, connectionId, ACCESS_TOKEN_KEY);
|
|
27
|
+
if (!token) throw new LinearAccessTokenMissingError(connectionId);
|
|
28
|
+
return token;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
async storeTokens (input) {
|
|
32
|
+
const workspaceId = await resolveWorkspaceId(input.connectionId);
|
|
33
|
+
const values = {
|
|
34
|
+
[ACCESS_TOKEN_KEY]: input.accessToken
|
|
35
|
+
};
|
|
36
|
+
if (input.refreshToken) values[REFRESH_TOKEN_KEY] = input.refreshToken;
|
|
37
|
+
await params.secrets.setSecrets({
|
|
38
|
+
workspaceId,
|
|
39
|
+
namespace: linearSecretsNamespace(input.connectionId),
|
|
40
|
+
values,
|
|
41
|
+
editedBy: input.editedBy
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
async getAccessToken (input) {
|
|
45
|
+
const forceRefresh = input.forceRefresh === true;
|
|
46
|
+
const workspaceId = await resolveWorkspaceId(input.connectionId);
|
|
47
|
+
const installation = await getLinearInstallationByConnectionId(input.connectionId);
|
|
48
|
+
const accessToken = await readAccessToken(workspaceId, input.connectionId);
|
|
49
|
+
if (!forceRefresh && !shouldRefresh(installation?.tokenExpiresAt ?? null)) {
|
|
50
|
+
return accessToken;
|
|
51
|
+
}
|
|
52
|
+
const inFlightRefresh = tokenRefreshes.get(input.connectionId);
|
|
53
|
+
if (inFlightRefresh) return inFlightRefresh;
|
|
54
|
+
const refresh = refreshAccessTokenWithLock({
|
|
55
|
+
connectionId: input.connectionId,
|
|
56
|
+
workspaceId,
|
|
57
|
+
originalAccessToken: accessToken,
|
|
58
|
+
forceRefresh,
|
|
59
|
+
client,
|
|
60
|
+
secrets: params.secrets,
|
|
61
|
+
readAccessToken,
|
|
62
|
+
readSecretToken
|
|
63
|
+
});
|
|
64
|
+
tokenRefreshes.set(input.connectionId, refresh);
|
|
65
|
+
void refresh.then(()=>clearTokenRefresh(input.connectionId, refresh), ()=>clearTokenRefresh(input.connectionId, refresh));
|
|
66
|
+
return refresh;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function shouldRefresh(expiresAt) {
|
|
71
|
+
return expiresAt !== null && expiresAt.getTime() <= Date.now() + TOKEN_REFRESH_MARGIN_MS;
|
|
72
|
+
}
|
|
73
|
+
async function refreshAccessTokenWithLock(params) {
|
|
74
|
+
const lock = await withLinearRefreshLock(params.connectionId, async ()=>refreshAccessTokenForConnection(params));
|
|
75
|
+
if (lock.acquired) return lock.value;
|
|
76
|
+
const rereadToken = await params.readAccessToken(params.workspaceId, params.connectionId);
|
|
77
|
+
if (rereadToken !== params.originalAccessToken) return rereadToken;
|
|
78
|
+
if (params.forceRefresh) {
|
|
79
|
+
throw new LinearIntegrationProviderError('provider-unavailable', 'Linear token refresh is already in progress');
|
|
80
|
+
}
|
|
81
|
+
return params.originalAccessToken;
|
|
82
|
+
}
|
|
83
|
+
function clearTokenRefresh(connectionId, refresh) {
|
|
84
|
+
if (tokenRefreshes.get(connectionId) === refresh) tokenRefreshes.delete(connectionId);
|
|
85
|
+
}
|
|
86
|
+
async function refreshAccessTokenForConnection(params) {
|
|
87
|
+
const currentAccessToken = await params.readAccessToken(params.workspaceId, params.connectionId);
|
|
88
|
+
const currentInstallation = await getLinearInstallationByConnectionId(params.connectionId);
|
|
89
|
+
const tokenChanged = currentAccessToken !== params.originalAccessToken;
|
|
90
|
+
const isFresh = !params.forceRefresh && !shouldRefresh(currentInstallation?.tokenExpiresAt ?? null);
|
|
91
|
+
if (tokenChanged || isFresh) return currentAccessToken;
|
|
92
|
+
const refreshToken = await params.readSecretToken(params.workspaceId, params.connectionId, REFRESH_TOKEN_KEY);
|
|
93
|
+
if (!refreshToken) {
|
|
94
|
+
if (params.forceRefresh) throw new LinearTokenUnrefreshableError(params.connectionId);
|
|
95
|
+
return currentAccessToken;
|
|
96
|
+
}
|
|
97
|
+
const refreshed = await params.client.refreshAccessToken({
|
|
98
|
+
refreshToken
|
|
99
|
+
});
|
|
100
|
+
const values = {
|
|
101
|
+
[ACCESS_TOKEN_KEY]: refreshed.accessToken
|
|
102
|
+
};
|
|
103
|
+
if (refreshed.refreshToken) values[REFRESH_TOKEN_KEY] = refreshed.refreshToken;
|
|
104
|
+
await params.secrets.setSecrets({
|
|
105
|
+
workspaceId: params.workspaceId,
|
|
106
|
+
namespace: linearSecretsNamespace(params.connectionId),
|
|
107
|
+
values
|
|
108
|
+
});
|
|
109
|
+
await updateLinearInstallationTokenExpiry({
|
|
110
|
+
connectionId: params.connectionId,
|
|
111
|
+
tokenExpiresAt: refreshed.expiresAt ?? null,
|
|
112
|
+
scopes: refreshed.scopes.length > 0 ? refreshed.scopes : undefined
|
|
113
|
+
});
|
|
114
|
+
return refreshed.accessToken;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
//# sourceMappingURL=tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/tokens.ts"],"sourcesContent":["import {createLinearApiClient, type LinearApiClient} from '#api/client.js';\nimport {\n LinearAccessTokenMissingError,\n LinearConnectionNotFoundError,\n LinearIntegrationProviderError,\n LinearTokenUnrefreshableError,\n} from '#core/errors.js';\nimport {\n getLinearInstallationByConnectionId,\n updateLinearInstallationTokenExpiry,\n withLinearRefreshLock,\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 LinearConnectionResolverResult {\n workspaceId: string;\n}\n\nexport interface CreateLinearTokenStoreParams {\n resolveConnection(connectionId: string): Promise<LinearConnectionResolverResult | undefined>;\n secrets: LinearSecretsStore;\n client?: LinearApiClient | undefined;\n}\n\nexport interface LinearSecretsStore {\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 StoreLinearTokensParams {\n connectionId: string;\n accessToken: string;\n refreshToken?: string | undefined;\n editedBy?: string | null | undefined;\n}\n\nexport interface GetLinearAccessTokenParams {\n connectionId: string;\n forceRefresh?: boolean | undefined;\n}\n\nexport interface LinearTokenStore {\n storeTokens(params: StoreLinearTokensParams): Promise<void>;\n getAccessToken(params: GetLinearAccessTokenParams): Promise<string>;\n}\n\nexport function linearSecretsNamespace(connectionId: string): string {\n return `system/integrations/linear/${connectionId}`;\n}\n\nexport function createLinearTokenStore(params: CreateLinearTokenStoreParams): LinearTokenStore {\n const client = params.client ?? createLinearApiClient();\n\n async function resolveWorkspaceId(connectionId: string): Promise<string> {\n const connection = await params.resolveConnection(connectionId);\n if (!connection) throw new LinearConnectionNotFoundError(connectionId);\n return connection.workspaceId;\n }\n\n function readSecretToken(\n workspaceId: string,\n connectionId: string,\n key: typeof ACCESS_TOKEN_KEY | typeof REFRESH_TOKEN_KEY,\n ): Promise<string | null> {\n return params.secrets.getSecret({\n workspaceId,\n namespace: linearSecretsNamespace(connectionId),\n key,\n });\n }\n\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 LinearAccessTokenMissingError(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\n await params.secrets.setSecrets({\n workspaceId,\n namespace: linearSecretsNamespace(input.connectionId),\n values,\n editedBy: input.editedBy,\n });\n },\n\n async getAccessToken(input) {\n const forceRefresh = input.forceRefresh === true;\n const workspaceId = await resolveWorkspaceId(input.connectionId);\n const installation = await getLinearInstallationByConnectionId(input.connectionId);\n const accessToken = await readAccessToken(workspaceId, input.connectionId);\n\n if (!forceRefresh && !shouldRefresh(installation?.tokenExpiresAt ?? null)) {\n return accessToken;\n }\n\n const inFlightRefresh = tokenRefreshes.get(input.connectionId);\n if (inFlightRefresh) return inFlightRefresh;\n\n const refresh = refreshAccessTokenWithLock({\n connectionId: input.connectionId,\n workspaceId,\n originalAccessToken: accessToken,\n forceRefresh,\n client,\n secrets: params.secrets,\n readAccessToken,\n readSecretToken,\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\nasync function refreshAccessTokenWithLock(params: {\n connectionId: string;\n workspaceId: string;\n originalAccessToken: string;\n forceRefresh: boolean;\n client: LinearApiClient;\n secrets: LinearSecretsStore;\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}): Promise<string> {\n const lock = await withLinearRefreshLock(params.connectionId, async () =>\n refreshAccessTokenForConnection(params),\n );\n\n if (lock.acquired) return lock.value;\n\n const rereadToken = await params.readAccessToken(params.workspaceId, params.connectionId);\n if (rereadToken !== params.originalAccessToken) return rereadToken;\n if (params.forceRefresh) {\n throw new LinearIntegrationProviderError(\n 'provider-unavailable',\n 'Linear token refresh is already in progress',\n );\n }\n return params.originalAccessToken;\n}\n\nfunction clearTokenRefresh(connectionId: string, refresh: Promise<string>): void {\n if (tokenRefreshes.get(connectionId) === refresh) tokenRefreshes.delete(connectionId);\n}\n\nasync function refreshAccessTokenForConnection(params: {\n connectionId: string;\n workspaceId: string;\n originalAccessToken: string;\n forceRefresh: boolean;\n client: LinearApiClient;\n secrets: LinearSecretsStore;\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}): Promise<string> {\n const currentAccessToken = await params.readAccessToken(params.workspaceId, params.connectionId);\n const currentInstallation = await getLinearInstallationByConnectionId(params.connectionId);\n const tokenChanged = currentAccessToken !== params.originalAccessToken;\n const isFresh =\n !params.forceRefresh && !shouldRefresh(currentInstallation?.tokenExpiresAt ?? null);\n if (tokenChanged || isFresh) return currentAccessToken;\n\n const refreshToken = await params.readSecretToken(\n params.workspaceId,\n params.connectionId,\n REFRESH_TOKEN_KEY,\n );\n if (!refreshToken) {\n if (params.forceRefresh) throw new LinearTokenUnrefreshableError(params.connectionId);\n return currentAccessToken;\n }\n\n const refreshed = await params.client.refreshAccessToken({refreshToken});\n const values: Record<string, string> = {[ACCESS_TOKEN_KEY]: refreshed.accessToken};\n if (refreshed.refreshToken) values[REFRESH_TOKEN_KEY] = refreshed.refreshToken;\n await params.secrets.setSecrets({\n workspaceId: params.workspaceId,\n namespace: linearSecretsNamespace(params.connectionId),\n values,\n });\n await updateLinearInstallationTokenExpiry({\n connectionId: params.connectionId,\n tokenExpiresAt: refreshed.expiresAt ?? null,\n scopes: refreshed.scopes.length > 0 ? refreshed.scopes : undefined,\n });\n\n return refreshed.accessToken;\n}\n"],"names":["createLinearApiClient","LinearAccessTokenMissingError","LinearConnectionNotFoundError","LinearIntegrationProviderError","LinearTokenUnrefreshableError","getLinearInstallationByConnectionId","updateLinearInstallationTokenExpiry","withLinearRefreshLock","ACCESS_TOKEN_KEY","REFRESH_TOKEN_KEY","TOKEN_REFRESH_MARGIN_MS","tokenRefreshes","Map","linearSecretsNamespace","connectionId","createLinearTokenStore","params","client","resolveWorkspaceId","connection","resolveConnection","workspaceId","readSecretToken","key","secrets","getSecret","namespace","readAccessToken","token","storeTokens","input","values","accessToken","refreshToken","setSecrets","editedBy","getAccessToken","forceRefresh","installation","shouldRefresh","tokenExpiresAt","inFlightRefresh","get","refresh","refreshAccessTokenWithLock","originalAccessToken","set","then","clearTokenRefresh","expiresAt","getTime","Date","now","lock","refreshAccessTokenForConnection","acquired","value","rereadToken","delete","currentAccessToken","currentInstallation","tokenChanged","isFresh","refreshed","refreshAccessToken","scopes","length","undefined"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { GetIntegrationConnectionByIdFn, IntegrationTx, PublishIntegrationEventReceivedFn, RecordDeliveryOnlyFn } from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import { type LinearWebhookBaseEnvelopeDto } from '@shipfox/api-integration-linear-dto';
|
|
3
|
+
export interface HandleLinearWebhookParams {
|
|
4
|
+
tx: IntegrationTx;
|
|
5
|
+
deliveryId: string;
|
|
6
|
+
payload: LinearWebhookBaseEnvelopeDto;
|
|
7
|
+
rawPayload: unknown;
|
|
8
|
+
publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;
|
|
9
|
+
recordDeliveryOnly: RecordDeliveryOnlyFn;
|
|
10
|
+
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
11
|
+
}
|
|
12
|
+
export type HandleLinearWebhookOutcome = 'published' | 'duplicate' | 'unknown-organization' | 'revoked-installation' | 'missing-connection' | 'inactive-connection' | 'unsupported-event';
|
|
13
|
+
export declare function handleLinearWebhook(params: HandleLinearWebhookParams): Promise<{
|
|
14
|
+
outcome: HandleLinearWebhookOutcome;
|
|
15
|
+
}>;
|
|
16
|
+
//# sourceMappingURL=webhook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/core/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,8BAA8B,EAC9B,aAAa,EACb,iCAAiC,EACjC,oBAAoB,EACrB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAEL,KAAK,4BAA4B,EAIlC,MAAM,qCAAqC,CAAC;AAI7C,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,aAAa,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,4BAA4B,CAAC;IACtC,UAAU,EAAE,OAAO,CAAC;IACpB,+BAA+B,EAAE,iCAAiC,CAAC;IACnE,kBAAkB,EAAE,oBAAoB,CAAC;IACzC,4BAA4B,EAAE,8BAA8B,CAAC;CAC9D;AAED,MAAM,MAAM,0BAA0B,GAClC,WAAW,GACX,WAAW,GACX,sBAAsB,GACtB,sBAAsB,GACtB,oBAAoB,GACpB,qBAAqB,GACrB,mBAAmB,CAAC;AAExB,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,yBAAyB,GAChC,OAAO,CAAC;IAAC,OAAO,EAAE,0BAA0B,CAAA;CAAC,CAAC,CA2FhD"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { LINEAR_PROVIDER, linearWebhookEnvelopeSchema } from '@shipfox/api-integration-linear-dto';
|
|
2
|
+
import { logger } from '@shipfox/node-opentelemetry';
|
|
3
|
+
import { getLinearInstallationByOrganizationId } from '#db/installations.js';
|
|
4
|
+
export async function handleLinearWebhook(params) {
|
|
5
|
+
const installation = await getLinearInstallationByOrganizationId(params.payload.organizationId, {
|
|
6
|
+
tx: params.tx
|
|
7
|
+
});
|
|
8
|
+
if (!installation) {
|
|
9
|
+
logger().warn({
|
|
10
|
+
deliveryId: params.deliveryId,
|
|
11
|
+
organizationId: params.payload.organizationId
|
|
12
|
+
}, 'linear webhook: unknown organization, dropping');
|
|
13
|
+
await recordDeliveryOnly(params);
|
|
14
|
+
return {
|
|
15
|
+
outcome: 'unknown-organization'
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (installation.status !== 'installed') {
|
|
19
|
+
logger().info({
|
|
20
|
+
deliveryId: params.deliveryId,
|
|
21
|
+
organizationId: params.payload.organizationId,
|
|
22
|
+
connectionId: installation.connectionId,
|
|
23
|
+
status: installation.status
|
|
24
|
+
}, 'linear webhook: installation is not installed, dropping');
|
|
25
|
+
await recordDeliveryOnly(params);
|
|
26
|
+
return {
|
|
27
|
+
outcome: 'revoked-installation'
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const connection = await params.getIntegrationConnectionById(installation.connectionId, {
|
|
31
|
+
tx: params.tx
|
|
32
|
+
});
|
|
33
|
+
if (!connection) {
|
|
34
|
+
logger().warn({
|
|
35
|
+
deliveryId: params.deliveryId,
|
|
36
|
+
organizationId: params.payload.organizationId,
|
|
37
|
+
connectionId: installation.connectionId
|
|
38
|
+
}, 'linear webhook: installation has no connection, dropping');
|
|
39
|
+
await recordDeliveryOnly(params);
|
|
40
|
+
return {
|
|
41
|
+
outcome: 'missing-connection'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (connection.lifecycleStatus !== 'active') {
|
|
45
|
+
const logContext = {
|
|
46
|
+
deliveryId: params.deliveryId,
|
|
47
|
+
organizationId: params.payload.organizationId,
|
|
48
|
+
connectionId: connection.id,
|
|
49
|
+
workspaceId: connection.workspaceId,
|
|
50
|
+
lifecycleStatus: connection.lifecycleStatus
|
|
51
|
+
};
|
|
52
|
+
if (connection.lifecycleStatus === 'error') {
|
|
53
|
+
logger().warn(logContext, 'linear webhook: connection in error state, dropping');
|
|
54
|
+
} else {
|
|
55
|
+
logger().info(logContext, 'linear webhook: connection disabled, dropping');
|
|
56
|
+
}
|
|
57
|
+
await recordDeliveryOnly(params);
|
|
58
|
+
return {
|
|
59
|
+
outcome: 'inactive-connection'
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const supported = linearWebhookEnvelopeSchema.safeParse(params.payload);
|
|
63
|
+
if (!supported.success) {
|
|
64
|
+
logger().info({
|
|
65
|
+
deliveryId: params.deliveryId,
|
|
66
|
+
organizationId: params.payload.organizationId,
|
|
67
|
+
type: params.payload.type,
|
|
68
|
+
action: params.payload.action
|
|
69
|
+
}, 'linear webhook: unsupported event, dropping');
|
|
70
|
+
await recordDeliveryOnly(params);
|
|
71
|
+
return {
|
|
72
|
+
outcome: 'unsupported-event'
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const result = await params.publishIntegrationEventReceived({
|
|
76
|
+
tx: params.tx,
|
|
77
|
+
event: {
|
|
78
|
+
provider: LINEAR_PROVIDER,
|
|
79
|
+
source: connection.slug,
|
|
80
|
+
event: linearWebhookEventName(supported.data),
|
|
81
|
+
workspaceId: connection.workspaceId,
|
|
82
|
+
connectionId: connection.id,
|
|
83
|
+
connectionName: connection.displayName,
|
|
84
|
+
deliveryId: params.deliveryId,
|
|
85
|
+
receivedAt: new Date().toISOString(),
|
|
86
|
+
payload: params.rawPayload
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
outcome: result.published ? 'published' : 'duplicate'
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function linearWebhookEventName(payload) {
|
|
94
|
+
if (payload.type === 'AgentSessionEvent') return `agentSession.${payload.action}`;
|
|
95
|
+
return `${payload.type}.${payload.action}`;
|
|
96
|
+
}
|
|
97
|
+
async function recordDeliveryOnly(params) {
|
|
98
|
+
await params.recordDeliveryOnly({
|
|
99
|
+
tx: params.tx,
|
|
100
|
+
provider: LINEAR_PROVIDER,
|
|
101
|
+
deliveryId: params.deliveryId
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
//# sourceMappingURL=webhook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/webhook.ts"],"sourcesContent":["import type {\n GetIntegrationConnectionByIdFn,\n IntegrationTx,\n PublishIntegrationEventReceivedFn,\n RecordDeliveryOnlyFn,\n} from '@shipfox/api-integration-core-dto';\nimport {\n LINEAR_PROVIDER,\n type LinearWebhookBaseEnvelopeDto,\n type LinearWebhookEnvelopeDto,\n type LinearWebhookEventName,\n linearWebhookEnvelopeSchema,\n} from '@shipfox/api-integration-linear-dto';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {getLinearInstallationByOrganizationId} from '#db/installations.js';\n\nexport interface HandleLinearWebhookParams {\n tx: IntegrationTx;\n deliveryId: string;\n payload: LinearWebhookBaseEnvelopeDto;\n rawPayload: unknown;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n}\n\nexport type HandleLinearWebhookOutcome =\n | 'published'\n | 'duplicate'\n | 'unknown-organization'\n | 'revoked-installation'\n | 'missing-connection'\n | 'inactive-connection'\n | 'unsupported-event';\n\nexport async function handleLinearWebhook(\n params: HandleLinearWebhookParams,\n): Promise<{outcome: HandleLinearWebhookOutcome}> {\n const installation = await getLinearInstallationByOrganizationId(params.payload.organizationId, {\n tx: params.tx,\n });\n if (!installation) {\n logger().warn(\n {deliveryId: params.deliveryId, organizationId: params.payload.organizationId},\n 'linear webhook: unknown organization, dropping',\n );\n await recordDeliveryOnly(params);\n return {outcome: 'unknown-organization'};\n }\n\n if (installation.status !== 'installed') {\n logger().info(\n {\n deliveryId: params.deliveryId,\n organizationId: params.payload.organizationId,\n connectionId: installation.connectionId,\n status: installation.status,\n },\n 'linear webhook: installation is not installed, dropping',\n );\n await recordDeliveryOnly(params);\n return {outcome: 'revoked-installation'};\n }\n\n const connection = await params.getIntegrationConnectionById(installation.connectionId, {\n tx: params.tx,\n });\n if (!connection) {\n logger().warn(\n {\n deliveryId: params.deliveryId,\n organizationId: params.payload.organizationId,\n connectionId: installation.connectionId,\n },\n 'linear webhook: installation has no connection, dropping',\n );\n await recordDeliveryOnly(params);\n return {outcome: 'missing-connection'};\n }\n\n if (connection.lifecycleStatus !== 'active') {\n const logContext = {\n deliveryId: params.deliveryId,\n organizationId: params.payload.organizationId,\n connectionId: connection.id,\n workspaceId: connection.workspaceId,\n lifecycleStatus: connection.lifecycleStatus,\n };\n if (connection.lifecycleStatus === 'error') {\n logger().warn(logContext, 'linear webhook: connection in error state, dropping');\n } else {\n logger().info(logContext, 'linear webhook: connection disabled, dropping');\n }\n await recordDeliveryOnly(params);\n return {outcome: 'inactive-connection'};\n }\n\n const supported = linearWebhookEnvelopeSchema.safeParse(params.payload);\n if (!supported.success) {\n logger().info(\n {\n deliveryId: params.deliveryId,\n organizationId: params.payload.organizationId,\n type: params.payload.type,\n action: params.payload.action,\n },\n 'linear webhook: unsupported event, dropping',\n );\n await recordDeliveryOnly(params);\n return {outcome: 'unsupported-event'};\n }\n\n const result = await params.publishIntegrationEventReceived({\n tx: params.tx,\n event: {\n provider: LINEAR_PROVIDER,\n source: connection.slug,\n event: linearWebhookEventName(supported.data),\n workspaceId: connection.workspaceId,\n connectionId: connection.id,\n connectionName: connection.displayName,\n deliveryId: params.deliveryId,\n receivedAt: new Date().toISOString(),\n payload: params.rawPayload,\n },\n });\n\n return {outcome: result.published ? 'published' : 'duplicate'};\n}\n\nfunction linearWebhookEventName(payload: LinearWebhookEnvelopeDto): LinearWebhookEventName {\n if (payload.type === 'AgentSessionEvent') return `agentSession.${payload.action}`;\n return `${payload.type}.${payload.action}`;\n}\n\nasync function recordDeliveryOnly(params: {\n tx: IntegrationTx;\n deliveryId: string;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n}): Promise<void> {\n await params.recordDeliveryOnly({\n tx: params.tx,\n provider: LINEAR_PROVIDER,\n deliveryId: params.deliveryId,\n });\n}\n"],"names":["LINEAR_PROVIDER","linearWebhookEnvelopeSchema","logger","getLinearInstallationByOrganizationId","handleLinearWebhook","params","installation","payload","organizationId","tx","warn","deliveryId","recordDeliveryOnly","outcome","status","info","connectionId","connection","getIntegrationConnectionById","lifecycleStatus","logContext","id","workspaceId","supported","safeParse","success","type","action","result","publishIntegrationEventReceived","event","provider","source","slug","linearWebhookEventName","data","connectionName","displayName","receivedAt","Date","toISOString","rawPayload","published"],"mappings":"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"}
|