@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,19 @@
|
|
|
1
|
+
import type { IntegrationCapability, IntegrationConnection } from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import { type RouteGroup } from '@shipfox/node-fastify';
|
|
3
|
+
import type { LinearApiClient } from '#api/client.js';
|
|
4
|
+
import { type ConnectLinearInstallationInput } from '#core/install.js';
|
|
5
|
+
import type { LinearTokenStore } from '#core/tokens.js';
|
|
6
|
+
export interface CreateLinearIntegrationRoutesOptions {
|
|
7
|
+
linear: LinearApiClient;
|
|
8
|
+
tokenStore: Pick<LinearTokenStore, 'storeTokens'>;
|
|
9
|
+
getExistingLinearConnection: (input: {
|
|
10
|
+
organizationId: string;
|
|
11
|
+
}) => Promise<IntegrationConnection<'linear'> | undefined>;
|
|
12
|
+
connectLinearInstallation: (input: ConnectLinearInstallationInput) => Promise<IntegrationConnection<'linear'>>;
|
|
13
|
+
disconnectLinearInstallation: (input: {
|
|
14
|
+
connectionId: string;
|
|
15
|
+
}) => Promise<void>;
|
|
16
|
+
connectionCapabilities: IntegrationCapability[];
|
|
17
|
+
}
|
|
18
|
+
export declare function createLinearIntegrationRoutes({ linear, tokenStore, getExistingLinearConnection, connectLinearInstallation, disconnectLinearInstallation, connectionCapabilities, }: CreateLinearIntegrationRoutesOptions): RouteGroup;
|
|
19
|
+
//# sourceMappingURL=install.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/install.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,qBAAqB,EAAE,qBAAqB,EAAC,MAAM,mCAAmC,CAAC;AASpG,OAAO,EAAc,KAAK,UAAU,EAAC,MAAM,uBAAuB,CAAC;AACnE,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAEpD,OAAO,EACL,KAAK,8BAA8B,EAGpC,MAAM,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,iBAAiB,CAAC;AAItD,MAAM,WAAW,oCAAoC;IACnD,MAAM,EAAE,eAAe,CAAC;IACxB,UAAU,EAAE,IAAI,CAAC,gBAAgB,EAAE,aAAa,CAAC,CAAC;IAClD,2BAA2B,EAAE,CAAC,KAAK,EAAE;QACnC,cAAc,EAAE,MAAM,CAAC;KACxB,KAAK,OAAO,CAAC,qBAAqB,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAC,CAAC;IAC3D,yBAAyB,EAAE,CACzB,KAAK,EAAE,8BAA8B,KAClC,OAAO,CAAC,qBAAqB,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC9C,4BAA4B,EAAE,CAAC,KAAK,EAAE;QAAC,YAAY,EAAE,MAAM,CAAA;KAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,sBAAsB,EAAE,qBAAqB,EAAE,CAAC;CACjD;AAED,wBAAgB,6BAA6B,CAAC,EAC5C,MAAM,EACN,UAAU,EACV,2BAA2B,EAC3B,yBAAyB,EACzB,4BAA4B,EAC5B,sBAAsB,GACvB,EAAE,oCAAoC,GAAG,UAAU,CA6EnD"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { AUTH_USER, requireUserContext, requireWorkspaceAccess } from '@shipfox/api-auth-context';
|
|
2
|
+
import { createLinearInstallBodySchema, createLinearInstallResponseSchema, linearCallbackQuerySchema, linearCallbackResponseSchema } from '@shipfox/api-integration-linear-dto';
|
|
3
|
+
import { requireWorkspaceMembership } from '@shipfox/api-workspaces';
|
|
4
|
+
import { defineRoute } from '@shipfox/node-fastify';
|
|
5
|
+
import { config } from '#config.js';
|
|
6
|
+
import { handleLinearCallback, handleLinearOAuthCallbackError } from '#core/install.js';
|
|
7
|
+
import { formatLinearOAuthScopes } from '#core/scopes.js';
|
|
8
|
+
import { signLinearInstallState } from '#core/state.js';
|
|
9
|
+
import { toIntegrationConnectionDto } from '#presentation/dto/integrations.js';
|
|
10
|
+
import { linearRouteErrorHandler } from './errors.js';
|
|
11
|
+
export function createLinearIntegrationRoutes({ linear, tokenStore, getExistingLinearConnection, connectLinearInstallation, disconnectLinearInstallation, connectionCapabilities }) {
|
|
12
|
+
const createInstallRoute = defineRoute({
|
|
13
|
+
method: 'POST',
|
|
14
|
+
path: '/install',
|
|
15
|
+
auth: AUTH_USER,
|
|
16
|
+
description: 'Create a Linear OAuth authorization URL for a workspace.',
|
|
17
|
+
schema: {
|
|
18
|
+
body: createLinearInstallBodySchema,
|
|
19
|
+
response: {
|
|
20
|
+
200: createLinearInstallResponseSchema
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
handler: (request)=>{
|
|
24
|
+
const { workspace_id: workspaceId } = request.body;
|
|
25
|
+
const actor = requireUserContext(request);
|
|
26
|
+
requireWorkspaceAccess({
|
|
27
|
+
request,
|
|
28
|
+
workspaceId
|
|
29
|
+
});
|
|
30
|
+
const state = signLinearInstallState({
|
|
31
|
+
workspaceId,
|
|
32
|
+
userId: actor.userId
|
|
33
|
+
});
|
|
34
|
+
const installUrl = new URL('https://linear.app/oauth/authorize');
|
|
35
|
+
installUrl.searchParams.set('client_id', config.LINEAR_OAUTH_CLIENT_ID);
|
|
36
|
+
installUrl.searchParams.set('redirect_uri', config.LINEAR_OAUTH_REDIRECT_URL);
|
|
37
|
+
installUrl.searchParams.set('response_type', 'code');
|
|
38
|
+
installUrl.searchParams.set('state', state);
|
|
39
|
+
installUrl.searchParams.set('actor', 'app');
|
|
40
|
+
installUrl.searchParams.set('scope', formatLinearOAuthScopes());
|
|
41
|
+
return {
|
|
42
|
+
install_url: installUrl.toString()
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
const callbackApiRoute = defineRoute({
|
|
47
|
+
method: 'GET',
|
|
48
|
+
path: '/callback/api',
|
|
49
|
+
auth: AUTH_USER,
|
|
50
|
+
description: 'Handle the Linear OAuth callback.',
|
|
51
|
+
schema: {
|
|
52
|
+
querystring: linearCallbackQuerySchema,
|
|
53
|
+
response: {
|
|
54
|
+
200: linearCallbackResponseSchema
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
errorHandler: linearRouteErrorHandler,
|
|
58
|
+
handler: async (request)=>{
|
|
59
|
+
const actor = requireUserContext(request);
|
|
60
|
+
const query = request.query;
|
|
61
|
+
if (isLinearOAuthErrorCallback(query)) {
|
|
62
|
+
return await handleLinearOAuthCallbackError({
|
|
63
|
+
state: query.state,
|
|
64
|
+
error: query.error,
|
|
65
|
+
errorDescription: query.error_description,
|
|
66
|
+
sessionUserId: actor.userId,
|
|
67
|
+
sessionMemberships: actor.memberships,
|
|
68
|
+
requireWorkspaceMembership
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const connection = await handleLinearCallback({
|
|
72
|
+
linear,
|
|
73
|
+
tokenStore,
|
|
74
|
+
code: query.code,
|
|
75
|
+
state: query.state,
|
|
76
|
+
sessionUserId: actor.userId,
|
|
77
|
+
sessionMemberships: actor.memberships,
|
|
78
|
+
requireWorkspaceMembership,
|
|
79
|
+
getExistingLinearConnection,
|
|
80
|
+
connectLinearInstallation,
|
|
81
|
+
disconnectLinearInstallation
|
|
82
|
+
});
|
|
83
|
+
return toIntegrationConnectionDto(connection, {
|
|
84
|
+
capabilities: connectionCapabilities
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
prefix: '/integrations/linear',
|
|
90
|
+
routes: [
|
|
91
|
+
createInstallRoute,
|
|
92
|
+
callbackApiRoute
|
|
93
|
+
]
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function isLinearOAuthErrorCallback(query) {
|
|
97
|
+
return 'error' in query && typeof query.error === 'string';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/install.ts"],"sourcesContent":["import {AUTH_USER, requireUserContext, requireWorkspaceAccess} from '@shipfox/api-auth-context';\nimport type {IntegrationCapability, IntegrationConnection} from '@shipfox/api-integration-core-dto';\nimport {\n createLinearInstallBodySchema,\n createLinearInstallResponseSchema,\n type LinearCallbackQueryDto,\n linearCallbackQuerySchema,\n linearCallbackResponseSchema,\n} from '@shipfox/api-integration-linear-dto';\nimport {requireWorkspaceMembership} from '@shipfox/api-workspaces';\nimport {defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {LinearApiClient} from '#api/client.js';\nimport {config} from '#config.js';\nimport {\n type ConnectLinearInstallationInput,\n handleLinearCallback,\n handleLinearOAuthCallbackError,\n} from '#core/install.js';\nimport {formatLinearOAuthScopes} from '#core/scopes.js';\nimport {signLinearInstallState} from '#core/state.js';\nimport type {LinearTokenStore} from '#core/tokens.js';\nimport {toIntegrationConnectionDto} from '#presentation/dto/integrations.js';\nimport {linearRouteErrorHandler} from './errors.js';\n\nexport interface CreateLinearIntegrationRoutesOptions {\n linear: LinearApiClient;\n tokenStore: Pick<LinearTokenStore, 'storeTokens'>;\n getExistingLinearConnection: (input: {\n organizationId: string;\n }) => Promise<IntegrationConnection<'linear'> | undefined>;\n connectLinearInstallation: (\n input: ConnectLinearInstallationInput,\n ) => Promise<IntegrationConnection<'linear'>>;\n disconnectLinearInstallation: (input: {connectionId: string}) => Promise<void>;\n connectionCapabilities: IntegrationCapability[];\n}\n\nexport function createLinearIntegrationRoutes({\n linear,\n tokenStore,\n getExistingLinearConnection,\n connectLinearInstallation,\n disconnectLinearInstallation,\n connectionCapabilities,\n}: CreateLinearIntegrationRoutesOptions): RouteGroup {\n const createInstallRoute = defineRoute({\n method: 'POST',\n path: '/install',\n auth: AUTH_USER,\n description: 'Create a Linear OAuth authorization URL for a workspace.',\n schema: {\n body: createLinearInstallBodySchema,\n response: {\n 200: createLinearInstallResponseSchema,\n },\n },\n handler: (request) => {\n const {workspace_id: workspaceId} = request.body;\n const actor = requireUserContext(request);\n\n requireWorkspaceAccess({request, workspaceId});\n\n const state = signLinearInstallState({workspaceId, userId: actor.userId});\n const installUrl = new URL('https://linear.app/oauth/authorize');\n installUrl.searchParams.set('client_id', config.LINEAR_OAUTH_CLIENT_ID);\n installUrl.searchParams.set('redirect_uri', config.LINEAR_OAUTH_REDIRECT_URL);\n installUrl.searchParams.set('response_type', 'code');\n installUrl.searchParams.set('state', state);\n installUrl.searchParams.set('actor', 'app');\n installUrl.searchParams.set('scope', formatLinearOAuthScopes());\n\n return {install_url: installUrl.toString()};\n },\n });\n\n const callbackApiRoute = defineRoute({\n method: 'GET',\n path: '/callback/api',\n auth: AUTH_USER,\n description: 'Handle the Linear OAuth callback.',\n schema: {\n querystring: linearCallbackQuerySchema,\n response: {\n 200: linearCallbackResponseSchema,\n },\n },\n errorHandler: linearRouteErrorHandler,\n handler: async (request) => {\n const actor = requireUserContext(request);\n const query = request.query;\n if (isLinearOAuthErrorCallback(query)) {\n return await handleLinearOAuthCallbackError({\n state: query.state,\n error: query.error,\n errorDescription: query.error_description,\n sessionUserId: actor.userId,\n sessionMemberships: actor.memberships,\n requireWorkspaceMembership,\n });\n }\n const connection = await handleLinearCallback({\n linear,\n tokenStore,\n code: query.code,\n state: query.state,\n sessionUserId: actor.userId,\n sessionMemberships: actor.memberships,\n requireWorkspaceMembership,\n getExistingLinearConnection,\n connectLinearInstallation,\n disconnectLinearInstallation,\n });\n\n return toIntegrationConnectionDto(connection, {capabilities: connectionCapabilities});\n },\n });\n\n return {\n prefix: '/integrations/linear',\n routes: [createInstallRoute, callbackApiRoute],\n };\n}\n\nfunction isLinearOAuthErrorCallback(\n query: LinearCallbackQueryDto,\n): query is LinearCallbackQueryDto & {\n error: string;\n error_description?: string | undefined;\n state: string;\n} {\n return 'error' in query && typeof query.error === 'string';\n}\n"],"names":["AUTH_USER","requireUserContext","requireWorkspaceAccess","createLinearInstallBodySchema","createLinearInstallResponseSchema","linearCallbackQuerySchema","linearCallbackResponseSchema","requireWorkspaceMembership","defineRoute","config","handleLinearCallback","handleLinearOAuthCallbackError","formatLinearOAuthScopes","signLinearInstallState","toIntegrationConnectionDto","linearRouteErrorHandler","createLinearIntegrationRoutes","linear","tokenStore","getExistingLinearConnection","connectLinearInstallation","disconnectLinearInstallation","connectionCapabilities","createInstallRoute","method","path","auth","description","schema","body","response","handler","request","workspace_id","workspaceId","actor","state","userId","installUrl","URL","searchParams","set","LINEAR_OAUTH_CLIENT_ID","LINEAR_OAUTH_REDIRECT_URL","install_url","toString","callbackApiRoute","querystring","errorHandler","query","isLinearOAuthErrorCallback","error","errorDescription","error_description","sessionUserId","sessionMemberships","memberships","connection","code","capabilities","prefix","routes"],"mappings":"AAAA,SAAQA,SAAS,EAAEC,kBAAkB,EAAEC,sBAAsB,QAAO,4BAA4B;AAEhG,SACEC,6BAA6B,EAC7BC,iCAAiC,EAEjCC,yBAAyB,EACzBC,4BAA4B,QACvB,sCAAsC;AAC7C,SAAQC,0BAA0B,QAAO,0BAA0B;AACnE,SAAQC,WAAW,QAAwB,wBAAwB;AAEnE,SAAQC,MAAM,QAAO,aAAa;AAClC,SAEEC,oBAAoB,EACpBC,8BAA8B,QACzB,mBAAmB;AAC1B,SAAQC,uBAAuB,QAAO,kBAAkB;AACxD,SAAQC,sBAAsB,QAAO,iBAAiB;AAEtD,SAAQC,0BAA0B,QAAO,oCAAoC;AAC7E,SAAQC,uBAAuB,QAAO,cAAc;AAepD,OAAO,SAASC,8BAA8B,EAC5CC,MAAM,EACNC,UAAU,EACVC,2BAA2B,EAC3BC,yBAAyB,EACzBC,4BAA4B,EAC5BC,sBAAsB,EACe;IACrC,MAAMC,qBAAqBf,YAAY;QACrCgB,QAAQ;QACRC,MAAM;QACNC,MAAM1B;QACN2B,aAAa;QACbC,QAAQ;YACNC,MAAM1B;YACN2B,UAAU;gBACR,KAAK1B;YACP;QACF;QACA2B,SAAS,CAACC;YACR,MAAM,EAACC,cAAcC,WAAW,EAAC,GAAGF,QAAQH,IAAI;YAChD,MAAMM,QAAQlC,mBAAmB+B;YAEjC9B,uBAAuB;gBAAC8B;gBAASE;YAAW;YAE5C,MAAME,QAAQvB,uBAAuB;gBAACqB;gBAAaG,QAAQF,MAAME,MAAM;YAAA;YACvE,MAAMC,aAAa,IAAIC,IAAI;YAC3BD,WAAWE,YAAY,CAACC,GAAG,CAAC,aAAahC,OAAOiC,sBAAsB;YACtEJ,WAAWE,YAAY,CAACC,GAAG,CAAC,gBAAgBhC,OAAOkC,yBAAyB;YAC5EL,WAAWE,YAAY,CAACC,GAAG,CAAC,iBAAiB;YAC7CH,WAAWE,YAAY,CAACC,GAAG,CAAC,SAASL;YACrCE,WAAWE,YAAY,CAACC,GAAG,CAAC,SAAS;YACrCH,WAAWE,YAAY,CAACC,GAAG,CAAC,SAAS7B;YAErC,OAAO;gBAACgC,aAAaN,WAAWO,QAAQ;YAAE;QAC5C;IACF;IAEA,MAAMC,mBAAmBtC,YAAY;QACnCgB,QAAQ;QACRC,MAAM;QACNC,MAAM1B;QACN2B,aAAa;QACbC,QAAQ;YACNmB,aAAa1C;YACbyB,UAAU;gBACR,KAAKxB;YACP;QACF;QACA0C,cAAcjC;QACdgB,SAAS,OAAOC;YACd,MAAMG,QAAQlC,mBAAmB+B;YACjC,MAAMiB,QAAQjB,QAAQiB,KAAK;YAC3B,IAAIC,2BAA2BD,QAAQ;gBACrC,OAAO,MAAMtC,+BAA+B;oBAC1CyB,OAAOa,MAAMb,KAAK;oBAClBe,OAAOF,MAAME,KAAK;oBAClBC,kBAAkBH,MAAMI,iBAAiB;oBACzCC,eAAenB,MAAME,MAAM;oBAC3BkB,oBAAoBpB,MAAMqB,WAAW;oBACrCjD;gBACF;YACF;YACA,MAAMkD,aAAa,MAAM/C,qBAAqB;gBAC5CO;gBACAC;gBACAwC,MAAMT,MAAMS,IAAI;gBAChBtB,OAAOa,MAAMb,KAAK;gBAClBkB,eAAenB,MAAME,MAAM;gBAC3BkB,oBAAoBpB,MAAMqB,WAAW;gBACrCjD;gBACAY;gBACAC;gBACAC;YACF;YAEA,OAAOP,2BAA2B2C,YAAY;gBAACE,cAAcrC;YAAsB;QACrF;IACF;IAEA,OAAO;QACLsC,QAAQ;QACRC,QAAQ;YAACtC;YAAoBuB;SAAiB;IAChD;AACF;AAEA,SAASI,2BACPD,KAA6B;IAM7B,OAAO,WAAWA,SAAS,OAAOA,MAAME,KAAK,KAAK;AACpD"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { GetIntegrationConnectionByIdFn, PublishIntegrationEventReceivedFn, RecordDeliveryOnlyFn } from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import { type RouteGroup } from '@shipfox/node-fastify';
|
|
3
|
+
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
4
|
+
export interface CreateLinearWebhookRoutesOptions {
|
|
5
|
+
coreDb: () => NodePgDatabase<Record<string, unknown>>;
|
|
6
|
+
publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;
|
|
7
|
+
recordDeliveryOnly: RecordDeliveryOnlyFn;
|
|
8
|
+
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
9
|
+
}
|
|
10
|
+
export declare function createLinearWebhookRoutes(options: CreateLinearWebhookRoutesOptions): RouteGroup;
|
|
11
|
+
//# sourceMappingURL=webhooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/webhooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,8BAA8B,EAE9B,iCAAiC,EACjC,oBAAoB,EACrB,MAAM,mCAAmC,CAAC;AAK3C,OAAO,EAEL,KAAK,UAAU,EAIhB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAS9D,MAAM,WAAW,gCAAgC;IAC/C,MAAM,EAAE,MAAM,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,+BAA+B,EAAE,iCAAiC,CAAC;IACnE,kBAAkB,EAAE,oBAAoB,CAAC;IACzC,4BAA4B,EAAE,8BAA8B,CAAC;CAC9D;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gCAAgC,GAAG,UAAU,CAyG/F"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { LINEAR_PROVIDER, linearWebhookBaseEnvelopeSchema } from '@shipfox/api-integration-linear-dto';
|
|
3
|
+
import { defineRoute, rawBodyPlugin, verifyHexHmacSignature, WEBHOOK_BODY_LIMIT } from '@shipfox/node-fastify';
|
|
4
|
+
import { logger } from '@shipfox/node-opentelemetry';
|
|
5
|
+
import { config } from '#config.js';
|
|
6
|
+
import { handleLinearWebhook } from '#core/webhook.js';
|
|
7
|
+
const DELIVERY_HEADER = 'linear-delivery';
|
|
8
|
+
const EVENT_HEADER = 'linear-event';
|
|
9
|
+
const SIGNATURE_HEADER = 'linear-signature';
|
|
10
|
+
const WEBHOOK_REPLAY_WINDOW_MS = 60_000;
|
|
11
|
+
export function createLinearWebhookRoutes(options) {
|
|
12
|
+
const webhookRoute = defineRoute({
|
|
13
|
+
method: 'POST',
|
|
14
|
+
path: '/',
|
|
15
|
+
auth: [],
|
|
16
|
+
description: 'Linear webhook receiver.',
|
|
17
|
+
options: {
|
|
18
|
+
bodyLimit: WEBHOOK_BODY_LIMIT
|
|
19
|
+
},
|
|
20
|
+
handler: async (request, reply)=>{
|
|
21
|
+
const deliveryHeader = request.headers[DELIVERY_HEADER];
|
|
22
|
+
const event = request.headers[EVENT_HEADER];
|
|
23
|
+
const signature = request.headers[SIGNATURE_HEADER];
|
|
24
|
+
if (typeof deliveryHeader !== 'string' || !deliveryHeader) {
|
|
25
|
+
reply.code(400);
|
|
26
|
+
return {
|
|
27
|
+
error: 'missing Linear-Delivery header'
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (typeof event !== 'string' || !event) {
|
|
31
|
+
reply.code(400);
|
|
32
|
+
return {
|
|
33
|
+
error: 'missing Linear-Event header'
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (typeof signature !== 'string' || !signature) {
|
|
37
|
+
reply.code(401);
|
|
38
|
+
return {
|
|
39
|
+
error: 'missing Linear-Signature header'
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const body = request.body;
|
|
43
|
+
if (!Buffer.isBuffer(body)) {
|
|
44
|
+
reply.code(400);
|
|
45
|
+
return {
|
|
46
|
+
error: 'expected raw JSON body'
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const rawBody = body.toString('utf8');
|
|
50
|
+
if (!verifyHexHmacSignature({
|
|
51
|
+
rawBody,
|
|
52
|
+
signature,
|
|
53
|
+
secret: config.LINEAR_WEBHOOK_SIGNING_SECRET
|
|
54
|
+
})) {
|
|
55
|
+
reply.code(401);
|
|
56
|
+
return {
|
|
57
|
+
error: 'invalid signature'
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
let parsedJson;
|
|
61
|
+
try {
|
|
62
|
+
parsedJson = JSON.parse(rawBody);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
logger().warn({
|
|
65
|
+
deliveryId: deliveryHeader,
|
|
66
|
+
err: error
|
|
67
|
+
}, 'linear webhook payload JSON parse failed');
|
|
68
|
+
reply.code(400);
|
|
69
|
+
return {
|
|
70
|
+
error: 'malformed JSON'
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const payload = linearWebhookBaseEnvelopeSchema.safeParse(parsedJson);
|
|
74
|
+
if (!payload.success) {
|
|
75
|
+
logger().warn({
|
|
76
|
+
deliveryId: deliveryHeader,
|
|
77
|
+
issues: payload.error.issues
|
|
78
|
+
}, 'linear webhook envelope failed schema validation');
|
|
79
|
+
await recordSignedDeliveryOnly(options, deliveryHeader);
|
|
80
|
+
reply.code(200);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (Math.abs(Date.now() - payload.data.webhookTimestamp) > WEBHOOK_REPLAY_WINDOW_MS) {
|
|
84
|
+
reply.code(401);
|
|
85
|
+
return {
|
|
86
|
+
error: 'stale webhook timestamp'
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (event !== payload.data.type) {
|
|
90
|
+
logger().warn({
|
|
91
|
+
deliveryId: deliveryHeader,
|
|
92
|
+
event,
|
|
93
|
+
type: payload.data.type
|
|
94
|
+
}, 'linear webhook event header did not match payload type');
|
|
95
|
+
await recordSignedDeliveryOnly(options, deliveryHeader);
|
|
96
|
+
reply.code(200);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
await options.coreDb().transaction(async (tx)=>{
|
|
100
|
+
await handleLinearWebhook({
|
|
101
|
+
tx,
|
|
102
|
+
// Linear documents this header as the delivery UUID; the signed timestamp is per-send.
|
|
103
|
+
deliveryId: deliveryHeader,
|
|
104
|
+
payload: payload.data,
|
|
105
|
+
rawPayload: parsedJson,
|
|
106
|
+
publishIntegrationEventReceived: options.publishIntegrationEventReceived,
|
|
107
|
+
recordDeliveryOnly: options.recordDeliveryOnly,
|
|
108
|
+
getIntegrationConnectionById: options.getIntegrationConnectionById
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
reply.code(200);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
prefix: '/webhooks/integrations/linear',
|
|
117
|
+
auth: [],
|
|
118
|
+
plugins: [
|
|
119
|
+
rawBodyPlugin
|
|
120
|
+
],
|
|
121
|
+
routes: [
|
|
122
|
+
webhookRoute
|
|
123
|
+
]
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async function recordSignedDeliveryOnly(options, deliveryId) {
|
|
127
|
+
await options.coreDb().transaction(async (tx)=>{
|
|
128
|
+
await options.recordDeliveryOnly({
|
|
129
|
+
tx,
|
|
130
|
+
provider: LINEAR_PROVIDER,
|
|
131
|
+
deliveryId
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
//# sourceMappingURL=webhooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/webhooks.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport type {\n GetIntegrationConnectionByIdFn,\n IntegrationTx,\n PublishIntegrationEventReceivedFn,\n RecordDeliveryOnlyFn,\n} from '@shipfox/api-integration-core-dto';\nimport {\n LINEAR_PROVIDER,\n linearWebhookBaseEnvelopeSchema,\n} from '@shipfox/api-integration-linear-dto';\nimport {\n defineRoute,\n type RouteGroup,\n rawBodyPlugin,\n verifyHexHmacSignature,\n WEBHOOK_BODY_LIMIT,\n} from '@shipfox/node-fastify';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport type {NodePgDatabase} from 'drizzle-orm/node-postgres';\nimport {config} from '#config.js';\nimport {handleLinearWebhook} from '#core/webhook.js';\n\nconst DELIVERY_HEADER = 'linear-delivery';\nconst EVENT_HEADER = 'linear-event';\nconst SIGNATURE_HEADER = 'linear-signature';\nconst WEBHOOK_REPLAY_WINDOW_MS = 60_000;\n\nexport interface CreateLinearWebhookRoutesOptions {\n coreDb: () => NodePgDatabase<Record<string, unknown>>;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n}\n\nexport function createLinearWebhookRoutes(options: CreateLinearWebhookRoutesOptions): RouteGroup {\n const webhookRoute = defineRoute({\n method: 'POST',\n path: '/',\n auth: [],\n description: 'Linear webhook receiver.',\n options: {bodyLimit: WEBHOOK_BODY_LIMIT},\n handler: async (request, reply) => {\n const deliveryHeader = request.headers[DELIVERY_HEADER];\n const event = request.headers[EVENT_HEADER];\n const signature = request.headers[SIGNATURE_HEADER];\n\n if (typeof deliveryHeader !== 'string' || !deliveryHeader) {\n reply.code(400);\n return {error: 'missing Linear-Delivery header'};\n }\n if (typeof event !== 'string' || !event) {\n reply.code(400);\n return {error: 'missing Linear-Event header'};\n }\n if (typeof signature !== 'string' || !signature) {\n reply.code(401);\n return {error: 'missing Linear-Signature header'};\n }\n\n const body = request.body;\n if (!Buffer.isBuffer(body)) {\n reply.code(400);\n return {error: 'expected raw JSON body'};\n }\n const rawBody = body.toString('utf8');\n\n if (\n !verifyHexHmacSignature({\n rawBody,\n signature,\n secret: config.LINEAR_WEBHOOK_SIGNING_SECRET,\n })\n ) {\n reply.code(401);\n return {error: 'invalid signature'};\n }\n\n let parsedJson: unknown;\n try {\n parsedJson = JSON.parse(rawBody);\n } catch (error) {\n logger().warn(\n {deliveryId: deliveryHeader, err: error},\n 'linear webhook payload JSON parse failed',\n );\n reply.code(400);\n return {error: 'malformed JSON'};\n }\n\n const payload = linearWebhookBaseEnvelopeSchema.safeParse(parsedJson);\n if (!payload.success) {\n logger().warn(\n {deliveryId: deliveryHeader, issues: payload.error.issues},\n 'linear webhook envelope failed schema validation',\n );\n await recordSignedDeliveryOnly(options, deliveryHeader);\n reply.code(200);\n return null;\n }\n\n if (Math.abs(Date.now() - payload.data.webhookTimestamp) > WEBHOOK_REPLAY_WINDOW_MS) {\n reply.code(401);\n return {error: 'stale webhook timestamp'};\n }\n\n if (event !== payload.data.type) {\n logger().warn(\n {deliveryId: deliveryHeader, event, type: payload.data.type},\n 'linear webhook event header did not match payload type',\n );\n await recordSignedDeliveryOnly(options, deliveryHeader);\n reply.code(200);\n return null;\n }\n\n await options.coreDb().transaction(async (tx) => {\n await handleLinearWebhook({\n tx,\n // Linear documents this header as the delivery UUID; the signed timestamp is per-send.\n deliveryId: deliveryHeader,\n payload: payload.data,\n rawPayload: parsedJson,\n publishIntegrationEventReceived: options.publishIntegrationEventReceived,\n recordDeliveryOnly: options.recordDeliveryOnly,\n getIntegrationConnectionById: options.getIntegrationConnectionById,\n });\n });\n\n reply.code(200);\n return null;\n },\n });\n\n return {\n prefix: '/webhooks/integrations/linear',\n auth: [],\n plugins: [rawBodyPlugin],\n routes: [webhookRoute],\n };\n}\n\nasync function recordSignedDeliveryOnly(\n options: Pick<CreateLinearWebhookRoutesOptions, 'coreDb' | 'recordDeliveryOnly'>,\n deliveryId: string,\n): Promise<void> {\n await options.coreDb().transaction(async (tx: IntegrationTx) => {\n await options.recordDeliveryOnly({\n tx,\n provider: LINEAR_PROVIDER,\n deliveryId,\n });\n });\n}\n"],"names":["Buffer","LINEAR_PROVIDER","linearWebhookBaseEnvelopeSchema","defineRoute","rawBodyPlugin","verifyHexHmacSignature","WEBHOOK_BODY_LIMIT","logger","config","handleLinearWebhook","DELIVERY_HEADER","EVENT_HEADER","SIGNATURE_HEADER","WEBHOOK_REPLAY_WINDOW_MS","createLinearWebhookRoutes","options","webhookRoute","method","path","auth","description","bodyLimit","handler","request","reply","deliveryHeader","headers","event","signature","code","error","body","isBuffer","rawBody","toString","secret","LINEAR_WEBHOOK_SIGNING_SECRET","parsedJson","JSON","parse","warn","deliveryId","err","payload","safeParse","success","issues","recordSignedDeliveryOnly","Math","abs","Date","now","data","webhookTimestamp","type","coreDb","transaction","tx","rawPayload","publishIntegrationEventReceived","recordDeliveryOnly","getIntegrationConnectionById","prefix","plugins","routes","provider"],"mappings":"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"}
|