@shipfox/api-integration-slack 4.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 (158) 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 +22 -0
  5. package/LICENSE +21 -0
  6. package/dist/api/client.d.ts +28 -0
  7. package/dist/api/client.d.ts.map +1 -0
  8. package/dist/api/client.js +163 -0
  9. package/dist/api/client.js.map +1 -0
  10. package/dist/config.d.ts +8 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +21 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/core/agent-tools-provider.d.ts +75 -0
  15. package/dist/core/agent-tools-provider.d.ts.map +1 -0
  16. package/dist/core/agent-tools-provider.js +111 -0
  17. package/dist/core/agent-tools-provider.js.map +1 -0
  18. package/dist/core/agent-tools.d.ts +65 -0
  19. package/dist/core/agent-tools.d.ts.map +1 -0
  20. package/dist/core/agent-tools.js +177 -0
  21. package/dist/core/agent-tools.js.map +1 -0
  22. package/dist/core/disconnect.d.ts +18 -0
  23. package/dist/core/disconnect.d.ts.map +1 -0
  24. package/dist/core/disconnect.js +23 -0
  25. package/dist/core/disconnect.js.map +1 -0
  26. package/dist/core/errors.d.ts +44 -0
  27. package/dist/core/errors.d.ts.map +1 -0
  28. package/dist/core/errors.js +71 -0
  29. package/dist/core/errors.js.map +1 -0
  30. package/dist/core/install.d.ts +48 -0
  31. package/dist/core/install.d.ts.map +1 -0
  32. package/dist/core/install.js +88 -0
  33. package/dist/core/install.js.map +1 -0
  34. package/dist/core/scopes.d.ts +4 -0
  35. package/dist/core/scopes.d.ts.map +1 -0
  36. package/dist/core/scopes.js +24 -0
  37. package/dist/core/scopes.js.map +1 -0
  38. package/dist/core/signature.d.ts +10 -0
  39. package/dist/core/signature.d.ts.map +1 -0
  40. package/dist/core/signature.js +18 -0
  41. package/dist/core/signature.js.map +1 -0
  42. package/dist/core/state.d.ts +12 -0
  43. package/dist/core/state.d.ts.map +1 -0
  44. package/dist/core/state.js +54 -0
  45. package/dist/core/state.js.map +1 -0
  46. package/dist/core/tokens.d.ts +35 -0
  47. package/dist/core/tokens.d.ts.map +1 -0
  48. package/dist/core/tokens.js +51 -0
  49. package/dist/core/tokens.js.map +1 -0
  50. package/dist/core/webhook.d.ts +25 -0
  51. package/dist/core/webhook.d.ts.map +1 -0
  52. package/dist/core/webhook.js +160 -0
  53. package/dist/core/webhook.js.map +1 -0
  54. package/dist/db/db.d.ts +403 -0
  55. package/dist/db/db.d.ts.map +1 -0
  56. package/dist/db/db.js +18 -0
  57. package/dist/db/db.js.map +1 -0
  58. package/dist/db/installations.d.ts +40 -0
  59. package/dist/db/installations.d.ts.map +1 -0
  60. package/dist/db/installations.js +73 -0
  61. package/dist/db/installations.js.map +1 -0
  62. package/dist/db/migrations.d.ts +2 -0
  63. package/dist/db/migrations.d.ts.map +1 -0
  64. package/dist/db/migrations.js +5 -0
  65. package/dist/db/migrations.js.map +1 -0
  66. package/dist/db/schema/common.d.ts +2 -0
  67. package/dist/db/schema/common.d.ts.map +1 -0
  68. package/dist/db/schema/common.js +4 -0
  69. package/dist/db/schema/common.js.map +1 -0
  70. package/dist/db/schema/installations.d.ts +203 -0
  71. package/dist/db/schema/installations.d.ts.map +1 -0
  72. package/dist/db/schema/installations.js +42 -0
  73. package/dist/db/schema/installations.js.map +1 -0
  74. package/dist/index.d.ts +59 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +73 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/presentation/dto/integrations.d.ts +2 -0
  79. package/dist/presentation/dto/integrations.d.ts.map +1 -0
  80. package/dist/presentation/dto/integrations.js +3 -0
  81. package/dist/presentation/dto/integrations.js.map +1 -0
  82. package/dist/presentation/e2eRoutes/create-connection.d.ts +16 -0
  83. package/dist/presentation/e2eRoutes/create-connection.d.ts.map +1 -0
  84. package/dist/presentation/e2eRoutes/create-connection.js +65 -0
  85. package/dist/presentation/e2eRoutes/create-connection.js.map +1 -0
  86. package/dist/presentation/e2eRoutes/index.d.ts +5 -0
  87. package/dist/presentation/e2eRoutes/index.d.ts.map +1 -0
  88. package/dist/presentation/e2eRoutes/index.js +11 -0
  89. package/dist/presentation/e2eRoutes/index.js.map +1 -0
  90. package/dist/presentation/routes/errors.d.ts +2 -0
  91. package/dist/presentation/routes/errors.d.ts.map +1 -0
  92. package/dist/presentation/routes/errors.js +75 -0
  93. package/dist/presentation/routes/errors.js.map +1 -0
  94. package/dist/presentation/routes/install.d.ts +19 -0
  95. package/dist/presentation/routes/install.d.ts.map +1 -0
  96. package/dist/presentation/routes/install.js +98 -0
  97. package/dist/presentation/routes/install.js.map +1 -0
  98. package/dist/presentation/routes/webhooks.d.ts +16 -0
  99. package/dist/presentation/routes/webhooks.d.ts.map +1 -0
  100. package/dist/presentation/routes/webhooks.js +166 -0
  101. package/dist/presentation/routes/webhooks.js.map +1 -0
  102. package/dist/tsconfig.test.tsbuildinfo +1 -0
  103. package/drizzle/0000_calm_fantastic_four.sql +16 -0
  104. package/drizzle/meta/0000_snapshot.json +132 -0
  105. package/drizzle/meta/_journal.json +13 -0
  106. package/drizzle.config.ts +7 -0
  107. package/package.json +71 -0
  108. package/src/api/client.test.ts +239 -0
  109. package/src/api/client.ts +243 -0
  110. package/src/config.test.ts +32 -0
  111. package/src/config.ts +20 -0
  112. package/src/connection-external-url.test.ts +25 -0
  113. package/src/core/agent-tools-provider.test.ts +256 -0
  114. package/src/core/agent-tools-provider.ts +147 -0
  115. package/src/core/agent-tools.test.ts +90 -0
  116. package/src/core/agent-tools.ts +189 -0
  117. package/src/core/disconnect.ts +26 -0
  118. package/src/core/errors.ts +92 -0
  119. package/src/core/install.test.ts +155 -0
  120. package/src/core/install.ts +137 -0
  121. package/src/core/scopes.test.ts +22 -0
  122. package/src/core/scopes.ts +25 -0
  123. package/src/core/signature.test.ts +45 -0
  124. package/src/core/signature.ts +41 -0
  125. package/src/core/state.test.ts +48 -0
  126. package/src/core/state.ts +82 -0
  127. package/src/core/tokens.test.ts +163 -0
  128. package/src/core/tokens.ts +98 -0
  129. package/src/core/webhook.test.ts +269 -0
  130. package/src/core/webhook.ts +223 -0
  131. package/src/db/db.ts +16 -0
  132. package/src/db/installations.test.ts +93 -0
  133. package/src/db/installations.ts +141 -0
  134. package/src/db/migrations.ts +4 -0
  135. package/src/db/schema/common.ts +3 -0
  136. package/src/db/schema/installations.ts +44 -0
  137. package/src/index.test.ts +25 -0
  138. package/src/index.ts +196 -0
  139. package/src/presentation/dto/integrations.ts +1 -0
  140. package/src/presentation/e2eRoutes/create-connection.ts +82 -0
  141. package/src/presentation/e2eRoutes/index.test.ts +96 -0
  142. package/src/presentation/e2eRoutes/index.ts +11 -0
  143. package/src/presentation/routes/errors.ts +68 -0
  144. package/src/presentation/routes/install.test.ts +229 -0
  145. package/src/presentation/routes/install.ts +117 -0
  146. package/src/presentation/routes/webhooks.test.ts +266 -0
  147. package/src/presentation/routes/webhooks.ts +170 -0
  148. package/test/api-secrets.d.ts +26 -0
  149. package/test/env.ts +13 -0
  150. package/test/factories/index.ts +1 -0
  151. package/test/globalSetup.ts +23 -0
  152. package/test/index.ts +1 -0
  153. package/test/setup.ts +17 -0
  154. package/tsconfig.build.json +9 -0
  155. package/tsconfig.build.tsbuildinfo +1 -0
  156. package/tsconfig.json +3 -0
  157. package/tsconfig.test.json +9 -0
  158. package/vitest.config.ts +12 -0
