@shipfox/api-integration-slack 4.0.0 → 6.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 +1 -1
- package/CHANGELOG.md +59 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +3 -1
- package/dist/api/client.js.map +1 -1
- package/dist/core/signature.d.ts +2 -1
- package/dist/core/signature.d.ts.map +1 -1
- package/dist/core/signature.js +10 -6
- package/dist/core/signature.js.map +1 -1
- package/dist/core/webhook-processor.d.ts +15 -0
- package/dist/core/webhook-processor.d.ts.map +1 -0
- package/dist/core/webhook-processor.js +127 -0
- package/dist/core/webhook-processor.js.map +1 -0
- package/dist/core/webhook.d.ts +5 -3
- package/dist/core/webhook.d.ts.map +1 -1
- package/dist/core/webhook.js +101 -2
- package/dist/core/webhook.js.map +1 -1
- package/dist/db/db.d.ts +34 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/installations.d.ts +2 -0
- package/dist/db/installations.d.ts.map +1 -1
- package/dist/db/installations.js +4 -2
- package/dist/db/installations.js.map +1 -1
- package/dist/db/schema/installations.d.ts +17 -0
- package/dist/db/schema/installations.d.ts.map +1 -1
- package/dist/db/schema/installations.js +3 -1
- package/dist/db/schema/installations.js.map +1 -1
- package/dist/index.d.ts +28 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -7
- package/dist/index.js.map +1 -1
- package/dist/presentation/routes/errors.d.ts.map +1 -1
- package/dist/presentation/routes/errors.js +22 -0
- package/dist/presentation/routes/errors.js.map +1 -1
- package/dist/presentation/routes/install.d.ts +6 -1
- package/dist/presentation/routes/install.d.ts.map +1 -1
- package/dist/presentation/routes/install.js +6 -4
- package/dist/presentation/routes/install.js.map +1 -1
- package/dist/presentation/routes/webhooks.d.ts +4 -1
- package/dist/presentation/routes/webhooks.d.ts.map +1 -1
- package/dist/presentation/routes/webhooks.js +94 -92
- package/dist/presentation/routes/webhooks.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0001_dark_tyrannus.sql +1 -0
- package/drizzle/meta/0001_snapshot.json +139 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +20 -30
- package/src/api/client.test.ts +24 -0
- package/src/api/client.ts +3 -1
- package/src/core/disconnect.test.ts +96 -0
- package/src/core/signature.test.ts +3 -3
- package/src/core/signature.ts +19 -9
- package/src/core/tokens.test.ts +16 -2
- package/src/core/webhook-processor.test.ts +106 -0
- package/src/core/webhook-processor.ts +152 -0
- package/src/core/webhook.test.ts +131 -2
- package/src/core/webhook.ts +143 -6
- package/src/db/installations.test.ts +15 -0
- package/src/db/installations.ts +11 -3
- package/src/db/schema/installations.ts +3 -1
- package/src/index.test.ts +11 -0
- package/src/index.ts +30 -3
- package/src/presentation/routes/errors.test.ts +25 -0
- package/src/presentation/routes/errors.ts +20 -0
- package/src/presentation/routes/install.test.ts +2 -4
- package/src/presentation/routes/install.ts +18 -3
- package/src/presentation/routes/webhooks.test.ts +46 -1
- package/src/presentation/routes/webhooks.ts +127 -97
- package/test/globalSetup.ts +0 -9
- package/tsconfig.build.tsbuildinfo +1 -1
- package/turbo.json +9 -0
- package/test/api-secrets.d.ts +0 -26
|
@@ -14,6 +14,11 @@ export interface CreateSlackIntegrationRoutesOptions {
|
|
|
14
14
|
connectionId: string;
|
|
15
15
|
}) => Promise<void>;
|
|
16
16
|
connectionCapabilities: IntegrationCapability[];
|
|
17
|
+
requireActiveWorkspaceMembership?: (input: {
|
|
18
|
+
workspaceId: string;
|
|
19
|
+
userId: string;
|
|
20
|
+
memberships: ReadonlyArray<import('@shipfox/api-auth-context').UserContextMembership>;
|
|
21
|
+
}) => Promise<unknown>;
|
|
17
22
|
}
|
|
18
|
-
export declare function createSlackIntegrationRoutes({ slack, tokenStore, getExistingSlackConnection, connectSlackInstallation, disconnectSlackInstallation, connectionCapabilities, }: CreateSlackIntegrationRoutesOptions): RouteGroup;
|
|
23
|
+
export declare function createSlackIntegrationRoutes({ slack, tokenStore, getExistingSlackConnection, connectSlackInstallation, disconnectSlackInstallation, connectionCapabilities, requireActiveWorkspaceMembership, }: CreateSlackIntegrationRoutesOptions): RouteGroup;
|
|
19
24
|
//# sourceMappingURL=install.d.ts.map
|
|
@@ -1 +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;
|
|
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;AAQpG,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;IAChD,gCAAgC,CAAC,EAAE,CAAC,KAAK,EAAE;QACzC,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,aAAa,CAAC,OAAO,2BAA2B,EAAE,qBAAqB,CAAC,CAAC;KACvF,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACxB;AAED,wBAAgB,4BAA4B,CAAC,EAC3C,KAAK,EACL,UAAU,EACV,0BAA0B,EAC1B,wBAAwB,EACxB,2BAA2B,EAC3B,sBAAsB,EACtB,gCAAgC,GACjC,EAAE,mCAAmC,GAAG,UAAU,CAkElD"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { AUTH_USER, requireUserContext, requireWorkspaceAccess } from '@shipfox/api-auth-context';
|
|
2
2
|
import { createSlackInstallBodySchema, createSlackInstallResponseSchema, slackCallbackQuerySchema, slackCallbackResponseSchema } from '@shipfox/api-integration-slack-dto';
|
|
3
|
-
import { requireWorkspaceMembership } from '@shipfox/api-workspaces';
|
|
4
3
|
import { defineRoute } from '@shipfox/node-fastify';
|
|
5
4
|
import { config } from '#config.js';
|
|
6
5
|
import { handleSlackCallback, handleSlackOAuthCallbackError } from '#core/install.js';
|
|
@@ -8,7 +7,7 @@ import { formatSlackBotScopes } from '#core/scopes.js';
|
|
|
8
7
|
import { signSlackInstallState } from '#core/state.js';
|
|
9
8
|
import { toIntegrationConnectionDto } from '#presentation/dto/integrations.js';
|
|
10
9
|
import { slackRouteErrorHandler } from './errors.js';
|
|
11
|
-
export function createSlackIntegrationRoutes({ slack, tokenStore, getExistingSlackConnection, connectSlackInstallation, disconnectSlackInstallation, connectionCapabilities }) {
|
|
10
|
+
export function createSlackIntegrationRoutes({ slack, tokenStore, getExistingSlackConnection, connectSlackInstallation, disconnectSlackInstallation, connectionCapabilities, requireActiveWorkspaceMembership }) {
|
|
12
11
|
const installRoute = defineRoute({
|
|
13
12
|
method: 'POST',
|
|
14
13
|
path: '/install',
|
|
@@ -63,7 +62,7 @@ export function createSlackIntegrationRoutes({ slack, tokenStore, getExistingSla
|
|
|
63
62
|
errorDescription: query.error_description,
|
|
64
63
|
sessionUserId: actor.userId,
|
|
65
64
|
sessionMemberships: actor.memberships,
|
|
66
|
-
requireWorkspaceMembership
|
|
65
|
+
requireWorkspaceMembership: requireActiveWorkspaceMembership ?? unavailableWorkspaceMembershipCheck
|
|
67
66
|
});
|
|
68
67
|
}
|
|
69
68
|
const connection = await handleSlackCallback({
|
|
@@ -73,7 +72,7 @@ export function createSlackIntegrationRoutes({ slack, tokenStore, getExistingSla
|
|
|
73
72
|
state: query.state,
|
|
74
73
|
sessionUserId: actor.userId,
|
|
75
74
|
sessionMemberships: actor.memberships,
|
|
76
|
-
requireWorkspaceMembership,
|
|
75
|
+
requireWorkspaceMembership: requireActiveWorkspaceMembership ?? unavailableWorkspaceMembershipCheck,
|
|
77
76
|
getExistingSlackConnection,
|
|
78
77
|
connectSlackInstallation,
|
|
79
78
|
disconnectSlackInstallation
|
|
@@ -91,6 +90,9 @@ export function createSlackIntegrationRoutes({ slack, tokenStore, getExistingSla
|
|
|
91
90
|
]
|
|
92
91
|
};
|
|
93
92
|
}
|
|
93
|
+
function unavailableWorkspaceMembershipCheck(_input) {
|
|
94
|
+
return Promise.reject(new Error('Workspaces inter-module client is not configured'));
|
|
95
|
+
}
|
|
94
96
|
function isSlackOAuthErrorCallback(query) {
|
|
95
97
|
return 'error' in query && typeof query.error === 'string';
|
|
96
98
|
}
|
|
@@ -1 +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 {
|
|
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 {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 requireActiveWorkspaceMembership?: (input: {\n workspaceId: string;\n userId: string;\n memberships: ReadonlyArray<import('@shipfox/api-auth-context').UserContextMembership>;\n }) => Promise<unknown>;\n}\n\nexport function createSlackIntegrationRoutes({\n slack,\n tokenStore,\n getExistingSlackConnection,\n connectSlackInstallation,\n disconnectSlackInstallation,\n connectionCapabilities,\n requireActiveWorkspaceMembership,\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 requireActiveWorkspaceMembership ?? unavailableWorkspaceMembershipCheck,\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 requireActiveWorkspaceMembership ?? unavailableWorkspaceMembershipCheck,\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 unavailableWorkspaceMembershipCheck(_input: {\n workspaceId: string;\n userId: string;\n memberships: ReadonlyArray<import('@shipfox/api-auth-context').UserContextMembership>;\n}): Promise<never> {\n return Promise.reject(new Error('Workspaces inter-module client is not configured'));\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","defineRoute","config","handleSlackCallback","handleSlackOAuthCallbackError","formatSlackBotScopes","signSlackInstallState","toIntegrationConnectionDto","slackRouteErrorHandler","createSlackIntegrationRoutes","slack","tokenStore","getExistingSlackConnection","connectSlackInstallation","disconnectSlackInstallation","connectionCapabilities","requireActiveWorkspaceMembership","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","requireWorkspaceMembership","unavailableWorkspaceMembershipCheck","connection","code","capabilities","prefix","routes","_input","Promise","reject","Error"],"mappings":"AAAA,SAAQA,SAAS,EAAEC,kBAAkB,EAAEC,sBAAsB,QAAO,4BAA4B;AAEhG,SACEC,4BAA4B,EAC5BC,gCAAgC,EAEhCC,wBAAwB,EACxBC,2BAA2B,QACtB,qCAAqC;AAC5C,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;AAoBnD,OAAO,SAASC,6BAA6B,EAC3CC,KAAK,EACLC,UAAU,EACVC,0BAA0B,EAC1BC,wBAAwB,EACxBC,2BAA2B,EAC3BC,sBAAsB,EACtBC,gCAAgC,EACI;IACpC,MAAMC,eAAehB,YAAY;QAC/BiB,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,QAAQxB,sBAAsB;gBAACsB;gBAAaG,QAAQF,MAAME,MAAM;YAAA;YACtE,MAAMC,aAAa,IAAIC,IAAI;YAC3BD,WAAWE,YAAY,CAACC,GAAG,CAAC,aAAajC,OAAOkC,qBAAqB;YACrEJ,WAAWE,YAAY,CAACC,GAAG,CAAC,SAAS9B;YACrC2B,WAAWE,YAAY,CAACC,GAAG,CAAC,gBAAgBjC,OAAOmC,wBAAwB;YAC3EL,WAAWE,YAAY,CAACC,GAAG,CAAC,SAASL;YACrC,OAAO;gBAACQ,aAAaN,WAAWO,QAAQ;YAAE;QAC5C;IACF;IAEA,MAAMC,mBAAmBvC,YAAY;QACnCiB,QAAQ;QACRC,MAAM;QACNC,MAAM1B;QACN2B,aAAa;QACbC,QAAQ;YACNmB,aAAa1C;YACbyB,UAAU;gBAAC,KAAKxB;YAA2B;QAC7C;QACA0C,cAAclC;QACdiB,SAAS,OAAOC;YACd,MAAMG,QAAQlC,mBAAmB+B;YACjC,MAAMiB,QAAQjB,QAAQiB,KAAK;YAC3B,IAAIC,0BAA0BD,QAAQ;gBACpC,OAAO,MAAMvC,8BAA8B;oBACzC0B,OAAOa,MAAMb,KAAK;oBAClBe,OAAOF,MAAME,KAAK;oBAClBC,kBAAkBH,MAAMI,iBAAiB;oBACzCC,eAAenB,MAAME,MAAM;oBAC3BkB,oBAAoBpB,MAAMqB,WAAW;oBACrCC,4BACEnC,oCAAoCoC;gBACxC;YACF;YACA,MAAMC,aAAa,MAAMlD,oBAAoB;gBAC3CO;gBACAC;gBACA2C,MAAMX,MAAMW,IAAI;gBAChBxB,OAAOa,MAAMb,KAAK;gBAClBkB,eAAenB,MAAME,MAAM;gBAC3BkB,oBAAoBpB,MAAMqB,WAAW;gBACrCC,4BACEnC,oCAAoCoC;gBACtCxC;gBACAC;gBACAC;YACF;YACA,OAAOP,2BAA2B8C,YAAY;gBAACE,cAAcxC;YAAsB;QACrF;IACF;IAEA,OAAO;QAACyC,QAAQ;QAAuBC,QAAQ;YAACxC;YAAcuB;SAAiB;IAAA;AACjF;AAEA,SAASY,oCAAoCM,MAI5C;IACC,OAAOC,QAAQC,MAAM,CAAC,IAAIC,MAAM;AAClC;AAEA,SAASjB,0BAA0BD,KAA4B;IAK7D,OAAO,WAAWA,SAAS,OAAOA,MAAME,KAAK,KAAK;AACpD"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { GetIntegrationConnectionByIdFn, PublishIntegrationEventReceivedFn, RecordDeliveryOnlyFn } from '@shipfox/api-integration-core-dto';
|
|
1
|
+
import type { ClaimWebhookDeliveryFn, GetIntegrationConnectionByIdFn, PublishIntegrationEventReceivedFn, RecordDeliveryOnlyFn } from '@shipfox/api-integration-core-dto';
|
|
2
2
|
import { type RouteGroup } from '@shipfox/node-fastify';
|
|
3
3
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
4
|
+
import { type SlackWebhookProcessor } from '#core/webhook-processor.js';
|
|
4
5
|
export declare const SLACK_WEBHOOK_BODY_LIMIT: number;
|
|
5
6
|
export declare const SLASH_COMMAND_ACK: {
|
|
6
7
|
readonly response_type: "ephemeral";
|
|
@@ -8,9 +9,11 @@ export declare const SLASH_COMMAND_ACK: {
|
|
|
8
9
|
};
|
|
9
10
|
export interface CreateSlackWebhookRoutesOptions {
|
|
10
11
|
coreDb: () => NodePgDatabase<Record<string, unknown>>;
|
|
12
|
+
claimWebhookDelivery: ClaimWebhookDeliveryFn;
|
|
11
13
|
publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;
|
|
12
14
|
recordDeliveryOnly: RecordDeliveryOnlyFn;
|
|
13
15
|
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
16
|
+
processor?: SlackWebhookProcessor | undefined;
|
|
14
17
|
}
|
|
15
18
|
export declare function createSlackWebhookRoutes(options: CreateSlackWebhookRoutesOptions): RouteGroup[];
|
|
16
19
|
//# sourceMappingURL=webhooks.d.ts.map
|
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/webhooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,sBAAsB,EACtB,8BAA8B,EAC9B,iCAAiC,EACjC,oBAAoB,EAGrB,MAAM,mCAAmC,CAAC;AAK3C,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAA8B,KAAK,qBAAqB,EAAC,MAAM,4BAA4B,CAAC;AAEnG,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,oBAAoB,EAAE,sBAAsB,CAAC;IAC7C,+BAA+B,EAAE,iCAAiC,CAAC;IACnE,kBAAkB,EAAE,oBAAoB,CAAC;IACzC,4BAA4B,EAAE,8BAA8B,CAAC;IAC7D,SAAS,CAAC,EAAE,qBAAqB,GAAG,SAAS,CAAC;CAC/C;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,+BAA+B,GAAG,UAAU,EAAE,CAqE/F"}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { createRawBodyPlugin, defineRoute } from '@shipfox/node-fastify';
|
|
4
|
-
import {
|
|
5
|
-
import { config } from '#config.js';
|
|
6
|
-
import { verifySlackSignature } from '#core/signature.js';
|
|
7
|
-
import { handleSlackCommand, handleSlackEvent } from '#core/webhook.js';
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createStoredWebhookRequest, WEBHOOK_MAX_RAW_BODY_BYTES } from '@shipfox/api-integration-core-dto';
|
|
3
|
+
import { ClientError, createRawBodyPlugin, defineRoute } from '@shipfox/node-fastify';
|
|
4
|
+
import { createSlackWebhookProcessor } from '#core/webhook-processor.js';
|
|
8
5
|
export const SLACK_WEBHOOK_BODY_LIMIT = 1024 * 1024;
|
|
9
6
|
export const SLASH_COMMAND_ACK = {
|
|
10
7
|
response_type: 'ephemeral',
|
|
@@ -19,6 +16,7 @@ const slackFormRawBodyPlugin = createRawBodyPlugin({
|
|
|
19
16
|
bodyLimit: SLACK_WEBHOOK_BODY_LIMIT
|
|
20
17
|
});
|
|
21
18
|
export function createSlackWebhookRoutes(options) {
|
|
19
|
+
const processor = options.processor ?? createSlackWebhookProcessor(options);
|
|
22
20
|
const eventsRoute = defineRoute({
|
|
23
21
|
method: 'POST',
|
|
24
22
|
path: '/',
|
|
@@ -28,58 +26,20 @@ export function createSlackWebhookRoutes(options) {
|
|
|
28
26
|
bodyLimit: SLACK_WEBHOOK_BODY_LIMIT
|
|
29
27
|
},
|
|
30
28
|
handler: async (request, reply)=>{
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
29
|
+
const body = rawRequestBody(request.body);
|
|
30
|
+
if (!body) {
|
|
33
31
|
reply.code(400);
|
|
34
32
|
return {
|
|
35
33
|
error: 'expected raw JSON body'
|
|
36
34
|
};
|
|
37
35
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
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;
|
|
36
|
+
const result = await processor.process(createSlackStoredWebhookRequest({
|
|
37
|
+
routeId: 'slack.event',
|
|
38
|
+
body,
|
|
39
|
+
headers: request.headers,
|
|
40
|
+
rawQueryString: rawQueryString(request)
|
|
41
|
+
}, new Date().toISOString()));
|
|
42
|
+
return sendSlackEventResponse(reply, result);
|
|
83
43
|
}
|
|
84
44
|
});
|
|
85
45
|
const commandsRoute = defineRoute({
|
|
@@ -91,39 +51,20 @@ export function createSlackWebhookRoutes(options) {
|
|
|
91
51
|
bodyLimit: SLACK_WEBHOOK_BODY_LIMIT
|
|
92
52
|
},
|
|
93
53
|
handler: async (request, reply)=>{
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
54
|
+
const body = rawRequestBody(request.body);
|
|
55
|
+
if (!body) {
|
|
96
56
|
reply.code(400);
|
|
97
57
|
return {
|
|
98
58
|
error: 'expected raw form body'
|
|
99
59
|
};
|
|
100
60
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
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;
|
|
61
|
+
const result = await processor.process(createSlackStoredWebhookRequest({
|
|
62
|
+
routeId: 'slack.command',
|
|
63
|
+
body,
|
|
64
|
+
headers: request.headers,
|
|
65
|
+
rawQueryString: rawQueryString(request)
|
|
66
|
+
}, new Date().toISOString()));
|
|
67
|
+
return sendSlackCommandResponse(reply, result);
|
|
127
68
|
}
|
|
128
69
|
});
|
|
129
70
|
return [
|
|
@@ -150,17 +91,78 @@ export function createSlackWebhookRoutes(options) {
|
|
|
150
91
|
];
|
|
151
92
|
}
|
|
152
93
|
function rawRequestBody(body) {
|
|
153
|
-
return
|
|
94
|
+
return body instanceof Uint8Array ? body : undefined;
|
|
154
95
|
}
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
96
|
+
function rawQueryString(request) {
|
|
97
|
+
return request.raw.url?.split('?')[1] ?? '';
|
|
98
|
+
}
|
|
99
|
+
function createSlackStoredWebhookRequest(input, receivedAt) {
|
|
100
|
+
if (input.body.byteLength > WEBHOOK_MAX_RAW_BODY_BYTES) {
|
|
101
|
+
throw new ClientError('Webhook request body is too large', 'body-too-large', {
|
|
102
|
+
status: 413
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
return createStoredWebhookRequest({
|
|
107
|
+
requestId: randomUUID(),
|
|
108
|
+
routeId: input.routeId,
|
|
109
|
+
receivedAt,
|
|
110
|
+
rawQueryString: input.rawQueryString,
|
|
111
|
+
headers: slackWebhookHeaders(input.headers),
|
|
112
|
+
body: input.body
|
|
113
|
+
});
|
|
114
|
+
} catch (error) {
|
|
115
|
+
throw new ClientError('Webhook request metadata is invalid', 'invalid-webhook-request', {
|
|
116
|
+
cause: error
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function slackWebhookHeaders(headers) {
|
|
121
|
+
return Object.fromEntries([
|
|
122
|
+
'content-type',
|
|
123
|
+
'x-slack-signature',
|
|
124
|
+
'x-slack-request-timestamp'
|
|
125
|
+
].flatMap((name)=>{
|
|
126
|
+
const value = headers[name];
|
|
127
|
+
return typeof value === 'string' ? [
|
|
128
|
+
[
|
|
129
|
+
name,
|
|
130
|
+
value
|
|
131
|
+
]
|
|
132
|
+
] : [];
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
function sendSlackEventResponse(reply, result) {
|
|
136
|
+
if (result.outcome === 'processed' && 'challenge' in result) {
|
|
137
|
+
reply.code(200);
|
|
138
|
+
return {
|
|
139
|
+
challenge: result.challenge
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (result.outcome === 'discarded' && (result.reason === 'invalid_signature' || result.reason === 'missing_required_input' || result.reason === 'stale_at_receipt')) {
|
|
143
|
+
reply.code(401);
|
|
144
|
+
return {
|
|
145
|
+
error: 'invalid signature'
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (result.outcome === 'discarded' && result.reason === 'malformed_payload') {
|
|
149
|
+
reply.code(400);
|
|
150
|
+
return {
|
|
151
|
+
error: 'malformed JSON'
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
reply.code(200);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
function sendSlackCommandResponse(reply, result) {
|
|
158
|
+
if (result.outcome === 'discarded' && (result.reason === 'invalid_signature' || result.reason === 'missing_required_input' || result.reason === 'stale_at_receipt')) {
|
|
159
|
+
reply.code(401);
|
|
160
|
+
return {
|
|
161
|
+
error: 'invalid signature'
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
reply.code(200);
|
|
165
|
+
return SLASH_COMMAND_ACK;
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
//# sourceMappingURL=webhooks.js.map
|
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/webhooks.ts"],"sourcesContent":["import {randomUUID} from 'node:crypto';\nimport type {\n ClaimWebhookDeliveryFn,\n GetIntegrationConnectionByIdFn,\n PublishIntegrationEventReceivedFn,\n RecordDeliveryOnlyFn,\n StoredWebhookRequest,\n WebhookProcessingResult,\n} from '@shipfox/api-integration-core-dto';\nimport {\n createStoredWebhookRequest,\n WEBHOOK_MAX_RAW_BODY_BYTES,\n} from '@shipfox/api-integration-core-dto';\nimport {\n ClientError,\n createRawBodyPlugin,\n defineRoute,\n type RouteGroup,\n} from '@shipfox/node-fastify';\nimport type {NodePgDatabase} from 'drizzle-orm/node-postgres';\nimport {createSlackWebhookProcessor, type SlackWebhookProcessor} from '#core/webhook-processor.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 claimWebhookDelivery: ClaimWebhookDeliveryFn;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n processor?: SlackWebhookProcessor | undefined;\n}\n\nexport function createSlackWebhookRoutes(options: CreateSlackWebhookRoutesOptions): RouteGroup[] {\n const processor = options.processor ?? createSlackWebhookProcessor(options);\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 body = rawRequestBody(request.body);\n if (!body) {\n reply.code(400);\n return {error: 'expected raw JSON body'};\n }\n const result = await processor.process(\n createSlackStoredWebhookRequest(\n {\n routeId: 'slack.event',\n body,\n headers: request.headers,\n rawQueryString: rawQueryString(request),\n },\n new Date().toISOString(),\n ),\n );\n return sendSlackEventResponse(reply, result);\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 body = rawRequestBody(request.body);\n if (!body) {\n reply.code(400);\n return {error: 'expected raw form body'};\n }\n const result = await processor.process(\n createSlackStoredWebhookRequest(\n {\n routeId: 'slack.command',\n body,\n headers: request.headers,\n rawQueryString: rawQueryString(request),\n },\n new Date().toISOString(),\n ),\n );\n return sendSlackCommandResponse(reply, result);\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): Uint8Array | undefined {\n return body instanceof Uint8Array ? body : undefined;\n}\n\nfunction rawQueryString(request: {raw: {url?: string | undefined}}): string {\n return request.raw.url?.split('?')[1] ?? '';\n}\n\nfunction createSlackStoredWebhookRequest(\n input: {\n routeId: 'slack.event' | 'slack.command';\n body: Uint8Array;\n headers: Record<string, string | string[] | undefined>;\n rawQueryString: string;\n },\n receivedAt: string,\n): StoredWebhookRequest {\n if (input.body.byteLength > WEBHOOK_MAX_RAW_BODY_BYTES) {\n throw new ClientError('Webhook request body is too large', 'body-too-large', {status: 413});\n }\n try {\n return createStoredWebhookRequest({\n requestId: randomUUID(),\n routeId: input.routeId,\n receivedAt,\n rawQueryString: input.rawQueryString,\n headers: slackWebhookHeaders(input.headers),\n body: input.body,\n });\n } catch (error) {\n throw new ClientError('Webhook request metadata is invalid', 'invalid-webhook-request', {\n cause: error,\n });\n }\n}\n\nfunction slackWebhookHeaders(headers: Record<string, string | string[] | undefined>) {\n return Object.fromEntries(\n ['content-type', 'x-slack-signature', 'x-slack-request-timestamp'].flatMap((name) => {\n const value = headers[name];\n return typeof value === 'string' ? [[name, value]] : [];\n }),\n );\n}\n\nfunction sendSlackEventResponse(\n reply: {code(statusCode: number): void},\n result: WebhookProcessingResult,\n) {\n if (result.outcome === 'processed' && 'challenge' in result) {\n reply.code(200);\n return {challenge: result.challenge};\n }\n if (\n result.outcome === 'discarded' &&\n (result.reason === 'invalid_signature' ||\n result.reason === 'missing_required_input' ||\n result.reason === 'stale_at_receipt')\n ) {\n reply.code(401);\n return {error: 'invalid signature'};\n }\n if (result.outcome === 'discarded' && result.reason === 'malformed_payload') {\n reply.code(400);\n return {error: 'malformed JSON'};\n }\n reply.code(200);\n return null;\n}\n\nfunction sendSlackCommandResponse(\n reply: {code(statusCode: number): void},\n result: WebhookProcessingResult,\n) {\n if (\n result.outcome === 'discarded' &&\n (result.reason === 'invalid_signature' ||\n result.reason === 'missing_required_input' ||\n result.reason === 'stale_at_receipt')\n ) {\n reply.code(401);\n return {error: 'invalid signature'};\n }\n reply.code(200);\n return SLASH_COMMAND_ACK;\n}\n"],"names":["randomUUID","createStoredWebhookRequest","WEBHOOK_MAX_RAW_BODY_BYTES","ClientError","createRawBodyPlugin","defineRoute","createSlackWebhookProcessor","SLACK_WEBHOOK_BODY_LIMIT","SLASH_COMMAND_ACK","response_type","text","slackJsonRawBodyPlugin","contentType","bodyLimit","slackFormRawBodyPlugin","createSlackWebhookRoutes","options","processor","eventsRoute","method","path","auth","description","handler","request","reply","body","rawRequestBody","code","error","result","process","createSlackStoredWebhookRequest","routeId","headers","rawQueryString","Date","toISOString","sendSlackEventResponse","commandsRoute","sendSlackCommandResponse","prefix","plugins","routes","Uint8Array","undefined","raw","url","split","input","receivedAt","byteLength","status","requestId","slackWebhookHeaders","cause","Object","fromEntries","flatMap","name","value","outcome","challenge","reason"],"mappings":"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"}
|