@shipfox/api-integration-slack 5.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 +35 -0
- 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/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -6
- 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 +2 -0
- package/dist/presentation/routes/webhooks.d.ts.map +1 -1
- package/dist/presentation/routes/webhooks.js +94 -93
- package/dist/presentation/routes/webhooks.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/core/disconnect.test.ts +47 -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/index.ts +19 -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 +18 -0
- package/src/presentation/routes/webhooks.ts +125 -98
- package/test/globalSetup.ts +0 -9
- package/tsconfig.build.tsbuildinfo +1 -1
- package/turbo.json +1 -1
- package/test/api-secrets.d.ts +0 -26
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
$ shipfox-swc
|
|
2
|
-
Successfully compiled:
|
|
2
|
+
Successfully compiled: 25 files with swc (476.32ms)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @shipfox/api-integration-slack
|
|
2
2
|
|
|
3
|
+
## 6.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 40a6e0f: Adds shared processing for signed Slack event and command webhook deliveries.
|
|
8
|
+
- f262539: Adds a composed webhook processor and optional provider-neutral delivery source for hosted API runtimes.
|
|
9
|
+
- a42b575: Exposes Secrets through its inter-module contract and migrates Agent, integrations, and Workflows consumers.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 3bb4e26: Fixes composed webhook processing and exposes Slack URL-verification responses through the shared contract.
|
|
14
|
+
- f73da5d: Enforces bounded API context imports and routes inter-module consumers through producer contracts.
|
|
15
|
+
- 326f4c0: Exposes Workspaces inter-module operations and moves Auth and OAuth providers onto injected clients.
|
|
16
|
+
- 1820feb: Adds Slack Settings installation and callback recovery while returning stable workspace access errors.
|
|
17
|
+
- Updated dependencies [0bb82a4]
|
|
18
|
+
- Updated dependencies [7ac43a4]
|
|
19
|
+
- Updated dependencies [f262539]
|
|
20
|
+
- Updated dependencies [3bb4e26]
|
|
21
|
+
- Updated dependencies [1b79cda]
|
|
22
|
+
- Updated dependencies [c2db8c3]
|
|
23
|
+
- Updated dependencies [8bdc149]
|
|
24
|
+
- Updated dependencies [b00ed29]
|
|
25
|
+
- Updated dependencies [8aa7cd3]
|
|
26
|
+
- Updated dependencies [326f4c0]
|
|
27
|
+
- Updated dependencies [4a91956]
|
|
28
|
+
- Updated dependencies [81f9544]
|
|
29
|
+
- Updated dependencies [4604a06]
|
|
30
|
+
- @shipfox/api-integration-core-dto@6.0.0
|
|
31
|
+
- @shipfox/node-drizzle@0.3.2
|
|
32
|
+
- @shipfox/api-workspaces-dto@6.0.0
|
|
33
|
+
- @shipfox/api-auth-context@6.0.0
|
|
34
|
+
- @shipfox/node-fastify@0.2.4
|
|
35
|
+
- @shipfox/inter-module@0.2.0
|
|
36
|
+
- @shipfox/api-integration-slack-dto@6.0.0
|
|
37
|
+
|
|
3
38
|
## 5.0.0
|
|
4
39
|
|
|
5
40
|
### Minor Changes
|
package/dist/core/signature.d.ts
CHANGED
|
@@ -2,9 +2,10 @@ export interface VerifySlackSignatureParams {
|
|
|
2
2
|
signingSecret: string;
|
|
3
3
|
signature: string | undefined;
|
|
4
4
|
timestamp: string | undefined;
|
|
5
|
-
rawBody:
|
|
5
|
+
rawBody: Uint8Array;
|
|
6
6
|
now?: number | undefined;
|
|
7
7
|
replayWindowMs?: number | undefined;
|
|
8
8
|
}
|
|
9
|
+
export declare function isSlackTimestampWithinReplayWindow(timestamp: string, now?: number, replayWindowMs?: number): boolean;
|
|
9
10
|
export declare function verifySlackSignature({ signingSecret, signature, timestamp, rawBody, now, replayWindowMs, }: VerifySlackSignatureParams): boolean;
|
|
10
11
|
//# sourceMappingURL=signature.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signature.d.ts","sourceRoot":"","sources":["../../src/core/signature.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,0BAA0B;IACzC,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"signature.d.ts","sourceRoot":"","sources":["../../src/core/signature.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,0BAA0B;IACzC,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,UAAU,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,wBAAgB,kCAAkC,CAChD,SAAS,EAAE,MAAM,EACjB,GAAG,SAAa,EAChB,cAAc,SAAU,GACvB,OAAO,CAQT;AAED,wBAAgB,oBAAoB,CAAC,EACnC,aAAa,EACb,SAAS,EACT,SAAS,EACT,OAAO,EACP,GAAgB,EAChB,cAAwB,GACzB,EAAE,0BAA0B,GAAG,OAAO,CAgBtC"}
|
package/dist/core/signature.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
const unixTimestampPattern = /^\d+$/;
|
|
3
|
-
export function
|
|
4
|
-
if (!
|
|
5
|
-
return false;
|
|
6
|
-
}
|
|
3
|
+
export function isSlackTimestampWithinReplayWindow(timestamp, now = Date.now(), replayWindowMs = 300_000) {
|
|
4
|
+
if (!unixTimestampPattern.test(timestamp)) return false;
|
|
7
5
|
const timestampSeconds = Number(timestamp);
|
|
8
6
|
if (!Number.isSafeInteger(timestampSeconds)) return false;
|
|
9
7
|
const timestampMilliseconds = timestampSeconds * 1000;
|
|
10
8
|
if (!Number.isSafeInteger(timestampMilliseconds)) return false;
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
return Math.abs(now - timestampMilliseconds) <= replayWindowMs;
|
|
10
|
+
}
|
|
11
|
+
export function verifySlackSignature({ signingSecret, signature, timestamp, rawBody, now = Date.now(), replayWindowMs = 300_000 }) {
|
|
12
|
+
if (!signingSecret || !signature || !timestamp) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
if (!isSlackTimestampWithinReplayWindow(timestamp, now, replayWindowMs)) return false;
|
|
16
|
+
const expected = `v0=${createHmac('sha256', signingSecret).update(`v0:${timestamp}:`).update(rawBody).digest('hex')}`;
|
|
13
17
|
const expectedBuffer = Buffer.from(expected, 'utf8');
|
|
14
18
|
const receivedBuffer = Buffer.from(signature, 'utf8');
|
|
15
19
|
return expectedBuffer.length === receivedBuffer.length && timingSafeEqual(expectedBuffer, receivedBuffer);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/signature.ts"],"sourcesContent":["import {createHmac, timingSafeEqual} from 'node:crypto';\n\nconst unixTimestampPattern = /^\\d+$/;\n\nexport interface VerifySlackSignatureParams {\n signingSecret: string;\n signature: string | undefined;\n timestamp: string | undefined;\n rawBody:
|
|
1
|
+
{"version":3,"sources":["../../src/core/signature.ts"],"sourcesContent":["import {createHmac, timingSafeEqual} from 'node:crypto';\n\nconst unixTimestampPattern = /^\\d+$/;\n\nexport interface VerifySlackSignatureParams {\n signingSecret: string;\n signature: string | undefined;\n timestamp: string | undefined;\n rawBody: Uint8Array;\n now?: number | undefined;\n replayWindowMs?: number | undefined;\n}\n\nexport function isSlackTimestampWithinReplayWindow(\n timestamp: string,\n now = Date.now(),\n replayWindowMs = 300_000,\n): boolean {\n if (!unixTimestampPattern.test(timestamp)) return false;\n\n const timestampSeconds = Number(timestamp);\n if (!Number.isSafeInteger(timestampSeconds)) return false;\n const timestampMilliseconds = timestampSeconds * 1000;\n if (!Number.isSafeInteger(timestampMilliseconds)) return false;\n return Math.abs(now - timestampMilliseconds) <= replayWindowMs;\n}\n\nexport function verifySlackSignature({\n signingSecret,\n signature,\n timestamp,\n rawBody,\n now = Date.now(),\n replayWindowMs = 300_000,\n}: VerifySlackSignatureParams): boolean {\n if (!signingSecret || !signature || !timestamp) {\n return false;\n }\n if (!isSlackTimestampWithinReplayWindow(timestamp, now, replayWindowMs)) return false;\n\n const expected = `v0=${createHmac('sha256', signingSecret)\n .update(`v0:${timestamp}:`)\n .update(rawBody)\n .digest('hex')}`;\n const expectedBuffer = Buffer.from(expected, 'utf8');\n const receivedBuffer = Buffer.from(signature, 'utf8');\n return (\n expectedBuffer.length === receivedBuffer.length &&\n timingSafeEqual(expectedBuffer, receivedBuffer)\n );\n}\n"],"names":["createHmac","timingSafeEqual","unixTimestampPattern","isSlackTimestampWithinReplayWindow","timestamp","now","Date","replayWindowMs","test","timestampSeconds","Number","isSafeInteger","timestampMilliseconds","Math","abs","verifySlackSignature","signingSecret","signature","rawBody","expected","update","digest","expectedBuffer","Buffer","from","receivedBuffer","length"],"mappings":"AAAA,SAAQA,UAAU,EAAEC,eAAe,QAAO,cAAc;AAExD,MAAMC,uBAAuB;AAW7B,OAAO,SAASC,mCACdC,SAAiB,EACjBC,MAAMC,KAAKD,GAAG,EAAE,EAChBE,iBAAiB,OAAO;IAExB,IAAI,CAACL,qBAAqBM,IAAI,CAACJ,YAAY,OAAO;IAElD,MAAMK,mBAAmBC,OAAON;IAChC,IAAI,CAACM,OAAOC,aAAa,CAACF,mBAAmB,OAAO;IACpD,MAAMG,wBAAwBH,mBAAmB;IACjD,IAAI,CAACC,OAAOC,aAAa,CAACC,wBAAwB,OAAO;IACzD,OAAOC,KAAKC,GAAG,CAACT,MAAMO,0BAA0BL;AAClD;AAEA,OAAO,SAASQ,qBAAqB,EACnCC,aAAa,EACbC,SAAS,EACTb,SAAS,EACTc,OAAO,EACPb,MAAMC,KAAKD,GAAG,EAAE,EAChBE,iBAAiB,OAAO,EACG;IAC3B,IAAI,CAACS,iBAAiB,CAACC,aAAa,CAACb,WAAW;QAC9C,OAAO;IACT;IACA,IAAI,CAACD,mCAAmCC,WAAWC,KAAKE,iBAAiB,OAAO;IAEhF,MAAMY,WAAW,CAAC,GAAG,EAAEnB,WAAW,UAAUgB,eACzCI,MAAM,CAAC,CAAC,GAAG,EAAEhB,UAAU,CAAC,CAAC,EACzBgB,MAAM,CAACF,SACPG,MAAM,CAAC,QAAQ;IAClB,MAAMC,iBAAiBC,OAAOC,IAAI,CAACL,UAAU;IAC7C,MAAMM,iBAAiBF,OAAOC,IAAI,CAACP,WAAW;IAC9C,OACEK,eAAeI,MAAM,KAAKD,eAAeC,MAAM,IAC/CzB,gBAAgBqB,gBAAgBG;AAEpC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ClaimWebhookDeliveryFn, GetIntegrationConnectionByIdFn, PublishIntegrationEventReceivedFn, RecordDeliveryOnlyFn, StoredWebhookRequest, WebhookProcessingResult } from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
3
|
+
export interface CreateSlackWebhookProcessorOptions {
|
|
4
|
+
coreDb: () => NodePgDatabase<Record<string, unknown>>;
|
|
5
|
+
claimWebhookDelivery: ClaimWebhookDeliveryFn;
|
|
6
|
+
publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;
|
|
7
|
+
recordDeliveryOnly: RecordDeliveryOnlyFn;
|
|
8
|
+
getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
|
|
9
|
+
}
|
|
10
|
+
export type SlackWebhookProcessingResult = WebhookProcessingResult;
|
|
11
|
+
export interface SlackWebhookProcessor {
|
|
12
|
+
process(request: StoredWebhookRequest): Promise<WebhookProcessingResult>;
|
|
13
|
+
}
|
|
14
|
+
export declare function createSlackWebhookProcessor(options: CreateSlackWebhookProcessorOptions): SlackWebhookProcessor;
|
|
15
|
+
//# sourceMappingURL=webhook-processor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-processor.d.ts","sourceRoot":"","sources":["../../src/core/webhook-processor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,sBAAsB,EACtB,8BAA8B,EAC9B,iCAAiC,EACjC,oBAAoB,EACpB,oBAAoB,EACpB,uBAAuB,EACxB,MAAM,mCAAmC,CAAC;AAO3C,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAQ9D,MAAM,WAAW,kCAAkC;IACjD,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;CAC9D;AAED,MAAM,MAAM,4BAA4B,GAAG,uBAAuB,CAAC;AAEnE,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;CAC1E;AAED,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,kCAAkC,GAC1C,qBAAqB,CAEvB"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { decodeWebhookBody } from '@shipfox/api-integration-core-dto';
|
|
3
|
+
import { slackEventsRequestSchema, slackSlashCommandSchema } from '@shipfox/api-integration-slack-dto';
|
|
4
|
+
import { logger } from '@shipfox/node-opentelemetry';
|
|
5
|
+
import { config } from '#config.js';
|
|
6
|
+
import { isSlackTimestampWithinReplayWindow, verifySlackSignature } from '#core/signature.js';
|
|
7
|
+
import { handleSlackCommand, handleSlackEvent } from '#core/webhook.js';
|
|
8
|
+
const SIGNATURE_HEADER = 'x-slack-signature';
|
|
9
|
+
const TIMESTAMP_HEADER = 'x-slack-request-timestamp';
|
|
10
|
+
export function createSlackWebhookProcessor(options) {
|
|
11
|
+
return {
|
|
12
|
+
process: (request)=>processSlackWebhookRequest(options, request)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function processSlackWebhookRequest(options, request) {
|
|
16
|
+
if (request.route_id !== 'slack.event' && request.route_id !== 'slack.command') {
|
|
17
|
+
throw new Error(`Slack processor cannot process ${request.route_id} requests`);
|
|
18
|
+
}
|
|
19
|
+
const signature = request.headers[SIGNATURE_HEADER];
|
|
20
|
+
const timestamp = request.headers[TIMESTAMP_HEADER];
|
|
21
|
+
if (!signature || !timestamp) {
|
|
22
|
+
return Promise.resolve({
|
|
23
|
+
outcome: 'discarded',
|
|
24
|
+
reason: 'missing_required_input'
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const rawBody = decodeWebhookBody(request.body);
|
|
28
|
+
const receiptTime = new Date(request.received_at).getTime();
|
|
29
|
+
if (!isSlackTimestampWithinReplayWindow(timestamp, receiptTime)) {
|
|
30
|
+
return Promise.resolve({
|
|
31
|
+
outcome: 'discarded',
|
|
32
|
+
reason: 'stale_at_receipt'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (!verifySlackSignature({
|
|
36
|
+
signingSecret: config.SLACK_SIGNING_SECRET,
|
|
37
|
+
signature,
|
|
38
|
+
timestamp,
|
|
39
|
+
rawBody,
|
|
40
|
+
now: receiptTime
|
|
41
|
+
})) {
|
|
42
|
+
return Promise.resolve({
|
|
43
|
+
outcome: 'discarded',
|
|
44
|
+
reason: 'invalid_signature'
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return request.route_id === 'slack.event' ? processSlackEvent(options, rawBody) : processSlackCommand(options, rawBody);
|
|
48
|
+
}
|
|
49
|
+
async function processSlackEvent(options, rawBody) {
|
|
50
|
+
let rawPayload;
|
|
51
|
+
try {
|
|
52
|
+
rawPayload = JSON.parse(Buffer.from(rawBody).toString('utf8'));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
logger().warn({
|
|
55
|
+
err: error
|
|
56
|
+
}, 'slack events payload JSON parse failed');
|
|
57
|
+
return {
|
|
58
|
+
outcome: 'discarded',
|
|
59
|
+
reason: 'malformed_payload'
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const event = slackEventsRequestSchema.safeParse(rawPayload);
|
|
63
|
+
if (!event.success) {
|
|
64
|
+
logger().warn({
|
|
65
|
+
issues: event.error.issues
|
|
66
|
+
}, 'slack events envelope failed schema validation');
|
|
67
|
+
return {
|
|
68
|
+
outcome: 'discarded',
|
|
69
|
+
reason: 'unsupported_event'
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const envelope = event.data;
|
|
73
|
+
if (envelope.type === 'url_verification') {
|
|
74
|
+
return {
|
|
75
|
+
outcome: 'processed',
|
|
76
|
+
challenge: envelope.challenge
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const result = await options.coreDb().transaction(async (tx)=>handleSlackEvent({
|
|
80
|
+
tx,
|
|
81
|
+
deliveryId: envelope.event_id,
|
|
82
|
+
envelope,
|
|
83
|
+
claimWebhookDelivery: options.claimWebhookDelivery,
|
|
84
|
+
publishIntegrationEventReceived: options.publishIntegrationEventReceived,
|
|
85
|
+
recordDeliveryOnly: options.recordDeliveryOnly,
|
|
86
|
+
getIntegrationConnectionById: options.getIntegrationConnectionById
|
|
87
|
+
}));
|
|
88
|
+
return toProcessingResult(result.outcome, envelope.event_id);
|
|
89
|
+
}
|
|
90
|
+
async function processSlackCommand(options, rawBody) {
|
|
91
|
+
const command = slackSlashCommandSchema.safeParse(Object.fromEntries(new URLSearchParams(Buffer.from(rawBody).toString('utf8'))));
|
|
92
|
+
if (!command.success) {
|
|
93
|
+
logger().warn({
|
|
94
|
+
issues: command.error.issues
|
|
95
|
+
}, 'slack command failed schema validation');
|
|
96
|
+
return {
|
|
97
|
+
outcome: 'discarded',
|
|
98
|
+
reason: 'unsupported_event'
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const result = await options.coreDb().transaction(async (tx)=>handleSlackCommand({
|
|
102
|
+
tx,
|
|
103
|
+
deliveryId: command.data.trigger_id,
|
|
104
|
+
command: command.data,
|
|
105
|
+
publishIntegrationEventReceived: options.publishIntegrationEventReceived,
|
|
106
|
+
recordDeliveryOnly: options.recordDeliveryOnly,
|
|
107
|
+
getIntegrationConnectionById: options.getIntegrationConnectionById
|
|
108
|
+
}));
|
|
109
|
+
return toProcessingResult(result.outcome, command.data.trigger_id);
|
|
110
|
+
}
|
|
111
|
+
function toProcessingResult(outcome, deliveryId) {
|
|
112
|
+
if (outcome === 'published') return {
|
|
113
|
+
outcome: 'processed',
|
|
114
|
+
deliveryId
|
|
115
|
+
};
|
|
116
|
+
if (outcome === 'duplicate') return {
|
|
117
|
+
outcome: 'duplicate',
|
|
118
|
+
deliveryId
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
outcome: 'discarded',
|
|
122
|
+
reason: outcome === 'unsupported-event' ? 'unsupported_event' : 'connection_unavailable',
|
|
123
|
+
deliveryId
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
//# sourceMappingURL=webhook-processor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/webhook-processor.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport type {\n ClaimWebhookDeliveryFn,\n GetIntegrationConnectionByIdFn,\n PublishIntegrationEventReceivedFn,\n RecordDeliveryOnlyFn,\n StoredWebhookRequest,\n WebhookProcessingResult,\n} from '@shipfox/api-integration-core-dto';\nimport {decodeWebhookBody} from '@shipfox/api-integration-core-dto';\nimport {\n slackEventsRequestSchema,\n slackSlashCommandSchema,\n} from '@shipfox/api-integration-slack-dto';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport type {NodePgDatabase} from 'drizzle-orm/node-postgres';\nimport {config} from '#config.js';\nimport {isSlackTimestampWithinReplayWindow, verifySlackSignature} from '#core/signature.js';\nimport {handleSlackCommand, handleSlackEvent, type SlackWebhookOutcome} from '#core/webhook.js';\n\nconst SIGNATURE_HEADER = 'x-slack-signature';\nconst TIMESTAMP_HEADER = 'x-slack-request-timestamp';\n\nexport interface CreateSlackWebhookProcessorOptions {\n coreDb: () => NodePgDatabase<Record<string, unknown>>;\n claimWebhookDelivery: ClaimWebhookDeliveryFn;\n publishIntegrationEventReceived: PublishIntegrationEventReceivedFn;\n recordDeliveryOnly: RecordDeliveryOnlyFn;\n getIntegrationConnectionById: GetIntegrationConnectionByIdFn;\n}\n\nexport type SlackWebhookProcessingResult = WebhookProcessingResult;\n\nexport interface SlackWebhookProcessor {\n process(request: StoredWebhookRequest): Promise<WebhookProcessingResult>;\n}\n\nexport function createSlackWebhookProcessor(\n options: CreateSlackWebhookProcessorOptions,\n): SlackWebhookProcessor {\n return {process: (request) => processSlackWebhookRequest(options, request)};\n}\n\nfunction processSlackWebhookRequest(\n options: CreateSlackWebhookProcessorOptions,\n request: StoredWebhookRequest,\n): Promise<WebhookProcessingResult> {\n if (request.route_id !== 'slack.event' && request.route_id !== 'slack.command') {\n throw new Error(`Slack processor cannot process ${request.route_id} requests`);\n }\n\n const signature = request.headers[SIGNATURE_HEADER];\n const timestamp = request.headers[TIMESTAMP_HEADER];\n if (!signature || !timestamp) {\n return Promise.resolve({outcome: 'discarded', reason: 'missing_required_input'});\n }\n\n const rawBody = decodeWebhookBody(request.body);\n const receiptTime = new Date(request.received_at).getTime();\n if (!isSlackTimestampWithinReplayWindow(timestamp, receiptTime)) {\n return Promise.resolve({outcome: 'discarded', reason: 'stale_at_receipt'});\n }\n if (\n !verifySlackSignature({\n signingSecret: config.SLACK_SIGNING_SECRET,\n signature,\n timestamp,\n rawBody,\n now: receiptTime,\n })\n ) {\n return Promise.resolve({outcome: 'discarded', reason: 'invalid_signature'});\n }\n\n return request.route_id === 'slack.event'\n ? processSlackEvent(options, rawBody)\n : processSlackCommand(options, rawBody);\n}\n\nasync function processSlackEvent(\n options: CreateSlackWebhookProcessorOptions,\n rawBody: Uint8Array,\n): Promise<WebhookProcessingResult> {\n let rawPayload: unknown;\n try {\n rawPayload = JSON.parse(Buffer.from(rawBody).toString('utf8'));\n } catch (error) {\n logger().warn({err: error}, 'slack events payload JSON parse failed');\n return {outcome: 'discarded', reason: 'malformed_payload'};\n }\n\n const event = slackEventsRequestSchema.safeParse(rawPayload);\n if (!event.success) {\n logger().warn({issues: event.error.issues}, 'slack events envelope failed schema validation');\n return {outcome: 'discarded', reason: 'unsupported_event'};\n }\n const envelope = event.data;\n if (envelope.type === 'url_verification') {\n return {outcome: 'processed', challenge: envelope.challenge};\n }\n\n const result = await options.coreDb().transaction(async (tx) =>\n handleSlackEvent({\n tx,\n deliveryId: envelope.event_id,\n envelope,\n claimWebhookDelivery: options.claimWebhookDelivery,\n publishIntegrationEventReceived: options.publishIntegrationEventReceived,\n recordDeliveryOnly: options.recordDeliveryOnly,\n getIntegrationConnectionById: options.getIntegrationConnectionById,\n }),\n );\n return toProcessingResult(result.outcome, envelope.event_id);\n}\n\nasync function processSlackCommand(\n options: CreateSlackWebhookProcessorOptions,\n rawBody: Uint8Array,\n): Promise<WebhookProcessingResult> {\n const command = slackSlashCommandSchema.safeParse(\n Object.fromEntries(new URLSearchParams(Buffer.from(rawBody).toString('utf8'))),\n );\n if (!command.success) {\n logger().warn({issues: command.error.issues}, 'slack command failed schema validation');\n return {outcome: 'discarded', reason: 'unsupported_event'};\n }\n\n const result = await options.coreDb().transaction(async (tx) =>\n 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 return toProcessingResult(result.outcome, command.data.trigger_id);\n}\n\nfunction toProcessingResult(\n outcome: SlackWebhookOutcome,\n deliveryId: string,\n): WebhookProcessingResult {\n if (outcome === 'published') return {outcome: 'processed', deliveryId};\n if (outcome === 'duplicate') return {outcome: 'duplicate', deliveryId};\n return {\n outcome: 'discarded',\n reason: outcome === 'unsupported-event' ? 'unsupported_event' : 'connection_unavailable',\n deliveryId,\n };\n}\n"],"names":["Buffer","decodeWebhookBody","slackEventsRequestSchema","slackSlashCommandSchema","logger","config","isSlackTimestampWithinReplayWindow","verifySlackSignature","handleSlackCommand","handleSlackEvent","SIGNATURE_HEADER","TIMESTAMP_HEADER","createSlackWebhookProcessor","options","process","request","processSlackWebhookRequest","route_id","Error","signature","headers","timestamp","Promise","resolve","outcome","reason","rawBody","body","receiptTime","Date","received_at","getTime","signingSecret","SLACK_SIGNING_SECRET","now","processSlackEvent","processSlackCommand","rawPayload","JSON","parse","from","toString","error","warn","err","event","safeParse","success","issues","envelope","data","type","challenge","result","coreDb","transaction","tx","deliveryId","event_id","claimWebhookDelivery","publishIntegrationEventReceived","recordDeliveryOnly","getIntegrationConnectionById","toProcessingResult","command","Object","fromEntries","URLSearchParams","trigger_id"],"mappings":"AAAA,SAAQA,MAAM,QAAO,cAAc;AASnC,SAAQC,iBAAiB,QAAO,oCAAoC;AACpE,SACEC,wBAAwB,EACxBC,uBAAuB,QAClB,qCAAqC;AAC5C,SAAQC,MAAM,QAAO,8BAA8B;AAEnD,SAAQC,MAAM,QAAO,aAAa;AAClC,SAAQC,kCAAkC,EAAEC,oBAAoB,QAAO,qBAAqB;AAC5F,SAAQC,kBAAkB,EAAEC,gBAAgB,QAAiC,mBAAmB;AAEhG,MAAMC,mBAAmB;AACzB,MAAMC,mBAAmB;AAgBzB,OAAO,SAASC,4BACdC,OAA2C;IAE3C,OAAO;QAACC,SAAS,CAACC,UAAYC,2BAA2BH,SAASE;IAAQ;AAC5E;AAEA,SAASC,2BACPH,OAA2C,EAC3CE,OAA6B;IAE7B,IAAIA,QAAQE,QAAQ,KAAK,iBAAiBF,QAAQE,QAAQ,KAAK,iBAAiB;QAC9E,MAAM,IAAIC,MAAM,CAAC,+BAA+B,EAAEH,QAAQE,QAAQ,CAAC,SAAS,CAAC;IAC/E;IAEA,MAAME,YAAYJ,QAAQK,OAAO,CAACV,iBAAiB;IACnD,MAAMW,YAAYN,QAAQK,OAAO,CAACT,iBAAiB;IACnD,IAAI,CAACQ,aAAa,CAACE,WAAW;QAC5B,OAAOC,QAAQC,OAAO,CAAC;YAACC,SAAS;YAAaC,QAAQ;QAAwB;IAChF;IAEA,MAAMC,UAAUzB,kBAAkBc,QAAQY,IAAI;IAC9C,MAAMC,cAAc,IAAIC,KAAKd,QAAQe,WAAW,EAAEC,OAAO;IACzD,IAAI,CAACzB,mCAAmCe,WAAWO,cAAc;QAC/D,OAAON,QAAQC,OAAO,CAAC;YAACC,SAAS;YAAaC,QAAQ;QAAkB;IAC1E;IACA,IACE,CAAClB,qBAAqB;QACpByB,eAAe3B,OAAO4B,oBAAoB;QAC1Cd;QACAE;QACAK;QACAQ,KAAKN;IACP,IACA;QACA,OAAON,QAAQC,OAAO,CAAC;YAACC,SAAS;YAAaC,QAAQ;QAAmB;IAC3E;IAEA,OAAOV,QAAQE,QAAQ,KAAK,gBACxBkB,kBAAkBtB,SAASa,WAC3BU,oBAAoBvB,SAASa;AACnC;AAEA,eAAeS,kBACbtB,OAA2C,EAC3Ca,OAAmB;IAEnB,IAAIW;IACJ,IAAI;QACFA,aAAaC,KAAKC,KAAK,CAACvC,OAAOwC,IAAI,CAACd,SAASe,QAAQ,CAAC;IACxD,EAAE,OAAOC,OAAO;QACdtC,SAASuC,IAAI,CAAC;YAACC,KAAKF;QAAK,GAAG;QAC5B,OAAO;YAAClB,SAAS;YAAaC,QAAQ;QAAmB;IAC3D;IAEA,MAAMoB,QAAQ3C,yBAAyB4C,SAAS,CAACT;IACjD,IAAI,CAACQ,MAAME,OAAO,EAAE;QAClB3C,SAASuC,IAAI,CAAC;YAACK,QAAQH,MAAMH,KAAK,CAACM,MAAM;QAAA,GAAG;QAC5C,OAAO;YAACxB,SAAS;YAAaC,QAAQ;QAAmB;IAC3D;IACA,MAAMwB,WAAWJ,MAAMK,IAAI;IAC3B,IAAID,SAASE,IAAI,KAAK,oBAAoB;QACxC,OAAO;YAAC3B,SAAS;YAAa4B,WAAWH,SAASG,SAAS;QAAA;IAC7D;IAEA,MAAMC,SAAS,MAAMxC,QAAQyC,MAAM,GAAGC,WAAW,CAAC,OAAOC,KACvD/C,iBAAiB;YACf+C;YACAC,YAAYR,SAASS,QAAQ;YAC7BT;YACAU,sBAAsB9C,QAAQ8C,oBAAoB;YAClDC,iCAAiC/C,QAAQ+C,+BAA+B;YACxEC,oBAAoBhD,QAAQgD,kBAAkB;YAC9CC,8BAA8BjD,QAAQiD,4BAA4B;QACpE;IAEF,OAAOC,mBAAmBV,OAAO7B,OAAO,EAAEyB,SAASS,QAAQ;AAC7D;AAEA,eAAetB,oBACbvB,OAA2C,EAC3Ca,OAAmB;IAEnB,MAAMsC,UAAU7D,wBAAwB2C,SAAS,CAC/CmB,OAAOC,WAAW,CAAC,IAAIC,gBAAgBnE,OAAOwC,IAAI,CAACd,SAASe,QAAQ,CAAC;IAEvE,IAAI,CAACuB,QAAQjB,OAAO,EAAE;QACpB3C,SAASuC,IAAI,CAAC;YAACK,QAAQgB,QAAQtB,KAAK,CAACM,MAAM;QAAA,GAAG;QAC9C,OAAO;YAACxB,SAAS;YAAaC,QAAQ;QAAmB;IAC3D;IAEA,MAAM4B,SAAS,MAAMxC,QAAQyC,MAAM,GAAGC,WAAW,CAAC,OAAOC,KACvDhD,mBAAmB;YACjBgD;YACAC,YAAYO,QAAQd,IAAI,CAACkB,UAAU;YACnCJ,SAASA,QAAQd,IAAI;YACrBU,iCAAiC/C,QAAQ+C,+BAA+B;YACxEC,oBAAoBhD,QAAQgD,kBAAkB;YAC9CC,8BAA8BjD,QAAQiD,4BAA4B;QACpE;IAEF,OAAOC,mBAAmBV,OAAO7B,OAAO,EAAEwC,QAAQd,IAAI,CAACkB,UAAU;AACnE;AAEA,SAASL,mBACPvC,OAA4B,EAC5BiC,UAAkB;IAElB,IAAIjC,YAAY,aAAa,OAAO;QAACA,SAAS;QAAaiC;IAAU;IACrE,IAAIjC,YAAY,aAAa,OAAO;QAACA,SAAS;QAAaiC;IAAU;IACrE,OAAO;QACLjC,SAAS;QACTC,QAAQD,YAAY,sBAAsB,sBAAsB;QAChEiC;IACF;AACF"}
|
package/dist/index.d.ts
CHANGED
|
@@ -23,13 +23,15 @@ export type { ConnectSlackInstallationInput, HandleSlackCallbackParams } from '#
|
|
|
23
23
|
export { handleSlackCallback, handleSlackOAuthCallbackError } from '#core/install.js';
|
|
24
24
|
export { assertSlackAuthorizationScopes, formatSlackBotScopes, SLACK_BOT_SCOPES, } from '#core/scopes.js';
|
|
25
25
|
export type { VerifySlackSignatureParams } from '#core/signature.js';
|
|
26
|
-
export { verifySlackSignature } from '#core/signature.js';
|
|
26
|
+
export { isSlackTimestampWithinReplayWindow, verifySlackSignature } from '#core/signature.js';
|
|
27
27
|
export type { SlackInstallStateClaims } from '#core/state.js';
|
|
28
28
|
export { signSlackInstallState, verifySlackInstallState } from '#core/state.js';
|
|
29
29
|
export type { CreateSlackTokenStoreParams, GetSlackAccessTokenParams, SlackConnectionResolverResult, SlackSecretsStore, SlackTokenStore, StoreSlackTokensParams, } from '#core/tokens.js';
|
|
30
30
|
export { createSlackTokenStore, slackSecretsNamespace } from '#core/tokens.js';
|
|
31
31
|
export type { HandleSlackCommandParams, HandleSlackEventParams, SlackWebhookOutcome, } from '#core/webhook.js';
|
|
32
32
|
export { handleSlackCommand, handleSlackEvent, isSelfAuthoredSlackEvent } from '#core/webhook.js';
|
|
33
|
+
export type { CreateSlackWebhookProcessorOptions, SlackWebhookProcessingResult, SlackWebhookProcessor, } from '#core/webhook-processor.js';
|
|
34
|
+
export { createSlackWebhookProcessor } from '#core/webhook-processor.js';
|
|
33
35
|
export type { SlackInstallation, SlackInstallationStatus, UpsertSlackInstallationParams, } from '#db/installations.js';
|
|
34
36
|
export { deleteSlackInstallationByConnectionId, getSlackInstallationByConnectionId, getSlackInstallationByTeamId, markSlackInstallationRevoked, upsertSlackInstallation, } from '#db/installations.js';
|
|
35
37
|
export { type CreateSlackE2eRoutesOptions, createSlackE2eRoutes, } from '#presentation/e2eRoutes/index.js';
|
|
@@ -56,6 +58,10 @@ export interface CreateSlackIntegrationProviderOptions {
|
|
|
56
58
|
}
|
|
57
59
|
export declare function createSlackIntegrationProvider(options?: CreateSlackIntegrationProviderOptions): {
|
|
58
60
|
routes: RouteGroup[];
|
|
61
|
+
webhookProcessors: {
|
|
62
|
+
routeIds: readonly ["slack.event", "slack.command"];
|
|
63
|
+
processor: import("#core/webhook-processor.js").SlackWebhookProcessor;
|
|
64
|
+
}[] | undefined;
|
|
59
65
|
deleteConnectionRecords?: (connection: {
|
|
60
66
|
id: string;
|
|
61
67
|
}, options: {
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAuB,KAAK,cAAc,EAAC,MAAM,gBAAgB,CAAC;AACzE,OAAO,EAAC,MAAM,EAAC,MAAM,YAAY,CAAC;AAClC,OAAO,EAAC,uBAAuB,EAAC,MAAM,+BAA+B,CAAC;AACtE,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { SLACK_PROVIDER } from '@shipfox/api-integration-slack-dto';
|
|
|
2
2
|
import { createSlackApiClient } from '#api/client.js';
|
|
3
3
|
import { config } from '#config.js';
|
|
4
4
|
import { SlackAgentToolsProvider } from '#core/agent-tools-provider.js';
|
|
5
|
+
import { createSlackWebhookProcessor } from '#core/webhook-processor.js';
|
|
5
6
|
import { closeDb, db } from '#db/db.js';
|
|
6
7
|
import { getSlackInstallationByConnectionId } from '#db/installations.js';
|
|
7
8
|
import { migrationsPath } from '#db/migrations.js';
|
|
@@ -14,10 +15,11 @@ export { disconnectSlackInstallation } from '#core/disconnect.js';
|
|
|
14
15
|
export { SlackAccessTokenUnavailableError, SlackAuthorizationScopeMismatchError, SlackBotTokenMissingError, SlackConnectionAlreadyLinkedError, SlackConnectionNotFoundError, SlackEnterpriseInstallUnsupportedError, SlackInstallationAlreadyLinkedError, SlackInstallStateActorMismatchError, SlackInstallStateError, SlackIntegrationProviderError, SlackOAuthCallbackError } from '#core/errors.js';
|
|
15
16
|
export { handleSlackCallback, handleSlackOAuthCallbackError } from '#core/install.js';
|
|
16
17
|
export { assertSlackAuthorizationScopes, formatSlackBotScopes, SLACK_BOT_SCOPES } from '#core/scopes.js';
|
|
17
|
-
export { verifySlackSignature } from '#core/signature.js';
|
|
18
|
+
export { isSlackTimestampWithinReplayWindow, verifySlackSignature } from '#core/signature.js';
|
|
18
19
|
export { signSlackInstallState, verifySlackInstallState } from '#core/state.js';
|
|
19
20
|
export { createSlackTokenStore, slackSecretsNamespace } from '#core/tokens.js';
|
|
20
21
|
export { handleSlackCommand, handleSlackEvent, isSelfAuthoredSlackEvent } from '#core/webhook.js';
|
|
22
|
+
export { createSlackWebhookProcessor } from '#core/webhook-processor.js';
|
|
21
23
|
export { deleteSlackInstallationByConnectionId, getSlackInstallationByConnectionId, getSlackInstallationByTeamId, markSlackInstallationRevoked, upsertSlackInstallation } from '#db/installations.js';
|
|
22
24
|
export { createSlackE2eRoutes } from '#presentation/e2eRoutes/index.js';
|
|
23
25
|
export { createSlackWebhookRoutes, SLACK_WEBHOOK_BODY_LIMIT, SLASH_COMMAND_ACK } from '#presentation/routes/webhooks.js';
|
|
@@ -35,8 +37,9 @@ export function createSlackIntegrationProvider(options = {}) {
|
|
|
35
37
|
throw new Error('Slack webhook routes require every core persistence dependency');
|
|
36
38
|
}
|
|
37
39
|
const routes = [];
|
|
40
|
+
const webhookProcessor = options.routes && hasSlackWebhookRoutesOptions(options.routes) ? createSlackWebhookProcessor(options.routes) : undefined;
|
|
38
41
|
if (options.routes && hasSlackInstallationRoutesOptions(options.routes)) {
|
|
39
|
-
const { tokenStore, getExistingSlackConnection, connectSlackInstallation, disconnectSlackInstallation } = options.routes;
|
|
42
|
+
const { tokenStore, getExistingSlackConnection, connectSlackInstallation, disconnectSlackInstallation, requireActiveWorkspaceMembership } = options.routes;
|
|
40
43
|
routes.push(createSlackIntegrationRoutes({
|
|
41
44
|
slack,
|
|
42
45
|
connectionCapabilities: adapters.agent_tools ? [
|
|
@@ -45,11 +48,17 @@ export function createSlackIntegrationProvider(options = {}) {
|
|
|
45
48
|
tokenStore,
|
|
46
49
|
getExistingSlackConnection,
|
|
47
50
|
connectSlackInstallation,
|
|
48
|
-
disconnectSlackInstallation
|
|
51
|
+
disconnectSlackInstallation,
|
|
52
|
+
...requireActiveWorkspaceMembership ? {
|
|
53
|
+
requireActiveWorkspaceMembership
|
|
54
|
+
} : {}
|
|
49
55
|
}));
|
|
50
56
|
}
|
|
51
|
-
if (options.routes && hasSlackWebhookRoutesOptions(options.routes)) {
|
|
52
|
-
routes.push(...createSlackWebhookRoutes(
|
|
57
|
+
if (options.routes && hasSlackWebhookRoutesOptions(options.routes) && webhookProcessor) {
|
|
58
|
+
routes.push(...createSlackWebhookRoutes({
|
|
59
|
+
...options.routes,
|
|
60
|
+
processor: webhookProcessor
|
|
61
|
+
}));
|
|
53
62
|
}
|
|
54
63
|
return {
|
|
55
64
|
provider: SLACK_PROVIDER,
|
|
@@ -61,7 +70,16 @@ export function createSlackIntegrationProvider(options = {}) {
|
|
|
61
70
|
return `https://app.slack.com/client/${encodeURIComponent(installation.teamId)}`;
|
|
62
71
|
},
|
|
63
72
|
...options.cleanup,
|
|
64
|
-
routes
|
|
73
|
+
routes,
|
|
74
|
+
webhookProcessors: webhookProcessor ? [
|
|
75
|
+
{
|
|
76
|
+
routeIds: [
|
|
77
|
+
'slack.event',
|
|
78
|
+
'slack.command'
|
|
79
|
+
],
|
|
80
|
+
processor: webhookProcessor
|
|
81
|
+
}
|
|
82
|
+
] : undefined
|
|
65
83
|
};
|
|
66
84
|
}
|
|
67
85
|
function hasSlackInstallationRoutesOptions(routes) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {SLACK_PROVIDER} from '@shipfox/api-integration-slack-dto';\nimport type {RouteGroup} from '@shipfox/node-fastify';\nimport {createSlackApiClient, type SlackApiClient} from '#api/client.js';\nimport {config} from '#config.js';\nimport {SlackAgentToolsProvider} from '#core/agent-tools-provider.js';\nimport type {SlackTokenStore} from '#core/tokens.js';\nimport {closeDb, db} from '#db/db.js';\nimport {getSlackInstallationByConnectionId} from '#db/installations.js';\nimport {migrationsPath} from '#db/migrations.js';\nimport {\n type CreateSlackIntegrationRoutesOptions,\n createSlackIntegrationRoutes,\n} from '#presentation/routes/install.js';\nimport {\n type CreateSlackWebhookRoutesOptions,\n createSlackWebhookRoutes,\n} from '#presentation/routes/webhooks.js';\n\ntype SlackInstallationRouteOptions = Omit<\n CreateSlackIntegrationRoutesOptions,\n 'slack' | 'connectionCapabilities'\n>;\ntype SlackRouteOptions = Partial<SlackInstallationRouteOptions> &\n Partial<CreateSlackWebhookRoutesOptions>;\n\nexport type {SlackProvider} from '@shipfox/api-integration-slack-dto';\nexport type {SlackApiClient, SlackAuthorization, SlackWebApiResponse} from '#api/client.js';\nexport {createSlackApiClient} from '#api/client.js';\nexport type {\n SlackAgentToolCatalogEntry,\n SlackAgentToolId,\n SlackAgentToolRequiredScope,\n} from '#core/agent-tools.js';\nexport {\n SLACK_TOOL_METHODS,\n slackAgentToolCatalog,\n slackAgentToolSelectionCatalog,\n} from '#core/agent-tools.js';\nexport type {\n SlackAgentToolsProviderOptions,\n SlackToolCallResult,\n} from '#core/agent-tools-provider.js';\nexport {SlackAgentToolsProvider} from '#core/agent-tools-provider.js';\nexport {\n type DisconnectSlackInstallationParams,\n disconnectSlackInstallation,\n} from '#core/disconnect.js';\nexport {\n SlackAccessTokenUnavailableError,\n type SlackAccessTokenUnavailableReason,\n SlackAuthorizationScopeMismatchError,\n SlackBotTokenMissingError,\n SlackConnectionAlreadyLinkedError,\n SlackConnectionNotFoundError,\n SlackEnterpriseInstallUnsupportedError,\n SlackInstallationAlreadyLinkedError,\n SlackInstallStateActorMismatchError,\n SlackInstallStateError,\n SlackIntegrationProviderError,\n SlackOAuthCallbackError,\n} from '#core/errors.js';\nexport type {ConnectSlackInstallationInput, HandleSlackCallbackParams} from '#core/install.js';\nexport {handleSlackCallback, handleSlackOAuthCallbackError} from '#core/install.js';\nexport {\n assertSlackAuthorizationScopes,\n formatSlackBotScopes,\n SLACK_BOT_SCOPES,\n} from '#core/scopes.js';\nexport type {VerifySlackSignatureParams} from '#core/signature.js';\nexport {verifySlackSignature} from '#core/signature.js';\nexport type {SlackInstallStateClaims} from '#core/state.js';\nexport {signSlackInstallState, verifySlackInstallState} from '#core/state.js';\nexport type {\n CreateSlackTokenStoreParams,\n GetSlackAccessTokenParams,\n SlackConnectionResolverResult,\n SlackSecretsStore,\n SlackTokenStore,\n StoreSlackTokensParams,\n} from '#core/tokens.js';\nexport {createSlackTokenStore, slackSecretsNamespace} from '#core/tokens.js';\nexport type {\n HandleSlackCommandParams,\n HandleSlackEventParams,\n SlackWebhookOutcome,\n} from '#core/webhook.js';\nexport {handleSlackCommand, handleSlackEvent, isSelfAuthoredSlackEvent} from '#core/webhook.js';\nexport type {\n SlackInstallation,\n SlackInstallationStatus,\n UpsertSlackInstallationParams,\n} from '#db/installations.js';\nexport {\n deleteSlackInstallationByConnectionId,\n getSlackInstallationByConnectionId,\n getSlackInstallationByTeamId,\n markSlackInstallationRevoked,\n upsertSlackInstallation,\n} from '#db/installations.js';\nexport {\n type CreateSlackE2eRoutesOptions,\n createSlackE2eRoutes,\n} from '#presentation/e2eRoutes/index.js';\nexport {\n type CreateSlackWebhookRoutesOptions,\n createSlackWebhookRoutes,\n SLACK_WEBHOOK_BODY_LIMIT,\n SLASH_COMMAND_ACK,\n} from '#presentation/routes/webhooks.js';\nexport {closeDb, config, db, migrationsPath};\n\nexport interface CreateSlackIntegrationProviderOptions {\n slack?: SlackApiClient | undefined;\n agentTools?: {tokenStore: Pick<SlackTokenStore, 'getAccessToken'>} | undefined;\n getSlackInstallationByConnectionId?: typeof getSlackInstallationByConnectionId | undefined;\n cleanup?:\n | {\n deleteConnectionRecords?: (\n connection: {id: string},\n options: {tx: unknown},\n ) => Promise<void>;\n deleteConnectionSecrets?: (connection: {id: string; workspaceId: string}) => Promise<void>;\n }\n | undefined;\n routes?: SlackRouteOptions | undefined;\n}\n\nexport function createSlackIntegrationProvider(\n options: CreateSlackIntegrationProviderOptions = {},\n) {\n const slack = options.slack ?? createSlackApiClient();\n const getInstallationByConnectionId =\n options.getSlackInstallationByConnectionId ?? getSlackInstallationByConnectionId;\n const adapters = options.agentTools\n ? {\n agent_tools: new SlackAgentToolsProvider({\n slack,\n tokenStore: options.agentTools.tokenStore,\n }),\n }\n : {};\n if (\n options.routes &&\n !hasSlackInstallationRoutesOptions(options.routes) &&\n !hasSlackWebhookRoutesOptions(options.routes)\n ) {\n throw new Error('Slack webhook routes require every core persistence dependency');\n }\n const routes: RouteGroup[] = [];\n if (options.routes && hasSlackInstallationRoutesOptions(options.routes)) {\n const {\n tokenStore,\n getExistingSlackConnection,\n connectSlackInstallation,\n disconnectSlackInstallation,\n } = options.routes;\n routes.push(\n createSlackIntegrationRoutes({\n slack,\n connectionCapabilities: adapters.agent_tools ? ['agent_tools'] : [],\n tokenStore,\n getExistingSlackConnection,\n connectSlackInstallation,\n disconnectSlackInstallation,\n }),\n );\n }\n if (options.routes && hasSlackWebhookRoutesOptions(options.routes)) {\n routes.push(...createSlackWebhookRoutes(options.routes));\n }\n\n return {\n provider: SLACK_PROVIDER,\n displayName: 'Slack',\n adapters,\n async connectionExternalUrl(connection: {id: string}): Promise<string | undefined> {\n const installation = await getInstallationByConnectionId(connection.id);\n if (!installation) return undefined;\n return `https://app.slack.com/client/${encodeURIComponent(installation.teamId)}`;\n },\n ...options.cleanup,\n routes,\n };\n}\n\nfunction hasSlackInstallationRoutesOptions(\n routes: SlackRouteOptions,\n): routes is SlackInstallationRouteOptions {\n return (\n routes.tokenStore !== undefined &&\n routes.getExistingSlackConnection !== undefined &&\n routes.connectSlackInstallation !== undefined &&\n routes.disconnectSlackInstallation !== undefined\n );\n}\n\nfunction hasSlackWebhookRoutesOptions(\n routes: SlackRouteOptions,\n): routes is CreateSlackWebhookRoutesOptions {\n return (\n routes.coreDb !== undefined &&\n routes.claimWebhookDelivery !== undefined &&\n routes.publishIntegrationEventReceived !== undefined &&\n routes.recordDeliveryOnly !== undefined &&\n routes.getIntegrationConnectionById !== undefined\n );\n}\n"],"names":["SLACK_PROVIDER","createSlackApiClient","config","SlackAgentToolsProvider","closeDb","db","getSlackInstallationByConnectionId","migrationsPath","createSlackIntegrationRoutes","createSlackWebhookRoutes","SLACK_TOOL_METHODS","slackAgentToolCatalog","slackAgentToolSelectionCatalog","disconnectSlackInstallation","SlackAccessTokenUnavailableError","SlackAuthorizationScopeMismatchError","SlackBotTokenMissingError","SlackConnectionAlreadyLinkedError","SlackConnectionNotFoundError","SlackEnterpriseInstallUnsupportedError","SlackInstallationAlreadyLinkedError","SlackInstallStateActorMismatchError","SlackInstallStateError","SlackIntegrationProviderError","SlackOAuthCallbackError","handleSlackCallback","handleSlackOAuthCallbackError","assertSlackAuthorizationScopes","formatSlackBotScopes","SLACK_BOT_SCOPES","verifySlackSignature","signSlackInstallState","verifySlackInstallState","createSlackTokenStore","slackSecretsNamespace","handleSlackCommand","handleSlackEvent","isSelfAuthoredSlackEvent","deleteSlackInstallationByConnectionId","getSlackInstallationByTeamId","markSlackInstallationRevoked","upsertSlackInstallation","createSlackE2eRoutes","SLACK_WEBHOOK_BODY_LIMIT","SLASH_COMMAND_ACK","createSlackIntegrationProvider","options","slack","getInstallationByConnectionId","adapters","agentTools","agent_tools","tokenStore","routes","hasSlackInstallationRoutesOptions","hasSlackWebhookRoutesOptions","Error","getExistingSlackConnection","connectSlackInstallation","push","connectionCapabilities","provider","displayName","connectionExternalUrl","connection","installation","id","undefined","encodeURIComponent","teamId","cleanup","coreDb","claimWebhookDelivery","publishIntegrationEventReceived","recordDeliveryOnly","getIntegrationConnectionById"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {SLACK_PROVIDER} from '@shipfox/api-integration-slack-dto';\nimport type {RouteGroup} from '@shipfox/node-fastify';\nimport {createSlackApiClient, type SlackApiClient} from '#api/client.js';\nimport {config} from '#config.js';\nimport {SlackAgentToolsProvider} from '#core/agent-tools-provider.js';\nimport type {SlackTokenStore} from '#core/tokens.js';\nimport {createSlackWebhookProcessor} from '#core/webhook-processor.js';\nimport {closeDb, db} from '#db/db.js';\nimport {getSlackInstallationByConnectionId} from '#db/installations.js';\nimport {migrationsPath} from '#db/migrations.js';\nimport {\n type CreateSlackIntegrationRoutesOptions,\n createSlackIntegrationRoutes,\n} from '#presentation/routes/install.js';\nimport {\n type CreateSlackWebhookRoutesOptions,\n createSlackWebhookRoutes,\n} from '#presentation/routes/webhooks.js';\n\ntype SlackInstallationRouteOptions = Omit<\n CreateSlackIntegrationRoutesOptions,\n 'slack' | 'connectionCapabilities'\n>;\ntype SlackRouteOptions = Partial<SlackInstallationRouteOptions> &\n Partial<CreateSlackWebhookRoutesOptions>;\n\nexport type {SlackProvider} from '@shipfox/api-integration-slack-dto';\nexport type {SlackApiClient, SlackAuthorization, SlackWebApiResponse} from '#api/client.js';\nexport {createSlackApiClient} from '#api/client.js';\nexport type {\n SlackAgentToolCatalogEntry,\n SlackAgentToolId,\n SlackAgentToolRequiredScope,\n} from '#core/agent-tools.js';\nexport {\n SLACK_TOOL_METHODS,\n slackAgentToolCatalog,\n slackAgentToolSelectionCatalog,\n} from '#core/agent-tools.js';\nexport type {\n SlackAgentToolsProviderOptions,\n SlackToolCallResult,\n} from '#core/agent-tools-provider.js';\nexport {SlackAgentToolsProvider} from '#core/agent-tools-provider.js';\nexport {\n type DisconnectSlackInstallationParams,\n disconnectSlackInstallation,\n} from '#core/disconnect.js';\nexport {\n SlackAccessTokenUnavailableError,\n type SlackAccessTokenUnavailableReason,\n SlackAuthorizationScopeMismatchError,\n SlackBotTokenMissingError,\n SlackConnectionAlreadyLinkedError,\n SlackConnectionNotFoundError,\n SlackEnterpriseInstallUnsupportedError,\n SlackInstallationAlreadyLinkedError,\n SlackInstallStateActorMismatchError,\n SlackInstallStateError,\n SlackIntegrationProviderError,\n SlackOAuthCallbackError,\n} from '#core/errors.js';\nexport type {ConnectSlackInstallationInput, HandleSlackCallbackParams} from '#core/install.js';\nexport {handleSlackCallback, handleSlackOAuthCallbackError} from '#core/install.js';\nexport {\n assertSlackAuthorizationScopes,\n formatSlackBotScopes,\n SLACK_BOT_SCOPES,\n} from '#core/scopes.js';\nexport type {VerifySlackSignatureParams} from '#core/signature.js';\nexport {isSlackTimestampWithinReplayWindow, verifySlackSignature} from '#core/signature.js';\nexport type {SlackInstallStateClaims} from '#core/state.js';\nexport {signSlackInstallState, verifySlackInstallState} from '#core/state.js';\nexport type {\n CreateSlackTokenStoreParams,\n GetSlackAccessTokenParams,\n SlackConnectionResolverResult,\n SlackSecretsStore,\n SlackTokenStore,\n StoreSlackTokensParams,\n} from '#core/tokens.js';\nexport {createSlackTokenStore, slackSecretsNamespace} from '#core/tokens.js';\nexport type {\n HandleSlackCommandParams,\n HandleSlackEventParams,\n SlackWebhookOutcome,\n} from '#core/webhook.js';\nexport {handleSlackCommand, handleSlackEvent, isSelfAuthoredSlackEvent} from '#core/webhook.js';\nexport type {\n CreateSlackWebhookProcessorOptions,\n SlackWebhookProcessingResult,\n SlackWebhookProcessor,\n} from '#core/webhook-processor.js';\nexport {createSlackWebhookProcessor} from '#core/webhook-processor.js';\nexport type {\n SlackInstallation,\n SlackInstallationStatus,\n UpsertSlackInstallationParams,\n} from '#db/installations.js';\nexport {\n deleteSlackInstallationByConnectionId,\n getSlackInstallationByConnectionId,\n getSlackInstallationByTeamId,\n markSlackInstallationRevoked,\n upsertSlackInstallation,\n} from '#db/installations.js';\nexport {\n type CreateSlackE2eRoutesOptions,\n createSlackE2eRoutes,\n} from '#presentation/e2eRoutes/index.js';\nexport {\n type CreateSlackWebhookRoutesOptions,\n createSlackWebhookRoutes,\n SLACK_WEBHOOK_BODY_LIMIT,\n SLASH_COMMAND_ACK,\n} from '#presentation/routes/webhooks.js';\nexport {closeDb, config, db, migrationsPath};\n\nexport interface CreateSlackIntegrationProviderOptions {\n slack?: SlackApiClient | undefined;\n agentTools?: {tokenStore: Pick<SlackTokenStore, 'getAccessToken'>} | undefined;\n getSlackInstallationByConnectionId?: typeof getSlackInstallationByConnectionId | undefined;\n cleanup?:\n | {\n deleteConnectionRecords?: (\n connection: {id: string},\n options: {tx: unknown},\n ) => Promise<void>;\n deleteConnectionSecrets?: (connection: {id: string; workspaceId: string}) => Promise<void>;\n }\n | undefined;\n routes?: SlackRouteOptions | undefined;\n}\n\nexport function createSlackIntegrationProvider(\n options: CreateSlackIntegrationProviderOptions = {},\n) {\n const slack = options.slack ?? createSlackApiClient();\n const getInstallationByConnectionId =\n options.getSlackInstallationByConnectionId ?? getSlackInstallationByConnectionId;\n const adapters = options.agentTools\n ? {\n agent_tools: new SlackAgentToolsProvider({\n slack,\n tokenStore: options.agentTools.tokenStore,\n }),\n }\n : {};\n if (\n options.routes &&\n !hasSlackInstallationRoutesOptions(options.routes) &&\n !hasSlackWebhookRoutesOptions(options.routes)\n ) {\n throw new Error('Slack webhook routes require every core persistence dependency');\n }\n const routes: RouteGroup[] = [];\n const webhookProcessor =\n options.routes && hasSlackWebhookRoutesOptions(options.routes)\n ? createSlackWebhookProcessor(options.routes)\n : undefined;\n if (options.routes && hasSlackInstallationRoutesOptions(options.routes)) {\n const {\n tokenStore,\n getExistingSlackConnection,\n connectSlackInstallation,\n disconnectSlackInstallation,\n requireActiveWorkspaceMembership,\n } = options.routes;\n routes.push(\n createSlackIntegrationRoutes({\n slack,\n connectionCapabilities: adapters.agent_tools ? ['agent_tools'] : [],\n tokenStore,\n getExistingSlackConnection,\n connectSlackInstallation,\n disconnectSlackInstallation,\n ...(requireActiveWorkspaceMembership ? {requireActiveWorkspaceMembership} : {}),\n }),\n );\n }\n if (options.routes && hasSlackWebhookRoutesOptions(options.routes) && webhookProcessor) {\n routes.push(...createSlackWebhookRoutes({...options.routes, processor: webhookProcessor}));\n }\n\n return {\n provider: SLACK_PROVIDER,\n displayName: 'Slack',\n adapters,\n async connectionExternalUrl(connection: {id: string}): Promise<string | undefined> {\n const installation = await getInstallationByConnectionId(connection.id);\n if (!installation) return undefined;\n return `https://app.slack.com/client/${encodeURIComponent(installation.teamId)}`;\n },\n ...options.cleanup,\n routes,\n webhookProcessors: webhookProcessor\n ? [{routeIds: ['slack.event', 'slack.command'] as const, processor: webhookProcessor}]\n : undefined,\n };\n}\n\nfunction hasSlackInstallationRoutesOptions(\n routes: SlackRouteOptions,\n): routes is SlackInstallationRouteOptions {\n return (\n routes.tokenStore !== undefined &&\n routes.getExistingSlackConnection !== undefined &&\n routes.connectSlackInstallation !== undefined &&\n routes.disconnectSlackInstallation !== undefined\n );\n}\n\nfunction hasSlackWebhookRoutesOptions(\n routes: SlackRouteOptions,\n): routes is CreateSlackWebhookRoutesOptions {\n return (\n routes.coreDb !== undefined &&\n routes.claimWebhookDelivery !== undefined &&\n routes.publishIntegrationEventReceived !== undefined &&\n routes.recordDeliveryOnly !== undefined &&\n routes.getIntegrationConnectionById !== undefined\n );\n}\n"],"names":["SLACK_PROVIDER","createSlackApiClient","config","SlackAgentToolsProvider","createSlackWebhookProcessor","closeDb","db","getSlackInstallationByConnectionId","migrationsPath","createSlackIntegrationRoutes","createSlackWebhookRoutes","SLACK_TOOL_METHODS","slackAgentToolCatalog","slackAgentToolSelectionCatalog","disconnectSlackInstallation","SlackAccessTokenUnavailableError","SlackAuthorizationScopeMismatchError","SlackBotTokenMissingError","SlackConnectionAlreadyLinkedError","SlackConnectionNotFoundError","SlackEnterpriseInstallUnsupportedError","SlackInstallationAlreadyLinkedError","SlackInstallStateActorMismatchError","SlackInstallStateError","SlackIntegrationProviderError","SlackOAuthCallbackError","handleSlackCallback","handleSlackOAuthCallbackError","assertSlackAuthorizationScopes","formatSlackBotScopes","SLACK_BOT_SCOPES","isSlackTimestampWithinReplayWindow","verifySlackSignature","signSlackInstallState","verifySlackInstallState","createSlackTokenStore","slackSecretsNamespace","handleSlackCommand","handleSlackEvent","isSelfAuthoredSlackEvent","deleteSlackInstallationByConnectionId","getSlackInstallationByTeamId","markSlackInstallationRevoked","upsertSlackInstallation","createSlackE2eRoutes","SLACK_WEBHOOK_BODY_LIMIT","SLASH_COMMAND_ACK","createSlackIntegrationProvider","options","slack","getInstallationByConnectionId","adapters","agentTools","agent_tools","tokenStore","routes","hasSlackInstallationRoutesOptions","hasSlackWebhookRoutesOptions","Error","webhookProcessor","undefined","getExistingSlackConnection","connectSlackInstallation","requireActiveWorkspaceMembership","push","connectionCapabilities","processor","provider","displayName","connectionExternalUrl","connection","installation","id","encodeURIComponent","teamId","cleanup","webhookProcessors","routeIds","coreDb","claimWebhookDelivery","publishIntegrationEventReceived","recordDeliveryOnly","getIntegrationConnectionById"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/errors.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/errors.ts"],"names":[],"mappings":"AAyBA,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CA8D5D"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ConnectionSlugConflictError } from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import { workspacesInterModuleContract } from '@shipfox/api-workspaces-dto/inter-module';
|
|
3
|
+
import { isInterModuleKnownError } from '@shipfox/inter-module';
|
|
2
4
|
import { ClientError } from '@shipfox/node-fastify';
|
|
3
5
|
import { SlackAuthorizationScopeMismatchError, SlackConnectionAlreadyLinkedError, SlackEnterpriseInstallUnsupportedError, SlackInstallationAlreadyLinkedError, SlackInstallStateActorMismatchError, SlackInstallStateError, SlackIntegrationProviderError, SlackOAuthCallbackError, SlackTokenRotationUnsupportedError } from '#core/errors.js';
|
|
4
6
|
function providerStatus(reason) {
|
|
@@ -7,6 +9,26 @@ function providerStatus(reason) {
|
|
|
7
9
|
return 422;
|
|
8
10
|
}
|
|
9
11
|
export function slackRouteErrorHandler(error) {
|
|
12
|
+
if (isInterModuleKnownError(workspacesInterModuleContract.methods.requireActiveMembership, error)) {
|
|
13
|
+
if (error.code === 'workspace-not-found') throw new ClientError('Workspace not found', 'not-found', {
|
|
14
|
+
status: 404,
|
|
15
|
+
details: {
|
|
16
|
+
workspace_id: error.details.workspaceId
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
if (error.code === 'membership-required') throw new ClientError('Workspace membership required', 'forbidden', {
|
|
20
|
+
status: 403,
|
|
21
|
+
details: {
|
|
22
|
+
workspace_id: error.details.workspaceId
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
throw new ClientError('Workspace is inactive', 'workspace-inactive', {
|
|
26
|
+
status: 403,
|
|
27
|
+
details: {
|
|
28
|
+
workspace_id: error.details.workspaceId
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
10
32
|
if (error instanceof SlackInstallStateError) {
|
|
11
33
|
throw new ClientError(error.message, 'invalid-slack-install-state', {
|
|
12
34
|
status: 400
|
|
@@ -1 +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","
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/errors.ts"],"sourcesContent":["import {\n ConnectionSlugConflictError,\n type IntegrationProviderErrorReason,\n} from '@shipfox/api-integration-core-dto';\nimport {workspacesInterModuleContract} from '@shipfox/api-workspaces-dto/inter-module';\nimport {isInterModuleKnownError} from '@shipfox/inter-module';\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 (\n isInterModuleKnownError(workspacesInterModuleContract.methods.requireActiveMembership, error)\n ) {\n if (error.code === 'workspace-not-found')\n throw new ClientError('Workspace not found', 'not-found', {\n status: 404,\n details: {workspace_id: error.details.workspaceId},\n });\n if (error.code === 'membership-required')\n throw new ClientError('Workspace membership required', 'forbidden', {\n status: 403,\n details: {workspace_id: error.details.workspaceId},\n });\n throw new ClientError('Workspace is inactive', 'workspace-inactive', {\n status: 403,\n details: {workspace_id: error.details.workspaceId},\n });\n }\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","workspacesInterModuleContract","isInterModuleKnownError","ClientError","SlackAuthorizationScopeMismatchError","SlackConnectionAlreadyLinkedError","SlackEnterpriseInstallUnsupportedError","SlackInstallationAlreadyLinkedError","SlackInstallStateActorMismatchError","SlackInstallStateError","SlackIntegrationProviderError","SlackOAuthCallbackError","SlackTokenRotationUnsupportedError","providerStatus","reason","slackRouteErrorHandler","error","methods","requireActiveMembership","code","status","details","workspace_id","workspaceId","message","missing_scopes","missingScopes","providerError","providerDescription","error_description","retry_after_seconds","retryAfterSeconds"],"mappings":"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"}
|
|
@@ -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
|
}
|