@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.
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/dist/api/client.d.ts +28 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +163 -0
- package/dist/api/client.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +21 -0
- package/dist/config.js.map +1 -0
- package/dist/core/agent-tools-provider.d.ts +75 -0
- package/dist/core/agent-tools-provider.d.ts.map +1 -0
- package/dist/core/agent-tools-provider.js +111 -0
- package/dist/core/agent-tools-provider.js.map +1 -0
- package/dist/core/agent-tools.d.ts +65 -0
- package/dist/core/agent-tools.d.ts.map +1 -0
- package/dist/core/agent-tools.js +177 -0
- package/dist/core/agent-tools.js.map +1 -0
- package/dist/core/disconnect.d.ts +18 -0
- package/dist/core/disconnect.d.ts.map +1 -0
- package/dist/core/disconnect.js +23 -0
- package/dist/core/disconnect.js.map +1 -0
- package/dist/core/errors.d.ts +44 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +71 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/install.d.ts +48 -0
- package/dist/core/install.d.ts.map +1 -0
- package/dist/core/install.js +88 -0
- package/dist/core/install.js.map +1 -0
- package/dist/core/scopes.d.ts +4 -0
- package/dist/core/scopes.d.ts.map +1 -0
- package/dist/core/scopes.js +24 -0
- package/dist/core/scopes.js.map +1 -0
- package/dist/core/signature.d.ts +10 -0
- package/dist/core/signature.d.ts.map +1 -0
- package/dist/core/signature.js +18 -0
- package/dist/core/signature.js.map +1 -0
- package/dist/core/state.d.ts +12 -0
- package/dist/core/state.d.ts.map +1 -0
- package/dist/core/state.js +54 -0
- package/dist/core/state.js.map +1 -0
- package/dist/core/tokens.d.ts +35 -0
- package/dist/core/tokens.d.ts.map +1 -0
- package/dist/core/tokens.js +51 -0
- package/dist/core/tokens.js.map +1 -0
- package/dist/core/webhook.d.ts +25 -0
- package/dist/core/webhook.d.ts.map +1 -0
- package/dist/core/webhook.js +160 -0
- package/dist/core/webhook.js.map +1 -0
- package/dist/db/db.d.ts +403 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +18 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/installations.d.ts +40 -0
- package/dist/db/installations.d.ts.map +1 -0
- package/dist/db/installations.js +73 -0
- package/dist/db/installations.js.map +1 -0
- package/dist/db/migrations.d.ts +2 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +5 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/schema/common.d.ts +2 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +4 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/installations.d.ts +203 -0
- package/dist/db/schema/installations.d.ts.map +1 -0
- package/dist/db/schema/installations.js +42 -0
- package/dist/db/schema/installations.js.map +1 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/presentation/dto/integrations.d.ts +2 -0
- package/dist/presentation/dto/integrations.d.ts.map +1 -0
- package/dist/presentation/dto/integrations.js +3 -0
- package/dist/presentation/dto/integrations.js.map +1 -0
- package/dist/presentation/e2eRoutes/create-connection.d.ts +16 -0
- package/dist/presentation/e2eRoutes/create-connection.d.ts.map +1 -0
- package/dist/presentation/e2eRoutes/create-connection.js +65 -0
- package/dist/presentation/e2eRoutes/create-connection.js.map +1 -0
- package/dist/presentation/e2eRoutes/index.d.ts +5 -0
- package/dist/presentation/e2eRoutes/index.d.ts.map +1 -0
- package/dist/presentation/e2eRoutes/index.js +11 -0
- package/dist/presentation/e2eRoutes/index.js.map +1 -0
- package/dist/presentation/routes/errors.d.ts +2 -0
- package/dist/presentation/routes/errors.d.ts.map +1 -0
- package/dist/presentation/routes/errors.js +75 -0
- package/dist/presentation/routes/errors.js.map +1 -0
- package/dist/presentation/routes/install.d.ts +19 -0
- package/dist/presentation/routes/install.d.ts.map +1 -0
- package/dist/presentation/routes/install.js +98 -0
- package/dist/presentation/routes/install.js.map +1 -0
- package/dist/presentation/routes/webhooks.d.ts +16 -0
- package/dist/presentation/routes/webhooks.d.ts.map +1 -0
- package/dist/presentation/routes/webhooks.js +166 -0
- package/dist/presentation/routes/webhooks.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_calm_fantastic_four.sql +16 -0
- package/drizzle/meta/0000_snapshot.json +132 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/package.json +71 -0
- package/src/api/client.test.ts +239 -0
- package/src/api/client.ts +243 -0
- package/src/config.test.ts +32 -0
- package/src/config.ts +20 -0
- package/src/connection-external-url.test.ts +25 -0
- package/src/core/agent-tools-provider.test.ts +256 -0
- package/src/core/agent-tools-provider.ts +147 -0
- package/src/core/agent-tools.test.ts +90 -0
- package/src/core/agent-tools.ts +189 -0
- package/src/core/disconnect.ts +26 -0
- package/src/core/errors.ts +92 -0
- package/src/core/install.test.ts +155 -0
- package/src/core/install.ts +137 -0
- package/src/core/scopes.test.ts +22 -0
- package/src/core/scopes.ts +25 -0
- package/src/core/signature.test.ts +45 -0
- package/src/core/signature.ts +41 -0
- package/src/core/state.test.ts +48 -0
- package/src/core/state.ts +82 -0
- package/src/core/tokens.test.ts +163 -0
- package/src/core/tokens.ts +98 -0
- package/src/core/webhook.test.ts +269 -0
- package/src/core/webhook.ts +223 -0
- package/src/db/db.ts +16 -0
- package/src/db/installations.test.ts +93 -0
- package/src/db/installations.ts +141 -0
- package/src/db/migrations.ts +4 -0
- package/src/db/schema/common.ts +3 -0
- package/src/db/schema/installations.ts +44 -0
- package/src/index.test.ts +25 -0
- package/src/index.ts +196 -0
- package/src/presentation/dto/integrations.ts +1 -0
- package/src/presentation/e2eRoutes/create-connection.ts +82 -0
- package/src/presentation/e2eRoutes/index.test.ts +96 -0
- package/src/presentation/e2eRoutes/index.ts +11 -0
- package/src/presentation/routes/errors.ts +68 -0
- package/src/presentation/routes/install.test.ts +229 -0
- package/src/presentation/routes/install.ts +117 -0
- package/src/presentation/routes/webhooks.test.ts +266 -0
- package/src/presentation/routes/webhooks.ts +170 -0
- package/test/api-secrets.d.ts +26 -0
- package/test/env.ts +13 -0
- package/test/factories/index.ts +1 -0
- package/test/globalSetup.ts +23 -0
- package/test/index.ts +1 -0
- package/test/setup.ts +17 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +9 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {Buffer} from 'node:buffer';
|
|
2
|
+
import type {
|
|
3
|
+
GetIntegrationConnectionByIdFn,
|
|
4
|
+
PublishIntegrationEventReceivedFn,
|
|
5
|
+
RecordDeliveryOnlyFn,
|
|
6
|
+
} from '@shipfox/api-integration-core-dto';
|
|
7
|
+
import {
|
|
8
|
+
slackEventsRequestSchema,
|
|
9
|
+
slackSlashCommandSchema,
|
|
10
|
+
} from '@shipfox/api-integration-slack-dto';
|
|
11
|
+
import {createRawBodyPlugin, defineRoute, type RouteGroup} from '@shipfox/node-fastify';
|
|
12
|
+
import {logger} from '@shipfox/node-opentelemetry';
|
|
13
|
+
import type {NodePgDatabase} from 'drizzle-orm/node-postgres';
|
|
14
|
+
import {config} from '#config.js';
|
|
15
|
+
import {verifySlackSignature} from '#core/signature.js';
|
|
16
|
+
import {handleSlackCommand, handleSlackEvent} from '#core/webhook.js';
|
|
17
|
+
|
|
18
|
+
export const SLACK_WEBHOOK_BODY_LIMIT = 1024 * 1024;
|
|
19
|
+
export const SLASH_COMMAND_ACK = {response_type: 'ephemeral', text: 'Working on it.'} as const;
|
|
20
|
+
|
|
21
|
+
const slackJsonRawBodyPlugin = createRawBodyPlugin({
|
|
22
|
+
contentType: 'application/json',
|
|
23
|
+
bodyLimit: SLACK_WEBHOOK_BODY_LIMIT,
|
|
24
|
+
});
|
|
25
|
+
const slackFormRawBodyPlugin = createRawBodyPlugin({
|
|
26
|
+
contentType: 'application/x-www-form-urlencoded',
|
|
27
|
+
bodyLimit: SLACK_WEBHOOK_BODY_LIMIT,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export interface CreateSlackWebhookRoutesOptions {
|
|
31
|
+
coreDb: () => NodePgDatabase<Record<string, unknown>>;
|
|
32
|
+
publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;
|
|
33
|
+
recordDeliveryOnly: RecordDeliveryOnlyFn;
|
|
34
|
+
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createSlackWebhookRoutes(options: CreateSlackWebhookRoutesOptions): RouteGroup[] {
|
|
38
|
+
const eventsRoute = defineRoute({
|
|
39
|
+
method: 'POST',
|
|
40
|
+
path: '/',
|
|
41
|
+
auth: [],
|
|
42
|
+
description: 'Slack Events API receiver.',
|
|
43
|
+
options: {bodyLimit: SLACK_WEBHOOK_BODY_LIMIT},
|
|
44
|
+
handler: async (request, reply) => {
|
|
45
|
+
const rawBody = rawRequestBody(request.body);
|
|
46
|
+
if (rawBody === undefined) {
|
|
47
|
+
reply.code(400);
|
|
48
|
+
return {error: 'expected raw JSON body'};
|
|
49
|
+
}
|
|
50
|
+
if (!hasValidSlackSignature(request.headers, rawBody)) {
|
|
51
|
+
reply.code(401);
|
|
52
|
+
return {error: 'invalid signature'};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let parsedJson: unknown;
|
|
56
|
+
try {
|
|
57
|
+
parsedJson = JSON.parse(rawBody);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logger().warn(
|
|
60
|
+
{errorName: error instanceof Error ? error.name : 'unknown'},
|
|
61
|
+
'slack events payload JSON parse failed',
|
|
62
|
+
);
|
|
63
|
+
reply.code(400);
|
|
64
|
+
return {error: 'malformed JSON'};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const parsed = slackEventsRequestSchema.safeParse(parsedJson);
|
|
68
|
+
if (!parsed.success) {
|
|
69
|
+
logger().warn(
|
|
70
|
+
{issues: parsed.error.issues},
|
|
71
|
+
'slack events envelope failed schema validation',
|
|
72
|
+
);
|
|
73
|
+
reply.code(200);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const eventRequest = parsed.data;
|
|
77
|
+
if (eventRequest.type === 'url_verification') {
|
|
78
|
+
reply.code(200);
|
|
79
|
+
return {challenge: eventRequest.challenge};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await options.coreDb().transaction(async (tx) => {
|
|
83
|
+
await handleSlackEvent({
|
|
84
|
+
tx,
|
|
85
|
+
deliveryId: eventRequest.event_id,
|
|
86
|
+
envelope: eventRequest,
|
|
87
|
+
publishIntegrationEventReceived: options.publishIntegrationEventReceived,
|
|
88
|
+
recordDeliveryOnly: options.recordDeliveryOnly,
|
|
89
|
+
getIntegrationConnectionById: options.getIntegrationConnectionById,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
reply.code(200);
|
|
93
|
+
return null;
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const commandsRoute = defineRoute({
|
|
98
|
+
method: 'POST',
|
|
99
|
+
path: '/',
|
|
100
|
+
auth: [],
|
|
101
|
+
description: 'Slack slash-command receiver.',
|
|
102
|
+
options: {bodyLimit: SLACK_WEBHOOK_BODY_LIMIT},
|
|
103
|
+
handler: async (request, reply) => {
|
|
104
|
+
const rawBody = rawRequestBody(request.body);
|
|
105
|
+
if (rawBody === undefined) {
|
|
106
|
+
reply.code(400);
|
|
107
|
+
return {error: 'expected raw form body'};
|
|
108
|
+
}
|
|
109
|
+
if (!hasValidSlackSignature(request.headers, rawBody)) {
|
|
110
|
+
reply.code(401);
|
|
111
|
+
return {error: 'invalid signature'};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const command = slackSlashCommandSchema.safeParse(
|
|
115
|
+
Object.fromEntries(new URLSearchParams(rawBody)),
|
|
116
|
+
);
|
|
117
|
+
if (!command.success) {
|
|
118
|
+
logger().warn({issues: command.error.issues}, 'slack command failed schema validation');
|
|
119
|
+
reply.code(200);
|
|
120
|
+
return SLASH_COMMAND_ACK;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await options.coreDb().transaction(async (tx) => {
|
|
124
|
+
await handleSlackCommand({
|
|
125
|
+
tx,
|
|
126
|
+
deliveryId: command.data.trigger_id,
|
|
127
|
+
command: command.data,
|
|
128
|
+
publishIntegrationEventReceived: options.publishIntegrationEventReceived,
|
|
129
|
+
recordDeliveryOnly: options.recordDeliveryOnly,
|
|
130
|
+
getIntegrationConnectionById: options.getIntegrationConnectionById,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
reply.code(200);
|
|
134
|
+
return SLASH_COMMAND_ACK;
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return [
|
|
139
|
+
{
|
|
140
|
+
prefix: '/webhooks/integrations/slack/events',
|
|
141
|
+
auth: [],
|
|
142
|
+
plugins: [slackJsonRawBodyPlugin],
|
|
143
|
+
routes: [eventsRoute],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
prefix: '/webhooks/integrations/slack/commands',
|
|
147
|
+
auth: [],
|
|
148
|
+
plugins: [slackFormRawBodyPlugin],
|
|
149
|
+
routes: [commandsRoute],
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function rawRequestBody(body: unknown): string | undefined {
|
|
155
|
+
return Buffer.isBuffer(body) ? body.toString('utf8') : undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function hasValidSlackSignature(
|
|
159
|
+
headers: Record<string, string | string[] | undefined>,
|
|
160
|
+
rawBody: string,
|
|
161
|
+
): boolean {
|
|
162
|
+
const signature = headers['x-slack-signature'];
|
|
163
|
+
const timestamp = headers['x-slack-request-timestamp'];
|
|
164
|
+
return verifySlackSignature({
|
|
165
|
+
signingSecret: config.SLACK_SIGNING_SECRET,
|
|
166
|
+
signature: typeof signature === 'string' ? signature : undefined,
|
|
167
|
+
timestamp: typeof timestamp === 'string' ? timestamp : undefined,
|
|
168
|
+
rawBody,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
declare module '@shipfox/api-secrets' {
|
|
2
|
+
import type {NodePgDatabase} from '@shipfox/node-drizzle';
|
|
3
|
+
|
|
4
|
+
export function getSecret(params: {
|
|
5
|
+
workspaceId: string;
|
|
6
|
+
namespace: string;
|
|
7
|
+
key: string;
|
|
8
|
+
}): Promise<string | null>;
|
|
9
|
+
|
|
10
|
+
export function setSecrets(params: {
|
|
11
|
+
workspaceId: string;
|
|
12
|
+
namespace: string;
|
|
13
|
+
values: Record<string, string>;
|
|
14
|
+
editedBy?: string | null | undefined;
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
|
|
17
|
+
export const secretsModule: {
|
|
18
|
+
database?:
|
|
19
|
+
| {
|
|
20
|
+
db(): NodePgDatabase<Record<string, unknown>>;
|
|
21
|
+
migrationsPath: string;
|
|
22
|
+
}
|
|
23
|
+
| unknown[]
|
|
24
|
+
| undefined;
|
|
25
|
+
};
|
|
26
|
+
}
|
package/test/env.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
process.env.POSTGRES_HOST ??= 'localhost';
|
|
2
|
+
process.env.POSTGRES_PORT ??= '5432';
|
|
3
|
+
process.env.POSTGRES_USERNAME ??= 'shipfox';
|
|
4
|
+
process.env.POSTGRES_PASSWORD ??= 'password';
|
|
5
|
+
process.env.POSTGRES_DATABASE = 'api_test';
|
|
6
|
+
process.env.POSTGRES_MAX_CONNECTIONS ??= '5';
|
|
7
|
+
process.env.TZ = 'UTC';
|
|
8
|
+
process.env.SLACK_OAUTH_CLIENT_ID = 'test-client-id';
|
|
9
|
+
process.env.SLACK_OAUTH_CLIENT_SECRET = 'test-client-secret';
|
|
10
|
+
process.env.SLACK_SIGNING_SECRET = 'test-signing-secret';
|
|
11
|
+
process.env.SLACK_OAUTH_REDIRECT_URL = 'https://shipfox.example.com/integrations/slack/callback';
|
|
12
|
+
process.env.SLACK_API_BASE_URL = 'http://127.0.0.1:0';
|
|
13
|
+
process.env.SECRETS_ENCRYPTION_KEK = 'ZmVkY2JhOTg3NjU0MzIxMGZlZGNiYTk4NzY1NDMyMTA=';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import './env.js';
|
|
2
|
+
import {runMigrations} from '@shipfox/node-drizzle';
|
|
3
|
+
import {closePostgresClient, createPostgresClient} from '@shipfox/node-postgres';
|
|
4
|
+
import {closeDb, db} from '#db/db.js';
|
|
5
|
+
import {migrationsPath} from '#db/migrations.js';
|
|
6
|
+
|
|
7
|
+
export async function setup() {
|
|
8
|
+
createPostgresClient();
|
|
9
|
+
|
|
10
|
+
await runMigrations(db(), migrationsPath, '__drizzle_migrations_integrations_slack');
|
|
11
|
+
const {secretsModule} = await import('@shipfox/api-secrets');
|
|
12
|
+
if (!secretsModule.database || Array.isArray(secretsModule.database)) {
|
|
13
|
+
throw new Error('Secrets module database is not configured');
|
|
14
|
+
}
|
|
15
|
+
await runMigrations(
|
|
16
|
+
secretsModule.database.db(),
|
|
17
|
+
secretsModule.database.migrationsPath,
|
|
18
|
+
'__drizzle_migrations_secrets',
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
closeDb();
|
|
22
|
+
await closePostgresClient();
|
|
23
|
+
}
|
package/test/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './factories/index.js';
|
package/test/setup.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import './env.js';
|
|
2
|
+
import {closePostgresClient, createPostgresClient} from '@shipfox/node-postgres';
|
|
3
|
+
import {afterAll, afterEach, beforeAll, vi} from '@shipfox/vitest/vi';
|
|
4
|
+
import {closeDb} from '#db/db.js';
|
|
5
|
+
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
createPostgresClient();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterAll(async () => {
|
|
15
|
+
closeDb();
|
|
16
|
+
await closePostgresClient();
|
|
17
|
+
});
|