@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,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
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@shipfox/ts-config/node",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["**/*.test.tsx", "**/*.test.ts"]
9
+ }