@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.
Files changed (151) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +89 -0
  5. package/LICENSE +21 -0
  6. package/README.md +88 -0
  7. package/dist/api/client.d.ts +29 -0
  8. package/dist/api/client.d.ts.map +1 -0
  9. package/dist/api/client.js +189 -0
  10. package/dist/api/client.js.map +1 -0
  11. package/dist/config.d.ts +8 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +22 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/core/agent-tools-provider.d.ts +32 -0
  16. package/dist/core/agent-tools-provider.d.ts.map +1 -0
  17. package/dist/core/agent-tools-provider.js +63 -0
  18. package/dist/core/agent-tools-provider.js.map +1 -0
  19. package/dist/core/agent-tools.d.ts +10 -0
  20. package/dist/core/agent-tools.d.ts.map +1 -0
  21. package/dist/core/agent-tools.js +920 -0
  22. package/dist/core/agent-tools.js.map +1 -0
  23. package/dist/core/disconnect.d.ts +18 -0
  24. package/dist/core/disconnect.d.ts.map +1 -0
  25. package/dist/core/disconnect.js +23 -0
  26. package/dist/core/disconnect.js.map +1 -0
  27. package/dist/core/errors.d.ts +35 -0
  28. package/dist/core/errors.d.ts.map +1 -0
  29. package/dist/core/errors.js +59 -0
  30. package/dist/core/errors.js.map +1 -0
  31. package/dist/core/install.d.ts +47 -0
  32. package/dist/core/install.d.ts.map +1 -0
  33. package/dist/core/install.js +102 -0
  34. package/dist/core/install.js.map +1 -0
  35. package/dist/core/scopes.d.ts +4 -0
  36. package/dist/core/scopes.d.ts.map +1 -0
  37. package/dist/core/scopes.js +17 -0
  38. package/dist/core/scopes.js.map +1 -0
  39. package/dist/core/state.d.ts +12 -0
  40. package/dist/core/state.d.ts.map +1 -0
  41. package/dist/core/state.js +54 -0
  42. package/dist/core/state.js.map +1 -0
  43. package/dist/core/tokens.d.ts +39 -0
  44. package/dist/core/tokens.d.ts.map +1 -0
  45. package/dist/core/tokens.js +117 -0
  46. package/dist/core/tokens.js.map +1 -0
  47. package/dist/core/webhook.d.ts +16 -0
  48. package/dist/core/webhook.d.ts.map +1 -0
  49. package/dist/core/webhook.js +105 -0
  50. package/dist/core/webhook.js.map +1 -0
  51. package/dist/db/db.d.ts +369 -0
  52. package/dist/db/db.d.ts.map +1 -0
  53. package/dist/db/db.js +18 -0
  54. package/dist/db/db.js.map +1 -0
  55. package/dist/db/installations.d.ts +53 -0
  56. package/dist/db/installations.d.ts.map +1 -0
  57. package/dist/db/installations.js +125 -0
  58. package/dist/db/installations.js.map +1 -0
  59. package/dist/db/migrations.d.ts +2 -0
  60. package/dist/db/migrations.d.ts.map +1 -0
  61. package/dist/db/migrations.js +5 -0
  62. package/dist/db/migrations.js.map +1 -0
  63. package/dist/db/schema/common.d.ts +2 -0
  64. package/dist/db/schema/common.d.ts.map +1 -0
  65. package/dist/db/schema/common.js +4 -0
  66. package/dist/db/schema/common.js.map +1 -0
  67. package/dist/db/schema/installations.d.ts +186 -0
  68. package/dist/db/schema/installations.d.ts.map +1 -0
  69. package/dist/db/schema/installations.js +40 -0
  70. package/dist/db/schema/installations.js.map +1 -0
  71. package/dist/index.d.ts +53 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +56 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/presentation/dto/integrations.d.ts +17 -0
  76. package/dist/presentation/dto/integrations.d.ts.map +1 -0
  77. package/dist/presentation/dto/integrations.js +6 -0
  78. package/dist/presentation/dto/integrations.js.map +1 -0
  79. package/dist/presentation/e2eRoutes/create-connection.d.ts +16 -0
  80. package/dist/presentation/e2eRoutes/create-connection.d.ts.map +1 -0
  81. package/dist/presentation/e2eRoutes/create-connection.js +64 -0
  82. package/dist/presentation/e2eRoutes/create-connection.js.map +1 -0
  83. package/dist/presentation/e2eRoutes/index.d.ts +5 -0
  84. package/dist/presentation/e2eRoutes/index.d.ts.map +1 -0
  85. package/dist/presentation/e2eRoutes/index.js +11 -0
  86. package/dist/presentation/e2eRoutes/index.js.map +1 -0
  87. package/dist/presentation/routes/errors.d.ts +2 -0
  88. package/dist/presentation/routes/errors.d.ts.map +1 -0
  89. package/dist/presentation/routes/errors.js +65 -0
  90. package/dist/presentation/routes/errors.js.map +1 -0
  91. package/dist/presentation/routes/install.d.ts +19 -0
  92. package/dist/presentation/routes/install.d.ts.map +1 -0
  93. package/dist/presentation/routes/install.js +100 -0
  94. package/dist/presentation/routes/install.js.map +1 -0
  95. package/dist/presentation/routes/webhooks.d.ts +11 -0
  96. package/dist/presentation/routes/webhooks.d.ts.map +1 -0
  97. package/dist/presentation/routes/webhooks.js +136 -0
  98. package/dist/presentation/routes/webhooks.js.map +1 -0
  99. package/dist/tsconfig.test.tsbuildinfo +1 -0
  100. package/drizzle/0000_initial.sql +15 -0
  101. package/drizzle/meta/0000_snapshot.json +126 -0
  102. package/drizzle/meta/_journal.json +13 -0
  103. package/drizzle.config.ts +7 -0
  104. package/package.json +72 -0
  105. package/src/api/client.test.ts +366 -0
  106. package/src/api/client.ts +281 -0
  107. package/src/config.test.ts +34 -0
  108. package/src/config.ts +22 -0
  109. package/src/connection-external-url.test.ts +64 -0
  110. package/src/core/agent-tools-provider.test.ts +245 -0
  111. package/src/core/agent-tools-provider.ts +106 -0
  112. package/src/core/agent-tools.ts +918 -0
  113. package/src/core/disconnect.test.ts +45 -0
  114. package/src/core/disconnect.ts +27 -0
  115. package/src/core/errors.ts +71 -0
  116. package/src/core/install.ts +162 -0
  117. package/src/core/scopes.ts +18 -0
  118. package/src/core/state.test.ts +51 -0
  119. package/src/core/state.ts +85 -0
  120. package/src/core/tokens.test.ts +281 -0
  121. package/src/core/tokens.ts +219 -0
  122. package/src/core/webhook.test.ts +318 -0
  123. package/src/core/webhook.ts +146 -0
  124. package/src/db/db.ts +18 -0
  125. package/src/db/installations.test.ts +182 -0
  126. package/src/db/installations.ts +215 -0
  127. package/src/db/migrations.ts +4 -0
  128. package/src/db/schema/common.ts +3 -0
  129. package/src/db/schema/installations.ts +42 -0
  130. package/src/index.test.ts +20 -0
  131. package/src/index.ts +153 -0
  132. package/src/presentation/dto/integrations.ts +9 -0
  133. package/src/presentation/e2eRoutes/create-connection.ts +85 -0
  134. package/src/presentation/e2eRoutes/index.test.ts +193 -0
  135. package/src/presentation/e2eRoutes/index.ts +14 -0
  136. package/src/presentation/routes/errors.ts +60 -0
  137. package/src/presentation/routes/install.test.ts +460 -0
  138. package/src/presentation/routes/install.ts +132 -0
  139. package/src/presentation/routes/webhooks.test.ts +407 -0
  140. package/src/presentation/routes/webhooks.ts +154 -0
  141. package/test/api-secrets.d.ts +28 -0
  142. package/test/env.ts +13 -0
  143. package/test/factories/index.ts +1 -0
  144. package/test/globalSetup.ts +23 -0
  145. package/test/index.ts +1 -0
  146. package/test/setup.ts +17 -0
  147. package/tsconfig.build.json +9 -0
  148. package/tsconfig.build.tsbuildinfo +1 -0
  149. package/tsconfig.json +3 -0
  150. package/tsconfig.test.json +9 -0
  151. 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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"}