@@ -0,0 +1,75 @@
1
+ import { ConnectionSlugConflictError } from '@shipfox/api-integration-core-dto';
2
+ import { ClientError } from '@shipfox/node-fastify';
3
+ import { SlackAuthorizationScopeMismatchError, SlackConnectionAlreadyLinkedError, SlackEnterpriseInstallUnsupportedError, SlackInstallationAlreadyLinkedError, SlackInstallStateActorMismatchError, SlackInstallStateError, SlackIntegrationProviderError, SlackOAuthCallbackError, SlackTokenRotationUnsupportedError } from '#core/errors.js';
4
+ function providerStatus(reason) {
5
+ if (reason === 'rate-limited') return 429;
6
+ if (reason === 'timeout' || reason === 'provider-unavailable') return 503;
7
+ return 422;
8
+ }
9
+ export function slackRouteErrorHandler(error) {
10
+ if (error instanceof SlackInstallStateError) {
11
+ throw new ClientError(error.message, 'invalid-slack-install-state', {
12
+ status: 400
13
+ });
14
+ }
15
+ if (error instanceof SlackInstallStateActorMismatchError) {
16
+ throw new ClientError(error.message, 'slack-install-state-actor-mismatch', {
17
+ status: 403
18
+ });
19
+ }
20
+ if (error instanceof SlackInstallationAlreadyLinkedError) {
21
+ throw new ClientError(error.message, 'slack-installation-already-linked', {
22
+ status: 409
23
+ });
24
+ }
25
+ if (error instanceof SlackConnectionAlreadyLinkedError) {
26
+ throw new ClientError(error.message, 'slack-connection-already-linked', {
27
+ status: 409
28
+ });
29
+ }
30
+ if (error instanceof SlackAuthorizationScopeMismatchError) {
31
+ throw new ClientError(error.message, 'slack-authorization-scope-mismatch', {
32
+ status: 422,
33
+ details: {
34
+ missing_scopes: error.missingScopes
35
+ }
36
+ });
37
+ }
38
+ if (error instanceof SlackEnterpriseInstallUnsupportedError) {
39
+ throw new ClientError(error.message, 'slack-enterprise-install-unsupported', {
40
+ status: 422
41
+ });
42
+ }
43
+ if (error instanceof SlackTokenRotationUnsupportedError) {
44
+ throw new ClientError(error.message, 'slack-token-rotation-unsupported', {
45
+ status: 422
46
+ });
47
+ }
48
+ if (error instanceof SlackOAuthCallbackError) {
49
+ throw new ClientError(error.message, 'slack-oauth-callback-error', {
50
+ status: 422,
51
+ details: {
52
+ error: error.providerError,
53
+ ...error.providerDescription ? {
54
+ error_description: error.providerDescription
55
+ } : {}
56
+ }
57
+ });
58
+ }
59
+ if (error instanceof ConnectionSlugConflictError) {
60
+ throw new ClientError(error.message, 'slug-conflict', {
61
+ status: 409
62
+ });
63
+ }
64
+ if (error instanceof SlackIntegrationProviderError) {
65
+ throw new ClientError(error.message, error.reason, {
66
+ details: {
67
+ retry_after_seconds: error.retryAfterSeconds
68
+ },
69
+ status: providerStatus(error.reason)
70
+ });
71
+ }
72
+ throw error;
73
+ }
74
+
75
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/presentation/routes/errors.ts"],"sourcesContent":["import {\n ConnectionSlugConflictError,\n type IntegrationProviderErrorReason,\n} from '@shipfox/api-integration-core-dto';\nimport {ClientError} from '@shipfox/node-fastify';\nimport {\n SlackAuthorizationScopeMismatchError,\n SlackConnectionAlreadyLinkedError,\n SlackEnterpriseInstallUnsupportedError,\n SlackInstallationAlreadyLinkedError,\n SlackInstallStateActorMismatchError,\n SlackInstallStateError,\n SlackIntegrationProviderError,\n SlackOAuthCallbackError,\n SlackTokenRotationUnsupportedError,\n} from '#core/errors.js';\n\nfunction providerStatus(reason: IntegrationProviderErrorReason): number {\n if (reason === 'rate-limited') return 429;\n if (reason === 'timeout' || reason === 'provider-unavailable') return 503;\n return 422;\n}\n\nexport function slackRouteErrorHandler(error: unknown): never {\n if (error instanceof SlackInstallStateError) {\n throw new ClientError(error.message, 'invalid-slack-install-state', {status: 400});\n }\n if (error instanceof SlackInstallStateActorMismatchError) {\n throw new ClientError(error.message, 'slack-install-state-actor-mismatch', {status: 403});\n }\n if (error instanceof SlackInstallationAlreadyLinkedError) {\n throw new ClientError(error.message, 'slack-installation-already-linked', {status: 409});\n }\n if (error instanceof SlackConnectionAlreadyLinkedError) {\n throw new ClientError(error.message, 'slack-connection-already-linked', {status: 409});\n }\n if (error instanceof SlackAuthorizationScopeMismatchError) {\n throw new ClientError(error.message, 'slack-authorization-scope-mismatch', {\n status: 422,\n details: {missing_scopes: error.missingScopes},\n });\n }\n if (error instanceof SlackEnterpriseInstallUnsupportedError) {\n throw new ClientError(error.message, 'slack-enterprise-install-unsupported', {status: 422});\n }\n if (error instanceof SlackTokenRotationUnsupportedError) {\n throw new ClientError(error.message, 'slack-token-rotation-unsupported', {status: 422});\n }\n if (error instanceof SlackOAuthCallbackError) {\n throw new ClientError(error.message, 'slack-oauth-callback-error', {\n status: 422,\n details: {\n error: error.providerError,\n ...(error.providerDescription ? {error_description: error.providerDescription} : {}),\n },\n });\n }\n if (error instanceof ConnectionSlugConflictError) {\n throw new ClientError(error.message, 'slug-conflict', {status: 409});\n }\n if (error instanceof SlackIntegrationProviderError) {\n throw new ClientError(error.message, error.reason, {\n details: {retry_after_seconds: error.retryAfterSeconds},\n status: providerStatus(error.reason),\n });\n }\n throw error;\n}\n"],"names":["ConnectionSlugConflictError","ClientError","SlackAuthorizationScopeMismatchError","SlackConnectionAlreadyLinkedError","SlackEnterpriseInstallUnsupportedError","SlackInstallationAlreadyLinkedError","SlackInstallStateActorMismatchError","SlackInstallStateError","SlackIntegrationProviderError","SlackOAuthCallbackError","SlackTokenRotationUnsupportedError","providerStatus","reason","slackRouteErrorHandler","error","message","status","details","missing_scopes","missingScopes","providerError","providerDescription","error_description","retry_after_seconds","retryAfterSeconds"],"mappings":"AAAA,SACEA,2BAA2B,QAEtB,oCAAoC;AAC3C,SAAQC,WAAW,QAAO,wBAAwB;AAClD,SACEC,oCAAoC,EACpCC,iCAAiC,EACjCC,sCAAsC,EACtCC,mCAAmC,EACnCC,mCAAmC,EACnCC,sBAAsB,EACtBC,6BAA6B,EAC7BC,uBAAuB,EACvBC,kCAAkC,QAC7B,kBAAkB;AAEzB,SAASC,eAAeC,MAAsC;IAC5D,IAAIA,WAAW,gBAAgB,OAAO;IACtC,IAAIA,WAAW,aAAaA,WAAW,wBAAwB,OAAO;IACtE,OAAO;AACT;AAEA,OAAO,SAASC,uBAAuBC,KAAc;IACnD,IAAIA,iBAAiBP,wBAAwB;QAC3C,MAAM,IAAIN,YAAYa,MAAMC,OAAO,EAAE,+BAA+B;YAACC,QAAQ;QAAG;IAClF;IACA,IAAIF,iBAAiBR,qCAAqC;QACxD,MAAM,IAAIL,YAAYa,MAAMC,OAAO,EAAE,sCAAsC;YAACC,QAAQ;QAAG;IACzF;IACA,IAAIF,iBAAiBT,qCAAqC;QACxD,MAAM,IAAIJ,YAAYa,MAAMC,OAAO,EAAE,qCAAqC;YAACC,QAAQ;QAAG;IACxF;IACA,IAAIF,iBAAiBX,mCAAmC;QACtD,MAAM,IAAIF,YAAYa,MAAMC,OAAO,EAAE,mCAAmC;YAACC,QAAQ;QAAG;IACtF;IACA,IAAIF,iBAAiBZ,sCAAsC;QACzD,MAAM,IAAID,YAAYa,MAAMC,OAAO,EAAE,sCAAsC;YACzEC,QAAQ;YACRC,SAAS;gBAACC,gBAAgBJ,MAAMK,aAAa;YAAA;QAC/C;IACF;IACA,IAAIL,iBAAiBV,wCAAwC;QAC3D,MAAM,IAAIH,YAAYa,MAAMC,OAAO,EAAE,wCAAwC;YAACC,QAAQ;QAAG;IAC3F;IACA,IAAIF,iBAAiBJ,oCAAoC;QACvD,MAAM,IAAIT,YAAYa,MAAMC,OAAO,EAAE,oCAAoC;YAACC,QAAQ;QAAG;IACvF;IACA,IAAIF,iBAAiBL,yBAAyB;QAC5C,MAAM,IAAIR,YAAYa,MAAMC,OAAO,EAAE,8BAA8B;YACjEC,QAAQ;YACRC,SAAS;gBACPH,OAAOA,MAAMM,aAAa;gBAC1B,GAAIN,MAAMO,mBAAmB,GAAG;oBAACC,mBAAmBR,MAAMO,mBAAmB;gBAAA,IAAI,CAAC,CAAC;YACrF;QACF;IACF;IACA,IAAIP,iBAAiBd,6BAA6B;QAChD,MAAM,IAAIC,YAAYa,MAAMC,OAAO,EAAE,iBAAiB;YAACC,QAAQ;QAAG;IACpE;IACA,IAAIF,iBAAiBN,+BAA+B;QAClD,MAAM,IAAIP,YAAYa,MAAMC,OAAO,EAAED,MAAMF,MAAM,EAAE;YACjDK,SAAS;gBAACM,qBAAqBT,MAAMU,iBAAiB;YAAA;YACtDR,QAAQL,eAAeG,MAAMF,MAAM;QACrC;IACF;IACA,MAAME;AACR"}
@@ -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 { SlackApiClient } from '#api/client.js';
4
+ import { type ConnectSlackInstallationInput } from '#core/install.js';
5
+ import type { SlackTokenStore } from '#core/tokens.js';
6
+ export interface CreateSlackIntegrationRoutesOptions {
7
+ slack: SlackApiClient;
8
+ tokenStore: Pick<SlackTokenStore, 'storeTokens'>;
9
+ getExistingSlackConnection: (input: {
10
+ teamId: string;
11
+ }) => Promise<IntegrationConnection<'slack'> | undefined>;
12
+ connectSlackInstallation: (input: ConnectSlackInstallationInput) => Promise<IntegrationConnection<'slack'>>;
13
+ disconnectSlackInstallation: (input: {
14
+ connectionId: string;
15
+ }) => Promise<void>;
16
+ connectionCapabilities: IntegrationCapability[];
17
+ }
18
+ export declare function createSlackIntegrationRoutes({ slack, tokenStore, getExistingSlackConnection, connectSlackInstallation, disconnectSlackInstallation, connectionCapabilities, }: CreateSlackIntegrationRoutesOptions): 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,cAAc,EAAC,MAAM,gBAAgB,CAAC;AAEnD,OAAO,EACL,KAAK,6BAA6B,EAGnC,MAAM,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,iBAAiB,CAAC;AAIrD,MAAM,WAAW,mCAAmC;IAClD,KAAK,EAAE,cAAc,CAAC;IACtB,UAAU,EAAE,IAAI,CAAC,eAAe,EAAE,aAAa,CAAC,CAAC;IACjD,0BAA0B,EAAE,CAAC,KAAK,EAAE;QAClC,MAAM,EAAE,MAAM,CAAC;KAChB,KAAK,OAAO,CAAC,qBAAqB,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC;IAC1D,wBAAwB,EAAE,CACxB,KAAK,EAAE,6BAA6B,KACjC,OAAO,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC,CAAC;IAC7C,2BAA2B,EAAE,CAAC,KAAK,EAAE;QAAC,YAAY,EAAE,MAAM,CAAA;KAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,sBAAsB,EAAE,qBAAqB,EAAE,CAAC;CACjD;AAED,wBAAgB,4BAA4B,CAAC,EAC3C,KAAK,EACL,UAAU,EACV,0BAA0B,EAC1B,wBAAwB,EACxB,2BAA2B,EAC3B,sBAAsB,GACvB,EAAE,mCAAmC,GAAG,UAAU,CAgElD"}
@@ -0,0 +1,98 @@
1
+ import { AUTH_USER, requireUserContext, requireWorkspaceAccess } from '@shipfox/api-auth-context';
2
+ import { createSlackInstallBodySchema, createSlackInstallResponseSchema, slackCallbackQuerySchema, slackCallbackResponseSchema } from '@shipfox/api-integration-slack-dto';
3
+ import { requireWorkspaceMembership } from '@shipfox/api-workspaces';
4
+ import { defineRoute } from '@shipfox/node-fastify';
5
+ import { config } from '#config.js';
6
+ import { handleSlackCallback, handleSlackOAuthCallbackError } from '#core/install.js';
7
+ import { formatSlackBotScopes } from '#core/scopes.js';
8
+ import { signSlackInstallState } from '#core/state.js';
9
+ import { toIntegrationConnectionDto } from '#presentation/dto/integrations.js';
10
+ import { slackRouteErrorHandler } from './errors.js';
11
+ export function createSlackIntegrationRoutes({ slack, tokenStore, getExistingSlackConnection, connectSlackInstallation, disconnectSlackInstallation, connectionCapabilities }) {
12
+ const installRoute = defineRoute({
13
+ method: 'POST',
14
+ path: '/install',
15
+ auth: AUTH_USER,
16
+ description: 'Create a Slack OAuth authorization URL for a workspace.',
17
+ schema: {
18
+ body: createSlackInstallBodySchema,
19
+ response: {
20
+ 200: createSlackInstallResponseSchema
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 = signSlackInstallState({
31
+ workspaceId,
32
+ userId: actor.userId
33
+ });
34
+ const installUrl = new URL('https://slack.com/oauth/v2/authorize');
35
+ installUrl.searchParams.set('client_id', config.SLACK_OAUTH_CLIENT_ID);
36
+ installUrl.searchParams.set('scope', formatSlackBotScopes());
37
+ installUrl.searchParams.set('redirect_uri', config.SLACK_OAUTH_REDIRECT_URL);
38
+ installUrl.searchParams.set('state', state);
39
+ return {
40
+ install_url: installUrl.toString()
41
+ };
42
+ }
43
+ });
44
+ const callbackApiRoute = defineRoute({
45
+ method: 'GET',
46
+ path: '/callback/api',
47
+ auth: AUTH_USER,
48
+ description: 'Handle the Slack OAuth callback.',
49
+ schema: {
50
+ querystring: slackCallbackQuerySchema,
51
+ response: {
52
+ 200: slackCallbackResponseSchema
53
+ }
54
+ },
55
+ errorHandler: slackRouteErrorHandler,
56
+ handler: async (request)=>{
57
+ const actor = requireUserContext(request);
58
+ const query = request.query;
59
+ if (isSlackOAuthErrorCallback(query)) {
60
+ return await handleSlackOAuthCallbackError({
61
+ state: query.state,
62
+ error: query.error,
63
+ errorDescription: query.error_description,
64
+ sessionUserId: actor.userId,
65
+ sessionMemberships: actor.memberships,
66
+ requireWorkspaceMembership
67
+ });
68
+ }
69
+ const connection = await handleSlackCallback({
70
+ slack,
71
+ tokenStore,
72
+ code: query.code,
73
+ state: query.state,
74
+ sessionUserId: actor.userId,
75
+ sessionMemberships: actor.memberships,
76
+ requireWorkspaceMembership,
77
+ getExistingSlackConnection,
78
+ connectSlackInstallation,
79
+ disconnectSlackInstallation
80
+ });
81
+ return toIntegrationConnectionDto(connection, {
82
+ capabilities: connectionCapabilities
83
+ });
84
+ }
85
+ });
86
+ return {
87
+ prefix: '/integrations/slack',
88
+ routes: [
89
+ installRoute,
90
+ callbackApiRoute
91
+ ]
92
+ };
93
+ }
94
+ function isSlackOAuthErrorCallback(query) {
95
+ return 'error' in query && typeof query.error === 'string';
96
+ }
97
+
98
+ //# 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 createSlackInstallBodySchema,\n createSlackInstallResponseSchema,\n type SlackCallbackQueryDto,\n slackCallbackQuerySchema,\n slackCallbackResponseSchema,\n} from '@shipfox/api-integration-slack-dto';\nimport {requireWorkspaceMembership} from '@shipfox/api-workspaces';\nimport {defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {SlackApiClient} from '#api/client.js';\nimport {config} from '#config.js';\nimport {\n type ConnectSlackInstallationInput,\n handleSlackCallback,\n handleSlackOAuthCallbackError,\n} from '#core/install.js';\nimport {formatSlackBotScopes} from '#core/scopes.js';\nimport {signSlackInstallState} from '#core/state.js';\nimport type {SlackTokenStore} from '#core/tokens.js';\nimport {toIntegrationConnectionDto} from '#presentation/dto/integrations.js';\nimport {slackRouteErrorHandler} from './errors.js';\n\nexport interface CreateSlackIntegrationRoutesOptions {\n slack: SlackApiClient;\n tokenStore: Pick<SlackTokenStore, 'storeTokens'>;\n getExistingSlackConnection: (input: {\n teamId: string;\n }) => Promise<IntegrationConnection<'slack'> | undefined>;\n connectSlackInstallation: (\n input: ConnectSlackInstallationInput,\n ) => Promise<IntegrationConnection<'slack'>>;\n disconnectSlackInstallation: (input: {connectionId: string}) => Promise<void>;\n connectionCapabilities: IntegrationCapability[];\n}\n\nexport function createSlackIntegrationRoutes({\n slack,\n tokenStore,\n getExistingSlackConnection,\n connectSlackInstallation,\n disconnectSlackInstallation,\n connectionCapabilities,\n}: CreateSlackIntegrationRoutesOptions): RouteGroup {\n const installRoute = defineRoute({\n method: 'POST',\n path: '/install',\n auth: AUTH_USER,\n description: 'Create a Slack OAuth authorization URL for a workspace.',\n schema: {\n body: createSlackInstallBodySchema,\n response: {200: createSlackInstallResponseSchema},\n },\n handler: (request) => {\n const {workspace_id: workspaceId} = request.body;\n const actor = requireUserContext(request);\n requireWorkspaceAccess({request, workspaceId});\n const state = signSlackInstallState({workspaceId, userId: actor.userId});\n const installUrl = new URL('https://slack.com/oauth/v2/authorize');\n installUrl.searchParams.set('client_id', config.SLACK_OAUTH_CLIENT_ID);\n installUrl.searchParams.set('scope', formatSlackBotScopes());\n installUrl.searchParams.set('redirect_uri', config.SLACK_OAUTH_REDIRECT_URL);\n installUrl.searchParams.set('state', state);\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 Slack OAuth callback.',\n schema: {\n querystring: slackCallbackQuerySchema,\n response: {200: slackCallbackResponseSchema},\n },\n errorHandler: slackRouteErrorHandler,\n handler: async (request) => {\n const actor = requireUserContext(request);\n const query = request.query;\n if (isSlackOAuthErrorCallback(query)) {\n return await handleSlackOAuthCallbackError({\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 handleSlackCallback({\n slack,\n tokenStore,\n code: query.code,\n state: query.state,\n sessionUserId: actor.userId,\n sessionMemberships: actor.memberships,\n requireWorkspaceMembership,\n getExistingSlackConnection,\n connectSlackInstallation,\n disconnectSlackInstallation,\n });\n return toIntegrationConnectionDto(connection, {capabilities: connectionCapabilities});\n },\n });\n\n return {prefix: '/integrations/slack', routes: [installRoute, callbackApiRoute]};\n}\n\nfunction isSlackOAuthErrorCallback(query: SlackCallbackQueryDto): query is SlackCallbackQueryDto & {\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","createSlackInstallBodySchema","createSlackInstallResponseSchema","slackCallbackQuerySchema","slackCallbackResponseSchema","requireWorkspaceMembership","defineRoute","config","handleSlackCallback","handleSlackOAuthCallbackError","formatSlackBotScopes","signSlackInstallState","toIntegrationConnectionDto","slackRouteErrorHandler","createSlackIntegrationRoutes","slack","tokenStore","getExistingSlackConnection","connectSlackInstallation","disconnectSlackInstallation","connectionCapabilities","installRoute","method","path","auth","description","schema","body","response","handler","request","workspace_id","workspaceId","actor","state","userId","installUrl","URL","searchParams","set","SLACK_OAUTH_CLIENT_ID","SLACK_OAUTH_REDIRECT_URL","install_url","toString","callbackApiRoute","querystring","errorHandler","query","isSlackOAuthErrorCallback","error","errorDescription","error_description","sessionUserId","sessionMemberships","memberships","connection","code","capabilities","prefix","routes"],"mappings":"AAAA,SAAQA,SAAS,EAAEC,kBAAkB,EAAEC,sBAAsB,QAAO,4BAA4B;AAEhG,SACEC,4BAA4B,EAC5BC,gCAAgC,EAEhCC,wBAAwB,EACxBC,2BAA2B,QACtB,qCAAqC;AAC5C,SAAQC,0BAA0B,QAAO,0BAA0B;AACnE,SAAQC,WAAW,QAAwB,wBAAwB;AAEnE,SAAQC,MAAM,QAAO,aAAa;AAClC,SAEEC,mBAAmB,EACnBC,6BAA6B,QACxB,mBAAmB;AAC1B,SAAQC,oBAAoB,QAAO,kBAAkB;AACrD,SAAQC,qBAAqB,QAAO,iBAAiB;AAErD,SAAQC,0BAA0B,QAAO,oCAAoC;AAC7E,SAAQC,sBAAsB,QAAO,cAAc;AAenD,OAAO,SAASC,6BAA6B,EAC3CC,KAAK,EACLC,UAAU,EACVC,0BAA0B,EAC1BC,wBAAwB,EACxBC,2BAA2B,EAC3BC,sBAAsB,EACc;IACpC,MAAMC,eAAef,YAAY;QAC/BgB,QAAQ;QACRC,MAAM;QACNC,MAAM1B;QACN2B,aAAa;QACbC,QAAQ;YACNC,MAAM1B;YACN2B,UAAU;gBAAC,KAAK1B;YAAgC;QAClD;QACA2B,SAAS,CAACC;YACR,MAAM,EAACC,cAAcC,WAAW,EAAC,GAAGF,QAAQH,IAAI;YAChD,MAAMM,QAAQlC,mBAAmB+B;YACjC9B,uBAAuB;gBAAC8B;gBAASE;YAAW;YAC5C,MAAME,QAAQvB,sBAAsB;gBAACqB;gBAAaG,QAAQF,MAAME,MAAM;YAAA;YACtE,MAAMC,aAAa,IAAIC,IAAI;YAC3BD,WAAWE,YAAY,CAACC,GAAG,CAAC,aAAahC,OAAOiC,qBAAqB;YACrEJ,WAAWE,YAAY,CAACC,GAAG,CAAC,SAAS7B;YACrC0B,WAAWE,YAAY,CAACC,GAAG,CAAC,gBAAgBhC,OAAOkC,wBAAwB;YAC3EL,WAAWE,YAAY,CAACC,GAAG,CAAC,SAASL;YACrC,OAAO;gBAACQ,aAAaN,WAAWO,QAAQ;YAAE;QAC5C;IACF;IAEA,MAAMC,mBAAmBtC,YAAY;QACnCgB,QAAQ;QACRC,MAAM;QACNC,MAAM1B;QACN2B,aAAa;QACbC,QAAQ;YACNmB,aAAa1C;YACbyB,UAAU;gBAAC,KAAKxB;YAA2B;QAC7C;QACA0C,cAAcjC;QACdgB,SAAS,OAAOC;YACd,MAAMG,QAAQlC,mBAAmB+B;YACjC,MAAMiB,QAAQjB,QAAQiB,KAAK;YAC3B,IAAIC,0BAA0BD,QAAQ;gBACpC,OAAO,MAAMtC,8BAA8B;oBACzCyB,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,oBAAoB;gBAC3CO;gBACAC;gBACAwC,MAAMT,MAAMS,IAAI;gBAChBtB,OAAOa,MAAMb,KAAK;gBAClBkB,eAAenB,MAAME,MAAM;gBAC3BkB,oBAAoBpB,MAAMqB,WAAW;gBACrCjD;gBACAY;gBACAC;gBACAC;YACF;YACA,OAAOP,2BAA2B2C,YAAY;gBAACE,cAAcrC;YAAsB;QACrF;IACF;IAEA,OAAO;QAACsC,QAAQ;QAAuBC,QAAQ;YAACtC;YAAcuB;SAAiB;IAAA;AACjF;AAEA,SAASI,0BAA0BD,KAA4B;IAK7D,OAAO,WAAWA,SAAS,OAAOA,MAAME,KAAK,KAAK;AACpD"}
@@ -0,0 +1,16 @@
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 declare const SLACK_WEBHOOK_BODY_LIMIT: number;
5
+ export declare const SLASH_COMMAND_ACK: {
6
+ readonly response_type: "ephemeral";
7
+ readonly text: "Working on it.";
8
+ };
9
+ export interface CreateSlackWebhookRoutesOptions {
10
+ coreDb: () => NodePgDatabase<Record<string, unknown>>;
11
+ publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;
12
+ recordDeliveryOnly: RecordDeliveryOnlyFn;
13
+ getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
14
+ }
15
+ export declare function createSlackWebhookRoutes(options: CreateSlackWebhookRoutesOptions): RouteGroup[];
16
+ //# 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,EAC9B,iCAAiC,EACjC,oBAAoB,EACrB,MAAM,mCAAmC,CAAC;AAK3C,OAAO,EAAmC,KAAK,UAAU,EAAC,MAAM,uBAAuB,CAAC;AAExF,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAK9D,eAAO,MAAM,wBAAwB,QAAc,CAAC;AACpD,eAAO,MAAM,iBAAiB;;;CAAgE,CAAC;AAW/F,MAAM,WAAW,+BAA+B;IAC9C,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,wBAAwB,CAAC,OAAO,EAAE,+BAA+B,GAAG,UAAU,EAAE,CAmH/F"}
@@ -0,0 +1,166 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { slackEventsRequestSchema, slackSlashCommandSchema } from '@shipfox/api-integration-slack-dto';
3
+ import { createRawBodyPlugin, defineRoute } from '@shipfox/node-fastify';
4
+ import { logger } from '@shipfox/node-opentelemetry';
5
+ import { config } from '#config.js';
6
+ import { verifySlackSignature } from '#core/signature.js';
7
+ import { handleSlackCommand, handleSlackEvent } from '#core/webhook.js';
8
+ export const SLACK_WEBHOOK_BODY_LIMIT = 1024 * 1024;
9
+ export const SLASH_COMMAND_ACK = {
10
+ response_type: 'ephemeral',
11
+ text: 'Working on it.'
12
+ };
13
+ const slackJsonRawBodyPlugin = createRawBodyPlugin({
14
+ contentType: 'application/json',
15
+ bodyLimit: SLACK_WEBHOOK_BODY_LIMIT
16
+ });
17
+ const slackFormRawBodyPlugin = createRawBodyPlugin({
18
+ contentType: 'application/x-www-form-urlencoded',
19
+ bodyLimit: SLACK_WEBHOOK_BODY_LIMIT
20
+ });
21
+ export function createSlackWebhookRoutes(options) {
22
+ const eventsRoute = defineRoute({
23
+ method: 'POST',
24
+ path: '/',
25
+ auth: [],
26
+ description: 'Slack Events API receiver.',
27
+ options: {
28
+ bodyLimit: SLACK_WEBHOOK_BODY_LIMIT
29
+ },
30
+ handler: async (request, reply)=>{
31
+ const rawBody = rawRequestBody(request.body);
32
+ if (rawBody === undefined) {
33
+ reply.code(400);
34
+ return {
35
+ error: 'expected raw JSON body'
36
+ };
37
+ }
38
+ if (!hasValidSlackSignature(request.headers, rawBody)) {
39
+ reply.code(401);
40
+ return {
41
+ error: 'invalid signature'
42
+ };
43
+ }
44
+ let parsedJson;
45
+ try {
46
+ parsedJson = JSON.parse(rawBody);
47
+ } catch (error) {
48
+ logger().warn({
49
+ errorName: error instanceof Error ? error.name : 'unknown'
50
+ }, 'slack events payload JSON parse failed');
51
+ reply.code(400);
52
+ return {
53
+ error: 'malformed JSON'
54
+ };
55
+ }
56
+ const parsed = slackEventsRequestSchema.safeParse(parsedJson);
57
+ if (!parsed.success) {
58
+ logger().warn({
59
+ issues: parsed.error.issues
60
+ }, 'slack events envelope failed schema validation');
61
+ reply.code(200);
62
+ return null;
63
+ }
64
+ const eventRequest = parsed.data;
65
+ if (eventRequest.type === 'url_verification') {
66
+ reply.code(200);
67
+ return {
68
+ challenge: eventRequest.challenge
69
+ };
70
+ }
71
+ await options.coreDb().transaction(async (tx)=>{
72
+ await handleSlackEvent({
73
+ tx,
74
+ deliveryId: eventRequest.event_id,
75
+ envelope: eventRequest,
76
+ publishIntegrationEventReceived: options.publishIntegrationEventReceived,
77
+ recordDeliveryOnly: options.recordDeliveryOnly,
78
+ getIntegrationConnectionById: options.getIntegrationConnectionById
79
+ });
80
+ });
81
+ reply.code(200);
82
+ return null;
83
+ }
84
+ });
85
+ const commandsRoute = defineRoute({
86
+ method: 'POST',
87
+ path: '/',
88
+ auth: [],
89
+ description: 'Slack slash-command receiver.',
90
+ options: {
91
+ bodyLimit: SLACK_WEBHOOK_BODY_LIMIT
92
+ },
93
+ handler: async (request, reply)=>{
94
+ const rawBody = rawRequestBody(request.body);
95
+ if (rawBody === undefined) {
96
+ reply.code(400);
97
+ return {
98
+ error: 'expected raw form body'
99
+ };
100
+ }
101
+ if (!hasValidSlackSignature(request.headers, rawBody)) {
102
+ reply.code(401);
103
+ return {
104
+ error: 'invalid signature'
105
+ };
106
+ }
107
+ const command = slackSlashCommandSchema.safeParse(Object.fromEntries(new URLSearchParams(rawBody)));
108
+ if (!command.success) {
109
+ logger().warn({
110
+ issues: command.error.issues
111
+ }, 'slack command failed schema validation');
112
+ reply.code(200);
113
+ return SLASH_COMMAND_ACK;
114
+ }
115
+ await options.coreDb().transaction(async (tx)=>{
116
+ await handleSlackCommand({
117
+ tx,
118
+ deliveryId: command.data.trigger_id,
119
+ command: command.data,
120
+ publishIntegrationEventReceived: options.publishIntegrationEventReceived,
121
+ recordDeliveryOnly: options.recordDeliveryOnly,
122
+ getIntegrationConnectionById: options.getIntegrationConnectionById
123
+ });
124
+ });
125
+ reply.code(200);
126
+ return SLASH_COMMAND_ACK;
127
+ }
128
+ });
129
+ return [
130
+ {
131
+ prefix: '/webhooks/integrations/slack/events',
132
+ auth: [],
133
+ plugins: [
134
+ slackJsonRawBodyPlugin
135
+ ],
136
+ routes: [
137
+ eventsRoute
138
+ ]
139
+ },
140
+ {
141
+ prefix: '/webhooks/integrations/slack/commands',
142
+ auth: [],
143
+ plugins: [
144
+ slackFormRawBodyPlugin
145
+ ],
146
+ routes: [
147
+ commandsRoute
148
+ ]
149
+ }
150
+ ];
151
+ }
152
+ function rawRequestBody(body) {
153
+ return Buffer.isBuffer(body) ? body.toString('utf8') : undefined;
154
+ }
155
+ function hasValidSlackSignature(headers, rawBody) {
156
+ const signature = headers['x-slack-signature'];
157
+ const timestamp = headers['x-slack-request-timestamp'];
158
+ return verifySlackSignature({
159
+ signingSecret: config.SLACK_SIGNING_SECRET,
160
+ signature: typeof signature === 'string' ? signature : undefined,
161
+ timestamp: typeof timestamp === 'string' ? timestamp : undefined,
162
+ rawBody
163
+ });
164
+ }
165
+
166
+ //# 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 PublishIntegrationEventReceivedFn,\n RecordDeliveryOnlyFn,\n} from '@shipfox/api-integration-core-dto';\nimport {\n slackEventsRequestSchema,\n slackSlashCommandSchema,\n} from '@shipfox/api-integration-slack-dto';\nimport {createRawBodyPlugin, defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport type {NodePgDatabase} from 'drizzle-orm/node-postgres';\nimport {config} from '#config.js';\nimport {verifySlackSignature} from '#core/signature.js';\nimport {handleSlackCommand, handleSlackEvent} from '#core/webhook.js';\n\nexport const SLACK_WEBHOOK_BODY_LIMIT = 1024 * 1024;\nexport const SLASH_COMMAND_ACK = {response_type: 'ephemeral', text: 'Working on it.'} as const;\n\nconst slackJsonRawBodyPlugin = createRawBodyPlugin({\n contentType: 'application/json',\n bodyLimit: SLACK_WEBHOOK_BODY_LIMIT,\n});\nconst slackFormRawBodyPlugin = createRawBodyPlugin({\n contentType: 'application/x-www-form-urlencoded',\n bodyLimit: SLACK_WEBHOOK_BODY_LIMIT,\n});\n\nexport interface CreateSlackWebhookRoutesOptions {\n coreDb: () => NodePgDatabase<Record<string, unknown>>;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n}\n\nexport function createSlackWebhookRoutes(options: CreateSlackWebhookRoutesOptions): RouteGroup[] {\n const eventsRoute = defineRoute({\n method: 'POST',\n path: '/',\n auth: [],\n description: 'Slack Events API receiver.',\n options: {bodyLimit: SLACK_WEBHOOK_BODY_LIMIT},\n handler: async (request, reply) => {\n const rawBody = rawRequestBody(request.body);\n if (rawBody === undefined) {\n reply.code(400);\n return {error: 'expected raw JSON body'};\n }\n if (!hasValidSlackSignature(request.headers, rawBody)) {\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 {errorName: error instanceof Error ? error.name : 'unknown'},\n 'slack events payload JSON parse failed',\n );\n reply.code(400);\n return {error: 'malformed JSON'};\n }\n\n const parsed = slackEventsRequestSchema.safeParse(parsedJson);\n if (!parsed.success) {\n logger().warn(\n {issues: parsed.error.issues},\n 'slack events envelope failed schema validation',\n );\n reply.code(200);\n return null;\n }\n const eventRequest = parsed.data;\n if (eventRequest.type === 'url_verification') {\n reply.code(200);\n return {challenge: eventRequest.challenge};\n }\n\n await options.coreDb().transaction(async (tx) => {\n await handleSlackEvent({\n tx,\n deliveryId: eventRequest.event_id,\n envelope: eventRequest,\n publishIntegrationEventReceived: options.publishIntegrationEventReceived,\n recordDeliveryOnly: options.recordDeliveryOnly,\n getIntegrationConnectionById: options.getIntegrationConnectionById,\n });\n });\n reply.code(200);\n return null;\n },\n });\n\n const commandsRoute = defineRoute({\n method: 'POST',\n path: '/',\n auth: [],\n description: 'Slack slash-command receiver.',\n options: {bodyLimit: SLACK_WEBHOOK_BODY_LIMIT},\n handler: async (request, reply) => {\n const rawBody = rawRequestBody(request.body);\n if (rawBody === undefined) {\n reply.code(400);\n return {error: 'expected raw form body'};\n }\n if (!hasValidSlackSignature(request.headers, rawBody)) {\n reply.code(401);\n return {error: 'invalid signature'};\n }\n\n const command = slackSlashCommandSchema.safeParse(\n Object.fromEntries(new URLSearchParams(rawBody)),\n );\n if (!command.success) {\n logger().warn({issues: command.error.issues}, 'slack command failed schema validation');\n reply.code(200);\n return SLASH_COMMAND_ACK;\n }\n\n await options.coreDb().transaction(async (tx) => {\n await handleSlackCommand({\n tx,\n deliveryId: command.data.trigger_id,\n command: command.data,\n publishIntegrationEventReceived: options.publishIntegrationEventReceived,\n recordDeliveryOnly: options.recordDeliveryOnly,\n getIntegrationConnectionById: options.getIntegrationConnectionById,\n });\n });\n reply.code(200);\n return SLASH_COMMAND_ACK;\n },\n });\n\n return [\n {\n prefix: '/webhooks/integrations/slack/events',\n auth: [],\n plugins: [slackJsonRawBodyPlugin],\n routes: [eventsRoute],\n },\n {\n prefix: '/webhooks/integrations/slack/commands',\n auth: [],\n plugins: [slackFormRawBodyPlugin],\n routes: [commandsRoute],\n },\n ];\n}\n\nfunction rawRequestBody(body: unknown): string | undefined {\n return Buffer.isBuffer(body) ? body.toString('utf8') : undefined;\n}\n\nfunction hasValidSlackSignature(\n headers: Record<string, string | string[] | undefined>,\n rawBody: string,\n): boolean {\n const signature = headers['x-slack-signature'];\n const timestamp = headers['x-slack-request-timestamp'];\n return verifySlackSignature({\n signingSecret: config.SLACK_SIGNING_SECRET,\n signature: typeof signature === 'string' ? signature : undefined,\n timestamp: typeof timestamp === 'string' ? timestamp : undefined,\n rawBody,\n });\n}\n"],"names":["Buffer","slackEventsRequestSchema","slackSlashCommandSchema","createRawBodyPlugin","defineRoute","logger","config","verifySlackSignature","handleSlackCommand","handleSlackEvent","SLACK_WEBHOOK_BODY_LIMIT","SLASH_COMMAND_ACK","response_type","text","slackJsonRawBodyPlugin","contentType","bodyLimit","slackFormRawBodyPlugin","createSlackWebhookRoutes","options","eventsRoute","method","path","auth","description","handler","request","reply","rawBody","rawRequestBody","body","undefined","code","error","hasValidSlackSignature","headers","parsedJson","JSON","parse","warn","errorName","Error","name","parsed","safeParse","success","issues","eventRequest","data","type","challenge","coreDb","transaction","tx","deliveryId","event_id","envelope","publishIntegrationEventReceived","recordDeliveryOnly","getIntegrationConnectionById","commandsRoute","command","Object","fromEntries","URLSearchParams","trigger_id","prefix","plugins","routes","isBuffer","toString","signature","timestamp","signingSecret","SLACK_SIGNING_SECRET"],"mappings":"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"}